pptd-utils 2026.10.2.dev3__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.
- pptd_utils/README.md +95 -0
- pptd_utils/__init__.py +10 -0
- pptd_utils/__main__.py +5 -0
- pptd_utils/_shape_data.py +134 -0
- pptd_utils/anim.py +367 -0
- pptd_utils/charts_native.py +847 -0
- pptd_utils/charts_raster.py +1019 -0
- pptd_utils/fonts.py +130 -0
- pptd_utils/icons.py +135 -0
- pptd_utils/images.py +105 -0
- pptd_utils/lines.py +180 -0
- pptd_utils/main.py +285 -0
- pptd_utils/render_text.py +241 -0
- pptd_utils/rich.py +213 -0
- pptd_utils/shapes.py +334 -0
- pptd_utils/slides_image.py +128 -0
- pptd_utils/style.py +253 -0
- pptd_utils/tables.py +241 -0
- pptd_utils/xmlutil.py +109 -0
- pptd_utils-2026.10.2.dev3.dist-info/METADATA +120 -0
- pptd_utils-2026.10.2.dev3.dist-info/RECORD +23 -0
- pptd_utils-2026.10.2.dev3.dist-info/WHEEL +4 -0
- pptd_utils-2026.10.2.dev3.dist-info/entry_points.txt +3 -0
pptd_utils/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# pptd_utils
|
|
2
|
+
|
|
3
|
+
Offline PPTD toolkit — full-spec `.pptd` → `.pptx` converter + slide image export (collage / single slide). Pure Python, no browser required.
|
|
4
|
+
|
|
5
|
+
Built against the PPTD v2 spec (`skills/cowork-ppt/reference/pptd.md`, relative to the repo root).
|
|
6
|
+
|
|
7
|
+
## Install / dependencies
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
pip install python-pptx Pillow PyYAML
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Optional (graceful fallback if missing):
|
|
14
|
+
|
|
15
|
+
| Package | Unlocks | Fallback |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `latex2mathml` + `mathml2omml` | native equations (OMML) from `\( ... \)` | literal text |
|
|
18
|
+
| `fontTools` | customFonts (Google Fonts woff2 → embedded TTF) | fonts skipped |
|
|
19
|
+
|
|
20
|
+
Slide images additionally need **LibreOffice** (`soffice` on PATH).
|
|
21
|
+
|
|
22
|
+
## CLI
|
|
23
|
+
|
|
24
|
+
Install from PyPI:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
python -m pip install pptd-utils
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Commands write beside the input path. `all` is the default; selections accept comma-separated 1-based slide numbers.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pptd check deck.pptd
|
|
34
|
+
pptd pptx deck.pptd
|
|
35
|
+
pptd png deck.pptd # all slides -> deck-png/
|
|
36
|
+
pptd png 1,2,3 deck.pptd # selected slides -> deck-png/
|
|
37
|
+
pptd png collage deck.pptd # contact sheet -> deck-collage.png
|
|
38
|
+
pptd pdf deck.pptd # all slides -> deck.pdf
|
|
39
|
+
pptd pdf 1,2,3 deck.pptd # selected slides -> deck.pdf
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
PNG and PDF export require LibreOffice (`soffice` on `PATH`).
|
|
43
|
+
|
|
44
|
+
## Python API
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from pptd_utils import convert, build, verify
|
|
48
|
+
from pptd_utils import slides_image
|
|
49
|
+
|
|
50
|
+
convert("deck.pptd", "out.pptx") # build + verify + print summary
|
|
51
|
+
build("deck.pptd", "out.pptx") # -> (n_slides, n_fonts)
|
|
52
|
+
verify("deck.pptd", "out.pptx") # structural self-check (asserts)
|
|
53
|
+
|
|
54
|
+
slides_image.render_collage("deck.pptd", "collage.png",
|
|
55
|
+
tile_w=480, gap=20, bg="#1F2430")
|
|
56
|
+
slides_image.render_slide("deck.pptd", 3, "slide3.png")
|
|
57
|
+
slides_image.render_slide_images("deck.pptd", "outdir/",
|
|
58
|
+
slides=[1, 3, 5]) # -> [(n, Path), ...]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Supported features (PPTD v2)
|
|
62
|
+
|
|
63
|
+
| Area | Coverage |
|
|
64
|
+
|---|---|
|
|
65
|
+
| text | full HTML tag set (strong/em/u/s/sup/sub/a/br/ul/ol/span style attrs), LaTeX runs, gradient + shadow text, letterSpacing, lineHeight / lineHeightPx, bullets/numbering, vertical text, hyperlinks, highlight |
|
|
66
|
+
| shapes | all 130 preset shapeNames (adjustments), custom SVG paths (`M/L/H/V/C/S/Q/A/Z`, multi-subpath holes), rotation/flip/opacity, gradient/radial fills |
|
|
67
|
+
| lines | sharp / smooth / round (fillet) curves, arrows (head/tail), dash styles |
|
|
68
|
+
| images | fit cover/contain/fill, crop (inset + negative outset padding), cropShape (any preset or custom path), opacity, border, shadow |
|
|
69
|
+
| icons | Font Awesome 6/7 free (fas/far/fab), solid + gradient fill, outline, shadow |
|
|
70
|
+
| tables | rowSpan/colSpan merges, theme tableStyles + inline, per-cell style chains, BorderSpec (per-side / null-clear), gradient cell fills, fontSize auto-fit |
|
|
71
|
+
| charts — native (editable) | bar (stacked/clustered/horizontal), line (smooth/dash/markers), area, scatter, bubble, pie/donut, radar, secondary axes, dataLabels + numberFormat, embedded workbooks |
|
|
72
|
+
| charts — raster (Pillow PNG) | candlestick, waterfall, heatmap (linear/diverging + colorbar), treemap, sunburst, sankey, stream stacks, mixed scatter+cartesian |
|
|
73
|
+
| animations | 21 effects (entrance/emphasis/exit incl. motion-path) → full `p:timing` tree, click/withPrevious/afterPrevious, repeat, easing |
|
|
74
|
+
| transitions | fade |
|
|
75
|
+
| fonts | customFonts embedding (see optional deps) |
|
|
76
|
+
| misc | page backgrounds (solid/gradient/image), notes, themes ($refs), elementId → animation targeting |
|
|
77
|
+
|
|
78
|
+
## How it works
|
|
79
|
+
|
|
80
|
+
- `pptx` generation: python-pptx + hand-built DrawingML for fills/effects/ordering; charts created via real chart parts (editable in PowerPoint) then plot XML rewritten; unsupported chart types rasterized at 2× and placed as pictures.
|
|
81
|
+
- images: each slide isolated into a one-slide pptx, rendered by LibreOffice headless, composed by Pillow.
|
|
82
|
+
(`ponytail:` one soffice run per slide — switch to single PDF + PyMuPDF for large decks.)
|
|
83
|
+
|
|
84
|
+
## Test
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
python3 tests/run.py # converts full-spec fixture + minimal + ml-smp, asserts structure
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Fixture `tests/fixtures/full/` exercises every feature area — use it as a syntax reference for your own decks.
|
|
91
|
+
|
|
92
|
+
## Notes
|
|
93
|
+
|
|
94
|
+
- Downloads (FA fonts, URL images/fonts) cache under `~/.cache/pptd_utils/`.
|
|
95
|
+
- Defaults follow the spec: MiSans font family, 18pt body, black text.
|
pptd_utils/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""pptd_utils — offline PPTD toolkit: .pptd -> .pptx converter + slide images (PPTD v2 spec).
|
|
2
|
+
|
|
3
|
+
python-pptx + Pillow only; optional: latex2mathml+mathml2omml (LaTeX->OMML),
|
|
4
|
+
fontTools+brotli (customFont embedding).
|
|
5
|
+
"""
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
|
|
8
|
+
from .main import build, verify, convert
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__", "build", "verify", "convert"]
|
pptd_utils/__main__.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Generated from pptd.md shapes.md: shapeName -> adjustment defaults (OOXML units)."""
|
|
2
|
+
|
|
3
|
+
SHAPES = {
|
|
4
|
+
"accentBorderCallout1": [18750, -8333, 112500, -38333],
|
|
5
|
+
"accentBorderCallout2": [18750, -8333, 18750, -16667, 112500, -46667],
|
|
6
|
+
"accentBorderCallout3": [18750, -8333, 18750, -16667, 100000, -16667, 112963, -8333],
|
|
7
|
+
"accentCallout1": [18750, -8333, 112500, -38333],
|
|
8
|
+
"accentCallout2": [18750, -8333, 18750, -16667, 112500, -46667],
|
|
9
|
+
"accentCallout3": [18750, -8333, 18750, -16667, 100000, -16667, 112963, -8333],
|
|
10
|
+
"arc": [16200000, 0],
|
|
11
|
+
"bentArrow": [25000, 25000, 25000, 43750],
|
|
12
|
+
"bentUpArrow": [25000, 25000, 25000],
|
|
13
|
+
"bevel": [12500],
|
|
14
|
+
"blockArc": [10800000, 0, 25000],
|
|
15
|
+
"borderCallout1": [18750, -8333, 112500, -38333],
|
|
16
|
+
"borderCallout2": [18750, -8333, 18750, -16667, 112500, -46667],
|
|
17
|
+
"borderCallout3": [18750, -8333, 18750, -16667, 100000, -16667, 112963, -8333],
|
|
18
|
+
"bracePair": [8333],
|
|
19
|
+
"bracketPair": [16667],
|
|
20
|
+
"callout1": [18750, -8333, 112500, -38333],
|
|
21
|
+
"callout2": [18750, -8333, 18750, -16667, 112500, -46667],
|
|
22
|
+
"callout3": [18750, -8333, 18750, -16667, 100000, -16667, 112963, -8333],
|
|
23
|
+
"can": [25000],
|
|
24
|
+
"chevron": [50000],
|
|
25
|
+
"chord": [2700000, 16200000],
|
|
26
|
+
"circularArrow": [12500, 1142319, 20457681, 10800000, 12500],
|
|
27
|
+
"cloud": [],
|
|
28
|
+
"cloudCallout": [-20833, 62500],
|
|
29
|
+
"corner": [50000, 50000],
|
|
30
|
+
"cube": [25000],
|
|
31
|
+
"curvedDownArrow": [25000, 50000, 25000],
|
|
32
|
+
"curvedLeftArrow": [25000, 50000, 25000],
|
|
33
|
+
"curvedRightArrow": [25000, 50000, 25000],
|
|
34
|
+
"curvedUpArrow": [25000, 50000, 25000],
|
|
35
|
+
"decagon": [105146],
|
|
36
|
+
"diagStripe": [50000],
|
|
37
|
+
"diamond": [],
|
|
38
|
+
"dodecagon": [],
|
|
39
|
+
"donut": [25000],
|
|
40
|
+
"doubleWave": [6250, 0],
|
|
41
|
+
"downArrow": [50000, 50000],
|
|
42
|
+
"downArrowCallout": [25000, 25000, 25000, 64977],
|
|
43
|
+
"ellipse": [],
|
|
44
|
+
"ellipseRibbon": [25000, 50000, 12500],
|
|
45
|
+
"ellipseRibbon2": [25000, 50000, 12500],
|
|
46
|
+
"foldedCorner": [],
|
|
47
|
+
"frame": [12500],
|
|
48
|
+
"funnel": [],
|
|
49
|
+
"gear6": [15000, 3526],
|
|
50
|
+
"gear9": [10000, 1763],
|
|
51
|
+
"halfFrame": [33333, 33333],
|
|
52
|
+
"heart": [],
|
|
53
|
+
"heptagon": [102572, 105210],
|
|
54
|
+
"hexagon": [25000, 115470],
|
|
55
|
+
"homePlate": [50000],
|
|
56
|
+
"horizontalScroll": [12500],
|
|
57
|
+
"irregularSeal1": [],
|
|
58
|
+
"irregularSeal2": [],
|
|
59
|
+
"leftArrow": [50000, 50000],
|
|
60
|
+
"leftArrowCallout": [25000, 25000, 25000, 64977],
|
|
61
|
+
"leftBrace": [8333, 50000],
|
|
62
|
+
"leftBracket": [8333],
|
|
63
|
+
"leftCircularArrow": [12500, -1142319, 1142319, 10800000, 12500],
|
|
64
|
+
"leftRightArrow": [50000, 50000],
|
|
65
|
+
"leftRightArrowCallout": [25000, 25000, 25000, 48123],
|
|
66
|
+
"leftRightCircularArrow": [12500, 1142319, 20457681, 11942319, 12500],
|
|
67
|
+
"leftRightRibbon": [50000, 50000, 16667],
|
|
68
|
+
"leftRightUpArrow": [25000, 25000, 25000],
|
|
69
|
+
"leftUpArrow": [25000, 25000, 25000],
|
|
70
|
+
"lightningBolt": [],
|
|
71
|
+
"lineInv": [],
|
|
72
|
+
"mathDivide": [23520, 5880, 11760],
|
|
73
|
+
"mathEqual": [23520, 11760],
|
|
74
|
+
"mathMinus": [23520],
|
|
75
|
+
"mathMultiply": [23520],
|
|
76
|
+
"mathNotEqual": [23520, 6600000, 11760],
|
|
77
|
+
"mathPlus": [23520],
|
|
78
|
+
"moon": [50000],
|
|
79
|
+
"noSmoking": [18750],
|
|
80
|
+
"nonIsoscelesTrapezoid": [25000, 25000],
|
|
81
|
+
"notchedRightArrow": [50000, 50000],
|
|
82
|
+
"octagon": [29289],
|
|
83
|
+
"parallelogram": [25000],
|
|
84
|
+
"pentagon": [105146, 110557],
|
|
85
|
+
"pie": [0, 16200000],
|
|
86
|
+
"pieWedge": [],
|
|
87
|
+
"plaque": [16667],
|
|
88
|
+
"plus": [25000],
|
|
89
|
+
"quadArrow": [22500, 22500, 22500],
|
|
90
|
+
"quadArrowCallout": [18515, 18515, 18515, 48123],
|
|
91
|
+
"rect": [],
|
|
92
|
+
"ribbon": [16667, 50000],
|
|
93
|
+
"ribbon2": [16667, 50000],
|
|
94
|
+
"rightArrow": [50000, 50000],
|
|
95
|
+
"rightArrowCallout": [25000, 25000, 25000, 64977],
|
|
96
|
+
"rightBrace": [8333, 50000],
|
|
97
|
+
"rightBracket": [8333],
|
|
98
|
+
"round1Rect": [16667],
|
|
99
|
+
"round2DiagRect": [16667, 0],
|
|
100
|
+
"round2SameRect": [16667, 0],
|
|
101
|
+
"roundRect": [16667],
|
|
102
|
+
"rtTriangle": [],
|
|
103
|
+
"smileyFace": [4653],
|
|
104
|
+
"snip1Rect": [16667],
|
|
105
|
+
"snip2DiagRect": [0, 16667],
|
|
106
|
+
"snip2SameRect": [16667, 0],
|
|
107
|
+
"snipRoundRect": [16667, 16667],
|
|
108
|
+
"star10": [42533, 105146],
|
|
109
|
+
"star12": [37500],
|
|
110
|
+
"star16": [37500],
|
|
111
|
+
"star24": [37500],
|
|
112
|
+
"star32": [37500],
|
|
113
|
+
"star4": [12500],
|
|
114
|
+
"star5": [19098, 105146, 110557],
|
|
115
|
+
"star6": [28868, 115470],
|
|
116
|
+
"star7": [34601, 102572, 105210],
|
|
117
|
+
"star8": [37500],
|
|
118
|
+
"stripedRightArrow": [50000, 50000],
|
|
119
|
+
"sun": [25000],
|
|
120
|
+
"swooshArrow": [25000, 16667],
|
|
121
|
+
"teardrop": [100000],
|
|
122
|
+
"trapezoid": [25000],
|
|
123
|
+
"triangle": [50000],
|
|
124
|
+
"upArrow": [50000, 50000],
|
|
125
|
+
"upArrowCallout": [25000, 25000, 25000, 64977],
|
|
126
|
+
"upDownArrow": [50000, 50000, 50000, 50000],
|
|
127
|
+
"upDownArrowCallout": [25000, 25000, 25000, 48123],
|
|
128
|
+
"uturnArrow": [25000, 25000, 25000, 43750, 75000],
|
|
129
|
+
"verticalScroll": [12500],
|
|
130
|
+
"wave": [12500, 0],
|
|
131
|
+
"wedgeEllipseCallout": [-20833, 62500],
|
|
132
|
+
"wedgeRectCallout": [-20833, 62500],
|
|
133
|
+
"wedgeRoundRectCallout": [-20833, 62500, 16667],
|
|
134
|
+
}
|
pptd_utils/anim.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""Animations (pptd 6.) -> OOXML p:timing tree.
|
|
2
|
+
|
|
3
|
+
PowerPoint timing model: mainSeq of click groups; each group holds "step"
|
|
4
|
+
pars launched with computed delays (withPrevious joins current step,
|
|
5
|
+
afterPrevious chains a new step after the previous one ends).
|
|
6
|
+
"""
|
|
7
|
+
from lxml import etree
|
|
8
|
+
from pptx.oxml.ns import qn
|
|
9
|
+
|
|
10
|
+
from .xmlutil import sub
|
|
11
|
+
|
|
12
|
+
P = "{http://schemas.openxmlformats.org/presentationml/2006/main}"
|
|
13
|
+
|
|
14
|
+
# presetID, presetClass, default ms
|
|
15
|
+
EFFECTS = {
|
|
16
|
+
"appear": (1, "entr", 1),
|
|
17
|
+
"fade-in": (10, "entr", 500),
|
|
18
|
+
"fly-in": (2, "entr", 500),
|
|
19
|
+
"zoom-in": (23, "entr", 500),
|
|
20
|
+
"wipe-in": (22, "entr", 500),
|
|
21
|
+
"float-in": (42, "entr", 500),
|
|
22
|
+
"peek-in": (12, "entr", 500),
|
|
23
|
+
"rise-in": (33, "entr", 1000),
|
|
24
|
+
"pulse": (26, "emph", 600),
|
|
25
|
+
"grow-shrink": (6, "emph", 2000),
|
|
26
|
+
"spin": (8, "emph", 2000),
|
|
27
|
+
"teeter": (32, "emph", 1000),
|
|
28
|
+
"fill-color": (22, "emph", 2000),
|
|
29
|
+
"transparency": (38, "emph", 2000),
|
|
30
|
+
"color-pulse": (36, "emph", 2000),
|
|
31
|
+
"disappear": (1, "exit", 1),
|
|
32
|
+
"fade-out": (10, "exit", 500),
|
|
33
|
+
"fly-out": (2, "exit", 500),
|
|
34
|
+
"zoom-out": (23, "exit", 500),
|
|
35
|
+
"wipe-out": (22, "exit", 500),
|
|
36
|
+
"float-out": (42, "exit", 500),
|
|
37
|
+
"motion-path": (0, "path", 2000),
|
|
38
|
+
}
|
|
39
|
+
DIR_SUBTYPE = {"up": 4, "down": 1, "left": 8, "right": 2}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _Ids:
|
|
43
|
+
def __init__(self):
|
|
44
|
+
self.n = 0
|
|
45
|
+
|
|
46
|
+
def next(self):
|
|
47
|
+
self.n += 1
|
|
48
|
+
return self.n
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def build_timing(slide, animations, shape_ids, slide_w=None, slide_h=None):
|
|
52
|
+
if not animations:
|
|
53
|
+
return
|
|
54
|
+
slide_w = slide_w or 960
|
|
55
|
+
slide_h = slide_h or 540
|
|
56
|
+
sld = slide._element
|
|
57
|
+
for e in sld.findall(qn("p:timing")):
|
|
58
|
+
sld.remove(e)
|
|
59
|
+
ids = _Ids()
|
|
60
|
+
groups = _group(animations)
|
|
61
|
+
timing = etree.SubElement(sld, qn("p:timing"))
|
|
62
|
+
tnLst = sub(timing, "p:tnLst")
|
|
63
|
+
root_par = sub(tnLst, "p:par")
|
|
64
|
+
ctn = sub(root_par, "p:cTn", {"id": str(ids.next()), "dur": "indefinite",
|
|
65
|
+
"restart": "never", "nodeType": "tmRoot"})
|
|
66
|
+
child = sub(ctn, "p:childTnLst")
|
|
67
|
+
seq = sub(child, "p:seq", {"concurrent": "1", "nextAc": "seek"})
|
|
68
|
+
seq_ctn = sub(seq, "p:cTn", {"id": str(ids.next()), "dur": "indefinite",
|
|
69
|
+
"nodeType": "mainSeq"})
|
|
70
|
+
seq_child = sub(seq_ctn, "p:childTnLst")
|
|
71
|
+
bldLst_shapes = []
|
|
72
|
+
|
|
73
|
+
for gi, group in enumerate(groups):
|
|
74
|
+
auto = group["auto"] # first anim with/after previous -> plays on entry
|
|
75
|
+
gpar = sub(seq_child, "p:par")
|
|
76
|
+
gctn = sub(gpar, "p:cTn", {"id": str(ids.next()), "dur": "indefinite",
|
|
77
|
+
"restart": "whenNotActive", "fill": "hold",
|
|
78
|
+
"nodeType": "clickGroup" if not auto else "afterGroup",
|
|
79
|
+
"presetID": "2", "presetClass": "entr"})
|
|
80
|
+
gcond = sub(gctn, "p:stCondLst")
|
|
81
|
+
sub(gcond, "p:cond", {"delay": "0" if auto else "indefinite"})
|
|
82
|
+
gchild = sub(gctn, "p:childTnLst")
|
|
83
|
+
t = 0 # cumulative time within group (ms)
|
|
84
|
+
for step in group["steps"]:
|
|
85
|
+
sp = sub(gchild, "p:par")
|
|
86
|
+
spctn = sub(sp, "p:cTn", {"id": str(ids.next()),
|
|
87
|
+
"fill": "hold", "nodeType": "clickEffect",
|
|
88
|
+
"presetID": "2", "presetClass": "entr"})
|
|
89
|
+
spcond = sub(spctn, "p:stCondLst")
|
|
90
|
+
sub(spcond, "p:cond", {"delay": str(int(step["start"]))})
|
|
91
|
+
spchild = sub(spctn, "p:childTnLst")
|
|
92
|
+
for anim in step["anims"]:
|
|
93
|
+
_effect(spchild, anim, shape_ids, ids, t=step["start"],
|
|
94
|
+
slide_w=slide_w, slide_h=slide_h)
|
|
95
|
+
bldLst_shapes.append(anim["elementId"])
|
|
96
|
+
t = step["end"]
|
|
97
|
+
# seq prev/next conditions
|
|
98
|
+
prev = sub(seq, "p:prevCondLst")
|
|
99
|
+
c = sub(prev, "p:cond", {"evt": "onPrev", "delay": "0"})
|
|
100
|
+
tgt = sub(c, "p:tgtEl")
|
|
101
|
+
sub(tgt, "p:sldTgt")
|
|
102
|
+
nxt = sub(seq, "p:nextCondLst")
|
|
103
|
+
c = sub(nxt, "p:cond", {"evt": "onNext", "delay": "0"})
|
|
104
|
+
tgt = sub(c, "p:tgtEl")
|
|
105
|
+
sub(tgt, "p:sldTgt")
|
|
106
|
+
|
|
107
|
+
if bldLst_shapes:
|
|
108
|
+
bl = sub(timing, "p:bldLst")
|
|
109
|
+
for sid in dict.fromkeys(bldLst_shapes):
|
|
110
|
+
spid = shape_ids.get(sid)
|
|
111
|
+
if spid:
|
|
112
|
+
sub(bl, "p:bldP", {"spid": str(spid), "grpId": "0"})
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _group(anims):
|
|
116
|
+
"""Group into click groups and time-shifted steps."""
|
|
117
|
+
groups = []
|
|
118
|
+
cur_group = None
|
|
119
|
+
cur_step = None
|
|
120
|
+
t = 0
|
|
121
|
+
for a in anims:
|
|
122
|
+
trigger = a.get("trigger", "onClick")
|
|
123
|
+
dur = _dur(a)
|
|
124
|
+
delay = float(a.get("delayMs", 0) or 0)
|
|
125
|
+
if cur_group is None or trigger == "onClick":
|
|
126
|
+
cur_group = {"auto": trigger in ("withPrevious", "afterPrevious")
|
|
127
|
+
and not groups, "steps": []}
|
|
128
|
+
groups.append(cur_group)
|
|
129
|
+
t = 0
|
|
130
|
+
cur_step = None
|
|
131
|
+
if cur_step is None or trigger == "onClick":
|
|
132
|
+
start = t + delay
|
|
133
|
+
cur_step = {"start": start, "anims": [a],
|
|
134
|
+
"end": start + dur}
|
|
135
|
+
cur_group["steps"].append(cur_step)
|
|
136
|
+
elif trigger == "withPrevious":
|
|
137
|
+
start = t + delay
|
|
138
|
+
cur_step["anims"].append(a)
|
|
139
|
+
cur_step["end"] = max(cur_step["end"], start + dur)
|
|
140
|
+
else: # afterPrevious
|
|
141
|
+
start = cur_step["end"] + delay
|
|
142
|
+
cur_step = {"start": start, "anims": [a],
|
|
143
|
+
"end": start + dur}
|
|
144
|
+
cur_group["steps"].append(cur_step)
|
|
145
|
+
t = cur_step["end"]
|
|
146
|
+
return groups
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _dur(a):
|
|
150
|
+
e = a.get("effect")
|
|
151
|
+
if e in ("appear", "disappear"):
|
|
152
|
+
return 1
|
|
153
|
+
d = EFFECTS.get(e, (0, "entr", 500))[2]
|
|
154
|
+
return float(a.get("durationMs") or d or 500)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _effect(parent, anim, shape_ids, ids, t=0, slide_w=960, slide_h=540):
|
|
158
|
+
spid = shape_ids.get(anim["elementId"])
|
|
159
|
+
if spid is None:
|
|
160
|
+
return
|
|
161
|
+
effect = anim.get("effect")
|
|
162
|
+
if effect not in EFFECTS:
|
|
163
|
+
return
|
|
164
|
+
pid, pclass, _ = EFFECTS[effect]
|
|
165
|
+
dur = int(_dur(anim))
|
|
166
|
+
delay = int(float(anim.get("delayMs", 0) or 0))
|
|
167
|
+
direction = anim.get("direction", "up")
|
|
168
|
+
repeat = int(anim.get("repeat", 1) or 1)
|
|
169
|
+
easing = anim.get("easing", "linear")
|
|
170
|
+
|
|
171
|
+
par = sub(parent, "p:par")
|
|
172
|
+
attrs = {"id": str(ids.next()), "presetID": str(pid),
|
|
173
|
+
"presetClass": pclass, "presetSubtype": "0",
|
|
174
|
+
"fill": "hold", "grpId": "0",
|
|
175
|
+
"nodeType": _node_type(anim)}
|
|
176
|
+
if effect in ("fly-in", "wipe-in", "peek-in", "fly-out", "wipe-out"):
|
|
177
|
+
attrs["presetSubtype"] = str(DIR_SUBTYPE.get(direction, 4))
|
|
178
|
+
if repeat > 1:
|
|
179
|
+
attrs["repeatCount"] = str(repeat)
|
|
180
|
+
if easing == "ease-in":
|
|
181
|
+
attrs["accel"] = "100000"
|
|
182
|
+
elif easing == "ease-out":
|
|
183
|
+
attrs["decel"] = "100000"
|
|
184
|
+
elif easing == "ease-in-out":
|
|
185
|
+
attrs["accel"] = "50000"
|
|
186
|
+
attrs["decel"] = "50000"
|
|
187
|
+
if effect in ("appear", "disappear"):
|
|
188
|
+
attrs["dur"] = "1"
|
|
189
|
+
ctn = sub(par, "p:cTn", attrs)
|
|
190
|
+
st = sub(ctn, "p:stCondLst")
|
|
191
|
+
sub(st, "p:cond", {"delay": str(delay)})
|
|
192
|
+
child = sub(ctn, "p:childTnLst")
|
|
193
|
+
|
|
194
|
+
def cbhvr(dur_ms, attr=None, additive=None):
|
|
195
|
+
cb = etree.Element(qn("p:cBhvr"))
|
|
196
|
+
if additive:
|
|
197
|
+
cb.set("additive", additive)
|
|
198
|
+
c = sub(cb, "p:cTn", {"id": str(ids.next()), "dur": str(max(1, int(dur_ms)))})
|
|
199
|
+
tg = sub(cb, "p:tgtEl")
|
|
200
|
+
sub(tg, "p:spTgt", {"spid": str(spid)})
|
|
201
|
+
if attr:
|
|
202
|
+
al = sub(cb, "p:attrNameLst")
|
|
203
|
+
sub(al, "p:attrName", text=attr)
|
|
204
|
+
return cb
|
|
205
|
+
|
|
206
|
+
def set_vis(visible, dur_ms=1, delay_ms=0):
|
|
207
|
+
s = etree.Element(qn("p:set"))
|
|
208
|
+
cb = cbhvr(dur_ms)
|
|
209
|
+
cb.find(qn("p:cTn")).set("fill", "hold")
|
|
210
|
+
if delay_ms:
|
|
211
|
+
cb.find(qn("p:cTn")).find(qn("p:stCondLst"))
|
|
212
|
+
c = cb.find(qn("p:cTn"))
|
|
213
|
+
st = sub(c, "p:stCondLst")
|
|
214
|
+
sub(st, "p:cond", {"delay": str(delay_ms)})
|
|
215
|
+
al = sub(cb, "p:attrNameLst")
|
|
216
|
+
sub(al, "p:attrName", text="style.visibility")
|
|
217
|
+
s.append(cb)
|
|
218
|
+
to = sub(s, "p:to")
|
|
219
|
+
sub(to, "p:strVal", {"val": "visible" if visible else "hidden"})
|
|
220
|
+
return s
|
|
221
|
+
|
|
222
|
+
def anim_effect(trans, filt, dur_ms):
|
|
223
|
+
ae = etree.Element(qn("p:animEffect"), {"transition": trans, "filter": filt})
|
|
224
|
+
ae.append(cbhvr(dur_ms))
|
|
225
|
+
return ae
|
|
226
|
+
|
|
227
|
+
def motion_xy(axis, frm, to, dur_ms):
|
|
228
|
+
an = etree.Element(qn("p:anim"), {"calcmode": "lin", "valueType": "num"})
|
|
229
|
+
cb = cbhvr(dur_ms, attr="ppt_%s" % axis, additive="base")
|
|
230
|
+
an.append(cb)
|
|
231
|
+
tav = sub(an, "p:tavLst")
|
|
232
|
+
for tm, v in ((0, frm), (100000, to)):
|
|
233
|
+
tv = sub(tav, "p:tav", {"tm": str(tm)})
|
|
234
|
+
val = sub(tv, "p:val")
|
|
235
|
+
sub(val, "p:strVal", {"val": v})
|
|
236
|
+
return an
|
|
237
|
+
|
|
238
|
+
if effect == "appear":
|
|
239
|
+
child.append(set_vis(True))
|
|
240
|
+
elif effect == "disappear":
|
|
241
|
+
child.append(set_vis(False))
|
|
242
|
+
elif effect == "fade-in":
|
|
243
|
+
child.append(set_vis(True))
|
|
244
|
+
child.append(anim_effect("in", "fade", dur))
|
|
245
|
+
elif effect == "fade-out":
|
|
246
|
+
child.append(anim_effect("out", "fade", dur))
|
|
247
|
+
s = set_vis(False, delay_ms=dur)
|
|
248
|
+
child.append(s)
|
|
249
|
+
elif effect in ("fly-in", "float-in", "rise-in", "peek-in"):
|
|
250
|
+
child.append(set_vis(True))
|
|
251
|
+
dist = "0.1+#ppt_h/2" if effect in ("float-in",) else None
|
|
252
|
+
if effect in ("fly-in", "float-in", "rise-in"):
|
|
253
|
+
frm_y = {"up": "1+#ppt_h/2", "down": "0-#ppt_h/2",
|
|
254
|
+
"left": "#ppt_y", "right": "#ppt_y"}[direction if effect != "rise-in" else "up"]
|
|
255
|
+
frm_x = {"up": "#ppt_x", "down": "#ppt_x", "left": "1+#ppt_w/2",
|
|
256
|
+
"right": "0-#ppt_w/2"}[direction if effect != "rise-in" else "up"]
|
|
257
|
+
if effect == "float-in":
|
|
258
|
+
frm_y = "0.1+#ppt_h/2" if direction == "up" else "-0.1-#ppt_h/2"
|
|
259
|
+
frm_x = "#ppt_x"
|
|
260
|
+
child.append(motion_xy("x", frm_x, "#ppt_x", dur))
|
|
261
|
+
child.append(motion_xy("y", frm_y, "#ppt_y", dur))
|
|
262
|
+
else: # peek-in: wipe without fade
|
|
263
|
+
filt = "wipe(%s)" % {"up": "up", "down": "down", "left": "right",
|
|
264
|
+
"right": "left"}.get(direction, "up")
|
|
265
|
+
child.append(anim_effect("in", filt, dur))
|
|
266
|
+
elif effect in ("fly-out", "float-out"):
|
|
267
|
+
to_y = {"up": "0-#ppt_h/2", "down": "1+#ppt_h/2"}.get(direction, "1+#ppt_h/2")
|
|
268
|
+
child.append(motion_xy("y", "#ppt_y", to_y if direction in ("up", "down") else "#ppt_y", dur))
|
|
269
|
+
if direction in ("left", "right"):
|
|
270
|
+
to_x = "0-#ppt_w/2" if direction == "left" else "1+#ppt_w/2"
|
|
271
|
+
child.append(motion_xy("x", "#ppt_x", to_x, dur))
|
|
272
|
+
s = set_vis(False, delay_ms=dur)
|
|
273
|
+
child.append(s)
|
|
274
|
+
elif effect in ("zoom-in", "zoom-out"):
|
|
275
|
+
child.append(set_vis(True))
|
|
276
|
+
scale = etree.Element(qn("p:animScale"))
|
|
277
|
+
cb = cbhvr(dur)
|
|
278
|
+
scale.append(cb)
|
|
279
|
+
if effect == "zoom-in":
|
|
280
|
+
f = sub(scale, "p:from", {"x": "0", "y": "0"})
|
|
281
|
+
t2 = sub(scale, "p:to", {"x": "100000", "y": "100000"})
|
|
282
|
+
else:
|
|
283
|
+
f = sub(scale, "p:from", {"x": "100000", "y": "100000"})
|
|
284
|
+
t2 = sub(scale, "p:to", {"x": "0", "y": "0"})
|
|
285
|
+
child.append(scale)
|
|
286
|
+
elif effect in ("wipe-in", "wipe-out"):
|
|
287
|
+
filt = "wipe(%s)" % {"up": "up", "down": "down", "left": "right",
|
|
288
|
+
"right": "left"}.get(direction, "up")
|
|
289
|
+
child.append(set_vis(True))
|
|
290
|
+
child.append(anim_effect("in" if effect == "wipe-in" else "out", filt, dur))
|
|
291
|
+
elif effect == "pulse":
|
|
292
|
+
scale = etree.Element(qn("p:animScale"))
|
|
293
|
+
cb = cbhvr(dur)
|
|
294
|
+
cb.find(qn("p:cTn")).set("autoRev", "1")
|
|
295
|
+
scale.append(cb)
|
|
296
|
+
sub(scale, "p:from", {"x": "100000", "y": "100000"})
|
|
297
|
+
sub(scale, "p:to", {"x": "110000", "y": "110000"})
|
|
298
|
+
child.append(scale)
|
|
299
|
+
elif effect == "grow-shrink":
|
|
300
|
+
scale = etree.Element(qn("p:animScale"))
|
|
301
|
+
scale.append(cbhvr(dur))
|
|
302
|
+
sub(scale, "p:to", {"x": "150000", "y": "150000"})
|
|
303
|
+
child.append(scale)
|
|
304
|
+
elif effect == "spin":
|
|
305
|
+
rot = etree.Element(qn("p:animRot"), {"by": "21600000"})
|
|
306
|
+
rot.append(cbhvr(dur))
|
|
307
|
+
child.append(rot)
|
|
308
|
+
elif effect == "teeter":
|
|
309
|
+
rot = etree.Element(qn("p:animRot"), {"by": "60000"})
|
|
310
|
+
cb = cbhvr(dur)
|
|
311
|
+
cb.find(qn("p:cTn")).set("autoRev", "1")
|
|
312
|
+
cb.find(qn("p:cTn")).set("repeatCount", "3")
|
|
313
|
+
rot.append(cb)
|
|
314
|
+
child.append(rot)
|
|
315
|
+
elif effect in ("fill-color", "color-pulse"):
|
|
316
|
+
ac = etree.Element(qn("p:animColor"), {"colorType": "rgb"})
|
|
317
|
+
cb = cbhvr(dur, attr="fillcolor")
|
|
318
|
+
if effect == "color-pulse":
|
|
319
|
+
cb.find(qn("p:cTn")).set("autoRev", "1")
|
|
320
|
+
ac.append(cb)
|
|
321
|
+
to = sub(ac, "p:to")
|
|
322
|
+
color = str(anim.get("color", "#FF0000")).lstrip("#")[:6]
|
|
323
|
+
a = sub(to, "a:srgbClr", {"val": color.upper()})
|
|
324
|
+
child.append(ac)
|
|
325
|
+
elif effect == "transparency":
|
|
326
|
+
an = etree.Element(qn("p:anim"), {"calcmode": "lin", "valueType": "num"})
|
|
327
|
+
cb = cbhvr(dur, attr="style.opacity")
|
|
328
|
+
an.append(cb)
|
|
329
|
+
tav = sub(an, "p:tavLst")
|
|
330
|
+
tv0 = sub(tav, "p:tav", {"tm": "0"})
|
|
331
|
+
v0 = sub(tv0, "p:val")
|
|
332
|
+
sub(v0, "p:fltVal", {"val": "1"})
|
|
333
|
+
tv1 = sub(tav, "p:tav", {"tm": "100000"})
|
|
334
|
+
v1 = sub(tv1, "p:val")
|
|
335
|
+
sub(v1, "p:fltVal", {"val": str(anim.get("amount", 0))})
|
|
336
|
+
child.append(an)
|
|
337
|
+
elif effect == "motion-path":
|
|
338
|
+
am = etree.Element(qn("p:animMotion"), {
|
|
339
|
+
"origin": "parent",
|
|
340
|
+
"path": _motion_path(anim.get("path", ""),
|
|
341
|
+
slide_w, slide_h),
|
|
342
|
+
"pathEditMode": "relative"})
|
|
343
|
+
am.append(cbhvr(dur))
|
|
344
|
+
child.append(am)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _motion_path(path, sw, sh):
|
|
348
|
+
"""px path -> animMotion slide-fraction path (M/L/C pairs)."""
|
|
349
|
+
import re
|
|
350
|
+
path = re.sub(r"([A-Za-z])([-+0-9.])", r"\1 \2", str(path))
|
|
351
|
+
out = []
|
|
352
|
+
idx = 0
|
|
353
|
+
for tok in re.finditer(r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?|[^\s,]+", path):
|
|
354
|
+
t = tok.group(0)
|
|
355
|
+
try:
|
|
356
|
+
v = float(t)
|
|
357
|
+
out.append(f"{(v / sw) if idx % 2 == 0 else (v / sh):.6f}")
|
|
358
|
+
idx += 1
|
|
359
|
+
except ValueError:
|
|
360
|
+
out.append(t)
|
|
361
|
+
return " ".join(out)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _node_type(anim):
|
|
365
|
+
return {"onClick": "clickEffect", "withPrevious": "withEffect",
|
|
366
|
+
"afterPrevious": "afterEffect"}.get(anim.get("trigger", "onClick"),
|
|
367
|
+
"clickEffect")
|