dsh-office-kit 0.4.2 → 0.4.3

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 (113) hide show
  1. package/assets/skills/excel-live-control/SKILL.md +119 -152
  2. package/assets/skills/excel-live-control/domain_guidance/financial_models.md +42 -58
  3. package/assets/skills/excel-live-control/domain_guidance/healthcare.md +36 -42
  4. package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +33 -34
  5. package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +34 -36
  6. package/assets/skills/excel-live-control/references/office-automation.md +127 -153
  7. package/assets/skills/excel-live-control/references/read_only_qna.md +40 -45
  8. package/assets/skills/excel-live-control/style_guidelines.md +56 -64
  9. package/assets/skills/excel-live-control/workflows/create_workflows.md +40 -47
  10. package/assets/skills/excel-live-control/workflows/edit_workflows.md +37 -45
  11. package/assets/skills/office-documents/SKILL.md +24 -60
  12. package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +5 -5
  13. package/assets/skills/office-documents/ooxml/comments.md +8 -8
  14. package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +10 -10
  15. package/assets/skills/office-documents/ooxml/rels_and_content_types.md +7 -7
  16. package/assets/skills/office-documents/ooxml/tracked_changes.md +12 -12
  17. package/assets/skills/office-documents/references/docx-structure.md +11 -13
  18. package/assets/skills/office-documents/references/template-elicitation.md +11 -13
  19. package/assets/skills/office-documents/references/writing-quality.md +20 -20
  20. package/assets/skills/office-documents/tasks/accessibility_a11y.md +16 -22
  21. package/assets/skills/office-documents/tasks/captions_crossrefs.md +23 -26
  22. package/assets/skills/office-documents/tasks/clean_tracked_changes.md +17 -22
  23. package/assets/skills/office-documents/tasks/comments_manage.md +9 -11
  24. package/assets/skills/office-documents/tasks/compare_diff.md +10 -16
  25. package/assets/skills/office-documents/tasks/create_edit.md +14 -16
  26. package/assets/skills/office-documents/tasks/equations.md +17 -28
  27. package/assets/skills/office-documents/tasks/fields_update.md +14 -16
  28. package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +19 -33
  29. package/assets/skills/office-documents/tasks/footnotes_endnotes.md +12 -14
  30. package/assets/skills/office-documents/tasks/forms_content_controls.md +16 -20
  31. package/assets/skills/office-documents/tasks/headings_numbering.md +17 -22
  32. package/assets/skills/office-documents/tasks/images_figures.md +15 -25
  33. package/assets/skills/office-documents/tasks/multi_doc_merge.md +13 -23
  34. package/assets/skills/office-documents/tasks/navigation_internal_links.md +18 -21
  35. package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +16 -22
  36. package/assets/skills/office-documents/tasks/protection_restrict_editing.md +19 -21
  37. package/assets/skills/office-documents/tasks/read_review.md +9 -20
  38. package/assets/skills/office-documents/tasks/redaction_anonymization.md +17 -18
  39. package/assets/skills/office-documents/tasks/sections_layout.md +16 -23
  40. package/assets/skills/office-documents/tasks/style_lint_normalize.md +18 -22
  41. package/assets/skills/office-documents/tasks/tables_spreadsheets.md +21 -31
  42. package/assets/skills/office-documents/tasks/templates_style_packs.md +17 -21
  43. package/assets/skills/office-documents/tasks/toc_workflow.md +10 -14
  44. package/assets/skills/office-documents/tasks/verify_render.md +11 -23
  45. package/assets/skills/office-documents/tasks/watermarks_background.md +11 -15
  46. package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +6 -6
  47. package/assets/skills/office-documents/troubleshooting/run_splitting.md +12 -12
  48. package/assets/skills/office-pdf/SKILL.md +36 -51
  49. package/assets/skills/office-pdf/references/form-fields.md +20 -20
  50. package/assets/skills/office-pdf/references/geometry.md +10 -10
  51. package/assets/skills/office-pdf/references/layout-quality.md +13 -13
  52. package/assets/skills/office-pdf/references/limits.md +12 -56
  53. package/assets/skills/office-pdf/tasks/annotations.md +5 -5
  54. package/assets/skills/office-pdf/tasks/create.md +6 -9
  55. package/assets/skills/office-pdf/tasks/edit-pages.md +14 -14
  56. package/assets/skills/office-pdf/tasks/forms.md +15 -15
  57. package/assets/skills/office-pdf/tasks/read.md +9 -9
  58. package/assets/skills/office-pdf/tasks/redact.md +18 -18
  59. package/assets/skills/office-pdf/tasks/stamp.md +10 -10
  60. package/assets/skills/office-presentations/SKILL.md +71 -91
  61. package/assets/skills/office-presentations/container_tools/render_slides.py +1 -1
  62. package/assets/skills/office-presentations/domain_guidance/finance.md +16 -16
  63. package/assets/skills/office-presentations/recipes/README.md +24 -35
  64. package/assets/skills/office-presentations/recipes/autolayout.md +12 -13
  65. package/assets/skills/office-presentations/recipes/connectors.md +11 -12
  66. package/assets/skills/office-presentations/recipes/images.md +11 -12
  67. package/assets/skills/office-presentations/recipes/master-and-layouts.md +18 -18
  68. package/assets/skills/office-presentations/recipes/native-charts.md +8 -8
  69. package/assets/skills/office-presentations/recipes/rich-text-runs.md +13 -13
  70. package/assets/skills/office-presentations/recipes/speaker-notes.md +7 -7
  71. package/assets/skills/office-presentations/recipes/tables.md +14 -14
  72. package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +9 -9
  73. package/assets/skills/office-presentations/recipes/theme-tokens.md +7 -8
  74. package/assets/skills/office-presentations/references/authoring-workflow.md +38 -43
  75. package/assets/skills/office-presentations/references/finalization.md +10 -12
  76. package/assets/skills/office-presentations/references/native-bullets.md +14 -18
  77. package/assets/skills/office-presentations/references/native-evidence.md +18 -32
  78. package/assets/skills/office-presentations/references/style-guidelines.md +27 -32
  79. package/assets/skills/office-presentations/references/templates.md +31 -60
  80. package/assets/skills/office-presentations/routing/google_slides.md +4 -4
  81. package/assets/skills/office-presentations/tasks/apply-annotations.md +17 -17
  82. package/assets/skills/office-presentations/tasks/charts-tables-notes.md +20 -23
  83. package/assets/skills/office-presentations/tasks/create-deck.md +19 -19
  84. package/assets/skills/office-presentations/tasks/edit-deck.md +14 -16
  85. package/assets/skills/office-presentations/tasks/inspect-and-verify.md +31 -34
  86. package/assets/skills/office-presentations/tasks/template-following.md +23 -28
  87. package/assets/skills/office-spreadsheets/SKILL.md +32 -39
  88. package/assets/skills/office-spreadsheets/recipes/charts.md +8 -26
  89. package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +9 -26
  90. package/assets/skills/office-spreadsheets/recipes/data-tables.md +7 -23
  91. package/assets/skills/office-spreadsheets/recipes/named-ranges.md +8 -21
  92. package/assets/skills/office-spreadsheets/recipes/number-formats.md +7 -23
  93. package/assets/skills/office-spreadsheets/recipes/sparklines.md +7 -20
  94. package/assets/skills/office-spreadsheets/references/charts.md +15 -34
  95. package/assets/skills/office-spreadsheets/references/domain/financial-models.md +10 -25
  96. package/assets/skills/office-spreadsheets/references/domain/healthcare.md +8 -19
  97. package/assets/skills/office-spreadsheets/references/domain/marketing.md +8 -22
  98. package/assets/skills/office-spreadsheets/references/domain/research.md +8 -19
  99. package/assets/skills/office-spreadsheets/references/formula-patterns.md +20 -50
  100. package/assets/skills/office-spreadsheets/references/style-guidelines.md +19 -44
  101. package/assets/skills/office-spreadsheets/references/workbook-structure.md +12 -35
  102. package/assets/skills/office-spreadsheets/routing/google-sheets.md +11 -26
  103. package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +10 -25
  104. package/assets/skills/office-spreadsheets/tasks/annotations.md +12 -30
  105. package/assets/skills/office-spreadsheets/tasks/create-workbook.md +5 -5
  106. package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +2 -2
  107. package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +5 -5
  108. package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +9 -9
  109. package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +10 -30
  110. package/assets/skills/shared/annotation-recipes.md +98 -120
  111. package/assets/skills/shared/common-workflow.md +70 -80
  112. package/dist/index.js +1 -1
  113. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  # Merge, split, reorder, rotate, delete pages
