dsh-office-kit 0.4.2 → 0.4.4

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 (123) 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/scripts/add_tracked_replacements.py +4 -3
  21. package/assets/skills/office-documents/scripts/docx_kit.py +68 -27
  22. package/assets/skills/office-documents/scripts/heading_audit.py +3 -4
  23. package/assets/skills/office-documents/scripts/internal_nav.py +3 -1
  24. package/assets/skills/office-documents/scripts/merge_docx_append.py +104 -0
  25. package/assets/skills/office-documents/scripts/table_geometry.py +13 -3
  26. package/assets/skills/office-documents/scripts/watermark_add.py +6 -5
  27. package/assets/skills/office-documents/scripts/watermark_audit_remove.py +6 -4
  28. package/assets/skills/office-documents/tasks/accessibility_a11y.md +16 -22
  29. package/assets/skills/office-documents/tasks/captions_crossrefs.md +23 -26
  30. package/assets/skills/office-documents/tasks/clean_tracked_changes.md +17 -22
  31. package/assets/skills/office-documents/tasks/comments_manage.md +9 -11
  32. package/assets/skills/office-documents/tasks/compare_diff.md +10 -16
  33. package/assets/skills/office-documents/tasks/create_edit.md +14 -16
  34. package/assets/skills/office-documents/tasks/equations.md +17 -28
  35. package/assets/skills/office-documents/tasks/fields_update.md +14 -16
  36. package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +19 -33
  37. package/assets/skills/office-documents/tasks/footnotes_endnotes.md +12 -14
  38. package/assets/skills/office-documents/tasks/forms_content_controls.md +16 -20
  39. package/assets/skills/office-documents/tasks/headings_numbering.md +17 -22
  40. package/assets/skills/office-documents/tasks/images_figures.md +15 -25
  41. package/assets/skills/office-documents/tasks/multi_doc_merge.md +13 -23
  42. package/assets/skills/office-documents/tasks/navigation_internal_links.md +18 -21
  43. package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +16 -22
  44. package/assets/skills/office-documents/tasks/protection_restrict_editing.md +19 -21
  45. package/assets/skills/office-documents/tasks/read_review.md +9 -20
  46. package/assets/skills/office-documents/tasks/redaction_anonymization.md +17 -18
  47. package/assets/skills/office-documents/tasks/sections_layout.md +16 -23
  48. package/assets/skills/office-documents/tasks/style_lint_normalize.md +18 -22
  49. package/assets/skills/office-documents/tasks/tables_spreadsheets.md +21 -31
  50. package/assets/skills/office-documents/tasks/templates_style_packs.md +17 -21
  51. package/assets/skills/office-documents/tasks/toc_workflow.md +10 -14
  52. package/assets/skills/office-documents/tasks/verify_render.md +11 -23
  53. package/assets/skills/office-documents/tasks/watermarks_background.md +10 -14
  54. package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +6 -6
  55. package/assets/skills/office-documents/troubleshooting/run_splitting.md +12 -12
  56. package/assets/skills/office-pdf/SKILL.md +36 -51
  57. package/assets/skills/office-pdf/references/form-fields.md +20 -20
  58. package/assets/skills/office-pdf/references/geometry.md +10 -10
  59. package/assets/skills/office-pdf/references/layout-quality.md +13 -13
  60. package/assets/skills/office-pdf/references/limits.md +12 -56
  61. package/assets/skills/office-pdf/tasks/annotations.md +5 -5
  62. package/assets/skills/office-pdf/tasks/create.md +6 -9
  63. package/assets/skills/office-pdf/tasks/edit-pages.md +14 -14
  64. package/assets/skills/office-pdf/tasks/forms.md +15 -15
  65. package/assets/skills/office-pdf/tasks/read.md +9 -9
  66. package/assets/skills/office-pdf/tasks/redact.md +18 -18
  67. package/assets/skills/office-pdf/tasks/stamp.md +10 -10
  68. package/assets/skills/office-presentations/SKILL.md +71 -91
  69. package/assets/skills/office-presentations/container_tools/preview_render.py +4 -1
  70. package/assets/skills/office-presentations/container_tools/render_slides.py +1 -1
  71. package/assets/skills/office-presentations/domain_guidance/finance.md +16 -16
  72. package/assets/skills/office-presentations/recipes/README.md +24 -35
  73. package/assets/skills/office-presentations/recipes/autolayout.md +12 -13
  74. package/assets/skills/office-presentations/recipes/connectors.md +11 -12
  75. package/assets/skills/office-presentations/recipes/images.md +11 -12
  76. package/assets/skills/office-presentations/recipes/master-and-layouts.md +18 -18
  77. package/assets/skills/office-presentations/recipes/native-charts.md +8 -8
  78. package/assets/skills/office-presentations/recipes/rich-text-runs.md +13 -13
  79. package/assets/skills/office-presentations/recipes/speaker-notes.md +7 -7
  80. package/assets/skills/office-presentations/recipes/tables.md +14 -14
  81. package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +9 -9
  82. package/assets/skills/office-presentations/recipes/theme-tokens.md +7 -8
  83. package/assets/skills/office-presentations/references/authoring-workflow.md +38 -43
  84. package/assets/skills/office-presentations/references/finalization.md +10 -12
  85. package/assets/skills/office-presentations/references/native-bullets.md +14 -18
  86. package/assets/skills/office-presentations/references/native-evidence.md +18 -32
  87. package/assets/skills/office-presentations/references/style-guidelines.md +27 -32
  88. package/assets/skills/office-presentations/references/templates.md +31 -60
  89. package/assets/skills/office-presentations/routing/google_slides.md +4 -4
  90. package/assets/skills/office-presentations/tasks/apply-annotations.md +17 -17
  91. package/assets/skills/office-presentations/tasks/charts-tables-notes.md +20 -23
  92. package/assets/skills/office-presentations/tasks/create-deck.md +19 -19
  93. package/assets/skills/office-presentations/tasks/edit-deck.md +14 -16
  94. package/assets/skills/office-presentations/tasks/inspect-and-verify.md +31 -34
  95. package/assets/skills/office-presentations/tasks/template-following.md +23 -28
  96. package/assets/skills/office-spreadsheets/SKILL.md +32 -39
  97. package/assets/skills/office-spreadsheets/recipes/charts.md +8 -26
  98. package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +9 -26
  99. package/assets/skills/office-spreadsheets/recipes/data-tables.md +7 -23
  100. package/assets/skills/office-spreadsheets/recipes/named-ranges.md +8 -21
  101. package/assets/skills/office-spreadsheets/recipes/number-formats.md +7 -23
  102. package/assets/skills/office-spreadsheets/recipes/sparklines.md +7 -20
  103. package/assets/skills/office-spreadsheets/references/charts.md +15 -34
  104. package/assets/skills/office-spreadsheets/references/domain/financial-models.md +10 -25
  105. package/assets/skills/office-spreadsheets/references/domain/healthcare.md +8 -19
  106. package/assets/skills/office-spreadsheets/references/domain/marketing.md +8 -22
  107. package/assets/skills/office-spreadsheets/references/domain/research.md +8 -19
  108. package/assets/skills/office-spreadsheets/references/formula-patterns.md +20 -50
  109. package/assets/skills/office-spreadsheets/references/style-guidelines.md +19 -44
  110. package/assets/skills/office-spreadsheets/references/workbook-structure.md +12 -35
  111. package/assets/skills/office-spreadsheets/routing/google-sheets.md +11 -26
  112. package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +10 -25
  113. package/assets/skills/office-spreadsheets/tasks/annotations.md +12 -30
  114. package/assets/skills/office-spreadsheets/tasks/create-workbook.md +5 -5
  115. package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +2 -2
  116. package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +5 -5
  117. package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +9 -9
  118. package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +10 -30
  119. package/assets/skills/shared/annotation-recipes.md +98 -120
  120. package/assets/skills/shared/common-workflow.md +70 -80
  121. package/dist/index.js +1 -1
  122. package/package.json +1 -1
  123. package/python/office_cli.py +38 -4
