@cteno/sdk-linux-x64 0.0.0-stage → 2.10.13

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.
Files changed (116) hide show
  1. package/LICENSE.daemon +31 -0
  2. package/README.md +4 -2
  3. package/agents/flow-analyzer/AGENT.md +71 -0
  4. package/bin/cteno-host-cli +4 -0
  5. package/bin/cteno-wiki +0 -0
  6. package/bin/rg +0 -0
  7. package/bin/secret-exec +0 -0
  8. package/manifest.json +12 -0
  9. package/package.json +33 -4
  10. package/plugins/agent-secrets/.cteno-plugin/plugin.json +133 -0
  11. package/plugins/agent-secrets/bin/linux-x64/fnox +4 -0
  12. package/plugins/agent-secrets/bin/linux-x64/secret-exec +0 -0
  13. package/plugins/agent-secrets/skills/secret/SKILL.md +201 -0
  14. package/plugins/cteno.browser/.cteno-plugin/plugin.json +65 -0
  15. package/plugins/cteno.browser/skills/browser/SKILL.md +28 -0
  16. package/plugins/cteno.inbox/.cteno-plugin/plugin.json +128 -0
  17. package/plugins/cteno.inbox/bin/all/cteno-connector-credential +31 -0
  18. package/plugins/cteno.inbox/bin/all/cteno-telegram-connector +33 -0
  19. package/plugins/cteno.inbox/bin/linux-x64/cteno-inbox +0 -0
  20. package/plugins/cteno.inbox/skills/cteno-inbox/SKILL.md +374 -0
  21. package/plugins/cteno.inbox/tools/inbox_focus/TOOL.md +44 -0
  22. package/plugins/cteno.memory/.cteno-plugin/plugin.json +176 -0
  23. package/plugins/cteno.memory/agents/llm-wiki/AGENT.md +96 -0
  24. package/plugins/cteno.memory/bin/linux-x64/cteno-wiki +0 -0
  25. package/plugins/cteno.memory/skills/codebase-wiki/SKILL.md +52 -0
  26. package/plugins/cteno.memory/skills/cteno.memory.native/SKILL.md +110 -0
  27. package/plugins/cteno.memory/tools/memory/TOOL.md +492 -0
  28. package/plugins/cteno.memory/ui/index.html +822 -0
  29. package/plugins/cteno.models/.cteno-plugin/plugin.json +191 -0
  30. package/plugins/cteno.websearch/.cteno-plugin/plugin.json +45 -0
  31. package/plugins/jev/.cteno-plugin/plugin.json +305 -0
  32. package/plugins/jev/skills/jev-browser/SKILL.md +66 -0
  33. package/plugins/jev/skills/jev-browser/jev_browser.py +335 -0
  34. package/skills/agent-create/SKILL.md +86 -0
  35. package/skills/cteno-host-cli/SKILL.md +248 -0
  36. package/skills/docx/LICENSE +23 -0
  37. package/skills/docx/NOTICE.md +8 -0
  38. package/skills/docx/SKILL.md +69 -0
  39. package/skills/docx/references/cjk_typography.md +357 -0
  40. package/skills/docx/references/design_principles.md +819 -0
  41. package/skills/docx/references/revisions-and-comments.md +88 -0
  42. package/skills/docx/references/track_changes_guide.md +200 -0
  43. package/skills/docx/references/typography_guide.md +294 -0
  44. package/skills/docx/scripts/cjk_fonts.py +244 -0
  45. package/skills/docx/scripts/docx_comments.py +289 -0
  46. package/skills/docx/scripts/docx_common.py +92 -0
  47. package/skills/docx/scripts/docx_create.py +259 -0
  48. package/skills/docx/scripts/docx_edit.py +250 -0
  49. package/skills/docx/scripts/docx_read.py +149 -0
  50. package/skills/docx/scripts/docx_revisions.py +146 -0
  51. package/skills/docx/scripts/docx_template.py +70 -0
  52. package/skills/docx/scripts/docx_validate.py +156 -0
  53. package/skills/docx/scripts/render.py +250 -0
  54. package/skills/docx/tests/test_docx_skill.py +525 -0
  55. package/skills/home-devices/SKILL.md +121 -0
  56. package/skills/mcp/SKILL.md +72 -0
  57. package/skills/mcp/scripts/mcp_setup.py +439 -0
  58. package/skills/pdf/LICENSE +23 -0
  59. package/skills/pdf/NOTICE.md +8 -0
  60. package/skills/pdf/SKILL.md +63 -0
  61. package/skills/pdf/references/design-tokens.md +381 -0
  62. package/skills/pdf/references/forms.md +99 -0
  63. package/skills/pdf/references/nano-pdf-editing.md +40 -0
  64. package/skills/pdf/references/ocr-extraction.md +165 -0
  65. package/skills/pdf/scripts/_raster.py +76 -0
  66. package/skills/pdf/scripts/extract_marker.py +88 -0
  67. package/skills/pdf/scripts/extract_pymupdf.py +101 -0
  68. package/skills/pdf/scripts/pdf_create.py +191 -0
  69. package/skills/pdf/scripts/pdf_fill_form.py +97 -0
  70. package/skills/pdf/scripts/pdf_form_layout.py +168 -0
  71. package/skills/pdf/scripts/pdf_make_form.py +145 -0
  72. package/skills/pdf/scripts/pdf_merge.py +50 -0
  73. package/skills/pdf/scripts/pdf_meta.py +115 -0
  74. package/skills/pdf/scripts/pdf_page_image.py +99 -0
  75. package/skills/pdf/scripts/pdf_read.py +153 -0
  76. package/skills/pdf/scripts/pdf_secure.py +71 -0
  77. package/skills/pdf/scripts/pdf_split.py +84 -0
  78. package/skills/pdf/scripts/pdf_stamp.py +143 -0
  79. package/skills/pdf/scripts/pdf_watermark.py +51 -0
  80. package/skills/pdf/scripts/render.py +250 -0
  81. package/skills/pdf/tests/test_pdf_skill.py +415 -0
  82. package/skills/pptx/LICENSE +23 -0
  83. package/skills/pptx/NOTICE.md +8 -0
  84. package/skills/pptx/SKILL.md +62 -0
  85. package/skills/pptx/references/design-system.md +392 -0
  86. package/skills/pptx/references/pptxgenjs.md +420 -0
  87. package/skills/pptx/references/slide-types.md +413 -0
  88. package/skills/pptx/scripts/cjk_fonts.py +244 -0
  89. package/skills/pptx/scripts/pptx_create.py +240 -0
  90. package/skills/pptx/scripts/pptx_edit.py +436 -0
  91. package/skills/pptx/scripts/pptx_from_template.py +88 -0
  92. package/skills/pptx/scripts/pptx_read.py +131 -0
  93. package/skills/pptx/scripts/pptx_render.py +96 -0
  94. package/skills/pptx/scripts/render.py +250 -0
  95. package/skills/pptx/tests/test_powerpoint_skill.py +477 -0
  96. package/skills/skill-create/SKILL.md +114 -0
  97. package/skills/video-generation/SKILL.md +27 -0
  98. package/skills/xlsx/LICENSE +23 -0
  99. package/skills/xlsx/NOTICE.md +8 -0
  100. package/skills/xlsx/SKILL.md +63 -0
  101. package/skills/xlsx/references/format.md +71 -0
  102. package/skills/xlsx/references/restructuring.md +71 -0
  103. package/skills/xlsx/scripts/csv_to_xlsx.py +110 -0
  104. package/skills/xlsx/scripts/formula_check.py +428 -0
  105. package/skills/xlsx/scripts/render.py +250 -0
  106. package/skills/xlsx/scripts/xlsx_create.py +265 -0
  107. package/skills/xlsx/scripts/xlsx_edit.py +269 -0
  108. package/skills/xlsx/scripts/xlsx_read.py +166 -0
  109. package/skills/xlsx/scripts/xlsx_recalc.py +119 -0
  110. package/skills/xlsx/scripts/xlsx_restructure.py +343 -0
  111. package/skills/xlsx/scripts/xlsx_to_csv.py +75 -0
  112. package/skills/xlsx/tests/test_xlsx_skill.py +544 -0
  113. package/tools/create_goal/TOOL.md +36 -0
  114. package/tools/image_generation/TOOL.md +81 -0
  115. package/tools/skill/TOOL.md +117 -0
  116. package/tools/update_profile_notes/TOOL.md +39 -0
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env python3
2
+ """Validate a form-spec layout BEFORE building the PDF, with optional
3
+ visual overlay rendering for review with a vision model.
4
+
5
+ Input is the same JSON spec pdf_make_form.py consumes: each field has
6
+ "page", "label_box" and "entry_box" as [x0, y0, x1, y1] in PDF points
7
+ (origin bottom-left). Checks per field:
8
+ - boxes lie within the page bounds
9
+ - boxes are well-formed (x0 < x1, y0 < y1)
10
+ - entry boxes meet minimum sizes (default 8x8 pt; 12 pt height for text)
11
+ - no two entry boxes on the same page overlap
12
+ - the label sits near its entry box (default within 150 pt gap)
13
+
14
+ Prints a JSON report {"ok": bool, "fields": [...], "errors": N};
15
+ exit 0 when clean, 1 when any check fails.
16
+
17
+ --render-overlay OUT.png rasterizes --overlay-page (default 1) of an
18
+ existing PDF (--pdf; a blank page of spec size if omitted) and draws
19
+ label boxes (blue) and entry boxes (red) with field names, for
20
+ review with `vision_analyze`. If no rasterizer (pypdfium2/pdftoppm) is
21
+ available the overlay is skipped with {"rendered": false, "missing": [...]}
22
+ and validation exit status is unchanged.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ import json
28
+ import sys
29
+
30
+ MIN_W = 8.0
31
+ MIN_H = 8.0
32
+ MIN_TEXT_H = 12.0
33
+ MAX_LABEL_GAP = 150.0
34
+
35
+
36
+ def _boxes_overlap(a, b) -> bool:
37
+ return not (a[2] <= b[0] or b[2] <= a[0] or a[3] <= b[1] or b[3] <= a[1])
38
+
39
+
40
+ def _box_gap(a, b) -> float:
41
+ dx = max(b[0] - a[2], a[0] - b[2], 0.0)
42
+ dy = max(b[1] - a[3], a[1] - b[3], 0.0)
43
+ return (dx ** 2 + dy ** 2) ** 0.5
44
+
45
+
46
+ def _page_size(spec: dict) -> tuple[float, float]:
47
+ sizes = {"a4": (595.27, 841.89), "letter": (612.0, 792.0)}
48
+ ps = spec.get("page_size", "A4")
49
+ if isinstance(ps, (list, tuple)) and len(ps) == 2:
50
+ return float(ps[0]), float(ps[1])
51
+ return sizes.get(str(ps).lower(), sizes["a4"])
52
+
53
+
54
+ def _check_box(box, width, height, min_w, min_h, kind) -> list[str]:
55
+ problems = []
56
+ if box is None:
57
+ return [f"{kind}_box missing"]
58
+ x0, y0, x1, y1 = (float(v) for v in box)
59
+ if x0 >= x1 or y0 >= y1:
60
+ problems.append(f"{kind}_box malformed (need x0<x1 and y0<y1): {box}")
61
+ return problems
62
+ if x0 < 0 or y0 < 0 or x1 > width or y1 > height:
63
+ problems.append(f"{kind}_box outside page bounds {width}x{height}: {box}")
64
+ if x1 - x0 < min_w or y1 - y0 < min_h:
65
+ problems.append(f"{kind}_box below minimum size {min_w}x{min_h}: {box}")
66
+ return problems
67
+
68
+
69
+ def validate(spec: dict) -> dict:
70
+ width, height = _page_size(spec)
71
+ fields = spec.get("fields", [])
72
+ report = []
73
+ entry_boxes: dict[int, list[tuple[str, list[float]]]] = {}
74
+ for f in fields:
75
+ name = f.get("name", "?")
76
+ page = int(f.get("page", 1))
77
+ problems = []
78
+ min_h = MIN_TEXT_H if f.get("type", "text") in ("text", "dropdown") else MIN_H
79
+ entry = f.get("entry_box")
80
+ problems += _check_box(entry, width, height, MIN_W, min_h, "entry")
81
+ label = f.get("label_box")
82
+ if f.get("label"):
83
+ problems += _check_box(label, width, height, 4, 4, "label")
84
+ if entry and label and len(problems) == 0:
85
+ gap = _box_gap([float(v) for v in label], [float(v) for v in entry])
86
+ if gap > MAX_LABEL_GAP:
87
+ problems.append(f"label is {gap:.0f}pt from its entry box (max {MAX_LABEL_GAP:.0f})")
88
+ if _boxes_overlap([float(v) for v in label], [float(v) for v in entry]):
89
+ problems.append("label_box overlaps its own entry_box")
90
+ if entry and not any("malformed" in p or "missing" in p for p in problems):
91
+ ebox = [float(v) for v in entry]
92
+ for other_name, other_box in entry_boxes.get(page, []):
93
+ if _boxes_overlap(ebox, other_box):
94
+ problems.append(f"entry_box overlaps field {other_name!r}")
95
+ entry_boxes.setdefault(page, []).append((name, ebox))
96
+ report.append({"name": name, "page": page, "ok": not problems, "problems": problems})
97
+ errors = sum(1 for r in report if not r["ok"])
98
+ return {"ok": errors == 0, "page_size": [width, height],
99
+ "field_count": len(report), "errors": errors, "fields": report}
100
+
101
+
102
+ def render_overlay(spec: dict, pdf_path: str | None, page: int, out_png: str,
103
+ dpi: int = 100) -> dict:
104
+ sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent))
105
+ import _raster
106
+ if not _raster.available_backends() and pdf_path:
107
+ return {"rendered": False, "missing": _raster.missing_hints()}
108
+ from PIL import Image, ImageDraw
109
+ width, height = _page_size(spec)
110
+ if pdf_path:
111
+ img = _raster.rasterize_page(pdf_path, page, dpi=dpi)
112
+ if img is None:
113
+ return {"rendered": False, "missing": _raster.missing_hints()}
114
+ scale = img.width / width
115
+ else:
116
+ scale = dpi / 72.0
117
+ img = Image.new("RGB", (int(width * scale), int(height * scale)), "white")
118
+ draw = ImageDraw.Draw(img)
119
+
120
+ def to_px(box):
121
+ x0, y0, x1, y1 = (float(v) for v in box)
122
+ return [x0 * scale, img.height - y1 * scale, x1 * scale, img.height - y0 * scale]
123
+
124
+ for f in spec.get("fields", []):
125
+ if int(f.get("page", 1)) != page:
126
+ continue
127
+ if f.get("entry_box"):
128
+ px = to_px(f["entry_box"])
129
+ draw.rectangle(px, outline=(220, 30, 30), width=2)
130
+ draw.text((px[0] + 2, px[1] + 2), str(f.get("name", "?")), fill=(220, 30, 30))
131
+ if f.get("label_box"):
132
+ draw.rectangle(to_px(f["label_box"]), outline=(30, 60, 220), width=2)
133
+ img.save(out_png)
134
+ return {"rendered": True, "overlay": out_png, "page": page,
135
+ "legend": {"entry_box": "red", "label_box": "blue"}}
136
+
137
+
138
+ def main() -> int:
139
+ for stream in (sys.stdout, sys.stderr):
140
+ try:
141
+ stream.reconfigure(encoding="utf-8")
142
+ except Exception:
143
+ pass
144
+ parser = argparse.ArgumentParser(
145
+ description="Validate form-spec layout (boxes, overlaps, label pairing); "
146
+ "optionally render an annotated overlay image.")
147
+ parser.add_argument("spec", help="Form spec JSON (same format as pdf_make_form.py)")
148
+ parser.add_argument("--pdf", help="Existing PDF to rasterize under the overlay "
149
+ "(blank page if omitted)")
150
+ parser.add_argument("--render-overlay", metavar="OUT_PNG",
151
+ help="Write an annotated PNG for visual review")
152
+ parser.add_argument("--overlay-page", type=int, default=1, help="1-based page (default 1)")
153
+ parser.add_argument("--dpi", type=int, default=100, help="Overlay render DPI (default 100)")
154
+ args = parser.parse_args()
155
+
156
+ with open(args.spec, encoding="utf-8") as fh:
157
+ spec = json.load(fh)
158
+ result = validate(spec)
159
+ if args.render_overlay:
160
+ result["overlay"] = render_overlay(spec, args.pdf, args.overlay_page,
161
+ args.render_overlay, args.dpi)
162
+ json.dump(result, sys.stdout, ensure_ascii=False, indent=2)
163
+ print()
164
+ return 0 if result["ok"] else 1
165
+
166
+
167
+ if __name__ == "__main__":
168
+ sys.exit(main())
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ """Create a fillable AcroForm PDF from a JSON spec (reportlab canvas.acroForm).
3
+
4
+ Spec format (UTF-8 JSON; coordinates in PDF points, origin bottom-left):
5
+ {
6
+ "title": "Example Intake Form",
7
+ "page_size": "A4", // or "letter" or [width, height]
8
+ "page_count": 1,
9
+ "fields": [
10
+ {"name": "surname", "type": "text", "page": 1,
11
+ "label": "Surname", "label_box": [72, 700, 150, 714],
12
+ "entry_box": [160, 696, 400, 716], "value": "", "tooltip": "Family name"},
13
+ {"name": "agree", "type": "checkbox", "page": 1,
14
+ "label": "I agree", "label_box": [72, 660, 150, 674],
15
+ "entry_box": [160, 658, 176, 674], "checked": false},
16
+ {"name": "color", "type": "radio", "page": 1,
17
+ "label": "Color", "label_box": [72, 620, 150, 634],
18
+ "entry_box": [160, 616, 400, 636], "options": ["red", "blue"],
19
+ "value": "red"},
20
+ {"name": "size", "type": "dropdown", "page": 1,
21
+ "label": "Size", "label_box": [72, 580, 150, 594],
22
+ "entry_box": [160, 576, 300, 596], "options": ["small", "large"],
23
+ "value": "small"}
24
+ ]
25
+ }
26
+ The same spec (label_box/entry_box/page) is what pdf_form_layout.py validates,
27
+ so lint the layout first, then build.
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import json
33
+ import sys
34
+
35
+
36
+ def _reconfigure_stdio() -> None:
37
+ for stream in (sys.stdout, sys.stderr):
38
+ try:
39
+ stream.reconfigure(encoding="utf-8")
40
+ except Exception:
41
+ pass
42
+
43
+
44
+ def _page_size(spec: dict):
45
+ from reportlab.lib.pagesizes import A4, letter
46
+ ps = spec.get("page_size", "A4")
47
+ if isinstance(ps, (list, tuple)) and len(ps) == 2:
48
+ return float(ps[0]), float(ps[1])
49
+ return letter if str(ps).lower() == "letter" else A4
50
+
51
+
52
+ def build_form(spec: dict, out_path: str) -> int:
53
+ try:
54
+ from reportlab.lib import colors
55
+ from reportlab.pdfgen import canvas
56
+ except ImportError:
57
+ print("Missing dependency: install with 'python3 -m pip install reportlab'", file=sys.stderr)
58
+ return 2
59
+
60
+ width, height = _page_size(spec)
61
+ page_count = int(spec.get("page_count", 1))
62
+ fields = spec.get("fields", [])
63
+ by_page: dict[int, list[dict]] = {}
64
+ for f in fields:
65
+ by_page.setdefault(int(f.get("page", 1)), []).append(f)
66
+ page_count = max([page_count, *by_page.keys()]) if by_page else page_count
67
+
68
+ c = canvas.Canvas(out_path, pagesize=(width, height))
69
+ if spec.get("title"):
70
+ c.setTitle(str(spec["title"]))
71
+ if spec.get("author"):
72
+ c.setAuthor(str(spec["author"]))
73
+ form = c.acroForm
74
+ created = []
75
+
76
+ for pageno in range(1, page_count + 1):
77
+ for f in by_page.get(pageno, []):
78
+ name = f["name"]
79
+ ftype = f.get("type", "text")
80
+ ex0, ey0, ex1, ey1 = (float(v) for v in f["entry_box"])
81
+ ew, eh = ex1 - ex0, ey1 - ey0
82
+ if f.get("label"):
83
+ lx, ly = (float(f["label_box"][0]), float(f["label_box"][1])) \
84
+ if f.get("label_box") else (ex0 - 90, ey0 + 4)
85
+ c.setFont("Helvetica", float(f.get("label_size", 10)))
86
+ c.setFillColor(colors.black)
87
+ c.drawString(lx, ly + 2, str(f["label"]))
88
+ tooltip = f.get("tooltip", "")
89
+ if ftype == "text":
90
+ form.textfield(name=name, x=ex0, y=ey0, width=ew, height=eh,
91
+ value=str(f.get("value", "")), tooltip=tooltip,
92
+ borderWidth=0.5, forceBorder=True)
93
+ elif ftype == "checkbox":
94
+ size = min(ew, eh)
95
+ form.checkbox(name=name, x=ex0, y=ey0, size=size,
96
+ checked=bool(f.get("checked", False)),
97
+ buttonStyle="check", tooltip=tooltip,
98
+ borderWidth=0.5, forceBorder=True)
99
+ elif ftype == "radio":
100
+ options = f.get("options", [])
101
+ if not options:
102
+ print(f"Warning: radio {name!r} has no options, skipped", file=sys.stderr)
103
+ continue
104
+ size = min(eh, ew / max(len(options), 1) * 0.5, 16)
105
+ slot = ew / len(options)
106
+ sel = f.get("value")
107
+ c.setFont("Helvetica", 8)
108
+ for i, opt in enumerate(options):
109
+ ox = ex0 + i * slot
110
+ form.radio(name=name, value=str(opt), x=ox, y=ey0, size=size,
111
+ selected=(str(opt) == str(sel)), buttonStyle="circle",
112
+ borderWidth=0.5, forceBorder=True)
113
+ c.drawString(ox + size + 2, ey0 + size / 3, str(opt))
114
+ elif ftype == "dropdown":
115
+ options = [str(o) for o in f.get("options", [])]
116
+ value = str(f.get("value", options[0] if options else ""))
117
+ form.choice(name=name, x=ex0, y=ey0, width=ew, height=eh,
118
+ options=options, value=value, tooltip=tooltip,
119
+ borderWidth=0.5, forceBorder=True)
120
+ else:
121
+ print(f"Warning: unknown field type {ftype!r} for {name!r}, skipped",
122
+ file=sys.stderr)
123
+ continue
124
+ created.append({"name": name, "type": ftype, "page": pageno})
125
+ c.showPage()
126
+ c.save()
127
+ print(json.dumps({"output": out_path, "pages": page_count, "fields": created},
128
+ ensure_ascii=False))
129
+ return 0
130
+
131
+
132
+ def main() -> int:
133
+ _reconfigure_stdio()
134
+ parser = argparse.ArgumentParser(
135
+ description="Create a fillable AcroForm PDF from a JSON spec (reportlab).")
136
+ parser.add_argument("spec", help="Path to UTF-8 JSON form spec")
137
+ parser.add_argument("-o", "--output", required=True, help="Output PDF path")
138
+ args = parser.parse_args()
139
+ with open(args.spec, encoding="utf-8") as fh:
140
+ spec = json.load(fh)
141
+ return build_form(spec, args.output)
142
+
143
+
144
+ if __name__ == "__main__":
145
+ sys.exit(main())
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env python3
2
+ """Merge multiple PDFs into one, optionally adding a bookmark per source file."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+
10
+
11
+ def main() -> int:
12
+ for stream in (sys.stdout, sys.stderr):
13
+ try:
14
+ stream.reconfigure(encoding="utf-8")
15
+ except Exception:
16
+ pass
17
+ parser = argparse.ArgumentParser(description="Merge PDFs (pypdf).")
18
+ parser.add_argument("inputs", nargs="+", help="Input PDF paths, in order")
19
+ parser.add_argument("-o", "--output", required=True, help="Output PDF path")
20
+ parser.add_argument("--bookmarks", action="store_true",
21
+ help="Add a top-level bookmark per input file (its basename)")
22
+ args = parser.parse_args()
23
+
24
+ try:
25
+ from pypdf import PdfReader, PdfWriter
26
+ except ImportError:
27
+ print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)
28
+ return 2
29
+
30
+ writer = PdfWriter()
31
+ total = 0
32
+ for path in args.inputs:
33
+ reader = PdfReader(path)
34
+ if reader.is_encrypted:
35
+ print(f"Error: {path} is encrypted; decrypt it first with pdf_secure.py --decrypt", file=sys.stderr)
36
+ return 3
37
+ start = total
38
+ for page in reader.pages:
39
+ writer.add_page(page)
40
+ total += 1
41
+ if args.bookmarks:
42
+ writer.add_outline_item(os.path.splitext(os.path.basename(path))[0], start)
43
+ with open(args.output, "wb") as fh:
44
+ writer.write(fh)
45
+ print(json.dumps({"output": args.output, "inputs": len(args.inputs), "page_count": total}))
46
+ return 0
47
+
48
+
49
+ if __name__ == "__main__":
50
+ sys.exit(main())
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env python3
2
+ """Document metadata and file attachments for PDFs (pypdf).
3
+
4
+ Modes (one required):
5
+ --set-meta set metadata keys given via --title/--author/...
6
+ --clear-meta drop all document info metadata
7
+ --attach FILE embed a file attachment
8
+ --list-attachments list embedded attachment names
9
+ --extract-attachments DIR write all attachments into DIR
10
+
11
+ Metadata note: values are stored in the classic DocInfo dictionary
12
+ (Title/Author/Subject/Keywords). XMP metadata, if present, is not
13
+ rewritten and may disagree in sophisticated viewers.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import sys
21
+ from pathlib import Path
22
+
23
+
24
+ def main() -> int:
25
+ for stream in (sys.stdout, sys.stderr):
26
+ try:
27
+ stream.reconfigure(encoding="utf-8")
28
+ except Exception:
29
+ pass
30
+ parser = argparse.ArgumentParser(description="Set/clear PDF metadata; manage attachments.")
31
+ parser.add_argument("pdf", help="Input PDF path")
32
+ mode = parser.add_mutually_exclusive_group(required=True)
33
+ mode.add_argument("--set-meta", action="store_true", help="Set metadata fields")
34
+ mode.add_argument("--clear-meta", action="store_true", help="Remove all DocInfo metadata")
35
+ mode.add_argument("--attach", metavar="FILE", help="Embed FILE as an attachment")
36
+ mode.add_argument("--list-attachments", action="store_true", help="List attachment names")
37
+ mode.add_argument("--extract-attachments", metavar="DIR", help="Extract attachments into DIR")
38
+ parser.add_argument("-o", "--output", help="Output PDF (required for write modes)")
39
+ parser.add_argument("--title")
40
+ parser.add_argument("--author")
41
+ parser.add_argument("--subject")
42
+ parser.add_argument("--keywords")
43
+ parser.add_argument("--password", help="Password if the input is encrypted")
44
+ args = parser.parse_args()
45
+
46
+ try:
47
+ from pypdf import PdfReader, PdfWriter
48
+ except ImportError:
49
+ print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)
50
+ return 2
51
+
52
+ reader = PdfReader(args.pdf)
53
+ if reader.is_encrypted:
54
+ if args.password is None or not reader.decrypt(args.password):
55
+ print("Error: input is encrypted; pass --password", file=sys.stderr)
56
+ return 3
57
+
58
+ if args.list_attachments:
59
+ names = list(reader.attachments.keys())
60
+ json.dump({"attachment_count": len(names), "attachments": names}, sys.stdout,
61
+ ensure_ascii=False, indent=2)
62
+ print()
63
+ return 0
64
+
65
+ if args.extract_attachments:
66
+ out_dir = Path(args.extract_attachments)
67
+ out_dir.mkdir(parents=True, exist_ok=True)
68
+ written = []
69
+ for name, contents in reader.attachments.items():
70
+ data = contents[0] if isinstance(contents, list) else contents
71
+ safe = os.path.basename(name) or "attachment.bin"
72
+ target = out_dir / safe
73
+ with open(target, "wb") as fh:
74
+ fh.write(bytes(data))
75
+ written.append(str(target))
76
+ json.dump({"extracted": written}, sys.stdout, ensure_ascii=False, indent=2)
77
+ print()
78
+ return 0
79
+
80
+ if not args.output:
81
+ print("Error: -o/--output is required for write modes", file=sys.stderr)
82
+ return 4
83
+
84
+ writer = PdfWriter()
85
+ writer.append(reader)
86
+
87
+ if args.set_meta:
88
+ meta = {}
89
+ for key, value in ((f"/{k.capitalize()}", getattr(args, k))
90
+ for k in ("title", "author", "subject", "keywords")):
91
+ if value is not None:
92
+ meta[key] = value
93
+ if not meta:
94
+ print("Error: --set-meta needs at least one of --title/--author/--subject/--keywords",
95
+ file=sys.stderr)
96
+ return 4
97
+ writer.add_metadata(meta)
98
+ result = {"output": args.output, "set": {k.lstrip("/"): v for k, v in meta.items()}}
99
+ elif args.clear_meta:
100
+ writer.metadata = None
101
+ result = {"output": args.output, "cleared": True}
102
+ else: # --attach
103
+ attach_path = Path(args.attach)
104
+ with open(attach_path, "rb") as fh:
105
+ writer.add_attachment(attach_path.name, fh.read())
106
+ result = {"output": args.output, "attached": attach_path.name}
107
+
108
+ with open(args.output, "wb") as fh:
109
+ writer.write(fh)
110
+ print(json.dumps(result, ensure_ascii=False))
111
+ return 0
112
+
113
+
114
+ if __name__ == "__main__":
115
+ sys.exit(main())
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env python3
2
+ """Export PDF pages as PNG images at a chosen DPI.
3
+
4
+ Rasterizer fallback chain: pypdfium2 (pip) -> pdftoppm (poppler-utils).
5
+ When neither is available, exits 0 with {"rendered": false, "missing": [...]}
6
+ so callers can branch instead of crashing.
7
+
8
+ Typical uses: visual verification with a vision model, and exporting
9
+ image-only (scanned) pages for hand-off to the references/ocr-extraction.md in this skill.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+
18
+
19
+ def parse_pages(spec: str, page_count: int) -> list[int]:
20
+ """'1-3,5,9-' (1-based, inclusive) -> sorted page list."""
21
+ pages: set[int] = set()
22
+ for part in spec.split(","):
23
+ part = part.strip()
24
+ if not part:
25
+ continue
26
+ if "-" in part:
27
+ start_s, _, end_s = part.partition("-")
28
+ start = int(start_s) if start_s else 1
29
+ end = int(end_s) if end_s else page_count
30
+ pages.update(range(start, end + 1))
31
+ else:
32
+ pages.add(int(part))
33
+ bad = [p for p in pages if not 1 <= p <= page_count]
34
+ if bad:
35
+ raise ValueError(f"pages out of range 1-{page_count}: {sorted(bad)}")
36
+ return sorted(pages)
37
+
38
+
39
+ def main() -> int:
40
+ for stream in (sys.stdout, sys.stderr):
41
+ try:
42
+ stream.reconfigure(encoding="utf-8")
43
+ except Exception:
44
+ pass
45
+ parser = argparse.ArgumentParser(description="Export PDF pages as PNG images.")
46
+ parser.add_argument("pdf", help="Input PDF path")
47
+ parser.add_argument("--pages", default="1-", help="1-based ranges, e.g. '1-3,5' (default: all)")
48
+ parser.add_argument("--dpi", type=int, default=150, help="Render DPI (default 150)")
49
+ parser.add_argument("--out-dir", required=True, help="Directory for PNG files")
50
+ parser.add_argument("--prefix", default="page", help="Output filename prefix (default 'page')")
51
+ parser.add_argument("--password", help="Password for encrypted PDFs")
52
+ args = parser.parse_args()
53
+
54
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
55
+ import _raster
56
+
57
+ if not _raster.available_backends():
58
+ json.dump({"rendered": False, "missing": _raster.missing_hints()}, sys.stdout)
59
+ print()
60
+ return 0
61
+
62
+ try:
63
+ from pypdf import PdfReader
64
+ except ImportError:
65
+ print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)
66
+ return 2
67
+ reader = PdfReader(args.pdf)
68
+ if reader.is_encrypted:
69
+ if args.password is None or not reader.decrypt(args.password):
70
+ print("File is encrypted; pass --password.", file=sys.stderr)
71
+ return 3
72
+ page_count = len(reader.pages)
73
+
74
+ try:
75
+ pages = parse_pages(args.pages, page_count)
76
+ except ValueError as exc:
77
+ print(f"Error: {exc}", file=sys.stderr)
78
+ return 4
79
+
80
+ out_dir = Path(args.out_dir)
81
+ out_dir.mkdir(parents=True, exist_ok=True)
82
+ files = []
83
+ for pageno in pages:
84
+ img = _raster.rasterize_page(args.pdf, pageno, dpi=args.dpi, password=args.password)
85
+ if img is None:
86
+ json.dump({"rendered": False, "missing": _raster.missing_hints()}, sys.stdout)
87
+ print()
88
+ return 0
89
+ out_path = out_dir / f"{args.prefix}{pageno:03d}.png"
90
+ img.save(out_path)
91
+ files.append(str(out_path))
92
+ json.dump({"rendered": True, "dpi": args.dpi, "page_count": page_count,
93
+ "files": files}, sys.stdout, ensure_ascii=False, indent=2)
94
+ print()
95
+ return 0
96
+
97
+
98
+ if __name__ == "__main__":
99
+ sys.exit(main())