@mmmbuto/nexuscrew 0.8.33 → 0.8.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +20 -1
- package/frontend/dist/assets/{index-CrggFFKZ.css → index-5PMZjYKR.css} +1 -1
- package/frontend/dist/assets/{index-CP5MeWCH.js → index-BeTp29rp.js} +1 -1
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/commands.js +109 -4
- package/lib/cli/pidfile.js +39 -14
- package/lib/nodes/tunnel.js +6 -1
- package/package.json +1 -1
- package/skills/fill-forms/SKILL.md +154 -0
- package/skills/fill-forms/agents/openai.yaml +4 -0
- package/skills/fill-forms/references/overlay-technique.md +99 -0
- package/skills/fill-forms/requirements.txt +4 -0
- package/skills/fill-forms/scripts/dump_docx.py +70 -0
- package/skills/fill-forms/scripts/fill_docx.py +207 -0
- package/skills/fill-forms/scripts/fill_pdf.py +424 -0
- package/skills/fill-forms/scripts/inspect_pdf.py +188 -0
- package/skills/fill-forms/scripts/prepare_signature.py +171 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Prepare an explicitly authorised local signature image for PDF overlay."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import os
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_args() -> argparse.Namespace:
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
description=(
|
|
15
|
+
"Remove paper background, crop and export a signature as transparent PNG. "
|
|
16
|
+
"Use only with the signer's explicit authorisation."
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument("src", type=Path)
|
|
20
|
+
parser.add_argument("out", type=Path)
|
|
21
|
+
parser.add_argument("--dark", type=int, default=90)
|
|
22
|
+
parser.add_argument("--light", type=int, default=185)
|
|
23
|
+
parser.add_argument("--margin", type=int, default=12)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--min-alpha",
|
|
26
|
+
type=int,
|
|
27
|
+
default=60,
|
|
28
|
+
help="discard low-opacity background pixels; 0 disables",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--max-width",
|
|
32
|
+
type=int,
|
|
33
|
+
default=0,
|
|
34
|
+
help="downsize to this pixel width; 0 preserves the cropped width",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--overwrite",
|
|
38
|
+
action="store_true",
|
|
39
|
+
help="replace an existing output file, never the source",
|
|
40
|
+
)
|
|
41
|
+
return parser.parse_args()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_dependencies():
|
|
45
|
+
try:
|
|
46
|
+
import numpy
|
|
47
|
+
from PIL import Image
|
|
48
|
+
except ImportError as exc:
|
|
49
|
+
raise SystemExit(
|
|
50
|
+
"Missing dependency: numpy and Pillow are required. With user "
|
|
51
|
+
"consent, install the packages listed in requirements.txt."
|
|
52
|
+
) from exc
|
|
53
|
+
return numpy, Image
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def validate_args(args: argparse.Namespace) -> tuple[Path, Path]:
|
|
57
|
+
source = args.src.resolve()
|
|
58
|
+
output_unresolved = args.out.expanduser()
|
|
59
|
+
if output_unresolved.is_symlink():
|
|
60
|
+
raise SystemExit("Output must not be a symbolic link.")
|
|
61
|
+
output = output_unresolved.resolve()
|
|
62
|
+
|
|
63
|
+
if not source.is_file():
|
|
64
|
+
raise SystemExit(f"Input image not found or not a regular file: {source}")
|
|
65
|
+
if source == output:
|
|
66
|
+
raise SystemExit("Source and output must be different files.")
|
|
67
|
+
if output.exists() and not args.overwrite:
|
|
68
|
+
raise SystemExit("Output already exists. Use --overwrite for an intentional revision.")
|
|
69
|
+
if output.suffix.lower() != ".png":
|
|
70
|
+
raise SystemExit("Output must use the .png extension.")
|
|
71
|
+
if not 0 <= args.dark < args.light <= 255:
|
|
72
|
+
raise SystemExit("Require 0 <= --dark < --light <= 255.")
|
|
73
|
+
if args.margin < 0 or args.margin > 1000:
|
|
74
|
+
raise SystemExit("--margin must be between 0 and 1000 pixels.")
|
|
75
|
+
if not 0 <= args.min_alpha <= 255:
|
|
76
|
+
raise SystemExit("--min-alpha must be between 0 and 255.")
|
|
77
|
+
if args.max_width < 0 or args.max_width > 20_000:
|
|
78
|
+
raise SystemExit("--max-width must be between 0 and 20000 pixels.")
|
|
79
|
+
return source, output
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def flatten_to_greyscale(Image, source):
|
|
83
|
+
if source.mode in ("RGBA", "LA") or (
|
|
84
|
+
source.mode == "P" and "transparency" in source.info
|
|
85
|
+
):
|
|
86
|
+
rgba = source.convert("RGBA")
|
|
87
|
+
flattened = Image.new("RGB", rgba.size, (255, 255, 255))
|
|
88
|
+
flattened.paste(rgba, mask=rgba.getchannel("A"))
|
|
89
|
+
return flattened.convert("L")
|
|
90
|
+
return source.convert("L")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main() -> None:
|
|
94
|
+
args = parse_args()
|
|
95
|
+
source_path, output_path = validate_args(args)
|
|
96
|
+
np, Image = load_dependencies()
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
with Image.open(source_path) as source:
|
|
100
|
+
source.load()
|
|
101
|
+
grey = np.asarray(flatten_to_greyscale(Image, source), dtype=np.float32)
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
raise SystemExit(f"Could not read input image: {exc}") from exc
|
|
104
|
+
|
|
105
|
+
dark = float(args.dark)
|
|
106
|
+
light = float(args.light)
|
|
107
|
+
alpha = np.clip((light - grey) / (light - dark), 0.0, 1.0)
|
|
108
|
+
|
|
109
|
+
ink = alpha > 0.35
|
|
110
|
+
height, width = ink.shape
|
|
111
|
+
column_ink = ink.sum(axis=0)
|
|
112
|
+
row_ink = ink.sum(axis=1)
|
|
113
|
+
minimum_column = max(3, int(0.004 * height))
|
|
114
|
+
minimum_row = max(3, int(0.004 * width))
|
|
115
|
+
columns = np.where(column_ink >= minimum_column)[0]
|
|
116
|
+
rows = np.where(row_ink >= minimum_row)[0]
|
|
117
|
+
if columns.size == 0 or rows.size == 0:
|
|
118
|
+
raise SystemExit(
|
|
119
|
+
"No consistent ink stroke detected. Adjust --dark/--light and inspect the source."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
margin = args.margin
|
|
123
|
+
x0 = max(0, int(columns[0]) - margin)
|
|
124
|
+
x1 = min(width, int(columns[-1]) + 1 + margin)
|
|
125
|
+
y0 = max(0, int(rows[0]) - margin)
|
|
126
|
+
y1 = min(height, int(rows[-1]) + 1 + margin)
|
|
127
|
+
|
|
128
|
+
cropped_alpha = (alpha[y0:y1, x0:x1] * 255).astype(np.uint8)
|
|
129
|
+
if args.min_alpha:
|
|
130
|
+
cropped_alpha[cropped_alpha < args.min_alpha] = 0
|
|
131
|
+
|
|
132
|
+
crop_height, crop_width = cropped_alpha.shape
|
|
133
|
+
pixels = np.zeros((crop_height, crop_width, 4), dtype=np.uint8)
|
|
134
|
+
pixels[..., 3] = cropped_alpha
|
|
135
|
+
prepared = Image.fromarray(pixels, "RGBA")
|
|
136
|
+
|
|
137
|
+
if args.max_width and crop_width > args.max_width:
|
|
138
|
+
new_height = max(1, round(crop_height * args.max_width / crop_width))
|
|
139
|
+
prepared = prepared.resize(
|
|
140
|
+
(args.max_width, new_height),
|
|
141
|
+
Image.Resampling.LANCZOS,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
handle = tempfile.NamedTemporaryFile(
|
|
146
|
+
prefix=f".{output_path.name}.",
|
|
147
|
+
suffix=".tmp",
|
|
148
|
+
dir=output_path.parent,
|
|
149
|
+
delete=False,
|
|
150
|
+
)
|
|
151
|
+
temporary = Path(handle.name)
|
|
152
|
+
handle.close()
|
|
153
|
+
try:
|
|
154
|
+
prepared.save(temporary, format="PNG")
|
|
155
|
+
os.replace(temporary, output_path)
|
|
156
|
+
os.chmod(output_path, 0o600)
|
|
157
|
+
except Exception:
|
|
158
|
+
temporary.unlink(missing_ok=True)
|
|
159
|
+
raise
|
|
160
|
+
print(
|
|
161
|
+
f"Saved: {output_path} | crop {prepared.width}x{prepared.height}px "
|
|
162
|
+
f"from {width}x{height}px"
|
|
163
|
+
)
|
|
164
|
+
print(
|
|
165
|
+
"Verify the PNG on a pure white background. Keep it private and use it "
|
|
166
|
+
"only for the specifically authorised document."
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
main()
|