2
2
 
3
- Pages are objects referenced from a page tree; changing the order or removing a page changes references, not content. Nothing is re-flowed, so a page moved into a document with a different page size keeps its own box and fonts.
3
+ Pages are referenced from a page tree; reordering or removing one changes references, not content. Nothing is re-flowed, so a page moved into a differently sized document keeps its own box and fonts.
4
4
 
5
5
  ## Fast path: `scripts/page_ops.py`
6
6
 
@@ -9,14 +9,14 @@ Pages are objects referenced from a page tree; changing the order or removing a
9
9
  --rotate 90 --pages 0,2 --remove 3 --merge "<other.pdf>" --order 2,0,1
10
10
  ```
11
11
 
12
- - Operations run in a fixed order: merge, then order, then remove, then rotate — a command means the same thing every time.
13
- - Every index is zero-based and addresses the combined page list (input pages first, appended pages after).
14
- - `--order` keeps and reorders; pages it does not list are dropped, which also makes it the duplicate/select tool when you repeat an index.
15
- - `--remove` drops indices from the list left by `--order`.
12
+ - Fixed operation order: merge, then order, then remove, then rotate — a command always means the same thing.
13
+ - Indices are zero-based over the combined page list (input pages first, appended pages after).
14
+ - `--order` keeps and reorders; unlisted pages are dropped, and a repeated index duplicates it.
15
+ - `--remove` drops indices from the list `--order` leaves.
16
16
  - `--rotate` takes 90, 180, or 270 clockwise and defaults to every remaining page; `--pages` limits it.
17
17
  - `--out` defaults to `<input>-edited.pdf`; the input is never overwritten.
18
18
 
19
- The script carries page content streams over as they are and reopens the result to report page count, rotation, and page size per page. When one call cannot express the operation, chain two calls into two output files rather than reusing a name.
19
+ The script carries content streams over unchanged and reopens the result to report page count, rotation, and page size per page. When one call cannot express the operation, chain two calls into two output files rather than reusing a name.
20
20
 
21
21
  ## Metadata
22
22
 
@@ -24,7 +24,7 @@ The script carries page content streams over as they are and reopens the result
24
24
  "<python>" "<skill>/scripts/metadata.py" --input "<file.pdf>" --set "/Title=Field report" --set "/Author=Ada" --out "<file-edited.pdf>"
25
25
  ```
