dsh-office-kit 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +99 -0
- package/assets/skills/office-documents/scripts/a11y_audit.py +201 -0
- package/assets/skills/office-documents/scripts/accept_tracked_changes.py +158 -0
- package/assets/skills/office-documents/scripts/add_tracked_replacements.py +155 -0
- package/assets/skills/office-documents/scripts/append_blocks.py +115 -0
- package/assets/skills/office-documents/scripts/apply_template_styles.py +111 -0
- package/assets/skills/office-documents/scripts/captions_and_crossrefs.py +109 -0
- package/assets/skills/office-documents/scripts/comments_add.py +143 -0
- package/assets/skills/office-documents/scripts/comments_apply_patch.py +121 -0
- package/assets/skills/office-documents/scripts/comments_extract.py +92 -0
- package/assets/skills/office-documents/scripts/comments_strip.py +81 -0
- package/assets/skills/office-documents/scripts/content_controls.py +199 -0
- package/assets/skills/office-documents/scripts/core_properties.py +131 -0
- package/assets/skills/office-documents/scripts/docx_kit.py +594 -0
- package/assets/skills/office-documents/scripts/docx_ooxml_patch.py +232 -0
- package/assets/skills/office-documents/scripts/docx_table_to_csv.py +106 -0
- package/assets/skills/office-documents/scripts/fields_materialize.py +73 -0
- package/assets/skills/office-documents/scripts/fields_report.py +66 -0
- package/assets/skills/office-documents/scripts/flatten_ref_fields.py +69 -0
- package/assets/skills/office-documents/scripts/footnotes_report.py +84 -0
- package/assets/skills/office-documents/scripts/google_docs_title_sanitize.py +103 -0
- package/assets/skills/office-documents/scripts/heading_audit.py +88 -0
- package/assets/skills/office-documents/scripts/images_audit.py +87 -0
- package/assets/skills/office-documents/scripts/insert_note.py +133 -0
- package/assets/skills/office-documents/scripts/insert_ref_fields.py +97 -0
- package/assets/skills/office-documents/scripts/insert_toc.py +91 -0
- package/assets/skills/office-documents/scripts/internal_nav.py +153 -0
- package/assets/skills/office-documents/scripts/make_fixtures.py +176 -0
- package/assets/skills/office-documents/scripts/merge_docx_append.py +246 -0
- package/assets/skills/office-documents/scripts/privacy_scrub.py +138 -0
- package/assets/skills/office-documents/scripts/redact_docx.py +120 -0
- package/assets/skills/office-documents/scripts/render_and_diff.py +108 -0
- package/assets/skills/office-documents/scripts/replace_text.py +161 -0
- package/assets/skills/office-documents/scripts/section_audit.py +98 -0
- package/assets/skills/office-documents/scripts/set_protection.py +99 -0
- package/assets/skills/office-documents/scripts/style_lint.py +118 -0
- package/assets/skills/office-documents/scripts/style_normalize.py +110 -0
- package/assets/skills/office-documents/scripts/table_geometry.py +174 -0
- package/assets/skills/office-documents/scripts/watermark_add.py +55 -0
- package/assets/skills/office-documents/scripts/watermark_audit_remove.py +74 -0
- package/assets/skills/office-documents/scripts/xlsx_to_docx_table.py +124 -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 +143 -0
- package/assets/skills/office-presentations/container_tools/core_properties.py +127 -0
- package/assets/skills/office-presentations/container_tools/create_montage.py +133 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_layout_geometry.py +292 -0
- package/assets/skills/office-presentations/container_tools/inspect_presentation_package_integrity.py +164 -0
- package/assets/skills/office-presentations/container_tools/native_chart_title_gate.py +151 -0
- package/assets/skills/office-presentations/container_tools/native_quantitative_chart_gate.py +206 -0
- package/assets/skills/office-presentations/container_tools/native_table_arithmetic_gate.py +219 -0
- package/assets/skills/office-presentations/container_tools/ooxml_helpers.py +352 -0
- package/assets/skills/office-presentations/container_tools/preview_render.py +382 -0
- package/assets/skills/office-presentations/container_tools/render_slides.py +146 -0
- package/assets/skills/office-presentations/container_tools/replace_text.py +175 -0
- package/assets/skills/office-presentations/container_tools/template_source_fidelity_gate.py +236 -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 +183 -0
- package/assets/skills/office-presentations/template_following_scripts/inspect_template_deck.py +161 -0
- package/assets/skills/office-presentations/template_following_scripts/make_contact_sheet.py +118 -0
- package/assets/skills/office-presentations/template_following_scripts/prepare_template_starter_deck.py +246 -0
- package/assets/skills/office-presentations/template_following_scripts/validate_template_plan.py +192 -0
- package/assets/skills/office-spreadsheets/SKILL.md +105 -101
- package/assets/skills/office-spreadsheets/container_tools/add_chart.py +243 -0
- package/assets/skills/office-spreadsheets/container_tools/add_image.py +114 -0
- package/assets/skills/office-spreadsheets/container_tools/append_rows.py +129 -0
- package/assets/skills/office-spreadsheets/container_tools/diff_workbooks.py +313 -0
- package/assets/skills/office-spreadsheets/container_tools/inspect_workbook.py +405 -0
- package/assets/skills/office-spreadsheets/container_tools/recalc_gate.py +287 -0
- package/assets/skills/office-spreadsheets/container_tools/set_cells.py +142 -0
- package/assets/skills/office-spreadsheets/container_tools/sheet_ops.py +141 -0
- package/assets/skills/office-spreadsheets/container_tools/style_cells.py +140 -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 +260 -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 +246 -89
- package/package.json +1 -1
- package/postinstall.mjs +92 -82
- package/python/office_cli.py +213 -254
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Connectors
|
|
2
|
+
|
|
3
|
+
## Mechanics
|
|
4
|
+
|
|
5
|
+
A connector is a line with two attachment sites. `add_connector` takes a begin
|
|
6
|
+
and end shape *plus* connection indices, which is what makes the line follow the
|
|
7
|
+
boxes when they move. If you pass coordinates instead, the line is a floating
|
|
8
|
+
line: it looks right in the file you made and wrong the first time a box is
|
|
9
|
+
nudged.
|
|
10
|
+
|
|
11
|
+
- `MSO_CONNECTOR.STRAIGHT | ELBOW | CURVE`
|
|
12
|
+
- `connector.begin_connect(shape, idx)` / `end_connect(shape, idx)`; `idx` is
|
|
13
|
+
the site index on the shape's outline (0 = top, 1 = left, 2 = bottom,
|
|
14
|
+
3 = right for a rectangle).
|
|
15
|
+
- Arrowheads are line properties: `line.end_arrowhead = MSO_ARROWHEAD.TRIANGLE`.
|
|
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.shapes import MSO_SHAPE, MSO_CONNECTOR
|
|
24
|
+
from pptx.dml.color import RGBColor
|
|
25
|
+
from pptx.enum.dml import MSO_LINE_DASH_STYLE
|
|
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
|
+
def box(left, top, text, width=2.6, height=1.1):
|
|
32
|
+
shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, Inches(left), Inches(top), Inches(width), Inches(height))
|
|
33
|
+
shape.text_frame.text = text
|
|
34
|
+
shape.text_frame.paragraphs[0].font.size = Pt(14)
|
|
35
|
+
shape.fill.solid()
|
|
36
|
+
shape.fill.fore_color.rgb = RGBColor(0xF2, 0xF5, 0xF9)
|
|
37
|
+
shape.line.color.rgb = RGBColor(0xC8, 0xD2, 0xDF)
|
|
38
|
+
return shape
|
|
39
|
+
|
|
40
|
+
order = box(0.9, 2.2, "Order booked")
|
|
41
|
+
pick = box(4.4, 2.2, "Picked")
|
|
42
|
+
ship = box(7.9, 2.2, "Shipped")
|
|
43
|
+
|
|
44
|
+
for start, end in ((order, pick), (pick, ship)):
|
|
45
|
+
connector = slide.shapes.add_connector(
|
|
46
|
+
MSO_CONNECTOR.STRAIGHT, 0, 0, Inches(0.1), Inches(0.1)
|
|
47
|
+
)
|
|
48
|
+
connector.begin_connect(start, 3) # right edge
|
|
49
|
+
connector.end_connect(end, 1) # left edge
|
|
50
|
+
connector.line.width = Pt(2)
|
|
51
|
+
connector.line.color.rgb = RGBColor(0x5F, 0x6B, 0x7A)
|
|
52
|
+
connector.line.end_arrowhead = 3 # triangle
|
|
53
|
+
|
|
54
|
+
prs.save("connectors.pptx")
|
|
55
|
+
from pptx.oxml.ns import qn
|
|
56
|
+
print("connectors:", sum(1 for s in slide.shapes if s._element.tag == qn("p:cxnSp")))
|
|
57
|
+
PY
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Rules
|
|
61
|
+
|
|
62
|
+
1. Connect sides that face each other: right → left for a row, bottom → top for
|
|
63
|
+
a column. A line crossing its own boxes reads as an error.
|
|
64
|
+
2. Elbow connectors for orthogonal flows, straight for a single hop, curve almost
|
|
65
|
+
never.
|
|
66
|
+
3. Two crossing connectors in one diagram is the point where the diagram becomes
|
|
67
|
+
a picture of a diagram; split it.
|
|
68
|
+
4. Label the connector ("12 min", "auto") with a small textbox near its middle,
|
|
69
|
+
not with arrow direction alone.
|
|
70
|
+
|
|
71
|
+
## Pitfalls this avoids
|
|
72
|
+
|
|
73
|
+
- `add_connector` with free coordinates: the line is positioned but unattached,
|
|
74
|
+
and the deck's own edits break it silently.
|
|
75
|
+
- Arrowhead index confusion: arrowhead values are a bitmask enum, not a boolean.
|
|
76
|
+
- Connectors behind filled shapes because they were added before the boxes.
|
|
77
|
+
|
|
78
|
+
## CLI fast path
|
|
79
|
+
|
|
80
|
+
There is no connector verb in `pptx create`; this recipe is the route. After
|
|
81
|
+
building, `container_tools/inspect_presentation_layout_geometry.py` flags
|
|
82
|
+
`connector_over_text` when a line crosses a text shape, which is the failure
|
|
83
|
+
mode you cannot see in a thumbnail.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Images
|
|
2
|
+
|
|
3
|
+
## Mechanics
|
|
4
|
+
|
|
5
|
+
`add_picture(path, left, top, width=…, height=…)`:
|
|
6
|
+
|
|
7
|
+
- Give **width only** and the height follows the aspect ratio. Give both and
|
|
8
|
+
the picture is stretched — a deliberate choice for a full-bleed background,
|
|
9
|
+
a mistake for a product photo.
|
|
10
|
+
- `picture.crop_left/right/top/bottom` are fractions of the *source*; they crop
|
|
11
|
+
without changing the frame, which is how a "cover" fit is done.
|
|
12
|
+
- The bytes live in `ppt/media/`; the same file added twice is stored once,
|
|
13
|
+
keyed by content hash.
|
|
14
|
+
- `picture._element.nvPicPr.cNvPr.set("descr", "…")` is the alt text. Decks get
|
|
15
|
+
read aloud and exported; an untagged picture is an empty sentence.
|
|
16
|
+
|
|
17
|
+
## Recipe: cover-fit into a fixed frame
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
"<python>" - <<'PY'
|
|
21
|
+
from pptx import Presentation
|
|
22
|
+
from pptx.util import Inches
|
|
23
|
+
from PIL import Image
|
|
24
|
+
|
|
25
|
+
def cover(picture, frame_w_in, frame_h_in, source_w, source_h):
|
|
26
|
+
"""Crop the picture so it fills the frame without distortion."""
|
|
27
|
+
frame_ratio = frame_w_in / frame_h_in
|
|
28
|
+
source_ratio = source_w / source_h
|
|
29
|
+
if source_ratio > frame_ratio: # too wide: crop the sides
|
|
30
|
+
keep = frame_ratio / source_ratio
|
|
31
|
+
picture.crop_left = picture.crop_right = (1 - keep) / 2
|
|
32
|
+
elif source_ratio < frame_ratio: # too tall: crop top and bottom
|
|
33
|
+
keep = source_ratio / frame_ratio
|
|
34
|
+
picture.crop_top = picture.crop_bottom = (1 - keep) / 2
|
|
35
|
+
|
|
36
|
+
prs = Presentation()
|
|
37
|
+
prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)
|
|
38
|
+
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
39
|
+
|
|
40
|
+
with Image.open("aisle.png") as image:
|
|
41
|
+
source_w, source_h = image.size
|
|
42
|
+
|
|
43
|
+
frame = slide.shapes.add_picture("aisle.png", Inches(0.9), Inches(1.7), Inches(8.0), Inches(4.5))
|
|
44
|
+
cover(frame, 8.0, 4.5, source_w, source_h)
|
|
45
|
+
frame._element.nvPicPr.cNvPr.set("descr", "Racking aisle in the pilot cell at peak hour")
|
|
46
|
+
|
|
47
|
+
# full-bleed background, sent to the back
|
|
48
|
+
background = slide.shapes.add_picture("aisle.png", 0, 0, prs.slide_width, prs.slide_height)
|
|
49
|
+
tree = slide.shapes._spTree
|
|
50
|
+
tree.remove(background._element)
|
|
51
|
+
tree.insert(2, background._element)
|
|
52
|
+
prs.save("image.pptx")
|
|
53
|
+
print("placed", source_w, "x", source_h)
|
|
54
|
+
PY
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Rules
|
|
58
|
+
|
|
59
|
+
1. Never upscale past the source resolution: a 900 px wide image at 8 in is
|
|
60
|
+
~110 ppi and will look soft on a projector. Check `Image.open(...).size`.
|
|
61
|
+
2. One dominant image per slide. A slide with a hero and three thumbnails is two
|
|
62
|
+
slides.
|
|
63
|
+
3. Full-bleed backgrounds need text contrast handled deliberately — a dark
|
|
64
|
+
scrim rectangle between picture and text, not hope.
|
|
65
|
+
4. Alt text on every picture, in the language of the deck.
|
|
66
|
+
|
|
67
|
+
## Pitfalls this avoids
|
|
68
|
+
|
|
69
|
+
- Passing both width and height and getting a squashed photograph nobody can
|
|
70
|
+
name as the cause.
|
|
71
|
+
- `add_picture` after the text: the picture covers the words. Add backgrounds
|
|
72
|
+
first, or move them to index 2.
|
|
73
|
+
- Re-using a screenshot of a chart or a table as "evidence": it is unreadable at
|
|
74
|
+
projector size and unverifiable. Use a native chart or table.
|
|
75
|
+
|
|
76
|
+
## CLI fast path
|
|
77
|
+
|
|
78
|
+
`pptx create` accepts `{"image": {"path": "...", "left": 0.9, "top": 1.6,
|
|
79
|
+
"width": 8.0}}` and derives the height from the aspect ratio — no crop, no alt
|
|
80
|
+
text. Cropping, alt text and background placement are the recipe's job.
|
|
@@ -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.
|