dsh-office-kit 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CAPABILITY.md +147 -68
- package/README.md +3 -1
- package/assets/skills/excel-live-control/SKILL.md +234 -0
- package/assets/skills/excel-live-control/container_tools/live_excel.py +1589 -0
- package/assets/skills/excel-live-control/domain_guidance/financial_models.md +86 -0
- package/assets/skills/excel-live-control/domain_guidance/healthcare.md +58 -0
- package/assets/skills/excel-live-control/domain_guidance/marketing_advertising.md +53 -0
- package/assets/skills/excel-live-control/domain_guidance/scientific_research.md +53 -0
- package/assets/skills/excel-live-control/references/office-automation.md +207 -0
- package/assets/skills/excel-live-control/references/read_only_qna.md +59 -0
- package/assets/skills/excel-live-control/style_guidelines.md +87 -0
- package/assets/skills/excel-live-control/workflows/create_workflows.md +78 -0
- package/assets/skills/excel-live-control/workflows/edit_workflows.md +72 -0
- package/assets/skills/office-documents/SKILL.md +90 -77
- package/assets/skills/office-documents/examples/end_to_end_smoke_test.md +86 -0
- package/assets/skills/office-documents/ooxml/comments.md +40 -0
- package/assets/skills/office-documents/ooxml/hyperlinks_and_fields.md +66 -0
- package/assets/skills/office-documents/ooxml/rels_and_content_types.md +62 -0
- package/assets/skills/office-documents/ooxml/tracked_changes.md +53 -0
- package/assets/skills/office-documents/references/docx-structure.md +83 -0
- package/assets/skills/office-documents/references/template-elicitation.md +49 -0
- package/assets/skills/office-documents/references/writing-quality.md +49 -0
- package/assets/skills/office-documents/render_docx.py +92 -0
- package/assets/skills/office-documents/scripts/a11y_audit.py +194 -0
- package/assets/skills/office-documents/scripts/accept_tracked_changes.py +151 -0
- package/assets/skills/office-documents/scripts/add_tracked_replacements.py +148 -0
- package/assets/skills/office-documents/scripts/append_blocks.py +108 -0
- package/assets/skills/office-documents/scripts/apply_template_styles.py +104 -0
- package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +102 -0
- package/assets/skills/office-documents/scripts/comments_add.py +136 -0
- package/assets/skills/office-documents/scripts/comments_apply_patch.py +114 -0
- package/assets/skills/office-documents/scripts/comments_extract.py +85 -0
- package/assets/skills/office-documents/scripts/comments_strip.py +74 -0
- package/assets/skills/office-documents/scripts/content_controls.py +192 -0
- package/assets/skills/office-documents/scripts/core_properties.py +124 -0
- package/assets/skills/office-documents/scripts/docx_kit.py +587 -0
- package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +225 -0
- package/assets/skills/office-documents/scripts/docx_table_to_csv.py +99 -0
- package/assets/skills/office-documents/scripts/fields_materialize.py +66 -0
- package/assets/skills/office-documents/scripts/fields_report.py +59 -0
- package/assets/skills/office-documents/scripts/flatten_ref_fields.py +62 -0
- package/assets/skills/office-documents/scripts/footnotes_report.py +77 -0
- package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +96 -0
- package/assets/skills/office-documents/scripts/heading_audit.py +81 -0
- package/assets/skills/office-documents/scripts/images_audit.py +80 -0
- package/assets/skills/office-documents/scripts/insert_note.py +126 -0
- package/assets/skills/office-documents/scripts/insert_ref_fields.py +90 -0
- package/assets/skills/office-documents/scripts/insert_toc.py +84 -0
- package/assets/skills/office-documents/scripts/internal_nav.py +146 -0
- package/assets/skills/office-documents/scripts/make_fixtures.py +169 -0
- package/assets/skills/office-documents/scripts/merge_docx_append.py +239 -0
- package/assets/skills/office-documents/scripts/privacy_scrub.py +131 -0
- package/assets/skills/office-documents/scripts/redact_docx.py +113 -0
- package/assets/skills/office-documents/scripts/render_and_diff.py +101 -0
- package/assets/skills/office-documents/scripts/replace_text.py +154 -0
- package/assets/skills/office-documents/scripts/section_audit.py +91 -0
- package/assets/skills/office-documents/scripts/set_protection.py +92 -0
- package/assets/skills/office-documents/scripts/style_lint.py +111 -0
- package/assets/skills/office-documents/scripts/style_normalize.py +103 -0
- package/assets/skills/office-documents/scripts/table_geometry.py +167 -0
- package/assets/skills/office-documents/scripts/watermark_add.py +48 -0
- package/assets/skills/office-documents/scripts/watermark_audit_remove.py +67 -0
- package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +117 -0
- package/assets/skills/office-documents/tasks/accessibility_a11y.md +45 -0
- package/assets/skills/office-documents/tasks/captions_crossrefs.md +49 -0
- package/assets/skills/office-documents/tasks/clean_tracked_changes.md +49 -0
- package/assets/skills/office-documents/tasks/comments_manage.md +59 -0
- package/assets/skills/office-documents/tasks/compare_diff.md +40 -0
- package/assets/skills/office-documents/tasks/create_edit.md +107 -0
- package/assets/skills/office-documents/tasks/equations.md +59 -0
- package/assets/skills/office-documents/tasks/fields_update.md +44 -0
- package/assets/skills/office-documents/tasks/fixtures_edge_cases.md +50 -0
- package/assets/skills/office-documents/tasks/footnotes_endnotes.md +38 -0
- package/assets/skills/office-documents/tasks/forms_content_controls.md +48 -0
- package/assets/skills/office-documents/tasks/headings_numbering.md +55 -0
- package/assets/skills/office-documents/tasks/images_figures.md +63 -0
- package/assets/skills/office-documents/tasks/multi_doc_merge.md +43 -0
- package/assets/skills/office-documents/tasks/navigation_internal_links.md +38 -0
- package/assets/skills/office-documents/tasks/privacy_scrub_metadata.md +45 -0
- package/assets/skills/office-documents/tasks/protection_restrict_editing.md +41 -0
- package/assets/skills/office-documents/tasks/read_review.md +50 -0
- package/assets/skills/office-documents/tasks/redaction_anonymization.md +42 -0
- package/assets/skills/office-documents/tasks/sections_layout.md +54 -0
- package/assets/skills/office-documents/tasks/style_lint_normalize.md +44 -0
- package/assets/skills/office-documents/tasks/tables_spreadsheets.md +64 -0
- package/assets/skills/office-documents/tasks/templates_style_packs.md +53 -0
- package/assets/skills/office-documents/tasks/toc_workflow.md +39 -0
- package/assets/skills/office-documents/tasks/verify_render.md +50 -0
- package/assets/skills/office-documents/tasks/watermarks_background.md +35 -0
- package/assets/skills/office-documents/troubleshooting/libreoffice_headless.md +47 -0
- package/assets/skills/office-documents/troubleshooting/run_splitting.md +36 -0
- package/assets/skills/office-pdf/SKILL.md +73 -56
- package/assets/skills/office-pdf/references/form-fields.md +56 -0
- package/assets/skills/office-pdf/references/geometry.md +44 -0
- package/assets/skills/office-pdf/references/layout-quality.md +41 -0
- package/assets/skills/office-pdf/references/limits.md +58 -0
- package/assets/skills/office-pdf/scripts/extract_text.py +172 -0
- package/assets/skills/office-pdf/scripts/forms.py +556 -0
- package/assets/skills/office-pdf/scripts/metadata.py +161 -0
- package/assets/skills/office-pdf/scripts/page_ops.py +198 -0
- package/assets/skills/office-pdf/scripts/redact_region.py +347 -0
- package/assets/skills/office-pdf/scripts/render_pages.py +207 -0
- package/assets/skills/office-pdf/scripts/split_pages.py +155 -0
- package/assets/skills/office-pdf/scripts/stamp_overlay.py +199 -0
- package/assets/skills/office-pdf/tasks/annotations.md +98 -0
- package/assets/skills/office-pdf/tasks/create.md +75 -0
- package/assets/skills/office-pdf/tasks/edit-pages.md +71 -0
- package/assets/skills/office-pdf/tasks/forms.md +67 -0
- package/assets/skills/office-pdf/tasks/read.md +62 -0
- package/assets/skills/office-pdf/tasks/redact.md +72 -0
- package/assets/skills/office-pdf/tasks/stamp.md +60 -0
- package/assets/skills/office-presentations/SKILL.md +141 -87
- package/assets/skills/office-presentations/container_tools/add_slides.py +136 -0
- package/assets/skills/office-presentations/container_tools/core_properties.py +120 -0
- package/assets/skills/office-presentations/container_tools/create_montage.py +126 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +285 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +157 -0
- package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +144 -0
- package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +199 -0
- package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +212 -0
- package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +345 -0
- package/assets/skills/office-presentations/container_tools/preview_render.py +375 -0
- package/assets/skills/office-presentations/container_tools/render_slides.py +139 -0
- package/assets/skills/office-presentations/container_tools/replace_text.py +168 -0
- package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +229 -0
- package/assets/skills/office-presentations/domain_guidance/finance.md +66 -0
- package/assets/skills/office-presentations/recipes/README.md +71 -0
- package/assets/skills/office-presentations/recipes/autolayout.md +88 -0
- package/assets/skills/office-presentations/recipes/connectors.md +83 -0
- package/assets/skills/office-presentations/recipes/images.md +80 -0
- package/assets/skills/office-presentations/recipes/master-and-layouts.md +127 -0
- package/assets/skills/office-presentations/recipes/native-charts.md +87 -0
- package/assets/skills/office-presentations/recipes/rich-text-runs.md +86 -0
- package/assets/skills/office-presentations/recipes/speaker-notes.md +65 -0
- package/assets/skills/office-presentations/recipes/tables.md +88 -0
- package/assets/skills/office-presentations/recipes/textbox-at-coordinate.md +71 -0
- package/assets/skills/office-presentations/recipes/theme-tokens.md +87 -0
- package/assets/skills/office-presentations/references/authoring-workflow.md +65 -0
- package/assets/skills/office-presentations/references/finalization.md +63 -0
- package/assets/skills/office-presentations/references/native-bullets.md +78 -0
- package/assets/skills/office-presentations/references/native-evidence.md +67 -0
- package/assets/skills/office-presentations/references/style-guidelines.md +66 -0
- package/assets/skills/office-presentations/references/templates.md +76 -0
- package/assets/skills/office-presentations/routing/google_slides.md +45 -0
- package/assets/skills/office-presentations/tasks/apply-annotations.md +83 -0
- package/assets/skills/office-presentations/tasks/charts-tables-notes.md +79 -0
- package/assets/skills/office-presentations/tasks/create-deck.md +106 -0
- package/assets/skills/office-presentations/tasks/edit-deck.md +93 -0
- package/assets/skills/office-presentations/tasks/inspect-and-verify.md +107 -0
- package/assets/skills/office-presentations/tasks/template-following.md +132 -0
- package/assets/skills/office-presentations/template_following_scripts/check_template_fidelity.py +176 -0
- package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +154 -0
- package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +111 -0
- package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +239 -0
- package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +185 -0
- package/assets/skills/office-spreadsheets/SKILL.md +105 -101
- package/assets/skills/office-spreadsheets/container_tools/add_chart.py +236 -0
- package/assets/skills/office-spreadsheets/container_tools/add_image.py +107 -0
- package/assets/skills/office-spreadsheets/container_tools/append_rows.py +122 -0
- package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +306 -0
- package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +398 -0
- package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +275 -0
- package/assets/skills/office-spreadsheets/container_tools/set_cells.py +135 -0
- package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +134 -0
- package/assets/skills/office-spreadsheets/container_tools/style_cells.py +133 -0
- package/assets/skills/office-spreadsheets/recipes/charts.md +66 -0
- package/assets/skills/office-spreadsheets/recipes/conditional-formats.md +69 -0
- package/assets/skills/office-spreadsheets/recipes/data-tables.md +58 -0
- package/assets/skills/office-spreadsheets/recipes/named-ranges.md +41 -0
- package/assets/skills/office-spreadsheets/recipes/number-formats.md +72 -0
- package/assets/skills/office-spreadsheets/recipes/sparklines.md +43 -0
- package/assets/skills/office-spreadsheets/references/charts.md +63 -0
- package/assets/skills/office-spreadsheets/references/domain/financial-models.md +40 -0
- package/assets/skills/office-spreadsheets/references/domain/healthcare.md +22 -0
- package/assets/skills/office-spreadsheets/references/domain/marketing.md +28 -0
- package/assets/skills/office-spreadsheets/references/domain/research.md +22 -0
- package/assets/skills/office-spreadsheets/references/formula-patterns.md +78 -0
- package/assets/skills/office-spreadsheets/references/style-guidelines.md +69 -0
- package/assets/skills/office-spreadsheets/references/workbook-structure.md +60 -0
- package/assets/skills/office-spreadsheets/routing/google-sheets.md +36 -0
- package/assets/skills/office-spreadsheets/tasks/analyze-workbook.md +37 -0
- package/assets/skills/office-spreadsheets/tasks/annotations.md +51 -0
- package/assets/skills/office-spreadsheets/tasks/create-workbook.md +66 -0
- package/assets/skills/office-spreadsheets/tasks/diff-workbooks.md +38 -0
- package/assets/skills/office-spreadsheets/tasks/edit-workbook.md +69 -0
- package/assets/skills/office-spreadsheets/tasks/inspect-workbook.md +38 -0
- package/assets/skills/office-spreadsheets/tasks/recalc-and-check.md +57 -0
- package/assets/skills/shared/annotation-recipes.md +252 -0
- package/assets/skills/shared/check_runtime.py +253 -0
- package/assets/skills/shared/common-workflow.md +139 -0
- package/assets/skills/shared/scripts/ensure_raster_image.py +187 -0
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/index.js +239 -89
- package/package.json +1 -1
- package/postinstall.mjs +92 -82
- package/python/office_cli.py +171 -246
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Masters, layouts and slide families
|
|
2
|
+
|
|
3
|
+
## Mechanics
|
|
4
|
+
|
|
5
|
+
A deck has three levels, and where you make a change decides how many slides it
|
|
6
|
+
fixes:
|
|
7
|
+
|
|
8
|
+
- **Slide master** (`ppt/slideMasters/`) — theme fonts, colours, background,
|
|
9
|
+
and the shapes every slide inherits (title bar, accent rule, logo).
|
|
10
|
+
- **Slide layout** (`ppt/slideLayouts/`) — one per slide family (title, title and
|
|
11
|
+
content, section, blank…), each with named placeholders at fixed rects.
|
|
12
|
+
- **Slide** — holds its own shapes plus *placeholder overrides*: text, and also
|
|
13
|
+
position, if someone dragged a placeholder.
|
|
14
|
+
|
|
15
|
+
Editing a placeholder's *text* is a per-slide change. Editing the layout's
|
|
16
|
+
placeholder *rect* moves that frame on every slide of that family. Editing the
|
|
17
|
+
master's background moves it everywhere.
|
|
18
|
+
|
|
19
|
+
`slide.slide_layout` is read-only; the family is fixed when the slide is created.
|
|
20
|
+
To change a slide's family, delete it and add a new one from the target layout —
|
|
21
|
+
losing its content, so plan families before filling them.
|
|
22
|
+
|
|
23
|
+
**One quirk to know:** `master.shapes` and `layout.shapes` are read-only in
|
|
24
|
+
`python-pptx` (`MasterShapes` has no `add_shape`). Chrome shapes are therefore
|
|
25
|
+
built on a scratch slide and their XML moved into the master's shape tree.
|
|
26
|
+
|
|
27
|
+
## Recipe: restyle a family from the master
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
"<python>" - <<'PY'
|
|
31
|
+
import copy
|
|
32
|
+
from pptx import Presentation
|
|
33
|
+
from pptx.util import Inches, Pt
|
|
34
|
+
from pptx.dml.color import RGBColor
|
|
35
|
+
from pptx.enum.shapes import MSO_SHAPE
|
|
36
|
+
|
|
37
|
+
prs = Presentation("template.pptx")
|
|
38
|
+
master = prs.slide_masters[0]
|
|
39
|
+
|
|
40
|
+
master.background.fill.solid()
|
|
41
|
+
master.background.fill.fore_color.rgb = RGBColor(0x0F, 0x17, 0x22)
|
|
42
|
+
|
|
43
|
+
# Chrome rule on every slide: build it on a scratch slide, move the XML, drop the slide.
|
|
44
|
+
scratch = prs.slides.add_slide(prs.slide_layouts[6])
|
|
45
|
+
rule = scratch.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(0), Inches(0), Inches(13.333), Inches(0.06))
|
|
46
|
+
rule.fill.solid()
|
|
47
|
+
rule.fill.fore_color.rgb = RGBColor(0xE0, 0x9B, 0x2D)
|
|
48
|
+
rule.line.fill.background()
|
|
49
|
+
master.shapes._spTree.append(copy.deepcopy(rule._element))
|
|
50
|
+
|
|
51
|
+
slide_id = prs.slides._sldIdLst[-1]
|
|
52
|
+
prs.part.drop_rel(slide_id.rId)
|
|
53
|
+
prs.slides._sldIdLst.remove(slide_id)
|
|
54
|
+
|
|
55
|
+
# Move the title frame for one family only.
|
|
56
|
+
layout = next(l for l in master.slide_layouts if "title and content" in l.name.lower())
|
|
57
|
+
title = next(p for p in layout.placeholders if p.placeholder_format.idx == 0)
|
|
58
|
+
title.left, title.top = Inches(0.9), Inches(0.55)
|
|
59
|
+
title.width, title.height = Inches(11.5), Inches(1.0)
|
|
60
|
+
for paragraph in title.text_frame.paragraphs:
|
|
61
|
+
paragraph.font.size = Pt(32)
|
|
62
|
+
paragraph.font.bold = True
|
|
63
|
+
|
|
64
|
+
prs.save("restyled.pptx")
|
|
65
|
+
print("layouts:", [l.name for l in master.slide_layouts], "slides:", len(prs.slides))
|
|
66
|
+
PY
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Recipe: delete the empty placeholders a layout leaves behind
|
|
70
|
+
|
|
71
|
+
A layout hands every slide a placeholder, filled or not. Section and title-only
|
|
72
|
+
layouts carry body slots that are empty by design; in edit view they print "Click
|
|
73
|
+
to add text" and there is no CLI verb for removing them, so do it in code:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
"<python>" - <<'PY'
|
|
77
|
+
from pptx import Presentation
|
|
78
|
+
|
|
79
|
+
prs = Presentation("deck.pptx")
|
|
80
|
+
removed = []
|
|
81
|
+
for index, slide in enumerate(prs.slides, start=1):
|
|
82
|
+
for placeholder in list(slide.placeholders):
|
|
83
|
+
if placeholder.has_text_frame and not placeholder.text_frame.text.strip():
|
|
84
|
+
removed.append((index, placeholder.placeholder_format.idx, placeholder.name))
|
|
85
|
+
placeholder._element.getparent().remove(placeholder._element)
|
|
86
|
+
prs.save("deck-cleaned.pptx")
|
|
87
|
+
print("removed:", removed)
|
|
88
|
+
PY
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This is what clears the `empty_placeholder` review finding in
|
|
92
|
+
`container_tools/inspect_presentation_layout_geometry.py`. Keep a placeholder you
|
|
93
|
+
still intend to fill, and never delete the one slide numbers are linked to.
|
|
94
|
+
|
|
95
|
+
## Reusing the source's own layouts
|
|
96
|
+
|
|
97
|
+
When the user supplies a template, do not rebuild its look — put content into
|
|
98
|
+
its placeholders. The inspection script prints each layout's placeholders with
|
|
99
|
+
index, kind and rect, which is what you need to address them:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
"<python>" container_tools/inspect_presentation_layout_geometry.py --input template.pptx --json-out geometry.json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Then address a placeholder by index on the slide:
|
|
106
|
+
`slide.placeholders[1].text = "..."`. Slides created from a layout always have
|
|
107
|
+
a placeholder for every non-empty slot, so index-based addressing is stable.
|
|
108
|
+
|
|
109
|
+
## Pitfalls this avoids
|
|
110
|
+
|
|
111
|
+
- Drawing the accent rule on every slide instead of the master: one edit later,
|
|
112
|
+
half the deck has two rules.
|
|
113
|
+
- Dragging a placeholder on a slide to fix one frame; the layout is untouched
|
|
114
|
+
and the next slide of the same family misbehaves again.
|
|
115
|
+
- Appending the scratch shape to the master without `drop_rel`: the orphan slide
|
|
116
|
+
part stays in the package and `inspect_presentation_package_integrity.py`
|
|
117
|
+
reports a slide count mismatch.
|
|
118
|
+
- Adding a *blank* slide and a floating textbox where the template has a title
|
|
119
|
+
placeholder: the deck no longer answers to its own master, and the outline
|
|
120
|
+
view is empty.
|
|
121
|
+
|
|
122
|
+
## CLI fast path
|
|
123
|
+
|
|
124
|
+
`pptx create --spec` starts from the blank default template and maps
|
|
125
|
+
`title|bullets|section|title_only|blank` to built-in layouts. Copying a supplied
|
|
126
|
+
template is `template_following_scripts/prepare_template_starter_deck.py`, which
|
|
127
|
+
keeps and fills the template's own slides.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Native charts
|
|
2
|
+
|
|
3
|
+
## Mechanics
|
|
4
|
+
|
|
5
|
+
A chart added with `slide.shapes.add_chart` is a real chart part
|
|
6
|
+
(`ppt/charts/chartN.xml`) with an embedded workbook
|
|
7
|
+
(`ppt/embeddings/*.xlsx`). That matters for three reasons:
|
|
8
|
+
|
|
9
|
+
- The audience can edit the numbers in PowerPoint; a picture of a chart cannot.
|
|
10
|
+
- The data is in the file, so a gate can check it. A screenshot cannot be
|
|
11
|
+
checked, and nobody notices a wrong screenshot.
|
|
12
|
+
- The chart inherits the slide's theme for colour and type, so charts match the
|
|
13
|
+
deck without per-chart styling.
|
|
14
|
+
|
|
15
|
+
`CategoryChartData` holds the numbers; `chart.plots[0]` holds the geometry of
|
|
16
|
+
the marks. Category order is the series' own order — sort before you pass it,
|
|
17
|
+
because a chart does not sort.
|
|
18
|
+
|
|
19
|
+
## Recipe
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
"<python>" - <<'PY'
|
|
23
|
+
from pptx import Presentation
|
|
24
|
+
from pptx.chart.data import CategoryChartData
|
|
25
|
+
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION, XL_LABEL_POSITION
|
|
26
|
+
from pptx.util import Inches, Pt
|
|
27
|
+
|
|
28
|
+
prs = Presentation()
|
|
29
|
+
prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)
|
|
30
|
+
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
31
|
+
|
|
32
|
+
data = CategoryChartData()
|
|
33
|
+
data.categories = ["Q1", "Q2", "Q3", "Q4"]
|
|
34
|
+
data.add_series("Orders fulfilled", (100, 128, 154, 171))
|
|
35
|
+
data.add_series("Touches", (410, 402, 386, 371))
|
|
36
|
+
|
|
37
|
+
frame = slide.shapes.add_chart(
|
|
38
|
+
XL_CHART_TYPE.COLUMN_CLUSTERED, Inches(0.9), Inches(1.6), Inches(11.5), Inches(5.1), data
|
|
39
|
+
)
|
|
40
|
+
chart = frame.chart
|
|
41
|
+
chart.has_title = True
|
|
42
|
+
chart.chart_title.text_frame.text = "Orders fulfilled per quarter"
|
|
43
|
+
chart.chart_title.text_frame.paragraphs[0].font.size = Pt(14)
|
|
44
|
+
chart.has_legend = True
|
|
45
|
+
chart.legend.position = XL_LEGEND_POSITION.BOTTOM
|
|
46
|
+
chart.legend.include_in_layout = False
|
|
47
|
+
|
|
48
|
+
plot = chart.plots[0]
|
|
49
|
+
plot.has_data_labels = True
|
|
50
|
+
plot.data_labels.number_format = "0"
|
|
51
|
+
plot.data_labels.number_format_is_linked = False
|
|
52
|
+
plot.data_labels.position = XL_LABEL_POSITION.OUTSIDE_END
|
|
53
|
+
plot.gap_width = 60
|
|
54
|
+
|
|
55
|
+
chart.value_axis.has_major_gridlines = True
|
|
56
|
+
chart.value_axis.tick_labels.font.size = Pt(11)
|
|
57
|
+
chart.category_axis.tick_labels.font.size = Pt(11)
|
|
58
|
+
prs.save("chart.pptx")
|
|
59
|
+
print("saved chart with", len(chart.series), "series")
|
|
60
|
+
PY
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Rules for charted numbers
|
|
64
|
+
|
|
65
|
+
1. One idea per chart. Two series is fine; six is a table.
|
|
66
|
+
2. No chart title that repeats the slide title — the title says what the chart
|
|
67
|
+
*means* ("Throughput rose while touches fell"), the chart shows the numbers.
|
|
68
|
+
3. Start the value axis at zero for bars, and say so in the notes if you do not.
|
|
69
|
+
4. Data labels beat a value axis grid; pick one, not both.
|
|
70
|
+
5. Never leave the default "Chart Title" placeholder text in a shipped deck.
|
|
71
|
+
|
|
72
|
+
## Pitfalls this avoids
|
|
73
|
+
|
|
74
|
+
- `number_format` without `number_format_is_linked = False` reverts to the
|
|
75
|
+
workbook's format the next time PowerPoint refreshes the chart.
|
|
76
|
+
- `legend.include_in_layout = False` is what stops the legend from stealing
|
|
77
|
+
plot height and squashing the bars.
|
|
78
|
+
- Deleting the embedded workbook (by zipping/unzipping carelessly) turns the
|
|
79
|
+
chart into an uneditable picture in every downstream viewer.
|
|
80
|
+
|
|
81
|
+
## CLI fast path
|
|
82
|
+
|
|
83
|
+
`pptx create` accepts
|
|
84
|
+
`{"chart": {"type": "column", "title": "...", "categories": [...], "series":
|
|
85
|
+
[{"name": "...", "values": [...]}], "left": 0.9, "top": 1.45, "width": 11.5,
|
|
86
|
+
"height": 5.1}}`. The recipe above is the route when you need axis, label or
|
|
87
|
+
legend control.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Rich text runs
|
|
2
|
+
|
|
3
|
+
## Mechanics
|
|
4
|
+
|
|
5
|
+
Text lives at three levels: `text_frame` → `paragraph` → `run`. A run is a span
|
|
6
|
+
with one formatting state. Assigning `paragraph.text` (or `text_frame.text`)
|
|
7
|
+
replaces every run, so **format runs, never paragraphs**:
|
|
8
|
+
|
|
9
|
+
- `paragraph.text = "x"` — one run, all previous formatting gone.
|
|
10
|
+
- `run = paragraph.add_run(); run.text = "x"; run.font.bold = True` — formatting
|
|
11
|
+
survives and can differ inside the paragraph.
|
|
12
|
+
- `paragraph.font.size` is not a thing PowerPoint respects for mixed content; it
|
|
13
|
+
sets the default for later runs only. Set the size on every run you add.
|
|
14
|
+
|
|
15
|
+
Paragraph-level properties (`alignment`, `level`, `space_after`,
|
|
16
|
+
`line_spacing`) apply to the whole paragraph and are safe to set once.
|
|
17
|
+
|
|
18
|
+
## Recipe
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
"<python>" - <<'PY'
|
|
22
|
+
from pptx import Presentation
|
|
23
|
+
from pptx.util import Inches, Pt
|
|
24
|
+
from pptx.dml.color import RGBColor
|
|
25
|
+
from pptx.enum.text import PP_ALIGN
|
|
26
|
+
|
|
27
|
+
prs = Presentation()
|
|
28
|
+
prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)
|
|
29
|
+
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
30
|
+
|
|
31
|
+
box = slide.shapes.add_textbox(Inches(0.9), Inches(2.0), Inches(11.0), Inches(1.6))
|
|
32
|
+
frame = box.text_frame
|
|
33
|
+
frame.word_wrap = True
|
|
34
|
+
|
|
35
|
+
MUTED = RGBColor(0x5F, 0x6B, 0x7A)
|
|
36
|
+
ACCENT = RGBColor(0xD9, 0x8B, 0x1E)
|
|
37
|
+
|
|
38
|
+
lead = frame.paragraphs[0]
|
|
39
|
+
lead.alignment = PP_ALIGN.LEFT
|
|
40
|
+
for text, bold, size, colour in (
|
|
41
|
+
("Orders fulfilled ", False, Pt(28), RGBColor(0x16, 0x1D, 0x28)),
|
|
42
|
+
("rose 71%", True, Pt(28), ACCENT),
|
|
43
|
+
(" across FY2026.", False, Pt(28), RGBColor(0x16, 0x1D, 0x28)),
|
|
44
|
+
):
|
|
45
|
+
run = lead.add_run()
|
|
46
|
+
run.text = text
|
|
47
|
+
run.font.bold = bold
|
|
48
|
+
run.font.size = size
|
|
49
|
+
run.font.color.rgb = colour
|
|
50
|
+
|
|
51
|
+
footnote = frame.add_paragraph()
|
|
52
|
+
footnote.space_before = Pt(10)
|
|
53
|
+
note = footnote.add_run()
|
|
54
|
+
note.text = "Q4 is forecast; the other three quarters are actuals."
|
|
55
|
+
note.font.size = Pt(12)
|
|
56
|
+
note.font.italic = True
|
|
57
|
+
note.font.color.rgb = MUTED
|
|
58
|
+
prs.save("runs.pptx")
|
|
59
|
+
print("runs in paragraph 0:", len(lead.runs))
|
|
60
|
+
PY
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Rules
|
|
64
|
+
|
|
65
|
+
1. A number you want the reader to remember gets its own run, and usually a
|
|
66
|
+
weight change — not a second bullet.
|
|
67
|
+
2. Superscripts, footnote markers and unit symbols are runs, not characters
|
|
68
|
+
smuggled into the sentence.
|
|
69
|
+
3. Keep one family of emphasis per deck: bold+accent for numbers, italic+muted
|
|
70
|
+
for provenance. Three competing emphases read as decoration.
|
|
71
|
+
4. Strings that repeat (units, currency, product names) belong in a variable so
|
|
72
|
+
a later edit does not leave two spellings.
|
|
73
|
+
|
|
74
|
+
## Pitfalls this avoids
|
|
75
|
+
|
|
76
|
+
- `paragraph.text = "Total: "` then `add_run()`: the first assignment already
|
|
77
|
+
replaced the frame's runs, so formatting set beforehand is gone.
|
|
78
|
+
- A run with no `font.size` inherits the layout's size, which is 44 pt on a
|
|
79
|
+
title layout — footnote markers become headlines.
|
|
80
|
+
- Building the paragraph by f-string with `**` markers that PowerPoint does not
|
|
81
|
+
interpret; markup in a deck is a run, not a syntax.
|
|
82
|
+
|
|
83
|
+
## CLI fast path
|
|
84
|
+
|
|
85
|
+
`pptx create` writes plain paragraphs; bullets and titles are single-run. Mixed
|
|
86
|
+
formatting inside one line is the recipe's job.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Speaker notes
|
|
2
|
+
|
|
3
|
+
## Mechanics
|
|
4
|
+
|
|
5
|
+
Notes live in `ppt/notesMasters/` plus one `ppt/notesSlides/notesSlideN.xml` per
|
|
6
|
+
slide, tied to the slide by a relationship. In `python-pptx` this is
|
|
7
|
+
`slide.notes_slide.notes_text_frame` — the notes slide is created on first
|
|
8
|
+
access, so reading it in a loop silently adds one per slide. Only touch it when
|
|
9
|
+
you are writing.
|
|
10
|
+
|
|
11
|
+
Notes are not visible in the render. That means a notes problem cannot be caught
|
|
12
|
+
by looking at the PNGs: check them with `pptx read` or by reading the XML back.
|
|
13
|
+
|
|
14
|
+
## Recipe
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
"<python>" - <<'PY'
|
|
18
|
+
from pptx import Presentation
|
|
19
|
+
from pptx.util import Pt
|
|
20
|
+
|
|
21
|
+
NOTES = {
|
|
22
|
+
1: "Open with the ask: approve two cells, not three.",
|
|
23
|
+
2: "41% comes from the Q3 time study; the two other numbers are from the driver logs.",
|
|
24
|
+
4: "Say the total out loud: 553 orders over four quarters.",
|
|
25
|
+
5: "Peak hour is 14:00-15:00; the bars are Q1-Q4, not hours.",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
prs = Presentation("deck.pptx")
|
|
29
|
+
for index, slide in enumerate(prs.slides, start=1):
|
|
30
|
+
if index not in NOTES:
|
|
31
|
+
continue
|
|
32
|
+
frame = slide.notes_slide.notes_text_frame
|
|
33
|
+
frame.text = NOTES[index]
|
|
34
|
+
for paragraph in frame.paragraphs:
|
|
35
|
+
paragraph.font.size = Pt(12)
|
|
36
|
+
prs.save("deck-notes.pptx")
|
|
37
|
+
print("notes written:", len(NOTES))
|
|
38
|
+
PY
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What belongs in a note
|
|
42
|
+
|
|
43
|
+
- The sentence you will actually say, not a copy of the slide.
|
|
44
|
+
- The provenance of any number the audience may challenge ("Q3 time study,
|
|
45
|
+
12 sites").
|
|
46
|
+
- The answer to the question the slide invites.
|
|
47
|
+
- Nothing that belongs on the slide: if the audience needs it, put it on the
|
|
48
|
+
slide, notes are invisible when the deck is sent as a PDF.
|
|
49
|
+
|
|
50
|
+
## Pitfalls this avoids
|
|
51
|
+
|
|
52
|
+
- Iterating `prs.slides` and *reading* `notes_slide` on a template with none:
|
|
53
|
+
every slide gains an empty notes part and the file grows.
|
|
54
|
+
- Long notes pasted as one paragraph with line breaks that only look like
|
|
55
|
+
paragraphs; each intended paragraph should be `add_paragraph()` so the notes
|
|
56
|
+
pane and the presenter view indent them.
|
|
57
|
+
- Assuming notes render into the preview: they do not, so verify them from the
|
|
58
|
+
file.
|
|
59
|
+
|
|
60
|
+
## CLI fast path
|
|
61
|
+
|
|
62
|
+
`pptx create` writes slides without notes. The recipe above is the route; then
|
|
63
|
+
`pptx read --input deck-notes.pptx` does not print notes, so read them back with
|
|
64
|
+
`ppt/slides` sibling `notesSlide` parts via
|
|
65
|
+
`inspect_presentation_package_integrity.py` (it counts `notesSlides`).
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Tables
|
|
2
|
+
|
|
3
|
+
## Mechanics
|
|
4
|
+
|
|
5
|
+
A table in a deck is a grid of cells on a canvas; it does not reflow. Three
|
|
6
|
+
properties decide whether it reads as a table or as soup:
|
|
7
|
+
|
|
8
|
+
- **Column widths** are the layout. `table.columns[i].width = Inches(3.2)` —
|
|
9
|
+
setting them is mandatory, because `add_table` splits the width evenly.
|
|
10
|
+
- **Row heights** are a minimum. Text taller than the row pushes it open, which
|
|
11
|
+
then runs into whatever sits below.
|
|
12
|
+
- **Cell margins** default to 0.1 in left/right and 0.05 in top/bottom on a
|
|
13
|
+
0.5 in row, which is most of the row. Reduce them when rows are tight.
|
|
14
|
+
|
|
15
|
+
Header row emphasis (`table.first_row = True`) is a table style switch, not a
|
|
16
|
+
formatting guess: it turns on the theme's header fill and bold.
|
|
17
|
+
|
|
18
|
+
## Recipe
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
"<python>" - <<'PY'
|
|
22
|
+
from pptx import Presentation
|
|
23
|
+
from pptx.util import Inches, Pt
|
|
24
|
+
from pptx.dml.color import RGBColor
|
|
25
|
+
|
|
26
|
+
ROWS = [
|
|
27
|
+
("Quarter", "Orders", "Touches"),
|
|
28
|
+
("Q1", "100", "410"),
|
|
29
|
+
("Q2", "128", "402"),
|
|
30
|
+
("Q3", "154", "386"),
|
|
31
|
+
("Q4", "171", "371"),
|
|
32
|
+
("Total", "553", "1569"),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
prs = Presentation()
|
|
36
|
+
prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)
|
|
37
|
+
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
38
|
+
|
|
39
|
+
shape = slide.shapes.add_table(
|
|
40
|
+
len(ROWS), len(ROWS[0]), Inches(0.9), Inches(1.7), Inches(9.6), Inches(0.46 * len(ROWS))
|
|
41
|
+
)
|
|
42
|
+
table = shape.table
|
|
43
|
+
table.first_row = True
|
|
44
|
+
for index, width in enumerate((3.4, 3.1, 3.1)):
|
|
45
|
+
table.columns[index].width = Inches(width)
|
|
46
|
+
for row_index, row in enumerate(ROWS):
|
|
47
|
+
table.rows[row_index].height = Inches(0.46)
|
|
48
|
+
for column_index, value in enumerate(row):
|
|
49
|
+
cell = table.cell(row_index, column_index)
|
|
50
|
+
cell.text = value
|
|
51
|
+
cell.margin_left = cell.margin_right = Inches(0.12)
|
|
52
|
+
cell.margin_top = cell.margin_bottom = Inches(0.04)
|
|
53
|
+
for paragraph in cell.text_frame.paragraphs:
|
|
54
|
+
paragraph.font.size = Pt(16)
|
|
55
|
+
if row_index == 0:
|
|
56
|
+
paragraph.font.bold = True
|
|
57
|
+
if column_index > 0:
|
|
58
|
+
paragraph.alignment = 2 # right
|
|
59
|
+
total = table.cell(len(ROWS) - 1, 0)
|
|
60
|
+
for paragraph in total.text_frame.paragraphs:
|
|
61
|
+
paragraph.font.bold = True
|
|
62
|
+
prs.save("table.pptx")
|
|
63
|
+
print(f"rows={len(ROWS)} total_width={sum(c.width for c in table.columns) / 914400:.2f}in")
|
|
64
|
+
PY
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Numbers people trust
|
|
68
|
+
|
|
69
|
+
- Right-align numeric columns, left-align the label column, and keep decimals
|
|
70
|
+
consistent per column (`553`, not `553.0` next to `171`).
|
|
71
|
+
- A total row is a claim. Show enough rows that the reader can check it, and
|
|
72
|
+
let `container_tools/native_table_arithmetic_gate.py` prove it from the file.
|
|
73
|
+
- Units belong in the header (`Orders`, `Touches`), not repeated in every cell.
|
|
74
|
+
- Money keeps its currency in the header or the first row; `¥1,240` in every
|
|
75
|
+
cell of a dense table is noise.
|
|
76
|
+
|
|
77
|
+
## Pitfalls this avoids
|
|
78
|
+
|
|
79
|
+
- Even column widths turning "Q1" into a 4 in column while numbers crowd.
|
|
80
|
+
- A table whose declared height is smaller than its content: the last row is
|
|
81
|
+
drawn below the intended block and collides with the footer.
|
|
82
|
+
- Colouring cells individually instead of letting `first_row` do the header.
|
|
83
|
+
|
|
84
|
+
## CLI fast path
|
|
85
|
+
|
|
86
|
+
`pptx create` accepts `{"table": {"headers": [...], "rows": [[...]]}}` and
|
|
87
|
+
places it at 0.8 in from the left, full content width, at the running cursor.
|
|
88
|
+
For a chosen rect, column widths or right-aligned numbers, use the recipe.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Textbox at a coordinate
|
|
2
|
+
|
|
3
|
+
## Mechanics
|
|
4
|
+
|
|
5
|
+
A free textbox is placed by its rect, not by a layout slot. Four facts control
|
|
6
|
+
what you see:
|
|
7
|
+
|
|
8
|
+
- `left`/`top` are the **top-left corner** of the frame, not of the glyphs. A
|
|
9
|
+
28 pt line has roughly 4 pt of space above the cap height inside its frame.
|
|
10
|
+
- `width` is the wrap width. `word_wrap = True` must be set explicitly on a new
|
|
11
|
+
box or long lines run past the slide edge instead of wrapping.
|
|
12
|
+
- `height` is the *frame*, not the content: PowerPoint lets text spill below it
|
|
13
|
+
(which overlaps whatever is under the box) unless autosize is turned on.
|
|
14
|
+
- Interior margins default to 0.1 in left/right and 0.05 in top/bottom. Setting
|
|
15
|
+
them to 0 makes the rect mean exactly what you measured.
|
|
16
|
+
|
|
17
|
+
## Recipe
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
"<python>" - <<'PY'
|
|
21
|
+
from pptx import Presentation
|
|
22
|
+
from pptx.util import Inches, Pt
|
|
23
|
+
from pptx.enum.text import MSO_ANCHOR
|
|
24
|
+
|
|
25
|
+
prs = Presentation()
|
|
26
|
+
prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)
|
|
27
|
+
slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank
|
|
28
|
+
|
|
29
|
+
box = slide.shapes.add_textbox(Inches(0.9), Inches(1.35), Inches(6.2), Inches(1.1))
|
|
30
|
+
frame = box.text_frame
|
|
31
|
+
frame.word_wrap = True
|
|
32
|
+
frame.vertical_anchor = MSO_ANCHOR.TOP
|
|
33
|
+
frame.margin_left = frame.margin_right = 0
|
|
34
|
+
frame.margin_top = frame.margin_bottom = 0
|
|
35
|
+
|
|
36
|
+
paragraph = frame.paragraphs[0]
|
|
37
|
+
paragraph.text = "Picking is 41% of floor labour hours"
|
|
38
|
+
paragraph.font.size = Pt(28)
|
|
39
|
+
paragraph.font.bold = True
|
|
40
|
+
prs.save("textbox.pptx")
|
|
41
|
+
print("saved")
|
|
42
|
+
PY
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Measuring before you place
|
|
46
|
+
|
|
47
|
+
Do not guess whether the text fits — compute it. A line of 28 pt text fits
|
|
48
|
+
about `width_in * 96 / (0.55 * 28)` characters (0.55 average advance for a
|
|
49
|
+
humanist sans), and each line needs `1.2 * 28 / 72` inches of height. Leave the
|
|
50
|
+
estimate in the script, then let the render confirm it.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
def fits(text: str, width_in: float, height_in: float, size_pt: float) -> bool:
|
|
54
|
+
per_line = max(1, int(width_in * 96 / (0.55 * size_pt)))
|
|
55
|
+
lines = -(-len(text) // per_line)
|
|
56
|
+
return lines * 1.2 * size_pt / 72 <= height_in
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Pitfalls this avoids
|
|
60
|
+
|
|
61
|
+
- A box sized to the text at 18 pt, later edited to 32 pt, now overlaps the
|
|
62
|
+
table below. Frame height follows the *largest* run, so size text first.
|
|
63
|
+
- `add_textbox` with `width=Inches(13.3)` on a 13.333 in slide plus default
|
|
64
|
+
margins = 0.2 in of overflow past the right edge. Subtract the margins.
|
|
65
|
+
- `MSO_ANCHOR.MIDDLE` on a title looks intentional on a cover and wrong on a
|
|
66
|
+
content slide; anchor per role, not globally.
|
|
67
|
+
|
|
68
|
+
## CLI fast path
|
|
69
|
+
|
|
70
|
+
`pptx create` places titles and bullet bodies on the layout's own frames. For
|
|
71
|
+
anything at a coordinate, the snippet above is the route.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Theme tokens
|
|
2
|
+
|
|
3
|
+
## Mechanics
|
|
4
|
+
|
|
5
|
+
The theme part (`ppt/theme/theme1.xml`) holds two palettes and two font lists:
|
|
6
|
+
|
|
7
|
+
- `dk1/lt1/dk2/lt2/accent1..6/hlink/folHlink` — every `RGBColor` you *do not*
|
|
8
|
+
set falls back to one of these, and PowerPoint's "theme colors" picker edits
|
|
9
|
+
them for the whole deck.
|
|
10
|
+
- `majorFont` / `minorFont` — the heading and body typefaces inherited by every
|
|
11
|
+
run without an explicit font.
|
|
12
|
+
|
|
13
|
+
So: write `RGBColor.rgb` when a colour must never change, and reference a theme
|
|
14
|
+
colour when the user may re-skin the deck. A deck with hard-coded hex
|
|
15
|
+
everywhere cannot be re-branded, which is usually why the user supplied a
|
|
16
|
+
template.
|
|
17
|
+
|
|
18
|
+
## Recipe: define the palette once, use it everywhere
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
"<python>" - <<'PY'
|
|
22
|
+
from pptx import Presentation
|
|
23
|
+
from pptx.dml.color import RGBColor
|
|
24
|
+
from pptx.util import Inches, Pt
|
|
25
|
+
from pptx.enum.dml import MSO_THEME_COLOR
|
|
26
|
+
|
|
27
|
+
PALETTE = {
|
|
28
|
+
"ink": RGBColor(0x16, 0x1D, 0x28),
|
|
29
|
+
"muted": RGBColor(0x5F, 0x6B, 0x7A),
|
|
30
|
+
"accent": RGBColor(0xD9, 0x8B, 0x1E),
|
|
31
|
+
"rule": RGBColor(0xD6, 0xDC, 0xE4),
|
|
32
|
+
}
|
|
33
|
+
TYPE = {"title": Pt(34), "body": Pt(18), "caption": Pt(12)}
|
|
34
|
+
|
|
35
|
+
prs = Presentation()
|
|
36
|
+
prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)
|
|
37
|
+
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
38
|
+
|
|
39
|
+
def put(text, left, top, width, size, colour, bold=False):
|
|
40
|
+
box = slide.shapes.add_textbox(Inches(left), Inches(top), Inches(width), Inches(0.6))
|
|
41
|
+
frame = box.text_frame
|
|
42
|
+
frame.word_wrap = True
|
|
43
|
+
frame.margin_left = frame.margin_right = 0
|
|
44
|
+
run = frame.paragraphs[0].add_run()
|
|
45
|
+
run.text = text
|
|
46
|
+
run.font.size = size
|
|
47
|
+
run.font.bold = bold
|
|
48
|
+
run.font.color.rgb = PALETTE[colour]
|
|
49
|
+
return box
|
|
50
|
+
|
|
51
|
+
put("Automation Review", 0.9, 2.4, 11.5, TYPE["title"], "ink", bold=True)
|
|
52
|
+
put("Warehouse network · FY2026", 0.9, 3.3, 11.5, TYPE["body"], "muted")
|
|
53
|
+
put("Source: FY2026 capital plan", 0.9, 6.7, 6.0, TYPE["caption"], "muted")
|
|
54
|
+
prs.save("themed.pptx")
|
|
55
|
+
print("palette keys:", sorted(PALETTE), "theme colour sample:", MSO_THEME_COLOR.ACCENT_1)
|
|
56
|
+
PY
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Reading the theme you were given
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from pptx import Presentation
|
|
63
|
+
prs = Presentation("template.pptx")
|
|
64
|
+
theme = prs.slide_masters[0].part.part_related_by(
|
|
65
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"
|
|
66
|
+
)
|
|
67
|
+
blob = theme.blob.decode("utf-8", "ignore")
|
|
68
|
+
print("accent1" in blob, "majorFont" in blob)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`template_following_scripts/inspect_template_deck.py` already reports the font
|
|
72
|
+
names it finds in the theme, so check there before introducing a new typeface.
|
|
73
|
+
|
|
74
|
+
## Pitfalls this avoids
|
|
75
|
+
|
|
76
|
+
- Two similar hexes drifting apart across slides (`#161d28` on one slide,
|
|
77
|
+
`#171e29` on the next) because the colour was retyped instead of referenced.
|
|
78
|
+
- A serif body font set from taste rather than from the theme, so the deck
|
|
79
|
+
contradicts the template it was built in.
|
|
80
|
+
- `font.color.theme_color = MSO_THEME_COLOR.ACCENT_1` *and* `font.color.rgb`
|
|
81
|
+
on the same run: the last assignment wins and the theme reference vanishes.
|
|
82
|
+
|
|
83
|
+
## CLI fast path
|
|
84
|
+
|
|
85
|
+
`pptx create` uses the default template's theme. For a branded deck, start from
|
|
86
|
+
the supplied template (see [master-and-layouts](master-and-layouts.md)) rather
|
|
87
|
+
than recolouring a blank deck.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Authoring workflow
|
|
2
|
+
|
|
3
|
+
## What a deck is, mechanically
|
|
4
|
+
|
|
5
|
+
A `.pptx` is a zip of XML parts: one `presentation.xml` naming the slide order, one
|
|
6
|
+
part per slide, a slide master per theme, a layout per family, chart parts with
|
|
7
|
+
embedded workbooks, notes parts, and a media folder. Everything the reader sees
|
|
8
|
+
comes from those parts, which is why the verification loop reads the file back
|
|
9
|
+
instead of trusting the code that wrote it.
|
|
10
|
+
|
|
11
|
+
Three consequences shape the whole workflow:
|
|
12
|
+
|
|
13
|
+
1. **Geometry is explicit.** Nothing reflows on open except autofit. If two
|
|
14
|
+
shapes overlap in the XML, they overlap on screen.
|
|
15
|
+
2. **Inheritance is real.** A slide draws its background, type sizes and bullet
|
|
16
|
+
glyphs from its layout and master. Overriding at slide level is a per-slide
|
|
17
|
+
decision; overriding at master level is a deck decision.
|
|
18
|
+
3. **Data can stay alive.** A native chart or table keeps its numbers in the
|
|
19
|
+
file and can be checked; a screenshot cannot.
|
|
20
|
+
|
|
21
|
+
## The loop
|
|
22
|
+
|
|
23
|
+
1. **Decide the story**: one sentence per slide, in order, before any XML. A
|
|
24
|
+
slide without a sentence is a slide the audience will not read.
|
|
25
|
+
2. **Pick the frame**: slide size from the target (`13.333 × 7.5 in` is the 16:9
|
|
26
|
+
default; check `prs.slide_width` of a supplied template, never assume).
|
|
27
|
+
3. **Place, top-down**: background, chrome, title, body, evidence. Later shapes
|
|
28
|
+
paint over earlier ones.
|
|
29
|
+
4. **Fill or delete every placeholder.** An empty placeholder is visible in edit
|
|
30
|
+
view and prints "Click to add text".
|
|
31
|
+
5. **Render, look, fix.** `container_tools/render_slides.py` then `read_image` on
|
|
32
|
+
every PNG. Repeat until nothing surprises you.
|
|
33
|
+
|
|
34
|
+
## The numbers that keep a deck readable
|
|
35
|
+
|
|
36
|
+
| Element | Value | Why |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| Slide size | 13.333 × 7.5 in | 16:9; 10 × 5.625 in for 16:9 in "wide" templates |
|
|
39
|
+
| Margin | 0.6–0.9 in | Below 0.5 in a projector crops it |
|
|
40
|
+
| Title | 28–36 pt, bold | 44 pt only on covers |
|
|
41
|
+
| Body | 18 pt minimum | The back row rule |
|
|
42
|
+
| Caption / source | 12–14 pt | Never below 11 pt |
|
|
43
|
+
| Line spacing | 1.1–1.2 | Tighter crowds ascenders |
|
|
44
|
+
| Gap between blocks | 0.2–0.4 in | The eye groups by proximity |
|
|
45
|
+
| Footer zone | bottom 0.55 in | Keep text out of it |
|
|
46
|
+
|
|
47
|
+
Convert once and keep the numbers in EMU or inches, not mixed: `Inches(x)` in
|
|
48
|
+
code, `x * 914400` when you must talk to the XML directly.
|
|
49
|
+
|
|
50
|
+
## Deciding slide count
|
|
51
|
+
|
|
52
|
+
One idea per slide. If a slide has two headlines competing, it is two slides. If
|
|
53
|
+
it has one headline and a table too large to read at 14 pt, the table becomes an
|
|
54
|
+
appendix slide and the body slide states the conclusion in one line.
|
|
55
|
+
|
|
56
|
+
A presentation of N minutes needs roughly N/2 content slides at 60–90 seconds
|
|
57
|
+
each, plus a cover and a closing ask.
|
|
58
|
+
|
|
59
|
+
## Where the detail lives
|
|
60
|
+
|
|
61
|
+
- Type, colour, density and cover art: [style-guidelines](style-guidelines.md).
|
|
62
|
+
- Bullets that read as sentences: [native-bullets](native-bullets.md).
|
|
63
|
+
- Charts, tables and notes as evidence: [native-evidence](native-evidence.md).
|
|
64
|
+
- Supplied templates: [templates](templates.md).
|
|
65
|
+
- Pre-delivery checklist: [finalization](finalization.md).
|