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.
Files changed (175) hide show
  1. pptx2/__init__.py +152 -0
  2. pptx2/_color.py +75 -0
  3. pptx2/_slide_importer.py +597 -0
  4. pptx2/_svg.py +155 -0
  5. pptx2/_template_applier.py +292 -0
  6. pptx2/_textstyle.py +187 -0
  7. pptx2/accessibility.py +365 -0
  8. pptx2/action.py +270 -0
  9. pptx2/animation.py +2237 -0
  10. pptx2/api.py +49 -0
  11. pptx2/audit.py +258 -0
  12. pptx2/chart/__init__.py +0 -0
  13. pptx2/chart/analytics.py +381 -0
  14. pptx2/chart/axis.py +543 -0
  15. pptx2/chart/category.py +200 -0
  16. pptx2/chart/chart.py +670 -0
  17. pptx2/chart/data.py +864 -0
  18. pptx2/chart/datalabel.py +406 -0
  19. pptx2/chart/legend.py +86 -0
  20. pptx2/chart/marker.py +70 -0
  21. pptx2/chart/palettes.py +129 -0
  22. pptx2/chart/plot.py +462 -0
  23. pptx2/chart/point.py +101 -0
  24. pptx2/chart/quick_layouts.py +325 -0
  25. pptx2/chart/series.py +334 -0
  26. pptx2/chart/xlsx.py +272 -0
  27. pptx2/chart/xmlwriter.py +1845 -0
  28. pptx2/compose/__init__.py +28 -0
  29. pptx2/compose/from_spec.py +1094 -0
  30. pptx2/design/__init__.py +8 -0
  31. pptx2/design/components.py +607 -0
  32. pptx2/design/figures.py +389 -0
  33. pptx2/design/layout.py +370 -0
  34. pptx2/design/recipes.py +1967 -0
  35. pptx2/design/style.py +209 -0
  36. pptx2/design/tokens.py +915 -0
  37. pptx2/diagrams.py +754 -0
  38. pptx2/dml/__init__.py +0 -0
  39. pptx2/dml/chtfmt.py +40 -0
  40. pptx2/dml/color.py +496 -0
  41. pptx2/dml/effect.py +909 -0
  42. pptx2/dml/fill.py +691 -0
  43. pptx2/dml/line.py +287 -0
  44. pptx2/dml/picture.py +212 -0
  45. pptx2/dml/three_d.py +381 -0
  46. pptx2/enum/__init__.py +0 -0
  47. pptx2/enum/action.py +71 -0
  48. pptx2/enum/animation.py +31 -0
  49. pptx2/enum/base.py +218 -0
  50. pptx2/enum/chart.py +574 -0
  51. pptx2/enum/dml.py +740 -0
  52. pptx2/enum/lang.py +685 -0
  53. pptx2/enum/presentation.py +133 -0
  54. pptx2/enum/shapes.py +1029 -0
  55. pptx2/enum/text.py +230 -0
  56. pptx2/exc.py +42 -0
  57. pptx2/formats.py +139 -0
  58. pptx2/geometry.py +420 -0
  59. pptx2/inherit.py +109 -0
  60. pptx2/lint.py +2256 -0
  61. pptx2/math.py +177 -0
  62. pptx2/media.py +197 -0
  63. pptx2/opc/__init__.py +0 -0
  64. pptx2/opc/constants.py +332 -0
  65. pptx2/opc/oxml.py +188 -0
  66. pptx2/opc/package.py +762 -0
  67. pptx2/opc/packuri.py +109 -0
  68. pptx2/opc/serialized.py +296 -0
  69. pptx2/opc/shared.py +20 -0
  70. pptx2/opc/spec.py +45 -0
  71. pptx2/oxml/__init__.py +555 -0
  72. pptx2/oxml/action.py +53 -0
  73. pptx2/oxml/chart/__init__.py +0 -0
  74. pptx2/oxml/chart/axis.py +337 -0
  75. pptx2/oxml/chart/chart.py +481 -0
  76. pptx2/oxml/chart/datalabel.py +253 -0
  77. pptx2/oxml/chart/legend.py +72 -0
  78. pptx2/oxml/chart/marker.py +61 -0
  79. pptx2/oxml/chart/plot.py +365 -0
  80. pptx2/oxml/chart/series.py +425 -0
  81. pptx2/oxml/chart/shared.py +220 -0
  82. pptx2/oxml/coreprops.py +288 -0
  83. pptx2/oxml/dml/__init__.py +0 -0
  84. pptx2/oxml/dml/color.py +135 -0
  85. pptx2/oxml/dml/effect.py +213 -0
  86. pptx2/oxml/dml/fill.py +316 -0
  87. pptx2/oxml/dml/line.py +12 -0
  88. pptx2/oxml/dml/three_d.py +110 -0
  89. pptx2/oxml/ns.py +135 -0
  90. pptx2/oxml/presentation.py +313 -0
  91. pptx2/oxml/shapes/__init__.py +19 -0
  92. pptx2/oxml/shapes/autoshape.py +467 -0
  93. pptx2/oxml/shapes/connector.py +107 -0
  94. pptx2/oxml/shapes/graphfrm.py +347 -0
  95. pptx2/oxml/shapes/groupshape.py +329 -0
  96. pptx2/oxml/shapes/picture.py +270 -0
  97. pptx2/oxml/shapes/shared.py +577 -0
  98. pptx2/oxml/simpletypes.py +1027 -0
  99. pptx2/oxml/slide.py +563 -0
  100. pptx2/oxml/table.py +650 -0
  101. pptx2/oxml/text.py +815 -0
  102. pptx2/oxml/theme.py +36 -0
  103. pptx2/oxml/xmlchemy.py +717 -0
  104. pptx2/package.py +222 -0
  105. pptx2/parts/__init__.py +0 -0
  106. pptx2/parts/chart.py +95 -0
  107. pptx2/parts/coreprops.py +167 -0
  108. pptx2/parts/diagram.py +37 -0
  109. pptx2/parts/embeddedpackage.py +93 -0
  110. pptx2/parts/image.py +275 -0
  111. pptx2/parts/media.py +37 -0
  112. pptx2/parts/presentation.py +136 -0
  113. pptx2/parts/slide.py +371 -0
  114. pptx2/presentation.py +408 -0
  115. pptx2/py.typed +0 -0
  116. pptx2/render.py +586 -0
  117. pptx2/section.py +272 -0
  118. pptx2/shapes/__init__.py +26 -0
  119. pptx2/shapes/autoshape.py +442 -0
  120. pptx2/shapes/base.py +1078 -0
  121. pptx2/shapes/connector.py +297 -0
  122. pptx2/shapes/freeform.py +337 -0
  123. pptx2/shapes/graphfrm.py +316 -0
  124. pptx2/shapes/group.py +264 -0
  125. pptx2/shapes/picture.py +422 -0
  126. pptx2/shapes/placeholder.py +468 -0
  127. pptx2/shapes/shapetree.py +2027 -0
  128. pptx2/shared.py +82 -0
  129. pptx2/skill/SKILL.md +450 -0
  130. pptx2/skill/__init__.py +78 -0
  131. pptx2/skill/__main__.py +64 -0
  132. pptx2/skill/references/animations.md +189 -0
  133. pptx2/skill/references/basics.md +421 -0
  134. pptx2/skill/references/charts.md +254 -0
  135. pptx2/skill/references/compose.md +234 -0
  136. pptx2/skill/references/design.md +366 -0
  137. pptx2/skill/references/effects.md +249 -0
  138. pptx2/skill/references/end-to-end-deck.md +231 -0
  139. pptx2/skill/references/geometry-and-arrows.md +334 -0
  140. pptx2/skill/references/lint.md +275 -0
  141. pptx2/skill/references/math.md +86 -0
  142. pptx2/skill/references/picture-effects.md +129 -0
  143. pptx2/skill/references/render.md +151 -0
  144. pptx2/skill/references/smart-art.md +75 -0
  145. pptx2/skill/references/space-aware-authoring.md +249 -0
  146. pptx2/skill/references/tables.md +244 -0
  147. pptx2/skill/references/theme.md +127 -0
  148. pptx2/skill/references/three-d.md +109 -0
  149. pptx2/skill/references/transitions.md +100 -0
  150. pptx2/slide.py +1244 -0
  151. pptx2/smart_art.py +220 -0
  152. pptx2/spec.py +633 -0
  153. pptx2/table.py +1181 -0
  154. pptx2/table_styles.py +184 -0
  155. pptx2/templates/default.pptx +0 -0
  156. pptx2/templates/docx-icon.emf +0 -0
  157. pptx2/templates/generic-icon.emf +0 -0
  158. pptx2/templates/notes.xml +23 -0
  159. pptx2/templates/notesMaster.xml +352 -0
  160. pptx2/templates/pptx-icon.emf +0 -0
  161. pptx2/templates/theme.xml +321 -0
  162. pptx2/templates/xlsx-icon.emf +0 -0
  163. pptx2/text/__init__.py +0 -0
  164. pptx2/text/fonts.py +482 -0
  165. pptx2/text/layout.py +374 -0
  166. pptx2/text/text.py +1272 -0
  167. pptx2/theme.py +721 -0
  168. pptx2/types.py +36 -0
  169. pptx2/util.py +263 -0
  170. python_pptx2-2.13.0.dist-info/METADATA +351 -0
  171. python_pptx2-2.13.0.dist-info/RECORD +175 -0
  172. python_pptx2-2.13.0.dist-info/WHEEL +5 -0
  173. python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
  174. python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
  175. 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.