dsh-office-kit 0.1.0 → 0.3.0

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 (198) hide show
  1. package/CAPABILITY.md +147 -68
  2. package/LICENSE.md +21 -21
  3. package/README.md +108 -106
  4. package/assets/skills/excel-live-control/SKILL.md +234 -0
  5. package/assets/skills/excel-live-control/container_tools/live_excel.py +1589 -0
  6. package/assets/skills/excel-live-control/domain_guidance/financial_models.md +86 -0
  7. package/assets/skills/excel-live-control/domain_guidance/healthcare.md +58 -0
  8. package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +53 -0
  9. package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +53 -0
  10. package/assets/skills/excel-live-control/references/office-automation.md +207 -0
  11. package/assets/skills/excel-live-control/references/read_only_qna.md +59 -0
  12. package/assets/skills/excel-live-control/style_guidelines.md +87 -0
  13. package/assets/skills/excel-live-control/workflows/create_workflows.md +78 -0
  14. package/assets/skills/excel-live-control/workflows/edit_workflows.md +72 -0
  15. package/assets/skills/office-documents/SKILL.md +90 -77
  16. package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +86 -0
  17. package/assets/skills/office-documents/ooxml/comments.md +40 -0
  18. package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +66 -0
  19. package/assets/skills/office-documents/ooxml/rels_and_content_types.md +62 -0
  20. package/assets/skills/office-documents/ooxml/tracked_changes.md +53 -0
  21. package/assets/skills/office-documents/references/docx-structure.md +83 -0
  22. package/assets/skills/office-documents/references/template-elicitation.md +49 -0
  23. package/assets/skills/office-documents/references/writing-quality.md +49 -0
  24. package/assets/skills/office-documents/render_docx.py +92 -0
  25. package/assets/skills/office-documents/scripts/a11y_audit.py +194 -0
  26. package/assets/skills/office-documents/scripts/accept_tracked_changes.py +151 -0
  27. package/assets/skills/office-documents/scripts/add_tracked_replacements.py +148 -0
  28. package/assets/skills/office-documents/scripts/append_blocks.py +108 -0
  29. package/assets/skills/office-documents/scripts/apply_template_styles.py +104 -0
  30. package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +102 -0
  31. package/assets/skills/office-documents/scripts/comments_add.py +136 -0
  32. package/assets/skills/office-documents/scripts/comments_apply_patch.py +114 -0
  33. package/assets/skills/office-documents/scripts/comments_extract.py +85 -0
  34. package/assets/skills/office-documents/scripts/comments_strip.py +74 -0
  35. package/assets/skills/office-documents/scripts/content_controls.py +192 -0
  36. package/assets/skills/office-documents/scripts/core_properties.py +124 -0
  37. package/assets/skills/office-documents/scripts/docx_kit.py +587 -0
  38. package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +225 -0
  39. package/assets/skills/office-documents/scripts/docx_table_to_csv.py +99 -0
  40. package/assets/skills/office-documents/scripts/fields_materialize.py +66 -0
  41. package/assets/skills/office-documents/scripts/fields_report.py +59 -0
  42. package/assets/skills/office-documents/scripts/flatten_ref_fields.py +62 -0
  43. package/assets/skills/office-documents/scripts/footnotes_report.py +77 -0
  44. package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +96 -0
  45. package/assets/skills/office-documents/scripts/heading_audit.py +81 -0
  46. package/assets/skills/office-documents/scripts/images_audit.py +80 -0
  47. package/assets/skills/office-documents/scripts/insert_note.py +126 -0
  48. package/assets/skills/office-documents/scripts/insert_ref_fields.py +90 -0
  49. package/assets/skills/office-documents/scripts/insert_toc.py +84 -0
  50. package/assets/skills/office-documents/scripts/internal_nav.py +146 -0
  51. package/assets/skills/office-documents/scripts/make_fixtures.py +169 -0
  52. package/assets/skills/office-documents/scripts/merge_docx_append.py +239 -0
  53. package/assets/skills/office-documents/scripts/privacy_scrub.py +131 -0
  54. package/assets/skills/office-documents/scripts/redact_docx.py +113 -0
  55. package/assets/skills/office-documents/scripts/render_and_diff.py +101 -0
  56. package/assets/skills/office-documents/scripts/replace_text.py +154 -0
  57. package/assets/skills/office-documents/scripts/section_audit.py +91 -0
  58. package/assets/skills/office-documents/scripts/set_protection.py +92 -0
  59. package/assets/skills/office-documents/scripts/style_lint.py +111 -0
  60. package/assets/skills/office-documents/scripts/style_normalize.py +103 -0
  61. package/assets/skills/office-documents/scripts/table_geometry.py +167 -0
  62. package/assets/skills/office-documents/scripts/watermark_add.py +48 -0
  63. package/assets/skills/office-documents/scripts/watermark_audit_remove.py +67 -0
  64. package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +117 -0
  65. package/assets/skills/office-documents/tasks/accessibility_a11y.md +45 -0
  66. package/assets/skills/office-documents/tasks/captions_crossrefs.md +49 -0
  67. package/assets/skills/office-documents/tasks/clean_tracked_changes.md +49 -0
  68. package/assets/skills/office-documents/tasks/comments_manage.md +59 -0
  69. package/assets/skills/office-documents/tasks/compare_diff.md +40 -0
  70. package/assets/skills/office-documents/tasks/create_edit.md +107 -0
  71. package/assets/skills/office-documents/tasks/equations.md +59 -0
  72. package/assets/skills/office-documents/tasks/fields_update.md +44 -0
  73. package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +50 -0
  74. package/assets/skills/office-documents/tasks/footnotes_endnotes.md +38 -0
  75. package/assets/skills/office-documents/tasks/forms_content_controls.md +48 -0
  76. package/assets/skills/office-documents/tasks/headings_numbering.md +55 -0
  77. package/assets/skills/office-documents/tasks/images_figures.md +63 -0
  78. package/assets/skills/office-documents/tasks/multi_doc_merge.md +43 -0
  79. package/assets/skills/office-documents/tasks/navigation_internal_links.md +38 -0
  80. package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +45 -0
  81. package/assets/skills/office-documents/tasks/protection_restrict_editing.md +41 -0
  82. package/assets/skills/office-documents/tasks/read_review.md +50 -0
  83. package/assets/skills/office-documents/tasks/redaction_anonymization.md +42 -0
  84. package/assets/skills/office-documents/tasks/sections_layout.md +54 -0
  85. package/assets/skills/office-documents/tasks/style_lint_normalize.md +44 -0
  86. package/assets/skills/office-documents/tasks/tables_spreadsheets.md +64 -0
  87. package/assets/skills/office-documents/tasks/templates_style_packs.md +53 -0
  88. package/assets/skills/office-documents/tasks/toc_workflow.md +39 -0
  89. package/assets/skills/office-documents/tasks/verify_render.md +50 -0
  90. package/assets/skills/office-documents/tasks/watermarks_background.md +35 -0
  91. package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +47 -0
  92. package/assets/skills/office-documents/troubleshooting/run_splitting.md +36 -0
  93. package/assets/skills/office-pdf/SKILL.md +73 -56
  94. package/assets/skills/office-pdf/references/form-fields.md +56 -0
  95. package/assets/skills/office-pdf/references/geometry.md +44 -0
  96. package/assets/skills/office-pdf/references/layout-quality.md +41 -0
  97. package/assets/skills/office-pdf/references/limits.md +58 -0
  98. package/assets/skills/office-pdf/scripts/extract_text.py +172 -0
  99. package/assets/skills/office-pdf/scripts/forms.py +556 -0
  100. package/assets/skills/office-pdf/scripts/metadata.py +161 -0
  101. package/assets/skills/office-pdf/scripts/page_ops.py +198 -0
  102. package/assets/skills/office-pdf/scripts/redact_region.py +347 -0
  103. package/assets/skills/office-pdf/scripts/render_pages.py +207 -0
  104. package/assets/skills/office-pdf/scripts/split_pages.py +155 -0
  105. package/assets/skills/office-pdf/scripts/stamp_overlay.py +199 -0
  106. package/assets/skills/office-pdf/tasks/annotations.md +98 -0
  107. package/assets/skills/office-pdf/tasks/create.md +75 -0
  108. package/assets/skills/office-pdf/tasks/edit-pages.md +71 -0
  109. package/assets/skills/office-pdf/tasks/forms.md +67 -0
  110. package/assets/skills/office-pdf/tasks/read.md +62 -0
  111. package/assets/skills/office-pdf/tasks/redact.md +72 -0
  112. package/assets/skills/office-pdf/tasks/stamp.md +60 -0
  113. package/assets/skills/office-presentations/SKILL.md +141 -87
  114. package/assets/skills/office-presentations/container_tools/add_slides.py +136 -0
  115. package/assets/skills/office-presentations/container_tools/core_properties.py +120 -0
  116. package/assets/skills/office-presentations/container_tools/create_montage.py +126 -0
  117. package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +285 -0
  118. package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +157 -0
  119. package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +144 -0
  120. package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +199 -0
  121. package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +212 -0
  122. package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +345 -0
  123. package/assets/skills/office-presentations/container_tools/preview_render.py +375 -0
  124. package/assets/skills/office-presentations/container_tools/render_slides.py +139 -0
  125. package/assets/skills/office-presentations/container_tools/replace_text.py +168 -0
  126. package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +229 -0
  127. package/assets/skills/office-presentations/domain_guidance/finance.md +66 -0
  128. package/assets/skills/office-presentations/recipes/README.md +71 -0
  129. package/assets/skills/office-presentations/recipes/autolayout.md +88 -0
  130. package/assets/skills/office-presentations/recipes/connectors.md +83 -0
  131. package/assets/skills/office-presentations/recipes/images.md +80 -0
  132. package/assets/skills/office-presentations/recipes/master-and-layouts.md +127 -0
  133. package/assets/skills/office-presentations/recipes/native-charts.md +87 -0
  134. package/assets/skills/office-presentations/recipes/rich-text-runs.md +86 -0
  135. package/assets/skills/office-presentations/recipes/speaker-notes.md +65 -0
  136. package/assets/skills/office-presentations/recipes/tables.md +88 -0
  137. package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +71 -0
  138. package/assets/skills/office-presentations/recipes/theme-tokens.md +87 -0
  139. package/assets/skills/office-presentations/references/authoring-workflow.md +65 -0
  140. package/assets/skills/office-presentations/references/finalization.md +63 -0
  141. package/assets/skills/office-presentations/references/native-bullets.md +78 -0
  142. package/assets/skills/office-presentations/references/native-evidence.md +67 -0
  143. package/assets/skills/office-presentations/references/style-guidelines.md +66 -0
  144. package/assets/skills/office-presentations/references/templates.md +76 -0
  145. package/assets/skills/office-presentations/routing/google_slides.md +45 -0
  146. package/assets/skills/office-presentations/tasks/apply-annotations.md +83 -0
  147. package/assets/skills/office-presentations/tasks/charts-tables-notes.md +79 -0
  148. package/assets/skills/office-presentations/tasks/create-deck.md +106 -0
  149. package/assets/skills/office-presentations/tasks/edit-deck.md +93 -0
  150. package/assets/skills/office-presentations/tasks/inspect-and-verify.md +107 -0
  151. package/assets/skills/office-presentations/tasks/template-following.md +132 -0
  152. package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py +176 -0
  153. package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +154 -0
  154. package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +111 -0
  155. package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +239 -0
  156. package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +185 -0
  157. package/assets/skills/office-spreadsheets/SKILL.md +105 -101
  158. package/assets/skills/office-spreadsheets/container_tools/add_chart.py +236 -0
  159. package/assets/skills/office-spreadsheets/container_tools/add_image.py +107 -0
  160. package/assets/skills/office-spreadsheets/container_tools/append_rows.py +122 -0
  161. package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +306 -0
  162. package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +398 -0
  163. package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +275 -0
  164. package/assets/skills/office-spreadsheets/container_tools/set_cells.py +135 -0
  165. package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +134 -0
  166. package/assets/skills/office-spreadsheets/container_tools/style_cells.py +133 -0
  167. package/assets/skills/office-spreadsheets/recipes/charts.md +66 -0
  168. package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +69 -0
  169. package/assets/skills/office-spreadsheets/recipes/data-tables.md +58 -0
  170. package/assets/skills/office-spreadsheets/recipes/named-ranges.md +41 -0
  171. package/assets/skills/office-spreadsheets/recipes/number-formats.md +72 -0
  172. package/assets/skills/office-spreadsheets/recipes/sparklines.md +43 -0
  173. package/assets/skills/office-spreadsheets/references/charts.md +63 -0
  174. package/assets/skills/office-spreadsheets/references/domain/financial-models.md +40 -0
  175. package/assets/skills/office-spreadsheets/references/domain/healthcare.md +22 -0
  176. package/assets/skills/office-spreadsheets/references/domain/marketing.md +28 -0
  177. package/assets/skills/office-spreadsheets/references/domain/research.md +22 -0
  178. package/assets/skills/office-spreadsheets/references/formula-patterns.md +78 -0
  179. package/assets/skills/office-spreadsheets/references/style-guidelines.md +69 -0
  180. package/assets/skills/office-spreadsheets/references/workbook-structure.md +60 -0
  181. package/assets/skills/office-spreadsheets/routing/google-sheets.md +36 -0
  182. package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +37 -0
  183. package/assets/skills/office-spreadsheets/tasks/annotations.md +51 -0
  184. package/assets/skills/office-spreadsheets/tasks/create-workbook.md +66 -0
  185. package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +38 -0
  186. package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +69 -0
  187. package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +38 -0
  188. package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +57 -0
  189. package/assets/skills/shared/annotation-recipes.md +252 -0
  190. package/assets/skills/shared/check_runtime.py +253 -0
  191. package/assets/skills/shared/common-workflow.md +139 -0
  192. package/assets/skills/shared/scripts/ensure_raster_image.py +187 -0
  193. package/dist/client.js +4 -4
  194. package/dist/client.js.map +1 -1
  195. package/dist/index.js +239 -89
  196. package/package.json +146 -149
  197. package/postinstall.mjs +92 -82
  198. package/python/office_cli.py +171 -246
