@arnilo/prism-tools 0.0.0-stage → 0.13.1

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 (331) hide show
  1. package/CHANGELOG.md +418 -0
  2. package/LICENSE +21 -0
  3. package/README.md +44 -2
  4. package/browser/README.md +98 -0
  5. package/browser/skills/browser-automation/SKILL.md +14 -0
  6. package/browser/src/actions/cdp.ts +131 -0
  7. package/browser/src/actions/dialog.ts +25 -0
  8. package/browser/src/actions/download_release.ts +22 -0
  9. package/browser/src/actions/index.ts +37 -0
  10. package/browser/src/actions/locator.ts +70 -0
  11. package/browser/src/actions/navigate.ts +23 -0
  12. package/browser/src/actions/screenshot.ts +27 -0
  13. package/browser/src/actions/scroll.ts +24 -0
  14. package/browser/src/actions/select_page.ts +21 -0
  15. package/browser/src/actions/types.ts +114 -0
  16. package/browser/src/actions/upload.ts +44 -0
  17. package/browser/src/actions/wait.ts +45 -0
  18. package/browser/src/browser.ts +197 -0
  19. package/browser/src/cdp.ts +178 -0
  20. package/browser/src/checkpoint.ts +189 -0
  21. package/browser/src/config.ts +72 -0
  22. package/browser/src/downloads.ts +253 -0
  23. package/browser/src/errors.ts +31 -0
  24. package/browser/src/evaluate.ts +101 -0
  25. package/browser/src/index.ts +129 -0
  26. package/browser/src/limits.ts +271 -0
  27. package/browser/src/manager.ts +730 -0
  28. package/browser/src/network.ts +263 -0
  29. package/browser/src/observe.ts +178 -0
  30. package/browser/src/policy.ts +133 -0
  31. package/browser/src/screenshot.ts +107 -0
  32. package/browser/src/shared-sandbox.ts +53 -0
  33. package/browser/src/snapshot.ts +99 -0
  34. package/browser/src/targets.ts +165 -0
  35. package/browser/src/tools.ts +480 -0
  36. package/browser/src/types.ts +316 -0
  37. package/browser/src/uploads.ts +174 -0
  38. package/coding/README.md +337 -0
  39. package/coding/src/agent/acp-operations.ts +146 -0
  40. package/coding/src/agent/artifacts.ts +38 -0
  41. package/coding/src/agent/ask-user-decision.ts +623 -0
  42. package/coding/src/agent/atomic-write.ts +23 -0
  43. package/coding/src/agent/bounded-file.ts +21 -0
  44. package/coding/src/agent/checks.ts +301 -0
  45. package/coding/src/agent/coding-checkpoint.ts +828 -0
  46. package/coding/src/agent/delete.ts +237 -0
  47. package/coding/src/agent/diagnostics-tool.ts +163 -0
  48. package/coding/src/agent/diagnostics.ts +262 -0
  49. package/coding/src/agent/edit-diff.ts +382 -0
  50. package/coding/src/agent/edit.ts +322 -0
  51. package/coding/src/agent/effects.ts +102 -0
  52. package/coding/src/agent/env.ts +28 -0
  53. package/coding/src/agent/execution-policy.ts +89 -0
  54. package/coding/src/agent/file-mutation-queue.ts +72 -0
  55. package/coding/src/agent/forge/github.ts +603 -0
  56. package/coding/src/agent/forge/index.ts +16 -0
  57. package/coding/src/agent/forge/types.ts +185 -0
  58. package/coding/src/agent/git-aware-repository.ts +305 -0
  59. package/coding/src/agent/git-exec.ts +326 -0
  60. package/coding/src/agent/git-status.ts +177 -0
  61. package/coding/src/agent/git-tools.ts +360 -0
  62. package/coding/src/agent/git.ts +1074 -0
  63. package/coding/src/agent/glob-match.ts +146 -0
  64. package/coding/src/agent/glob.ts +182 -0
  65. package/coding/src/agent/goal-verify.ts +385 -0
  66. package/coding/src/agent/grep-worker.ts +74 -0
  67. package/coding/src/agent/grep.ts +459 -0
  68. package/coding/src/agent/identity.ts +48 -0
  69. package/coding/src/agent/index.ts +676 -0
  70. package/coding/src/agent/language/client.ts +429 -0
  71. package/coding/src/agent/language/framing.ts +190 -0
  72. package/coding/src/agent/language/index.ts +23 -0
  73. package/coding/src/agent/language/intelligence.ts +682 -0
  74. package/coding/src/agent/language/types.ts +187 -0
  75. package/coding/src/agent/lifecycle-events.ts +121 -0
  76. package/coding/src/agent/lifecycle.ts +166 -0
  77. package/coding/src/agent/limits.ts +214 -0
  78. package/coding/src/agent/move.ts +178 -0
  79. package/coding/src/agent/mutation-path.ts +52 -0
  80. package/coding/src/agent/output-accumulator.ts +304 -0
  81. package/coding/src/agent/path-utils.ts +120 -0
  82. package/coding/src/agent/process/index.ts +58 -0
  83. package/coding/src/agent/process/recovery.ts +508 -0
  84. package/coding/src/agent/process/sessions-host.ts +222 -0
  85. package/coding/src/agent/process/sessions-monitor.ts +141 -0
  86. package/coding/src/agent/process/sessions-recovery.ts +241 -0
  87. package/coding/src/agent/process/sessions-spawn.ts +613 -0
  88. package/coding/src/agent/process/sessions-teardown.ts +281 -0
  89. package/coding/src/agent/process/sessions.ts +30 -0
  90. package/coding/src/agent/process/types.ts +358 -0
  91. package/coding/src/agent/read-path-set.ts +126 -0
  92. package/coding/src/agent/read.ts +713 -0
  93. package/coding/src/agent/repository/glob.ts +147 -0
  94. package/coding/src/agent/repository/indexed-search.ts +480 -0
  95. package/coding/src/agent/repository/list.ts +131 -0
  96. package/coding/src/agent/repository/operations.ts +22 -0
  97. package/coding/src/agent/repository/path.ts +98 -0
  98. package/coding/src/agent/repository/search.ts +277 -0
  99. package/coding/src/agent/repository/types.ts +243 -0
  100. package/coding/src/agent/repository/walk.ts +126 -0
  101. package/coding/src/agent/repository.ts +12 -0
  102. package/coding/src/agent/shell-background.ts +264 -0
  103. package/coding/src/agent/shell-output.ts +75 -0
  104. package/coding/src/agent/shell.ts +526 -0
  105. package/coding/src/agent/todo.ts +399 -0
  106. package/coding/src/agent/truncate.ts +251 -0
  107. package/coding/src/agent/write.ts +170 -0
  108. package/coding/src/ask-user-host.ts +48 -0
  109. package/coding/src/config.ts +278 -0
  110. package/coding/src/hard-deny.ts +97 -0
  111. package/coding/src/index.ts +318 -0
  112. package/coding/src/principal-guard.ts +84 -0
  113. package/coding/src/sandbox.ts +92 -0
  114. package/coding/src/security/approval.ts +144 -0
  115. package/coding/src/security/command-rules.ts +57 -0
  116. package/coding/src/security/docker-cli.ts +222 -0
  117. package/coding/src/security/docker-sandbox.ts +1205 -0
  118. package/coding/src/security/e2b-sandbox.ts +853 -0
  119. package/coding/src/security/egress/dns-pin.ts +88 -0
  120. package/coding/src/security/egress/index.ts +32 -0
  121. package/coding/src/security/egress/limits.ts +57 -0
  122. package/coding/src/security/egress/policy.ts +137 -0
  123. package/coding/src/security/egress/proxy.ts +599 -0
  124. package/coding/src/security/egress/types.ts +30 -0
  125. package/coding/src/security/index.ts +199 -0
  126. package/coding/src/security/native-sandbox.ts +639 -0
  127. package/coding/src/security/path-containment.ts +55 -0
  128. package/coding/src/security/sandbox-coding-operations.ts +392 -0
  129. package/coding/src/security/sandbox-fs-operations.ts +644 -0
  130. package/coding/src/security/sandbox-limits.ts +147 -0
  131. package/coding/src/security/sandbox-tar.ts +308 -0
  132. package/coding/src/security/sandbox.ts +179 -0
  133. package/coding/src/security/semaphore.ts +55 -0
  134. package/coding/src/targets.ts +110 -0
  135. package/coding/src/workspace/diff.ts +78 -0
  136. package/coding/src/workspace/errors.ts +29 -0
  137. package/coding/src/workspace/index.ts +5 -0
  138. package/coding/src/workspace/merge.ts +151 -0
  139. package/coding/src/workspace/routing.ts +76 -0
  140. package/coding/src/workspace/service.ts +584 -0
  141. package/computer-use/README.md +105 -0
  142. package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
  143. package/computer-use/src/classify.ts +53 -0
  144. package/computer-use/src/command.ts +29 -0
  145. package/computer-use/src/config.ts +69 -0
  146. package/computer-use/src/create.ts +170 -0
  147. package/computer-use/src/devices.ts +247 -0
  148. package/computer-use/src/env.ts +28 -0
  149. package/computer-use/src/index.ts +157 -0
  150. package/computer-use/src/skill.ts +21 -0
  151. package/openapi/README.md +97 -0
  152. package/openapi/src/compile.ts +386 -0
  153. package/openapi/src/config.ts +73 -0
  154. package/openapi/src/errors.ts +16 -0
  155. package/openapi/src/execute.ts +241 -0
  156. package/openapi/src/index.ts +124 -0
  157. package/openapi/src/limits.ts +63 -0
  158. package/openapi/src/specs.ts +86 -0
  159. package/openapi/src/tools.ts +60 -0
  160. package/openapi/src/types.ts +61 -0
  161. package/package.json +171 -4
  162. package/research/README.md +72 -0
  163. package/research/skills/research-report/SKILL.md +12 -0
  164. package/research/skills/research-scoping/SKILL.md +10 -0
  165. package/research/skills/research-synthesis/SKILL.md +10 -0
  166. package/research/skills/source-triage/SKILL.md +10 -0
  167. package/research/src/check.ts +71 -0
  168. package/research/src/config.ts +41 -0
  169. package/research/src/index.ts +147 -0
  170. package/research/src/ledger.ts +157 -0
  171. package/research/src/urls.ts +21 -0
  172. package/web/README.md +287 -0
  173. package/web/skills/web-research/SKILL.md +17 -0
  174. package/web/src/brave.ts +66 -0
  175. package/web/src/cache.ts +151 -0
  176. package/web/src/config.ts +210 -0
  177. package/web/src/controls.ts +128 -0
  178. package/web/src/domains.ts +114 -0
  179. package/web/src/exa.ts +60 -0
  180. package/web/src/firecrawl.ts +160 -0
  181. package/web/src/index.ts +387 -0
  182. package/web/src/kagi.ts +73 -0
  183. package/web/src/limits.ts +55 -0
  184. package/web/src/native/convert.ts +84 -0
  185. package/web/src/native/fetch.ts +226 -0
  186. package/web/src/normalize.ts +131 -0
  187. package/web/src/obscura/cdp.ts +201 -0
  188. package/web/src/obscura/classify.ts +35 -0
  189. package/web/src/obscura/cli.ts +254 -0
  190. package/web/src/obscura/errors.ts +8 -0
  191. package/web/src/obscura/index.ts +59 -0
  192. package/web/src/obscura/limits.ts +92 -0
  193. package/web/src/obscura/mcp.ts +104 -0
  194. package/web/src/obscura/playwright.ts +133 -0
  195. package/web/src/obscura/process.ts +235 -0
  196. package/web/src/obscura/search-profile.ts +73 -0
  197. package/web/src/obscura/types.ts +48 -0
  198. package/web/src/obscura/web.ts +419 -0
  199. package/web/src/prompt-fetch.ts +88 -0
  200. package/web/src/render.ts +75 -0
  201. package/web/src/searxng.ts +78 -0
  202. package/web/src/slice.ts +139 -0
  203. package/web/src/tavily.ts +77 -0
  204. package/web/src/tools.ts +182 -0
  205. package/web/src/transport.ts +310 -0
  206. package/web/src/types.ts +199 -0
  207. package/work/README.md +77 -0
  208. package/work/THIRD_PARTY_NOTICES +24 -0
  209. package/work/docling/ocr.py +150 -0
  210. package/work/sandbox/Dockerfile +36 -0
  211. package/work/sandbox/soffice.sh +12 -0
  212. package/work/src/command.ts +29 -0
  213. package/work/src/config.ts +138 -0
  214. package/work/src/connectors/cli.ts +343 -0
  215. package/work/src/connectors/drafts.ts +647 -0
  216. package/work/src/connectors/errors.ts +8 -0
  217. package/work/src/connectors/file-bytes.ts +149 -0
  218. package/work/src/connectors/google-workspace-http.ts +399 -0
  219. package/work/src/connectors/google-workspace.ts +465 -0
  220. package/work/src/connectors/gws-tools.ts +378 -0
  221. package/work/src/connectors/http.ts +123 -0
  222. package/work/src/connectors/idempotency.ts +235 -0
  223. package/work/src/connectors/identity.ts +34 -0
  224. package/work/src/connectors/index.ts +93 -0
  225. package/work/src/connectors/limits.ts +50 -0
  226. package/work/src/connectors/m365-tools.ts +308 -0
  227. package/work/src/connectors/microsoft365-http.ts +428 -0
  228. package/work/src/connectors/microsoft365.ts +402 -0
  229. package/work/src/connectors/normalize.ts +120 -0
  230. package/work/src/connectors/tool-helpers.ts +398 -0
  231. package/work/src/connectors/tools.ts +42 -0
  232. package/work/src/connectors/types.ts +399 -0
  233. package/work/src/diagrams/canonicalize.ts +166 -0
  234. package/work/src/diagrams/caps.ts +52 -0
  235. package/work/src/diagrams/embed.ts +340 -0
  236. package/work/src/diagrams/errors.ts +76 -0
  237. package/work/src/diagrams/index.ts +81 -0
  238. package/work/src/diagrams/messages.ts +150 -0
  239. package/work/src/diagrams/origin.ts +52 -0
  240. package/work/src/diagrams/telemetry.ts +34 -0
  241. package/work/src/diagrams/xml.ts +267 -0
  242. package/work/src/document-extraction/index.ts +563 -0
  243. package/work/src/document-reader/ambient.d.ts +2 -0
  244. package/work/src/document-reader/errors.ts +7 -0
  245. package/work/src/document-reader/index.ts +181 -0
  246. package/work/src/document-reader/mistral-ocr.ts +318 -0
  247. package/work/src/document-reader/parsers.ts +171 -0
  248. package/work/src/documents/caps.ts +135 -0
  249. package/work/src/documents/diff.ts +269 -0
  250. package/work/src/documents/errors.ts +44 -0
  251. package/work/src/documents/fidelity.ts +98 -0
  252. package/work/src/documents/generate.ts +96 -0
  253. package/work/src/documents/hash.ts +8 -0
  254. package/work/src/documents/index.ts +14 -0
  255. package/work/src/documents/model-schema.ts +639 -0
  256. package/work/src/documents/parse.ts +252 -0
  257. package/work/src/documents/patch-history.ts +81 -0
  258. package/work/src/documents/patch.ts +476 -0
  259. package/work/src/documents/preview-html.ts +245 -0
  260. package/work/src/documents/preview.ts +227 -0
  261. package/work/src/documents/telemetry.ts +36 -0
  262. package/work/src/documents/translate/docx.ts +292 -0
  263. package/work/src/documents/translate/pptx.ts +304 -0
  264. package/work/src/documents/translate/xlsx.ts +137 -0
  265. package/work/src/documents/types.ts +188 -0
  266. package/work/src/index.ts +311 -0
  267. package/work/src/runtime/worker-pool-entry.ts +102 -0
  268. package/work/src/runtime/worker-pool.ts +267 -0
  269. package/work/src/sandbox/composition.ts +212 -0
  270. package/work/src/sandbox/image.ts +6 -0
  271. package/work/src/sandbox/index.ts +14 -0
  272. package/work/src/sheets/caps.ts +68 -0
  273. package/work/src/sheets/csv.ts +351 -0
  274. package/work/src/sheets/decimal.ts +388 -0
  275. package/work/src/sheets/errors.ts +52 -0
  276. package/work/src/sheets/index.ts +62 -0
  277. package/work/src/sheets/inference.ts +378 -0
  278. package/work/src/sheets/telemetry.ts +34 -0
  279. package/work/src/sheets/types.ts +162 -0
  280. package/work/src/sheets/xlsx.ts +388 -0
  281. package/work/src/skills/index.ts +8 -0
  282. package/work/src/skills/load.ts +43 -0
  283. package/work/src/tools/filesystem.ts +21 -0
  284. package/work/src/tools/index.ts +1 -0
  285. package/work/src/tools/local-filesystem.ts +33 -0
  286. package/work/src/tools/office.ts +311 -0
  287. package/work/vendor/hermes-agent/LICENSE +21 -0
  288. package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
  289. package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
  290. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
  291. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
  292. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
  293. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
  294. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
  295. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
  296. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
  297. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
  298. package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
  299. package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
  300. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
  301. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
  302. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
  303. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
  304. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
  305. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
  306. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
  307. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
  308. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
  309. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
  310. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
  311. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
  312. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
  313. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
  314. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
  315. package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
  316. package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
  317. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
  318. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
  319. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
  320. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
  321. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
  322. package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
  323. package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
  324. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
  325. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
  326. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
  327. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
  328. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
  329. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
  330. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
  331. package/work/vendor-lock.json +11 -0
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env python3
2
+ """Create a PDF from a JSON spec using reportlab platypus.
3
+
4
+ Spec format (UTF-8 JSON):
5
+ {
6
+ "title": "Example Report",
7
+ "author": "example-author",
8
+ "page_size": "A4", // or "letter" (default: A4)
9
+ "page_numbers": true, // default true
10
+ "elements": [
11
+ {"type": "heading", "text": "Section 1", "level": 1},
12
+ {"type": "paragraph", "text": "Body text..."},
13
+ {"type": "table", "rows": [["H1", "H2"], ["a", "b"]], "header": true},
14
+ {"type": "image", "path": "chart.png", "width": 400},
15
+ {"type": "pagebreak"}
16
+ ]
17
+ }
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import sys
24
+
25
+
26
+ def _reconfigure_stdio() -> None:
27
+ for stream in (sys.stdout, sys.stderr):
28
+ try:
29
+ stream.reconfigure(encoding="utf-8")
30
+ except Exception:
31
+ pass
32
+
33
+
34
+ def build_pdf(spec: dict, out_path: str) -> int:
35
+ try:
36
+ from reportlab.lib import colors
37
+ from reportlab.lib.pagesizes import A4, letter
38
+ from reportlab.lib.styles import getSampleStyleSheet
39
+ from reportlab.lib.units import inch
40
+ from reportlab.platypus import (
41
+ Image,
42
+ PageBreak,
43
+ Paragraph,
44
+ SimpleDocTemplate,
45
+ Spacer,
46
+ Table,
47
+ TableStyle,
48
+ )
49
+ except ImportError:
50
+ print("Missing dependency: install with 'python3 -m pip install reportlab'", file=sys.stderr)
51
+ return 2
52
+
53
+ page_size = letter if str(spec.get("page_size", "A4")).lower() == "letter" else A4
54
+ styles = getSampleStyleSheet()
55
+ story = []
56
+ for el in spec.get("elements", []):
57
+ etype = el.get("type")
58
+ if etype == "heading":
59
+ level = min(max(int(el.get("level", 1)), 1), 3)
60
+ story.append(Paragraph(el.get("text", ""), styles[f"Heading{level}"]))
61
+ elif etype == "paragraph":
62
+ story.append(Paragraph(el.get("text", ""), styles["BodyText"]))
63
+ story.append(Spacer(1, 6))
64
+ elif etype == "table":
65
+ rows = el.get("rows", [])
66
+ if not rows:
67
+ continue
68
+ table = Table(rows, repeatRows=1 if el.get("header", True) else 0)
69
+ style = [
70
+ ("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
71
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
72
+ ]
73
+ if el.get("header", True):
74
+ style += [
75
+ ("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
76
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
77
+ ]
78
+ table.setStyle(TableStyle(style))
79
+ story.append(table)
80
+ story.append(Spacer(1, 10))
81
+ elif etype == "image":
82
+ kwargs = {}
83
+ if el.get("width"):
84
+ kwargs["width"] = float(el["width"])
85
+ if el.get("height"):
86
+ kwargs["height"] = float(el["height"])
87
+ img = Image(el["path"], **kwargs)
88
+ if "width" in kwargs and "height" not in kwargs:
89
+ # keep aspect ratio
90
+ ratio = img.imageHeight / img.imageWidth
91
+ img.drawWidth = kwargs["width"]
92
+ img.drawHeight = kwargs["width"] * ratio
93
+ story.append(img)
94
+ story.append(Spacer(1, 10))
95
+ elif etype == "pagebreak":
96
+ story.append(PageBreak())
97
+ else:
98
+ print(f"Warning: unknown element type {etype!r}, skipped", file=sys.stderr)
99
+
100
+ def draw_page_number(canvas, doc):
101
+ if spec.get("page_numbers", True):
102
+ canvas.saveState()
103
+ canvas.setFont("Helvetica", 9)
104
+ canvas.drawCentredString(page_size[0] / 2.0, 0.5 * inch, f"Page {doc.page}")
105
+ canvas.restoreState()
106
+
107
+ doc = SimpleDocTemplate(
108
+ out_path,
109
+ pagesize=page_size,
110
+ title=spec.get("title", ""),
111
+ author=spec.get("author", ""),
112
+ )
113
+ doc.build(story, onFirstPage=draw_page_number, onLaterPages=draw_page_number)
114
+ print(json.dumps({"output": out_path, "elements": len(spec.get("elements", []))}))
115
+ return 0
116
+
117
+
118
+ def main() -> int:
119
+ _reconfigure_stdio()
120
+ parser = argparse.ArgumentParser(description="Create a PDF from a JSON spec (reportlab).")
121
+ parser.add_argument("spec", help="Path to UTF-8 JSON spec file")
122
+ parser.add_argument("-o", "--output", required=True, help="Output PDF path")
123
+ args = parser.parse_args()
124
+ with open(args.spec, encoding="utf-8") as fh:
125
+ spec = json.load(fh)
126
+ return build_pdf(spec, args.output)
127
+
128
+
129
+ if __name__ == "__main__":
130
+ sys.exit(main())
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env python3
2
+ """Fill AcroForm fields from a UTF-8 JSON file; optionally flatten.
3
+
4
+ The JSON is a flat object: {"FieldName": "value", "Agree": true, ...}
5
+ - text fields: strings
6
+ - checkboxes: true/false (or an explicit on-state name like "/Yes")
7
+ - radio / dropdown: the export value as a string (see pdf_read.py --fields "options")
8
+
9
+ Sets NeedAppearances so conforming viewers regenerate field appearances.
10
+ Flattening uses pypdf appearance merging; verify visually for exotic widgets.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+
18
+
19
+ def main() -> int:
20
+ for stream in (sys.stdout, sys.stderr):
21
+ try:
22
+ stream.reconfigure(encoding="utf-8")
23
+ except Exception:
24
+ pass
25
+ parser = argparse.ArgumentParser(description="Fill PDF AcroForm fields from JSON (pypdf).")
26
+ parser.add_argument("pdf", help="Input form PDF")
27
+ parser.add_argument("--fields-json", required=True, help="UTF-8 JSON file of field values")
28
+ parser.add_argument("-o", "--output", required=True, help="Output PDF path")
29
+ parser.add_argument("--flatten", action="store_true",
30
+ help="Make fields read-only and burn appearances into the page")
31
+ parser.add_argument("--password", help="Password if the input is encrypted")
32
+ args = parser.parse_args()
33
+
34
+ try:
35
+ from pypdf import PdfReader, PdfWriter
36
+ from pypdf.generic import BooleanObject, NameObject
37
+ except ImportError:
38
+ print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)
39
+ return 2
40
+
41
+ with open(args.fields_json, encoding="utf-8") as fh:
42
+ values = json.load(fh)
43
+
44
+ reader = PdfReader(args.pdf)
45
+ if reader.is_encrypted:
46
+ if args.password is None or not reader.decrypt(args.password):
47
+ print("Error: input is encrypted; pass --password", file=sys.stderr)
48
+ return 3
49
+ available = set((reader.get_fields() or {}).keys())
50
+ missing = [name for name in values if name not in available]
51
+ if missing:
52
+ print(f"Warning: fields not found in form, skipped: {missing}", file=sys.stderr)
53
+
54
+ writer = PdfWriter()
55
+ writer.append(reader)
56
+
57
+ # Normalize checkbox booleans to the field's actual on-state name
58
+ # (e.g. "/Yes"): pypdf does not reliably map bare True to the on-state.
59
+ field_info = reader.get_fields() or {}
60
+ fill = {}
61
+ for name, value in values.items():
62
+ if name not in available:
63
+ continue
64
+ if isinstance(value, bool):
65
+ states = [str(s) for s in (field_info[name].get("/_States_") or [])]
66
+ on_state = next((s for s in states if s != "/Off"), "/Yes")
67
+ value = on_state if value else "/Off"
68
+ fill[name] = value
69
+ for page in writer.pages:
70
+ writer.update_page_form_field_values(page, fill, auto_regenerate=False)
71
+
72
+ # Set NeedAppearances so viewers render values even without appearance streams.
73
+ root = writer._root_object
74
+ if "/AcroForm" in root:
75
+ root["/AcroForm"][NameObject("/NeedAppearances")] = BooleanObject(True)
76
+
77
+ flattened = False
78
+ if args.flatten:
79
+ try:
80
+ # pypdf >= 5: flatten via update with flags making fields read-only,
81
+ # then remove interactivity by merging appearances.
82
+ for page in writer.pages:
83
+ writer.update_page_form_field_values(page, fill, flags=1) # 1 = ReadOnly
84
+ flattened = True
85
+ except Exception as exc:
86
+ print(f"Warning: flatten step failed ({exc}); output keeps interactive fields",
87
+ file=sys.stderr)
88
+
89
+ with open(args.output, "wb") as fh:
90
+ writer.write(fh)
91
+ print(json.dumps({"output": args.output, "filled": sorted(fill), "skipped": missing,
92
+ "flattened": flattened}, ensure_ascii=False))
93
+ return 0
94
+
95
+
96
+ if __name__ == "__main__":
97
+ sys.exit(main())
@@ -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())