@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,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())
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env python3
2
+ """Read a PDF: per-page text, tables, metadata, or form fields. JSON to stdout."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import csv
7
+ import json
8
+ import os
9
+ import sys
10
+
11
+
12
+ def _reconfigure_stdio() -> None:
13
+ for stream in (sys.stdout, sys.stderr):
14
+ try:
15
+ stream.reconfigure(encoding="utf-8")
16
+ except Exception:
17
+ pass
18
+
19
+
20
+ def _need(module: str, package: str):
21
+ try:
22
+ return __import__(module)
23
+ except ImportError:
24
+ print(f"Missing dependency: install with 'python3 -m pip install {package}'", file=sys.stderr)
25
+ raise SystemExit(2)
26
+
27
+
28
+ def read_text(path: str, password: str | None) -> dict:
29
+ pdfplumber = _need("pdfplumber", "pdfplumber")
30
+ pages = []
31
+ with pdfplumber.open(path, password=password) as pdf:
32
+ for page in pdf.pages:
33
+ pages.append(page.extract_text() or "")
34
+ return {"page_count": len(pages), "pages": pages}
35
+
36
+
37
+ def read_tables(path: str, password: str | None, csv_dir: str | None) -> dict:
38
+ pdfplumber = _need("pdfplumber", "pdfplumber")
39
+ result = []
40
+ written = []
41
+ with pdfplumber.open(path, password=password) as pdf:
42
+ for pageno, page in enumerate(pdf.pages, start=1):
43
+ for tidx, table in enumerate(page.extract_tables()):
44
+ result.append({"page": pageno, "index": tidx, "rows": table})
45
+ if csv_dir:
46
+ os.makedirs(csv_dir, exist_ok=True)
47
+ csv_path = os.path.join(csv_dir, f"page{pageno}_table{tidx}.csv")
48
+ with open(csv_path, "w", encoding="utf-8", newline="") as fh:
49
+ csv.writer(fh).writerows([[c if c is not None else "" for c in row] for row in table])
50
+ written.append(csv_path)
51
+ out = {"table_count": len(result), "tables": result}
52
+ if csv_dir:
53
+ out["csv_files"] = written
54
+ return out
55
+
56
+
57
+ def read_meta(path: str, password: str | None) -> dict:
58
+ pypdf = _need("pypdf", "pypdf")
59
+ reader = pypdf.PdfReader(path)
60
+ encrypted = reader.is_encrypted
61
+ if encrypted:
62
+ if password is None or not reader.decrypt(password):
63
+ return {"encrypted": True, "note": "Provide --password to read metadata of an encrypted file."}
64
+ meta = {}
65
+ if reader.metadata:
66
+ for key, value in reader.metadata.items():
67
+ meta[str(key).lstrip("/")] = str(value)
68
+ pages = []
69
+ for idx, page in enumerate(reader.pages, start=1):
70
+ box = page.mediabox
71
+ pages.append({
72
+ "page": idx,
73
+ "width": float(box.width),
74
+ "height": float(box.height),
75
+ "rotation": int(page.get("/Rotate", 0)),
76
+ })
77
+ # scanned-page heuristic: no extractable text but page has images
78
+ likely_scanned = []
79
+ try:
80
+ pdfplumber = _need("pdfplumber", "pdfplumber")
81
+ with pdfplumber.open(path, password=password) as pdf:
82
+ for pageno, page in enumerate(pdf.pages, start=1):
83
+ text = (page.extract_text() or "").strip()
84
+ if not text and page.images:
85
+ likely_scanned.append(pageno)
86
+ except SystemExit:
87
+ raise
88
+ except Exception as exc: # pragma: no cover - heuristic only
89
+ print(f"Warning: scanned-page check failed: {exc}", file=sys.stderr)
90
+ out = {
91
+ "encrypted": encrypted,
92
+ "page_count": len(reader.pages),
93
+ "metadata": meta,
94
+ "pages": pages,
95
+ "likely_scanned_pages": likely_scanned,
96
+ }
97
+ if likely_scanned:
98
+ out["note"] = ("Image-only pages detected: no text layer to extract. "
99
+ "Use the references/ocr-extraction.md in this skill for OCR.")
100
+ return out
101
+
102
+
103
+ FIELD_TYPES = {"/Tx": "text", "/Btn": "button", "/Ch": "choice", "/Sig": "signature"}
104
+
105
+
106
+ def read_fields(path: str, password: str | None) -> dict:
107
+ pypdf = _need("pypdf", "pypdf")
108
+ reader = pypdf.PdfReader(path)
109
+ if reader.is_encrypted:
110
+ if password is None or not reader.decrypt(password):
111
+ print("File is encrypted; pass --password.", file=sys.stderr)
112
+ raise SystemExit(3)
113
+ fields = reader.get_fields() or {}
114
+ out = {}
115
+ for name, field in fields.items():
116
+ ftype = FIELD_TYPES.get(str(field.get("/FT")), str(field.get("/FT")))
117
+ value = field.get("/V")
118
+ states = field.get("/_States_")
119
+ entry = {"type": ftype, "value": None if value is None else str(value)}
120
+ if states:
121
+ entry["options"] = [str(s) for s in states]
122
+ out[name] = entry
123
+ return {"field_count": len(out), "fields": out}
124
+
125
+
126
+ def main() -> int:
127
+ _reconfigure_stdio()
128
+ parser = argparse.ArgumentParser(description="Extract text, tables, metadata, or form fields from a PDF.")
129
+ parser.add_argument("pdf", help="Input PDF path")
130
+ mode = parser.add_mutually_exclusive_group(required=True)
131
+ mode.add_argument("--text", action="store_true", help="Per-page text as JSON")
132
+ mode.add_argument("--tables", action="store_true", help="Tables as JSON (optionally CSV via --csv-dir)")
133
+ mode.add_argument("--meta", action="store_true", help="Metadata, page sizes, encrypted/scanned flags")
134
+ mode.add_argument("--fields", action="store_true", help="AcroForm fields with types and values")
135
+ parser.add_argument("--csv-dir", help="Also write each table as a CSV file into this directory")
136
+ parser.add_argument("--password", help="Password for encrypted PDFs")
137
+ args = parser.parse_args()
138
+
139
+ if args.text:
140
+ result = read_text(args.pdf, args.password)
141
+ elif args.tables:
142
+ result = read_tables(args.pdf, args.password, args.csv_dir)
143
+ elif args.meta:
144
+ result = read_meta(args.pdf, args.password)
145
+ else:
146
+ result = read_fields(args.pdf, args.password)
147
+ json.dump(result, sys.stdout, ensure_ascii=False, indent=2)
148
+ print()
149
+ return 0
150
+
151
+
152
+ if __name__ == "__main__":
153
+ sys.exit(main())
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env python3
2
+ """Encrypt or decrypt a PDF with passwords (AES-256 via pypdf).
3
+
4
+ Note: permission flags set at encryption time are advisory — viewers may honor
5
+ them, but any PDF library can strip them. Only the user password gates content.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ import sys
12
+
13
+
14
+ def main() -> int:
15
+ for stream in (sys.stdout, sys.stderr):
16
+ try:
17
+ stream.reconfigure(encoding="utf-8")
18
+ except Exception:
19
+ pass
20
+ parser = argparse.ArgumentParser(description="Encrypt/decrypt PDFs (pypdf, AES-256).")
21
+ parser.add_argument("pdf", help="Input PDF path")
22
+ parser.add_argument("-o", "--output", required=True, help="Output PDF path")
23
+ mode = parser.add_mutually_exclusive_group(required=True)
24
+ mode.add_argument("--encrypt", action="store_true", help="Encrypt the PDF")
25
+ mode.add_argument("--decrypt", action="store_true", help="Remove encryption (password required)")
26
+ parser.add_argument("--user-password", help="User (open) password for --encrypt")
27
+ parser.add_argument("--owner-password", help="Owner password for --encrypt (defaults to user password)")
28
+ parser.add_argument("--password", help="Known password for --decrypt")
29
+ args = parser.parse_args()
30
+
31
+ try:
32
+ from pypdf import PdfReader, PdfWriter
33
+ except ImportError:
34
+ print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)
35
+ return 2
36
+
37
+ reader = PdfReader(args.pdf)
38
+ if args.encrypt:
39
+ if not args.user_password:
40
+ print("Error: --encrypt requires --user-password", file=sys.stderr)
41
+ return 2
42
+ if reader.is_encrypted:
43
+ print("Error: input already encrypted; decrypt first", file=sys.stderr)
44
+ return 3
45
+ writer = PdfWriter()
46
+ writer.append(reader)
47
+ writer.encrypt(
48
+ user_password=args.user_password,
49
+ owner_password=args.owner_password or args.user_password,
50
+ algorithm="AES-256",
51
+ )
52
+ action = "encrypted"
53
+ else:
54
+ if not reader.is_encrypted:
55
+ print("Error: input is not encrypted", file=sys.stderr)
56
+ return 3
57
+ if args.password is None or not reader.decrypt(args.password):
58
+ print("Error: wrong or missing --password", file=sys.stderr)
59
+ return 4
60
+ writer = PdfWriter()
61
+ writer.append(reader)
62
+ action = "decrypted"
63
+
64
+ with open(args.output, "wb") as fh:
65
+ writer.write(fh)
66
+ print(json.dumps({"output": args.output, "action": action, "page_count": len(reader.pages)}))
67
+ return 0
68
+
69
+
70
+ if __name__ == "__main__":
71
+ sys.exit(main())
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env python3
2
+ """Extract page ranges from a PDF, optionally rotating and/or compressing pages."""
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+
9
+
10
+ def parse_pages(spec: str, page_count: int) -> list[int]:
11
+ """Parse a 1-based page spec like '1-3,5,9-' into 0-based indices."""
12
+ indices: list[int] = []
13
+ for part in spec.split(","):
14
+ part = part.strip()
15
+ if not part:
16
+ continue
17
+ if "-" in part:
18
+ start_s, _, end_s = part.partition("-")
19
+ start = int(start_s) if start_s else 1
20
+ end = int(end_s) if end_s else page_count
21
+ else:
22
+ start = end = int(part)
23
+ if start < 1 or end > page_count or start > end:
24
+ raise ValueError(f"Page range {part!r} out of bounds (1-{page_count})")
25
+ indices.extend(range(start - 1, end))
26
+ return indices
27
+
28
+
29
+ def main() -> int:
30
+ for stream in (sys.stdout, sys.stderr):
31
+ try:
32
+ stream.reconfigure(encoding="utf-8")
33
+ except Exception:
34
+ pass
35
+ parser = argparse.ArgumentParser(
36
+ description="Split/extract pages from a PDF (pypdf). Pages are 1-based: '1-3,5,9-'.")
37
+ parser.add_argument("pdf", help="Input PDF path")
38
+ parser.add_argument("--pages", required=True, help="1-based page spec, e.g. '1-3,5,9-'")
39
+ parser.add_argument("-o", "--output", required=True, help="Output PDF path")
40
+ parser.add_argument("--rotate", type=int, default=0,
41
+ help="Rotate extracted pages clockwise (multiple of 90)")
42
+ parser.add_argument("--compress", action="store_true",
43
+ help="Deflate content streams (modest savings; does not recompress images)")
44
+ parser.add_argument("--password", help="Password if the input is encrypted")
45
+ args = parser.parse_args()
46
+
47
+ if args.rotate % 90 != 0:
48
+ print("Error: --rotate must be a multiple of 90", file=sys.stderr)
49
+ return 2
50
+
51
+ try:
52
+ from pypdf import PdfReader, PdfWriter
53
+ except ImportError:
54
+ print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)
55
+ return 2
56
+
57
+ reader = PdfReader(args.pdf)
58
+ if reader.is_encrypted:
59
+ if args.password is None or not reader.decrypt(args.password):
60
+ print("Error: input is encrypted; pass --password", file=sys.stderr)
61
+ return 3
62
+ try:
63
+ indices = parse_pages(args.pages, len(reader.pages))
64
+ except ValueError as exc:
65
+ print(f"Error: {exc}", file=sys.stderr)
66
+ return 2
67
+
68
+ writer = PdfWriter()
69
+ for idx in indices:
70
+ page = reader.pages[idx]
71
+ if args.rotate:
72
+ page.rotate(args.rotate)
73
+ writer.add_page(page)
74
+ if args.compress:
75
+ for page in writer.pages:
76
+ page.compress_content_streams()
77
+ with open(args.output, "wb") as fh:
78
+ writer.write(fh)
79
+ print(json.dumps({"output": args.output, "page_count": len(indices), "rotated": args.rotate}))
80
+ return 0
81
+
82
+
83
+ if __name__ == "__main__":
84
+ sys.exit(main())
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env python3
2
+ """Stamp text or an image at coordinates onto selected PDF pages.
3
+
4
+ Builds an in-memory single-page overlay with reportlab, then merges it
5
+ onto each selected page with pypdf. Coordinates are PDF points, origin
6
+ bottom-left. Covers 'sign here' arrows, diagonal DRAFT banners, and
7
+ page-corner labels.
8
+
9
+ Examples:
10
+ pdf_stamp.py in.pdf -o out.pdf --text "DRAFT" --x 200 --y 400 \
11
+ --font-size 60 --rotation 45 --opacity 0.3 --color "#cc0000"
12
+ pdf_stamp.py in.pdf -o out.pdf --image sig.png --x 400 --y 60 \
13
+ --width 120 --pages 3
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import io
19
+ import json
20
+ import sys
21
+
22
+
23
+ def parse_pages(spec: str, page_count: int) -> list[int]:
24
+ pages: set[int] = set()
25
+ for part in spec.split(","):
26
+ part = part.strip()
27
+ if not part:
28
+ continue
29
+ if "-" in part:
30
+ start_s, _, end_s = part.partition("-")
31
+ start = int(start_s) if start_s else 1
32
+ end = int(end_s) if end_s else page_count
33
+ pages.update(range(start, end + 1))
34
+ else:
35
+ pages.add(int(part))
36
+ bad = [p for p in pages if not 1 <= p <= page_count]
37
+ if bad:
38
+ raise ValueError(f"pages out of range 1-{page_count}: {sorted(bad)}")
39
+ return sorted(pages)
40
+
41
+
42
+ def build_overlay(args, page_width: float, page_height: float) -> bytes:
43
+ from reportlab.lib.colors import HexColor
44
+ from reportlab.pdfgen import canvas
45
+ buf = io.BytesIO()
46
+ c = canvas.Canvas(buf, pagesize=(page_width, page_height))
47
+ c.saveState()
48
+ try:
49
+ c.setFillAlpha(float(args.opacity))
50
+ c.setStrokeAlpha(float(args.opacity))
51
+ except Exception:
52
+ pass # very old reportlab: no alpha support
53
+ c.translate(float(args.x), float(args.y))
54
+ if args.rotation:
55
+ c.rotate(float(args.rotation))
56
+ if args.text:
57
+ c.setFont(args.font, float(args.font_size))
58
+ c.setFillColor(HexColor(args.color))
59
+ c.drawString(0, 0, args.text)
60
+ else:
61
+ kwargs = {}
62
+ if args.width:
63
+ kwargs["width"] = float(args.width)
64
+ if args.height:
65
+ kwargs["height"] = float(args.height)
66
+ if "width" in kwargs and "height" not in kwargs:
67
+ from PIL import Image as PILImage
68
+ with PILImage.open(args.image) as im:
69
+ kwargs["height"] = kwargs["width"] * im.height / im.width
70
+ c.drawImage(args.image, 0, 0, mask="auto", **kwargs)
71
+ c.restoreState()
72
+ c.save()
73
+ return buf.getvalue()
74
+
75
+
76
+ def main() -> int:
77
+ for stream in (sys.stdout, sys.stderr):
78
+ try:
79
+ stream.reconfigure(encoding="utf-8")
80
+ except Exception:
81
+ pass
82
+ parser = argparse.ArgumentParser(description="Stamp text or an image onto PDF pages.")
83
+ parser.add_argument("pdf", help="Input PDF path")
84
+ parser.add_argument("-o", "--output", required=True, help="Output PDF path")
85
+ what = parser.add_mutually_exclusive_group(required=True)
86
+ what.add_argument("--text", help="Text to stamp")
87
+ what.add_argument("--image", help="Image file to stamp (PNG/JPEG)")
88
+ parser.add_argument("--x", type=float, required=True, help="X in points (origin bottom-left)")
89
+ parser.add_argument("--y", type=float, required=True, help="Y in points")
90
+ parser.add_argument("--pages", default="1-", help="1-based ranges, e.g. '1-3,5' (default: all)")
91
+ parser.add_argument("--font", default="Helvetica", help="Font name (default Helvetica)")
92
+ parser.add_argument("--font-size", type=float, default=24, help="Font size in points")
93
+ parser.add_argument("--color", default="#000000", help="Text color as #RRGGBB")
94
+ parser.add_argument("--rotation", type=float, default=0, help="Degrees counterclockwise")
95
+ parser.add_argument("--opacity", type=float, default=1.0, help="0.0-1.0 (default 1.0)")
96
+ parser.add_argument("--width", type=float, help="Image width in points")
97
+ parser.add_argument("--height", type=float, help="Image height in points")
98
+ parser.add_argument("--under", action="store_true",
99
+ help="Place the stamp under existing content instead of over it")
100
+ parser.add_argument("--password", help="Password if the input is encrypted")
101
+ args = parser.parse_args()
102
+
103
+ try:
104
+ from pypdf import PdfReader, PdfWriter
105
+ except ImportError:
106
+ print("Missing dependency: install with 'python3 -m pip install pypdf reportlab'",
107
+ file=sys.stderr)
108
+ return 2
109
+
110
+ reader = PdfReader(args.pdf)
111
+ if reader.is_encrypted:
112
+ if args.password is None or not reader.decrypt(args.password):
113
+ print("Error: input is encrypted; pass --password", file=sys.stderr)
114
+ return 3
115
+ try:
116
+ pages = set(parse_pages(args.pages, len(reader.pages)))
117
+ except ValueError as exc:
118
+ print(f"Error: {exc}", file=sys.stderr)
119
+ return 4
120
+
121
+ writer = PdfWriter()
122
+ overlay_cache: dict[tuple[float, float], object] = {}
123
+ for idx, page in enumerate(reader.pages, start=1):
124
+ if idx in pages:
125
+ size = (float(page.mediabox.width), float(page.mediabox.height))
126
+ if size not in overlay_cache:
127
+ overlay_pdf = PdfReader(io.BytesIO(build_overlay(args, *size)))
128
+ overlay_cache[size] = overlay_pdf.pages[0]
129
+ stamp = overlay_cache[size]
130
+ if args.under:
131
+ page.merge_page(stamp, over=False)
132
+ else:
133
+ page.merge_page(stamp)
134
+ writer.add_page(page)
135
+ with open(args.output, "wb") as fh:
136
+ writer.write(fh)
137
+ print(json.dumps({"output": args.output, "stamped_pages": sorted(pages),
138
+ "kind": "text" if args.text else "image"}, ensure_ascii=False))
139
+ return 0
140
+
141
+
142
+ if __name__ == "__main__":
143
+ sys.exit(main())