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,366 @@
|
|
|
1
|
+
# Design system layer (Phase 9)
|
|
2
|
+
|
|
3
|
+
The `pptx2.design` package turns the low-level API into something where
|
|
4
|
+
the *default* output looks good. Nothing here adds new XML — it's all
|
|
5
|
+
built on top of the foundations from earlier phases.
|
|
6
|
+
|
|
7
|
+
## Design tokens
|
|
8
|
+
|
|
9
|
+
`DesignTokens` is a source-agnostic container for brand tokens:
|
|
10
|
+
palette, typography, radii, shadows, spacings.
|
|
11
|
+
|
|
12
|
+
> **Reads return rich objects, not strings.** `tokens.palette[k]` returns
|
|
13
|
+
> an `RGBColor`, `tokens.typography[k]` returns a `TypographyToken`,
|
|
14
|
+
> `tokens.radii[k]` / `tokens.spacings[k]` return `Length`. Every public
|
|
15
|
+
> setter accepts the rich form, so usually you just pass the lookup
|
|
16
|
+
> through — no `RGBColor.from_hex(...)` round-trip required:
|
|
17
|
+
>
|
|
18
|
+
> ```python
|
|
19
|
+
> shape.fill.fore_color.rgb = tokens.palette["primary"] # ✓ pass RGBColor
|
|
20
|
+
> shape.fill.fore_color.rgb = "#4F9DFF" # ✓ hex string
|
|
21
|
+
> # Don't:
|
|
22
|
+
> shape.fill.fore_color.rgb = RGBColor.from_hex(tokens.palette["primary"])
|
|
23
|
+
> # 'RGBColor' object has no attribute 'startswith'
|
|
24
|
+
> ```
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from pptx2.design.tokens import DesignTokens
|
|
28
|
+
|
|
29
|
+
tokens = DesignTokens.from_dict({
|
|
30
|
+
"palette": {
|
|
31
|
+
"primary": "#4F9DFF",
|
|
32
|
+
"neutral": "#1F2937",
|
|
33
|
+
"background": "#FFFFFF",
|
|
34
|
+
"positive": "#10B981",
|
|
35
|
+
"negative": "#EF4444",
|
|
36
|
+
"on_primary": "#FFFFFF",
|
|
37
|
+
},
|
|
38
|
+
"typography": {
|
|
39
|
+
# Recipes look up the keys "heading" and "body". Other keys are
|
|
40
|
+
# available for your own use. Bare floats are treated as POINTS;
|
|
41
|
+
# bare ints are EMU. Use floats unless you know what you're doing.
|
|
42
|
+
"heading": {"family": "Inter", "size": 44.0, "bold": True},
|
|
43
|
+
"body": {"family": "Inter", "size": 18.0},
|
|
44
|
+
"caption": {"family": "Inter", "size": 12.0, "italic": True},
|
|
45
|
+
},
|
|
46
|
+
"shadows": {
|
|
47
|
+
# 'blur' / 'distance' are bare-float points too.
|
|
48
|
+
"card": {"blur": 18.0, "distance": 4.0, "alpha": 0.18},
|
|
49
|
+
},
|
|
50
|
+
"radii": {"card": 12.0, "button": 6.0},
|
|
51
|
+
"spacings": {"sm": 8.0, "md": 16.0, "lg": 32.0},
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Other constructors
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# Optional pyyaml dependency
|
|
59
|
+
tokens = DesignTokens.from_yaml("brand.yml")
|
|
60
|
+
|
|
61
|
+
# Extracts the six accent slots, dk1/dk2/lt1/lt2, hyperlink slots, and
|
|
62
|
+
# major/minor fonts from a deck or template
|
|
63
|
+
tokens = DesignTokens.from_pptx("template.pptx")
|
|
64
|
+
|
|
65
|
+
# Layer brand-spec overrides on top of a template-extracted base
|
|
66
|
+
tokens = DesignTokens.from_pptx("template.pptx").merge(
|
|
67
|
+
DesignTokens.from_dict({"palette": {"accent": "#FF6600"}})
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Per-slide appearance overrides
|
|
72
|
+
|
|
73
|
+
A slide can depart from its master without you editing the master:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
slide.color_variant = "dark" # swap bg/tx against the same theme
|
|
77
|
+
slide.color_variant = "light" # the master's default mapping
|
|
78
|
+
slide.color_variant = None # drop the override entirely
|
|
79
|
+
|
|
80
|
+
# Backgrounds: give the slide its own and inheritance breaks itself.
|
|
81
|
+
slide.background.fill.solid()
|
|
82
|
+
slide.background.fill.fore_color.rgb = "#102030"
|
|
83
|
+
slide.follow_master_background # now False (read-only)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`color_variant` takes `"light"` / `"dark"` / `None` — **not** an index.
|
|
87
|
+
`"dark"` swaps backgrounds and text (`bg1=dk1`, `tx1=lt1`, …) so one
|
|
88
|
+
slide reads dark without touching the deck theme. Reading it back
|
|
89
|
+
returns `None` when the slide carries a custom mapping matching neither
|
|
90
|
+
named variant. For any other mapping use `set_clr_map_override(...)`.
|
|
91
|
+
|
|
92
|
+
`slide.design_group("kpi-card")` is a context manager that tags every
|
|
93
|
+
shape created inside it with the same `lint_group`, so a cluster of
|
|
94
|
+
deliberately-overlapping shapes is declared once rather than per shape:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
with slide.design_group("kpi-card-1"):
|
|
98
|
+
card = slide.shapes.add_shape(...)
|
|
99
|
+
label = slide.shapes.add_textbox(...) # both tagged "kpi-card-1"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
There are two adjacent spellings — `slide.shapes.lint_group_scope(name=...)`
|
|
103
|
+
is the same idea on the shape tree, and `slide.lint_group_overlaps(*shapes)`
|
|
104
|
+
tags shapes you already made. Reach for `design_group` while building,
|
|
105
|
+
`lint_group_overlaps` after the fact.
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
## Token-resolving shape style
|
|
109
|
+
|
|
110
|
+
Every shape exposes a `ShapeStyle` facade. Setters fan assignments out
|
|
111
|
+
to the low-level proxies:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
shape.style.fill = tokens.palette["primary"]
|
|
115
|
+
shape.style.line = tokens.palette["primary"]
|
|
116
|
+
shape.style.shadow = tokens.shadows["card"]
|
|
117
|
+
shape.style.text_color = tokens.palette["on_primary"]
|
|
118
|
+
shape.style.font = tokens.typography["body"]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Partial `ShadowToken` assignments leave unset fields untouched, so
|
|
122
|
+
overrides are non-destructive. To clear an effect entirely:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
shape.style.shadow = None
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Layout primitives
|
|
129
|
+
|
|
130
|
+
Pure build-time geometry — no XML is read or mutated until `place()`.
|
|
131
|
+
|
|
132
|
+
### Grid
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from pptx2.design.layout import Grid
|
|
136
|
+
from pptx2.util import Pt
|
|
137
|
+
|
|
138
|
+
grid = Grid(slide, cols=12, rows=6, gutter=Pt(12), margin=Pt(48))
|
|
139
|
+
|
|
140
|
+
# Place a shape that spans columns 0..5, rows 0..3
|
|
141
|
+
grid.place(card1, col=0, row=0, col_span=6, row_span=4)
|
|
142
|
+
grid.place(card2, col=6, row=0, col_span=6, row_span=4)
|
|
143
|
+
|
|
144
|
+
# Or compute a Box without placing
|
|
145
|
+
box = grid.cell(col=0, row=4, col_span=12, row_span=2)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
A grid doesn't have to span the whole slide — `Grid.from_box` puts one
|
|
149
|
+
over any region (a `BBox`, a `Box`, or a plain `(left, top, width,
|
|
150
|
+
height)` tuple), so a panel can carry its own grid with no slide
|
|
151
|
+
reference:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from pptx2 import BBox
|
|
155
|
+
|
|
156
|
+
panel = BBox.from_inches(0.75, 2.4, 11.8, 3.6)
|
|
157
|
+
grid = Grid.from_box(panel, cols=5, rows=2, gutter=Pt(12))
|
|
158
|
+
grid.place(card, col=2, row=1)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
For the plain "n equal boxes across this region" case, `panel.columns(5,
|
|
162
|
+
gap=Pt(12))` / `panel.rows(2, ...)` from `BBox` are shorter — reach for
|
|
163
|
+
`Grid` when you need column spans.
|
|
164
|
+
|
|
165
|
+
### Stack
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from pptx2.design.layout import Stack
|
|
169
|
+
|
|
170
|
+
stack = Stack(direction="vertical", gap=Pt(8),
|
|
171
|
+
left=Pt(48), top=Pt(48), width=Pt(600))
|
|
172
|
+
|
|
173
|
+
stack.place(title, height=Pt(64))
|
|
174
|
+
stack.place(subtitle, height=Pt(28))
|
|
175
|
+
stack.place(body, height=Pt(280))
|
|
176
|
+
|
|
177
|
+
stack.reset() # rewind cursor
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`direction="horizontal"` walks left-to-right with `gap` between items.
|
|
181
|
+
|
|
182
|
+
## Slide recipes
|
|
183
|
+
|
|
184
|
+
Opinionated parameterized slide constructors. Each takes the host
|
|
185
|
+
`Presentation`, recipe-specific kwargs, an optional `DesignTokens`,
|
|
186
|
+
and an optional `transition=` name:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from pptx2.design.recipes import (
|
|
190
|
+
title_slide, bullet_slide, kpi_slide,
|
|
191
|
+
quote_slide, image_hero_slide,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
title_slide(
|
|
195
|
+
prs,
|
|
196
|
+
title="Q4 Review",
|
|
197
|
+
subtitle="April 2026",
|
|
198
|
+
tokens=tokens,
|
|
199
|
+
transition="morph",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
bullet_slide(
|
|
203
|
+
prs,
|
|
204
|
+
title="Customer impact",
|
|
205
|
+
bullets=[
|
|
206
|
+
"Two flagship customers shipped this week.",
|
|
207
|
+
"NPS improved 8 points QoQ.",
|
|
208
|
+
"EU expansion ahead of plan.",
|
|
209
|
+
],
|
|
210
|
+
tokens=tokens,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
kpi_slide(
|
|
214
|
+
prs,
|
|
215
|
+
title="Run-rate metrics",
|
|
216
|
+
kpis=[
|
|
217
|
+
{"label": "ARR", "value": "$182M", "delta": +0.27},
|
|
218
|
+
{"label": "NDR", "value": "131%", "delta": +0.03},
|
|
219
|
+
{"label": "CAC payback", "value": "8 mo", "delta": -0.10},
|
|
220
|
+
],
|
|
221
|
+
tokens=tokens,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
quote_slide(
|
|
225
|
+
prs,
|
|
226
|
+
quote="The new dashboards saved my team a week per sprint.",
|
|
227
|
+
attribution="Director of Eng, Flagship Customer",
|
|
228
|
+
tokens=tokens,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
image_hero_slide(
|
|
232
|
+
prs,
|
|
233
|
+
title="Q4 2026",
|
|
234
|
+
image="hero.jpg", # path or binary file-like
|
|
235
|
+
tokens=tokens,
|
|
236
|
+
)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Recipes use the `Blank` layout and place every shape themselves so the
|
|
240
|
+
rendered geometry doesn't depend on the host template's master.
|
|
241
|
+
|
|
242
|
+
`kpi_slide` honours `palette["positive"]` / `palette["negative"]` when
|
|
243
|
+
tinting deltas (falls back to green/red when unset). It applies
|
|
244
|
+
`tokens.shadows["card"]` to each card when present.
|
|
245
|
+
|
|
246
|
+
`image_hero_slide` uses `palette["on_primary"]` for overlay text and
|
|
247
|
+
tints the bottom band with `palette["primary"]` at 55% alpha.
|
|
248
|
+
|
|
249
|
+
## Shape-level building blocks
|
|
250
|
+
|
|
251
|
+
For mixed layouts where the slide-level recipes don't fit, reach for
|
|
252
|
+
the shape-level components in `pptx2.design.components`. Both
|
|
253
|
+
honour the deck's `DesignTokens` and return small dataclasses
|
|
254
|
+
exposing the constituent shapes, so callers can compose them into
|
|
255
|
+
custom layouts without re-implementing the styling:
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
from pptx2 import add_kpi_card, add_progress_bar
|
|
259
|
+
from pptx2.util import Inches
|
|
260
|
+
|
|
261
|
+
kpi = add_kpi_card(
|
|
262
|
+
slide,
|
|
263
|
+
left=Inches(1), top=Inches(1),
|
|
264
|
+
width=Inches(2.5), height=Inches(1.9),
|
|
265
|
+
label="ARR",
|
|
266
|
+
value="$182M",
|
|
267
|
+
delta={"delta": +0.27},
|
|
268
|
+
tokens=tokens,
|
|
269
|
+
)
|
|
270
|
+
# kpi.card / kpi.value_box / kpi.label_box / kpi.delta_box are
|
|
271
|
+
# accessible for further per-deck tweaks.
|
|
272
|
+
|
|
273
|
+
bar = add_progress_bar(
|
|
274
|
+
slide,
|
|
275
|
+
left=Inches(1), top=Inches(3),
|
|
276
|
+
width=Inches(6), height=Inches(0.3),
|
|
277
|
+
fraction=0.42, # 0..1 — clamped if you go over
|
|
278
|
+
tokens=tokens,
|
|
279
|
+
fill_color="#4F9DFF", # optional override
|
|
280
|
+
)
|
|
281
|
+
# bar.track / bar.fill — animate or restyle either independently.
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
All shape-level components tag their stacked shapes with
|
|
285
|
+
`lint_group` so the linter doesn't flag the intentional overlap
|
|
286
|
+
(label-on-card, fill-on-track) as a collision.
|
|
287
|
+
|
|
288
|
+
Other components in the same module:
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
from pptx2 import (
|
|
292
|
+
add_gauge, # progress bar with optional target tick
|
|
293
|
+
add_status_pill, # coloured pill + centred label, e.g. "LIVE"
|
|
294
|
+
add_stat_strip, # n KPI tiles laid out across a strip with gutter
|
|
295
|
+
add_article_card, # title + blurb + optional CTA pill
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
add_gauge(slide, left=Inches(1), top=Inches(2), width=Inches(4),
|
|
299
|
+
height=Inches(0.3), fraction=0.62, target=0.80, tokens=tokens)
|
|
300
|
+
|
|
301
|
+
add_stat_strip(slide, left=Inches(0.5), top=Inches(1.5),
|
|
302
|
+
width=Inches(12), height=Inches(1.9),
|
|
303
|
+
items=[{"label": "ARR", "value": "$182M"},
|
|
304
|
+
{"label": "NDR", "value": "131%", "delta": +0.03},
|
|
305
|
+
{"label": "CAC payback", "value": "8 mo"}],
|
|
306
|
+
tokens=tokens)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Starter pack
|
|
310
|
+
|
|
311
|
+
`examples/starter_pack/` ships three example token sets — `modern`,
|
|
312
|
+
`classic`, and `editorial` — each exporting both a raw `SPEC` dict and
|
|
313
|
+
a ready-to-use `TOKENS`:
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
from examples.starter_pack import modern, classic, editorial
|
|
317
|
+
|
|
318
|
+
prs = Presentation()
|
|
319
|
+
title_slide(prs, title="Hello", subtitle="World", tokens=modern.TOKENS)
|
|
320
|
+
prs.save("modern.pptx")
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Run `python -m examples.starter_pack.build_preview` to render one
|
|
324
|
+
preview deck per set into `examples/starter_pack/_out/`.
|
|
325
|
+
|
|
326
|
+
## End-to-end branded deck
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
from pptx2 import Presentation
|
|
330
|
+
from pptx2.design.tokens import DesignTokens
|
|
331
|
+
from pptx2.design.recipes import (
|
|
332
|
+
title_slide, bullet_slide, kpi_slide, quote_slide,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
tokens = DesignTokens.from_dict({
|
|
336
|
+
"palette": {
|
|
337
|
+
"primary": "#4F9DFF",
|
|
338
|
+
"neutral": "#1F2937",
|
|
339
|
+
"positive": "#10B981",
|
|
340
|
+
"negative": "#EF4444",
|
|
341
|
+
"on_primary": "#FFFFFF",
|
|
342
|
+
},
|
|
343
|
+
"typography": {
|
|
344
|
+
# Recipes look up "heading" and "body". Floats = points, ints = EMU.
|
|
345
|
+
"heading": {"family": "Inter", "size": 44.0, "bold": True},
|
|
346
|
+
"body": {"family": "Inter", "size": 18.0},
|
|
347
|
+
},
|
|
348
|
+
"shadows": {"card": {"blur": 18.0, "distance": 4.0, "alpha": 0.18}},
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
prs = Presentation()
|
|
352
|
+
title_slide(prs, title="Q4 Review", subtitle="April 2026",
|
|
353
|
+
tokens=tokens, transition="morph")
|
|
354
|
+
kpi_slide(prs, title="Run-rate metrics", kpis=[
|
|
355
|
+
{"label": "ARR", "value": "$182M", "delta": +0.27},
|
|
356
|
+
{"label": "NDR", "value": "131%", "delta": +0.03},
|
|
357
|
+
], tokens=tokens)
|
|
358
|
+
bullet_slide(prs, title="Customer impact", bullets=[
|
|
359
|
+
"Two flagship customers shipped this week.",
|
|
360
|
+
"NPS improved 8 points QoQ.",
|
|
361
|
+
], tokens=tokens)
|
|
362
|
+
quote_slide(prs, quote="The new dashboards saved my team a week per sprint.",
|
|
363
|
+
attribution="Director of Eng", tokens=tokens)
|
|
364
|
+
|
|
365
|
+
prs.save("q4-review.pptx")
|
|
366
|
+
```
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Fills and visual effects (Phase 3 + Phase 6)
|
|
2
|
+
|
|
3
|
+
**This is the canonical reference for fills, colour alpha, gradients,
|
|
4
|
+
and effects.** Other files link here rather than repeat it.
|
|
5
|
+
|
|
6
|
+
Every shape in `python-pptx2` exposes non-mutating effect proxies. Reads
|
|
7
|
+
return `None` when nothing is set; writes lazily create the underlying
|
|
8
|
+
`<a:effectLst>` / `<a:ln>` element.
|
|
9
|
+
|
|
10
|
+
Contents: outer shadow · removing a shadow · glow · soft edges · blur ·
|
|
11
|
+
reflection · the card look · corner radius · alpha-tinted fills ·
|
|
12
|
+
gradient fills · line ends/caps/joins.
|
|
13
|
+
|
|
14
|
+
## Outer shadow
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from pptx2.util import Pt
|
|
18
|
+
from pptx2.dml.color import RGBColor
|
|
19
|
+
|
|
20
|
+
shadow = card.shadow
|
|
21
|
+
shadow.blur_radius = Pt(8)
|
|
22
|
+
shadow.distance = Pt(4)
|
|
23
|
+
shadow.direction = 90.0 # degrees, 90 = down
|
|
24
|
+
shadow.color.rgb = RGBColor(0, 0, 0)
|
|
25
|
+
shadow.color.alpha = 0.35 # 35% opacity
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
To restore inheritance, assign `None` to each property — the
|
|
29
|
+
`<a:outerShdw>` element is dropped when the last attribute goes away.
|
|
30
|
+
|
|
31
|
+
### Removing a shadow entirely: `shadow.clear()`
|
|
32
|
+
|
|
33
|
+
Restoring inheritance is **not** the same as having no shadow. Auto
|
|
34
|
+
shapes created by `add_shape` carry a `<p:style>` with
|
|
35
|
+
`<a:effectRef idx="2"/>`, which resolves against the theme's effect
|
|
36
|
+
styles — a soft drop shadow in most themes. Clear the explicit
|
|
37
|
+
properties and that inherited shadow is what you're left with: the
|
|
38
|
+
"phantom shadow I never asked for".
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, *box)
|
|
42
|
+
card.shadow.clear() # flat card, guaranteed
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`clear()` drops every explicit shadow element (outer, inner, preset),
|
|
46
|
+
writes the empty `<a:effectLst/>` that overrides inherited effects, and
|
|
47
|
+
re-points `<a:effectRef>` at the theme's empty slot (`idx="0"`). Other
|
|
48
|
+
effects written on the shape — glow, soft edges, blur, reflection — are
|
|
49
|
+
kept. Theme-derived ones are not: `effectRef` names one whole entry in
|
|
50
|
+
the theme's effect-style list, so if a custom theme pairs its shadow
|
|
51
|
+
with a glow, that glow goes too — re-apply it explicitly on the shape.
|
|
52
|
+
Stock Office themes reference shadow-only styles, so this rarely bites.
|
|
53
|
+
It's idempotent and safe on shapes that never had a shadow, including
|
|
54
|
+
text boxes and pictures (which have no `<p:style>` to re-point). On a
|
|
55
|
+
shape imported with an `<a:effectDag>` (an effect tree rather than a
|
|
56
|
+
flat list) the shadow nodes are pruned from that tree instead — the two
|
|
57
|
+
are mutually exclusive in the schema, so writing a list alongside one
|
|
58
|
+
would produce a deck PowerPoint offers to repair.
|
|
59
|
+
|
|
60
|
+
> ⚠ `shadow.inherit` (read or write) emits a `DeprecationWarning` in
|
|
61
|
+
> 1.1+. Read individual properties for `None`; use `clear()` to remove.
|
|
62
|
+
> `inherit = False` only writes the empty `<a:effectLst/>` — it stays
|
|
63
|
+
> symmetric with `inherit = True` and so cannot touch the theme effect
|
|
64
|
+
> reference. It does **not** remove an inherited shadow; `clear()` does.
|
|
65
|
+
|
|
66
|
+
## Glow
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
card.glow.radius = Pt(6)
|
|
70
|
+
card.glow.color.rgb = RGBColor(0x4F, 0x9D, 0xFF)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Soft edges
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
card.soft_edges.radius = Pt(3)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Blur
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
card.blur.radius = Pt(4)
|
|
83
|
+
card.blur.grow = True # grow with the shape
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Reflection
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
card.reflection.blur_radius = Pt(2)
|
|
90
|
+
card.reflection.distance = Pt(1)
|
|
91
|
+
card.reflection.start_alpha = 0.5
|
|
92
|
+
card.reflection.end_alpha = 0.0
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Combining for a "card" look
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from pptx2.enum.shapes import MSO_SHAPE
|
|
99
|
+
from pptx2.util import Inches, Pt
|
|
100
|
+
from pptx2.dml.color import RGBColor
|
|
101
|
+
|
|
102
|
+
card = slide.shapes.add_shape(
|
|
103
|
+
MSO_SHAPE.ROUNDED_RECTANGLE,
|
|
104
|
+
Inches(1), Inches(1.5), Inches(4), Inches(2.5),
|
|
105
|
+
)
|
|
106
|
+
card.fill.solid()
|
|
107
|
+
card.fill.fore_color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
|
|
108
|
+
card.line.fill.background() # no border
|
|
109
|
+
|
|
110
|
+
card.corner_radius = Pt(6) # not adjustments[0]
|
|
111
|
+
|
|
112
|
+
card.shadow.blur_radius = Pt(18)
|
|
113
|
+
card.shadow.distance = Pt(4)
|
|
114
|
+
card.shadow.direction = 90.0
|
|
115
|
+
card.shadow.color.rgb = RGBColor(0, 0, 0)
|
|
116
|
+
card.shadow.color.alpha = 0.18
|
|
117
|
+
|
|
118
|
+
card.soft_edges.radius = Pt(1)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
For a **flat** card, swap the four shadow lines for `card.shadow.clear()`
|
|
122
|
+
— see above; assigning `None` to them is not equivalent.
|
|
123
|
+
|
|
124
|
+
### Corner radius in points
|
|
125
|
+
|
|
126
|
+
`shape.corner_radius` reads and writes a rounded rectangle's radius as a
|
|
127
|
+
length, converting to and from the fraction-of-the-shorter-side that
|
|
128
|
+
OOXML stores in `adjustments[0]`:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
card.corner_radius = Pt(6)
|
|
132
|
+
card.corner_radius.pt # -> 6.0
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Defined for `ROUNDED_RECTANGLE`, `ROUND_1_RECTANGLE`,
|
|
136
|
+
`ROUND_2_SAME_RECTANGLE`, and `ROUND_2_DIAG_RECTANGLE` (the two-radius
|
|
137
|
+
geometries keep their second corner pair on `adjustments[1]`). Raises
|
|
138
|
+
rather than silently clipping when the radius exceeds half the shorter
|
|
139
|
+
side.
|
|
140
|
+
|
|
141
|
+
## Alpha-tinted fills
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
card.fill.solid()
|
|
145
|
+
card.fill.fore_color.rgb = RGBColor(0x4F, 0x9D, 0xFF)
|
|
146
|
+
card.fill.fore_color.alpha = 0.55 # glassy
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`alpha` is also available on the lazy proxy returned by `Font.color`
|
|
150
|
+
and `LineFormat.color`:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
title_run.font.color.rgb = RGBColor(0x1F, 0x29, 0x37)
|
|
154
|
+
title_run.font.color.alpha = 0.9
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Gradient fills
|
|
158
|
+
|
|
159
|
+
The two-liner most decks want — a multi-stop linear gradient at an
|
|
160
|
+
angle:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
bar.fill.linear_gradient("#06D6FE", "#B14AED", angle=90) # top→bottom
|
|
164
|
+
bar.fill.linear_gradient(
|
|
165
|
+
[("#06D6FE", 0.0), ("#FFFFFF", 0.5), ("#B14AED", 1.0)],
|
|
166
|
+
angle=45,
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`angle` follows the OOXML convention: `0` is left→right, `90` is
|
|
171
|
+
top→bottom, `180` is right→left, `270` is bottom→top.
|
|
172
|
+
|
|
173
|
+
### Other kinds, and mutable stops
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
fill = card.fill
|
|
177
|
+
fill.gradient(kind="radial") # also "linear", "rectangular", "shape"
|
|
178
|
+
fill.gradient_kind # → "radial"
|
|
179
|
+
|
|
180
|
+
stops = fill.gradient_stops
|
|
181
|
+
stops.replace([
|
|
182
|
+
(0.0, "#0F2D6B"), # hex with or without leading '#'
|
|
183
|
+
(0.55, RGBColor(0x4F, 0x9D, 0xFF)),
|
|
184
|
+
(1.0, (255, 255, 255)), # plain RGB tuple also accepted
|
|
185
|
+
])
|
|
186
|
+
|
|
187
|
+
# Add or remove individual stops
|
|
188
|
+
stops.append(0.85, "#A8C0FF")
|
|
189
|
+
del stops[1]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
OOXML enforces a 2-stop minimum; the helper raises if you try to drop
|
|
193
|
+
below that.
|
|
194
|
+
|
|
195
|
+
## Line ends, caps, joins, compound lines
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from pptx2.enum.dml import (
|
|
199
|
+
MSO_LINE_CAP_STYLE,
|
|
200
|
+
MSO_LINE_COMPOUND_STYLE,
|
|
201
|
+
MSO_LINE_JOIN_STYLE,
|
|
202
|
+
MSO_LINE_END_TYPE,
|
|
203
|
+
MSO_LINE_END_SIZE,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
line = arrow.line
|
|
207
|
+
line.head_end.type = MSO_LINE_END_TYPE.TRIANGLE
|
|
208
|
+
line.head_end.width = MSO_LINE_END_SIZE.MEDIUM
|
|
209
|
+
line.head_end.length = MSO_LINE_END_SIZE.LARGE
|
|
210
|
+
line.tail_end.type = MSO_LINE_END_TYPE.OVAL
|
|
211
|
+
line.cap = MSO_LINE_CAP_STYLE.ROUND
|
|
212
|
+
line.compound = MSO_LINE_COMPOUND_STYLE.DOUBLE
|
|
213
|
+
line.join = MSO_LINE_JOIN_STYLE.BEVEL
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Reads on an unset attribute return `None` — assigning `None` clears
|
|
217
|
+
just that attribute. When the last attribute on a head/tail end goes
|
|
218
|
+
away the `<a:headEnd>` / `<a:tailEnd>` element is dropped so theme
|
|
219
|
+
inheritance is preserved.
|
|
220
|
+
|
|
221
|
+
## Reading effects without mutating
|
|
222
|
+
|
|
223
|
+
Always safe to inspect:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
if card.shadow.blur_radius is None:
|
|
227
|
+
print("no explicit shadow")
|
|
228
|
+
else:
|
|
229
|
+
print("blur:", card.shadow.blur_radius.pt)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
No `<a:effectLst>` is written by the read.
|
|
233
|
+
|
|
234
|
+
## Inner & preset shadows
|
|
235
|
+
|
|
236
|
+
Besides `shape.shadow` (outer), shapes expose two sibling shadow effects:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
shape.inner_shadow.blur_radius = Pt(4) # shadow cast INTO the shape
|
|
240
|
+
shape.inner_shadow.distance = Pt(3)
|
|
241
|
+
shape.inner_shadow.direction = 45.0
|
|
242
|
+
shape.inner_shadow.color.rgb = "112233"
|
|
243
|
+
|
|
244
|
+
shape.preset_shadow.preset = "shdw5" # or MSO_PRESET_SHADOW.SHADOW_5
|
|
245
|
+
shape.preset_shadow.color.rgb = "aabbcc" # one of shdw1..shdw20
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
A colour child and the required `prst` are written automatically, so
|
|
249
|
+
geometry-only shadows stay schema-valid.
|