26
26
 
27
- Keys are the document information fields (`Title`, `Author`, `Subject`, `Keywords`, `Creator`, `Producer`, `CreationDate`, `ModDate`), plain or `/Name`, in any case. The script writes a new file and reopens it, so `applied` and `not_applied` tell you what actually persisted.
27
+ Keys are the document information fields (`Title`, `Author`, `Subject`, `Keywords`, `Creator`, `Producer`, `CreationDate`, `ModDate`), plain or `/Name`, in any case. The script writes a new file and reopens it; `applied`/`not_applied` show what persisted.
28
28
 
29
29
  ## Split
30
30
 
@@ -32,7 +32,7 @@ Keys are the document information fields (`Title`, `Author`, `Subject`, `Keyword
32
32
  "<python>" "<skill>/scripts/split_pages.py" --input "<file.pdf>" --out-dir "<parts>" --ranges "1-2,3,5-"
33
33
  ```
34
34
 
35
- 1-based inclusive ranges, `--every N` for equal chunks, default one file per page. The script copies the source metadata, refuses overlapping or out-of-range ranges, and fails if the parts do not add up to the source page count — that check is what catches a typo in the range string. `--prefix` sets the filename stem.
35
+ 1-based inclusive ranges, or `--every N` for equal chunks; the default is one file per page. The script copies the source metadata, refuses overlapping or out-of-range ranges, and fails when the parts do not add up to the source page count — the check that catches a typo in the range string. `--prefix` sets the filename stem.
36
36
 
37
37
  ## Direct page-tree work
38
38
 
@@ -53,19 +53,19 @@ print("pages", len(writer.pages))
53
53
  PY
54
54
  ```
55
55
 
56
- `add_page` copies the page and its resources into the new file; annotations that point across pages (links) can break when their target is removed. After a reorder or deletion, check the links and bookmarks on the render, not only the text.
56
+ `add_page` copies the page and its resources; annotations pointing across pages (links) can break when their target is removed. After a reorder or deletion, check links and bookmarks on the render, not only the text.
57
57
 
58
58
  ## Rotate
59
59
 
60
- Page rotation is a `/Rotate` entry (multiples of 90) applied by viewers; it does not transpose the content stream. `page.rotate(90)` in pypdf, or `page_ops.py --rotate 90 --pages ...`, is enough. If a page was scanned sideways and also carries a stale `/Rotate`, the two add up — the render shows whether you are looking at the wrong one, and `scripts/render_pages.py` reports each page's rotation.
60
+ Page `/Rotate` (multiples of 90) is applied by viewers and does not transpose the content stream. `page.rotate(90)` in pypdf, or `page_ops.py --rotate 90 --pages ...`, is enough. A page scanned sideways that also carries a stale `/Rotate` accumulates both — the render shows which one you are looking at, and `scripts/render_pages.py` reports each page's rotation.
61
61
 
62
62
  ## Verify
63
63
 
64
64
  After any structural edit:
65
65
 
66
66
  - compare page counts (`pdf read` or the split report) — one missing page is easy to miss in a long render;
67
- - render at least the first and last page of every part, and every page you rotated or reordered;
68
- - for merges, confirm the page size and orientation of the appended pages did not change, and that page numbers or bates stamps do not collide;
69
- - for deletions of numbered pages, tell the user which numbers were removed, because the remaining page labels will shift.
67
+ - render the first and last page of every part, plus every page you rotated or reordered;
68
+ - for merges, confirm appended pages kept their size and orientation, and that page numbers or bates stamps do not collide;
69
+ - for deletions of numbered pages, tell the user which numbers were removed, because the remaining page labels shift.
70
70
 
71
- `scripts/render_pages.py --input "<file.pdf>" --region 0,0,1,1 --json-out ...` prints per-page character and drawing counts, which is a cheap way to confirm a page is not blank before you deliver.
71
+ `scripts/render_pages.py --input "<file.pdf>" --region 0,0,1,1 --json-out ...` prints per-page character and drawing counts — a cheap blank-page check before delivery.
@@ -1,6 +1,6 @@
1
1
  # AcroForm inspection and filling
2
2
 
3
- A form PDF carries `/AcroForm` in the catalog. Its `/Fields` array holds field dictionaries: `/T` the partial name, `/FT` the type (`Tx` text, `Btn` button, `Ch` choice, `Sig` signature), `/V` the value, `/Ff` flags, `/DA` the default appearance, and `/AP` appearance streams. A widget is the field's visual rectangle (`/Rect`, `/P` parent page, `/AS` its currently selected state name). A field can own several widgets (radio groups), and a widget can be a separate object from its field, reached through `/Parent`. Text in a filled field is usually not in the page content stream — it lives in the widget's `/AP /N` appearance, which is why "the value is in `/V`" and "the page shows the value" are two different facts to check.
3
+ A form PDF carries `/AcroForm` in the catalog. `/Fields` holds field dictionaries: `/T` name, `/FT` type, `/V` value, `/Ff` flags, `/DA` default appearance, `/AP` appearance stream. Widgets carry `/Rect`, `/P`, `/AS`, and a widget can be a separate object reached through `/Parent`. Full field mechanics: `references/form-fields.md`. Text in a filled field usually lives in the widget's `/AP /N`, not the page content stream, so "value in `/V`" and "page shows the value" are two different facts.
4
4
 
5
5
  ## Inspect first
6
6
 
@@ -8,7 +8,7 @@ A form PDF carries `/AcroForm` in the catalog. Its `/Fields` array holds field d
8
8
  "<python>" "<skill>/scripts/forms.py" inspect --input "<file.pdf>"
9
9
  ```
10
10
 
11
- Reports, per field: id/name, type, value, read-only/required, widget count, page numbers, current states for buttons, and whether each widget has an `/AP` stream. Read the whole report before touching values: duplicate names, missing appearance streams, and read-only flags are the usual reason a fill "does nothing".
11
+ Reports per field: name, type, value, read-only/required, widget count, pages, button states, and whether each widget has `/AP`. Read it all before touching values: duplicate names, missing appearances, and read-only flags are why a fill "does nothing".
12
12
 
13
13
  ## Fill
14
14
 
@@ -20,20 +20,20 @@ Reports, per field: id/name, type, value, read-only/required, widget count, page
20
20
  { "full_name": "Ada Lovelace", "email": "ada@example.org", "agree": true, "plan": "/basic" }
21
21
  ```
22
22
 
23
- Text fields take strings, checkboxes take booleans or the state name, radio groups take the state you want selected, choice fields take a string or list. The dictionary is keyed by field name (`/T`, nested names joined with dots). The script fails with a non-zero exit when a name does not exist instead of silently skipping it, when a value is not one of the allowed states, and when the saved file fails the verification below. Pass `--flatten` to bake the values into the page and drop the form.
23
+ Text fields take strings; checkboxes booleans or the state name; radio groups the state to select; choice fields a string or list. Keys are field names (`/T`, nested names joined with dots). `--flatten` bakes the values in and drops the form. The script exits non-zero on an unknown name (it never skips silently), a value outside the allowed states, or a saved file that fails the verification below.
24
24
 
25
25
  ## The discipline that makes a fill real
26
26
 
27
- pypdf writes `/V` and rebuilds the widget appearance. Both must be re-checked after saving, because a writer can update one and not the other:
27
+ pypdf writes `/V` and rebuilds the widget appearance; a writer can do one without the other, so check both after saving.
28
28
 
29
29
  1. Reopen the saved file.
30
- 2. For every field you set, read `/V` from the field dictionary and compare it with the requested value.
31
- 3. For every widget, confirm `/AP /N` exists and is a non-empty stream with a `/BBox`; for radio and checkbox groups, confirm `/AS` names a state that exists in `/AP /N`.
30
+ 2. Read `/V` for every field you set and compare it with the requested value.
31
+ 3. Confirm every widget has a non-empty `/AP /N` stream with a `/BBox`; for radio and checkbox groups, `/AS` must name a state present in `/AP /N`.
32
32
  4. Render the page and look at the field.
33
33
 
34
- Steps 1–3 catch a stale or missing appearance; step 4 catches a `/V` that is right while the page still shows the old text. The script does 1–3 itself and prints `applied`/`not_applied`; treat `not_applied` as a failure to fix, never as a warning.
34
+ Steps 1–3 catch a stale or missing appearance; step 4 catches a right `/V` over old text. The script does 1–3 and prints `applied`/`not_applied`; `not_applied` is a failure to fix, not a warning.
35
35
 
36
- When you build the check by hand:
36
+ By hand:
37
37
 
38
38
  ```bash
39
39
  "<python>" - <<'PY'
@@ -49,19 +49,19 @@ for name, field in fields.items():
49
49
  PY
50
50
  ```
51
51
 
52
- Radio groups are the common failure: pypdf updates the group `/V`, but the widget-level `/AS` for the previously selected button can stay behind, so two buttons look selected. The script resynchronises every widget's `/AS` from the group value; if you fill by hand, do the same.
52
+ Radio groups fail most: pypdf updates the group `/V` but leaves the old button's `/AS`, so two look selected. The script resyncs every widget's `/AS` from the group value; do the same by hand.
53
53
 
54
54
  ## Flatten
55
55
 
56
- Flattening is for files that must no longer be editable. Update with `flatten=True` (the value is painted into the page content), then remove every widget annotation (`subtypes="/Widget"`) and drop `/AcroForm` from the catalog. Verify afterwards: no `/Widget` annotations remain and the catalog has no `/AcroForm`. If widgets remain, the file still looks editable in readers even though the content carries the value.
56
+ Flatten when the file must stay uneditable: fill with `flatten=True` (the value is painted into page content), remove every widget annotation (`subtypes="/Widget"`), and drop `/AcroForm` from the catalog. Then confirm no `/Widget` and no `/AcroForm` remain; otherwise readers still treat the file as editable.
57
57
 
58
58
  ## What cannot be filled, and the fix
59
59
 
60
- - **A field with no appearance stream and a broken `/DA`**: the reader may refuse to show a new value. Render after filling; if the value is invisible, rebuild that widget's appearance with reportlab and merge it over the page.
61
- - **A signature field (`/FT /Sig`)**: signing requires a certificate and a signer implementation that this runtime does not ship. Adding text to a signed field invalidates the signature. Tell the user the signature cannot be created here and what would (a certificate plus a signing tool).
62
- - **A form where the values are drawn as static text, not widgets**: the file is not a form. Inspect with `page.get_text()` — if the labels and lines are plain content and `reader.get_fields()` is empty, treat it as an edit job, not a fill job.
63
- - **A flat, scanned form**: no field tree exists; filling means drawing text at coordinates (a stamp overlay) and the render decides whether it lines up.
60
+ - **No appearance stream and a broken `/DA`**: the reader may not show a new value. Render after filling; if invisible, rebuild that widget's appearance with reportlab and merge it over the page.
61
+ - **A signature field (`/FT /Sig`)**: needs a certificate and a signer this runtime does not ship, and text on a signed field invalidates the signature. Say it cannot be created here; it needs a certificate plus a signing tool.
62
+ - **Values drawn as static text**: not a form. If `page.get_text()` shows the labels and lines and `reader.get_fields()` is empty, treat it as an edit job.
63
+ - **A flat, scanned form**: no field tree; draw text at coordinates (a stamp overlay) and let the render decide whether it lines up.
64
64
 
65
65
  ## Verify
66
66
 
67
- Always: reopen, check `/V`, check `/AP`, then `"<python>" "<cli>" pdf render --input "<file-filled.pdf>" --out-dir "render"` and `read_image` the page. The filled value must be visible inside its widget box, without clipping or overlapping the box border.
67
+ Reopen, check `/V`, check `/AP`, then `"<python>" "<cli>" pdf render --input "<file-filled.pdf>" --out-dir "render"` and `read_image`. The value must be visible inside its widget box, not clipped or over the border.
@@ -1,6 +1,6 @@
1
1
  # Read text, tables, and page images
2
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.
3
+ Extraction reads the page content stream; glyph runs carry position and font, which a library groups into words and lines. A page without text operators (a scan, a photo export) yields nothing, whatever the extractor — a property of the file, not of the tool.
4
4
 
5
5
  ## Fast path: plain text and structure
6
6
 
@@ -16,7 +16,7 @@ Returns `page_count`, `word_count`, document metadata, per-page text (with chara
16
16
  "<python>" "<skill>/scripts/extract_text.py" --input "<file.pdf>" --json-out "<tables.json>"
17
17
  ```
18
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.
19
+ Detection uses the page's ruling lines and word spacing, so a borderless table often comes back as running text; `--layout` keeps the original line spacing for a manual look.
20
20
 
21
21
  When the detection looks wrong, inspect rather than guess:
22
22
 
@@ -32,11 +32,11 @@ with pdfplumber.open("in.pdf") as pdf:
32
32
  PY
33
33
  ```
34
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.
35
+ `chars`, `lines`, and `rects` tell you whether the table has rules at all. With no lines, read it from `extract_text` with `--layout`, where column gaps survive as spaces.
36
36
 
37
37
  ## Page images
38
38
 
39
- A page may contain embedded rasters that are invisible in text extraction. List them with PyMuPDF:
39
+ A page may embed rasters that text extraction does not show. List them with PyMuPDF:
40
40
 
41
41
  ```bash
42
42
  "<python>" - <<'PY'
@@ -48,15 +48,15 @@ for number, page in enumerate(doc, 1):
48
48
  PY
49
49
  ```
50
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.
51
+ Extract one with `page.get_pixmap(clip=...)` or `doc.extract_image(xref)` when the user needs the picture itself. To *see* it, render the page at 150–200 dpi and crop with `read_image`.
52
52
 
53
53
  ## What fails and how to resolve it
54
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.
55
+ - **Scanned page, no text**: no text layer exists; say so, with resolutions from `references/limits.md`. Never claim extraction succeeded on an empty result.
56
+ - **Empty string from a page that shows text**: broken ToUnicode map, or text drawn as vector outlines. Check the render; outlines cannot be recovered by extraction.
57
+ - **Wrong reading order in multi-column layouts**: extraction follows operator order. Crop each column with a rect and extract per column; `get_textbox(rect)` on a PyMuPDF page is the simplest split.
58
58
  - **Ligatures and soft hyphens**: extraction may glue or split words; quote extracted text only after reading the same passage on the render.
59
59
 
60
60
  ## Verify
61
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.
62
+ The render is the check: the characters you return must be visible in it. Before building on extracted text, check for a page whose text is suspiciously short (missing text layer), duplicated (headers repeated per page), or full of replacement glyphs (broken encoding).
@@ -1,22 +1,22 @@
1
- # Remove a region of a page
1
+ # Redact a region of a page
2
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.
3
+ Redaction deletes glyphs and vector paths inside the rectangle from the content stream, so no extractor recovers them. A black box that covers text is not a redaction: "black out then flatten" leaks the original.
4
4
 
5
5
  ## Fast path: the region script
6
6
 
7
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}}' \
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
10
  --json-out "<redact.json>"
11
11
  ```
12
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.
13
+ The payload is a 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`, per-page before/after counts, and `leftover_chars_in_rect`. `--region`/`--page` take single values without JSON; multiple annotations apply page by page.
14
14
 
15
- ## Why the rectangle needs padding
15
+ ## Padding
16
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.
17
+ Glyphs overhang the geometric text box and preview boxes are approximate. Expand by 2–3 points to catch ascenders, descenders, rules touching the text, and the anti-aliased edge without touching neighbours. Measure: `get_textbox(rect)` lists what the rectangle takes; `leftover_chars_in_rect` must be zero afterwards.
18
18
 
19
- Do it by hand when you need a different shape or a dry run:
19
+ By hand, for another shape or a dry run:
20
20
 
21
21
  ```bash
22
22
  "<python>" - <<'PY'
@@ -40,26 +40,26 @@ PY
40
40
 
41
41
  ## The flags are not interchangeable
42
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.
43
+ - `fill=None` erases without painting. `fill=(1, 1, 1)` paints a rectangle that becomes a new drawing and hides the page's background tint; the drawing count goes up, not down.
44
+ - `images=PDF_REDACT_IMAGE_NONE` leaves pictures untouched, so a scanned region stays visible. `PDF_REDACT_IMAGE_PIXELS` blanks the pixels a rectangle covers inside an image; use a stronger setting only when the whole image should go.
45
+ - `graphics=PDF_REDACT_LINE_ART_REMOVE_IF_TOUCHED` deletes intersecting paths; paths entirely inside are always removed. `PDF_REDACT_LINE_ART_NONE` keeps all line art, for text-only removal.
46
+ - Text removal is the default (`PDF_REDACT_TEXT_REMOVE`); keeping text while erasing its surroundings is almost never what the user means.
47
47
 
48
48
  ## Counting the result
49
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.
50
+ `removed_chars` is the page-level difference in extracted text; `region_chars_removed` is what was inside the rectangle. They differ by the newline or space joining the block to its neighbours: expect equal or one apart. Fewer characters removed than the region held means the rectangle missed part of the block — enlarge `--pad` or redo the geometry.
51
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.
52
+ Sample: page 1 region `{left: 0.064, top: 0.318, width: 0.891, height: 0.153}` deletes the "Latest News" block alone — 16 drawings → 5, page text 1049 → 879 (block 169), `leftover_chars_in_rect` 0, and the body paragraph, header, and footer still extract and render.
53
53
 
54
54
  ## Leftovers redaction does not cover
55
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.
56
+ - **Metadata and embedded files**: the info dictionary and attachments are not page content; clear them for a clean file (`scripts/metadata.py`). An ID/xref may still fingerprint the file.
57
+ - **Previous revisions**: incrementally saved files keep old objects; save with `garbage=3, deflate=True`. For forensic certainty, say the output was rebuilt and confirm by text search that the words are gone.
58
+ - **Images inside the region**: they stay by default. For a screenshot of the sensitive text, use `--images pixels` or rebuild the page.
59
59
 
60
60
  ## Verify
61
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:
62
+ Render the page: the region empty, the surrounding text intact, and ruled boxes of the block gone rather than half-erased. Then search the output for the removed words:
63
63
 
64
64
  ```bash
65
65
  "<python>" - <<'PY'
@@ -1,6 +1,6 @@
1
1
  # Stamp a footer, header, or label
2
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.
3
+ An overlay is a small one-page PDF drawn with reportlab and merged onto the target page, so the original content stream is untouched and existing widgets keep working. Positions are points from the bottom-left of the page box; an A4 page is 842 points tall.
4
4
 
5
5
  ## Fast path: the stamp script
6
6
 
@@ -9,11 +9,11 @@ An overlay is a tiny one-page PDF drawn with reportlab and merged onto the targe
9
9
  --text "Confidential - page {page} of {pages}" --position bottom-center --render-dir "<render>"
10
10
  ```
11
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.
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 for a look.
13
13
 
14
14
  ## Draw it yourself
15
15
 
16
- Use reportlab directly when the stamp is a bar, a rotated line, or a block of text:
16
+ Use reportlab directly for a bar, a rotated line, or a block of text:
17
17
 
18
18
  ```bash
19
19
  "<python>" - <<'PY'
@@ -45,16 +45,16 @@ with open("out.pdf", "wb") as handle:
45
45
  PY
46
46
  ```
47
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.
48
+ Use each page's own `mediabox` when sizes may mix; 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 what it covers — put it in a margin or inside a band you mean to cover.
49
49
 
50
50
  ## Pitfalls
51
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.
52
+ - **Existing footers**: check the bottom margin first; the render is the proof, not the coordinate math. If a page number is already there, skip the stamp or use the opposite side.
53
+ - **Rotated pages**: a `/Rotate 90` page renders sideways, so text drawn unrotated appears rotated. Rotate the stamp (`c.rotate(90)`, adjusting the origin), or rotate the page back first.
54
+ - **Repeated stamping**: merging the same overlay twice doubles the opacity at the same position; re-run from the original input when iterating.
55
+ - **Fonts**: Helvetica handles Latin; a CJK stamp needs a registered TTF, or the text renders as blank boxes. Check the render.
56
+ - **Opacity**: `setFillAlpha` works only in viewers that honour transparency groups; a stamp that must show on any viewer is safer at full opacity.
57
57
 
58
58
  ## Verify
59
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.
60
+ Render every stamped page: 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 `"<python>" "<cli>" pdf read --input "<file-stamped.pdf>"` — the stamp text joins the page text, so searching for the label confirms it landed once, not twice.
@@ -1,94 +1,80 @@
1
1
  ---
2
- description: Build, inspect, restyle, and verify PowerPoint .pptx slide decks with python-pptx plus the bundled Office helper CLI.
2
+ description: Create, edit, inspect, and verify PowerPoint .pptx slide decks — slides, templates, charts, render — with python-pptx and the bundled Office helper CLI.
3
3
  ---
4
4
 
5
5
  # Presentations (.pptx)
6
6
 
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.
7
+ A `.pptx` is a zip of XML parts — slides, layouts, masters, charts with embedded
8
+ workbooks, notes, media — and the reader sees only those parts. Author them, then
9
+ read the file back.
11
10
 
12
- Two layers:
13
-
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/).
21
-
22
- Authoring is `python-pptx`. Not a page-description format, not a screenshot: real
23
- shapes, real chart parts, real notes.
11
+ `<python> "<cli>" pptx {create|read|render}` plus `office_status` builds from a
12
+ JSON spec, reads structure and renders; it does not mutate. Everything else is
13
+ here: [recipes](recipes/README.md), mutation tools and gates in
14
+ [container_tools/](container_tools/), templates in
15
+ [template_following_scripts/](template_following_scripts/). Author with
16
+ `python-pptx` — real shapes, chart parts, notes, never a screenshot.
24
17
 
