dsh-office-kit 0.2.0 → 0.3.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 (197) hide show
  1. package/CAPABILITY.md +147 -68
  2. package/README.md +3 -1
  3. package/assets/skills/excel-live-control/SKILL.md +234 -0
  4. package/assets/skills/excel-live-control/container_tools/live_excel.py +1589 -0
  5. package/assets/skills/excel-live-control/domain_guidance/financial_models.md +86 -0
  6. package/assets/skills/excel-live-control/domain_guidance/healthcare.md +58 -0
  7. package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +53 -0
  8. package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +53 -0
  9. package/assets/skills/excel-live-control/references/office-automation.md +207 -0
  10. package/assets/skills/excel-live-control/references/read_only_qna.md +59 -0
  11. package/assets/skills/excel-live-control/style_guidelines.md +87 -0
  12. package/assets/skills/excel-live-control/workflows/create_workflows.md +78 -0
  13. package/assets/skills/excel-live-control/workflows/edit_workflows.md +72 -0
  14. package/assets/skills/office-documents/SKILL.md +90 -77
  15. package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +86 -0
  16. package/assets/skills/office-documents/ooxml/comments.md +40 -0
  17. package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +66 -0
  18. package/assets/skills/office-documents/ooxml/rels_and_content_types.md +62 -0
  19. package/assets/skills/office-documents/ooxml/tracked_changes.md +53 -0
  20. package/assets/skills/office-documents/references/docx-structure.md +83 -0
  21. package/assets/skills/office-documents/references/template-elicitation.md +49 -0
  22. package/assets/skills/office-documents/references/writing-quality.md +49 -0
  23. package/assets/skills/office-documents/render_docx.py +99 -0
  24. package/assets/skills/office-documents/scripts/a11y_audit.py +201 -0
  25. package/assets/skills/office-documents/scripts/accept_tracked_changes.py +158 -0
  26. package/assets/skills/office-documents/scripts/add_tracked_replacements.py +155 -0
  27. package/assets/skills/office-documents/scripts/append_blocks.py +115 -0
  28. package/assets/skills/office-documents/scripts/apply_template_styles.py +111 -0
  29. package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +109 -0
  30. package/assets/skills/office-documents/scripts/comments_add.py +143 -0
  31. package/assets/skills/office-documents/scripts/comments_apply_patch.py +121 -0
  32. package/assets/skills/office-documents/scripts/comments_extract.py +92 -0
  33. package/assets/skills/office-documents/scripts/comments_strip.py +81 -0
  34. package/assets/skills/office-documents/scripts/content_controls.py +199 -0
  35. package/assets/skills/office-documents/scripts/core_properties.py +131 -0
  36. package/assets/skills/office-documents/scripts/docx_kit.py +594 -0
  37. package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +232 -0
  38. package/assets/skills/office-documents/scripts/docx_table_to_csv.py +106 -0
  39. package/assets/skills/office-documents/scripts/fields_materialize.py +73 -0
  40. package/assets/skills/office-documents/scripts/fields_report.py +66 -0
  41. package/assets/skills/office-documents/scripts/flatten_ref_fields.py +69 -0
  42. package/assets/skills/office-documents/scripts/footnotes_report.py +84 -0
  43. package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +103 -0
  44. package/assets/skills/office-documents/scripts/heading_audit.py +88 -0
  45. package/assets/skills/office-documents/scripts/images_audit.py +87 -0
  46. package/assets/skills/office-documents/scripts/insert_note.py +133 -0
  47. package/assets/skills/office-documents/scripts/insert_ref_fields.py +97 -0
  48. package/assets/skills/office-documents/scripts/insert_toc.py +91 -0
  49. package/assets/skills/office-documents/scripts/internal_nav.py +153 -0
  50. package/assets/skills/office-documents/scripts/make_fixtures.py +176 -0
  51. package/assets/skills/office-documents/scripts/merge_docx_append.py +246 -0
  52. package/assets/skills/office-documents/scripts/privacy_scrub.py +138 -0
  53. package/assets/skills/office-documents/scripts/redact_docx.py +120 -0
  54. package/assets/skills/office-documents/scripts/render_and_diff.py +108 -0
  55. package/assets/skills/office-documents/scripts/replace_text.py +161 -0
  56. package/assets/skills/office-documents/scripts/section_audit.py +98 -0
  57. package/assets/skills/office-documents/scripts/set_protection.py +99 -0
  58. package/assets/skills/office-documents/scripts/style_lint.py +118 -0
  59. package/assets/skills/office-documents/scripts/style_normalize.py +110 -0
  60. package/assets/skills/office-documents/scripts/table_geometry.py +174 -0
  61. package/assets/skills/office-documents/scripts/watermark_add.py +55 -0
  62. package/assets/skills/office-documents/scripts/watermark_audit_remove.py +74 -0
  63. package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +124 -0
  64. package/assets/skills/office-documents/tasks/accessibility_a11y.md +45 -0
  65. package/assets/skills/office-documents/tasks/captions_crossrefs.md +49 -0
  66. package/assets/skills/office-documents/tasks/clean_tracked_changes.md +49 -0
  67. package/assets/skills/office-documents/tasks/comments_manage.md +59 -0
  68. package/assets/skills/office-documents/tasks/compare_diff.md +40 -0
  69. package/assets/skills/office-documents/tasks/create_edit.md +107 -0
  70. package/assets/skills/office-documents/tasks/equations.md +59 -0
  71. package/assets/skills/office-documents/tasks/fields_update.md +44 -0
  72. package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +50 -0
  73. package/assets/skills/office-documents/tasks/footnotes_endnotes.md +38 -0
  74. package/assets/skills/office-documents/tasks/forms_content_controls.md +48 -0
  75. package/assets/skills/office-documents/tasks/headings_numbering.md +55 -0
  76. package/assets/skills/office-documents/tasks/images_figures.md +63 -0
  77. package/assets/skills/office-documents/tasks/multi_doc_merge.md +43 -0
  78. package/assets/skills/office-documents/tasks/navigation_internal_links.md +38 -0
  79. package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +45 -0
  80. package/assets/skills/office-documents/tasks/protection_restrict_editing.md +41 -0
  81. package/assets/skills/office-documents/tasks/read_review.md +50 -0
  82. package/assets/skills/office-documents/tasks/redaction_anonymization.md +42 -0
  83. package/assets/skills/office-documents/tasks/sections_layout.md +54 -0
  84. package/assets/skills/office-documents/tasks/style_lint_normalize.md +44 -0
  85. package/assets/skills/office-documents/tasks/tables_spreadsheets.md +64 -0
  86. package/assets/skills/office-documents/tasks/templates_style_packs.md +53 -0
  87. package/assets/skills/office-documents/tasks/toc_workflow.md +39 -0
  88. package/assets/skills/office-documents/tasks/verify_render.md +50 -0
  89. package/assets/skills/office-documents/tasks/watermarks_background.md +35 -0
  90. package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +47 -0
  91. package/assets/skills/office-documents/troubleshooting/run_splitting.md +36 -0
  92. package/assets/skills/office-pdf/SKILL.md +73 -56
  93. package/assets/skills/office-pdf/references/form-fields.md +56 -0
  94. package/assets/skills/office-pdf/references/geometry.md +44 -0
  95. package/assets/skills/office-pdf/references/layout-quality.md +41 -0
  96. package/assets/skills/office-pdf/references/limits.md +58 -0
  97. package/assets/skills/office-pdf/scripts/extract_text.py +172 -0
  98. package/assets/skills/office-pdf/scripts/forms.py +556 -0
  99. package/assets/skills/office-pdf/scripts/metadata.py +161 -0
  100. package/assets/skills/office-pdf/scripts/page_ops.py +198 -0
  101. package/assets/skills/office-pdf/scripts/redact_region.py +347 -0
  102. package/assets/skills/office-pdf/scripts/render_pages.py +207 -0
  103. package/assets/skills/office-pdf/scripts/split_pages.py +155 -0
  104. package/assets/skills/office-pdf/scripts/stamp_overlay.py +199 -0
  105. package/assets/skills/office-pdf/tasks/annotations.md +98 -0
  106. package/assets/skills/office-pdf/tasks/create.md +75 -0
  107. package/assets/skills/office-pdf/tasks/edit-pages.md +71 -0
  108. package/assets/skills/office-pdf/tasks/forms.md +67 -0
  109. package/assets/skills/office-pdf/tasks/read.md +62 -0
  110. package/assets/skills/office-pdf/tasks/redact.md +72 -0
  111. package/assets/skills/office-pdf/tasks/stamp.md +60 -0
  112. package/assets/skills/office-presentations/SKILL.md +141 -87
  113. package/assets/skills/office-presentations/container_tools/add_slides.py +143 -0
  114. package/assets/skills/office-presentations/container_tools/core_properties.py +127 -0
  115. package/assets/skills/office-presentations/container_tools/create_montage.py +133 -0
  116. package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +292 -0
  117. package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +164 -0
  118. package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +151 -0
  119. package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +206 -0
  120. package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +219 -0
  121. package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +352 -0
  122. package/assets/skills/office-presentations/container_tools/preview_render.py +382 -0
  123. package/assets/skills/office-presentations/container_tools/render_slides.py +146 -0
  124. package/assets/skills/office-presentations/container_tools/replace_text.py +175 -0
  125. package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +236 -0
  126. package/assets/skills/office-presentations/domain_guidance/finance.md +66 -0
  127. package/assets/skills/office-presentations/recipes/README.md +71 -0
  128. package/assets/skills/office-presentations/recipes/autolayout.md +88 -0
  129. package/assets/skills/office-presentations/recipes/connectors.md +83 -0
  130. package/assets/skills/office-presentations/recipes/images.md +80 -0
  131. package/assets/skills/office-presentations/recipes/master-and-layouts.md +127 -0
  132. package/assets/skills/office-presentations/recipes/native-charts.md +87 -0
  133. package/assets/skills/office-presentations/recipes/rich-text-runs.md +86 -0
  134. package/assets/skills/office-presentations/recipes/speaker-notes.md +65 -0
  135. package/assets/skills/office-presentations/recipes/tables.md +88 -0
  136. package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +71 -0
  137. package/assets/skills/office-presentations/recipes/theme-tokens.md +87 -0
  138. package/assets/skills/office-presentations/references/authoring-workflow.md +65 -0
  139. package/assets/skills/office-presentations/references/finalization.md +63 -0
  140. package/assets/skills/office-presentations/references/native-bullets.md +78 -0
  141. package/assets/skills/office-presentations/references/native-evidence.md +67 -0
  142. package/assets/skills/office-presentations/references/style-guidelines.md +66 -0
  143. package/assets/skills/office-presentations/references/templates.md +76 -0
  144. package/assets/skills/office-presentations/routing/google_slides.md +45 -0
  145. package/assets/skills/office-presentations/tasks/apply-annotations.md +83 -0
  146. package/assets/skills/office-presentations/tasks/charts-tables-notes.md +79 -0
  147. package/assets/skills/office-presentations/tasks/create-deck.md +106 -0
  148. package/assets/skills/office-presentations/tasks/edit-deck.md +93 -0
  149. package/assets/skills/office-presentations/tasks/inspect-and-verify.md +107 -0
  150. package/assets/skills/office-presentations/tasks/template-following.md +132 -0
  151. package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py +183 -0
  152. package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +161 -0
  153. package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +118 -0
  154. package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +246 -0
  155. package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +192 -0
  156. package/assets/skills/office-spreadsheets/SKILL.md +105 -101
  157. package/assets/skills/office-spreadsheets/container_tools/add_chart.py +243 -0
  158. package/assets/skills/office-spreadsheets/container_tools/add_image.py +114 -0
  159. package/assets/skills/office-spreadsheets/container_tools/append_rows.py +129 -0
  160. package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +313 -0
  161. package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +405 -0
  162. package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +287 -0
  163. package/assets/skills/office-spreadsheets/container_tools/set_cells.py +142 -0
  164. package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +141 -0
  165. package/assets/skills/office-spreadsheets/container_tools/style_cells.py +140 -0
  166. package/assets/skills/office-spreadsheets/recipes/charts.md +66 -0
  167. package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +69 -0
  168. package/assets/skills/office-spreadsheets/recipes/data-tables.md +58 -0
  169. package/assets/skills/office-spreadsheets/recipes/named-ranges.md +41 -0
  170. package/assets/skills/office-spreadsheets/recipes/number-formats.md +72 -0
  171. package/assets/skills/office-spreadsheets/recipes/sparklines.md +43 -0
  172. package/assets/skills/office-spreadsheets/references/charts.md +63 -0
  173. package/assets/skills/office-spreadsheets/references/domain/financial-models.md +40 -0
  174. package/assets/skills/office-spreadsheets/references/domain/healthcare.md +22 -0
  175. package/assets/skills/office-spreadsheets/references/domain/marketing.md +28 -0
  176. package/assets/skills/office-spreadsheets/references/domain/research.md +22 -0
  177. package/assets/skills/office-spreadsheets/references/formula-patterns.md +78 -0
  178. package/assets/skills/office-spreadsheets/references/style-guidelines.md +69 -0
  179. package/assets/skills/office-spreadsheets/references/workbook-structure.md +60 -0
  180. package/assets/skills/office-spreadsheets/routing/google-sheets.md +36 -0
  181. package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +37 -0
  182. package/assets/skills/office-spreadsheets/tasks/annotations.md +51 -0
  183. package/assets/skills/office-spreadsheets/tasks/create-workbook.md +66 -0
  184. package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +38 -0
  185. package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +69 -0
  186. package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +38 -0
  187. package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +57 -0
  188. package/assets/skills/shared/annotation-recipes.md +252 -0
  189. package/assets/skills/shared/check_runtime.py +260 -0
  190. package/assets/skills/shared/common-workflow.md +139 -0
  191. package/assets/skills/shared/scripts/ensure_raster_image.py +187 -0
  192. package/dist/client.js +4 -4
  193. package/dist/client.js.map +1 -1
  194. package/dist/index.js +246 -89
  195. package/package.json +1 -1
  196. package/postinstall.mjs +92 -82
  197. package/python/office_cli.py +213 -254
@@ -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()