@@ -1,86 +1,103 @@
1
1
  ---
2
- description: Create, read, edit, merge, rotate, and render PDF files with the bundled Office helper CLI.
2
+ description: Create, inspect, edit, redact, stamp, and render PDF files with the bundled Office helper CLI and self-contained Python scripts.
3
3
  ---
4
4
 
5
5
  # PDF
6
6
 
7
- Create PDFs, extract their content, edit metadata/pages, and rasterize them for visual verification. This skill handles local files only; OCR and cloud PDF services are out of scope.
7
+ A PDF page is a content stream of text-showing and path-painting operators behind a page box. Every job in this pack either reads that stream (PyMuPDF, pdfplumber, pypdf), writes a new one (reportlab), or rasterises the result so you can look at it. Work from the mechanics outward: inspect first, choose the smallest operation that changes what the user asked for, then prove the change on a render. The helper CLI and the scripts are fast paths, not limits — each task file carries a runnable snippet for jobs they do not cover yet.
8
+
9
+ | User job | What actually happens | Fast path |
10
+ | --- | --- | --- |
11
+ | Look at a page, crop, or measure geometry | Rasterise the page box with PyMuPDF at a DPI, read page size, rotation, text, drawings, widgets | `scripts/render_pages.py`, `pdf render` |
12
+ | Extract text or tables | pdfplumber reads characters and clusters table rules per page | `scripts/extract_text.py`, `pdf read` |
13
+ | Inspect or fill an AcroForm | pypdf walks `/AcroForm` widgets, then rebuilds each `/AP` appearance from `/V` | `scripts/forms.py`, snippet in `tasks/forms.md` |
14
+ | Remove a region (redaction) | PyMuPDF redaction annotations plus `apply_redactions` strip text and line art inside a rect | `scripts/redact_region.py`, snippet in `tasks/redact.md` |
15
+ | Read or rewrite metadata | The document info dictionary; producer keys cannot change page content | `scripts/metadata.py` |
16
+ | Merge, split, reorder, rotate, delete pages | Page-tree rearrangement; content is copied, not re-flowed | `scripts/page_ops.py`, `scripts/split_pages.py`, snippet in `tasks/edit-pages.md` |
17
+ | Stamp a footer, header, or banner | reportlab draws an overlay page, pypdf merges it over the target pages | `scripts/stamp_overlay.py` |
18
+ | Build a new document | reportlab draws primitives and text into a page | `pdf create`, `tasks/create.md` |
8
19
 
