python-pptx2 2.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pptx2/__init__.py +152 -0
- pptx2/_color.py +75 -0
- pptx2/_slide_importer.py +597 -0
- pptx2/_svg.py +155 -0
- pptx2/_template_applier.py +292 -0
- pptx2/_textstyle.py +187 -0
- pptx2/accessibility.py +365 -0
- pptx2/action.py +270 -0
- pptx2/animation.py +2237 -0
- pptx2/api.py +49 -0
- pptx2/audit.py +258 -0
- pptx2/chart/__init__.py +0 -0
- pptx2/chart/analytics.py +381 -0
- pptx2/chart/axis.py +543 -0
- pptx2/chart/category.py +200 -0
- pptx2/chart/chart.py +670 -0
- pptx2/chart/data.py +864 -0
- pptx2/chart/datalabel.py +406 -0
- pptx2/chart/legend.py +86 -0
- pptx2/chart/marker.py +70 -0
- pptx2/chart/palettes.py +129 -0
- pptx2/chart/plot.py +462 -0
- pptx2/chart/point.py +101 -0
- pptx2/chart/quick_layouts.py +325 -0
- pptx2/chart/series.py +334 -0
- pptx2/chart/xlsx.py +272 -0
- pptx2/chart/xmlwriter.py +1845 -0
- pptx2/compose/__init__.py +28 -0
- pptx2/compose/from_spec.py +1094 -0
- pptx2/design/__init__.py +8 -0
- pptx2/design/components.py +607 -0
- pptx2/design/figures.py +389 -0
- pptx2/design/layout.py +370 -0
- pptx2/design/recipes.py +1967 -0
- pptx2/design/style.py +209 -0
- pptx2/design/tokens.py +915 -0
- pptx2/diagrams.py +754 -0
- pptx2/dml/__init__.py +0 -0
- pptx2/dml/chtfmt.py +40 -0
- pptx2/dml/color.py +496 -0
- pptx2/dml/effect.py +909 -0
- pptx2/dml/fill.py +691 -0
- pptx2/dml/line.py +287 -0
- pptx2/dml/picture.py +212 -0
- pptx2/dml/three_d.py +381 -0
- pptx2/enum/__init__.py +0 -0
- pptx2/enum/action.py +71 -0
- pptx2/enum/animation.py +31 -0
- pptx2/enum/base.py +218 -0
- pptx2/enum/chart.py +574 -0
- pptx2/enum/dml.py +740 -0
- pptx2/enum/lang.py +685 -0
- pptx2/enum/presentation.py +133 -0
- pptx2/enum/shapes.py +1029 -0
- pptx2/enum/text.py +230 -0
- pptx2/exc.py +42 -0
- pptx2/formats.py +139 -0
- pptx2/geometry.py +420 -0
- pptx2/inherit.py +109 -0
- pptx2/lint.py +2256 -0
- pptx2/math.py +177 -0
- pptx2/media.py +197 -0
- pptx2/opc/__init__.py +0 -0
- pptx2/opc/constants.py +332 -0
- pptx2/opc/oxml.py +188 -0
- pptx2/opc/package.py +762 -0
- pptx2/opc/packuri.py +109 -0
- pptx2/opc/serialized.py +296 -0
- pptx2/opc/shared.py +20 -0
- pptx2/opc/spec.py +45 -0
- pptx2/oxml/__init__.py +555 -0
- pptx2/oxml/action.py +53 -0
- pptx2/oxml/chart/__init__.py +0 -0
- pptx2/oxml/chart/axis.py +337 -0
- pptx2/oxml/chart/chart.py +481 -0
- pptx2/oxml/chart/datalabel.py +253 -0
- pptx2/oxml/chart/legend.py +72 -0
- pptx2/oxml/chart/marker.py +61 -0
- pptx2/oxml/chart/plot.py +365 -0
- pptx2/oxml/chart/series.py +425 -0
- pptx2/oxml/chart/shared.py +220 -0
- pptx2/oxml/coreprops.py +288 -0
- pptx2/oxml/dml/__init__.py +0 -0
- pptx2/oxml/dml/color.py +135 -0
- pptx2/oxml/dml/effect.py +213 -0
- pptx2/oxml/dml/fill.py +316 -0
- pptx2/oxml/dml/line.py +12 -0
- pptx2/oxml/dml/three_d.py +110 -0
- pptx2/oxml/ns.py +135 -0
- pptx2/oxml/presentation.py +313 -0
- pptx2/oxml/shapes/__init__.py +19 -0
- pptx2/oxml/shapes/autoshape.py +467 -0
- pptx2/oxml/shapes/connector.py +107 -0
- pptx2/oxml/shapes/graphfrm.py +347 -0
- pptx2/oxml/shapes/groupshape.py +329 -0
- pptx2/oxml/shapes/picture.py +270 -0
- pptx2/oxml/shapes/shared.py +577 -0
- pptx2/oxml/simpletypes.py +1027 -0
- pptx2/oxml/slide.py +563 -0
- pptx2/oxml/table.py +650 -0
- pptx2/oxml/text.py +815 -0
- pptx2/oxml/theme.py +36 -0
- pptx2/oxml/xmlchemy.py +717 -0
- pptx2/package.py +222 -0
- pptx2/parts/__init__.py +0 -0
- pptx2/parts/chart.py +95 -0
- pptx2/parts/coreprops.py +167 -0
- pptx2/parts/diagram.py +37 -0
- pptx2/parts/embeddedpackage.py +93 -0
- pptx2/parts/image.py +275 -0
- pptx2/parts/media.py +37 -0
- pptx2/parts/presentation.py +136 -0
- pptx2/parts/slide.py +371 -0
- pptx2/presentation.py +408 -0
- pptx2/py.typed +0 -0
- pptx2/render.py +586 -0
- pptx2/section.py +272 -0
- pptx2/shapes/__init__.py +26 -0
- pptx2/shapes/autoshape.py +442 -0
- pptx2/shapes/base.py +1078 -0
- pptx2/shapes/connector.py +297 -0
- pptx2/shapes/freeform.py +337 -0
- pptx2/shapes/graphfrm.py +316 -0
- pptx2/shapes/group.py +264 -0
- pptx2/shapes/picture.py +422 -0
- pptx2/shapes/placeholder.py +468 -0
- pptx2/shapes/shapetree.py +2027 -0
- pptx2/shared.py +82 -0
- pptx2/skill/SKILL.md +450 -0
- pptx2/skill/__init__.py +78 -0
- pptx2/skill/__main__.py +64 -0
- pptx2/skill/references/animations.md +189 -0
- pptx2/skill/references/basics.md +421 -0
- pptx2/skill/references/charts.md +254 -0
- pptx2/skill/references/compose.md +234 -0
- pptx2/skill/references/design.md +366 -0
- pptx2/skill/references/effects.md +249 -0
- pptx2/skill/references/end-to-end-deck.md +231 -0
- pptx2/skill/references/geometry-and-arrows.md +334 -0
- pptx2/skill/references/lint.md +275 -0
- pptx2/skill/references/math.md +86 -0
- pptx2/skill/references/picture-effects.md +129 -0
- pptx2/skill/references/render.md +151 -0
- pptx2/skill/references/smart-art.md +75 -0
- pptx2/skill/references/space-aware-authoring.md +249 -0
- pptx2/skill/references/tables.md +244 -0
- pptx2/skill/references/theme.md +127 -0
- pptx2/skill/references/three-d.md +109 -0
- pptx2/skill/references/transitions.md +100 -0
- pptx2/slide.py +1244 -0
- pptx2/smart_art.py +220 -0
- pptx2/spec.py +633 -0
- pptx2/table.py +1181 -0
- pptx2/table_styles.py +184 -0
- pptx2/templates/default.pptx +0 -0
- pptx2/templates/docx-icon.emf +0 -0
- pptx2/templates/generic-icon.emf +0 -0
- pptx2/templates/notes.xml +23 -0
- pptx2/templates/notesMaster.xml +352 -0
- pptx2/templates/pptx-icon.emf +0 -0
- pptx2/templates/theme.xml +321 -0
- pptx2/templates/xlsx-icon.emf +0 -0
- pptx2/text/__init__.py +0 -0
- pptx2/text/fonts.py +482 -0
- pptx2/text/layout.py +374 -0
- pptx2/text/text.py +1272 -0
- pptx2/theme.py +721 -0
- pptx2/types.py +36 -0
- pptx2/util.py +263 -0
- python_pptx2-2.13.0.dist-info/METADATA +351 -0
- python_pptx2-2.13.0.dist-info/RECORD +175 -0
- python_pptx2-2.13.0.dist-info/WHEEL +5 -0
- python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
- python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
- python_pptx2-2.13.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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)`.
|