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,189 @@
|
|
|
1
|
+
# Animations (Phase 5)
|
|
2
|
+
|
|
3
|
+
`pptx2.animation` ships a preset-only API that maps directly onto
|
|
4
|
+
PowerPoint's built-in animation library. All generated XML is valid
|
|
5
|
+
OOXML and round-trips through PowerPoint without loss.
|
|
6
|
+
|
|
7
|
+
## Imports
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from pptx2.animation import Entrance, Exit, Emphasis, MotionPath, Trigger
|
|
11
|
+
from pptx2.util import Inches, Pt
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`Trigger` is an alias for `pptx2.enum.animation.PP_ANIM_TRIGGER`.
|
|
15
|
+
|
|
16
|
+
## Triggers and delay
|
|
17
|
+
|
|
18
|
+
Every preset accepts an optional `trigger` and `delay` (milliseconds):
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
Entrance.fade(slide, shape) # default: ON_CLICK
|
|
22
|
+
Entrance.fly_in(slide, shape, trigger=Trigger.WITH_PREVIOUS)
|
|
23
|
+
Entrance.zoom(slide, shape, trigger=Trigger.AFTER_PREVIOUS, delay=500)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Entrance presets
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
Entrance.appear(slide, shape)
|
|
30
|
+
Entrance.fade(slide, shape)
|
|
31
|
+
Entrance.fly_in(slide, shape, direction="bottom") # also "top", "left", "right"
|
|
32
|
+
Entrance.float_in(slide, shape)
|
|
33
|
+
Entrance.wipe(slide, shape)
|
|
34
|
+
Entrance.zoom(slide, shape)
|
|
35
|
+
Entrance.wheel(slide, shape)
|
|
36
|
+
Entrance.random_bars(slide, shape)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Exit presets
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
Exit.disappear(slide, shape)
|
|
43
|
+
Exit.fade(slide, shape)
|
|
44
|
+
Exit.fly_out(slide, shape)
|
|
45
|
+
Exit.float_out(slide, shape)
|
|
46
|
+
Exit.wipe(slide, shape)
|
|
47
|
+
Exit.zoom(slide, shape)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Emphasis presets
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
Emphasis.pulse(slide, shape)
|
|
54
|
+
Emphasis.spin(slide, shape)
|
|
55
|
+
Emphasis.teeter(slide, shape)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Per-paragraph reveal
|
|
59
|
+
|
|
60
|
+
Reveal a text frame one paragraph at a time, fired by a single click:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
body = slide.placeholders[1].text_frame
|
|
64
|
+
Entrance.fade(slide, body, by_paragraph=True)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Supported presets for `by_paragraph=True`: `appear`, `fade`, `wipe`,
|
|
68
|
+
`zoom`, `wheel`, `random_bars`. The first paragraph fires on the
|
|
69
|
+
caller-supplied trigger (or `ON_CLICK`); subsequent paragraphs default
|
|
70
|
+
to `Trigger.AFTER_PREVIOUS`.
|
|
71
|
+
|
|
72
|
+
## Sequencing — chain effects from one click
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
with slide.animations.sequence():
|
|
76
|
+
Entrance.fade(slide, title_shape)
|
|
77
|
+
Entrance.fly_in(slide, body_shape)
|
|
78
|
+
Emphasis.pulse(slide, badge_shape)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Inside the `with` block:
|
|
82
|
+
- The first effect fires on `Trigger.ON_CLICK` (or whatever `start=` is
|
|
83
|
+
passed to `sequence(start=...)`).
|
|
84
|
+
- Every subsequent effect defaults to `Trigger.AFTER_PREVIOUS`.
|
|
85
|
+
- Explicit per-call triggers still win.
|
|
86
|
+
|
|
87
|
+
Sequences cannot be nested.
|
|
88
|
+
|
|
89
|
+
## Grouping — animate sub-shapes as one cluster
|
|
90
|
+
|
|
91
|
+
`group()` is the right primitive for "this card / row / panel made of
|
|
92
|
+
several sub-shapes should animate as one visual unit." Inside the
|
|
93
|
+
block, the first effect uses the group's `start` trigger and every
|
|
94
|
+
subsequent effect defaults to `Trigger.WITH_PREVIOUS`:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
for i, card in enumerate(cards):
|
|
98
|
+
with slide.animations.group(delay=0 if i == 0 else 200):
|
|
99
|
+
Entrance.fade(slide, card.body)
|
|
100
|
+
Entrance.fade(slide, card.title)
|
|
101
|
+
Entrance.fade(slide, card.blurb)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This produces one `AFTER_PREVIOUS` start plus N-1 `WITH_PREVIOUS`
|
|
105
|
+
followers per card — much cheaper for PowerPoint to render than the
|
|
106
|
+
same number of independent click-groups, and dramatically reduces the
|
|
107
|
+
"slide takes a beat to appear" lag on dense decks.
|
|
108
|
+
|
|
109
|
+
Use `group()` for sub-shape clusters; use `sequence()` for cluster-to-
|
|
110
|
+
cluster chains. They can't be combined.
|
|
111
|
+
|
|
112
|
+
## Inspecting and clearing animations
|
|
113
|
+
|
|
114
|
+
`SlideAnimations` is now iterable and supports `len()`:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
print(len(slide.animations)) # how many entries?
|
|
118
|
+
for entry in slide.animations:
|
|
119
|
+
print(entry.kind, entry.preset, entry.trigger,
|
|
120
|
+
"shape=", entry.shape.name if entry.shape else None,
|
|
121
|
+
"duration=", entry.duration, "delay=", entry.delay)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Each entry is a read-only `AnimationEntry`. To remove the entry from
|
|
125
|
+
the slide call `entry.remove()`; to wipe the whole slide call
|
|
126
|
+
`slide.animations.clear()` (returns the count of entries removed).
|
|
127
|
+
|
|
128
|
+
`slide.animations.purge_orphans()` is the narrower cousin — it only
|
|
129
|
+
removes entries whose target shape has been deleted.
|
|
130
|
+
|
|
131
|
+
## Motion paths
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
MotionPath.line(slide, shape, dx=Inches(2), dy=Inches(1))
|
|
135
|
+
MotionPath.diagonal(slide, shape, dx=Inches(3), dy=Inches(2))
|
|
136
|
+
MotionPath.circle(slide, shape, radius=Inches(1), clockwise=True)
|
|
137
|
+
MotionPath.arc(slide, shape, dx=Inches(3), dy=Inches(0), height=0.4)
|
|
138
|
+
MotionPath.zigzag(slide, shape, dx=Inches(4), dy=Inches(0),
|
|
139
|
+
segments=6, amplitude=0.2)
|
|
140
|
+
MotionPath.spiral(slide, shape, radius=Inches(2),
|
|
141
|
+
turns=2.5, clockwise=True)
|
|
142
|
+
|
|
143
|
+
# Pass a raw OOXML motion-path expression
|
|
144
|
+
MotionPath.custom(slide, shape, "M 0 0 L 0.5 0.5 L 1 0")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
All preset constructors normalise EMU inputs against the slide
|
|
148
|
+
dimensions before emitting the path attribute, so the *absolute* travel
|
|
149
|
+
distance is preserved across slide sizes.
|
|
150
|
+
|
|
151
|
+
## Round-trip safety
|
|
152
|
+
|
|
153
|
+
Animations authored in PowerPoint survive a read–modify–write cycle.
|
|
154
|
+
Generated effects are appended to the existing `<p:tnLst>` timing tree
|
|
155
|
+
without touching pre-existing `<p:par>` nodes:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
prs = Presentation("hand-authored.pptx")
|
|
159
|
+
slide = prs.slides[0]
|
|
160
|
+
Entrance.fade(slide, slide.shapes[0]) # adds, doesn't disturb
|
|
161
|
+
prs.save("with-extra-fade.pptx")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## End-to-end example
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from pptx2 import Presentation
|
|
168
|
+
from pptx2.animation import Entrance, Emphasis, MotionPath, Trigger
|
|
169
|
+
from pptx2.util import Inches
|
|
170
|
+
|
|
171
|
+
prs = Presentation()
|
|
172
|
+
slide = prs.slides.add_slide(prs.slide_layouts[5])
|
|
173
|
+
slide.shapes.title.text = "Animated demo"
|
|
174
|
+
|
|
175
|
+
box = slide.shapes.add_textbox(Inches(1), Inches(2), Inches(8), Inches(1))
|
|
176
|
+
box.text_frame.text = "Click anywhere to animate"
|
|
177
|
+
|
|
178
|
+
# A 3-step click-driven sequence
|
|
179
|
+
with slide.animations.sequence():
|
|
180
|
+
Entrance.fade(slide, slide.shapes.title)
|
|
181
|
+
Entrance.fly_in(slide, box, direction="left")
|
|
182
|
+
Emphasis.pulse(slide, box)
|
|
183
|
+
|
|
184
|
+
# Extra effect after the sequence: a motion path on the box
|
|
185
|
+
MotionPath.arc(slide, box, dx=Inches(2), dy=Inches(0), height=0.3,
|
|
186
|
+
trigger=Trigger.AFTER_PREVIOUS)
|
|
187
|
+
|
|
188
|
+
prs.save("animated.pptx")
|
|
189
|
+
```
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# Basics — the inherited 1.0.2 surface
|
|
2
|
+
|
|
3
|
+
Everything in this file works the same as upstream `python-pptx 1.0.2`.
|
|
4
|
+
It's here so you don't have to leave the skill for boring boilerplate.
|
|
5
|
+
|
|
6
|
+
## Open / create / save
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from pptx2 import Presentation
|
|
10
|
+
|
|
11
|
+
prs = Presentation() # blank deck, default 16:9
|
|
12
|
+
prs = Presentation("template.pptx") # open existing
|
|
13
|
+
prs.save("out.pptx")
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`Presentation(...)` also accepts a binary file-like object — useful for
|
|
17
|
+
HTTP responses or in-memory generation:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import io
|
|
21
|
+
buf = io.BytesIO()
|
|
22
|
+
prs.save(buf)
|
|
23
|
+
buf.seek(0)
|
|
24
|
+
return buf.getvalue()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Slide size
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from pptx2.util import Inches
|
|
31
|
+
|
|
32
|
+
prs.slide_width = Inches(13.333) # 16:9 widescreen
|
|
33
|
+
prs.slide_height = Inches(7.5)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Adding slides
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
title_layout = prs.slide_layouts[0] # 0 = Title, 1 = Title+Content,
|
|
40
|
+
blank_layout = prs.slide_layouts[6] # 5 = Title only, 6 = Blank, ...
|
|
41
|
+
|
|
42
|
+
slide = prs.slides.add_slide(title_layout)
|
|
43
|
+
slide.shapes.title.text = "Q4 Review"
|
|
44
|
+
slide.placeholders[1].text = "April 2026"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Layouts are master-dependent; use `prs.slide_master.slide_layouts` if you
|
|
48
|
+
want to be explicit, or iterate `for L in prs.slide_layouts: print(L.name)`
|
|
49
|
+
to discover what the template ships.
|
|
50
|
+
|
|
51
|
+
## Text boxes
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from pptx2.util import Inches, Pt
|
|
55
|
+
from pptx2.dml.color import RGBColor
|
|
56
|
+
|
|
57
|
+
box = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(1))
|
|
58
|
+
tf = box.text_frame
|
|
59
|
+
tf.word_wrap = True
|
|
60
|
+
|
|
61
|
+
p = tf.paragraphs[0]
|
|
62
|
+
p.text = "Hello world"
|
|
63
|
+
p.font.name = "Inter"
|
|
64
|
+
p.font.size = Pt(36)
|
|
65
|
+
p.font.bold = True
|
|
66
|
+
p.font.color.rgb = RGBColor(0x1F, 0x29, 0x37)
|
|
67
|
+
|
|
68
|
+
p2 = tf.add_paragraph()
|
|
69
|
+
p2.text = "Subtitle goes here"
|
|
70
|
+
p2.font.size = Pt(18)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### One call instead of per-run styling
|
|
74
|
+
|
|
75
|
+
Setting `font.*` on every paragraph is the single most common source of
|
|
76
|
+
bloated deck-building code, and it silently misses runs you didn't
|
|
77
|
+
enumerate. `set_paragraph_defaults` applies to *every* paragraph and run
|
|
78
|
+
in the frame, including ones added later in the same breath:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
tf.text = "Headline"
|
|
82
|
+
tf.add_paragraph().text = "Supporting line"
|
|
83
|
+
|
|
84
|
+
tf.set_paragraph_defaults(
|
|
85
|
+
font_name="Inter", size=Pt(14), bold=True, color="#333333",
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Accepted kwargs are exactly `font_name`, `size`, `bold`, `italic`,
|
|
90
|
+
`color` — all keyword-only, all optional. Note it does **not** take
|
|
91
|
+
spacing arguments; `space_before` / `space_after` / `line_spacing` are
|
|
92
|
+
per-paragraph (below).
|
|
93
|
+
|
|
94
|
+
### Spacing and margins
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
p.space_before = Pt(6)
|
|
98
|
+
p.space_after = Pt(6)
|
|
99
|
+
p.line_spacing = 1.2 # multiple, or Pt(20) for exact
|
|
100
|
+
|
|
101
|
+
tf.margin_left = Inches(0.2) # also margin_right/top/bottom
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Paragraphs can also be built run-by-run when you need mixed styling in
|
|
105
|
+
one line:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
p = tf.add_paragraph()
|
|
109
|
+
run = p.add_run(); run.text = "Bold lead-in. "
|
|
110
|
+
run.font.bold = True
|
|
111
|
+
run.font.underline = True
|
|
112
|
+
p.add_line_break() # soft break, stays in the paragraph
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Run-level type styling
|
|
116
|
+
|
|
117
|
+
`font` exposes the run-property knobs that separate "looks branded" from
|
|
118
|
+
"looks generated" — set them on a paragraph's `font` or a specific run's
|
|
119
|
+
`font`:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
eyebrow.font.all_caps = True # or .small_caps = True (mutually exclusive)
|
|
123
|
+
eyebrow.font.letter_spacing = Pt(2) # tracking; negative tightens
|
|
124
|
+
old.font.strikethrough = True
|
|
125
|
+
units.font.superscript = True # or .subscript (share one baseline)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
All are tri-state: `None` (the default) inherits from the theme/master,
|
|
129
|
+
`True`/`False` write an explicit override. They round-trip and validate
|
|
130
|
+
against the OOXML schema.
|
|
131
|
+
|
|
132
|
+
### Text effects (outline, shadow, glow)
|
|
133
|
+
|
|
134
|
+
Per-run glyph effects live on `run.font`, mirroring shape
|
|
135
|
+
`.line`/`.shadow`/`.glow`:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
run.font.outline.color.rgb = "FF0000" # coloured glyph outline...
|
|
139
|
+
run.font.outline.width = Pt(1) # ...of a given stroke width
|
|
140
|
+
run.font.shadow.color.rgb = "808080"; run.font.shadow.blur_radius = Pt(3)
|
|
141
|
+
run.font.glow.color.rgb = "00B0F0"; run.font.glow.radius = Pt(6)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Reads are non-mutating — nothing is written until you assign, so theme
|
|
145
|
+
inheritance is preserved.
|
|
146
|
+
|
|
147
|
+
## Auto shapes
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from pptx2.enum.shapes import MSO_SHAPE
|
|
151
|
+
|
|
152
|
+
card = slide.shapes.add_shape(
|
|
153
|
+
MSO_SHAPE.ROUNDED_RECTANGLE,
|
|
154
|
+
left=Inches(1), top=Inches(2),
|
|
155
|
+
width=Inches(4), height=Inches(2.5),
|
|
156
|
+
)
|
|
157
|
+
card.fill.solid()
|
|
158
|
+
card.fill.fore_color.rgb = RGBColor(0xF8, 0xFA, 0xFC)
|
|
159
|
+
card.line.color.rgb = RGBColor(0xE5, 0xE7, 0xEB)
|
|
160
|
+
card.line.width = Pt(1)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Per-color alpha (transparency)
|
|
164
|
+
|
|
165
|
+
Both fill and line colours expose `alpha` in the `[0.0, 1.0]` range —
|
|
166
|
+
useful for hairline dividers, glow shapes, and translucent overlays.
|
|
167
|
+
Assign after a colour is set:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
divider.line.color.rgb = RGBColor(0x0D, 0x0D, 0x0D)
|
|
171
|
+
divider.line.color.alpha = 0.08 # 8% opaque hairline
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Two-stop linear gradients
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
bar.fill.linear_gradient("#06D6FE", "#B14AED", angle=90) # top→bottom
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`angle` follows the OOXML convention: `0` is left→right, `90` is
|
|
181
|
+
top→bottom, `180` is right→left, `270` is bottom→top.
|
|
182
|
+
|
|
183
|
+
→ **Multi-stop gradients, gradient kinds, mutable stop lists, and the
|
|
184
|
+
full alpha surface live in `effects.md`** — the canonical fills and
|
|
185
|
+
effects reference. Don't go looking in two places.
|
|
186
|
+
|
|
187
|
+
## Pictures
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
pic = slide.shapes.add_picture(
|
|
191
|
+
"hero.jpg",
|
|
192
|
+
left=Inches(0), top=Inches(0),
|
|
193
|
+
width=prs.slide_width, height=prs.slide_height,
|
|
194
|
+
)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Cropping
|
|
198
|
+
|
|
199
|
+
Crop values are *fractions of the original image*, not lengths — `0.1`
|
|
200
|
+
trims 10% off that edge. They compose with the placement box, so crop
|
|
201
|
+
first, then size:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
pic = slide.shapes.add_picture("hero.png", Inches(1), Inches(1),
|
|
205
|
+
width=Inches(4))
|
|
206
|
+
pic.crop_left = 0.10 # also crop_right/top/bottom
|
|
207
|
+
pic.crop_top = 0.05
|
|
208
|
+
pic.alt_text = "Q4 revenue by segment" # set this; screen readers need it
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Anchored placement
|
|
212
|
+
|
|
213
|
+
`add_picture`, `add_shape`, and `add_textbox` accept an
|
|
214
|
+
``anchor=`` keyword that collapses the
|
|
215
|
+
``add → measure → reposition`` idiom for branding elements:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
# Logo at bottom-right with a 0.25" margin, height-only sizing:
|
|
219
|
+
slide.shapes.add_picture(
|
|
220
|
+
"logo.png",
|
|
221
|
+
anchor="bottom-right",
|
|
222
|
+
margin=Inches(0.25),
|
|
223
|
+
height=Inches(0.32),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Title centred in the top half of a parent card:
|
|
227
|
+
slide.shapes.add_textbox(
|
|
228
|
+
Inches(0), Inches(0), Inches(2), Inches(0.5),
|
|
229
|
+
anchor="top-center", margin=Inches(0.25),
|
|
230
|
+
container=card, # any shape with .width / .height
|
|
231
|
+
)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`anchor` is one of `top-left`, `top-center`, `top-right`,
|
|
235
|
+
`middle-left`, `middle-center` (or bare `center`),
|
|
236
|
+
`middle-right`, `bottom-left`, `bottom-center`, `bottom-right`.
|
|
237
|
+
Both `center` / `centre` spellings are accepted. `container` is the
|
|
238
|
+
slide by default; pass any shape (or anything exposing
|
|
239
|
+
`.width` / `.height`) to anchor inside a card / group / placeholder.
|
|
240
|
+
|
|
241
|
+
## Grouping shapes
|
|
242
|
+
|
|
243
|
+
`slide.shapes.add_group_shape()` returns a `GroupShape` whose
|
|
244
|
+
`.shapes` collection has the same `add_*` methods as a slide. The
|
|
245
|
+
group's offset/extent shrink-wrap to its members as you add them.
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
group = slide.shapes.add_group_shape()
|
|
249
|
+
group.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(1), Inches(1), Inches(2), Inches(1))
|
|
250
|
+
group.shapes.add_shape(MSO_SHAPE.OVAL, Inches(4), Inches(2), Inches(1), Inches(1))
|
|
251
|
+
|
|
252
|
+
group.fill.solid() # tint the whole group (members paint on top)
|
|
253
|
+
group.fill.fore_color.rgb = "1F4E79"
|
|
254
|
+
group.move(Inches(0.5), Inches(0)) # translate group + every member, O(1)
|
|
255
|
+
|
|
256
|
+
for shape in group.walk(): # depth-first, recurses into nested groups
|
|
257
|
+
... # filter shape.shape_type for leaves only
|
|
258
|
+
|
|
259
|
+
group.fit_to_children() # re-tighten bbox after editing members directly
|
|
260
|
+
promoted = group.ungroup() # dissolve; members keep their on-screen geometry
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
- A group admits a **fill** but not a **line** — the OOXML schema has
|
|
264
|
+
no `a:ln` on `p:grpSpPr`, so there is intentionally no `group.line`.
|
|
265
|
+
(Outline a group by outlining a backing rectangle inside it.)
|
|
266
|
+
- `ungroup()` returns the promoted shapes and preserves z-order. It
|
|
267
|
+
raises `ValueError` on a rotated/flipped group; reset rotation and
|
|
268
|
+
flip to 0 first.
|
|
269
|
+
|
|
270
|
+
## Tables
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
table_shape = slide.shapes.add_table(
|
|
274
|
+
rows=4, cols=3,
|
|
275
|
+
left=Inches(1), top=Inches(2),
|
|
276
|
+
width=Inches(8), height=Inches(3),
|
|
277
|
+
style="clean", # disable inherited style flags for hand-styled tables
|
|
278
|
+
)
|
|
279
|
+
table = table_shape.table
|
|
280
|
+
|
|
281
|
+
# Header
|
|
282
|
+
for i, label in enumerate(("Metric", "Value", "Δ QoQ")):
|
|
283
|
+
cell = table.cell(0, i)
|
|
284
|
+
cell.text = label
|
|
285
|
+
cell.text_frame.paragraphs[0].font.bold = True
|
|
286
|
+
|
|
287
|
+
# Body
|
|
288
|
+
for row, (k, v, d) in enumerate([("ARR", "$182M", "+27%"),
|
|
289
|
+
("NDR", "131%", "+3%"),
|
|
290
|
+
("CAC payback", "8 mo", "−1 mo")], start=1):
|
|
291
|
+
table.cell(row, 0).text = k
|
|
292
|
+
table.cell(row, 1).text = v
|
|
293
|
+
table.cell(row, 2).text = d
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Pass `style="clean"` whenever you plan to apply custom cell borders
|
|
297
|
+
or fills. The default inherited table style otherwise overlays them
|
|
298
|
+
and renders inconsistently across PowerPoint and LibreOffice.
|
|
299
|
+
|
|
300
|
+
(See `tables.md` for `Cell.borders`, the post-fork addition.)
|
|
301
|
+
|
|
302
|
+
## Charts
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
from pptx2.chart.data import CategoryChartData
|
|
306
|
+
from pptx2.enum.chart import XL_CHART_TYPE
|
|
307
|
+
|
|
308
|
+
data = CategoryChartData()
|
|
309
|
+
data.categories = ["Q1", "Q2", "Q3", "Q4"]
|
|
310
|
+
data.add_series("ARR", (100, 130, 155, 182))
|
|
311
|
+
|
|
312
|
+
chart_shape = slide.shapes.add_chart(
|
|
313
|
+
XL_CHART_TYPE.COLUMN_CLUSTERED,
|
|
314
|
+
Inches(1), Inches(2), Inches(8), Inches(4.5),
|
|
315
|
+
data,
|
|
316
|
+
)
|
|
317
|
+
chart = chart_shape.chart
|
|
318
|
+
chart.has_title = True
|
|
319
|
+
chart.chart_title.text_frame.text = "ARR ($M)"
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
(See `charts.md` for chart palettes, quick layouts, and per-series fills.)
|
|
323
|
+
|
|
324
|
+
## Knowing what a shape is
|
|
325
|
+
|
|
326
|
+
Before touching a shape you found by iteration, ask what it actually is
|
|
327
|
+
— the accessors raise rather than return `None` when the shape has no
|
|
328
|
+
such content:
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
for shape in slide.shapes:
|
|
332
|
+
if shape.has_text_frame:
|
|
333
|
+
print(shape.text_frame.text)
|
|
334
|
+
if shape.has_table:
|
|
335
|
+
print(shape.table.rows)
|
|
336
|
+
if shape.has_chart:
|
|
337
|
+
print(shape.chart.chart_type)
|
|
338
|
+
if shape.is_placeholder:
|
|
339
|
+
print("placeholder idx", shape.placeholder_format.idx,
|
|
340
|
+
"type", shape.placeholder_format.type)
|
|
341
|
+
print(shape.shape_id, shape.name) # shape_id is unique per slide
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`shape.delete()` removes a shape *and* purges any animation timing
|
|
345
|
+
entries that targeted it — PowerPoint silently "repairs" decks with
|
|
346
|
+
orphan timing references, so prefer it over detaching the element by
|
|
347
|
+
hand.
|
|
348
|
+
|
|
349
|
+
## Iterating an existing deck
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
prs = Presentation("input.pptx")
|
|
353
|
+
for slide in prs.slides:
|
|
354
|
+
for shape in slide.shapes:
|
|
355
|
+
if shape.has_text_frame:
|
|
356
|
+
for para in shape.text_frame.paragraphs:
|
|
357
|
+
for run in para.runs:
|
|
358
|
+
print(run.text)
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Common units
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
from pptx2.util import Inches, Pt, Cm, Emu, Mm
|
|
365
|
+
|
|
366
|
+
Inches(1) # 914400 EMU
|
|
367
|
+
Pt(12) # 152400 EMU
|
|
368
|
+
Cm(2.54) # ≈ Inches(1)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Use these everywhere — never write the EMU integers directly.
|
|
372
|
+
|
|
373
|
+
Arithmetic on lengths is fine — python-pptx2 coerces float coordinates
|
|
374
|
+
to integer EMU at the API boundary, so this works:
|
|
375
|
+
|
|
376
|
+
```python
|
|
377
|
+
card_w = (Inches(12.33) - Inches(0.25)) / 2 # produces a float
|
|
378
|
+
slide.shapes.add_chart(chart_type, x, y, card_w, height, data)
|
|
379
|
+
slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, card_w, h)
|
|
380
|
+
shape.width = card_w # setter coerces too
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Both ``/`` (true division → float) and ``//`` (floor division → int)
|
|
384
|
+
work; pick whichever reads more cleanly. The coercion is round-half-
|
|
385
|
+
to-even, so it's unbiased over long expression chains.
|
|
386
|
+
|
|
387
|
+
### International & layout text properties
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
p = tf.paragraphs[0]
|
|
391
|
+
p.rtl = True # right-to-left (Hebrew / Arabic / Farsi)
|
|
392
|
+
p.start_at = 5 # numbered list starting at 5 (arabicPeriod)
|
|
393
|
+
p.set_numbered("romanLcPeriod", 3) # i. ii. iii. starting at 3
|
|
394
|
+
|
|
395
|
+
tf.column_count = 2 # two-column text body
|
|
396
|
+
tf.column_spacing = Pt(18) # gutter between columns
|
|
397
|
+
|
|
398
|
+
p.tab_stops.add_tab_stop(Inches(1), "center") # left | center | right | decimal
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Sections, slide order, and speaker notes
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
# Named sections (PowerPoint outline / slide-sorter groupings)
|
|
405
|
+
prs.sections.add("Intro", start_slide_index=0)
|
|
406
|
+
prs.sections.add("Body", start_slide_index=2)
|
|
407
|
+
for section in prs.sections:
|
|
408
|
+
print(section.name, section.slide_ids) # section.name is read/write
|
|
409
|
+
|
|
410
|
+
# Reorder slides without touching XML
|
|
411
|
+
prs.slides.move(0, 2) # send slide 0 to position 2
|
|
412
|
+
prs.slides.reorder([2, 0, 1]) # full permutation (indices or Slide objects)
|
|
413
|
+
|
|
414
|
+
# First-class speaker notes
|
|
415
|
+
slide.notes = "Remember to thank the sponsors." # creates the notes slide
|
|
416
|
+
print(slide.notes) # "" when the slide has no notes
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
`start_slide_index` claims every slide from that position to the deck
|
|
420
|
+
end. `move` raises `IndexError` out of range; `reorder` raises
|
|
421
|
+
`ValueError` unless given a clean permutation.
|