9
20
  ## Before any file work
10
21
 
11
22
  1. Call the `office_status` tool.
12
- 2. Use the `python` and `cli` values from the `## Office runtime` JSON block at the end of this file. They are verified absolute paths.
13
- 3. If no compatible Python is reported, stop and tell the user how to install it. Never claim a PDF was produced when the CLI was unavailable.
23
+ 2. Read the `## Office runtime` JSON block at the end of this file and use its `python`, `cli`, `skill`, and `shared` values verbatim, quoted: `"<python>"`, `"<cli>"`, `"<skill>/scripts/<name>.py"`.
24
+ 3. If that block reports no working runtime, stop. Give the user the `node "..."` remedy command it names, then call `office_status` again. Never fabricate a PDF result you could not verify.
14
25
 
15
- ## Create
26
+ Scripts under `"<skill>/scripts/"` are standalone argparse CLIs. Run `<python> <script> --help` before using one you have not called in this session.
16
27
 
17
- ```bash
18
- "<python>" "<cli>" pdf create --spec "<spec.json>" --out "<output.pdf>"
19
- ```
28
+ ## Route selection
20
29
 
21
- ```json
22
- {
23
- "title": "Product Brief",
24
- "author": "DSH Office Suite",
25
- "page_size": "A4",
26
- "blocks": [
27
- { "type": "heading", "level": 1, "text": "Product Brief" },
28
- { "type": "paragraph", "text": "This PDF contains Chinese, English, numbers, and punctuation." },
29
- { "type": "bullets", "items": ["Fast", "Local", "Verifiable"] },
30
- { "type": "table", "headers": ["Item", "Value"], "rows": [["Version", "1.0"], ["Status", "Draft"]] },
31
- { "type": "image", "path": "<absolute image path>", "width": 4.5 },
32
- { "type": "page_break" }
33
- ]
34
- }
35
- ```
30
+ Decide the route before you type a command. Every route ends in the same render check.
36
31
 
