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,75 @@
1
+ # SmartArt text substitution (Phase 8)
2
+
3
+ Full SmartArt creation is intentionally **out of scope** — the layout
4
+ algorithms are proprietary and non-trivial to reverse-engineer.
5
+
6
+ What `python-pptx2` *does* support is text substitution inside an
7
+ *existing* template's SmartArt. The classic use case: a corporate
8
+ org-chart template whose names need refreshing every quarter.
9
+
10
+ ## Iterating SmartArt on a slide
11
+
12
+ ```python
13
+ prs = Presentation("org-chart-template.pptx")
14
+ slide = prs.slides[0]
15
+
16
+ for sa in slide.smart_art:
17
+ print("nodes:", sa.texts)
18
+ ```
19
+
20
+ `slide.smart_art` is a `SmartArtCollection`. Each item is a
21
+ `SmartArtShape` with:
22
+
23
+ - `texts` — ordered list of node text strings
24
+ - `set_text(values, *, strict=True)` — replaces node text in document
25
+ order without touching layout, style, or color parts
26
+
27
+ ## Replacing names
28
+
29
+ ```python
30
+ slide.smart_art[0].set_text(["Alex", "Priya", "Sam", "Lin", "Jordan"])
31
+ ```
32
+
33
+ By default `set_text` is `strict=True` and raises if `len(values)`
34
+ doesn't match the number of nodes in the diagram. Pass
35
+ `strict=False` to truncate / pad with the existing text instead:
36
+
37
+ ```python
38
+ slide.smart_art[0].set_text(["Alex", "Priya"], strict=False)
39
+ ```
40
+
41
+ ## Round-trip
42
+
43
+ `DiagramDataPart` and its sibling part classes are registered so the
44
+ SmartArt `diagrams/data#.xml`, `layout#`, `quickStyle#`, and `colors#`
45
+ parts are handled as typed `XmlPart` subclasses. Reads never mutate.
46
+
47
+ ## What this is not
48
+
49
+ - **No creation.** You can't build a new SmartArt graphic from
50
+ scratch. Author it in PowerPoint as a template, then use this API to
51
+ refresh it.
52
+ - **No structural edits.** Adding/removing nodes is not supported. The
53
+ list you pass to `set_text` must align with the existing nodes.
54
+ - **No styling changes.** Color and quick-style parts are left alone.
55
+
56
+ ## End-to-end: refresh a quarterly org chart
57
+
58
+ ```python
59
+ from pptx2 import Presentation
60
+
61
+ prs = Presentation("org-chart-template.pptx")
62
+ slide = prs.slides[0]
63
+
64
+ names = [
65
+ "Alex Halwell", # CEO
66
+ "Priya Shah", # COO
67
+ "Sam Tucker", # CFO
68
+ "Lin Chen", # CTO
69
+ "Jordan Reyes", # CRO
70
+ "Morgan Patel", # CMO
71
+ ]
72
+ slide.smart_art[0].set_text(names)
73
+
74
+ prs.save("org-chart-2026q2.pptx")
75
+ ```
@@ -0,0 +1,249 @@
1
+ # Space-aware authoring
2
+
3
+ This is the **headline reason `python-pptx2` exists**. Generated decks
4
+ break in two predictable ways:
5
+
6
+ 1. Text overflows its container.
7
+ 2. Boxes sit off the slide.
8
+
9
+ The library gives you three layered tools to prevent both — used in
10
+ this order, they catch ~all real-world cases:
11
+
12
+ 1. **Pre-flight measurement** — choose a font size that fits *before*
13
+ committing the text.
14
+ 2. **Auto-fit on the text frame** — let PowerPoint shrink the font on
15
+ the way down if the text is dynamic.
16
+ 3. **The linter** — catch what slipped through, before save.
17
+
18
+ Use all three. They compose. None of them require Microsoft PowerPoint
19
+ to be installed.
20
+
21
+ ## 1. Pre-flight measurement: pick the right size up front
22
+
23
+ `TextFrame.fit_text(...)` measures with Pillow font metrics and sets
24
+ the largest whole-point font size that fits the box:
25
+
26
+ ```python
27
+ from pptx2.util import Inches, Pt
28
+
29
+ box = slide.shapes.add_textbox(Inches(1), Inches(2), Inches(8), Inches(1.5))
30
+ tf = box.text_frame
31
+ tf.text = dynamic_title
32
+
33
+ # Largest whole-point size ≤ max_size that fits in the box's extents
34
+ tf.fit_text(font_family="Inter", max_size=44, bold=True)
35
+ ```
36
+
37
+ `fit_text` also sets `auto_size = MSO_AUTO_SIZE.NONE`, so PowerPoint
38
+ won't second-guess the size at render time, and returns the point size
39
+ it applied.
40
+
41
+ ### The guarantee is only as good as the font metrics
42
+
43
+ **Read this before trusting `fit_text` with a brand font.** The fit is
44
+ computed from the metrics of the font *on the machine running the
45
+ build*. If the requested family isn't installed — the usual case for a
46
+ display face like Instrument Serif or Inter inside a container or CI
47
+ runner — measurement silently falls back to Pillow's bundled default
48
+ font. You still get a number, and it's usually in the right
49
+ neighbourhood, but it is an **estimate**: a wider real face can still
50
+ overflow the box.
51
+
52
+ python-pptx2 makes that visible rather than silent:
53
+
54
+ ```python
55
+ from pptx2.text.fonts import font_is_installed, installed_font_families
56
+
57
+ font_is_installed("Inter", bold=True) # -> False in most containers
58
+ installed_font_families() # what this machine can actually measure
59
+ ```
60
+
61
+ * Naming a family that isn't installed emits a `FontMetricsWarning`.
62
+ Omitting the argument does not — no particular face was asked for —
63
+ but passing `"Calibri"` explicitly does, because that's a request
64
+ like any other.
65
+ * `strict=True` turns any fallback into a `ValueError`, so a build that
66
+ must be exact fails loudly instead of shipping a guess.
67
+
68
+ Three ways to keep the guarantee, best first:
69
+
70
+ ```python
71
+ # 1. Ship the metrics with the build — exact, works anywhere
72
+ tf.fit_text("Instrument Serif", max_size=44,
73
+ font_file="fonts/InstrumentSerif-Regular.ttf")
74
+
75
+ # 2. Fail the build rather than ship an estimate
76
+ tf.fit_text("Inter", max_size=24, strict=True)
77
+
78
+ # 3. Degrade deliberately to a family you know is present
79
+ family = "Inter" if font_is_installed("Inter") else "DejaVu Sans"
80
+ tf.fit_text(family, max_size=24)
81
+ ```
82
+
83
+ When none of those apply, treat the display-font sizes as
84
+ hand-tunable and verify with a real render
85
+ (`pptx2.render.render_slides`, needs LibreOffice). Note that
86
+ `slide.lint()` is **not** affected — its overflow check uses a
87
+ font-agnostic character-width heuristic — which is why layer 3 below
88
+ still earns its keep when the metrics are approximate.
89
+
90
+ For finer control (e.g. you want to size text *for* a known box but
91
+ leave styling to a recipe), use the underlying fitter directly:
92
+
93
+ ```python
94
+ from pptx2.text.layout import TextFitter
95
+
96
+ best_pt = TextFitter.best_fit_font_size(
97
+ text="Q4 2026 Customer Outcomes Review",
98
+ extents=(Inches(8), Inches(1.5)),
99
+ max_size=44,
100
+ font_file="/usr/share/fonts/truetype/inter/Inter-Bold.ttf",
101
+ )
102
+ ```
103
+
104
+ `best_fit_font_size` returns an int point size; the caller decides what
105
+ to do with it.
106
+
107
+ ## 2. Auto-fit: let PowerPoint shrink at render time
108
+
109
+ When the text isn't fully known at authoring time (or you want
110
+ PowerPoint to adapt as the user edits the deck), set
111
+ `text_frame.auto_size`:
112
+
113
+ ```python
114
+ from pptx2.enum.text import MSO_AUTO_SIZE
115
+
116
+ # Shrink the text to fit
117
+ tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
118
+
119
+ # Or grow the shape to fit the text
120
+ tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT
121
+
122
+ # Or do nothing (the default — overflowing text is just clipped)
123
+ tf.auto_size = MSO_AUTO_SIZE.NONE
124
+ ```
125
+
126
+ `TEXT_TO_FIT_SHAPE` is the right default for headline / KPI / bullet
127
+ cards where the box geometry is fixed and the text is dynamic.
128
+ `SHAPE_TO_FIT_TEXT` is the right default for body copy where the box
129
+ should grow vertically.
130
+
131
+ > ⚠ `auto_size` is rendered by PowerPoint itself — `python-pptx2` only
132
+ > writes the flag. If you want determinism (CI screenshots, PDF
133
+ > export pipelines), prefer `fit_text` so the size is baked into the
134
+ > XML.
135
+
136
+ ## 3. The linter: catch what slipped through
137
+
138
+ Run `slide.lint()` before save. It uses Pillow font metrics so it
139
+ catches overflow even on auto-fit text frames, and it knows the slide's
140
+ real dimensions so off-slide shapes are caught regardless of slide
141
+ size:
142
+
143
+ ```python
144
+ from pptx2.lint import OffSlide, TextOverflow, ShapeCollision
145
+ from pptx2.exc import LintError
146
+
147
+ errors = []
148
+ for slide in prs.slides:
149
+ report = slide.lint()
150
+
151
+ # Cheap auto-fix first (currently nudges off-slide shapes back in)
152
+ report.auto_fix()
153
+
154
+ # Re-collect what's left
155
+ for issue in slide.lint().issues:
156
+ if issue.severity.value == "error":
157
+ errors.append(issue)
158
+
159
+ if errors:
160
+ raise LintError("; ".join(str(e) for e in errors))
161
+
162
+ prs.save("out.pptx")
163
+ ```
164
+
165
+ For decks built through `pptx2.compose.from_spec(...)`, fold the linter
166
+ into the spec itself:
167
+
168
+ ```python
169
+ prs = from_spec({
170
+ "slides": [...],
171
+ "lint": "raise", # also "warn", "off"
172
+ })
173
+ ```
174
+
175
+ ## Putting it together: a robust headline
176
+
177
+ ```python
178
+ from pptx2 import Presentation
179
+ from pptx2.enum.text import MSO_AUTO_SIZE
180
+ from pptx2.util import Inches
181
+
182
+ def add_headline(prs, slide, text):
183
+ box = slide.shapes.add_textbox(
184
+ Inches(0.6), Inches(0.4),
185
+ Inches(prs.slide_width.inches - 1.2), Inches(1.2),
186
+ )
187
+ tf = box.text_frame
188
+ tf.word_wrap = True
189
+ tf.text = text
190
+
191
+ # 1. Pre-flight size pass — bakes a determined size into the XML
192
+ tf.fit_text(font_family="Inter", max_size=44, bold=True)
193
+
194
+ # 2. Belt-and-braces: if the user later types more, PowerPoint
195
+ # will shrink rather than overflow.
196
+ tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
197
+
198
+ return box
199
+
200
+ # 3. Linter as the safety net at save time
201
+ for slide in prs.slides:
202
+ slide.lint().auto_fix()
203
+
204
+ prs.save("headline.pptx")
205
+ ```
206
+
207
+ ## Geometry helpers — never hand-place EMUs
208
+
209
+ Off-slide shapes nearly always come from arithmetic mistakes when
210
+ positioning. Use the design layer's `Grid` and `Stack` instead of
211
+ adding `Inches(...)`s by hand:
212
+
213
+ ```python
214
+ from pptx2.design.layout import Grid, Stack
215
+ from pptx2.util import Pt
216
+
217
+ # 12-column grid with a uniform gutter and outer margin
218
+ grid = Grid(slide, cols=12, rows=6, gutter=Pt(12), margin=Pt(48))
219
+ grid.place(card1, col=0, row=0, col_span=6, row_span=4)
220
+ grid.place(card2, col=6, row=0, col_span=6, row_span=4)
221
+
222
+ # Vertical cursor with a known total width
223
+ stack = Stack(direction="vertical", gap=Pt(8),
224
+ left=Pt(48), top=Pt(48), width=Pt(600))
225
+ stack.place(title, height=Pt(64))
226
+ stack.place(body, height=Pt(280))
227
+ ```
228
+
229
+ Both compute geometry from the slide's actual dimensions, so you can't
230
+ accidentally walk off the right edge — and they're pure arithmetic
231
+ (no XML reads or writes) until `place()` is called.
232
+
233
+ ## Why not just slap `auto_size = SHAPE_TO_FIT_TEXT` on everything?
234
+
235
+ It's tempting, but it fights with the design. A "Customer impact"
236
+ title that grows to two lines pushes the body content down, which
237
+ might collide with a chart, which the linter then flags. The chain
238
+ keeps moving the failure further from the cause.
239
+
240
+ The robust pattern is:
241
+
242
+ - **Fixed geometry, fixed font size** for branded slides where the
243
+ designer made a deliberate choice. Use `fit_text` to *verify* the
244
+ size still fits when content is dynamic.
245
+ - **`TEXT_TO_FIT_SHAPE`** as the catch-all for headlines / KPI cards.
246
+ - **`SHAPE_TO_FIT_TEXT`** only when the slide is a "wall of text" type
247
+ where vertical growth is acceptable.
248
+ - **Linter at the end**, always — it's the only thing that sees the
249
+ *whole* slide instead of one shape at a time.
@@ -0,0 +1,244 @@
1
+ # Tables
2
+
3
+ Most of the table API is unchanged from upstream `python-pptx`. The
4
+ post-fork additions are `cell.format(...)` / `table.format_cells(...)`
5
+ styling, `Cell.borders`, and `Table.fit_to_box` — reach for those before
6
+ dropping to raw fill/font mutation.
7
+
8
+ ## Adding a table
9
+
10
+ ```python
11
+ from pptx2.util import Inches, Pt
12
+ from pptx2.dml.color import RGBColor
13
+
14
+ shape = slide.shapes.add_table(
15
+ rows=4, cols=3,
16
+ left=Inches(1), top=Inches(2),
17
+ width=Inches(8), height=Inches(3),
18
+ )
19
+ table = shape.table
20
+ ```
21
+
22
+ ## Headers and cell text
23
+
24
+ ```python
25
+ HEADERS = ["Metric", "Value", "Δ QoQ"]
26
+ for col, label in enumerate(HEADERS):
27
+ table.cell(0, col).text = label
28
+
29
+ ROWS = [
30
+ ("ARR", "$182M", "+27%"),
31
+ ("NDR", "131%", "+3%"),
32
+ ("CAC payback", "8 mo", "−1 mo"),
33
+ ]
34
+ for r, row in enumerate(ROWS, start=1):
35
+ for c, value in enumerate(row):
36
+ table.cell(r, c).text = value
37
+ ```
38
+
39
+ ## Column widths and row heights
40
+
41
+ ```python
42
+ table.columns[0].width = Inches(3.5)
43
+ table.columns[1].width = Inches(2.5)
44
+ table.columns[2].width = Inches(2.0)
45
+
46
+ table.rows[0].height = Inches(0.6)
47
+ for r in range(1, len(table.rows)):
48
+ table.rows[r].height = Inches(0.5)
49
+ ```
50
+
51
+ ## Styling cells: `format` and `format_cells`
52
+
53
+ `cell.format(...)` sets fill and text styling in one call, using the
54
+ same keyword vocabulary as `slide.shapes.add_text(...)`. Every argument
55
+ is optional and `None` means "leave alone", so calls layer:
56
+
57
+ ```python
58
+ table.cell(0, 0).format(
59
+ fill="#1F2937", # hex / (r, g, b) / RGBColor — or "none"
60
+ color="#FFFFFF", # text colour
61
+ font="Inter",
62
+ size_pt=12,
63
+ bold=True,
64
+ italic=False,
65
+ align="center", # left / center / right / justify
66
+ anchor="middle", # top / middle / bottom
67
+ margin=(2, 8, 2, 8), # points: scalar, or (top, right, bottom, left)
68
+ word_wrap=True,
69
+ )
70
+ ```
71
+
72
+ `table.format_cells(rows=..., cols=..., ...)` applies the same keywords
73
+ across a selection. `rows` / `cols` each accept `None` (all), an `int`
74
+ (negative counts from the end), a `slice`, or any iterable of indices —
75
+ so a whole table's look is a handful of calls:
76
+
77
+ ```python
78
+ table.format_cells(rows=0, fill="#1F2937", color="#FFFFFF", bold=True)
79
+ table.format_cells(rows=slice(1, None), size_pt=11, anchor="middle")
80
+ table.format_cells(rows=range(2, len(table.rows), 2), fill="#F6F7F9") # banding
81
+ table.format_cells(cols=-1, align="right") # numbers
82
+ ```
83
+
84
+ Both return the cell / table, so they chain. Spanned (merged-away)
85
+ cells are skipped; style the merge origin instead.
86
+
87
+ Either order works — style an empty header row and then assign
88
+ `cell.text`, or populate first and style afterwards. The formatting is
89
+ recorded as the cell's text-body defaults (`<a:lstStyle>`) as well as on
90
+ its current runs, and a text replacement leaves those defaults alone.
91
+
92
+ > A cell's vertical anchor and insets live on `<a:tcPr>`, not on its
93
+ > text frame's `<a:bodyPr>` — PowerPoint reads the cell properties and
94
+ > ignores the body ones. `format(anchor=..., margin=...)` writes them to
95
+ > the right place; setting `cell.text_frame.vertical_anchor` does not.
96
+
97
+ The low-level surface is still there when you need it
98
+ (`cell.fill.solid()`, `cell.vertical_anchor = MSO_VERTICAL_ANCHOR.MIDDLE`,
99
+ per-run `font` objects) — `format` just removes the loop.
100
+
101
+ ## Built-in banding and header toggles
102
+
103
+ Before hand-styling every cell, check whether the table style already
104
+ does it. These six booleans drive the banding and emphasis that the
105
+ applied table style defines, so one flag replaces a loop:
106
+
107
+ ```python
108
+ tbl.first_row = True # emphasise the header row
109
+ tbl.last_row = False # emphasise a totals row
110
+ tbl.first_col = True # emphasise the label column
111
+ tbl.last_col = False
112
+ tbl.horz_banding = True # alternating row shading
113
+ tbl.vert_banding = False # alternating column shading
114
+ ```
115
+
116
+ `banded_rows` / `banded_cols` are aliases for the two banding flags.
117
+ They only have a visible effect when the table still carries a style
118
+ that defines banded formatting — see the next section.
119
+
120
+ ## Walking every cell
121
+
122
+ `iter_cells()` flattens the grid so you don't nest two loops, and it
123
+ skips nothing:
124
+
125
+ ```python
126
+ for cell in tbl.iter_cells():
127
+ cell.margin_left = Inches(0.08) # also margin_right/top/bottom
128
+ ```
129
+
130
+ Merged regions need care when reading: a merged block reports one
131
+ *origin* cell plus the cells it swallowed.
132
+
133
+ ```python
134
+ cell = tbl.cell(0, 0)
135
+ cell.is_merge_origin # True on the top-left cell of a merged block
136
+ cell.is_spanned # True on the cells the merge absorbed
137
+ cell.span_height # rows covered (1 when unmerged)
138
+ cell.span_width # columns covered
139
+ ```
140
+
141
+ Write text to the *origin* cell; a spanned cell's text is not rendered.
142
+
143
+ ## Detaching the default table style
144
+
145
+ Every table created via `slide.shapes.add_table(...)` is born with
146
+ the "Medium Style 2 — Accent 1" `tableStyleId` attached. The style
147
+ ships a banded-row overlay that PowerPoint and LibreOffice render
148
+ on top of any per-cell fills you set — and that overlay survives
149
+ `table.horz_banding = False` and `table.first_row = False`, because
150
+ those flags only suppress `bandRow` / `firstRow` markup, not the
151
+ style's own banding rules.
152
+
153
+ When you want full control of every cell's appearance, detach the
154
+ default style outright:
155
+
156
+ ```python
157
+ table.clear_style() # drops <a:tableStyleId>
158
+ table.format_cells(fill="#FFFFFF") # now every cell is yours
159
+ ```
160
+
161
+ ## Cell borders (Phase 4 — post-fork addition)
162
+
163
+ `cell.borders` exposes per-edge `LineFormat` proxies plus convenience
164
+ helpers. Backed by the OOXML `a:lnL/lnR/lnT/lnB/lnTlToBr/lnBlToTr`
165
+ children of `a:tcPr`.
166
+
167
+ ### Per-edge
168
+
169
+ ```python
170
+ cell.borders.left.color.rgb = RGBColor(0xE5, 0xE7, 0xEB)
171
+ cell.borders.left.width = Pt(0.5)
172
+ cell.borders.bottom.color.rgb = RGBColor(0x1F, 0x29, 0x37)
173
+ cell.borders.bottom.width = Pt(1.5)
174
+ cell.borders.diagonal_down.color.rgb = RGBColor(0xEF, 0x44, 0x44)
175
+ ```
176
+
177
+ ### All edges in one call
178
+
179
+ ```python
180
+ cell.borders.all(width=Pt(0.5), color=RGBColor(0xE5, 0xE7, 0xEB))
181
+ cell.borders.outer(width=Pt(1.0), color=RGBColor(0x1F, 0x29, 0x37))
182
+ cell.borders.none() # clears every edge
183
+ ```
184
+
185
+ ### Zebra-striped borders pattern
186
+
187
+ ```python
188
+ LIGHT = RGBColor(0xE5, 0xE7, 0xEB)
189
+ DARK = RGBColor(0x1F, 0x29, 0x37)
190
+
191
+ # Header row — bottom edge dark
192
+ for col in range(len(HEADERS)):
193
+ table.cell(0, col).borders.bottom.color.rgb = DARK
194
+ table.cell(0, col).borders.bottom.width = Pt(1.5)
195
+
196
+ # Body rows — light row separator
197
+ for r in range(1, len(table.rows)):
198
+ for c in range(len(HEADERS)):
199
+ cell = table.cell(r, c)
200
+ cell.borders.bottom.color.rgb = LIGHT
201
+ cell.borders.bottom.width = Pt(0.5)
202
+ ```
203
+
204
+ ## Reading borders
205
+
206
+ Reads on an unset edge return a `LineFormat` without mutating the XML.
207
+ Be careful with the falsy check: an unset `width` reads back as `Emu(0)`,
208
+ **not** `None`, so `is None` never fires:
209
+
210
+ ```python
211
+ if not cell.borders.bottom.width: # unset reads Emu(0), never None
212
+ print("inherits border from style")
213
+ ```
214
+
215
+ ## Rotated / stacked cell text
216
+
217
+ Use `cell.text_direction` for matrix-style or rotated column headers:
218
+
219
+ ```python
220
+ cell.text_direction = "rotate90" # "horizontal" (default), "rotate90",
221
+ cell.text_direction = "stacked" # "rotate270", "stacked"
222
+ cell.vertical_anchor = MSO_ANCHOR.MIDDLE # t / ctr / b within the cell
223
+ ```
224
+
225
+ Reading returns the friendly string (`"horizontal"` when unset); assigning
226
+ `"horizontal"` or `None` clears it. Maps to `<a:tcPr vert="…">` /
227
+ `anchor="…"` — schema-valid and round-trip clean.
228
+
229
+ ## Built-in table styles
230
+
231
+ Apply any of PowerPoint's ~70 built-in table styles by name or GUID:
232
+
233
+ ```python
234
+ table.style = "Medium Style 2 - Accent 1" # friendly name
235
+ table.style = "Table Grid"
236
+ table.style = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}" # raw GUID also OK
237
+ print(table.style) # -> friendly name (or raw GUID / None)
238
+ table.style = None # detach (same as table.clear_style())
239
+ ```
240
+
241
+ Discover valid names via `from pptx2.table_styles import TABLE_STYLES`.
242
+ An unknown name raises `ValueError` with a "did you mean" suggestion.
243
+ Writing just the style GUID is schema-valid — nothing is added to
244
+ `tableStyles.xml`.
@@ -0,0 +1,127 @@
1
+ # Themes (Phase 6 + 7)
2
+
3
+ `Presentation.theme` returns a `Theme` proxy that's both readable and
4
+ writable. Theme parts are loaded as a typed `ThemePart(XmlPart)` so
5
+ writes round-trip on save.
6
+
7
+ ## Reading the palette
8
+
9
+ ```python
10
+ from pptx2.enum.dml import MSO_THEME_COLOR
11
+
12
+ accent1 = prs.theme.colors[MSO_THEME_COLOR.ACCENT_1] # → RGBColor
13
+ accent2 = prs.theme.colors[MSO_THEME_COLOR.ACCENT_2]
14
+ bg1 = prs.theme.colors[MSO_THEME_COLOR.BACKGROUND_1] # canonical lt1
15
+ text1 = prs.theme.colors[MSO_THEME_COLOR.TEXT_1] # canonical dk1
16
+ hyper = prs.theme.colors[MSO_THEME_COLOR.HYPERLINK]
17
+ follow = prs.theme.colors[MSO_THEME_COLOR.FOLLOWED_HYPERLINK]
18
+ ```
19
+
20
+ The six accent slots, the dk1/dk2/lt1/lt2 background slots, and the
21
+ hyperlink slots are addressable.
22
+
23
+ ## Reading fonts
24
+
25
+ ```python
26
+ major = prs.theme.fonts.major # heading font (str)
27
+ minor = prs.theme.fonts.minor # body font (str)
28
+ ```
29
+
30
+ ## Writing the palette
31
+
32
+ ```python
33
+ from pptx2.dml.color import RGBColor
34
+
35
+ prs.theme.colors[MSO_THEME_COLOR.ACCENT_1] = RGBColor(0x4F, 0x9D, 0xFF)
36
+ prs.theme.colors[MSO_THEME_COLOR.ACCENT_2] = RGBColor(0x10, 0xB9, 0x81)
37
+ ```
38
+
39
+ Alias slots (`BACKGROUND_1` / `BACKGROUND_2` / `TEXT_1` / `TEXT_2`)
40
+ resolve to their canonical `lt1` / `lt2` / `dk1` / `dk2` target.
41
+
42
+ ## Writing fonts
43
+
44
+ ```python
45
+ prs.theme.fonts.major = "Inter"
46
+ prs.theme.fonts.minor = "Inter"
47
+ ```
48
+
49
+ Rewrites the `<a:majorFont>/<a:minorFont>/<a:latin typeface=…/>`
50
+ typeface.
51
+
52
+ ## Bulk-copy from another theme
53
+
54
+ ```python
55
+ brand = Presentation("brand.potx")
56
+ prs.theme.apply(brand.theme) # copies palette + major/minor fonts
57
+ ```
58
+
59
+ ## Theme-aware color resolution
60
+
61
+ `pptx2.inherit.resolve_color` returns the effective `RGBColor` for any
62
+ `ColorFormat` (or the lazy proxy on `Font.color` / `LineFormat.color`).
63
+ Explicit RGB values are returned as-is, scheme colors resolve through
64
+ the theme, and unset colors return `None` without mutating XML:
65
+
66
+ ```python
67
+ from pptx2.inherit import resolve_color
68
+
69
+ rgb = resolve_color(run.font.color, theme=prs.theme)
70
+ if rgb is None:
71
+ print("inherits from layout/master")
72
+ else:
73
+ print("effective color:", rgb)
74
+ ```
75
+
76
+ `brightness` is honoured by blending toward white or black, mirroring
77
+ PowerPoint's `lumMod` / `lumOff` model.
78
+
79
+ > ⚠ Full placeholder-walking (`slide → layout → master`) is *not*
80
+ > implemented; this resolver covers the 80% case (theme-color lookup)
81
+ > without touching XML.
82
+
83
+ ## End-to-end: rebrand a deck
84
+
85
+ ```python
86
+ from pptx2 import Presentation
87
+ from pptx2.dml.color import RGBColor
88
+ from pptx2.enum.dml import MSO_THEME_COLOR
89
+
90
+ prs = Presentation("input.pptx")
91
+
92
+ # Punchy palette
93
+ prs.theme.colors[MSO_THEME_COLOR.ACCENT_1] = RGBColor(0xFF, 0x66, 0x00)
94
+ prs.theme.colors[MSO_THEME_COLOR.ACCENT_2] = RGBColor(0x12, 0x1E, 0x4D)
95
+ prs.theme.colors[MSO_THEME_COLOR.HYPERLINK] = RGBColor(0x12, 0x1E, 0x4D)
96
+
97
+ # Inter everywhere
98
+ prs.theme.fonts.major = "Inter"
99
+ prs.theme.fonts.minor = "Inter"
100
+
101
+ prs.save("rebranded.pptx")
102
+ ```
103
+
104
+ Anything in the deck that referenced `accent1` / `accent2` /
105
+ `majorFont` / `minorFont` will pick up the new values automatically.
106
+
107
+ ## Dark mode & palette-from-seed
108
+
109
+ Flip a deck to a dark palette in one call (backgrounds/text invert,
110
+ accents stay AA-legible):
111
+
112
+ ```python
113
+ prs.theme.to_dark_mode() # in place; returns the Theme
114
+ prs.theme.to_dark_mode(min_contrast=7.0) # AAA accents
115
+ ```
116
+
117
+ Bootstrap a whole palette from one brand colour:
118
+
119
+ ```python
120
+ from pptx2.design.tokens import DesignTokens
121
+
122
+ tokens = DesignTokens.from_seed("#3B5BDB", harmony="triadic")
123
+ tokens.palette["primary"] # == the seed
124
+ tokens.validate_color_blindness("deuteranopia") # -> [(name_a, name_b), ...]
125
+ ```
126
+
127
+ `from_seed` is deterministic and accepts hex / `RGBColor` / `(r, g, b)`.