dsh-office-kit 0.2.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 (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 +92 -0
  24. package/assets/skills/office-documents/scripts/a11y_audit.py +194 -0
  25. package/assets/skills/office-documents/scripts/accept_tracked_changes.py +151 -0
  26. package/assets/skills/office-documents/scripts/add_tracked_replacements.py +148 -0
  27. package/assets/skills/office-documents/scripts/append_blocks.py +108 -0
  28. package/assets/skills/office-documents/scripts/apply_template_styles.py +104 -0
  29. package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +102 -0
  30. package/assets/skills/office-documents/scripts/comments_add.py +136 -0
  31. package/assets/skills/office-documents/scripts/comments_apply_patch.py +114 -0
  32. package/assets/skills/office-documents/scripts/comments_extract.py +85 -0
  33. package/assets/skills/office-documents/scripts/comments_strip.py +74 -0
  34. package/assets/skills/office-documents/scripts/content_controls.py +192 -0
  35. package/assets/skills/office-documents/scripts/core_properties.py +124 -0
  36. package/assets/skills/office-documents/scripts/docx_kit.py +587 -0
  37. package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +225 -0
  38. package/assets/skills/office-documents/scripts/docx_table_to_csv.py +99 -0
  39. package/assets/skills/office-documents/scripts/fields_materialize.py +66 -0
  40. package/assets/skills/office-documents/scripts/fields_report.py +59 -0
  41. package/assets/skills/office-documents/scripts/flatten_ref_fields.py +62 -0
  42. package/assets/skills/office-documents/scripts/footnotes_report.py +77 -0
  43. package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +96 -0
  44. package/assets/skills/office-documents/scripts/heading_audit.py +81 -0
  45. package/assets/skills/office-documents/scripts/images_audit.py +80 -0
  46. package/assets/skills/office-documents/scripts/insert_note.py +126 -0
  47. package/assets/skills/office-documents/scripts/insert_ref_fields.py +90 -0
  48. package/assets/skills/office-documents/scripts/insert_toc.py +84 -0
  49. package/assets/skills/office-documents/scripts/internal_nav.py +146 -0
  50. package/assets/skills/office-documents/scripts/make_fixtures.py +169 -0
  51. package/assets/skills/office-documents/scripts/merge_docx_append.py +239 -0
  52. package/assets/skills/office-documents/scripts/privacy_scrub.py +131 -0
  53. package/assets/skills/office-documents/scripts/redact_docx.py +113 -0
  54. package/assets/skills/office-documents/scripts/render_and_diff.py +101 -0
  55. package/assets/skills/office-documents/scripts/replace_text.py +154 -0
  56. package/assets/skills/office-documents/scripts/section_audit.py +91 -0
  57. package/assets/skills/office-documents/scripts/set_protection.py +92 -0
  58. package/assets/skills/office-documents/scripts/style_lint.py +111 -0
  59. package/assets/skills/office-documents/scripts/style_normalize.py +103 -0
  60. package/assets/skills/office-documents/scripts/table_geometry.py +167 -0
  61. package/assets/skills/office-documents/scripts/watermark_add.py +48 -0
  62. package/assets/skills/office-documents/scripts/watermark_audit_remove.py +67 -0
  63. package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +117 -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 +136 -0
  114. package/assets/skills/office-presentations/container_tools/core_properties.py +120 -0
  115. package/assets/skills/office-presentations/container_tools/create_montage.py +126 -0
  116. package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +285 -0
  117. package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +157 -0
  118. package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +144 -0
  119. package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +199 -0
  120. package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +212 -0
  121. package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +345 -0
  122. package/assets/skills/office-presentations/container_tools/preview_render.py +375 -0
  123. package/assets/skills/office-presentations/container_tools/render_slides.py +139 -0
  124. package/assets/skills/office-presentations/container_tools/replace_text.py +168 -0
  125. package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +229 -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 +176 -0
  152. package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +154 -0
  153. package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +111 -0
  154. package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +239 -0
  155. package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +185 -0
  156. package/assets/skills/office-spreadsheets/SKILL.md +105 -101
  157. package/assets/skills/office-spreadsheets/container_tools/add_chart.py +236 -0
  158. package/assets/skills/office-spreadsheets/container_tools/add_image.py +107 -0
  159. package/assets/skills/office-spreadsheets/container_tools/append_rows.py +122 -0
  160. package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +306 -0
  161. package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +398 -0
  162. package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +275 -0
  163. package/assets/skills/office-spreadsheets/container_tools/set_cells.py +135 -0
  164. package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +134 -0
  165. package/assets/skills/office-spreadsheets/container_tools/style_cells.py +133 -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 +253 -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 +239 -89
  195. package/package.json +1 -1
  196. package/postinstall.mjs +92 -82
  197. package/python/office_cli.py +171 -246
@@ -0,0 +1,62 @@
1
+ # Read text, tables, and page images
2
+
3
+ Text extraction reads the page content stream: glyph runs carry position and a font, and a library groups them into words and lines. A page with no text operators (a scan or a photo export) yields nothing no matter which extractor is used — that is a property of the file, not of the tool.
4
+
5
+ ## Fast path: plain text and structure
6
+
7
+ ```bash
8
+ "<python>" "<cli>" pdf read --input "<file.pdf>" --json-out "<read.json>"
9
+ ```
10
+
11
+ Returns `page_count`, `word_count`, document metadata, per-page text (with character counts), and AcroForm field identifiers. `scripts/render_pages.py --input "<file.pdf>"` adds geometry: page size, rotation, drawings, annotations, widgets, and the characters inside an optional crop.
12
+
13
+ ## Tables and layout with pdfplumber
14
+
15
+ ```bash
16
+ "<python>" "<skill>/scripts/extract_text.py" --input "<file.pdf>" --json-out "<tables.json>"
17
+ ```
18
+
19
+ Each page reports its characters and detected tables as rows of cell strings. The detection uses the page's ruling lines and word spacing, so a borderless table often comes back as running text; `--layout` keeps the spacing of the original lines for a manual look.
20
+
21
+ When the detection looks wrong, inspect rather than guess:
22
+
23
+ ```bash
24
+ "<python>" - <<'PY'
25
+ import pdfplumber
26
+ with pdfplumber.open("in.pdf") as pdf:
27
+ page = pdf.pages[0]
28
+ print("chars", len(page.chars), "lines", len(page.lines), "rects", len(page.rects))
29
+ for table in page.find_tables():
30
+ print(table.bbox, table.extract()[:3])
31
+ print(page.extract_text()[:400])
32
+ PY
33
+ ```
34
+
35
+ `chars`, `lines`, and `rects` tell you whether the table has rules at all. A table with no lines is best read from `extract_text` with `--layout`, where the column gaps survive as spaces.
36
+
37
+ ## Page images
38
+
39
+ A page may contain embedded rasters that are invisible in text extraction. List them with PyMuPDF:
40
+
41
+ ```bash
42
+ "<python>" - <<'PY'
43
+ import pymupdf
44
+ doc = pymupdf.open("in.pdf")
45
+ for number, page in enumerate(doc, 1):
46
+ for image in page.get_images(full=True):
47
+ print(number, image[0], image[2], "x", image[3])
48
+ PY
49
+ ```
50
+
51
+ Extract one with `page.get_pixmap(clip=...)` or `doc.extract_image(xref)` when the user needs the picture itself. Rendering the page at 150–200 dpi and cropping with `read_image` is the reliable way to *see* it.
52
+
53
+ ## What fails and how to resolve it
54
+
55
+ - **Scanned page, no text**: there is no text layer to read. Say so; the render shows the image of the words. Resolutions: work from a text-based copy of the file, or read the pages visually and type the content into a rebuild. Do not claim extraction succeeded on an empty result.
56
+ - **Empty string from a page that shows text**: the page may use a font with a broken ToUnicode map, or the text may be drawn as vector outlines. Check the render before concluding; if it is outlines, extraction cannot recover the words.
57
+ - **Wrong reading order in multi-column layouts**: raw extraction follows the operator order. Crop each column with a rect and extract per column, `get_textbox(rect)` on a PyMuPDF page is the simplest split.
58
+ - **Ligatures and soft hyphens**: extraction may glue or split words; quote extracted text only after reading the same passage on the render.
59
+
60
+ ## Verify
61
+
62
+ Rendering the page is the check for every extraction: the characters you return must be visible in that image. Watch for a page whose extracted text is suspiciously short (a missing text layer), duplicated (headers repeated per page), or full of replacement glyphs (broken encoding) before you build anything on top of it.
@@ -0,0 +1,72 @@
1
+ # Remove a region of a page
2
+
3
+ Redaction here means *destruction*, not a black box: the glyphs and vector paths inside a rectangle are deleted from the content stream, so no text extractor can recover them afterwards. A rectangle that only covers them visually is not a redaction, and "black out then flatten" leaks the original text.
4
+
5
+ ## Fast path: the region script
6
+
7
+ ```bash
8
+ "<python>" "<skill>/scripts/redact_region.py" --input "<file.pdf>" --out "<file-redacted.pdf>" \
9
+ --annotations '{"page":1,"region":{"left":0.064,"top":0.318,"width":0.891,"height":0.153}}' \
10
+ --json-out "<redact.json>"
11
+ ```
12
+
13
+ The payload is the annotation shape: 1-based page plus a region normalised 0–1 against the rendered page. The script converts it, expands the rectangle by `--pad` points (default 2.5), calls `add_redact_annot(rect, fill=None)` then `apply_redactions(images=PDF_REDACT_IMAGE_NONE, graphics=PDF_REDACT_LINE_ART_REMOVE_IF_TOUCHED, text=PDF_REDACT_TEXT_REMOVE)`, and reports `removed_chars`, `removed_drawings`, the per-page before/after counts, and `leftover_chars_in_rect`. `--region`/`--page` do the same for a single value without JSON. Multiple annotations in one payload are applied page by page.
14
+
15
+ ## Why the rectangle needs padding
16
+
17
+ A line of text sits on a baseline and its glyphs can overhang the geometric box by a fraction of a point; selection boxes from a preview are approximate too. Expanding by 2–3 points catches ascenders, descenders, rules that touch the text, and the anti-aliased edge, while staying clear of neighbours. Measure, do not guess: `get_textbox(rect)` before the redaction lists what the rectangle will take, and `leftover_chars_in_rect` after it must be zero.
18
+
19
+ Do it by hand when you need a different shape or a dry run:
20
+
21
+ ```bash
22
+ "<python>" - <<'PY'
23
+ import pymupdf
24
+ doc = pymupdf.open("in.pdf")
25
+ page = doc[0]
26
+ r = page.rect
27
+ rect = pymupdf.Rect(r.x0 + 0.064 * r.width, r.y0 + 0.318 * r.height,
28
+ r.x0 + 0.955 * r.width, r.y0 + 0.471 * r.height)
29
+ rect = (rect + (-2.5, -2.5, 2.5, 2.5)) & page.rect
30
+ print("will remove:", repr(page.get_textbox(rect)[:200]))
31
+ before = len(page.get_text()), len(page.get_drawings())
32
+ page.add_redact_annot(rect, fill=None)
33
+ page.apply_redactions(images=pymupdf.PDF_REDACT_IMAGE_NONE,
34
+ graphics=pymupdf.PDF_REDACT_LINE_ART_REMOVE_IF_TOUCHED)
35
+ after = len(page.get_text()), len(page.get_drawings())
36
+ print("chars/drawings", before, "->", after)
37
+ doc.save("out.pdf", garbage=3, deflate=True)
38
+ PY
39
+ ```
40
+
41
+ ## The flags are not interchangeable
42
+
43
+ - `fill=None` erases without painting. A fill colour (`fill=(1, 1, 1)`) paints a rectangle, which itself becomes a new drawing and hides the page's own background tint; the drawing count then goes up, not down.
44
+ - `images=PDF_REDACT_IMAGE_NONE` leaves pictures untouched: a scanned region stays visible. Use `PDF_REDACT_IMAGE_PIXELS` to blank the pixels a rectangle covers inside an image, and only use a stronger setting when the user wants the whole image gone.
45
+ - `graphics=PDF_REDACT_LINE_ART_REMOVE_IF_TOUCHED` deletes paths that intersect the rectangle; paths entirely inside it are always removed. `PDF_REDACT_LINE_ART_NONE` keeps all line art, which is right when only text should disappear.
46
+ - Text removal is the default (`PDF_REDACT_TEXT_REMOVE`); keeping the text while erasing its surroundings is almost never what the user means.
47
+
48
+ ## Counting the result
49
+
50
+ `removed_chars` is the page-level difference in extracted text; `region_chars_removed` is the text that was inside the rectangle. They differ by the newline or space that joins the removed block to its neighbours, so expect them to be equal or one apart. Document-level totals are the sum over the changed pages. A run that removes fewer characters than the region held means the rectangle missed part of the block — enlarge `--pad` or convert the geometry again.
51
+
52
+ Cross-check on the sample document: page 1, region `{left: 0.064, top: 0.318, width: 0.891, height: 0.153}` deletes the "Latest News" block and nothing else. Measured: 16 drawings → 5, page text 1049 → 879 characters (the block held 169), `leftover_chars_in_rect` 0, and the body paragraph, header, and footer still extract and render.
53
+
54
+ ## Leftovers redaction does not cover
55
+
56
+ - **Metadata and embedded files**: the info dictionary and attachments are not page content. Clear them when the user asks for a clean file (see `scripts/metadata.py`), and note that a document ID/xref may still fingerprint the file.
57
+ - **Previous revisions**: incrementally saved files can retain old objects. Saving with `garbage=3, deflate=True` compacts the file; when the user needs forensic certainty, say that the output was rebuilt and confirm with a text search that the words are gone.
58
+ - **Images inside the region**: by default they stay. If the region is a screenshot of the sensitive text, switch to `--images pixels` or rebuild the page.
59
+
60
+ ## Verify
61
+
62
+ Render after the redaction and look at the page. The region must be empty, the surrounding text intact, and any ruled boxes that belonged to the block gone rather than half-erased. Then search the output for the removed words:
63
+
64
+ ```bash
65
+ "<python>" - <<'PY'
66
+ import pymupdf
67
+ text = pymupdf.open("out.pdf")[0].get_text()
68
+ print("latest news" in text.lower(), len(text))
69
+ PY
70
+ ```
71
+
72
+ If they still appear, the rectangle missed them — fix the geometry, do not hand the file over.
@@ -0,0 +1,60 @@
1
+ # Stamp a footer, header, or label
2
+
3
+ An overlay is a tiny one-page PDF drawn with reportlab and merged onto the target page, so the original content stream is untouched and existing widgets keep working. Position is in points measured from the bottom-left of the page box; a stamp on an A4 page has 842 points of height to work with.
4
+
5
+ ## Fast path: the stamp script
6
+
7
+ ```bash
8
+ "<python>" "<skill>/scripts/stamp_overlay.py" --input "<file.pdf>" --out "<file-stamped.pdf>" \
9
+ --text "Confidential - page {page} of {pages}" --position bottom-center --render-dir "<render>"
10
+ ```
11
+
12
+ `{page}` and `{pages}` are substituted per page. Positions: `bottom-left`, `bottom-center`, `bottom-right`, `top-left`, `top-center`, `top-right`; `--pages "1,3-5"` limits the stamp; `--font-size`, `--font`, `--color`, `--margin`, and `--opacity` control the look. The script merges page by page with pypdf, refuses to overwrite the input, and can render the stamped pages so you can look at them.
13
+
14
+ ## Draw it yourself
15
+
16
+ Use reportlab directly when the stamp is a bar, a rotated line, or a block of text:
17
+
18
+ ```bash
19
+ "<python>" - <<'PY'
20
+ import io
21
+ from pypdf import PdfReader, PdfWriter
22
+ from reportlab.lib.pagesizes import A4
23
+ from reportlab.lib.colors import HexColor
24
+ from reportlab.pdfgen import canvas
25
+
26
+ reader = PdfReader("in.pdf")
27
+ writer = PdfWriter()
28
+ for index, page in enumerate(reader.pages):
29
+ width = float(page.mediabox.width)
30
+ height = float(page.mediabox.height)
31
+ buffer = io.BytesIO()
32
+ c = canvas.Canvas(buffer, pagesize=(width, height))
33
+ c.setFillColor(HexColor("#c0392b"))
34
+ c.setFont("Helvetica-Bold", 10)
35
+ c.saveState()
36
+ c.translate(width / 2, 18)
37
+ c.drawCentredString(0, 0, f"CONFIDENTIAL | page {index + 1} of {len(reader.pages)}")
38
+ c.restoreState()
39
+ c.save()
40
+ buffer.seek(0)
41
+ page.merge_page(PdfReader(buffer).pages[0])
42
+ writer.add_page(page)
43
+ with open("out.pdf", "wb") as handle:
44
+ writer.write(handle)
45
+ PY
46
+ ```
47
+
48
+ Use the page's own `mediabox` rather than a hard-coded A4 size when the source may mix page sizes; a stamp drawn for A4 lands in the wrong place on a landscape page. `merge_page` paints over the page, so a solid bar hides whatever it covers — place it in a margin or inside a band you intend to cover.
49
+
50
+ ## Pitfalls
51
+
52
+ - **Collisions with existing footers**: check the bottom margin first; the render is the proof, not the coordinate math. If the document already has a page number, either skip the stamp or put it on the opposite side.
53
+ - **Rotated pages**: a page with `/Rotate 90` renders sideways; text drawn in the unrotated coordinate system will appear rotated in the viewer. Either rotate the stamp with `c.rotate(90)` and adjust the origin, or rotate the page back first.
54
+ - **Repeated stamping**: merging the same overlay twice doubles the opacity and positions identically; re-run from the original input when iterating.
55
+ - **Fonts**: Helvetica handles Latin; a CJK stamp needs a registered TTF, otherwise the text renders as blank boxes. Check the render.
56
+ - **Opacity**: `setFillAlpha` is applied by viewers that honour transparency groups; a stamp that must be visible on any viewer is safer with full opacity.
57
+
58
+ ## Verify
59
+
60
+ Render every stamped page and check: the text sits inside the margin, does not overlap body text or an existing footer, is the size the user asked for, and appears on exactly the selected pages. Then run `"<python>" "<cli>" pdf read --input "<file-stamped.pdf>"` — the stamp's text will join the page text, so a search for the label confirms it landed once, not twice.
@@ -1,110 +1,164 @@
1
1
  ---
2
- description: Create, inspect, edit, and render PowerPoint .pptx slide decks with the bundled Office helper CLI.
2
+ description: Build, inspect, restyle, and verify PowerPoint .pptx slide decks with python-pptx plus the bundled Office helper CLI.
3
3
  ---
4
4
 
5
5
  # Presentations (.pptx)
6
6
 
7
- Build local PowerPoint decks with native text, tables, charts, and images, then render them to verify layout. This skill uses `python-pptx`; it does not use the `@oai/artifact-tool` runtime and does not edit Google Slides.
7
+ A deck is a zip of XML parts: slides, layouts, masters, charts with embedded
8
+ workbooks, notes, media. Everything the audience sees comes from those parts, so
9
+ this pack teaches you to (a) author those parts correctly and (b) read them back
10
+ before delivering.
8
11
 
9
- ## Before any file work
12
+ Two layers:
10
13
 
11
- 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.
13
- 3. If no compatible Python is reported, stop and tell the user. Do not deliver a deck you could not create or verify.
14
+ - **The helper CLI** — a deterministic accelerator for common cases:
15
+ `<python> "<cli>" pptx {create|read|render}`, plus `office_status`. It creates
16
+ from a JSON spec, reads structure back, and renders. It does not mutate.
17
+ - **This pack** — the capability layer. Recipes for anything the CLI does not
18
+ cover ([`recipes/`](recipes/README.md)), runnable tools for mutation and gates
19
+ in [`container_tools/`](container_tools/), template-following in
20
+ [`template_following_scripts/`](template_following_scripts/).
14
21
 
15
- ## Create
22
+ Authoring is `python-pptx`. Not a page-description format, not a screenshot: real
23
+ shapes, real chart parts, real notes.
16
24
 
17
- ```bash
18
- "<python>" "<cli>" pptx create --spec "<spec.json>" --out "<output.pptx>"
19
- ```
25
+ ## Before any file work
20
26
 
21
- ```json
22
- {
23
- "title": "Annual Review",
24
- "author": "DSH Office Suite",
25
- "slides": [
26
- { "layout": "title", "title": "Annual Review", "subtitle": "FY2026 · Internal" },
27
- { "layout": "bullets", "title": "Highlights", "bullets": ["Revenue up 18.4%", { "text": "New markets", "level": 1 }] },
28
- {
29
- "layout": "blank",
30
- "title": "Revenue by quarter",
31
- "table": { "headers": ["Quarter", "Revenue"], "rows": [["Q1", 100], ["Q2", 128]] },
32
- "chart": {
33
- "type": "column",
34
- "title": "Quarterly revenue",
35
- "left": 3.6,
36
- "top": 3.5,
37
- "width": 8.8,
38
- "height": 3.0,
39
- "categories": ["Q1", "Q2"],
40
- "series": [{ "name": "Revenue", "values": [100, 128] }]
41
- },
42
- "image": { "path": "<absolute image path>", "left": 0.8, "top": 3.9, "width": 2.2 }
43
- }
44
- ]
45
- }
46
- ```
27
+ 1. Call the `office_status` tool.
28
+ 2. Read the `## Office runtime` JSON block appended to this file and use the
29
+ absolute `python`, `cli`, `skill` and `shared` values from it. Quote paths.
30
+ 3. If that block reports no working runtime, stop: tell the user to run the
31
+ `node "..."` remedy command it names, then call `office_status` again. Never
32
+ claim a deck was created or verified without a runtime to create and verify
33
+ it with.
47
34
 
48
- Layouts: `title`, `bullets`, `section`, `title_only`, `blank`. The deck is 16:9 (13.333 × 7.5 in).
35
+ ## Delivery
49
36
 
50
- - `bullets` uses the body placeholder; items may be strings or `{ "text", "level" }`.
51
- - `blank` / `title_only` place content with a vertical cursor starting at 1.5 in. A table advances the cursor; a chart without `top` starts at the cursor and gets a fitted height; an image without `top` starts at the cursor. Explicit `left`/`top`/`width`/`height` in inches are honored, so use them when combining elements side by side.
52
- - A blank slide with a `title` gets a generated title textbox (32 pt bold).
53
- - Chart types: `column`, `line`, `pie` (`bar` is accepted as a column alias). Each series needs a `name` and numeric `values`.
37
+ 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
38
 
55
- ## Read
39
+ - Creating a file: write it into the user's workspace, verify it, then `present`.
40
+ - 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.
56
41
 
57
- ```bash
58
- "<python>" "<cli>" pptx read --input "<file.pptx>"
59
- ```
42
+ ## Annotations
60
43
 
61
- Returns slide count, per-slide title, all text, tables, chart categories/series, and image counts. Use it to confirm edits and to inventory a deck you did not create.
44
+ 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.
62
45
 
63
- ## Edit
46
+ 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
47
 
65
- ```bash
66
- "<python>" "<cli>" pptx edit --input "<file.pptx>" --spec "<edit.json>" --out "<file-edited.pptx>"
67
- ```
68
-
69
- ```json
70
- {
71
- "replace": [{ "find": "18.4%", "replace": "21.0%" }],
72
- "slides": [{ "layout": "bullets", "title": "Next steps", "bullets": ["Finalize acceptance", "Publish docs"] }],
73
- "core": { "title": "Annual Review (final)" }
74
- }
75
- ```
48
+ **pptx mapping.** `page` is the preview slide index (1-based, the `slide-NN.png`
49
+ you rendered). The region selects shapes whose bounding box overlaps it:
50
+ `left_in = region.left * slide_width_in`, and likewise for top/width/height.
51
+ `text` disambiguates overlapping shapes (a title over a background panel).
52
+ Resolve candidates from
53
+ `container_tools/inspect_presentation_layout_geometry.py --json-out geometry.json`,
54
+ then apply the edit through a mutation tool or a recipe. Full procedure:
55
+ [tasks/apply-annotations.md](tasks/apply-annotations.md).
76
56
 
77
- `replace` edits text runs. It cannot edit text split across runs, chart data labels, notes, or tables; use table replacement only by recreating the table on a new slide. New slides are appended.
57
+ ## Verify
78
58
 
79
- ## Render and verify
59
+ Render, look at every page, then deliver — in that order, with nothing skipped.
80
60
 
81
61
  ```bash
82
- "<python>" "<cli>" pptx render --input "<file.pptx>" --out-dir "<render-dir>"
62
+ "<python>" "<skill>/container_tools/render_slides.py" --input "<file.pptx>" --out-dir "<render-dir>"
83
63
  ```
84
64
 
85
- The renderer prefers Microsoft PowerPoint COM on Windows and LibreOffice otherwise, producing a PDF and one PNG per slide. Inspect every slide for:
86
-
87
- - title and body within margins;
88
- - tables and charts not overlapping each other or images;
89
- - legible font sizes (body ≥ 18 pt, title ≥ 28 pt);
90
- - no clipped text or empty placeholders;
91
- - chart labels/categories correct.
92
-
93
- If the render shows overlap, fix coordinates and re-render. Do not claim a deck is presentation-ready if it was never rendered.
94
-
95
- ## Design rules
96
-
97
- - One idea per slide. Prefer 3–6 bullets; split dense content.
98
- - Use sentence case, parallel phrasing, and real figures only.
99
- - Use native tables/charts, not screenshots of spreadsheets where a native chart fits.
100
- - Keep a consistent palette and alignment grid; do not decorate with random shapes.
101
- - Choose `blank` for composed layouts and `bullets`/`title` for simple text slides.
102
-
103
- ## Not supported
104
-
105
- - Google Slides / Google Drive editing.
106
- - Slide master, theme, template, or layout editing.
107
- - Animations, transitions, speaker notes, hyperlinks, audio/video, SmartArt.
108
- - Editing existing chart data in place, or replacing table cells in place.
109
- - `@oai/artifact-tool` helpers, montage/integrity gates, or template-fidelity workflows.
110
- - PowerPoint-perfect pagination; rendering is a fidelity check.
65
+ `render_slides.py` uses the CLI's `pptx render` — which drives Microsoft
66
+ PowerPoint, WPS Presentation, or LibreOffice, whichever is installed — and falls
67
+ back to a draft preview when none is, so this one command is always the right
68
+ entry point. The JSON names the engine it used in `engine`.
69
+
70
+ Then open each PNG with `read_image` (or build one sheet with
71
+ `"<skill>/template_following_scripts/make_contact_sheet.py"`). Check: titles
72
+ one or two lines, body ≥ 18 pt, nothing clipped or outside the slide, no empty
73
+ placeholder, table and chart not overlapping, chart readable at thumbnail size.
74
+
75
+ If no office suite is installed, `render_slides.py` falls back to
76
+ `--engine draft`, a PyMuPDF geometry preview. It proves placement, not
77
+ typography — say which one you used.
78
+
79
+ Gates that print JSON with a `passed` field, all blocking when they say `false`:
80
+ `inspect_presentation_package_integrity.py`,
81
+ `inspect_presentation_layout_geometry.py`, `native_chart_title_gate.py`,
82
+ `native_quantitative_chart_gate.py`, `native_table_arithmetic_gate.py`,
83
+ `template_source_fidelity_gate.py`. Details:
84
+ [tasks/inspect-and-verify.md](tasks/inspect-and-verify.md).
85
+
86
+ Never present a deck you have not rendered and looked at.
87
+
88
+ ## Tasks
89
+
90
+ One file per job:
91
+
92
+ | Job | Start here |
93
+ |---|---|
94
+ | Create a deck | [tasks/create-deck.md](tasks/create-deck.md) |
95
+ | Edit an existing deck | [tasks/edit-deck.md](tasks/edit-deck.md) |
96
+ | Inspect, gate and verify | [tasks/inspect-and-verify.md](tasks/inspect-and-verify.md) |
97
+ | Follow a supplied template | [tasks/template-following.md](tasks/template-following.md) |
98
+ | Charts, tables, speaker notes | [tasks/charts-tables-notes.md](tasks/charts-tables-notes.md) |
99
+ | Apply an annotation payload | [tasks/apply-annotations.md](tasks/apply-annotations.md) |
100
+
101
+ ## Tools and recipes
102
+
103
+ Mutations (each prints one JSON object, exits non-zero on failure, defaults to
104
+ `<original>-edited.pptx`):
105
+
106
+ | Need | Command |
107
+ |---|---|
108
+ | Find/replace text, run-level | `"<skill>/container_tools/replace_text.py" --input "<deck>" --find "old" --replace "new"` |
109
+ | Append slides | `"<skill>/container_tools/add_slides.py" --input "<deck>" --spec "<slides.json>"` |
110
+ | Title/author/subject/keywords | `"<skill>/container_tools/core_properties.py" --input "<deck>" --title "..."` |
111
+ | Render (office, else draft) | `"<skill>/container_tools/render_slides.py" --input "<deck>" --out-dir "<dir>"` |
112
+ | Contact sheet / montage | `"<skill>/template_following_scripts/make_contact_sheet.py" --dir "<dir>" --out "<sheet.png>"` |
113
+
114
+ Template flow: `inspect_template_deck.py` → write a frame map →
115
+ `validate_template_plan.py` → `prepare_template_starter_deck.py` →
116
+ `check_template_fidelity.py`.
117
+
118
+ Recipes (paste-runnable, each with the pitfall it avoids):
119
+ [README](recipes/README.md) ·
120
+ [textbox at a coordinate](recipes/textbox-at-coordinate.md) ·
121
+ [auto-layout](recipes/autolayout.md) ·
122
+ [tables](recipes/tables.md) ·
123
+ [native charts](recipes/native-charts.md) ·
124
+ [masters and layouts](recipes/master-and-layouts.md) ·
125
+ [speaker notes](recipes/speaker-notes.md) ·
126
+ [theme tokens](recipes/theme-tokens.md) ·
127
+ [rich text runs](recipes/rich-text-runs.md) ·
128
+ [images](recipes/images.md) ·
129
+ [connectors](recipes/connectors.md).
130
+
131
+ Guidance: [authoring workflow](references/authoring-workflow.md) ·
132
+ [style](references/style-guidelines.md) ·
133
+ [bullets](references/native-bullets.md) ·
134
+ [evidence](references/native-evidence.md) ·
135
+ [templates](references/templates.md) ·
136
+ [finalization](references/finalization.md) ·
137
+ [finance decks](domain_guidance/finance.md) ·
138
+ [Google Slides hand-off](routing/google_slides.md).
139
+
140
+ ## Non-negotiables
141
+
142
+ - One idea per slide; a title is a claim, not a label.
143
+ - Body ≥ 18 pt, title ≥ 28 pt, captions ≥ 12 pt.
144
+ - Native charts and tables, never a picture of a spreadsheet.
145
+ - Every placeholder filled or deleted; no "Chart Title", no trailing full stop
146
+ on a heading.
147
+ - Numbers in the deck must agree with the numbers that were supplied.
148
+ - New output lives in the user's workspace; an edit is a new file.
149
+
150
+ ## Genuine gaps
151
+
152
+ - No Microsoft Office, WPS Office, or LibreOffice installed → no true render;
153
+ use `--engine draft` and say the check was geometric. Installing any of them
154
+ fixes it.
155
+ - Fonts the user's machine lacks are substituted by any renderer; the pack
156
+ cannot install fonts.
157
+ - Google Slides import fidelity is out of our control; see
158
+ [routing/google_slides.md](routing/google_slides.md).
159
+
160
+ ## Shared workflow
161
+
162
+ Use `"<shared>/common-workflow.md"` for the cross-format delivery and
163
+ verification loop, and `"<shared>/annotation-recipes.md"` when turning an
164
+ annotation payload into an edit.
@@ -0,0 +1,136 @@
1
+ """Append slides to an existing deck using the shared slide builder.
2
+
3
+ The slide builder lives in `python/office_cli.py` (`_pptx_add_slide`), so this
4
+ tool imports it instead of reimplementing titles, bullet bodies, tables, charts
5
+ and pictures. Anything `pptx create --spec` accepts inside `slides[]` is
6
+ accepted here.
7
+
8
+ Usage
9
+ -----
10
+ <python> add_slides.py --input deck.pptx --spec extra-slides.json
11
+ <python> add_slides.py --input deck.pptx --slide '{"layout":"bullets","title":"Next"}' --out deck-2.pptx
12
+
13
+ Exit codes
14
+ ----------
15
+ 0 slides appended
16
+ 2 bad arguments, invalid spec, or a slide that the builder rejected
17
+ 3 the output file could not be written
18
+
19
+ The JSON report on stdout is `{ok, input, output, added, slides, counts}`.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import json
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ PACKAGE_ROOT = Path(__file__).resolve().parents[4]
29
+ CLI_DIR = PACKAGE_ROOT / "python"
30
+ COUNTERS = ("slides", "bullets", "tables", "charts", "images")
31
+
32
+
33
+ def _emit(payload: dict, code: int = 0) -> int:
34
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
35
+ return code
36
+
37
+
38
+ def _load_slides(spec_path: str | None, inline: list[str]) -> list[dict]:
39
+ slides: list[dict] = []
40
+ if spec_path:
41
+ data = json.loads(Path(spec_path).read_text(encoding="utf-8"))
42
+ if isinstance(data, dict):
43
+ slides.extend(data.get("slides") or [])
44
+ elif isinstance(data, list):
45
+ slides.extend(data)
46
+ else:
47
+ raise ValueError("spec must be an object with 'slides' or a list of slides")
48
+ for raw in inline:
49
+ slides.append(json.loads(raw))
50
+ for slide in slides:
51
+ if not isinstance(slide, dict):
52
+ raise ValueError(f"slide must be an object: {slide!r}")
53
+ return slides
54
+
55
+
56
+ def main() -> int:
57
+ parser = argparse.ArgumentParser(
58
+ description="Append slides to a .pptx with the same builder the helper CLI uses.",
59
+ epilog="Exit codes: 0 appended, 2 bad input, 3 write failure.",
60
+ )
61
+ parser.add_argument("--input", required=True, help="deck to extend")
62
+ parser.add_argument("--spec", help="JSON file: {slides:[...]} or a list of slides")
63
+ parser.add_argument("--slide", action="append", default=[], help="one slide as inline JSON (repeatable)")
64
+ parser.add_argument("--out", help="output deck (default: <input>-edited.pptx)")
65
+ args = parser.parse_args()
66
+
67
+ source = Path(args.input)
68
+ if not source.is_file():
69
+ return _emit({"ok": False, "error": f"no such file: {source}"}, 2)
70
+ output = Path(args.out) if args.out else source.with_name(f"{source.stem}-edited{source.suffix}")
71
+
72
+ try:
73
+ slides = _load_slides(args.spec, args.slide)
74
+ except (OSError, ValueError, json.JSONDecodeError) as error:
75
+ return _emit({"ok": False, "error": f"bad spec: {error}"}, 2)
76
+ if not slides:
77
+ return _emit({"ok": False, "error": "nothing to add: pass --spec or --slide"}, 2)
78
+
79
+ if not CLI_DIR.is_dir():
80
+ return _emit({"ok": False, "error": f"helper CLI not found at {CLI_DIR}"}, 2)
81
+ sys.path.insert(0, str(CLI_DIR))
82
+ try:
83
+ import office_cli # noqa: PLC0415 - optional dependency, resolved at run time
84
+ except ImportError as error:
85
+ return _emit({"ok": False, "error": f"cannot import the slide builder: {error}"}, 2)
86
+
87
+ from pptx import Presentation # noqa: PLC0415
88
+
89
+ try:
90
+ prs = Presentation(str(source))
91
+ except Exception as error: # noqa: BLE001
92
+ return _emit({"ok": False, "error": f"cannot open {source}: {error}"}, 2)
93
+
94
+ before = len(prs.slides)
95
+ counters = {key: 0 for key in COUNTERS}
96
+ added: list[dict] = []
97
+ for slide_spec in slides:
98
+ try:
99
+ slide = office_cli._pptx_add_slide(prs, slide_spec, counters) # noqa: SLF001
100
+ except Exception as error: # noqa: BLE001 - report which slide failed
101
+ return _emit(
102
+ {"ok": False, "error": f"slide {len(added) + 1} rejected: {error}", "added": added},
103
+ 2,
104
+ )
105
+ added.append(
106
+ {
107
+ "position": len(prs.slides),
108
+ "layout": str(slide_spec.get("layout", "bullets")),
109
+ "title": slide_spec.get("title"),
110
+ "placeholder_count": len(slide.placeholders),
111
+ }
112
+ )
113
+
114
+ try:
115
+ output.parent.mkdir(parents=True, exist_ok=True)
116
+ prs.save(str(output))
117
+ except Exception as error: # noqa: BLE001
118
+ return _emit({"ok": False, "error": f"cannot write {output}: {error}"}, 3)
119
+
120
+ return _emit(
121
+ {
122
+ "schema": "add_slides/1",
123
+ "ok": True,
124
+ "input": str(source),
125
+ "output": str(output),
126
+ "added": len(added),
127
+ "before": before,
128
+ "slides": len(prs.slides),
129
+ "counts": counters,
130
+ "details": added,
131
+ }
132
+ )
133
+
134
+
135
+ if __name__ == "__main__":
136
+ sys.exit(main())