@@ -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.
@@ -19,6 +19,7 @@ Exit codes: 0 ok, 2 unreadable input, 3 nothing rasterised.
19
19
  from __future__ import annotations
20
20
 
21
21
  import json
22
+ import math
22
23
  import sys
23
24
  from pathlib import Path
24
25
  from typing import Any
@@ -254,7 +255,9 @@ def _draw_chart(page, shape, rect: fitz.Rect, slide_no: int, notes: list[dict[st
254
255
  for index, value in enumerate(values):
255
256
  sweep = 360.0 * abs(value) / total
256
257
  shade = shades[index % len(shades)]
257
- page.draw_pie(center, radius, fitz.Point(radius * 1.0, 0), start, start + sweep, color=RULE, fill=(shade, shade, shade * 0.95 + 0.05), width=0.6)
258
+ # PyMuPDF 1.28 dropped Page.draw_pie; draw_sector has been stable since 1.16.
259
+ edge = fitz.Point(center.x + radius * math.cos(math.radians(start)), center.y + radius * math.sin(math.radians(start)))
260
+ page.draw_sector(center, edge, sweep, color=RULE, fill=(shade, shade, shade * 0.95 + 0.05), width=0.6)
258
261
  start += sweep
259
262
  return
260
263
 
@@ -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(
@@ -5,14 +5,14 @@ The mechanics are the same as any deck; the failure modes are specific.
5
5
  ## What a finance audience checks first
6
6
 
7
7
  1. Does the total add up? A slide whose numbers do not sum loses the argument
8
- before the argument starts. Run the arithmetic gate on every declared total.
8
+ before it starts. Run the arithmetic gate on every declared total.
9
9
  2. Is the period stated? "Revenue up 12%" without a base period is not a claim.
10
- 3. Is the unit stated? Thousands, millions, currency, and whether it is gross or
11
- net. Put it in the header, once.
12
- 4. Is the comparison like-for-like? Full quarter against full quarter; do not
13
- put a 13-week quarter beside a 12-week one without saying so.
14
- 5. Does the chart axis start at zero? For bars, yes, always — a truncated bar
15
- chart in a finance deck reads as manipulation even when it is not.
10
+ 3. Is the unit stated? Thousands, millions, currency, gross or net. Put it in the
11
+ header, once.
12
+ 4. Is the comparison like-for-like? Full quarter against full quarter; do not put
13
+ a 13-week quarter beside a 12-week one without saying so.
14
+ 5. Does the chart axis start at zero? For bars, always — a truncated bar chart in
15
+ a finance deck reads as manipulation even when it is not.
16
16
 
17
17
  ## The financial slide kit
18
18
 
@@ -30,11 +30,11 @@ The mechanics are the same as any deck; the failure modes are specific.
30
30
  - Two significant figures is usually enough for a headline (`£2.4m`, not
31
31
  `£2,384,117`). Keep full precision in the appendix table.
32
32
  - Percentages: one decimal at most, and say what they are a percentage *of*.
33
- - Round consistently: if the headline says 2.4m, the table should not say
34
- 2.38m in one row and 2.4m in the next.
35
- - Negative numbers: use a minus sign consistently (`−12%`) and keep
36
- parentheses for tables where accounting convention is expected.
37
- - A variance is meaningless without its sign and its direction; state whether
33
+ - Round consistently: if the headline says 2.4m, the table should not say 2.38m
34
+ in one row and 2.4m in the next.
35
+ - Negative numbers: one minus sign convention (`−12%`), parentheses kept for
36
+ tables where accounting convention is expected.
37
+ - A variance is meaningless without its sign and direction; state whether
38
38
  favourable is up or down (revenue vs cost).
39
39
 
40
40
  ## Charts that survive a CFO
@@ -45,14 +45,14 @@ The mechanics are the same as any deck; the failure modes are specific.
45
45
  - One series per chart unless the comparison *is* the point.
46
46
  - Do not chart a table of six numbers; the table is more honest and faster to
47
47
  read.
48
- - Forecast periods are visually distinct (lighter fill, dashed line) and named
49
- in the caption.
48
+ - Forecast periods are visually distinct (lighter fill, dashed line) and named in
49
+ the caption.
50
50
 
51
51
  ## Provenance
52
52
 
53
53
  Every non-obvious number gets a note with: source file or system, extraction
54
- date, period covered, and any adjustment made. The speaker notes are the right
55
- place; a footnote-heavy slide is a slide nobody reads.
54
+ date, period covered, any adjustment made. The speaker notes are the right place;
55
+ a footnote-heavy slide is a slide nobody reads.
56
56
 
57
57
  ## Check before delivery
58
58
 
@@ -1,12 +1,9 @@
1
1
  # Slide mechanics and paste-runnable recipes
2
2
 
3
- The helper CLI covers the common path: a small deck with a title, bullet slides,
4
- a table, a chart and a picture. Everything else — precise coordinates, master
5
- edits, notes, theme tokens, connectors — is ordinary `python-pptx` work, and
6
- these recipes are the honest route.
7
-
8
- Read the mechanics first, then paste the recipe. Every snippet runs as-is from a
9
- shell, from any working directory:
3
+ The helper CLI covers the common path: title, bullets, table, chart, picture.
4
+ Everything else — exact coordinates, master edits, notes, theme tokens,
5
+ connectors — is `python-pptx` work. These recipes are the route and run as-is
6
+ from a shell, from any working directory:
10
7
 
11
8
  ```bash
12
9
  "<python>" - <<'PY'
@@ -21,17 +18,16 @@ PY
21
18
  |---|---|
22
19
  | Where is this shape? | `left`, `top`, `width`, `height` in **EMU** (English Metric Units). |
23
20
  | What is an inch? | `914400` EMU. `pptx.util.Inches(1)` is the polite wrapper. |
24
- | What is a point? | `12700` EMU. Font sizes use points, geometry uses EMU, and mixing them silently breaks layout. |
21
+ | What is a point? | `12700` EMU. Font sizes use points, geometry uses EMU; mixing them silently breaks layout. |
25
22
  | What is a slide? | `prs.slide_width` / `prs.slide_height`; check them, never assume 16:9. |
26
23
 
27
- Helper conversions worth memorising: `Emu(914400) == Inches(1)`,
28
- `Pt(28) == Emu(355600)`, and a fraction of the slide is
24
+ `Emu(914400) == Inches(1)`, `Pt(28) == Emu(355600)`, and a slide fraction is
29
25
  `int(0.1 * prs.slide_width)`.
30
26
 
31
27
  ## Z-order
32
28
 
33
- Shapes paint in the order they live in `slide.shapes`. A shape added later
34
- covers one added earlier. To reorder without deleting:
29
+ Shapes paint in `slide.shapes` order; later covers earlier. Reorder without
30
+ deleting:
35
31
 
36
32
  ```python
37
33
  tree = slide.shapes._spTree
@@ -40,32 +36,25 @@ tree.remove(shape._element)
40
36
  tree.insert(2, shape._element) # after nvGrpSpPr and grpSpPr
41
37
  ```
42
38
 
43
- Index `2` is the first painted shape; that is where a full-bleed background
44
- image belongs.
39
+ Index `2` is the first painted shape — where a full-bleed background belongs.
45
40
 
46
41
  ## What this pack ships
47
42
 
48
43
  | Recipe | Use it when |
49
44
  |---|---|
50
45
  | [textbox-at-coordinate](textbox-at-coordinate.md) | Text must land on an exact rect, not "somewhere under the title". |
51
- | [autolayout](autolayout.md) | The same build has to fit 3 bullets or 9 without hand-tuning. |
52
- | [tables](tables.md) | Numbers need a grid with column widths and per-cell formatting. |
53
- | [native-charts](native-charts.md) | Data must stay editable, not become a picture of a chart. |
54
- | [master-and-layouts](master-and-layouts.md) | The whole deck changes at once: title bar, accent line, footer. |
55
- | [speaker-notes](speaker-notes.md) | The presenter needs a script attached to each slide. |
56
- | [theme-tokens](theme-tokens.md) | Colours and fonts must come from one place. |
57
- | [rich-text-runs](rich-text-runs.md) | One paragraph mixes bold, accent colour and a footnote. |
58
- | [images](images.md) | A picture needs a crop, a cover fit or a real alt text. |
59
- | [connectors](connectors.md) | Two boxes are joined by a line that must survive a move. |
60
-
61
- ## Order of work that keeps decks out of trouble
62
-
63
- 1. Read the target geometry off the template or the user's reference
64
- (`container_tools/inspect_presentation_layout_geometry.py` prints it as JSON).
65
- 2. Place shapes in EMU, top-down: background, chrome, title, body, evidence.
66
- 3. Set type sizes by the *smallest* screen the deck will be shown on; body 18 pt
67
- and up, footnotes 12 pt and up.
68
- 4. Fill every placeholder you keep and delete the ones you do not — an empty
69
- placeholder prints "Click to add text" in edit view and looks unfinished.
70
- 5. Render, look, then fix. `container_tools/render_slides.py` plus `read_image`
71
- is the loop; a deck nobody looked at is not verified.
46
+ | [autolayout](autolayout.md) | The same build must fit 3 bullets or 9 without hand-tuning. |
47
+ | [tables](tables.md) | Numbers need column widths and per-cell formatting. |
48
+ | [native-charts](native-charts.md) | Data must stay editable, not a chart picture. |
49
+ | [master-and-layouts](master-and-layouts.md) | Whole-deck changes: title bar, accent line, footer. |
50
+ | [speaker-notes](speaker-notes.md) | A script attached to each slide. |
51
+ | [theme-tokens](theme-tokens.md) | Colours and fonts from one place. |
52
+ | [rich-text-runs](rich-text-runs.md) | One paragraph mixes bold, accent colour, footnote. |
53
+ | [images](images.md) | A crop, a cover fit or real alt text. |
54
+ | [connectors](connectors.md) | A line between boxes that survives a move. |
55
+
56
+ ## Order of work
57
+
58
+ Follow the loop in [authoring-workflow](../references/authoring-workflow.md):
59
+ story → frame → place top-down in EMU → fill or delete placeholders → render and
60
+ look. Type sizes live in [style-guidelines](../references/style-guidelines.md).
@@ -2,19 +2,18 @@
2
2
 
3
3
  ## Mechanics
4
4
 
5
- PowerPoint has two different "autofit" ideas and mixing them up wastes a round
6
- trip:
5
+ PowerPoint has two "autofit" ideas; mixing them up wastes a round trip:
7
6
 
8
7
  - `word_wrap` decides whether text wraps inside the width. Turn it on for every
9
8
  authored box.
10
9
  - `auto_size` decides whether the *frame* follows the text (`MSO_AUTO_SIZE.
11
10
  SHAPE_TO_FIT_TEXT`) or the text shrinks to fit the frame
12
11
  (`TEXT_TO_FIT_SHAPE`). `TEXT_TO_FIT_SHAPE` is a **render-time** behaviour:
13
- PowerPoint recalculates it when the file is opened, so a deck that looks fine
14
- in a draft preview can still shrink to 8 pt on a real screen.
12
+ PowerPoint recalculates it when the file opens, so a deck that looks fine in a
13
+ draft preview can still shrink to 8 pt on a real screen.
15
14
 
16
- The reliable pattern is: fixed frame, explicit sizes, content measured against
17
- the frame, no text-to-fit. Estimate, then verify by rendering.
15
+ The reliable pattern: fixed frame, explicit sizes, content measured against the
16
+ frame, no text-to-fit. Estimate, then verify by rendering.
18
17
 
19
18
  ## Recipe: fit a variable number of bullets
20
19
 
@@ -66,7 +65,7 @@ PY
66
65
  ## Rules that hold regardless of content
67
66
 
68
67
  1. Body text is 18 pt or larger; below that the back row cannot read it.
69
- 2. Six bullets or three lines of body is the point where a slide should split.
68
+ 2. Six bullets or three lines of body is where a slide should split.
70
69
  3. Longer text shrinks the number of *words*, not the type size. If the fit loop
71
70
  lands below the body floor, cut copy or split the slide.
72
71
  4. Keep 0.5 in of bottom margin for the footer and for projectors that crop.
@@ -77,12 +76,12 @@ PY
77
76
  - `frame.text` then `paragraph.font.size` on paragraph 0 only: a multi-paragraph
78
77
  assignment leaves paragraphs 1..n at the inherited 18 pt.
79
78
  - Text-to-fit hiding the overflow: the file opens fine and the audience reads
80
- 9 pt. Measure, do not delegate the fit to the renderer.
81
- - A frame height that exactly equals the text height: one extra word on edit and
82
- the block runs into the evidence below. Reserve 10% slack.
79
+ 9 pt. Measure; do not delegate the fit to the renderer.
80
+ - A frame height exactly equal to the text height: one extra word on edit and the
81
+ block runs into the evidence below. Reserve 10% slack.
83
82
 
84
83
  ## CLI fast path
85
84
 
86
- `pptx create` with `layout: "bullets"` fills the layout's body placeholder at
87
- its own size. Use the loop above when the number of bullets is not known before
88
- the content is written.
85
+ `pptx create` with `layout: "bullets"` fills the layout's body placeholder at its
86
+ own size. Use the loop above when the number of bullets is unknown before the
87
+ content is written.