25
18
  ## Before any file work
26
19
 
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.
20
+ Call `office_status`. Read the `## Office runtime` JSON block appended to this
21
+ file for the absolute `python`, `cli`, `skill` and `shared` values; quote paths.
22
+ No working runtime → stop, give the user the `node "..."` remedy command it
23
+ names, call `office_status` again. Never claim a deck was created or verified
24
+ without a runtime.
34
25
 
35
26
  ## Delivery
36
27
 
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.
28
+ `present` the finished file, then state the change in one sentence. A bare path
29
+ is not delivery: chat renders `.docx`/`.pptx`/`.xlsx`/`.pdf` links as
30
+ "二进制文件,无法显示改动".
38
31
 
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.
32
+ - Create: write into the user's workspace, verify, `present`.
33
+ - Edit: never overwrite the original. Write `<original>-edited.<ext>` (or the
34
+ user's name), verify, `present`, and give both paths.
41
35
 
42
36
  ## Annotations
43
37
 
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.
38
+ An `office-annotations` block — `[{ page, text, requirement, region: { left,
39
+ top, width, height } }]` — is a change request, not editor state. `page` is the
40
+ 1-based preview slide; `region` is 0–1 on it, mapped to inches by
41
+ `left_in = region.left * slide_width_in`. Select shapes by bounding-box overlap,
42
+ `text` breaking ties. Apply the smallest edit satisfying `requirement`, re-render,
43
+ confirm. Unmappable → say what you cannot locate; never guess.
45
44
 
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.
47
-
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).
45
+ Full procedure, mapping arithmetic and the edit table:
46
+ [tasks/apply-annotations.md](tasks/apply-annotations.md). Candidates come from
47
+ `container_tools/inspect_presentation_layout_geometry.py --json-out
48
+ geometry.json`.
56
49
 
