python-pptx2 2.13.0__py3-none-any.whl
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.
- pptx2/__init__.py +152 -0
- pptx2/_color.py +75 -0
- pptx2/_slide_importer.py +597 -0
- pptx2/_svg.py +155 -0
- pptx2/_template_applier.py +292 -0
- pptx2/_textstyle.py +187 -0
- pptx2/accessibility.py +365 -0
- pptx2/action.py +270 -0
- pptx2/animation.py +2237 -0
- pptx2/api.py +49 -0
- pptx2/audit.py +258 -0
- pptx2/chart/__init__.py +0 -0
- pptx2/chart/analytics.py +381 -0
- pptx2/chart/axis.py +543 -0
- pptx2/chart/category.py +200 -0
- pptx2/chart/chart.py +670 -0
- pptx2/chart/data.py +864 -0
- pptx2/chart/datalabel.py +406 -0
- pptx2/chart/legend.py +86 -0
- pptx2/chart/marker.py +70 -0
- pptx2/chart/palettes.py +129 -0
- pptx2/chart/plot.py +462 -0
- pptx2/chart/point.py +101 -0
- pptx2/chart/quick_layouts.py +325 -0
- pptx2/chart/series.py +334 -0
- pptx2/chart/xlsx.py +272 -0
- pptx2/chart/xmlwriter.py +1845 -0
- pptx2/compose/__init__.py +28 -0
- pptx2/compose/from_spec.py +1094 -0
- pptx2/design/__init__.py +8 -0
- pptx2/design/components.py +607 -0
- pptx2/design/figures.py +389 -0
- pptx2/design/layout.py +370 -0
- pptx2/design/recipes.py +1967 -0
- pptx2/design/style.py +209 -0
- pptx2/design/tokens.py +915 -0
- pptx2/diagrams.py +754 -0
- pptx2/dml/__init__.py +0 -0
- pptx2/dml/chtfmt.py +40 -0
- pptx2/dml/color.py +496 -0
- pptx2/dml/effect.py +909 -0
- pptx2/dml/fill.py +691 -0
- pptx2/dml/line.py +287 -0
- pptx2/dml/picture.py +212 -0
- pptx2/dml/three_d.py +381 -0
- pptx2/enum/__init__.py +0 -0
- pptx2/enum/action.py +71 -0
- pptx2/enum/animation.py +31 -0
- pptx2/enum/base.py +218 -0
- pptx2/enum/chart.py +574 -0
- pptx2/enum/dml.py +740 -0
- pptx2/enum/lang.py +685 -0
- pptx2/enum/presentation.py +133 -0
- pptx2/enum/shapes.py +1029 -0
- pptx2/enum/text.py +230 -0
- pptx2/exc.py +42 -0
- pptx2/formats.py +139 -0
- pptx2/geometry.py +420 -0
- pptx2/inherit.py +109 -0
- pptx2/lint.py +2256 -0
- pptx2/math.py +177 -0
- pptx2/media.py +197 -0
- pptx2/opc/__init__.py +0 -0
- pptx2/opc/constants.py +332 -0
- pptx2/opc/oxml.py +188 -0
- pptx2/opc/package.py +762 -0
- pptx2/opc/packuri.py +109 -0
- pptx2/opc/serialized.py +296 -0
- pptx2/opc/shared.py +20 -0
- pptx2/opc/spec.py +45 -0
- pptx2/oxml/__init__.py +555 -0
- pptx2/oxml/action.py +53 -0
- pptx2/oxml/chart/__init__.py +0 -0
- pptx2/oxml/chart/axis.py +337 -0
- pptx2/oxml/chart/chart.py +481 -0
- pptx2/oxml/chart/datalabel.py +253 -0
- pptx2/oxml/chart/legend.py +72 -0
- pptx2/oxml/chart/marker.py +61 -0
- pptx2/oxml/chart/plot.py +365 -0
- pptx2/oxml/chart/series.py +425 -0
- pptx2/oxml/chart/shared.py +220 -0
- pptx2/oxml/coreprops.py +288 -0
- pptx2/oxml/dml/__init__.py +0 -0
- pptx2/oxml/dml/color.py +135 -0
- pptx2/oxml/dml/effect.py +213 -0
- pptx2/oxml/dml/fill.py +316 -0
- pptx2/oxml/dml/line.py +12 -0
- pptx2/oxml/dml/three_d.py +110 -0
- pptx2/oxml/ns.py +135 -0
- pptx2/oxml/presentation.py +313 -0
- pptx2/oxml/shapes/__init__.py +19 -0
- pptx2/oxml/shapes/autoshape.py +467 -0
- pptx2/oxml/shapes/connector.py +107 -0
- pptx2/oxml/shapes/graphfrm.py +347 -0
- pptx2/oxml/shapes/groupshape.py +329 -0
- pptx2/oxml/shapes/picture.py +270 -0
- pptx2/oxml/shapes/shared.py +577 -0
- pptx2/oxml/simpletypes.py +1027 -0
- pptx2/oxml/slide.py +563 -0
- pptx2/oxml/table.py +650 -0
- pptx2/oxml/text.py +815 -0
- pptx2/oxml/theme.py +36 -0
- pptx2/oxml/xmlchemy.py +717 -0
- pptx2/package.py +222 -0
- pptx2/parts/__init__.py +0 -0
- pptx2/parts/chart.py +95 -0
- pptx2/parts/coreprops.py +167 -0
- pptx2/parts/diagram.py +37 -0
- pptx2/parts/embeddedpackage.py +93 -0
- pptx2/parts/image.py +275 -0
- pptx2/parts/media.py +37 -0
- pptx2/parts/presentation.py +136 -0
- pptx2/parts/slide.py +371 -0
- pptx2/presentation.py +408 -0
- pptx2/py.typed +0 -0
- pptx2/render.py +586 -0
- pptx2/section.py +272 -0
- pptx2/shapes/__init__.py +26 -0
- pptx2/shapes/autoshape.py +442 -0
- pptx2/shapes/base.py +1078 -0
- pptx2/shapes/connector.py +297 -0
- pptx2/shapes/freeform.py +337 -0
- pptx2/shapes/graphfrm.py +316 -0
- pptx2/shapes/group.py +264 -0
- pptx2/shapes/picture.py +422 -0
- pptx2/shapes/placeholder.py +468 -0
- pptx2/shapes/shapetree.py +2027 -0
- pptx2/shared.py +82 -0
- pptx2/skill/SKILL.md +450 -0
- pptx2/skill/__init__.py +78 -0
- pptx2/skill/__main__.py +64 -0
- pptx2/skill/references/animations.md +189 -0
- pptx2/skill/references/basics.md +421 -0
- pptx2/skill/references/charts.md +254 -0
- pptx2/skill/references/compose.md +234 -0
- pptx2/skill/references/design.md +366 -0
- pptx2/skill/references/effects.md +249 -0
- pptx2/skill/references/end-to-end-deck.md +231 -0
- pptx2/skill/references/geometry-and-arrows.md +334 -0
- pptx2/skill/references/lint.md +275 -0
- pptx2/skill/references/math.md +86 -0
- pptx2/skill/references/picture-effects.md +129 -0
- pptx2/skill/references/render.md +151 -0
- pptx2/skill/references/smart-art.md +75 -0
- pptx2/skill/references/space-aware-authoring.md +249 -0
- pptx2/skill/references/tables.md +244 -0
- pptx2/skill/references/theme.md +127 -0
- pptx2/skill/references/three-d.md +109 -0
- pptx2/skill/references/transitions.md +100 -0
- pptx2/slide.py +1244 -0
- pptx2/smart_art.py +220 -0
- pptx2/spec.py +633 -0
- pptx2/table.py +1181 -0
- pptx2/table_styles.py +184 -0
- pptx2/templates/default.pptx +0 -0
- pptx2/templates/docx-icon.emf +0 -0
- pptx2/templates/generic-icon.emf +0 -0
- pptx2/templates/notes.xml +23 -0
- pptx2/templates/notesMaster.xml +352 -0
- pptx2/templates/pptx-icon.emf +0 -0
- pptx2/templates/theme.xml +321 -0
- pptx2/templates/xlsx-icon.emf +0 -0
- pptx2/text/__init__.py +0 -0
- pptx2/text/fonts.py +482 -0
- pptx2/text/layout.py +374 -0
- pptx2/text/text.py +1272 -0
- pptx2/theme.py +721 -0
- pptx2/types.py +36 -0
- pptx2/util.py +263 -0
- python_pptx2-2.13.0.dist-info/METADATA +351 -0
- python_pptx2-2.13.0.dist-info/RECORD +175 -0
- python_pptx2-2.13.0.dist-info/WHEEL +5 -0
- python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
- python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
- python_pptx2-2.13.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# End-to-end: a complete branded deck
|
|
2
|
+
|
|
3
|
+
A worked example that exercises most of the post-fork features in one
|
|
4
|
+
script: design tokens, recipes, transitions, animations, a chart with
|
|
5
|
+
a custom palette, a layout pass through the linter, and an optional
|
|
6
|
+
thumbnail render.
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
"""
|
|
10
|
+
Build a branded Q4 review deck.
|
|
11
|
+
|
|
12
|
+
Demonstrates: DesignTokens, recipes, deck-wide transitions,
|
|
13
|
+
sequenced animations, chart palette, lint-on-save, thumbnails.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from pptx2 import Presentation
|
|
20
|
+
from pptx2.animation import Emphasis, Entrance, Trigger
|
|
21
|
+
from pptx2.chart.data import CategoryChartData
|
|
22
|
+
from pptx2.design.recipes import (
|
|
23
|
+
bullet_slide,
|
|
24
|
+
image_hero_slide,
|
|
25
|
+
kpi_slide,
|
|
26
|
+
quote_slide,
|
|
27
|
+
title_slide,
|
|
28
|
+
)
|
|
29
|
+
from pptx2.design.tokens import DesignTokens
|
|
30
|
+
from pptx2.dml.color import RGBColor
|
|
31
|
+
from pptx2.enum.presentation import MSO_TRANSITION_TYPE
|
|
32
|
+
from pptx2.enum.chart import XL_CHART_TYPE
|
|
33
|
+
from pptx2.util import Inches, Pt
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---- Tokens ------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
TOKENS = DesignTokens.from_dict(
|
|
39
|
+
{
|
|
40
|
+
"palette": {
|
|
41
|
+
"primary": "#4F9DFF",
|
|
42
|
+
"neutral": "#1F2937",
|
|
43
|
+
"background": "#FFFFFF",
|
|
44
|
+
"positive": "#10B981",
|
|
45
|
+
"negative": "#EF4444",
|
|
46
|
+
"on_primary": "#FFFFFF",
|
|
47
|
+
},
|
|
48
|
+
"typography": {
|
|
49
|
+
# NB: recipes look up the keys "heading" and "body". Bare
|
|
50
|
+
# floats are treated as POINTS; bare ints are EMU.
|
|
51
|
+
"heading": {"family": "Inter", "size": 44.0, "bold": True},
|
|
52
|
+
"body": {"family": "Inter", "size": 18.0},
|
|
53
|
+
},
|
|
54
|
+
"shadows": {
|
|
55
|
+
"card": {"blur": 18.0, "distance": 4.0, "alpha": 0.18},
|
|
56
|
+
},
|
|
57
|
+
"radii": {"card": 12.0},
|
|
58
|
+
"spacings": {"sm": 8.0, "md": 16.0, "lg": 32.0},
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---- Build the deck ----------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def build(out_path: str | Path) -> Presentation:
|
|
66
|
+
prs = Presentation()
|
|
67
|
+
prs.slide_width = Inches(13.333)
|
|
68
|
+
prs.slide_height = Inches(7.5)
|
|
69
|
+
|
|
70
|
+
# Cover
|
|
71
|
+
title_slide(
|
|
72
|
+
prs,
|
|
73
|
+
title="Q4 2026 Review",
|
|
74
|
+
subtitle="April 2026",
|
|
75
|
+
tokens=TOKENS,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# KPIs
|
|
79
|
+
kpi_slide(
|
|
80
|
+
prs,
|
|
81
|
+
title="Run-rate metrics",
|
|
82
|
+
kpis=[
|
|
83
|
+
{"label": "ARR", "value": "$182M", "delta": +0.27},
|
|
84
|
+
{"label": "NDR", "value": "131%", "delta": +0.03},
|
|
85
|
+
{"label": "CAC payback", "value": "8 mo", "delta": -0.10},
|
|
86
|
+
],
|
|
87
|
+
tokens=TOKENS,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Bullets — annotated with a sequenced paragraph reveal.
|
|
91
|
+
# bullet_slide adds the title textbox first and the body textbox
|
|
92
|
+
# second, so shapes[1] is reliably the body.
|
|
93
|
+
bs = bullet_slide(
|
|
94
|
+
prs,
|
|
95
|
+
title="Customer impact",
|
|
96
|
+
bullets=[
|
|
97
|
+
"Two flagship customers shipped this week.",
|
|
98
|
+
"NPS improved 8 points QoQ.",
|
|
99
|
+
"EU expansion ahead of plan.",
|
|
100
|
+
],
|
|
101
|
+
tokens=TOKENS,
|
|
102
|
+
)
|
|
103
|
+
body_tf = bs.shapes[1].text_frame
|
|
104
|
+
Entrance.fade(bs, body_tf, by_paragraph=True)
|
|
105
|
+
|
|
106
|
+
# Custom chart slide — chart palette + quick layout
|
|
107
|
+
cs = prs.slides.add_slide(prs.slide_layouts[5])
|
|
108
|
+
cs.shapes.title.text = "ARR by segment ($M)"
|
|
109
|
+
data = CategoryChartData()
|
|
110
|
+
data.categories = ["Enterprise", "Mid-market", "SMB", "Self-serve"]
|
|
111
|
+
data.add_series("FY25", (62, 41, 18, 9))
|
|
112
|
+
data.add_series("FY26", (94, 55, 23, 10))
|
|
113
|
+
chart_shape = cs.shapes.add_chart(
|
|
114
|
+
XL_CHART_TYPE.BAR_CLUSTERED,
|
|
115
|
+
Inches(1), Inches(1.8), Inches(11), Inches(5.0),
|
|
116
|
+
data,
|
|
117
|
+
)
|
|
118
|
+
chart = chart_shape.chart
|
|
119
|
+
chart.apply_palette("modern")
|
|
120
|
+
chart.apply_quick_layout("title_axes_legend_bottom")
|
|
121
|
+
chart.chart_title.text_frame.text = "ARR by segment ($M)"
|
|
122
|
+
|
|
123
|
+
# Quote
|
|
124
|
+
# Recipes use the Blank layout, so slide.shapes.title is None;
|
|
125
|
+
# quote_slide adds the quote textbox first (shapes[0]).
|
|
126
|
+
qs = quote_slide(
|
|
127
|
+
prs,
|
|
128
|
+
quote="The new dashboards saved my team a week per sprint.",
|
|
129
|
+
attribution="Director of Eng, Flagship Customer",
|
|
130
|
+
tokens=TOKENS,
|
|
131
|
+
)
|
|
132
|
+
Emphasis.pulse(qs, qs.shapes[0], trigger=Trigger.AFTER_PREVIOUS)
|
|
133
|
+
|
|
134
|
+
# Hero closer (supply your own image path)
|
|
135
|
+
image_hero_slide(
|
|
136
|
+
prs,
|
|
137
|
+
title="Thank you",
|
|
138
|
+
image="assets/closer.jpg",
|
|
139
|
+
tokens=TOKENS,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Deck-wide fade transition, then upgrade the cover to Morph
|
|
143
|
+
prs.set_transition(kind=MSO_TRANSITION_TYPE.FADE, duration=400)
|
|
144
|
+
prs.slides[0].transition.kind = MSO_TRANSITION_TYPE.MORPH
|
|
145
|
+
prs.slides[0].transition.duration = 1500
|
|
146
|
+
|
|
147
|
+
# Space-aware safety net: lint every slide, auto-fix off-slide
|
|
148
|
+
# shapes, and bail loudly if anything is still error-severity.
|
|
149
|
+
_lint_or_die(prs)
|
|
150
|
+
|
|
151
|
+
prs.save(out_path)
|
|
152
|
+
return prs
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _lint_or_die(prs: Presentation) -> None:
|
|
156
|
+
from pptx2.exc import LintError
|
|
157
|
+
|
|
158
|
+
# Pass 1: nudge off-slide shapes inside the slide bounds
|
|
159
|
+
for slide in prs.slides:
|
|
160
|
+
slide.lint().auto_fix()
|
|
161
|
+
|
|
162
|
+
# Pass 2: collect anything still failing
|
|
163
|
+
errors = []
|
|
164
|
+
for i, slide in enumerate(prs.slides):
|
|
165
|
+
for issue in slide.lint().issues:
|
|
166
|
+
if issue.severity.value == "error":
|
|
167
|
+
errors.append(f"slide {i + 1}: {issue}")
|
|
168
|
+
|
|
169
|
+
if errors:
|
|
170
|
+
raise LintError("\n".join(errors))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---- Optional: rasterise thumbnails ------------------------------------------
|
|
174
|
+
|
|
175
|
+
def render_thumbnails(prs: Presentation, out_dir: str | Path) -> list[Path]:
|
|
176
|
+
"""Best-effort thumbnail render. Skips gracefully if soffice is missing."""
|
|
177
|
+
from pptx2.render import ThumbnailRendererUnavailable
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
return prs.render_thumbnails(out_dir=out_dir)
|
|
181
|
+
except ThumbnailRendererUnavailable as exc:
|
|
182
|
+
print(f"thumbnail render skipped: {exc}")
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
deck = build("q4-review.pptx")
|
|
188
|
+
render_thumbnails(deck, "thumbs")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## What this exercises
|
|
192
|
+
|
|
193
|
+
- **Phase 9** — `DesignTokens.from_dict`, four recipes (`title_slide`,
|
|
194
|
+
`kpi_slide`, `bullet_slide`, `quote_slide`, `image_hero_slide`),
|
|
195
|
+
shape-style fan-out via `tokens=`.
|
|
196
|
+
- **Phase 5** — `Entrance.fade(..., by_paragraph=True)` for a reveal,
|
|
197
|
+
`Emphasis.pulse(..., trigger=Trigger.AFTER_PREVIOUS)` chained off
|
|
198
|
+
the previous click.
|
|
199
|
+
- **Phase 4** — `Slide.transition` for the per-slide Morph,
|
|
200
|
+
`Presentation.set_transition` for the deck-wide fade.
|
|
201
|
+
- **Phase 10** — `Chart.apply_palette("modern")` and
|
|
202
|
+
`Chart.apply_quick_layout("title_axes_legend_bottom")`.
|
|
203
|
+
- **Phase 2** — `_lint_or_die(...)` as a generation safety net:
|
|
204
|
+
`slide.lint().auto_fix()` on every slide to nudge off-slide shapes
|
|
205
|
+
back inside, then a second pass that raises `LintError` on any
|
|
206
|
+
remaining error-severity issue (text overflow, residual off-slide,
|
|
207
|
+
etc).
|
|
208
|
+
- **Phase 10** — optional `render_thumbnails(...)` for downstream
|
|
209
|
+
tooling, with graceful fall-through when LibreOffice isn't
|
|
210
|
+
installed.
|
|
211
|
+
|
|
212
|
+
## Adapting for production
|
|
213
|
+
|
|
214
|
+
- Persist `TOKENS` separately (YAML or `.pptx`) and load with
|
|
215
|
+
`DesignTokens.from_yaml(...)` / `DesignTokens.from_pptx(...)`. That
|
|
216
|
+
way design and code evolve independently.
|
|
217
|
+
- In production, use `slide.tidy()` to repair what can be fixed
|
|
218
|
+
automatically — it clamps `OffSlide` shapes back inside the bounds,
|
|
219
|
+
autofits overflowing text frames, and restacks contradicted
|
|
220
|
+
`layer_above` declarations — then decide what to do with the residual
|
|
221
|
+
issues: log warning-severity ones but ship the deck, raise on
|
|
222
|
+
error-severity ones. Keep the stricter "raise on any error" behaviour
|
|
223
|
+
in CI.
|
|
224
|
+
- For the save-time gate, set `prs.lint_on_save = "raise"` and just call
|
|
225
|
+
`prs.save(...)`: every slide is linted before anything is written, so
|
|
226
|
+
a failing deck never reaches disk. `"warn"` logs instead. The
|
|
227
|
+
equivalent for spec-built decks is
|
|
228
|
+
`pptx2.compose.from_spec(..., lint="raise")`.
|
|
229
|
+
- If you want the chart palette to align with brand tokens rather than
|
|
230
|
+
the built-in `"modern"`, pass an explicit list:
|
|
231
|
+
`chart.apply_palette([TOKENS.palette["primary"], ...])`.
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# Geometry, text, arrows, and diagrams (v2.8+)
|
|
2
|
+
|
|
3
|
+
This reference covers the high-level helpers added in v2.8 for building
|
|
4
|
+
slides programmatically without writing EMU arithmetic or XML for
|
|
5
|
+
arrowheads. Everything here is built on top of the inherited 1.0.2 API
|
|
6
|
+
— the underlying ``add_shape`` / ``add_connector`` / ``text_frame``
|
|
7
|
+
surface still works exactly as it always did.
|
|
8
|
+
|
|
9
|
+
**Reach for these helpers** when you're generating decks dynamically
|
|
10
|
+
(LLM, JSON spec, DB rows). Reach for the lower-level APIs only when
|
|
11
|
+
the helpers don't cover your case.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## The `BBox` value object
|
|
16
|
+
|
|
17
|
+
Every rectangular region on a slide can be expressed as a
|
|
18
|
+
:class:`BBox` (importable from the package root):
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from pptx2 import BBox
|
|
22
|
+
from pptx2.util import Inches
|
|
23
|
+
|
|
24
|
+
bb = BBox.from_inches(1, 2, 8, 4) # left, top, width, height
|
|
25
|
+
bb.right # Emu(9 inches)
|
|
26
|
+
bb.cx, bb.cy # centre
|
|
27
|
+
bb.area # int (EMU²)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`BBox` is immutable, frozen-dataclass, and unpacks to `(left, top,
|
|
31
|
+
width, height)` — so it splats straight into add_shape and friends:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, *bb)
|
|
35
|
+
slide.shapes.add_textbox(*bb.inset(all=Inches(0.2)))
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Constructors
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
BBox.from_inches(1, 2, 4, 3)
|
|
42
|
+
BBox.from_emu(914400, 1828800, 3657600, 2743200)
|
|
43
|
+
BBox.from_shape(some_shape) # snapshot
|
|
44
|
+
BBox.from_slide(slide) # full slide
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`shape.bbox` is the same as `BBox.from_shape(shape)` — a snapshot of
|
|
48
|
+
the shape's current geometry. Mutating the shape afterwards does not
|
|
49
|
+
update the box. `bb.apply_to(shape)` pushes a box back onto a shape.
|
|
50
|
+
|
|
51
|
+
### Transforms
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
bb.shifted(dx=Inches(1)) # translate
|
|
55
|
+
bb.resized(width=Inches(6)) # change one dimension
|
|
56
|
+
bb.inset(all=Inches(0.2)) # shrink uniformly
|
|
57
|
+
bb.inset(x=Inches(0.5), y=Inches(0.2)) # per-axis
|
|
58
|
+
bb.inset(left=Inches(0.1), right=Inches(0.5), top=Inches(0.2))
|
|
59
|
+
bb.sub(0.25, 0, 0.5, 1.0) # normalised sub-box
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`inset()` with no args is a no-op; negative values expand outward.
|
|
63
|
+
|
|
64
|
+
### Splits
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
bb.columns(3, gap=Pt(16)) # n equal columns ← the n-up case
|
|
68
|
+
bb.rows(2, gap=Pt(12)) # n equal rows
|
|
69
|
+
bb.split_h([1, 1]) # two equal columns
|
|
70
|
+
bb.split_h([2, 1]) # 66%/33% — unequal, so ratios
|
|
71
|
+
bb.split_h([1, 1, 1], gap=Inches(0.1))
|
|
72
|
+
bb.split_v([1, 2]) # vertical
|
|
73
|
+
bb.grid(3, 2, gap_x=Inches(0.1)) # row-major 3x2 cells
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Never hand-compute `col_w = (avail - (n - 1) * gap) / n`.** That
|
|
77
|
+
arithmetic is what `columns` / `rows` / `grid` are for, and they
|
|
78
|
+
apportion widths so the cells partition the box *exactly* — no rounding
|
|
79
|
+
drift accumulating into the last column:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
row = BBox.from_inches(0.75, 2.4, 11.8, 2.2)
|
|
83
|
+
for box, item in zip(row.columns(3, gap=Pt(16)), items):
|
|
84
|
+
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, *box)
|
|
85
|
+
card.shadow.clear()
|
|
86
|
+
card.corner_radius = Pt(6)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
For a 5-column × 2-row panel, `row.grid(5, 2, gap_x=Pt(12), gap_y=Pt(12))`
|
|
90
|
+
returns the ten cells row-major — or use
|
|
91
|
+
`Grid.from_box(row, cols=5, rows=2, gutter=Pt(12))` when you want
|
|
92
|
+
span-aware placement (see `design.md`).
|
|
93
|
+
|
|
94
|
+
### Predicates
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
a.contains(b) # b fully inside a
|
|
98
|
+
a.intersects(b)
|
|
99
|
+
a.intersection(b) # overlap region (BBox)
|
|
100
|
+
a.union(b) # smallest enclosing
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## One-call text — `slide.shapes.add_text`
|
|
106
|
+
|
|
107
|
+
The historical pattern (add_textbox + tf.word_wrap + tf.text + paragraph
|
|
108
|
+
alignment + run font.name/size/bold/color) collapses to one call:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
slide.shapes.add_text(
|
|
112
|
+
BBox.from_inches(1, 1, 8, 1),
|
|
113
|
+
text="Q4 revenue overview",
|
|
114
|
+
font="Inter",
|
|
115
|
+
size_pt=24,
|
|
116
|
+
bold=True,
|
|
117
|
+
color="#0B5CFF", # hex, RGBColor, or (r,g,b) all OK
|
|
118
|
+
align="center", # str shortcut; no enum import
|
|
119
|
+
anchor="middle", # vertical anchor
|
|
120
|
+
margin_pt=4,
|
|
121
|
+
word_wrap=True,
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Positional length form also works for back-compat:
|
|
126
|
+
`add_text(Inches(1), Inches(2), Inches(8), Inches(1), text="…")`.
|
|
127
|
+
|
|
128
|
+
Returns the textbox shape so further mutation is possible.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Hex-string colour shortcuts
|
|
133
|
+
|
|
134
|
+
`shape.fill_hex(hex)` and `shape.line_hex(hex, weight_pt=)` replace the
|
|
135
|
+
three-line `fill.solid(); fore_color.rgb = RGBColor(...)` ritual. Both
|
|
136
|
+
return `self` for chaining:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, *bb) \
|
|
140
|
+
.fill_hex("#0B5CFF") \
|
|
141
|
+
.line_hex("#0D0D0D", weight_pt=1.25)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`hex_color=None` clears the fill (transparent background).
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Format-preserving text replacement
|
|
149
|
+
|
|
150
|
+
`shape.set_text_preserving_format(new_text)` is what you want for
|
|
151
|
+
templated placeholders (e.g. `"<TITLE>"`) — it captures the first
|
|
152
|
+
run's font face / size / colour / bold / italic, rebuilds the text,
|
|
153
|
+
and re-applies the formatting to every new run:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
title_shape.set_text_preserving_format("Q4 revenue overview")
|
|
157
|
+
# Font, size, colour, bold all preserved verbatim.
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Multi-line works (`"line one\nline two"`); every paragraph inherits
|
|
161
|
+
the template formatting.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Arrows that actually have arrowheads
|
|
166
|
+
|
|
167
|
+
`add_connector(MSO_CONNECTOR.STRAIGHT, x1, y1, x2, y2)` returns a line
|
|
168
|
+
with no arrowhead. **Use `add_arrow` instead**:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
slide.shapes.add_arrow(
|
|
172
|
+
start=box_a, # Shape, BBox, or (x, y)
|
|
173
|
+
end=box_b,
|
|
174
|
+
head="triangle", # "triangle"|"arrow"|"stealth"|"diamond"|"oval"|"none"
|
|
175
|
+
tail=None,
|
|
176
|
+
color="#0B5CFF",
|
|
177
|
+
weight_pt=1.5,
|
|
178
|
+
style="solid", # "solid"|"dashed"|"dotted"
|
|
179
|
+
inset_pt=6.0, # pull endpoints back from shape edges
|
|
180
|
+
end_side="auto", # "top"|"right"|"bottom"|"left"|"auto"
|
|
181
|
+
route="straight", # "straight"|"elbow"|"curved"
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
When `start` / `end` is a Shape or BBox, the arrow auto-routes to the
|
|
186
|
+
mid-edge nearest the opposite endpoint. `inset_pt` is the small pullback
|
|
187
|
+
applied so the arrowhead triangle doesn't bleed into the target box.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Picture replacement and container detection
|
|
192
|
+
|
|
193
|
+
For a picture that's broken or sub-quality:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
def diagram(slide, bbox):
|
|
197
|
+
left, right = bbox.split_h([1, 1], gap=Inches(0.1))
|
|
198
|
+
slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, *left).fill_hex("#0B5CFF")
|
|
199
|
+
slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, *right).fill_hex("#FFFFFF")
|
|
200
|
+
|
|
201
|
+
picture.replace_with(diagram, padding=Inches(0.1))
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The picture is deleted; the builder is called with a `BBox` sized to
|
|
205
|
+
the picture's old footprint (minus optional padding). The builder
|
|
206
|
+
draws native shapes in the freed area.
|
|
207
|
+
|
|
208
|
+
When the picture sits inside a "card" rectangle plus a heading,
|
|
209
|
+
`picture.enclosing_container()` returns the bbox of the enclosing card
|
|
210
|
+
(trimmed to avoid sibling text). This is the bbox you actually want
|
|
211
|
+
to redraw into, not the picture's own bbox:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
container_bbox = picture.enclosing_container(exclude_text=True)
|
|
215
|
+
picture.replace_with(builder, padding=Inches(0.05))
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Slide-level helpers
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
slide.slide_bbox() # BBox of the full slide
|
|
224
|
+
slide.content_bbox() # BBox of all non-decorative shapes
|
|
225
|
+
slide.find_empty_region(min_width=Inches(2), min_height=Inches(1))
|
|
226
|
+
slide.tidy() # lint + auto_fix(safe subset)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`tidy()` is the one-call cleanup before save: it lints, runs the safe
|
|
230
|
+
subset of auto-fixes (OffSlide clamp, TextOverflow flip), and returns
|
|
231
|
+
the list of fixes applied.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Diagram recipes — `pptx2.diagrams`
|
|
236
|
+
|
|
237
|
+
Six built-in diagram patterns covering ~80% of architecture-deck
|
|
238
|
+
content. Each takes a slide, a `BBox`, and a small content spec:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
from pptx2.diagrams import (
|
|
242
|
+
horizontal_pipeline, vertical_pipeline,
|
|
243
|
+
hub_and_spoke, cycle, decision_tree,
|
|
244
|
+
comparison_columns,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Pipeline
|
|
248
|
+
horizontal_pipeline(
|
|
249
|
+
slide, bbox,
|
|
250
|
+
steps=["Extract", "Classify", "Enrich", "Output"],
|
|
251
|
+
accent="#0B5CFF",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Hub-and-spoke
|
|
255
|
+
hub_and_spoke(
|
|
256
|
+
slide, bbox,
|
|
257
|
+
centre="Agent",
|
|
258
|
+
spokes=["Memory", "Tools", "Planning", "Perception"],
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Cyclic loop
|
|
262
|
+
cycle(
|
|
263
|
+
slide, bbox,
|
|
264
|
+
steps=["Observe", "Orient", "Decide", "Act"],
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Decision tree (one level of children optional)
|
|
268
|
+
decision_tree(
|
|
269
|
+
slide, bbox,
|
|
270
|
+
root="Is the deck dynamic?",
|
|
271
|
+
branches=[
|
|
272
|
+
{"label": "Yes", "children": ["Use lint()", "Use fit_text"]},
|
|
273
|
+
{"label": "No", "children": ["Author manually"]},
|
|
274
|
+
],
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# N-column comparison
|
|
278
|
+
comparison_columns(
|
|
279
|
+
slide, bbox,
|
|
280
|
+
columns=[
|
|
281
|
+
{"title": "Pros", "body": ["Fast", "Cheap", "Composable"]},
|
|
282
|
+
{"title": "Cons", "body": ["New API"]},
|
|
283
|
+
],
|
|
284
|
+
)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Each recipe returns a small dataclass (`PipelineResult`, `HubAndSpokeResult`,
|
|
288
|
+
…) exposing the underlying shapes for further tweaks.
|
|
289
|
+
|
|
290
|
+
Step / column dicts accept per-item `fill` and `text_color` overrides:
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
horizontal_pipeline(
|
|
294
|
+
slide, bbox,
|
|
295
|
+
steps=[
|
|
296
|
+
{"label": "Cleaned", "fill": "#E8F0FF", "text_color": "#0B3D9C"},
|
|
297
|
+
{"label": "Features", "fill": "#FFFFFF"},
|
|
298
|
+
],
|
|
299
|
+
)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## End-to-end audit — `pptx2.audit`
|
|
305
|
+
|
|
306
|
+
For agents producing a full deck, `audit(prs)` returns a structured
|
|
307
|
+
"what I shipped" summary:
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
from pptx2 import audit
|
|
311
|
+
|
|
312
|
+
report = audit(prs)
|
|
313
|
+
print(report.markdown())
|
|
314
|
+
|
|
315
|
+
# Or programmatically:
|
|
316
|
+
report.has_errors # bool
|
|
317
|
+
report.lint_issues # [(slide_idx, LintIssue), ...]
|
|
318
|
+
report.broken_pictures # [(idx, picture), ...]
|
|
319
|
+
report.empty_slides # [idx, ...]
|
|
320
|
+
report.font_warnings # [(idx, font), ...]
|
|
321
|
+
report.size_warnings # [(idx, name, bytes), ...]
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The audit is read-only — it never mutates the deck. The markdown
|
|
325
|
+
output is structured for chat replies.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Render helpers
|
|
330
|
+
|
|
331
|
+
`render_slides(prs, slides=[0, 1, 2], out_dir="thumbs",
|
|
332
|
+
name_template="slide-{:02d}.png")` is the friendlier wrapper around
|
|
333
|
+
`render_slide_thumbnails` — same engine, but with sensible argument
|
|
334
|
+
naming and a `name_template` for clean file names.
|