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,275 @@
1
+ # Layout linter (Phase 2)
2
+
3
+ Programmatic decks tend to ship the same handful of bugs over and
4
+ over: text spilling out of its container, shapes off-slide, layered
5
+ elements that aren't intended overlaps. The linter is built for
6
+ exactly that use case — it's especially useful when feeding decks
7
+ generated from LLM output or arbitrary user input.
8
+
9
+ ## Run on a slide
10
+
11
+ ```python
12
+ report = slide.lint()
13
+ report.issues # list[LintIssue]
14
+ report.has_errors # bool
15
+ print(report.summary())
16
+ ```
17
+
18
+ For a whole deck, iterate the slides yourself:
19
+
20
+ ```python
21
+ all_issues = []
22
+ for slide in prs.slides:
23
+ all_issues.extend(slide.lint().issues)
24
+ ```
25
+
26
+ `from_spec` (see `compose.md`) accepts a deck-level
27
+ ``"lint": "warn" | "raise"`` field that walks every slide for you.
28
+
29
+ ## Issue types
30
+
31
+ ```python
32
+ from pptx2.lint import TextOverflow, OffSlide, ShapeCollision
33
+
34
+ for issue in report.issues:
35
+ if isinstance(issue, TextOverflow):
36
+ print("overflow", issue.shapes[0].name, "ratio", issue.ratio)
37
+ elif isinstance(issue, OffSlide):
38
+ print("off-slide", issue.shapes[0].name, "side", issue.side)
39
+ elif isinstance(issue, ShapeCollision):
40
+ a, b = issue.shapes
41
+ print("collision", a.name, b.name,
42
+ "intersection_pct", issue.intersection_pct)
43
+ ```
44
+
45
+ `LayerOrderViolation` carries the declared `layer` name, and its
46
+ `shapes` tuple is `(declaring_shape, layer_shape)`.
47
+
48
+ Every issue carries a `severity` (`LintSeverity.ERROR` / `WARNING` /
49
+ `INFO`), a `code` string, a `message`, and a `shapes` tuple of the
50
+ shapes it implicates.
51
+
52
+ `TextOverflow` uses Pillow font metrics and respects margins, vertical
53
+ anchor, line spacing, and `auto_size`.
54
+
55
+ ## Auto-fix
56
+
57
+ ```python
58
+ fixes = report.auto_fix() # mutates; returns list[str]
59
+ preview = report.auto_fix(dry_run=True) # no mutation; returns list[str]
60
+ ```
61
+
62
+ What's currently fixable:
63
+
64
+ - **`OffSlide`** → translates the shape so it sits inside the slide
65
+ bounds (shrinking it first if it is larger than the slide). Returns a
66
+ one-line description of each nudge.
67
+ - **`TextOverflow`** → flips the frame to
68
+ `MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE` so PowerPoint shrinks the runs at
69
+ render time. Non-destructive: the text is preserved verbatim.
70
+ - **`OffGridDrift`** → snaps the drifted edge onto the dominant grid
71
+ line.
72
+ - **`LayerOrderViolation`** → restacks the shape that declared
73
+ `layer_above` so the drawing order matches what you declared.
74
+ Geometry is untouched.
75
+
76
+ Reported only, never auto-fixed:
77
+
78
+ - **`ShapeCollision`** → auto-nudging would almost always break the
79
+ design. Declare the overlap instead — see below.
80
+ - **`LowContrast`**, **`MinFontSize`**, **`ZOrderAnomaly`**,
81
+ **`MasterPlaceholderCollision`** → need designer judgment.
82
+
83
+ `slide.tidy()` is the one-call wrapper: it lints, then applies the safe
84
+ subset (`fix_offslide`, `fix_overflow`, `fix_layer_order` on by default;
85
+ `fix_grid_drift` off).
86
+
87
+ ## Declaring an overlap is intentional
88
+
89
+ `ShapeCollision` is the noisiest rule, because deliberate layering —
90
+ a badge on a card, an accent bar on a panel — looks identical to a
91
+ copy-paste bug from a bounding box alone. Tell the linter what you
92
+ meant and it stops guessing. Three ways, narrowest last:
93
+
94
+ ```python
95
+ # 1. Group tag — n-ary and symmetric. Everything sharing a non-empty
96
+ # tag may overlap everything else in the tag.
97
+ card.lint_group = "kpi-1"
98
+ accent.lint_group = "kpi-1"
99
+ slide.lint_group("kpi-1", card, accent, label) # batch form
100
+ slide.lint_group_overlaps(card, accent, label) # auto-names the group
101
+
102
+ # 2. Pairwise allowance — licenses exactly one pair, nothing else.
103
+ badge.allow_overlap_with(card)
104
+ badge.disallow_overlap_with(card) # revoke
105
+ badge.overlap_allowances # frozenset[int] of shape ids
106
+
107
+ # 3. Layer hints — the only form that also asserts z-order.
108
+ card.layer = "card"
109
+ badge.layer_above = "card"
110
+ ```
111
+
112
+ Use a **group** when several shapes form one visual cluster; a
113
+ **pairwise allowance** when only one specific overlap is meant to be
114
+ legal and you want the rest still policed; **layer hints** when the
115
+ stacking order itself matters.
116
+
117
+ An allowance may only name a shape on the **same slide**, and passing
118
+ one from another slide raises `ValueError`. Use `shape.delete()` rather
119
+ than removing the element by hand — it purges allowances naming the
120
+ deleted shape, which matters because ids get recycled and a stale one
121
+ would later match an unrelated shape. Allowances are keyed on
122
+ `cNvPr/@id`, which is unique per slide but repeats across a deck — so a
123
+ borrowed id would either read as a bogus self-reference or silently
124
+ match an unrelated shape here and suppress a collision that was real.
125
+
126
+ Layer hints are the only one that can *fail*. Declaring
127
+ `layer_above = "card"` asserts this shape is painted on top of every
128
+ overlapping shape whose `layer` is `"card"`. If the shape tree says
129
+ otherwise — the shape claiming to be on top comes earlier in `spTree`
130
+ and is drawn underneath — you get a `LayerOrderViolation` (severity
131
+ ERROR), because the declaration records what you meant and the drawing
132
+ order is what failed to deliver it:
133
+
134
+ ```python
135
+ report = slide.lint()
136
+ report.auto_fix() # restacks it for you; geometry untouched
137
+ ```
138
+
139
+ A shape name with a dotted prefix is grouped implicitly, so naming
140
+ shapes `"card.bg"` / `"card.title"` groups them under `"card"` with no
141
+ extra calls. Set `shape.lint_group = ""` to opt a dotted name out.
142
+
143
+ All of it lives in the shape's `cNvPr/extLst`, the OOXML-sanctioned
144
+ extension point, so it survives save/open and PowerPoint leaves it
145
+ alone. Related but different: `shape.lint_skip = {"MinFontSize"}`
146
+ silences a rule on a shape rather than declaring intent.
147
+
148
+ ## Machine-readable output (for agents / CI)
149
+
150
+ `summary()` is for humans; `to_dict()` / `to_json()` are for code. Use
151
+ them to feed lint results back into an LLM auto-fix loop or a CI gate
152
+ instead of regex-parsing the summary string.
153
+
154
+ ```python
155
+ report = slide.lint()
156
+ report.to_dict() # {"has_errors", "issue_count", "issues": [...]}
157
+ report.to_json() # same, as a JSON string (indent=2 default)
158
+ ```
159
+
160
+ Each issue is self-describing — it carries `code`, `severity`,
161
+ `message`, the names of the `shapes` involved, and every
162
+ detector-specific field (`OffSlide.side`, `TextOverflow.ratio`, the
163
+ `ShapeCollision` scoring, …):
164
+
165
+ ```json
166
+ {"code": "OffSlide", "severity": "error",
167
+ "message": "Shape 'Rectangle 1' extends beyond the right edge of the slide.",
168
+ "shapes": ["Rectangle 1"], "side": "right"}
169
+ ```
170
+
171
+ The whole-deck `audit()` report has the same pair:
172
+
173
+ ```python
174
+ from pptx2 import audit
175
+
176
+ data = audit(prs).to_dict() # adds a "slide" index to every lint issue
177
+ if data["has_errors"]:
178
+ ... # hand `data` straight to the model to fix
179
+ ```
180
+
181
+ ## Save-time hooks (via `from_spec`)
182
+
183
+ If you build the deck through `pptx2.compose.from_spec`, the spec dict
184
+ accepts a top-level ``"lint"`` field:
185
+
186
+ ```python
187
+ from pptx2.compose import from_spec
188
+
189
+ prs = from_spec({
190
+ "slides": [...],
191
+ "lint": "raise", # also "warn" or "off" (default)
192
+ })
193
+ ```
194
+
195
+ `"warn"` logs every issue through stdlib `logging`; `"raise"` raises
196
+ `pptx2.exc.LintError` if any error-severity issue is found.
197
+
198
+ ## Save-time hooks (any presentation)
199
+
200
+ Every `Presentation` has a `lint_on_save` switch, whatever built it:
201
+
202
+ ```python
203
+ prs = pptx2.Presentation("deck.pptx")
204
+ prs.lint_on_save = "off" # default — no checks, no cost
205
+ prs.lint_on_save = "warn" # log error-severity issues, still write the file
206
+ prs.lint_on_save = "raise" # raise LintError instead of writing the file
207
+
208
+ prs.save("out.pptx")
209
+ ```
210
+
211
+ Only **error**-severity issues count; warnings and info never trigger it.
212
+ The lint pass runs *before* anything is written, so `"raise"` never leaves a
213
+ bad file on disk. `"warn"` logs on the `pptx2.presentation` logger. The
214
+ setting lives on the in-memory object only — re-open the saved file and it is
215
+ back to `"off"`.
216
+
217
+ ## Recommended pattern for generators
218
+
219
+ ```python
220
+ from pptx2.exc import LintError
221
+
222
+ prs = build_deck_from_user_input(...)
223
+
224
+ # 1. Auto-fix what we can, slide by slide
225
+ for slide in prs.slides:
226
+ report = slide.lint()
227
+ report.auto_fix()
228
+
229
+ # 2. Re-run and bail on any remaining errors
230
+ remaining = []
231
+ for slide in prs.slides:
232
+ remaining.extend(i for i in slide.lint().issues
233
+ if getattr(i, "severity", None) == "error")
234
+ if remaining:
235
+ raise LintError("; ".join(str(i) for i in remaining))
236
+
237
+ prs.save("out.pptx")
238
+ ```
239
+
240
+ ## CI loop: SARIF export + baseline diff
241
+
242
+ `summary()` is for humans; SARIF and diff are for CI.
243
+
244
+ ```python
245
+ report = slide.lint()
246
+ report.to_sarif(slide_index=0) # SARIF v2.1.0 dict (GitHub code-scanning)
247
+ report.to_sarif_json() # JSON string
248
+
249
+ from pptx2.lint import lint_report_to_sarif
250
+ sarif = lint_report_to_sarif([s.lint() for s in prs.slides]) # whole deck
251
+
252
+ report.fingerprints() # list[str] -- the stable ids diff() uses
253
+ new_issues = current.diff(baseline) # only issues NOT in baseline
254
+ both = current.diff_detail(baseline) # {"added": [...], "fixed": [...]}
255
+ ```
256
+
257
+ Severities map ERROR→error, WARNING→warning, INFO→note. A
258
+ moved-but-still-broken shape keeps its fingerprint, so `diff()` won't
259
+ flag it as new.
260
+
261
+ ## Accessibility
262
+
263
+ ```python
264
+ picture.alt_text = "Bar chart of Q3 revenue by region." # -> <p:cNvPr descr=...>
265
+ picture.title_text = "Q3 revenue" # -> <p:cNvPr title=...>
266
+
267
+ from pptx2 import accessibility
268
+ report = accessibility.audit_accessibility(prs) # read-only
269
+ if report.has_errors: # picture with no alt text = error
270
+ print(report.markdown())
271
+ report.to_dict() # JSON-serializable for a CI gate / LLM loop
272
+ ```
273
+
274
+ Flags: `MissingAltText` (pictures = error), `LowContrast` (below WCAG AA
275
+ 4.5:1), `NoSlideTitle` (no title landmark for navigation).
@@ -0,0 +1,86 @@
1
+ # Native equations from LaTeX
2
+
3
+ PowerPoint stores editable equations as Office Math (OMML) inside an
4
+ `a14:m` marker. python-pptx2 does **not** compile LaTeX itself. It calls
5
+ two libraries and wraps the result:
6
+
7
+ 1. [`latex2mathml`](https://pypi.org/project/latex2mathml/) — LaTeX → MathML
8
+ 2. [`mathml2omml`](https://pypi.org/project/mathml2omml/) — MathML → OMML
9
+
10
+ Install them with:
11
+
12
+ ```bash
13
+ pip install "python-pptx2[math]"
14
+ ```
15
+
16
+ Missing converters raise `MathBackendUnavailable` with that install line.
17
+
18
+ ## Display equation — `slide.shapes.add_equation`
19
+
20
+ Same calling convention as `add_text`: a `BBox` or `(left, top, width, height)`.
21
+
22
+ ```python
23
+ from pptx2 import Presentation, BBox
24
+ from pptx2.util import Inches
25
+
26
+ prs = Presentation()
27
+ slide = prs.slides.add_slide(prs.slide_layouts[6])
28
+
29
+ slide.shapes.add_equation(
30
+ BBox.from_inches(1, 2, 8, 1.5),
31
+ latex=r"\frac{-b \pm \sqrt{b^2-4ac}}{2a}",
32
+ size_pt=28,
33
+ color="#111827",
34
+ align="center",
35
+ )
36
+
37
+ slide.shapes.add_equation(
38
+ Inches(1), Inches(4), Inches(8), Inches(1),
39
+ latex=r"E = mc^2",
40
+ )
41
+ ```
42
+
43
+ The shape is a text box. The equation is editable in PowerPoint's
44
+ equation editor. `display=True` (the default) wraps OMML in
45
+ `m:oMathPara`.
46
+
47
+ Keyword args: `latex` (required), `display`, `font`, `size_pt`,
48
+ `color`, `align`, `anchor`, `margin_pt`.
49
+
50
+ ## Inline equation — `paragraph.add_math`
51
+
52
+ Sits between ordinary runs in the same paragraph:
53
+
54
+ ```python
55
+ box = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(1))
56
+ p = box.text_frame.paragraphs[0]
57
+ p.add_run().text = "Euler's identity "
58
+ p.add_math(r"e^{i\pi} + 1 = 0")
59
+ p.add_run().text = " holds for all real π."
60
+ ```
61
+
62
+ `display=False` (the default here) emits inline `m:oMath`. Pass
63
+ `size_pt` / `color` / `font` to style the math runs.
64
+
65
+ `paragraph.clear()` and assigning `paragraph.text` remove the equation
66
+ along with the runs.
67
+
68
+ ## Bare converter
69
+
70
+ ```python
71
+ from pptx2.math import latex_to_omml
72
+
73
+ omml = latex_to_omml(r"\sum_{i=1}^{n} i")
74
+ # '<m:oMath>…</m:oMath>'
75
+ ```
76
+
77
+ Wrappers (`$…$`, `$$…$$`, `\(…\)`, `\[…\]`, `equation` / `align`
78
+ environments) are stripped before conversion.
79
+
80
+ ## What this is not
81
+
82
+ - Not a TeX engine. Environments like `tikzpicture` will not render.
83
+ - Not Microsoft's `MML2OMML.XSL` (that stylesheet cannot be
84
+ redistributed). The MathML → OMML step is the `mathml2omml` package.
85
+ - Not an image. If you need a PNG of a formula, use
86
+ `add_matplotlib_figure` with mathtext instead.
@@ -0,0 +1,129 @@
1
+ # Picture effects + native SVG (Phase 6)
2
+
3
+ Pictures gain a dedicated `effects` accessor that wraps the OOXML
4
+ `<a:blip>` filters, plus native SVG support with PNG fallback.
5
+
6
+ ## Picture filters
7
+
8
+ ```python
9
+ from pptx2 import Presentation
10
+ from pptx2.util import Inches
11
+ from pptx2.dml.color import RGBColor
12
+
13
+ prs = Presentation()
14
+ slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank
15
+
16
+ pic = slide.shapes.add_picture(
17
+ "hero.jpg", Inches(0), Inches(0),
18
+ width=prs.slide_width, height=prs.slide_height,
19
+ )
20
+
21
+ # Continuous adjustments — all in [-1.0, 1.0] (or [0.0, 1.0] for transparency)
22
+ pic.effects.transparency = 0.3 # 30% see-through
23
+ pic.effects.brightness = 0.10
24
+ pic.effects.contrast = 0.05
25
+ ```
26
+
27
+ ## Recolor presets
28
+
29
+ `recolor` is a **property, not a method** — assign to it. There are
30
+ exactly four modes; anything else raises `ValueError`, and the existing
31
+ effect is left intact because the value is validated before it mutates.
32
+
33
+ ```python
34
+ pic.effects.recolor = "grayscale"
35
+ pic.effects.recolor = "sepia" # a preset duotone under the hood
36
+ pic.effects.recolor = "washout" # PowerPoint's "Washout"
37
+ pic.effects.recolor = "duotone" # neutral grey duotone; see below
38
+ # to pick your own two colours
39
+
40
+ pic.effects.recolor # read it back: the mode, or None
41
+ ```
42
+
43
+ There is no `"black_and_white"` mode. `"washout"` is the closest
44
+ equivalent.
45
+
46
+ ## Duotone
47
+
48
+ ```python
49
+ pic.effects.set_duotone(
50
+ RGBColor(0x12, 0x1E, 0x4D), # shadow color
51
+ "#A8C0FF", # highlight color (hex with or without '#')
52
+ )
53
+
54
+ # Plain RGB tuples are also accepted
55
+ pic.effects.set_duotone((18, 30, 77), (168, 192, 255))
56
+ ```
57
+
58
+ To clear, assign `None` — there is no `clear_recolor()` method:
59
+
60
+ ```python
61
+ pic.effects.recolor = None # drops any duotone / grayscale / etc.
62
+ ```
63
+
64
+ ## Native SVG with PNG fallback
65
+
66
+ `add_svg_picture` embeds both an SVG and a PNG fallback inside the
67
+ same `<a:blip>` so PowerPoint and earlier viewers each render the
68
+ right one.
69
+
70
+ ```python
71
+ # Auto-rasterise via the optional `cairosvg` dependency
72
+ slide.shapes.add_svg_picture("logo.svg", Inches(0.5), Inches(0.5))
73
+
74
+ # Bring your own fallback PNG
75
+ slide.shapes.add_svg_picture(
76
+ "logo.svg",
77
+ Inches(0.5), Inches(0.5),
78
+ width=Inches(1.5), height=Inches(1.5),
79
+ png_fallback="logo.png",
80
+ )
81
+ ```
82
+
83
+ If `cairosvg` isn't installed and you don't pass `png_fallback`, the
84
+ call raises `pptx2._svg.CairoSvgUnavailable` with a clear install hint.
85
+
86
+ The `image/svg+xml` content type is registered with the package so
87
+ SVG parts authored elsewhere round-trip through PowerPoint untouched.
88
+
89
+ ## End-to-end: tinted photo with overlay text
90
+
91
+ ```python
92
+ from pptx2 import Presentation
93
+ from pptx2.util import Inches, Pt
94
+ from pptx2.dml.color import RGBColor
95
+ from pptx2.enum.shapes import MSO_SHAPE
96
+
97
+ prs = Presentation()
98
+ slide = prs.slides.add_slide(prs.slide_layouts[6])
99
+
100
+ # Full-bleed hero image, duotoned to brand colors
101
+ pic = slide.shapes.add_picture(
102
+ "hero.jpg", 0, 0,
103
+ width=prs.slide_width, height=prs.slide_height,
104
+ )
105
+ pic.effects.set_duotone(RGBColor(0x12, 0x1E, 0x4D), "#A8C0FF")
106
+
107
+ # Bottom band with overlay text
108
+ band = slide.shapes.add_shape(
109
+ MSO_SHAPE.RECTANGLE,
110
+ 0, prs.slide_height - Inches(1.5),
111
+ prs.slide_width, Inches(1.5),
112
+ )
113
+ band.fill.solid()
114
+ band.fill.fore_color.rgb = RGBColor(0x12, 0x1E, 0x4D)
115
+ band.fill.fore_color.alpha = 0.55
116
+ band.line.fill.background()
117
+
118
+ box = slide.shapes.add_textbox(
119
+ Inches(0.6), prs.slide_height - Inches(1.2),
120
+ prs.slide_width - Inches(1.2), Inches(0.9),
121
+ )
122
+ p = box.text_frame.paragraphs[0]
123
+ p.text = "Q4 2026"
124
+ p.font.size = Pt(40)
125
+ p.font.bold = True
126
+ p.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
127
+
128
+ prs.save("hero.pptx")
129
+ ```
@@ -0,0 +1,151 @@
1
+ # Slide thumbnails (Phase 10)
2
+
3
+ `pptx2.render` shells out to LibreOffice to rasterise slides as PNGs.
4
+ This is for review tooling, dashboards, and CI artifacts — it does not
5
+ require Microsoft PowerPoint or an Office license, but `soffice` must
6
+ be on `$PATH` (or you can point at a custom binary).
7
+
8
+ ## Convenience methods
9
+
10
+ ```python
11
+ # All slides → ./thumbs/<n>.png
12
+ paths = prs.render_thumbnails(out_dir="thumbs")
13
+
14
+ # Single slide as bytes
15
+ png = slide.render_thumbnail(return_bytes=True)
16
+
17
+ # Single slide written to a specific path
18
+ slide.render_thumbnail(out_path="cover.png")
19
+ ```
20
+
21
+ ## Module-level entry points
22
+
23
+ ```python
24
+ from pptx2.render import (
25
+ render_slide_thumbnails,
26
+ render_slide_thumbnail,
27
+ )
28
+
29
+ paths = render_slide_thumbnails(
30
+ prs,
31
+ out_dir="thumbs",
32
+ slide_indexes=[0, 3, 7], # only these slides
33
+ soffice_bin="/opt/libreoffice/program/soffice",
34
+ timeout=60, # seconds
35
+ )
36
+
37
+ png = render_slide_thumbnail(slide, return_bytes=True)
38
+ ```
39
+
40
+ The output resolution is whatever LibreOffice's headless PNG
41
+ converter chooses — there's no `width=` knob. If you need a specific
42
+ size, post-process with Pillow (``Image.open(...).resize(...)``).
43
+
44
+ ## Pointing at a custom binary
45
+
46
+ Three ways to choose `soffice`, in priority order:
47
+
48
+ 1. The `soffice_bin=` keyword argument
49
+ 2. The `POWER_PPTX_SOFFICE` environment variable
50
+ 3. The first `soffice` (or `libreoffice`) on `$PATH`
51
+
52
+ ```python
53
+ import os
54
+ os.environ["POWER_PPTX_SOFFICE"] = "/opt/libreoffice/program/soffice"
55
+ prs.render_thumbnails(out_dir="thumbs")
56
+ ```
57
+
58
+ ## Known limitations of the LibreOffice thumbnail path
59
+
60
+ The `soffice` + `pdftoppm` pipeline that backs `render_thumbnails`
61
+ renders most decks faithfully, but a few cross-renderer quirks are
62
+ worth knowing about — these show up in the thumbnail even when the
63
+ generated `.pptx` opens correctly in PowerPoint:
64
+
65
+ * **Emoji glyphs render as tofu** on systems without an emoji font
66
+ installed (most Linux runtimes lack one by default). PowerPoint
67
+ uses Segoe UI Emoji as a fallback; LibreOffice headless typically
68
+ does not. For decks meant to be thumbnail-reviewed on the same
69
+ machine, prefer Unicode glyphs from the shipped DejaVu families
70
+ (`•`, `→`, `★`, `‹›`, `■`, etc.) over emoji codepoints.
71
+ * **Un-aligned text in fresh textboxes** renders centered instead of
72
+ left-aligned. A textbox created via `slide.shapes.add_textbox(...)`
73
+ has no `a:pPr/@algn` attribute; PowerPoint treats that as the
74
+ OOXML default (left), LibreOffice as centered. Set
75
+ `paragraph.alignment = PP_ALIGN.LEFT` explicitly to make
76
+ thumbnails match how PowerPoint will render the slide.
77
+ * **`wrap="none" + spAutoFit` (the textbox default) re-centers under
78
+ LibreOffice.** A fresh textbox has `<a:bodyPr wrap="none">
79
+ <a:spAutoFit/></a:bodyPr>`. PowerPoint shrinks the box around the
80
+ declared anchor; LibreOffice re-centers the shrunken box inside
81
+ its original width, so a kicker line declared at
82
+ `left=Inches(1.1), width=Inches(11)` renders near the middle of
83
+ the slide rather than the left edge. Setting `tf.word_wrap = True`
84
+ suppresses `spAutoFit` and keeps the declared geometry intact —
85
+ fold it into every textbox in a recipe-style helper.
86
+
87
+ ## Errors
88
+
89
+ ```python
90
+ from pptx2.render import (
91
+ ThumbnailRendererUnavailable,
92
+ ThumbnailRendererError,
93
+ )
94
+
95
+ try:
96
+ paths = prs.render_thumbnails(out_dir="thumbs")
97
+ except ThumbnailRendererUnavailable as e:
98
+ # soffice not on PATH — message includes an install hint
99
+ print(e)
100
+ except ThumbnailRendererError as e:
101
+ # soffice ran but produced no PNG / exited non-zero / timed out
102
+ print(e)
103
+ ```
104
+
105
+ ## Patterns
106
+
107
+ ### Generate review images for an HTML preview
108
+
109
+ ```python
110
+ import base64
111
+
112
+ prs.save("deck.pptx")
113
+ images = []
114
+ for i in range(len(prs.slides)):
115
+ png = prs.slides[i].render_thumbnail(return_bytes=True)
116
+ images.append(base64.b64encode(png).decode("ascii"))
117
+
118
+ html = "\n".join(
119
+ f'<img src="data:image/png;base64,{b64}" width="640">'
120
+ for b64 in images
121
+ )
122
+ ```
123
+
124
+ ### CI artefacts
125
+
126
+ ```python
127
+ # In tests/conftest.py or similar
128
+ from pathlib import Path
129
+
130
+ def attach_deck_thumbs(prs, out: Path):
131
+ out.mkdir(exist_ok=True)
132
+ return prs.render_thumbnails(out_dir=out)
133
+ ```
134
+
135
+ ### Skip on dev machines without LibreOffice
136
+
137
+ ```python
138
+ import shutil
139
+ import pytest
140
+
141
+ requires_soffice = pytest.mark.skipif(
142
+ shutil.which("soffice") is None and shutil.which("libreoffice") is None,
143
+ reason="LibreOffice not installed",
144
+ )
145
+
146
+ @requires_soffice
147
+ def test_renders_thumbnails(tmp_path):
148
+ prs = build_demo_deck()
149
+ paths = prs.render_thumbnails(out_dir=tmp_path)
150
+ assert len(paths) == len(prs.slides)
151
+ ```