57
50
  ## Verify
58
51
 
59
- Render, look at every page, then deliver — in that order, with nothing skipped.
52
+ Render, look at every page, then deliver.
60
53
 
61
54
  ```bash
62
55
  "<python>" "<skill>/container_tools/render_slides.py" --input "<file.pptx>" --out-dir "<render-dir>"
63
56
  ```
64
57
 
65
- `render_slides.py` uses the CLI's `pptx render` — which renders through the DSH
66
- LibreOffice kit — and falls back to a draft preview when the kit is missing, so
67
- this one command is always the right entry point. The JSON names the engine it
68
- used in `engine` (`libreoffice-kit` means a faithful render).
58
+ `render_slides.py` uses the CLI's `pptx render` (DSH LibreOffice kit) and falls
59
+ back to a draft preview without the kit, so it is always the right entry point.
60
+ JSON `engine: "libreoffice-kit"` means a faithful render.
69
61
 
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.
62
+ `read_image` every PNG, or one sheet via
63
+ `"<skill>/template_following_scripts/make_contact_sheet.py"`. Check: titles one
64
+ or two lines, body ≥ 18 pt, nothing clipped or off-slide, no empty placeholder,
65
+ table and chart not overlapping, chart readable at thumbnail size.
74
66
 
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.
67
+ No office suite: `--engine draft` is a PyMuPDF geometry preview — placement, not
68
+ typography. Say which you used.
78
69
 
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).
70
+ The six blocking gates and their flags are owned by
71
+ [tasks/inspect-and-verify.md](tasks/inspect-and-verify.md); a gate that reports
72
+ `passed: false` stops delivery.
85
73
 
