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,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.