37
- Supported blocks: `title`, `heading` (`level` 1–2), `paragraph`, `quote`, `bullets`, `numbered`, `table`, `image` (`path`, `width` inches; height is derived at 0.66 aspect), `page_break`, `spacer` (`height` inches). Use `pages: [{ "blocks": [...], "break": true }]` for explicit page grouping.
32
+ - **The file does not exist** → create it from a `pdf create` spec, or with reportlab when the layout does not fit the block model (`tasks/create.md`).
33
+ - **The file exists and must change** → read it first (`pdf read`, and a render), then mutate it with the script named in the task file. The helper CLI has no edit verb: it creates, reads, renders, and reports status only; every change is a `scripts/*.py` call.
34
+ - **A script covers the job** → run it, then verify its JSON report and its rendered pages. Scripts refuse to overwrite their input and exit non-zero on a failed self-check; treat both as hard stops.
35
+ - **A read/render verb covers it** → `pdf read`, `pdf render`, or `pdf status`, then verify in the task's own way. Never route a mutation through the CLI.
36
+ - **Neither covers it** → run the runnable snippet in the matching `tasks/*.md` file, or adapt it. Do not invent a CLI verb, and do not tell the user the job is impossible because no verb exists.
37
+ - **Choose the smallest operation** that satisfies the request: metadata change before rebuild, page move before re-creation, one-line patch before re-typing the page.
38
38
 
39
- CJK text uses the first available system font (`msyh`, `simsun`, PingFang, Noto Sans CJK, DejaVu). If no CJK font exists, Chinese characters may render as blanks; verify before delivery.
39
+ ## Failure handling
40
40
 
41
- ## Read
41
+ | Symptom | Meaning | Next step |
42
+ | --- | --- | --- |
43
+ | `{"ok": false, ...}` and non-zero exit | Invalid argument, missing file, or a refused overwrite | Read the `error` field, fix what it names, re-run. Never strip a step to make it pass. |
44
+ | `encrypted` in the error | The file needs a password | Ask the user for a decrypted copy; do not guess passwords or write one into a command. |
45
+ | `no document renderer found: install LibreOffice, Microsoft Office, or WPS Office` | Media conversion for docx/pptx/xlsx is unavailable | PDF rendering still works (PyMuPDF); for a PDF this is harmless. For other formats report that appearance could not be checked. |
46
+ | `leftover_chars_in_rect` greater than 0 | The redaction rectangle missed text | Enlarge `--pad` or reconvert the region; re-render before delivering. |
47
+ | `not_applied` is not empty | A form value did not persist after save | Treat as a failure: check the field name, its allowed states, and whether the widget has an appearance stream. |
48
+ | `chars: 0` on a page that shows text | Scanned page, no text layer | Say so; follow `references/limits.md` for the OCR situation. Do not return an empty extraction as success. |
49
+ | Render shows the change, `pdf read` does not | The edit landed in pixels only | Fix the content stream or note the limitation; a visible-only change usually means a cover-up, not an edit. |
42
50
 
43
- ```bash
44
- "<python>" "<cli>" pdf read --input "<file.pdf>"
45
- ```
51
+ ## Quality rules
46
52
 
47
- Returns `page_count`, `word_count`, metadata, per-page text and character counts, AcroForm field identifiers, and `ocr: { available: false }`. Extraction quality depends on the source PDF (scanned pages have no text layer).
53
+ - One heading scale, one body size, and one margin system per document (`references/layout-quality.md`).
54
+ - Prove every structural change on a render of the changed page, not only on a text dump.
55
+ - Keep page geometry consistent when merging or stamping: use each page's own box.
56
+ - Do not claim redaction while the words are still extractable.
57
+ - Preserve the user's original: write `<original>-edited.pdf`, never over it.
58
+ - Mark deliberate shortcuts (a rebuild instead of a reflow, a stamped patch instead of a content edit) in the reply, so the user knows what kind of change they received.
59
+ - Deliver with `present`, then one sentence about what changed and where the file is.
48
60
 
49
- ## Edit
61
+ ## Delivery
50
62
 
51
- ```bash
52
- "<python>" "<cli>" pdf edit --input "<file.pdf>" --spec "<edit.json>" --out "<file-edited.pdf>"
53
- ```
63
+ Always hand the finished file to the user with the `present` tool, then summarise in one sentence what changed. A bare path is not delivery: the chat transcript renders `.docx`/`.pptx`/`.xlsx`/`.pdf` links as "二进制文件,无法显示改动" and the user cannot open them.
54
64
 
55
- ```json
56
- {
57
- "metadata": { "Title": "Product Brief (final)", "Author": "DSH Office Suite" },
58
- "merge": ["<other.pdf>"],
59
- "rotate": 90,
60
- "pages": [0],
61
- "remove_pages": [2]
62
- }
63
- ```
65
+ - Creating a file: write it into the user's workspace, verify it, then `present`.
66
+ - Editing an existing file: never overwrite the original. Write `<original>-edited.<ext>` (or a name the user asked for), verify it, `present` it, and state both paths.
67
+
68
+ ## Annotations
69
+
70
+ The user may reply with an `office-annotations` block: `[{ page, text, requirement, region: { left, top, width, height } }]`. `region` is normalised 0–1 against the rendered preview page the user drew on; `requirement` is free-form natural language. Treat it as a change request, not as editor state: locate what the region covers, apply the smallest edit that satisfies `requirement`, re-render, and confirm the region now looks right.
71
+
72
+ When the payload cannot be mapped to a concrete object (the region is empty or the file has no geometry), do not guess silently — say what you could not locate.
64
73
 
