@nguyenphp/antigravity-marketing 1.0.18 → 1.0.19
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/README.md +186 -78
- package/package.json +4 -3
- package/templates/.agent/skills/marketing-report-expert/SKILL.md +70 -0
- package/templates/.agent/skills/minimax-docx/LICENSE +21 -0
- package/templates/.agent/skills/minimax-docx/SKILL.md +274 -0
- package/templates/.agent/skills/minimax-docx/assets/styles/academic_styles.xml +250 -0
- package/templates/.agent/skills/minimax-docx/assets/styles/corporate_styles.xml +284 -0
- package/templates/.agent/skills/minimax-docx/assets/styles/default_styles.xml +449 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/aesthetic-rules.xsd +470 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/business-rules.xsd +130 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/common-types.xsd +159 -0
- package/templates/.agent/skills/minimax-docx/assets/xsd/wml-subset.xsd +589 -0
- package/templates/.agent/skills/minimax-docx/references/cjk_typography.md +357 -0
- package/templates/.agent/skills/minimax-docx/references/cjk_university_template_guide.md +184 -0
- package/templates/.agent/skills/minimax-docx/references/comments_guide.md +191 -0
- package/templates/.agent/skills/minimax-docx/references/design_good_bad_examples.md +829 -0
- package/templates/.agent/skills/minimax-docx/references/design_principles.md +819 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_element_order.md +308 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part1.md +4061 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part2.md +2820 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_encyclopedia_part3.md +3381 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_namespaces.md +82 -0
- package/templates/.agent/skills/minimax-docx/references/openxml_units.md +72 -0
- package/templates/.agent/skills/minimax-docx/references/scenario_a_create.md +284 -0
- package/templates/.agent/skills/minimax-docx/references/scenario_b_edit_content.md +295 -0
- package/templates/.agent/skills/minimax-docx/references/scenario_c_apply_template.md +456 -0
- package/templates/.agent/skills/minimax-docx/references/track_changes_guide.md +200 -0
- package/templates/.agent/skills/minimax-docx/references/troubleshooting.md +506 -0
- package/templates/.agent/skills/minimax-docx/references/typography_guide.md +294 -0
- package/templates/.agent/skills/minimax-docx/references/xsd_validation_guide.md +158 -0
- package/templates/.agent/skills/minimax-docx/scripts/doc_to_docx.sh +40 -0
- package/templates/.agent/skills/minimax-docx/scripts/docx_preview.sh +37 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/MiniMaxAIDocx.Cli.csproj +19 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/Program.cs +18 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/AnalyzeCommand.cs +147 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ApplyTemplateCommand.cs +322 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/CreateCommand.cs +324 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/DiffCommand.cs +155 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/EditContentCommand.cs +487 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/FixOrderCommand.cs +108 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/MergeRunsCommand.cs +122 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ValidateCommand.cs +107 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/MiniMaxAIDocx.Core.csproj +15 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/CommentSynchronizer.cs +169 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/ElementOrder.cs +80 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/NamespaceConstants.cs +42 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/RunMerger.cs +81 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/StyleAnalyzer.cs +81 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/TrackChangesHelper.cs +99 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/UnitConverter.cs +23 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples.cs +1832 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch1.cs +910 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch2.cs +999 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch3.cs +1048 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch4.cs +1038 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/CharacterFormattingSamples.cs +1020 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/DocumentCreationSamples.cs +1121 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FieldAndTocSamples.cs +624 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FootnoteAndCommentSamples.cs +675 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/HeaderFooterSamples.cs +838 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ImageSamples.cs +917 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ListAndNumberingSamples.cs +826 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ParagraphFormattingSamples.cs +1199 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/StyleSystemSamples.cs +1487 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TableSamples.cs +1163 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TrackChangesSamples.cs +595 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/CjkHelper.cs +39 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/FontDefaults.cs +24 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/PageSizes.cs +20 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/BusinessRuleValidator.cs +224 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/GateCheckValidator.cs +148 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/ValidationResult.cs +23 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/XsdValidator.cs +69 -0
- package/templates/.agent/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.slnx +4 -0
- package/templates/.agent/skills/minimax-docx/scripts/env_check.sh +196 -0
- package/templates/.agent/skills/minimax-docx/scripts/setup.ps1 +274 -0
- package/templates/.agent/skills/minimax-docx/scripts/setup.sh +504 -0
- package/templates/.agent/skills/minimax-multimodal-toolkit/SKILL.md +359 -0
- package/templates/.agent/skills/minimax-pdf/README.md +222 -0
- package/templates/.agent/skills/minimax-pdf/SKILL.md +201 -0
- package/templates/.agent/skills/minimax-pdf/design/design.md +381 -0
- package/templates/.agent/skills/minimax-pdf/scripts/cover.py +1579 -0
- package/templates/.agent/skills/minimax-pdf/scripts/fill_inspect.py +200 -0
- package/templates/.agent/skills/minimax-pdf/scripts/fill_write.py +242 -0
- package/templates/.agent/skills/minimax-pdf/scripts/make.sh +491 -0
- package/templates/.agent/skills/minimax-pdf/scripts/merge.py +112 -0
- package/templates/.agent/skills/minimax-pdf/scripts/palette.py +559 -0
- package/templates/.agent/skills/minimax-pdf/scripts/reformat_parse.py +374 -0
- package/templates/.agent/skills/minimax-pdf/scripts/render_body.py +1055 -0
- package/templates/.agent/skills/minimax-pdf/scripts/render_cover.cjs +111 -0
- package/templates/.agent/skills/minimax-xlsx/SKILL.md +138 -0
- package/templates/.agent/skills/minimax-xlsx/references/create.md +691 -0
- package/templates/.agent/skills/minimax-xlsx/references/edit.md +684 -0
- package/templates/.agent/skills/minimax-xlsx/references/fix.md +37 -0
- package/templates/.agent/skills/minimax-xlsx/references/format.md +768 -0
- package/templates/.agent/skills/minimax-xlsx/references/ooxml-cheatsheet.md +231 -0
- package/templates/.agent/skills/minimax-xlsx/references/read-analyze.md +97 -0
- package/templates/.agent/skills/minimax-xlsx/references/validate.md +772 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/formula_check.py +422 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/libreoffice_recalc.py +248 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/shared_strings_builder.py +163 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/style_audit.py +575 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_add_column.py +395 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_insert_row.py +274 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_pack.py +87 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_reader.py +362 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_shift_rows.py +396 -0
- package/templates/.agent/skills/minimax-xlsx/scripts/xlsx_unpack.py +130 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/[Content_Types].xml +9 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/_rels/.rels +6 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/_rels/workbook.xml.rels +19 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/sharedStrings.xml +33 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/styles.xml +160 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/workbook.xml +30 -0
- package/templates/.agent/skills/minimax-xlsx/templates/minimal_xlsx/xl/worksheets/sheet1.xml +70 -0
- package/templates/.agent/skills/pptx-generator/SKILL.md +249 -0
- package/templates/.agent/skills/pptx-generator/references/design-system.md +392 -0
- package/templates/.agent/skills/pptx-generator/references/editing.md +162 -0
- package/templates/.agent/skills/pptx-generator/references/pitfalls.md +112 -0
- package/templates/.agent/skills/pptx-generator/references/pptxgenjs.md +420 -0
- package/templates/.agent/skills/pptx-generator/references/slide-types.md +413 -0
- package/templates/.agent/skills/tutorial-video-expert/SKILL.md +88 -0
- package/templates/.agent/skills/ui-ux-pro-max/SKILL.md +170 -585
- package/templates/.agent/skills/vision-analysis/SKILL.md +174 -0
- package/templates/.agent/workflows/analyze.md +3 -0
- package/templates/.agent/workflows/brand-report.md +44 -0
- package/templates/.agent/workflows/report.md +49 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
fill_inspect.py — Inspect form fields in an existing PDF.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python3 fill_inspect.py --input form.pdf
|
|
7
|
+
python3 fill_inspect.py --input form.pdf --out fields.json
|
|
8
|
+
|
|
9
|
+
Outputs a JSON summary of every fillable field: name, type, current value,
|
|
10
|
+
allowed values (for checkboxes / dropdowns), and page number.
|
|
11
|
+
|
|
12
|
+
Exit codes: 0 success, 1 bad args / file not found, 2 dep missing, 3 read error
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
import importlib.util
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def ensure_deps():
|
|
25
|
+
if importlib.util.find_spec("pypdf") is None:
|
|
26
|
+
import subprocess
|
|
27
|
+
subprocess.check_call(
|
|
28
|
+
[sys.executable, "-m", "pip", "install", "--break-system-packages", "-q", "pypdf"]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
ensure_deps()
|
|
33
|
+
from pypdf import PdfReader
|
|
34
|
+
from pypdf.generic import ArrayObject, DictionaryObject, NameObject, TextStringObject
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── Field type resolution ──────────────────────────────────────────────────────
|
|
38
|
+
def _field_type(field) -> str:
|
|
39
|
+
ft = field.get("/FT")
|
|
40
|
+
if ft is None:
|
|
41
|
+
return "unknown"
|
|
42
|
+
ft = str(ft)
|
|
43
|
+
if ft == "/Tx":
|
|
44
|
+
return "text"
|
|
45
|
+
if ft == "/Btn":
|
|
46
|
+
ff = int(field.get("/Ff", 0))
|
|
47
|
+
return "radio" if ff & (1 << 15) else "checkbox"
|
|
48
|
+
if ft == "/Ch":
|
|
49
|
+
ff = int(field.get("/Ff", 0))
|
|
50
|
+
return "dropdown" if ff & (1 << 17) else "listbox"
|
|
51
|
+
if ft == "/Sig":
|
|
52
|
+
return "signature"
|
|
53
|
+
return "unknown"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _field_value(field) -> str | None:
|
|
57
|
+
v = field.get("/V")
|
|
58
|
+
return str(v) if v is not None else None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _field_options(field, ftype: str) -> dict:
|
|
62
|
+
extra = {}
|
|
63
|
+
if ftype in ("checkbox",):
|
|
64
|
+
ap = field.get("/AP")
|
|
65
|
+
if ap and "/N" in ap:
|
|
66
|
+
states = [str(k) for k in ap["/N"]]
|
|
67
|
+
extra["states"] = states
|
|
68
|
+
checked = next((s for s in states if s != "/Off"), None)
|
|
69
|
+
if checked:
|
|
70
|
+
extra["checked_value"] = checked
|
|
71
|
+
if ftype in ("dropdown", "listbox"):
|
|
72
|
+
opt = field.get("/Opt")
|
|
73
|
+
if opt:
|
|
74
|
+
choices = []
|
|
75
|
+
for item in opt:
|
|
76
|
+
if isinstance(item, (list, ArrayObject)) and len(item) >= 2:
|
|
77
|
+
choices.append({"value": str(item[0]), "label": str(item[1])})
|
|
78
|
+
else:
|
|
79
|
+
choices.append({"value": str(item), "label": str(item)})
|
|
80
|
+
extra["choices"] = choices
|
|
81
|
+
if ftype == "radio":
|
|
82
|
+
kids = field.get("/Kids")
|
|
83
|
+
if kids:
|
|
84
|
+
values = []
|
|
85
|
+
for kid in kids:
|
|
86
|
+
ap = kid.get("/AP")
|
|
87
|
+
if ap and "/N" in ap:
|
|
88
|
+
for k in ap["/N"]:
|
|
89
|
+
if str(k) != "/Off":
|
|
90
|
+
values.append(str(k))
|
|
91
|
+
extra["radio_values"] = values
|
|
92
|
+
return extra
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _walk_fields(fields, page_map: dict, parent_name: str = "") -> list:
|
|
96
|
+
"""Recursively collect all leaf fields."""
|
|
97
|
+
result = []
|
|
98
|
+
for field in fields:
|
|
99
|
+
name = str(field.get("/T", ""))
|
|
100
|
+
full = f"{parent_name}.{name}" if parent_name else name
|
|
101
|
+
|
|
102
|
+
kids = field.get("/Kids")
|
|
103
|
+
# Kids that have /T are sub-fields (groups), not widget annotations
|
|
104
|
+
if kids:
|
|
105
|
+
named_kids = [k for k in kids if "/T" in k]
|
|
106
|
+
if named_kids:
|
|
107
|
+
result.extend(_walk_fields(named_kids, page_map, full))
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
ftype = _field_type(field)
|
|
111
|
+
if ftype == "unknown":
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
entry = {
|
|
115
|
+
"name": full,
|
|
116
|
+
"type": ftype,
|
|
117
|
+
"value": _field_value(field),
|
|
118
|
+
}
|
|
119
|
+
entry.update(_field_options(field, ftype))
|
|
120
|
+
|
|
121
|
+
# Page lookup via /P indirect reference
|
|
122
|
+
p_ref = field.get("/P")
|
|
123
|
+
if p_ref and hasattr(p_ref, "idnum"):
|
|
124
|
+
entry["page"] = page_map.get(p_ref.idnum, "?")
|
|
125
|
+
|
|
126
|
+
result.append(entry)
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def inspect(pdf_path: str) -> dict:
|
|
131
|
+
try:
|
|
132
|
+
reader = PdfReader(pdf_path)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
return {"status": "error", "error": str(e)}
|
|
135
|
+
|
|
136
|
+
# Build page-number lookup: {object_id: 1-based page number}
|
|
137
|
+
page_map = {}
|
|
138
|
+
for i, page in enumerate(reader.pages):
|
|
139
|
+
if hasattr(page, "indirect_reference") and page.indirect_reference:
|
|
140
|
+
page_map[page.indirect_reference.idnum] = i + 1
|
|
141
|
+
|
|
142
|
+
acroform = reader.trailer.get("/Root", {}).get("/AcroForm")
|
|
143
|
+
if acroform is None or "/Fields" not in acroform:
|
|
144
|
+
return {
|
|
145
|
+
"status": "ok",
|
|
146
|
+
"has_fields": False,
|
|
147
|
+
"field_count": 0,
|
|
148
|
+
"fields": [],
|
|
149
|
+
"note": "This PDF has no fillable form fields.",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fields = _walk_fields(list(acroform["/Fields"]), page_map)
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
"status": "ok",
|
|
156
|
+
"has_fields": bool(fields),
|
|
157
|
+
"field_count": len(fields),
|
|
158
|
+
"fields": fields,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def main():
|
|
163
|
+
parser = argparse.ArgumentParser(description="Inspect PDF form fields")
|
|
164
|
+
parser.add_argument("--input", required=True, help="PDF file to inspect")
|
|
165
|
+
parser.add_argument("--out", default="", help="Write JSON to file (optional)")
|
|
166
|
+
args = parser.parse_args()
|
|
167
|
+
|
|
168
|
+
if not os.path.exists(args.input):
|
|
169
|
+
print(json.dumps({"status": "error", "error": f"File not found: {args.input}"}),
|
|
170
|
+
file=sys.stderr)
|
|
171
|
+
sys.exit(1)
|
|
172
|
+
|
|
173
|
+
result = inspect(args.input)
|
|
174
|
+
|
|
175
|
+
output = json.dumps(result, indent=2, ensure_ascii=False)
|
|
176
|
+
|
|
177
|
+
if args.out:
|
|
178
|
+
with open(args.out, "w") as f:
|
|
179
|
+
f.write(output)
|
|
180
|
+
|
|
181
|
+
print(output)
|
|
182
|
+
|
|
183
|
+
# Human-readable summary
|
|
184
|
+
if result["status"] == "ok" and result["has_fields"]:
|
|
185
|
+
print(f"\n── Fields in {args.input} ──────────────────────────────",
|
|
186
|
+
file=sys.stderr)
|
|
187
|
+
for f in result["fields"]:
|
|
188
|
+
pg = f" p.{f['page']}" if "page" in f else ""
|
|
189
|
+
val = f" = {f['value']}" if f.get("value") else ""
|
|
190
|
+
extra = ""
|
|
191
|
+
if "choices" in f:
|
|
192
|
+
extra = f" [{', '.join(c['value'] for c in f['choices'][:4])}{'…' if len(f['choices'])>4 else ''}]"
|
|
193
|
+
elif "states" in f:
|
|
194
|
+
extra = f" {f['states']}"
|
|
195
|
+
print(f" {f['type']:12} {f['name']}{pg}{val}{extra}", file=sys.stderr)
|
|
196
|
+
print("", file=sys.stderr)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__ == "__main__":
|
|
200
|
+
main()
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
fill_write.py — Write values into PDF form fields.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
# From a JSON data file
|
|
7
|
+
python3 fill_write.py --input form.pdf --data values.json --out filled.pdf
|
|
8
|
+
|
|
9
|
+
# Inline JSON
|
|
10
|
+
python3 fill_write.py --input form.pdf --out filled.pdf \
|
|
11
|
+
--values '{"FirstName": "Jane", "Agree": "true"}'
|
|
12
|
+
|
|
13
|
+
values format:
|
|
14
|
+
{
|
|
15
|
+
"FieldName": "text value", # text field
|
|
16
|
+
"CheckBox1": "true", # checkbox (true / false)
|
|
17
|
+
"Dropdown1": "OptionValue", # dropdown (must match an existing choice value)
|
|
18
|
+
"Radio1": "/Choice2" # radio (must match a radio value)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Exit codes: 0 success, 1 bad args, 2 dep missing, 3 read/write error, 4 validation error
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
import importlib.util
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def ensure_deps():
|
|
34
|
+
if importlib.util.find_spec("pypdf") is None:
|
|
35
|
+
import subprocess
|
|
36
|
+
subprocess.check_call(
|
|
37
|
+
[sys.executable, "-m", "pip", "install", "--break-system-packages", "-q", "pypdf"]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
ensure_deps()
|
|
42
|
+
from pypdf import PdfReader, PdfWriter
|
|
43
|
+
from pypdf.generic import NameObject, TextStringObject, BooleanObject
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ── Field helpers ─────────────────────────────────────────────────────────────
|
|
47
|
+
def _field_type(field) -> str:
|
|
48
|
+
ft = str(field.get("/FT", ""))
|
|
49
|
+
if ft == "/Tx": return "text"
|
|
50
|
+
if ft == "/Btn":
|
|
51
|
+
ff = int(field.get("/Ff", 0))
|
|
52
|
+
return "radio" if ff & (1 << 15) else "checkbox"
|
|
53
|
+
if ft == "/Ch":
|
|
54
|
+
ff = int(field.get("/Ff", 0))
|
|
55
|
+
return "dropdown" if ff & (1 << 17) else "listbox"
|
|
56
|
+
return "unknown"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_checkbox_on_value(field) -> str:
|
|
60
|
+
"""Return the /AP /N key that means 'checked' (anything except /Off)."""
|
|
61
|
+
ap = field.get("/AP")
|
|
62
|
+
if ap and "/N" in ap:
|
|
63
|
+
for k in ap["/N"]:
|
|
64
|
+
if str(k) != "/Off":
|
|
65
|
+
return str(k)
|
|
66
|
+
return "/Yes"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _get_dropdown_values(field) -> list[str]:
|
|
70
|
+
opt = field.get("/Opt")
|
|
71
|
+
if not opt:
|
|
72
|
+
return []
|
|
73
|
+
values = []
|
|
74
|
+
for item in opt:
|
|
75
|
+
try:
|
|
76
|
+
from pypdf.generic import ArrayObject
|
|
77
|
+
if isinstance(item, (list, ArrayObject)) and len(item) >= 1:
|
|
78
|
+
values.append(str(item[0]))
|
|
79
|
+
else:
|
|
80
|
+
values.append(str(item))
|
|
81
|
+
except Exception:
|
|
82
|
+
values.append(str(item))
|
|
83
|
+
return values
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ── Walk + fill ───────────────────────────────────────────────────────────────
|
|
87
|
+
def _walk_and_fill(fields, data: dict, filled: list, errors: list, parent: str = ""):
|
|
88
|
+
for field in fields:
|
|
89
|
+
name = str(field.get("/T", ""))
|
|
90
|
+
full = f"{parent}.{name}" if parent else name
|
|
91
|
+
|
|
92
|
+
# Recurse into named groups
|
|
93
|
+
kids = field.get("/Kids")
|
|
94
|
+
if kids:
|
|
95
|
+
named = [k for k in kids if "/T" in k]
|
|
96
|
+
if named:
|
|
97
|
+
_walk_and_fill(named, data, filled, errors, full)
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
if full not in data:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
value = data[full]
|
|
104
|
+
ftype = _field_type(field)
|
|
105
|
+
|
|
106
|
+
if ftype == "text":
|
|
107
|
+
field.update({
|
|
108
|
+
NameObject("/V"): TextStringObject(str(value)),
|
|
109
|
+
NameObject("/DV"): TextStringObject(str(value)),
|
|
110
|
+
})
|
|
111
|
+
filled.append(full)
|
|
112
|
+
|
|
113
|
+
elif ftype == "checkbox":
|
|
114
|
+
truthy = str(value).lower() in ("true", "1", "yes", "on")
|
|
115
|
+
on_val = _get_checkbox_on_value(field)
|
|
116
|
+
pdf_val = on_val if truthy else "/Off"
|
|
117
|
+
field.update({
|
|
118
|
+
NameObject("/V"): NameObject(pdf_val),
|
|
119
|
+
NameObject("/AS"): NameObject(pdf_val),
|
|
120
|
+
})
|
|
121
|
+
filled.append(full)
|
|
122
|
+
|
|
123
|
+
elif ftype in ("dropdown", "listbox"):
|
|
124
|
+
allowed = _get_dropdown_values(field)
|
|
125
|
+
if allowed and str(value) not in allowed:
|
|
126
|
+
errors.append({
|
|
127
|
+
"field": full,
|
|
128
|
+
"error": f"Value '{value}' not in allowed choices: {allowed}"
|
|
129
|
+
})
|
|
130
|
+
continue
|
|
131
|
+
field.update({NameObject("/V"): TextStringObject(str(value))})
|
|
132
|
+
filled.append(full)
|
|
133
|
+
|
|
134
|
+
elif ftype == "radio":
|
|
135
|
+
# Radio value must start with /
|
|
136
|
+
pdf_val = str(value) if str(value).startswith("/") else f"/{value}"
|
|
137
|
+
field.update({
|
|
138
|
+
NameObject("/V"): NameObject(pdf_val),
|
|
139
|
+
NameObject("/AS"): NameObject(pdf_val),
|
|
140
|
+
})
|
|
141
|
+
filled.append(full)
|
|
142
|
+
|
|
143
|
+
else:
|
|
144
|
+
errors.append({"field": full, "error": f"Unsupported field type: {ftype}"})
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def fill(pdf_path: str, out_path: str, data: dict) -> dict:
|
|
148
|
+
try:
|
|
149
|
+
reader = PdfReader(pdf_path)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
return {"status": "error", "error": str(e)}
|
|
152
|
+
|
|
153
|
+
writer = PdfWriter()
|
|
154
|
+
writer.clone_document_from_reader(reader)
|
|
155
|
+
|
|
156
|
+
acroform = writer._root_object.get("/AcroForm") # type: ignore[attr-defined]
|
|
157
|
+
if acroform is None or "/Fields" not in acroform:
|
|
158
|
+
return {
|
|
159
|
+
"status": "error",
|
|
160
|
+
"error": "This PDF has no fillable form fields.",
|
|
161
|
+
"hint": "Run fill_inspect.py first to confirm the PDF has fields.",
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Enable appearance regeneration so viewers show the new values
|
|
165
|
+
acroform.update({NameObject("/NeedAppearances"): BooleanObject(True)})
|
|
166
|
+
|
|
167
|
+
filled: list[str] = []
|
|
168
|
+
errors: list[dict] = []
|
|
169
|
+
_walk_and_fill(list(acroform["/Fields"]), data, filled, errors)
|
|
170
|
+
|
|
171
|
+
# Warn about requested fields that were never found
|
|
172
|
+
not_found = [k for k in data if k not in filled and not any(e["field"] == k for e in errors)]
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
os.makedirs(os.path.dirname(os.path.abspath(out_path)), exist_ok=True)
|
|
176
|
+
with open(out_path, "wb") as f:
|
|
177
|
+
writer.write(f)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
return {"status": "error", "error": f"Write failed: {e}"}
|
|
180
|
+
|
|
181
|
+
result = {
|
|
182
|
+
"status": "ok",
|
|
183
|
+
"out": out_path,
|
|
184
|
+
"filled_count": len(filled),
|
|
185
|
+
"filled_fields": filled,
|
|
186
|
+
"size_kb": os.path.getsize(out_path) // 1024,
|
|
187
|
+
}
|
|
188
|
+
if errors:
|
|
189
|
+
result["validation_errors"] = errors
|
|
190
|
+
if not_found:
|
|
191
|
+
result["not_found"] = not_found
|
|
192
|
+
result["hint"] = "Run fill_inspect.py to see all available field names."
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def main():
|
|
197
|
+
parser = argparse.ArgumentParser(description="Fill PDF form fields")
|
|
198
|
+
parser.add_argument("--input", required=True, help="Input PDF with form fields")
|
|
199
|
+
parser.add_argument("--out", required=True, help="Output PDF path")
|
|
200
|
+
group = parser.add_mutually_exclusive_group(required=True)
|
|
201
|
+
group.add_argument("--data", help="Path to JSON file with field values")
|
|
202
|
+
group.add_argument("--values", help="Inline JSON string with field values")
|
|
203
|
+
args = parser.parse_args()
|
|
204
|
+
|
|
205
|
+
if not os.path.exists(args.input):
|
|
206
|
+
print(json.dumps({"status": "error", "error": f"File not found: {args.input}"}),
|
|
207
|
+
file=sys.stderr)
|
|
208
|
+
sys.exit(1)
|
|
209
|
+
|
|
210
|
+
# Load data
|
|
211
|
+
try:
|
|
212
|
+
if args.data:
|
|
213
|
+
with open(args.data) as f:
|
|
214
|
+
data = json.load(f)
|
|
215
|
+
else:
|
|
216
|
+
data = json.loads(args.values)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
print(json.dumps({"status": "error", "error": f"JSON parse error: {e}"}),
|
|
219
|
+
file=sys.stderr)
|
|
220
|
+
sys.exit(1)
|
|
221
|
+
|
|
222
|
+
result = fill(args.input, args.out, data)
|
|
223
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
224
|
+
|
|
225
|
+
if result["status"] == "ok":
|
|
226
|
+
print(f"\n── Fill complete ───────────────────────────────────────",
|
|
227
|
+
file=sys.stderr)
|
|
228
|
+
print(f" Output : {result['out']}", file=sys.stderr)
|
|
229
|
+
print(f" Filled : {result['filled_count']} field(s)", file=sys.stderr)
|
|
230
|
+
if result.get("validation_errors"):
|
|
231
|
+
print(f" Errors :", file=sys.stderr)
|
|
232
|
+
for e in result["validation_errors"]:
|
|
233
|
+
print(f" • {e['field']}: {e['error']}", file=sys.stderr)
|
|
234
|
+
if result.get("not_found"):
|
|
235
|
+
print(f" Not found: {result['not_found']}", file=sys.stderr)
|
|
236
|
+
print("", file=sys.stderr)
|
|
237
|
+
else:
|
|
238
|
+
sys.exit(3)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
main()
|