86
74
  Never present a deck you have not rendered and looked at.
87
75
 
88
76
  ## Tasks
89
77
 
90
- One file per job:
91
-
92
78
  | Job | Start here |
93
79
  |---|---|
94
80
  | Create a deck | [tasks/create-deck.md](tasks/create-deck.md) |
@@ -100,32 +86,27 @@ One file per job:
100
86
 
101
87
  ## Tools and recipes
102
88
 
103
- Mutations (each prints one JSON object, exits non-zero on failure, defaults to
104
- `<original>-edited.pptx`):
89
+ Mutations: one JSON object, non-zero exit on failure, default
90
+ `<original>-edited.pptx`.
105
91
 
106
92
  | Need | Command |
107
93
  |---|---|
108
- | Find/replace text, run-level | `"<skill>/container_tools/replace_text.py" --input "<deck>" --find "old" --replace "new"` |
94
+ | Replace text, run-level | `"<skill>/container_tools/replace_text.py" --input "<deck>" --find "old" --replace "new"` |
109
95
  | 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>"` |
96
+ | Core properties | `"<skill>/container_tools/core_properties.py" --input "<deck>" --title "..."` |
97
+ | Render | `"<skill>/container_tools/render_slides.py" --input "<deck>" --out-dir "<dir>"` |
98
+ | Contact sheet | `"<skill>/template_following_scripts/make_contact_sheet.py" --dir "<dir>" --out "<sheet.png>"` |
113
99
 