65
- - `merge` appends pages from other PDFs.
66
- - `rotate` is degrees clockwise; `pages` limits it to zero-based page indexes.
67
- - `remove_pages` is zero-based and applied to the original pages; merged pages are appended after.
68
- - Metadata keys may be plain (`Title`) or PDF names (`/Title`); the CLI normalizes them.
74
+ For PDF the `page` value is the 1-based page of the file itself, and `region × page.rect` is the real place to operate on: convert once with PyMuPDF and use the device rect directly. `text` is the words under the mark and wins over the region when they disagree; `imageName`, when present, is a screenshot of the marked area that you can open with `read_image`. Full resolution recipe: `tasks/annotations.md` and `"<shared>/annotation-recipes.md"`.
69
75
 
70
- The input file is never modified.
76
+ ## Verify
71
77
 
72
- ## Render and verify
78
+ Render before you present, and again after every fix:
73
79
 
74
80
  ```bash
75
81
  "<python>" "<cli>" pdf render --input "<file.pdf>" --out-dir "<render-dir>" --dpi 110
76
82
  ```
77
83
 
78
- Renders every page to PNG with PyMuPDF. Open the images and check page count, clipping, overlap, missing glyphs, and scanned-page readability. If `read` returns little text and the render shows an image of text, OCR is required; say so instead of pretending the text was extracted.
84
+ Open each PNG with `read_image`, and compare it with the same page rendered before the change. Look for text clipped at the page box, overlapping stamps or form fields, fonts replaced by boxes, rotated pages that now sit sideways, tables spilling past the margin, and scanned pages whose text is an image. Then run `"<python>" "<cli>" pdf read --input "<file.pdf>"` and confirm the text, page count, and form fields match what you edited. Two or three render/patch passes are normal; if a defect survives three, say what still looks wrong instead of calling it fixed.
85
+
86
+ ## Tasks
87
+
88
+ | Task | Read |
89
+ | --- | --- |
90
+ | Create a PDF from a spec or reportlab | `tasks/create.md` |
91
+ | Extract text, tables, or page images | `tasks/read.md` |
92
+ | Inspect, fill, or flatten an AcroForm | `tasks/forms.md` |
93
+ | Remove a region of a page | `tasks/redact.md` |
94
+ | Merge, split, reorder, rotate, delete pages | `tasks/edit-pages.md` |
95
+ | Stamp a footer, header, or label | `tasks/stamp.md` |
96
+ | Resolve an `office-annotations` region | `tasks/annotations.md` |
97
+ | Decide what a PDF cannot do | `references/limits.md` |
98
+
99
+ `references/geometry.md` explains page boxes and normalised regions, `references/layout-quality.md` is the pre-delivery visual checklist, `references/form-fields.md` covers field flags, appearance streams, and flattening.
79
100
 
80
- ## Not supported
101
+ ## Shared workflow
81
102
 