114
- Template flow: `inspect_template_deck.py` → write a frame map →
100
+ Template flow: `inspect_template_deck.py` → frame map →
115
101
  `validate_template_plan.py` → `prepare_template_starter_deck.py` →
116
102
  `check_template_fidelity.py`.
117
103
 
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) ·
104
+ Recipes: [README](recipes/README.md) · [textbox at a
105
+ coordinate](recipes/textbox-at-coordinate.md) · [auto-layout](recipes/autolayout.md) ·
106
+ [tables](recipes/tables.md) · [native charts](recipes/native-charts.md) ·
107
+ [masters and layouts](recipes/master-and-layouts.md) · [speaker
108
+ notes](recipes/speaker-notes.md) · [theme tokens](recipes/theme-tokens.md) ·
109
+ [rich text runs](recipes/rich-text-runs.md) · [images](recipes/images.md) ·
129
110
  [connectors](recipes/connectors.md).
130
111
 
131
112
  Guidance: [authoring workflow](references/authoring-workflow.md) ·
@@ -135,31 +116,30 @@ Guidance: [authoring workflow](references/authoring-workflow.md) ·
135
116
  [templates](references/templates.md) ·
136
117
  [finalization](references/finalization.md) ·
137
118
  [finance decks](domain_guidance/finance.md) ·