82
- - OCR / scanned-document text recognition.
83
- - Filling, creating, or flattening AcroForms (field identifiers are reported for inspection only).
84
- - Digital signatures, encryption/password handling, redaction, annotations, bookmarks editing.
85
- - PDF/A, tagged-PDF accessibility repair, or exact reflow of an existing document.
86
- - Google Drive or other online PDF services.
103
+ Follow `"<shared>/common-workflow.md"` for the create-or-edit decision, the failure ladder, and the edit naming rule, and `"<shared>/annotation-recipes.md"` for the payload shapes the client sends.
@@ -0,0 +1,56 @@
1
+ # Form field mechanics
2
+
3
+ ## Field tree
4
+
5
+ `/AcroForm` in the catalog holds `/Fields`, the root field array. A field dictionary has:
6
+
7
+ - `/T` — partial name; the full name is the partial names of ancestors joined with dots.
8
+ - `/FT` — field type, inherited: `Tx` text, `Btn` button, `Ch` choice, `Sig` signature.
9
+ - `/V` — current value. Text fields store a string; buttons store the selected state name (`/Yes`, `/Off`, `/basic`); choice fields store a string or an array of strings; signature fields store a dictionary.
10
+ - `/Ff` — flags bitmask, inherited. Bit 1 (`1`) read-only, bit 2 (`2`) required, bit 13 (`4096`) multiselect for choices, bits 15–17 (`65536`/`131072`/`262144`) button behaviour (no-toggle, radio, pushbutton).
11
+ - `/DA` — default appearance string; controls the font and colour an appearance generator uses.
12
+ - `/Kids` — child fields or widgets.
13
+ - `/AP` — appearance dictionary: `/N` normal, `/R` rollover, `/D` down. For a button, `/N` maps each state name to a form XObject; for text and choice fields, `/N` is one stream.
14
+ - `/Rect`, `/P`, `/AS` — widget geometry, owning page, and the button state a widget currently shows.
15
+ - `/NeedAppearances` in `/AcroForm` — documented flag that asks viewers to regenerate appearances; many viewers ignore it, which is why a file can hold a correct `/V` and show nothing.
16
+
17
+ ## What a fill must satisfy
18
+
19
+ 1. Field `/V` equals the requested value, in the field's expected representation (boolean → state name, radio → that button's on-state, choice → string or list).
20
+ 2. The widget's `/AP /N` exists and is a stream or state map with a `/BBox` and `/Resources`; the stream's content paints the value.
21
+ 3. The widget's `/AS` names a state present in `/AP /N`; for a radio group every widget's `/AS` must be the group's chosen state or `/Off`.
22
+ 4. The value is visible on the page: rendered inside `/Rect`, not clipped, not hidden behind the page content.
23
+
24
+ Updating `/V` alone satisfies 1 and fails 2–4. That is the "filled but blank" failure. Rebuilding the appearance and forgetting to remove a stale `/AS` satisfies 1–3 and shows two selected radio buttons. Reopening and checking 1–3 after save, then rendering for 4, is the whole discipline.
25
+
26
+ pypdf's `update_page_form_field_values(writer, page, values, flags, auto_regenerate, flatten)` handles 1–3 for common field types when the writer cloned the document properly (and `writer.reattach_fields(page)` was called where needed). Button groups need the extra `/AS` synchronisation described in `tasks/forms.md`.
27
+
28
+ ## Flattening
29
+
30
+ Two meanings, both destructive in different ways:
31
+
32
+ - `flatten=True` in `update_page_form_field_values` paints each widget's current appearance into the page content and removes the interactive field.
33
+ - Post-process: remove `/Widget` annotations with `writer.remove_annotations(subtypes="/Widget")` and pop `/AcroForm` from the catalog. This alone leaves the widget's appearance in place, so the page looks unchanged while the fields stop responding.
34
+
35
+ Do both when the user wants a non-editable form. Verify: zero `/Widget` annotations remain, no `/AcroForm` in the catalog, and the rendered page still shows the values.
36
+
37
+ ## Appearance streams
38
+
39
+ An appearance stream is a form XObject.
40
+
41
+ ```
42
+ /Type /XObject
43
+ /Subtype /Form
44
+ /BBox [0 0 200 14]
45
+ /Resources << /Font << /Helv 5 0 R >> >>
46
+ ```
47
+
48
+ `/BBox` is the drawing area in the widget's own coordinates; the widget's `/Rect` maps that area onto the page. A generated stream usually sets the font from `/DA`, positions text at a small inset, and clips to `/BBox`. When a field must look right and its own appearance is missing, drawing a replacement with reportlab at `/Rect` coordinates and merging it into the page produces the same visual result without lying about the field value.
49
+
50
+ ## Signature fields
51
+
52
+ `/FT /Sig` requires a certificate and a signing implementation. Filling text into a signature field, or editing any page of an already-signed document, breaks the signature. Say that plainly and offer the unsigned rebuild; do not present an edited document as signed.
53
+
54
+ ## Choice fields
55
+
56
+ `/Opt` lists the allowed options; `/V` is the chosen value or values. A single-select choice takes a string; multi-select takes an array. If the value is not in `/Opt`, viewers show blank — validate against `/Opt` before writing, and report the allowed options when the requested value is missing.
@@ -0,0 +1,44 @@
1
+ # Page geometry
2
+
3
+ PDF units are points: 1/72 inch. A4 is 595 × 842 points, Letter 612 × 792. Numbers in a spec, a region, or a stamp are points unless stated otherwise.
4
+
5
+ ## Boxes
6
+
7
+ - **MediaBox** — the physical page, the outermost boundary.
8
+ - **CropBox** — the visible area; defaults to MediaBox. A viewer shows the CropBox.
9
+ - **BleedBox / TrimBox / ArtBox** — print-production hints, usually absent in office output.
10
+
11
+ PyMuPDF's `page.rect` is the CropBox (or MediaBox when no CropBox exists), with `x0, y0` normally 0 and `x1, y1` the width and height. pypdf exposes `.mediabox` and `.cropbox` directly. When you move content between documents, use the page's own box, not a constant: mixed-size sources are common after a merge.
12
+
13
+ ## Coordinates
14
+
15
+ The origin is the bottom-left of the page box, x to the right, y upward. Library calls that take a rectangle (`page.get_textbox(rect)`, `add_redact_annot`, `get_pixmap(clip=...)`) use that system. Library calls that report word boxes (`page.get_text("words")`, `search_for`) report rectangles in the same system — a word's box is not a baseline, so a text redaction rect is close but may clip the line.
16
+
17
+ reportlab's `drawString(x, y, text)` places a *baseline*, measured from the bottom. Converting a top-down measurement `top` to that system: `y = page_height - top - font_size * 0.8` puts the glyph body roughly where it was.
18
+
19
+ ## Rotation
20
+
21
+ `/Rotate` is a multiple of 90 stored per page; viewers apply it on top of the content. `page.rect` in PyMuPDF is already the rotated (visible) geometry, while `page.mediabox` stays unrotated. Redactions and text boxes use the unrotated content stream, so a rotated page needs the rectangle transformed: simplest is `page.set_rotation(0)`, do the work, then set the rotation back — the render proves whether you got it right.
22
+
23
+ ## Normalised regions
24
+
25
+ `office-annotations` regions are fractions of the rendered preview page:
26
+
27
+ ```
28
+ x0 = page.x0 + left * page.width
29
+ x1 = page.x0 + (left + width) * page.width
30
+ y0 = page.y0 + top * page.height
31
+ y1 = page.y0 + (top + height) * page.height
32
+ ```
33
+
34
+ That is the whole conversion for an unrotated page. For a rotated page, convert against the preview's own orientation and map back; the words under the converted rect tell you immediately if the mapping is off.
35
+
36
+ ## Pixels and DPI
37
+
38
+ A raster at `dpi` has `pixels = points * dpi / 72`. A 110 dpi A4 page is roughly 909 × 1287 pixels. When the user draws a region on a rendered image, the fraction is DPI-independent — you never need the pixel size to convert it. For image quality on print, 150 dpi is the minimum for a screenshot, 300 dpi for artwork.
39
+
40
+ ## Text measurements
41
+
42
+ `page.get_text("dict")` gives blocks → lines → spans, each span with `size` (font size in points), `font`, `origin` (the baseline point), and the text. Use the span measurements, not the bounding box, when you redraw a line: the box includes ascenders and descenders that change the apparent size.
43
+
44
+ `page.get_textbox(rect)` returns the characters whose boxes intersect `rect`, in reading order. That is the correct way to confirm what a region covers; `page.get_text("words", clip=rect)` returns word tuples when you need the boxes too.
@@ -0,0 +1,41 @@
1
+ # Layout and delivery quality
2
+
3
+ Run this list on the rendered pages before presenting a file. It is the difference between "the command exited 0" and "the document is right".
4
+
5
+ ## Page level
6
+
7
+ - Page count matches the request; no stray blank page at the end from a trailing `page_break` or an empty `showPage`.
8
+ - Every page has the intended size and orientation — a merged section with Letter pages inside an A4 report is visible in the render.
9
+ - Margins are consistent; nothing is clipped at the page box edge, including stamps and footers.
10
+ - The page box is the CropBox the user expects; a crop that hides a footer is a bug, not a clean-up.
11
+
12
+ ## Text
13
+
14
+ - Body size, leading, and font are consistent; a patched line that uses a different size or face is a visible patch job.
15
+ - Headings descend in a sensible scale and do not collide with the body.
16
+ - No overflow past the right margin, no hyphen-only line breaks, no replacement boxes for missing glyphs.
17
+ - Non-Latin text renders as characters, not blanks — this needs a registered font with the right coverage.
18
+ - Extracted text matches what is visible: run `pdf read` on the final file and spot-check the edited region.
19
+
20
+ ## Tables and figures
21
+
22
+ - Table columns fit the text column; long cell text wraps rather than clipping.
23
+ - A table that splits across pages repeats its header row.
24
+ - Images keep their aspect ratio; a stretched logo is a defect.
25
+ - Ruled lines and fills from a redacted block are gone, not half-erased.
26
+ - Charts and screenshots are legible at print size; text inside an image is not searchable, so keep the numbers in the body text too.
27
+
28
+ ## Stamps and annotations
29
+
30
+ - Footer/header text sits in the margin, not over body copy.
31
+ - Page numbers are correct on the first and last page (off-by-one is the classic overlay bug).
32
+ - A stamp appears once per page; repeated merges double it.
33
+ - Filled form values sit inside their widget boxes, are not clipped by the box border, and use the field's own font where the appearance stream was preserved.
34
+
35
+ ## Fonts and links
36
+
37
+ `page.get_fonts(full=True)` lists embedded fonts. A subset font is fine for display; it means the file is not intended for text-heavy editing. Check that no font is missing for the glyphs you wrote — the render shows boxes where coverage is absent. Links and bookmarks that pointed at reordered or deleted pages may dangle; the render shows clickable areas only, so trust the original page order rather than assuming the links moved with the content.
38
+
39
+ ## Structural sanity
40
+
41
+ Open the output one more time with the same route the user will use: `"<python>" "<cli>" pdf read --input "<file>"`. It confirms the file parses, the page count, the metadata, and that text is extractable. A file that renders but fails to parse is worse than an obvious error, because it fails later in someone else's viewer.
@@ -0,0 +1,58 @@
1
+ # When a PDF job is beyond this pack
2
+
3
+ Every entry names the missing capability, what you can still deliver, and how the user resolves it. Never answer with a bare "unsupported": say what is missing, what the floor is, and give the next step.
4
+
5
+ ## Scanned pages with no text layer
6
+
7
+ The words exist only as pixels; extraction and search cannot see them. `render_pages.py` reports `chars: 0` for such a page, and the render shows the text as an image.
8
+
9
+ - Still possible: render, crop with `read_image`, and transcribe the visible text; redact the image pixels with `--images pixels`; rebuild the page as a created PDF from the transcription.
10
+ - Resolution: the user supplies a text-based copy of the file, or runs OCR where they have it (this runtime has no OCR engine, so do not promise the words will be extracted here).
11
+
12
+ ## Encrypted or password-protected files
13
+
14
+ pypdf and PyMuPDF can open with a password only if it is provided; every script here exits with "encrypted" and does no work.
15
+
16
+ - Resolution: ask the user to decrypt the file first, then re-run. Never write a password into a command line that will be logged.
17
+
18
+ ## Signing and certificates
19
+
20
+ Creating a digital signature needs a certificate, a signer, and a signature field; the runtime has none of them. Editing a signed document invalidates its signature.
21
+
22
+ - Still possible: fill unsigned fields, rebuild the content, deliver an unsigned file — and say clearly that the signature is gone.
23
+ - Resolution: signing must happen in a tool with a certificate; the user applies it after the content is final.
24
+
25
+ ## Paragraph-level re-editing
26
+
27
+ PDF has no paragraph model: the content stream is positioned glyphs and paths. "Rewrite this paragraph" becomes a block-level operation — redact the old block, stamp the new text, and check the reflow yourself. Truly reflowing a page means rebuilding it from extracted content.
28
+
29
+ - Still possible: replace a line or a block in place (see `tasks/annotations.md`), remove a block, stamp a note.
30
+ - Resolution for reflow: rebuild the document from a spec with the corrected text, and say that it was rebuilt rather than patched.
31
+
32
+ ## Lexical repair of broken files
33
+
34
+ A file with a damaged cross-reference table may still render (viewers rebuild the xref) while strict parsers refuse it. If PyMuPDF or pypdf cannot open the file, do not silently produce an empty document.
35
+
36
+ - Still possible: report the parse error, ask the user to open and re-save the file in a reader (which repairs it), then re-run.
37
+ - Resolution: whichever tool originally produced the file should be told about the corruption.
38
+
39
+ ## Tagged PDF and accessibility remediation
40
+
41
+ Reading order, alternative text, and tags live in a structure tree that the libraries here can read but not author reliably. A visual edit does not add tags.
42
+
43
+ - Still possible: keep the visual result correct and say the file is untagged.
44
+ - Resolution: accessibility remediation needs an authoring tool that writes the structure tree.
45
+
46
+ ## PDF/A and archival conformance
47
+
48
+ Conformance requires font embedding rules, metadata schemas, and colour profiles that these libraries do not validate.
49
+
50
+ - Still possible: many PDF/A inputs will survive a page-level edit and remain close, but claiming conformance would be a guess.
51
+ - Resolution: validate with a dedicated PDF/A checker before delivery.
52
+
53
+ ## Creating or restructuring form fields
54
+
55
+ pypdf can fill existing fields; creating a new interactive field with a correct `/AP` state map is much more than a fill. reportlab can draw a form-like appearance, but without a widget tree it is not a form.
56
+
57
+ - Still possible: stamp labels and values at coordinates so the printed result looks filled.
58
+ - Resolution: an authoring tool for interactive forms, or accept a non-interactive output and say so.
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env python3
2
+ """Extract per-page text and tables from a PDF with pdfplumber.
3
+
4
+ Use this when you need the words, not the picture: page text, word counts, and
5
+ detected tables as row arrays. Visual fidelity is not part of the contract -
6
+ layout, wrapping, and column order may differ from what the rendered page
7
+ shows - so always render before judging a document on this output alone.
8
+
9
+ Exit codes: 0 success, 1 bad arguments, 2 unreadable input or write failure.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ for _stream in (sys.stdout, sys.stderr):
20
+ try:
21
+ _stream.reconfigure(encoding="utf-8", errors="replace")
22
+ except (AttributeError, ValueError):
23
+ pass
24
+
25
+
26
+ def emit(value, out=None):
27
+ text = json.dumps(value, ensure_ascii=False, indent=2, default=str)
28
+ if out:
29
+ target = Path(out).expanduser()
30
+ target.parent.mkdir(parents=True, exist_ok=True)
31
+ target.write_text(text + "\n", encoding="utf-8")
32
+ print(text)
33
+
34
+
35
+ def fail(message, code=1, detail=None):
36
+ payload = {"ok": False, "error": message}
37
+ if detail is not None:
38
+ payload["detail"] = detail
39
+ print(json.dumps(payload, ensure_ascii=False, indent=2, default=str), file=sys.stderr)
40
+ raise SystemExit(code)
41
+
42
+
43
+ def parse_pages(spec, page_count):
44
+ if not spec or spec.strip().lower() == "all":
45
+ return list(range(1, page_count + 1))
46
+ pages = []
47
+ try:
48
+ for part in spec.split(","):
49
+ part = part.strip()
50
+ if not part:
51
+ continue
52
+ if "-" in part:
53
+ start_text, end_text = part.split("-", 1)
54
+ start, end = int(start_text), int(end_text)
55
+ else:
56
+ start = end = int(part)
57
+ if start < 1 or end > page_count or start > end:
58
+ fail(f"page range out of bounds: {part} (document has {page_count} pages)")
59
+ pages.extend(range(start, end + 1))
60
+ except ValueError:
61
+ fail(f"cannot parse page selection: {spec!r}")
62
+ return sorted(dict.fromkeys(pages))
63
+
64
+
65
+ def clean(cell):
66
+ return "" if cell is None else str(cell).replace("\n", " ").strip()
67
+
68
+
69
+ def main():
70
+ parser = argparse.ArgumentParser(
71
+ description="Extract page text and detected tables from a PDF.",
72
+ epilog="Example: extract_text.py --input report.pdf --pages 1-2 --layout",
73
+ )
74
+ parser.add_argument("--input", required=True, help="PDF file to read")
75
+ parser.add_argument("--pages", default="all", help='1-based selection such as "1-3,7" (default: all)')
76
+ parser.add_argument("--layout", action="store_true", help="Keep rough horizontal layout in the text output")
77
+ parser.add_argument("--no-tables", action="store_true", help="Skip table detection (faster on long documents)")
78
+ parser.add_argument("--text-out", help="Also save the joined page text to this file")
79
+ parser.add_argument("--json-out", help="Also write the JSON report to this path")
80
+ args = parser.parse_args()
81
+
82
+ source = Path(args.input).expanduser()
83
+ if not source.is_file():
84
+ fail(f"input not found: {source}", code=2)
85
+
86
+ try:
87
+ import pdfplumber
88
+ except ImportError:
89
+ fail("pdfplumber is not installed; run the plugin postinstall again", code=2)
90
+
91
+ try:
92
+ pdf = pdfplumber.open(str(source))
93
+ except Exception as exc:
94
+ fail(f"cannot open PDF: {exc}", code=2)
95
+
96
+ with pdf:
97
+ page_count = len(pdf.pages)
98
+ selected = parse_pages(args.pages, page_count)
99
+ reports = []
100
+ warnings = []
101
+ joined = []
102
+ total_chars = 0
103
+ total_tables = 0
104
+ for index in selected:
105
+ page = pdf.pages[index - 1]
106
+ try:
107
+ text = page.extract_text(layout=args.layout) or ""
108
+ except Exception as exc:
109
+ fail(f"page {index} text extraction failed: {exc}", code=2)
110
+ report = {
111
+ "page": index,
112
+ "width_pt": round(float(page.width), 2),
113
+ "height_pt": round(float(page.height), 2),
114
+ "rotation": int(page.rotation or 0),
115
+ "chars": len(text),
116
+ "words": len(text.split()),
117
+ "text": text,
118
+ "tables": [],
119
+ }
120
+ if not args.no_tables:
121
+ try:
122
+ tables = page.find_tables()
123
+ except Exception as exc:
124
+ tables = []
125
+ warnings.append(f"page {index}: table detection failed: {exc}")
126
+ for order, table in enumerate(tables, start=1):
127
+ rows = [[clean(cell) for cell in row] for row in table.extract()]
128
+ report["tables"].append(
129
+ {
130
+ "index": order,
131
+ "bbox": [round(float(value), 2) for value in table.bbox],
132
+ "row_count": len(rows),
133
+ "column_count": max((len(row) for row in rows), default=0),
134
+ "rows": rows,
135
+ }
136
+ )
137
+ total_tables += len(report["tables"])
138
+ if report["chars"] == 0 and len(page.images) > 0:
139
+ warnings.append(
140
+ f"page {index} has images but no extractable text: it is probably a scan, "
141
+ "and this runtime has no OCR"
142
+ )
143
+ total_chars += report["chars"]
144
+ joined.append(f"===== page {index} =====\n{text}")
145
+ reports.append(report)
146
+
147
+ if args.text_out:
148
+ try:
149
+ target = Path(args.text_out).expanduser()
150
+ target.parent.mkdir(parents=True, exist_ok=True)
151
+ target.write_text("\n\n".join(joined).rstrip() + "\n", encoding="utf-8")
152
+ except OSError as exc:
153
+ fail(f"cannot write text output: {exc}", code=2)
154
+
155
+ emit(
156
+ {
157
+ "ok": True,
158
+ "input": str(source),
159
+ "page_count": page_count,
160
+ "pages_read": len(selected),
161
+ "text_out": str(Path(args.text_out).expanduser()) if args.text_out else None,
162
+ "total_chars": total_chars,
163
+ "total_tables": total_tables,
164
+ "pages": reports,
165
+ "warnings": warnings,
166
+ },
167
+ args.json_out,
168
+ )
169
+
170
+
171
+ if __name__ == "__main__":
172
+ main()