138
- [Google Slides hand-off](routing/google_slides.md).
119
+ [Google Slides](routing/google_slides.md).
139
120
 
140
121
  ## Non-negotiables
141
122
 
142
123
  - One idea per slide; a title is a claim, not a label.
143
124
  - Body ≥ 18 pt, title ≥ 28 pt, captions ≥ 12 pt.
144
125
  - 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.
126
+ - Every placeholder filled or deleted; no "Chart Title", no trailing full stop on
127
+ a heading.
128
+ - Numbers in the deck agree with the numbers supplied.
129
+ - New output in the user's workspace; an edit is a new file.
149
130
 
150
131
  ## Genuine gaps
151
132
 
152
- - The DSH LibreOffice kit missing (`renderers.can_render.pptx: false`) → no true
153
- render; use `--engine draft` and say the check was geometric. A harness install
154
- that ships the kit fixes it (the plugin records its path in `config.json` when it
155
- loads); a standalone Office/LibreOffice install does not.
156
- - Fonts the user's machine lacks are substituted by any renderer; the pack
157
- cannot install fonts.
158
- - Google Slides import fidelity is out of our control; see
133
+ - DSH LibreOffice kit missing (`office_status` reports `renderers.available: false`) →
134
+ no true render. Use `--engine draft` and say the check was geometric; only a harness
135
+ install shipping the kit fixes it (the plugin records its path in `config.json`
136
+ on load), not a standalone Office/LibreOffice.
137
+ - Fonts the user lacks are substituted by any renderer; the pack cannot install
138
+ them.
139
+ - Google Slides import fidelity is outside our control; see
159
140
  [routing/google_slides.md](routing/google_slides.md).
160
141
 
161
142
  ## Shared workflow
162
143
 
163
- Use `"<shared>/common-workflow.md"` for the cross-format delivery and
164
- verification loop, and `"<shared>/annotation-recipes.md"` when turning an
165
- annotation payload into an edit.
144
+ Use `"<shared>/common-workflow.md"` for the cross-format delivery loop, and
145
+ `"<shared>/annotation-recipes.md"` to turn an annotation payload into an edit.
@@ -58,7 +58,7 @@ def rasterise_pdf(pdf_path: Path, out_dir: Path, dpi: int) -> list[str]:
58
58
 
59
59
 
60
60
  def render_with_office(source: Path, out_dir: Path, dpi: int) -> dict:
61
- """Export through the CLI, which tries every installed office suite."""
61
+ """Export through the CLI, which renders with the bundled LibreOffice kit."""
62
62
  scratch = Path(tempfile.mkdtemp(prefix="dsh-render-"))
63
63
  try:
64
64
  completed = subprocess.run(