pptx-kit 0.0.0 → 0.2.0

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,1618 @@
1
+ # pptx-kit
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b03e0cb: feat: fidelity calibration sweep — measured against LibreOffice ground truth,
8
+ mean fg-SSIM rose from ≈0.66 to ≈0.78 (≈0.81 excluding documented
9
+ divergences). Body placeholders now inherit the master `bodyStyle` bullet and
10
+ hanging indent through the paragraph cascade (new `bullet` field on
11
+ `ParagraphProperties` from `getParagraphPropertiesEffective`); charts no
12
+ longer invent a legend when the XML authors no `<c:legend>`, and the value
13
+ axis gets Excel-style headroom above the data max with the tick step
14
+ preserved; the chart builder writes `<c:smooth val="0"/>` explicitly on line
15
+ series (the schema default for an absent element is smooth=1, which made
16
+ LibreOffice draw unauthored lines as curves); and the pure-SVG text layer is
17
+ nudged 0.75px left to land on LibreOffice's pixel grid.
18
+
19
+ ## 0.1.0
20
+
21
+ ### Minor Changes
22
+
23
+ - 263bf52: feat: `getShapeAdjustValues(shape)` returns the `<a:prstGeom><a:avLst>
24
+ <a:gd name=… fmla="val N"/></a:avLst>` map (preset adjust-handle
25
+ values). Only literal `val` formulas are surfaced; computed formulas
26
+ (`pin`, `+-`, etc.) reference the preset's built-in guides and
27
+ aren't useful without them.
28
+
29
+ Playground reads `adj` on `roundRect` to project the authored corner
30
+ radius — previously every rounded rectangle painted at a hard-coded
31
+ 18%. Other presets (callouts, arrows, etc.) can adopt the same getter
32
+ as their renderers grow.
33
+
34
+ - a944352: feat(site/playground): render auto-numbered bullets. Paragraphs with
35
+ `bulletStyle === 'number'` or `{ autoNum: '…' }` now emit the next
36
+ number in sequence (1., 2., 3., …; A., B., C., …; i., ii., iii., …)
37
+ rather than a generic dot. Counter resets on a non-numbered paragraph
38
+ or a level change, matching PowerPoint's behaviour.
39
+
40
+ Covers the common `ST_TextAutoNumberScheme` tokens — arabicPeriod /
41
+ ParenR / ParenBoth, romanUc / Lc with Period / ParenR / ParenBoth,
42
+ alphaUc / Lc with Period / ParenR / ParenBoth.
43
+
44
+ - 63c4453: feat: chart axis _tick labels_ honor authored `<c:txPr>` font / color.
45
+ `ChartSpec.categoryAxisLabelStyle` and `valueAxisLabelStyle` carry the
46
+ font / color extracted from `<c:catAx><c:txPr>` and `<c:valAx><c:txPr>`.
47
+ A shared `axisTickAttrs` helper composes the SVG `font-*` / `fill`
48
+ attributes; the value-axis renderer and category-axis renderer both
49
+ project it onto every tick label.
50
+ - e60dc26: feat: chart value-axis tick marks. `ChartSpec.valueAxisMajorTickMark`
51
+ and `categoryAxisMajorTickMark` carry `<c:majorTickMark val="in|out|
52
+ cross|none"/>`. The playground value-axis renderer draws short stubs
53
+ on the appropriate side of the plot edge (default `out` matches
54
+ PowerPoint's stock look); `none` suppresses them entirely.
55
+ - 3a2b974: feat: chart axis titles honor authored `<a:rPr>` font / color.
56
+ `ChartSpec.categoryAxisTitleStyle` and `valueAxisTitleStyle` carry the
57
+ same `ChartTextStyle` shape as `titleStyle`. The playground renderer
58
+ projects size / bold / italic / fill onto both axis title labels,
59
+ sharing the helper that drives the chart title.
60
+ - b8c676d: feat(site/playground): bar chart category-axis labels honor
61
+ `categoryAxisLabelRotationDeg`. The horizontal-value renderer used
62
+ the rotation field only for column charts (categories along the
63
+ x-axis); now the bar variant (categories down the y-axis) also
64
+ rotates each label around its anchor and widens its ellipsis budget
65
+ for tilted labels.
66
+ - 0b61a96: feat(site/playground): apply the 3-level gradient bg cascade. When
67
+ the slide reports `'gradient'` but doesn't author the actual stops,
68
+ the renderer walks slide → layout → master gradient-fill readers
69
+ to find the inherited gradient definition.
70
+ - 2896447: feat: `getSlideLayoutBackgroundImageBytes(pres, layout)` and
71
+ `getSlideMasterBackgroundImageBytes(pres, layout)` complete the
72
+ picture-background cascade. The slide reader already returned bytes
73
+ for slide-level `<a:blipFill>` backgrounds; the new readers resolve
74
+ the same shape on layouts and masters via their own rel lists. The
75
+ playground renderer threads slide → layout → master fallback, so
76
+ template-defined photo backgrounds finally show on inheriting slides.
77
+ - 8a4dafb: feat(site/playground): apply the 3-level pattern bg cascade. When the
78
+ slide reports `'pattern'` but doesn't author the actual pattern preset,
79
+ the renderer now walks slide → layout → master pattern-fill readers,
80
+ paralleling the gradient cascade.
81
+ - 4b1fd76: feat: `getSlideLayoutBackgroundPatternFill(pres, layout)` and
82
+ `getSlideMasterBackgroundPatternFill(pres, layout)` complete the
83
+ pattern-background cascade. Slides reporting `'pattern'` can now
84
+ resolve the actual preset / colors by walking slide → layout →
85
+ master, paralleling the gradient / solid / blip cascades.
86
+ - 63dee61: feat: `getShapeBodyPrEffective(pres, shape)` — `<a:bodyPr>` cascade
87
+ covering anchor, wrap, vertical-text direction, and inset margins.
88
+ Walks shape → layout placeholder → master placeholder bodyPr the same
89
+ way the rPr / pPr cascades do. Playground uses it so placeholders
90
+ inherit text alignment / margins from the layout / master without
91
+ each slide having to re-author them.
92
+ - 36bd14d: feat(site/playground): shape text honors `<a:bodyPr wrap="none"/>`.
93
+ The reader's effective wrap value was already threaded through; the
94
+ renderer now emits `white-space:nowrap` when wrap is `'none'`,
95
+ keeping single-line text frames (vertical labels, breadcrumbs,
96
+ fixed-width badges) from wrapping into multiple lines.
97
+ - e31b414: feat(site/playground): paragraphs with image bullets (`<a:pPr><a:buBlip>`)
98
+ render a filled-square glyph (■) instead of inheriting the default
99
+ round bullet. The reader already exposed `isParagraphBulletPicture`;
100
+ the playground now threads it through paragraph metadata so the
101
+ visual cue lands.
102
+ - e30c9f3: feat: `isParagraphBulletPicture(shape, p)` returns `true` when the
103
+ paragraph uses an image as its bullet (`<a:pPr><a:buBlip>`).
104
+ Renderers without image-bullet support can fall back to a generic
105
+ glyph; UIs that want to indicate the bullet is custom have a clean
106
+ yes/no signal.
107
+ - 263bf52: feat: `getParagraphBulletStyle(pres, shape, p)` returns the
108
+ paragraph-level bullet overrides — color (theme-resolved), percent
109
+ size, fixed-point size, font face — from `<a:buClr>` / `<a:buSzPct>`
110
+ / `<a:buSzPts>` / `<a:buFont>`. Playground projects each onto the
111
+ bullet `<span>`, so decks that style bullets in an accent color or
112
+ sized-up font (a common branding move) render correctly instead of
113
+ falling back to the body's color.
114
+ - b53b420: feat: chart category-axis tick labels honor `<a:bodyPr rot="N"/>`.
115
+ `ChartSpec.categoryAxisLabelRotationDeg` carries the authored rotation
116
+ (converted from OOXML's 60000ths-of-a-degree to plain degrees). The
117
+ playground renderer rotates each tick label around its anchor and
118
+ shifts the text-anchor side based on the sign of the rotation so dense
119
+ charts with 45°/-45°/90° rotated labels render the way PowerPoint
120
+ shows them. Rotated labels also get a longer truncation budget before
121
+ ellipsization.
122
+ - 4f5cc4c: feat: round out gridline color round-trip with 3 more fields —
123
+ `valueAxisMinorGridlineColor`, `categoryAxisMajorGridlineColor`, and
124
+ `categoryAxisMinorGridlineColor`. Previously only the value-axis major
125
+ color was carried. All four now share a new chart-builder
126
+ `gridlinesElement(local, color?)` helper and a chart-reader
127
+ `readGridlineColor(gl)` helper; the existing major-gridline color
128
+ inline parse was replaced with a call to the shared reader for
129
+ consistency.
130
+ - 9fba547: feat(chart): `ChartSpec.chartAreaFill` and `plotAreaFill` read
131
+ `<c:chartSpace><c:spPr><a:solidFill>` and `<c:plotArea><c:spPr>
132
+ <a:solidFill>`. Playground paints the chart-area backdrop in the
133
+ authored color (replacing the hard-coded white) and adds a tinted
134
+ rect behind the plot area when `plotAreaFill` is authored. Common
135
+ on branded dashboards that paint a subtle background behind the
136
+ chart bars.
137
+ - 9d79d53: feat: chart-area / plot-area authored outline strokes.
138
+ `ChartSpec.chartAreaStrokeColor` reads `<c:chartSpace><c:spPr><a:ln>`;
139
+ `ChartSpec.plotAreaStrokeColor` reads `<c:plotArea><c:spPr><a:ln>`.
140
+ The playground renderer projects them onto the chart-area card
141
+ border and the plot-area inner rect — branded charts with thick / no
142
+ / colored card borders finally render the way PowerPoint shows them.
143
+ - eb4159e: feat(chart): `ChartSpec.valueAxisHidden` and `categoryAxisHidden`
144
+ read `<c:valAx><c:delete val="1"/>` and `<c:catAx><c:delete val="1"/>`.
145
+ Playground skips rendering the axis when hidden — common on KPI tile
146
+ charts that show just the data points without axis labels.
147
+ - 2710f56: feat: `ChartSpec.categoryAxisLineColor` and `valueAxisLineColor` —
148
+ authored stroke color on the axis line itself
149
+ (`<c:catAx|valAx><c:spPr><a:ln><a:solidFill><a:srgbClr val=…/>`).
150
+ `undefined` falls back to the renderer's default. Read by chart-reader,
151
+ written by chart-builder in the correct CT_CatAx / CT_ValAx schema
152
+ order (after the tick-mark elements, before `<c:txPr>`).
153
+ - c5761f4: feat(chart): `ChartSpec.categoryAxisOrientation` and
154
+ `valueAxisOrientation` read `<c:catAx>/<c:valAx><c:scaling>
155
+ <c:orientation val="minMax|maxMin"/>`. Tools and renderers that
156
+ care about category render order (typically bar charts emit
157
+ `maxMin` so the first category sits at the top) can act on these
158
+ without dropping to XML.
159
+ - 45e889d: feat: `ChartSpec.valueAxis` reports the authored
160
+ `<c:valAx><c:scaling>` min / max. Playground respects them when
161
+ computing axis ranges, so charts with a fixed authored scale (e.g.
162
+ percentage charts pinned to 0..100) render with the same scale the
163
+ deck author saw instead of auto-fitting to the data.
164
+
165
+ Adds the `ChartAxisScaling` interface to the public type surface.
166
+
167
+ - 6dd7281: feat: `ChartSpec.categoryAxisTitleRotationDeg` and
168
+ `ChartSpec.valueAxisTitleRotationDeg` — rotation in plain degrees
169
+ (clockwise) on the per-axis title. Maps to
170
+ `<c:catAx|valAx><c:title><c:tx><c:rich><a:bodyPr rot="N"/>` (60000ths
171
+ of a degree on the wire). PowerPoint often emits `-90` on the value-
172
+ axis title; the field now survives round-trip. Read by chart-reader
173
+ via a new `readTitleRotationDeg` helper; written by chart-builder
174
+ through an extended `titleElement(title, style?, rotationDeg?)`
175
+ signature.
176
+ - 2bf32ca: feat(chart): `ChartSpec.categoryAxisTitle` and `valueAxisTitle` read
177
+ the per-axis `<c:title>` rich text on `<c:catAx>` (or `<c:dateAx>` /
178
+ `<c:serAx>`) and `<c:valAx>`. Playground paints the value-axis title
179
+ rotated -90° along the y-axis and the category-axis title centered
180
+ below the x-axis.
181
+ - db36287: feat: chart builder writes back plot-area / chart-area fill + stroke
182
+ colors. A new `spPrChildren(fill, stroke)` helper emits
183
+ `<c:spPr><a:solidFill><a:srgbClr/></a:solidFill><a:ln>…</a:ln>`. The
184
+ builder appends it under `<c:plotArea>` when `plotAreaFill` or
185
+ `plotAreaStrokeColor` is set, and under `<c:chartSpace>` (root) when
186
+ `chartAreaFill` or `chartAreaStrokeColor` is set. Round-trip test
187
+ verifies all four survive.
188
+ - 02434bb: feat: chart builder writes back value-axis extras and tick marks.
189
+ `<c:valAx>` now emits `<c:scaling><c:logBase>`, `<c:majorTickMark>`,
190
+ and `<c:dispUnits><c:builtInUnit>` when authored on `ChartSpec`;
191
+ `<c:catAx>` emits `<c:majorTickMark>`. Round-tripping a chart with
192
+ these fields no longer drops them. Covered by a new round-trip test
193
+ in `fn-chart-readback`.
194
+ - 0afa65b: feat: chart builder writes back full value-axis scaling. `<c:valAx>`
195
+ now emits `<c:scaling><c:min/>/<c:max/>`, `<c:numFmt formatCode>`,
196
+ `<c:majorUnit>`, and `<c:minorUnit>` when authored — completing the
197
+ read/write parity for `ChartSpec.valueAxis`. Round-trip test added.
198
+ - 7bee159: feat: chart builder writes back axis titles, hidden flags, and
199
+ category-axis tick-label config. `<c:valAx>` / `<c:catAx>` now emit:
200
+
201
+ - `<c:title>` with style (from `valueAxisTitleStyle` /
202
+ `categoryAxisTitleStyle`) when an axis title is authored
203
+ - `<c:delete val="1"/>` when `valueAxisHidden` / `categoryAxisHidden`
204
+ - `<c:tickLblPos>` and `<c:tickLblSkip>` when authored on the
205
+ category axis
206
+
207
+ Closes the read/write gap for these `ChartSpec` fields. Round-trip
208
+ test added.
209
+
210
+ - bddd838: feat: chart builder writes back chart-level data-label config. A new
211
+ `dLblsElement` helper builds `<c:dLbls>` with `showVal` / `showCatName`
212
+ / `showSerName` / `showPercent` toggles plus optional `<c:numFmt>`,
213
+ `<c:dLblPos>`, and `<c:separator>`. Wired into every chart variant
214
+ (bar / column / line / pie / doughnut / area), so round-tripping a
215
+ chart with authored data labels preserves them.
216
+ - 3f6f848: feat: chart builder writes back per-data-point `<c:dPt>` overrides.
217
+ New `dPtElements(colors, explosions)` helper emits sparse
218
+ `<c:dPt><c:idx><c:bubble3D val="0"/>[<c:explosion>]
219
+ [<c:spPr><a:solidFill><a:srgbClr/>]</c:dPt>` entries from
220
+ `ChartSeries.pointColors` and `ChartSeries.pointExplosions`.
221
+ Round-trip test asserts both sparse arrays survive.
222
+ - 8a9bab4: feat: chart builder writes back a wide slate of optional chart fields.
223
+ The chart-builder now emits, when authored on `ChartSpec`:
224
+
225
+ - `<c:varyColors>` (per chart kind), `<c:gapWidth>`, `<c:overlap>`
226
+ on bar / column
227
+ - `<c:grouping>` honors `ChartSpec.grouping` (`'clustered' | 'stacked'
228
+ | 'percentStacked' | 'standard'`) on bar / column / line / area
229
+ - `<c:dropLines>`, `<c:hiLowLines>` on line
230
+ - `<c:firstSliceAng>` on pie / doughnut; `<c:holeSize>` on doughnut
231
+ honors `holeSizePct`
232
+ - `<c:majorGridlines>` (with optional `<c:spPr><a:ln><a:solidFill>`
233
+ color), `<c:minorGridlines>` on the value axis
234
+ - `<c:title><c:overlay>` honoring `titleOverlay`
235
+
236
+ Round-trip test asserts the additions all survive read → save →
237
+ reload.
238
+
239
+ - e7078d7: feat: chart builder writes back legend.textStyle + axis orientation
240
+ reversals. `<c:legend><c:txPr>` now carries the authored font / color
241
+ from `legend.textStyle` (via the existing `axisTxPrElement` helper);
242
+ `<c:scaling><c:orientation>` honors `categoryAxisOrientation` and
243
+ `valueAxisOrientation` (defaulting to `minMax`). Round-trip test
244
+ asserts all four survive read → save → reload.
245
+ - 034207d: feat: chart builder writes back `<c:legend>` and `<c:dispBlanksAs>`.
246
+ The chart-root previously emitted only the default legend / blanks
247
+ behavior; the builder now:
248
+
249
+ - emits `<c:legend>` with `legendPos`, `overlay`, and one
250
+ `<c:legendEntry><c:idx><c:delete val="1"/></c:legendEntry>` per
251
+ hidden series index — or skips the element when
252
+ `spec.legend.position === null` (author wants no legend)
253
+ - threads `spec.dispBlanksAs` (`'gap' | 'zero' | 'span'`) into
254
+ `<c:dispBlanksAs>`
255
+
256
+ Round-trip test added.
257
+
258
+ - f643b29: feat: chart builder writes back per-series `<c:dLbls>` overrides.
259
+ `dLblsElement` is refactored to take the labels arg directly via
260
+ `buildDLblsFromLabels(dl)`; `seriesElement` now emits per-series
261
+ `<c:dLbls>` when authored, so charts with per-series label toggles /
262
+ numberFormat / position survive round-trip. Round-trip test covers
263
+ all four fields plus the no-override case.
264
+ - cf2d02b: feat: chart builder writes back series-level optional fields. Each
265
+ `<c:ser>` now emits:
266
+
267
+ - richer `<c:spPr>` with `<a:ln w="…"><a:prstDash/>` when
268
+ `series.lineWidthEmu` or `lineDash` is authored
269
+ - `<c:invertIfNegative val="1"/>` when set
270
+ - `<c:marker><c:symbol/><c:size/></c:marker>` from `markerSymbol` /
271
+ `markerSizePt`
272
+ - `<c:smooth val="1"/>` when set
273
+
274
+ Round-trip test covers all five fields.
275
+
276
+ - 7f0b8ee: feat: chart builder writes back `ChartSpec.titleStyle`. Previously the
277
+ reader picked up authored `<a:rPr sz/b/i><a:solidFill>` on chart
278
+ titles but the builder dropped any incoming style, so round-tripping
279
+ (read → save → reload) lost the title font / color. The builder now
280
+ emits `<a:rPr>` attributes and an inner `<a:solidFill><a:srgbClr/>`
281
+ when a `titleStyle` is provided. New round-trip test
282
+ (`fn-chart-readback`) covers this; total tests 801.
283
+ - bf62577: feat: chart builder writes back per-series `<c:trendline>`. A new
284
+ `trendlineElement(tl)` helper emits `<c:trendlineType>`,
285
+ `<c:period>` (movingAvg), `<c:order>` (poly), `<c:forward>` /
286
+ `<c:backward>`, and `<c:spPr><a:ln><a:solidFill>` color when
287
+ authored. Closes the read/write gap for `ChartSeries.trendline`;
288
+ round-trip test covers type / period / forward / backward / color.
289
+ - 925fd6f: feat: chart builder writes back axis tick-label style + rotation via
290
+ `<c:txPr>`. New `axisTxPrElement(style, rotationDeg)` helper emits the
291
+ `<c:txPr><a:bodyPr rot/><a:lstStyle/><a:p><a:pPr><a:defRPr…/></a:pPr></a:p></c:txPr>`
292
+ payload from `categoryAxisLabelStyle` / `categoryAxisLabelRotationDeg`
293
+ and the value-axis counterparts. Closes the read/write gap for these
294
+ fields; round-trip test added.
295
+ - ba80399: feat: `ChartSpec.categoryAxisMajorGridlines` and
296
+ `ChartSpec.categoryAxisMinorGridlines` — companions to the existing
297
+ `valueAxis*` pair. Bar charts (where the category axis sits on the
298
+ vertical edge) actually use these as horizontal guide lines per
299
+ category band. Mapped to `<c:catAx><c:majorGridlines/>` /
300
+ `<c:minorGridlines/>`. Read by chart-reader, written by chart-builder
301
+ in the correct CT_CatAx schema order (right after `<c:axPos>`).
302
+ - 2d61d26: feat: `ChartSpec.categoryAxisLabelOffset` and
303
+ `ChartSpec.categoryAxisLabelAlign` — two more category-axis tuning
304
+ knobs from ECMA-376. `<c:catAx><c:lblOffset val="N"/>` (0..1000, default 100) controls the distance from the axis line to the labels as a
305
+ percent of text size; `<c:catAx><c:lblAlgn val="ctr|l|r"/>` controls
306
+ how multi-line category labels align relative to their tick mark. Both
307
+ are read by chart-reader and written by chart-builder in the correct
308
+ CT_CatAx schema order.
309
+ - 21f58cb: feat: `ChartSpec.categoryAxisNoMultiLevelLabel` — toggle multi-level
310
+ (hierarchical) category labels via `<c:catAx><c:noMultiLvlLbl val/>`.
311
+ PowerPoint defaults to `0` (multi-level labels stack); set to `true`
312
+ to flatten hierarchical categories into a single row. Read by
313
+ chart-reader, written by chart-builder at the schema-required last
314
+ position inside `<c:catAx>`.
315
+ - c561df4: feat: `ChartSpec.categoryAxisNumberFormat` — number-format code for the
316
+ category-axis tick labels (`<c:catAx><c:numFmt formatCode="…"/>`). Most
317
+ useful on date-style categories (`"mm/dd/yyyy"`, `"mmm-yyyy"`) but
318
+ accepts any Excel format string. Independent of `valueAxis.numberFormat`.
319
+ Read by chart-reader, written by chart-builder in the correct CT_CatAx
320
+ schema order (after `<c:title>`, before `<c:majorTickMark>`).
321
+ - 3ecc11b: feat(chart): category-axis label-skip + position. `ChartSpec.categoryAxisTickLabelSkip`
322
+ reads `<c:catAx><c:tickLblSkip val="N"/>` (render every Nth label),
323
+ and `categoryAxisTickLabelPos` reads `<c:tickLblPos val="…"/>`
324
+ (`'none'` hides labels but keeps the axis line; `'low'`/`'high'`/
325
+ `'nextTo'` are the other tokens). Playground honors both — dense
326
+ time-series charts with `tickLblSkip="5"` no longer overlap their
327
+ labels.
328
+ - e04ccff: feat: `ChartSpec.dataLabels` carries the chart-level `<c:dLbls>` toggles
329
+ — `showValue`, `showCategory`, `showSeriesName`, `showPercent` — read
330
+ from each plotted-kind element. Playground projects them onto bar /
331
+ column tops (numeric value above each bar) and pie / doughnut slices
332
+ (value, percent, and / or category text painted at the slice mid-arc).
333
+
334
+ Adds the `ChartDataLabels` interface to the public type surface.
335
+
336
+ - 199031b: feat: chart data labels honor `<c:dLbls><c:numFmt formatCode="…"/>`.
337
+ `ChartDataLabels.numberFormat` exposes the format code on both
338
+ chart-level and per-series toggle groups, and the playground renderer
339
+ projects value labels through the same Excel-format subset the value
340
+ axis already supports (`"0%"`, `"$#,##0"`, `"0.00"`, etc). Per-series
341
+ formats win over the chart-level default.
342
+ - b77c0ed: feat(chart): `ChartSpec.dispBlanksAs` reads `<c:dispBlanksAs>`
343
+ (`'gap' | 'zero' | 'span'`). Playground line / area renderer:
344
+
345
+ - `gap` (default): breaks the path on null values
346
+ - `zero`: substitutes 0 so the line dips to the baseline
347
+ - `span`: connects the surrounding points across the gap
348
+
349
+ Previously every null value was coerced to 0, which silently
350
+ flattened the chart whenever the deck had genuine missing data.
351
+
352
+ - 0eee0da: feat: `ChartDataLabels.textStyle` — the default-run text style for chart
353
+ data labels is now read and written. `<c:dLbls><c:txPr><a:defRPr/>`
354
+ is parsed into `ChartTextStyle` (sizePt / bold / italic / color) and
355
+ emitted in CT_DLbls schema order (after `<c:numFmt>`, before
356
+ `<c:dLblPos>`). Both the chart-level `dataLabels` and per-series
357
+ `series[i].dataLabels` honor the field.
358
+ - 17f57b3: feat: `ChartSeries.pointColors` — sparse map of per-data-point fill
359
+ overrides read from `<c:ser><c:dPt><c:spPr><a:solidFill>`. Pie /
360
+ doughnut decks almost always emit one of these per slice; the playground
361
+ now paints each slice in its authored color (and reflects it in the
362
+ legend swatches) rather than cycling through the accent palette.
363
+ - 4d2cecb: feat(chart): `ChartSpec.dropLines` and `hiLowLines` read
364
+ `<c:dropLines>` and `<c:hiLowLines>` on line / area / stock plots.
365
+ Playground renders drop lines from each first-series data point down
366
+ to the value axis (dashed gray) and hi-low lines as a vertical span
367
+ between the highest and lowest series value at each category
368
+ (solid darker gray). The latter is the canonical OHLC pattern.
369
+ - 263bf52: feat: chart reader now recognises scatter, bubble, radar, stock, and
370
+ (2D / 3D) surface charts and degrades them to the closest already-
371
+ modelled kind so renderers paint something useful instead of the
372
+ "unsupported chart kind" placeholder. Scatter / bubble series read
373
+ their `<c:yVal>` channel; their `<c:xVal>` / `<c:bubbleSize>` are
374
+ not yet surfaced.
375
+ - 0a9236f: feat(chart): `ChartSpec.gapWidthPct` and `overlapPct` read from
376
+ `<c:gapWidth>` and `<c:overlap>` on bar / column plots. Playground
377
+ sizes bars per ECMA-376 §21.2.2.75 — `barW = groupW / (clusterUnits +
378
+ gapWidth/100)` with `clusterUnits = 1 + (S - 1)(1 - overlap/100)` —
379
+ so authored bar spacing matches PowerPoint instead of the hard-coded
380
+ 0.8 / 0.7 ratios.
381
+ - b88dbb8: feat(chart): `ChartSpec.valueAxisMajorGridlines` / `valueAxisMinorGridlines`
382
+ read the presence of `<c:majorGridlines/>` / `<c:minorGridlines/>`
383
+ under `<c:valAx>`. Playground hides gridlines when `majorGridlines`
384
+ is explicitly `false` (absent in the source XML) — common on KPI
385
+ charts that show clean bars / lines without horizontal rules behind
386
+ them. Tick labels still render.
387
+ - 4caa5ad: feat(chart): `ChartSeries.invertIfNegative` reads `<c:ser>
388
+ <c:invertIfNegative val="1"/>`. Playground's bar / column renderer
389
+ paints negative bars in a darker shade of the series color when the
390
+ flag is set — matching PowerPoint's profit/loss visualization.
391
+ - b603115: feat: `ChartSpec.language` (`<c:chartSpace><c:lang val=…/>`) and
392
+ `ChartSpec.date1904` (`<c:date1904 val=…/>`) — chartSpace-level Office
393
+ metadata round-tripped for parity. `language` is the Office UI
394
+ language code (e.g. `'en-US'`, `'ja-JP'`); `date1904` selects the
395
+ 1904 date epoch (default `false` = Excel 1900 epoch, surface only
396
+ when explicitly true). pptx-kit's renderers don't act on either yet.
397
+ - 028e3b7: feat: chart `<c:legend><c:legendEntry><c:delete val="1"/>` honored.
398
+ `ChartSpec.legend.hiddenIndices` carries the series indices the
399
+ author wants suppressed from the legend (typically trendline series).
400
+ The playground filters the parallel legend arrays (names, colors,
401
+ marker glyphs) in lock-step so the remaining entries stay aligned,
402
+ without affecting plotted data.
403
+ - c141173: feat(chart): `ChartSpec.legend` carries the `<c:legend><c:legendPos>`
404
+ token — `'r' | 't' | 'b' | 'l' | 'tr'`. Playground projects each
405
+ onto the appropriate edge (horizontal row for top / bottom, vertical
406
+ stack for the side / corner positions). Charts whose `<c:legend>`
407
+ sets `position` to `null` paint without a legend.
408
+ - 9a49faf: feat(chart): `ChartAxisScaling.majorUnit` and `minorUnit` read
409
+ `<c:valAx><c:majorUnit>` / `<c:minorUnit>` tick spacing. Playground's
410
+ value-axis renderer emits ticks at each multiple of the authored
411
+ majorUnit instead of nice-rounded auto-ticks when present.
412
+ - f99d548: feat: `ChartSpec.valueAxisMinorTickMark` and `categoryAxisMinorTickMark`
413
+ — minor-tick-mark mode siblings of the existing `*MajorTickMark` pair.
414
+ Maps to `<c:catAx><c:minorTickMark val="in|out|cross|none"/>` and the
415
+ value-axis equivalent. Read by chart-reader, written by chart-builder
416
+ in the correct schema order (right after `<c:majorTickMark>`).
417
+ - 28d77ea: fix: chart categories accept `<c:cat><c:numRef>` (numeric / date
418
+ categories). Previously the category-axis dropped to an empty
419
+ labels array when the chart authored a numeric category channel
420
+ (common for date-axis line charts authored in Excel). Falls back
421
+ to formatting each cached numeric value as a string so date /
422
+ number cats appear on the axis instead of disappearing.
423
+ - 7b3ba0a: feat(chart): axis number formats now accept Excel's `"$"#,##0`
424
+ quoted-literal prefix / suffix syntax. PowerPoint typically emits
425
+ currency as `"$"#,##0` (or `"\$"#,##0`) rather than the bare `$`
426
+ form, so the previous detection missed it.
427
+ - d2f86d2: feat(chart): `ChartAxisScaling.numberFormat` reads `<c:valAx><c:numFmt
428
+ formatCode="…"/>`. Playground projects the most common Excel format
429
+ codes to axis labels — percent (`'0%'`, `'0.0%'`), thousand
430
+ separator (`'#,##0'`, `'#,##0.0'`), and currency prefixes
431
+ (`'$#,##0'`, `'¥#,##0'`). Other codes fall through to the generic
432
+ auto-formatted label.
433
+ - 3efdbeb: feat(chart): `ChartSpec.titleOverlay` and `ChartSpec.legend.overlay`
434
+ read `<c:title><c:overlay>` / `<c:legend><c:overlay>`. When `true`,
435
+ the title / legend sits on top of the plot area instead of taking a
436
+ horizontal strip. Playground sizes the plot area accordingly — gives
437
+ the chart back the extra vertical real estate when overlay is set.
438
+ - 4cde872: feat: `ChartSpec.plotVisibleCellsOnly` — toggle `<c:plotVisOnly val/>`.
439
+ PowerPoint's default is `true` (only plot visible cells); the field
440
+ exists to let authors opt into `false` (plot hidden rows / columns too).
441
+ The reader surfaces `false` only when the wire is explicitly `0` so
442
+ round-tripping the common default doesn't drag a redundant explicit
443
+ `true` into the spec.
444
+ - 693ba3e: feat: `ChartSpec.roundedCorners` — round-trip the chartSpace-level
445
+ `<c:roundedCorners val>` toggle. PowerPoint's default is `false`; the
446
+ reader surfaces `true` only when the wire is explicitly `1` and the
447
+ builder emits the element only when authored, so common defaults stay
448
+ clean. Schema position is BEFORE `<c:chart>` (per CT_ChartSpace).
449
+ - 733120a: feat(chart): `ChartSeries.smooth` reads `<c:smooth val="1"/>`. Playground
450
+ line / area renderer interpolates a cubic-Bézier curve through the
451
+ data points (Catmull-Rom-to-Bezier with 0.5 tension) when `smooth` is
452
+ true, matching PowerPoint's "smooth line" preset visually.
453
+ - d581121: feat(site/playground): bar (horizontal), line, and area charts now
454
+ honour `ChartSpec.grouping` for stacked / percentStacked layouts —
455
+ matching the column-chart treatment added previously. Data labels
456
+ render inside the stacked segments for bar (white bold), at the
457
+ appropriate cumulative position for line / area, and percent-stacked
458
+ versions normalize each category to 100%.
459
+ - 33c2c11: feat: `ChartSpec.grouping` carries the `<c:grouping>` token —
460
+ `'clustered' | 'stacked' | 'percentStacked' | 'standard'`. Playground
461
+ column chart renders stacked / percent-stacked layouts: series stack
462
+ within each category, and percent-stacked normalises to 0..100% per
463
+ column with in-bar value labels.
464
+
465
+ Adds the `ChartGrouping` type to the public surface.
466
+
467
+ - 53148e4: feat: `ChartSpec.chartStyle` — round-trip the chartSpace-level
468
+ `<c:style val="N"/>` PowerPoint chart-style preset (1..48). Encodes a
469
+ curated combo of theme accent colors, gradients, effects, and font
470
+ sizes from the PowerPoint "Chart Styles" gallery. Read and written for
471
+ round-trip parity; pptx-kit's renderers don't interpret the preset
472
+ yet, but the field survives save/reload.
473
+ - b1cfda3: feat: `ChartSpec.categoryAxisTickMarkSkip` — the second half of the
474
+ ECMA-376 `<c:catAx>` skip pair. `<c:tickLblSkip>` (already supported)
475
+ controls label-skip stride; `<c:tickMarkSkip val="N"/>` independently
476
+ draws every Nth tick mark. Useful when you want fewer label collisions
477
+ but the same dense tick lattice. Read by chart-reader and written by
478
+ chart-builder.
479
+ - 2599a46: feat: chart titles read `<c:tx><c:strRef>` workbook-cell references.
480
+ Previously only literal `<c:rich>` titles surfaced; titles authored
481
+ via Excel's "Link to source cell" wizard (which emits `<c:strRef>`
482
+ with a `<c:strCache>` of the resolved text) now flow through to
483
+ `ChartSpec.title` as the cached value. Affects the title shown above
484
+ the chart and, transitively, axis-title rendering.
485
+ - da1e50d: feat: chart titles honor `<a:rPr>` font / color overrides.
486
+ `ChartSpec.titleStyle` carries the authored size (in pt), bold, italic,
487
+ and fill color extracted from the title's first `<a:r><a:rPr>` (or
488
+ `<a:pPr><a:defRPr>` as fallback). The playground renderer projects
489
+ those through to the SVG `<text>`. Templates that brand their chart
490
+ titles to a non-default size / color finally render with the authored
491
+ look.
492
+ - 5f84cfc: feat: `ChartTrendline.displayEquation` and `ChartTrendline.displayRSquared`
493
+ — two booleans that toggle the regression-equation label and R²
494
+ coefficient overlay next to a trendline. Map to
495
+ `<c:trendline><c:dispEq val="1"/>` and `<c:dispRSqr val="1"/>`. Read by
496
+ chart-reader; written by chart-builder in the correct CT_Trendline
497
+ schema order (after `<c:backward>`, before any `<c:trendlineLbl>`).
498
+ - a978251: feat: `ChartTrendline.name` — round-trip a custom trendline label
499
+ (`<c:trendline><c:name>…`). PowerPoint auto-generates a label like
500
+ "Linear (X)" or "MA(5) (X)" when this element is omitted; authors who
501
+ want a different label (or who imported one from another tool) now
502
+ have the field. Read by chart-reader; written by chart-builder at the
503
+ CT_Trendline schema-required first position (before `<c:spPr>`).
504
+ - 57eeffa: feat(chart): `ChartSeries.trendline` reads `<c:trendline>` —
505
+ regression type (linear / exp / log / poly / power / movingAvg),
506
+ moving-average period, polynomial order, and the trendline's stroke
507
+ color. Playground overlays a dashed trendline on bar / column / line
508
+ charts; linear / log / exp use fitted regressions, movingAvg uses a
509
+ rolling mean.
510
+
511
+ Adds the `ChartTrendline` type to the public surface.
512
+
513
+ - 24b2794: feat: `ChartSpec.valueAxisCrossBetween` — controls whether the value
514
+ axis crosses the category axis _between_ tick marks (the default for
515
+ bar/column/area) or _at_ each tick mark (the default for line/scatter).
516
+ Maps to `<c:valAx><c:crossBetween val="between|midCat"/>`. Read by
517
+ chart-reader, written by chart-builder in the correct CT_ValAx schema
518
+ order (after `<c:crossesAt>`).
519
+ - 6a236be: feat: `ChartSpec.valueAxisCrosses` — controls where the category axis
520
+ crosses the value axis. Accepts either an enum keyword
521
+ (`'autoZero' | 'min' | 'max'` → `<c:valAx><c:crosses val=…/>`) or a
522
+ numeric tagged form (`{ at: N }` → `<c:valAx><c:crossesAt val=N/>`).
523
+ The two forms are mutually exclusive per the schema; `crossesAt` wins
524
+ when both are present on read. Read by chart-reader, written by
525
+ chart-builder in the correct CT_ValAx schema order (after `<c:crossAx>`).
526
+ - b2c1304: feat: chart `<c:varyColors>` for single-series bar / column.
527
+ `ChartSpec.varyColors` carries the `<c:plottedKind><c:varyColors val="1"/>`
528
+ flag. When set and the chart has exactly one series, the renderer
529
+ assigns each data point a distinct accent color (mirroring
530
+ PowerPoint's "Vary colors by point" toggle for column / bar). Pies
531
+ already varied colors implicitly.
532
+ - 69431a9: feat: `getSlideColorMapOverride(slide)` returns the slide's
533
+ `<p:clrMapOvr><a:overrideClrMapping/>` token-remap, or `null` when the
534
+ slide inherits the master's color map. Returned as a plain `Record`
535
+ with the eight stable tokens (`bg1`, `tx1`, `bg2`, `tx2`, `accent1`-
536
+ `accent6`, `hlink`, `folHlink`) keyed to their override targets.
537
+ Useful for renderers that need to know when a slide reinterprets the
538
+ theme's color story.
539
+ - 263bf52: feat: apply ECMA-376 §20.1.2.3.x color transforms when resolving colors.
540
+
541
+ - New `resolveDrawingColor(colorEl, theme)` resolves any DrawingML color
542
+ element (`<a:srgbClr>` / `<a:schemeClr>` / `<a:sysClr>` / `<a:prstClr>`)
543
+ with all transform children (`<a:lumMod>`, `<a:lumOff>`, `<a:shade>`,
544
+ `<a:tint>`, `<a:satMod>` / `Off`, `<a:hueMod>` / `Off`, `<a:gray>`,
545
+ `<a:inv>`, `<a:comp>`) applied. Scheme tokens are looked up against
546
+ the supplied theme.
547
+ - New `getShapeFillColorResolved(pres, shape)` and
548
+ `getShapeStrokeColorResolved(pres, shape)` return the exact `#RRGGBB`
549
+ PowerPoint paints — useful for renderers / exporters where the legacy
550
+ `getShapeFillColor` / `getShapeStrokeColor` strings (`#RRGGBB` or
551
+ `scheme:<token>`) miss both scheme resolution and color transforms.
552
+ - `getShapeRunFormatEffective` now applies the same pipeline at every
553
+ layer of the rPr cascade, so a run inheriting `accent1 lumMod=40000
554
+ lumOff=60000` (PowerPoint's "Accent 1, Lighter 60%") resolves to the
555
+ concrete tinted hex instead of leaking the raw token through.
556
+
557
+ - 263bf52: feat(site/playground): bent / curved connector routing.
558
+
559
+ `bentConnector{2,3,4,5}` render as the matching L / Z / two-step /
560
+ three-step paths, and `curvedConnector{2,3,4,5}` render as quadratic
561
+ / cubic Bézier curves between the connector's bounding-box endpoints.
562
+ Previously every connector preset projected to a straight line; flow-
563
+ chart and diagram decks now show the right cadence.
564
+
565
+ - e65228f: feat: read custom geometry. New `getShapeCustomGeometry(shape)` returns a
566
+ shape's `<a:custGeom>` (ECMA-376 §20.1.9) as a fully-evaluated path list —
567
+ guide formulas (`avLst`/`gdLst`, all §20.1.9.11 operators) are resolved
568
+ against the shape extents so the returned `moveTo`/`lnTo`/`arcTo`/
569
+ `quadBezTo`/`cubicBezTo`/`close` commands carry only numbers. The preview
570
+ renderer now draws custom geometry as a real SVG path (arcs converted to
571
+ cubic Béziers) instead of a labelled rectangle placeholder; only a custGeom
572
+ that fails to evaluate still falls back, marked `data-pptx-fallback`.
573
+ - 1e774b8: feat(site/playground): pie / doughnut / line / area honor
574
+ `<c:dLblPos>` data-label positions. Pie supports `ctr` (default
575
+ midline), `inEnd` (just inside the rim), and `outEnd` (outside the
576
+ pie, with a darker fill so it shows on the chart-area backdrop). Line
577
+ and area chart per-point labels honor `ctr`, `t` (default), `b`, `l`,
578
+ `r`. Column / bar already shipped in the prior batch.
579
+ - fc019d1: feat: chart data label position. `ChartDataLabels.position` carries the
580
+ `<c:dLbls><c:dLblPos val="…"/>` token (typed as
581
+ `ChartDataLabelPosition`). The reader extracts it at both chart-level
582
+ and per-series scope. The playground renderer projects `ctr`, `inEnd`,
583
+ `outEnd`, `inBase` onto clustered column and bar labels — outside-end
584
+ remains the default, but authored positions now move labels inside the
585
+ bar or to the base as PowerPoint shows them.
586
+ - 3cfba8d: feat: chart data label separator. `ChartDataLabels.separator` carries
587
+ the `<c:dLbls><c:separator>…</c:separator>` text used to join
588
+ multiple label parts (value + percent + category etc.). The pie /
589
+ doughnut renderer threads the per-series override, falling back to
590
+ the chart-level separator and finally to a single space. Common
591
+ values: `", "`, `"\n"`, `"; "`.
592
+ - 003e7b5: feat: density-array companions for tables and images —
593
+ `getPresentationTableCountsBySlide(pres)` and
594
+ `getPresentationImageCountsBySlide(pres)`. Both return a dense
595
+ per-slide count array (0 for slides without that asset kind),
596
+ matching the shape / chart / comment / text-length counterparts.
597
+ Completes the deck-density family.
598
+ - fd1519a: feat(site/playground): `<c:dispUnits>` value-axis label. When the
599
+ chart authors a display-units token (`thousands`, `millions`, etc.)
600
+ the value-axis now emits an italic "Thousands" / "Millions" /
601
+ … label rotated alongside the axis (vertical orientation) or to
602
+ the right of the rightmost tick (horizontal). Completes the
603
+ display-units rendering — values are already divided, and now the
604
+ scale self-describes.
605
+ - c5f0b60: feat: chart value-axis honors `<c:dispUnits><c:builtInUnit/>`.
606
+ `ChartAxisScaling.displayUnits` carries the authored scale token
607
+ (`hundreds`, `thousands`, `millions`, etc.). The playground divides
608
+ each value-axis tick by the corresponding divisor before formatting,
609
+ so charts authored "in millions" finally render as `10` / `20` /
610
+ `30` instead of `10000000`.
611
+ - 263bf52: feat: add `getShapeRunFormatEffective(pres, shape, p, r)` — resolves a
612
+ run's character properties (font, size, color, bold, italic, underline)
613
+ through the full ECMA-376 §21.1.2.4.7 inheritance chain: run `<a:rPr>` →
614
+ `<a:endParaRPr>` → paragraph `<a:defRPr>` → text-body `<a:lstStyle>` →
615
+ matching layout placeholder → matching master placeholder → master
616
+ `<p:txStyles>` → theme `<a:fontScheme>`. Theme tokens like `+mj-lt` are
617
+ expanded to the deck's major/minor typefaces. The existing
618
+ `getShapeRunFormat` still returns the literal `<a:rPr>` only.
619
+ - 25654cf: feat: `getShapeEffectsEffective(pres, shape)` walks the layout →
620
+ master placeholder cascade for `<a:effectLst>`. Effect lists override
621
+ rather than compose (matching PowerPoint's behaviour), so the first
622
+ layer that supplies any effects wins. Playground uses it so
623
+ placeholder shadows / glows / soft edges inherited from the master
624
+ finally render on slides that don't repeat the effect list.
625
+ - acc9b15: feat: effects & fills polish. The reflection effect (`a:reflection`) now
626
+ renders as a vertically mirrored, gradient-masked copy honoring start/end
627
+ alpha, distance, and the signed `sy` scale; picture bullets (`a:buBlip`)
628
+ render as real inline images in both text layout modes via the new core
629
+ reader `getParagraphBulletImageBytes` (the "■" fallback remains only when
630
+ bullet bytes are genuinely unavailable); and gradient fills inherited
631
+ through the placeholder layout/master cascade resolve via the new
632
+ `getShapeGradientFillEffective` instead of painting a hardcoded orange
633
+ tint.
634
+ - 263bf52: feat: `getShapeEffects(pres, shape)` returns every effect on the
635
+ shape's `<a:effectLst>` (`outerShdw`, `innerShdw`, `glow`, `reflection`,
636
+ `softEdge`, `blur`) in document order, with each effect's color
637
+ (transform-resolved against the theme), opacity, blur radius, distance,
638
+ and angle. PowerPoint composes multiple effects in a single filter
639
+ stack — the existing `getShapeEffect` only surfaced the first one.
640
+
641
+ The playground renderer now emits an SVG `<filter>` chain that
642
+ composes the same effects, including a synthesized inner shadow
643
+ (SVG has no `feInnerShadow` primitive — built via offset + composite).
644
+
645
+ Also adds the `ShapeEffectAny` type union to the public surface.
646
+
647
+ - 18d3ceb: feat: `getShapeFillEffective(pres, shape)` walks the layout → master
648
+ placeholder cascade when the shape's own fill is `'inherit'`. Returns
649
+ the first non-inherit fill found. Playground reaches for it as its
650
+ primary fill source so placeholder fills authored on the master /
651
+ layout finally show through.
652
+ - 81ce680: feat: `findShapesByPreset(slide, preset)` returns every shape whose
653
+ `<a:prstGeom prst="…"/>` matches. Useful for diagram introspection:
654
+ find all `'leftArrow'`s for a workflow swap, replace every `'cloud'`
655
+ with `'rect'`, etc. Shapes without a preset (custGeom / pictures /
656
+ charts / tables / connectors / groups) are filtered out.
657
+ - 019a934: feat: `findChartsWithDataLabels(slide)` — slide-scoped auditor for
658
+ charts whose chart-level or per-series `dataLabels` enable at least
659
+ one of `showValue` / `showCategory` / `showSeriesName` / `showPercent`.
660
+ Purely presence-based; doesn't validate numberFormat or position.
661
+ Charts whose kind isn't modeled are skipped.
662
+ - cb5d037: feat: `findChartsWithTrendlines(slide)` — slide-scoped finder for
663
+ charts that carry at least one `<c:trendline>` on any of their
664
+ series. Useful for deck-audit reports — trendlines are easy to add
665
+ and easy to forget. Charts whose kind isn't modeled are skipped.
666
+ - 1653804: feat: `findCommentsByAuthor(pres, authorName)` and
667
+ `findSlidesWithCommentsByAuthor(pres, authorName)` now accept a
668
+ `RegExp` as well as a literal string. Useful for "every comment from
669
+ review bots" (`/^review-bot/`) or "every comment from anyone with a
670
+ given email domain" patterns. Backward compatible — string callers
671
+ still get exact-equality matching.
672
+ - b6d9ea4: feat: `findShapeByName(slide, name)` now accepts a `RegExp` as well
673
+ as a literal string. Mirrors the RegExp support just landed on
674
+ `findShapesByName` (multi-match). Returns the first match in document
675
+ order; backward compatible with existing string callers.
676
+ - 7e59ac4: feat: `findShapeInPresentation(pres, name)` now accepts a `RegExp` as
677
+ well as a literal string. Mirrors the RegExp support on the
678
+ slide-scoped `findShapeByName`. Backward compatible — string callers
679
+ still get exact-equality.
680
+ - 70a2327: feat: `findShapesByEffect(pres, slide, kind)` — returns every shape on
681
+ the slide whose `<a:effectLst>` carries an effect of the given `kind`
682
+ (`'outerShdw'`, `'innerShdw'`, `'glow'`, `'reflection'`, `'softEdge'`,
683
+ `'blur'`). Pure presence check; doesn't walk the layout / master
684
+ cascade. Useful for "which shapes have a shadow / glow on this
685
+ slide?" audits.
686
+ - 94c4480: feat: `findShapesByHyperlink(slide, url)` — slide-scoped finder that
687
+ returns every shape whose hyperlink target matches `url` (substring or
688
+ `RegExp`). Pairs the existing presentation-level
689
+ `findSlidesByHyperlink` for cases where the caller already has a
690
+ specific slide and wants the linking shapes inside it.
691
+ - e71664d: feat: `findShapesByName(slide, name)` now accepts a `RegExp` as well
692
+ as a literal string. Useful when template-cloned shapes share a
693
+ prefix (`'TextPlaceholder1'`, `'TextPlaceholder2'`, …). Backward
694
+ compatible — string callers still get exact-equality matching.
695
+ - f57a4ab: feat: `findShapesInRect(slide, x, y, w, h)` — marquee-style region
696
+ finder. Returns every shape whose bounds overlap the rectangle
697
+ (touching edges count). Shapes with no resolvable bounds are skipped.
698
+ Companion to `findShapesAtPoint(slide, x, y)` for cases where the
699
+ caller wants a region of the slide rather than a single point.
700
+ - a7c00cb: feat: `findShapesWithAnimation(slide)` — returns every shape on the
701
+ slide whose `getShapeAnimation` is not `null`. Pair to
702
+ `slideHasAnimations`. Useful for "which shapes on this slide actually
703
+ animate?" audits before exporting to a video pipeline that doesn't
704
+ honor PowerPoint's timing tree.
705
+ - 87d7fbb: feat: `findShapesWithHyperlinks(slide)` — every shape on the slide
706
+ that carries any hyperlink, regardless of target. Counterpart to
707
+ `findShapesByHyperlink(slide, url)` (which requires a matching URL)
708
+ for "audit every clickable shape on this slide" workflows.
709
+ - 5cc4f75: feat: `findSlideByTitle(pres, title)` now accepts a `RegExp` as well
710
+ as a literal string. Pairs the RegExp support on
711
+ `findSlidesByText` / `findShapeByName` / `findCommentsByAuthor`.
712
+ Backward compatible — string callers still get exact-equality.
713
+ - 134943d: feat: `findSlidesByLayoutPartName(pres, layoutPartName)` — finds every
714
+ slide whose resolved layout part name matches `layoutPartName` (e.g.
715
+ `'/ppt/slideLayouts/slideLayout3.xml'`). Pair to the existing
716
+ `findSlidesByLayoutName` / `findSlidesByLayoutType`. Keyed on the
717
+ actual package path, so it's stable across template-name collisions
718
+ and PowerPoint UI locales.
719
+ - 20613b5: feat: `findSlidesWithChartKind(pres, kind)` — kind-filtered variant of
720
+ the existing `getSlidesWithCharts`. Returns every slide carrying at
721
+ least one chart of the given `ChartKind` (`'bar'`, `'column'`,
722
+ `'line'`, `'pie'`, `'doughnut'`, `'area'`). Built on `getSlideCharts`
723
+ so the predicate respects the spec the renderers actually see.
724
+ - 7c545c9: feat: `findSlidesWithChartTrendlines(pres)` — deck-level variant of
725
+ the slide-scoped `findChartsWithTrendlines`. Returns every slide
726
+ carrying at least one chart with a trendline on any series. Useful
727
+ for "audit every trendline in this deck" workflows before publishing.
728
+ - 666343d: feat: `getEmptySlides(pres)` — returns every slide whose `<p:spTree>`
729
+ carries no shapes (per `getSlideShapes`). Useful as a pre-publish
730
+ "find the section dividers I forgot to fill in" check.
731
+ - b4dbcc0: feat: `getPresentationChartCountsBySlide(pres)` — dense per-slide chart
732
+ count array. Counts every chart returned by `getSlideCharts` regardless
733
+ of whether its spec parsed; pair with `getPresentationChartKindCounts`
734
+ for kind-level totals. Rounds out the density-array family alongside
735
+ `getPresentationCommentCountsBySlide`,
736
+ `getPresentationShapeCountsBySlide`, and
737
+ `getPresentationTextLengthsBySlide`.
738
+ - a4ca6ca: feat: `getPresentationChartKindCounts(pres)` — deck-wide histogram of
739
+ `ChartKind` → count. Returns a frozen `Record` with every kind
740
+ present (zeros for absent kinds), so destructuring and chart-style
741
+ audits stay typed without runtime checks. Charts whose spec doesn't
742
+ parse are skipped, matching `findChartByKind` / `findSlidesWithChartKind`.
743
+ - f7dbcc4: feat: `getPresentationCommentCountsByAuthor(pres)` — deck-wide
744
+ histogram of comment counts keyed by author display name. Useful for
745
+ "who reviewed this deck the most?" audits. Authors sharing a display
746
+ name get merged into the same bucket; pair with
747
+ `getPresentationCommenters` when authors with identical names need to
748
+ be kept separate by `id`.
749
+ - be1a608: feat: `getPresentationCommentCountsBySlide(pres)` — dense per-slide
750
+ comment count array. Every slide appears as an element (count `0`
751
+ when the slide has no comments), so callers can chart comment
752
+ density per slide without re-indexing.
753
+ - 2789bb3: feat: `getPresentationHyperlinkCountsBySlide(pres)` — dense per-slide
754
+ hyperlink count array. Counts shapes whose `getShapeHyperlink` is
755
+ non-null. Cheaper than `getAllHyperlinks` when the caller only wants
756
+ per-slide counts. Rounds out the deck-density family.
757
+ - 2f67bd7: feat: `getPresentationNotesLengthsBySlide(pres)` — dense per-slide
758
+ speaker-notes length array. Pair with
759
+ `getPresentationTextLengthsBySlide` for handout / talk-track audits —
760
+ slides with little on-screen text but heavy notes are usually the
761
+ slow part of a presentation.
762
+ - 1974b01: feat: `getPresentationShapeCountsBySlide(pres)` — dense per-slide
763
+ shape count array. Counts whatever `getSlideShapes` flattens (top-
764
+ level + group-children). Useful for charting shape density per slide
765
+ and identifying outliers for cleanup.
766
+ - 6569f9d: feat: `getPresentationTextLengthsBySlide(pres)` — dense per-slide
767
+ visible-text length array. Counts code points (surrogate pairs as 1)
768
+ per `getSlideTextLength`. Pair with `getPresentationShapeCountsBySlide`
769
+ for slide-density audits.
770
+ - b793c74: feat: `getSlideLayoutUsageCountsByType(pres)` — companion to
771
+ `getSlideLayoutUsageCounts`, but keyed on the OOXML layout-type enum
772
+ token (`title`, `obj`, `twoObj`, `blank`, …) instead of the user-
773
+ visible name. Stable across PowerPoint UI locales. Useful for "how
774
+ many content slides vs. dividers vs. title slides?" audits.
775
+ - 3891fa2: feat: `getSlideLayoutUsageCounts(pres)` — layout name → number-of-slides
776
+ histogram. Every layout enumerated by `getSlideLayouts` appears as a
777
+ key (count `0` for unreferenced layouts), so the function surfaces
778
+ unused layouts directly — useful for trimming template decks that
779
+ ship with placeholder layouts the working deck never picks up.
780
+ - aad46e4: feat: `getSlideMasterUsageCounts(pres)` — master part name → number of
781
+ slides chaining to that master. Every master in the package appears as
782
+ a key (count `0` for unreferenced masters), so it surfaces unused
783
+ masters directly. Pair with `getSlideLayoutUsageCounts` for the
784
+ layout layer in multi-master template decks.
785
+ - 02fe159: feat: `getSlideTables(slide)` — returns every table graphic-frame
786
+ shape on the slide, in document order. Pair to `getSlideCharts` for
787
+ cases where the caller wants just the tables; convenience over
788
+ `getSlideShapes(slide).filter(isTableShape)`.
789
+ - acf5880: feat: `getUnusedSlideLayouts(pres)` — returns the layouts in the
790
+ package that no slide references. Useful when trimming a template
791
+ deck — unused layouts contribute parts and rels without ever
792
+ rendering. Iteration order matches `getSlideLayouts`.
793
+ - eca84ce: feat: `getUnusedSlideMasters(pres)` — master part names that no slide
794
+ chains to (count of `0` in `getSlideMasterUsageCounts`). Pair to
795
+ `getUnusedSlideLayouts`. Useful when trimming multi-master template
796
+ decks of dead theme variants.
797
+ - 263bf52: feat: `getShapeGradientFill` now surfaces non-linear gradient paths
798
+ (`<a:path path="circle|rect|shape">`) and the `<a:fillToRect>` focus
799
+ rectangle. `GradientFillOptions` gains `path` and `focus` fields so
800
+ renderers can reproduce radial, rectangular, and shape-following
801
+ gradients instead of falling back to a linear approximation.
802
+
803
+ The playground renderer emits an SVG `<radialGradient>` for the
804
+ non-linear paths, with reversed stop offsets so the first ECMA-376
805
+ stop sits at the focus center (matching PowerPoint's outward
806
+ painting order).
807
+
808
+ - 855076d: feat: chart value-axis major gridlines honor authored stroke color.
809
+ `ChartSpec.valueAxisMajorGridlineColor` extracts the
810
+ `<c:majorGridlines><c:spPr><a:ln><a:solidFill><a:srgbClr/>` color and
811
+ the playground renderer paints gridlines with it (falls through to the
812
+ existing light-gray default when no color is authored). Branded
813
+ templates with custom gridline tints finally render correctly.
814
+ - 74b227e: feat(site/playground): hyperlink tooltips. Shape and per-run
815
+ hyperlinks now surface their `<a:hlinkClick tooltip="…"/>` text —
816
+ shapes get an SVG `<title>` child on the `<a>` wrapper, runs get a
817
+ `title=` attribute on the HTML anchor. PowerPoint shows these on
818
+ hover during the slideshow; the playground now does too.
819
+ - a610e82: feat: `getShapeHyperlinkTooltip(shape)` and
820
+ `getShapeRunHyperlinkTooltip(shape, p, r)` return the
821
+ `<a:hlinkClick tooltip="…"/>` text. Tooltips show up in PowerPoint
822
+ when the user hovers a linked shape in slide-show mode — useful for
823
+ accessibility and link-preview surfaces.
824
+ - cbdda7c: feat(site/playground): render `<a:duotone>` image recolor. The filter
825
+ pipeline desaturates the picture to luminance, then samples a
826
+ two-color gradient (firstColor → secondColor) via a 16-step
827
+ `feComponentTransfer` table. Pictures with PowerPoint's Color >
828
+ Recolor preset finally render in their authored two-color tint.
829
+ - c4a89c1: feat: `getShapeImageDuotone(pres, shape)` reads the picture's
830
+ `<a:blip><a:duotone>` two-color recolor effect — the typical
831
+ "Picture Tools > Recolor" output. Returns the two hex-resolved
832
+ colors (or `null` for each that the duotone didn't author). Lets
833
+ downstream renderers project the duotone via SVG `<filter>` or
834
+ inform consumers that the picture has a color-replacement applied.
835
+ - 99fcb65: feat: image color-effect readers — `isShapeImageGrayscale(shape)`
836
+ detects `<a:blip><a:grayscl/>` (Color > Grayscale), and
837
+ `getShapeImageBiLevelThreshold(shape)` returns the threshold percent
838
+ for `<a:blip><a:biLevel thresh="…"/>` (Color > Black and White).
839
+ Renderers can project these onto CSS / SVG filters.
840
+ - 263bf52: feat: `getShapeImageLinkUrl(shape)` returns the external URL of a
841
+ picture whose `<a:blip>` carries an `r:link` (Insert > "Link to file")
842
+ instead of `r:embed`. Bytes for these aren't in the package; the
843
+ playground now shows the linked URL in the placeholder rather than a
844
+ generic "no bytes" label.
845
+ - 508627a: feat(site/playground): grayscale + biLevel image filters in the
846
+ playground. The filter pipeline now composes:
847
+
848
+ 1. brightness + contrast (linear feComponentTransfer)
849
+ 2. grayscale (luminance-preserving feColorMatrix) when
850
+ `<a:blip><a:grayscl/>` is set
851
+ 3. biLevel two-tone (discrete tableValues snapped at the authored
852
+ threshold) when `<a:blip><a:biLevel thresh="…"/>` is set
853
+
854
+ Pictures with Color > Grayscale or Color > Black and White now
855
+ render with the same visual treatment PowerPoint shows.
856
+
857
+ - 66edcbc: feat: add `isShapeTextBox(shape)` — `true` when a shape is a text box
858
+ (`<p:cNvSpPr txBox="1">`) rather than an autoshape. The two have different
859
+ default text formatting (text boxes left/top, autoshapes center/middle), so
860
+ renderers and layout code need to tell them apart.
861
+ - 263bf52: feat: `getSlideLayoutBackground(layout)` mirrors `getSlideBackground`
862
+ for slide layouts. Playground falls back to it when the slide's own
863
+ background reports `'inherit'`, so brand-color or template backgrounds
864
+ authored on the layout actually paint behind slides that don't override
865
+ the bg themselves.
866
+ - 2a1d712: feat: `getSlideLayoutBackgroundGradientFill(layout)` returns the
867
+ gradient definition when a layout's background is
868
+ `<p:bgPr><a:gradFill>`. Same shape as the slide-level variant —
869
+ renderers can reuse the same projection logic for layout gradient
870
+ backgrounds via the shared `gradientDef` helper.
871
+ - a1229d5: feat: `getSlideLayoutBackgroundShapes(pres, layout)` returns the
872
+ non-placeholder shapes on a layout as a render-ready view
873
+ (`SlideLayoutBackgroundShape` — bounds, preset, fillHex, strokeHex,
874
+ strokeWidthEmu, rotation, flip). Playground paints them behind the
875
+ slide's own shapes so brand-template decoration (corner bars, divider
876
+ lines, background rectangles) shows through on slides that don't
877
+ redefine the layout themselves.
878
+
879
+ Adds the `SlideLayoutBackgroundShape` type to the public surface.
880
+
881
+ - ba056db: feat: `getSlideLayoutBackground` now handles `<p:bgRef>` the same way
882
+ `getSlideBackground` does. Layouts in real brand templates almost
883
+ always reference the theme via `<p:bgRef>` rather than authoring an
884
+ explicit `<p:bgPr>` — picking up the inner color element as a solid
885
+ fill closes the cascade so the playground paints the right brand
886
+ color even when the slide's own background reports `inherit`.
887
+ - 3ea2ed5: feat(site/playground): line / area chart legend swatches use the
888
+ series marker glyph. The legend previously rendered every series as
889
+ a 9×9 square color rect. For `line` / `area` charts the renderer now
890
+ passes the per-series `markerSymbol` (circle / square / diamond /
891
+ triangle / star / x / plus / dash / dot) so legend entries match
892
+ the data points. Bar / column / pie keep the square swatch.
893
+ - b1073ff: feat(site/playground): right / left chart legend stack centers
894
+ vertically. Previously the `r` and `l` legend positions both
895
+ stacked from a fixed `f.y + 12` top, the same as `tr`. PowerPoint
896
+ vertically-centers right / left legends inside the chart area; the
897
+ renderer now matches by computing `yStart` from the legend's total
898
+ height. `tr` keeps the top-anchored stack.
899
+ - ee27024: feat: chart legend honors authored `<c:txPr>` font / color.
900
+ `ChartSpec.legend.textStyle` carries the same `ChartTextStyle` shape
901
+ used for the chart title and axis titles. The playground renderer
902
+ projects font-size, bold, italic, and fill color onto every legend
903
+ label across all four position layouts (right / left / top / bottom /
904
+ top-right).
905
+ - fca13ca: feat(site/playground): line and area charts paint per-point value labels
906
+ when `<c:dLbls><c:showVal val="1"/>` is set. Labels sit just above each
907
+ marker and route through the chart number-format projector (so
908
+ `<c:numFmt formatCode="0%"/>` etc. apply the same as on bar / pie).
909
+ Honors the per-series → chart-level cascade.
910
+ - 263bf52: feat: `getParagraphLineSpacing(shape, p)` returns the paragraph's
911
+ `<a:lnSpc>` as `{ kind: 'pct' | 'pts', value }`. Percent values come
912
+ through as a unit fraction (1.5 = 150%); point values are pt.
913
+
914
+ The playground projects both forms to CSS `line-height` per paragraph,
915
+ and uses the existing `getParagraphSpacing` to project spcBef / spcAft
916
+ to `margin-top` / `margin-bottom`. Text blocks now keep the vertical
917
+ rhythm the deck authored instead of falling back to a fixed line
918
+ height for everything.
919
+
920
+ - 5381e9d: feat(chart): line / area charts now overlay the per-series
921
+ `<c:trendline>` when authored. Same regression types as the
922
+ column-chart variant (linear / log / exp / movingAvg / poly+power
923
+ fallback). Only emitted on the clustered layout — stacked plots
924
+ already convey the cumulative shape.
925
+ - ef4d410: feat: `getSlideMasterBackground(pres, layout)` returns the master's
926
+ `<p:bg>` (both `<p:bgPr>` and `<p:bgRef>` forms). Playground extends
927
+ its background fallback chain to slide → layout → master so brand
928
+ backgrounds authored on the master alone finally render on inheriting
929
+ slides instead of falling through to the theme's `light1`.
930
+ - 9c7a852: feat: `getSlideMasterBackgroundGradientFill(pres, layout)` returns
931
+ the master's gradient background when `<p:bg><p:bgPr><a:gradFill>`
932
+ is authored. Completes the three-level bg cascade for gradient
933
+ backgrounds — slides can fall through slide → layout → master.
934
+ - 60df186: feat: more name-based finders now accept `RegExp` —
935
+ `findSlideLayout(pres, name)`,
936
+ `findCommentAuthorByName(pres, authorName)`, and
937
+ `findSlidesByLayoutName(pres, layoutName)`. Pairs the RegExp support
938
+ recently added to `findShapeByName` / `findShapesByName` /
939
+ `findCommentsByAuthor` / `findSlideByTitle`. String callers unchanged.
940
+ - 10a9d05: feat: `getParagraphPropertiesEffective(pres, shape, p)` — paragraph-property
941
+ cascade mirroring the rPr one. Resolves alignment, left / right / first-line
942
+ indents, line spacing, paragraph spacing (before / after), and rtl through
943
+ the paragraph → text-body lstStyle → layout placeholder lstStyle →
944
+ master placeholder lstStyle → master txStyles chain.
945
+
946
+ The playground uses it as the primary source of paragraph properties so
947
+ placeholders inherit their default alignment / line-spacing / indent from
948
+ the layout / master, with any per-slide override winning on top.
949
+
950
+ Adds the `ParagraphProperties` type to the public surface.
951
+
952
+ - 263bf52: feat: `getShapeParagraphElements(shape, paragraphIndex)` returns the
953
+ inline children of a paragraph (runs, field placeholders, and line
954
+ breaks) in document order. Renderers can walk this list to reproduce
955
+ the full visible content — footer / date / slide-number `<a:fld>`
956
+ text was previously dropped by the strict `<a:r>`-only run accessors.
957
+
958
+ The playground now uses it: footer text + slide numbers + datetime
959
+ fields show up in the preview, and `<a:br>` line breaks render as
960
+ real `<br/>` inside the foreignObject body.
961
+
962
+ Adds the `ShapeParagraphElement` discriminated union to the public
963
+ type surface.
964
+
965
+ - 263bf52: feat: `getParagraphIndent(shape, p)` returns the paragraph's
966
+ `<a:pPr marL marR indent>` values in EMU (`null` for sides the
967
+ paragraph doesn't author). Playground projects each side to CSS
968
+ `padding-left` / `padding-right` / `text-indent` and skips the
969
+ level-based default when the paragraph carries an explicit `marL`.
970
+ - 263bf52: feat: `getShapePatternFill(pres, shape)` returns the pattern preset
971
+ token plus the foreground / background colors resolved against the
972
+ deck's theme. Pairs with the existing `setShapePatternFill`. The
973
+ playground renderer now paints real SVG `<pattern>` tiles for the
974
+ common `ST_PresetPatternVal` tokens (pct5..pct90, light/dark diagonal
975
+ and horizontal/vertical stripes, grids, weave, wave, sphere, diamonds)
976
+ instead of substituting a flat tint.
977
+ - 1b08908: feat(chart): per-series `<c:ser><c:dLbls>` overrides. `ChartSeries.dataLabels`
978
+ mirrors the chart-level `ChartSpec.dataLabels`; the series-level
979
+ override wins when present. Playground's bar / column renderers
980
+ check the per-series flag first so one series can show labels while
981
+ others stay clean — common in financial decks.
982
+ - 263bf52: feat: playground now applies the picture corrections that already
983
+ shipped on the API: source-rectangle crop (`<a:srcRect>`), brightness
984
+ (`<a:lumOff>`), contrast (`<a:lumMod>`), and opacity (`<a:alphaModFix>`).
985
+
986
+ Crops project to an enlarged `<image>` element clipped to the shape's
987
+ bounds (matching PowerPoint's "Crop" tool). Brightness + contrast
988
+ compose into an SVG `<feComponentTransfer>` filter. Opacity drives
989
+ the `opacity` attribute directly.
990
+
991
+ - 7303fa8: feat(chart): `ChartSpec.firstSliceAngleDeg` reads `<c:firstSliceAng>`
992
+ and `ChartSpec.holeSizePct` reads `<c:holeSize>` for doughnut charts.
993
+ Playground rotates the first slice's starting position clockwise from
994
+ 12 o'clock per the authored angle, and sizes the doughnut hole at the
995
+ authored percent (10..90) of the outer radius instead of the
996
+ hard-coded 55%.
997
+ - 0ca34e1: feat: pie / doughnut slice explosion. `ChartSeries.pointExplosions`
998
+ exposes the per-data-point pull-out percentage from `<c:dPt><c:explosion val="N"/>`,
999
+ and the playground renderer offsets exploded slices (and their labels)
1000
+ outward along the slice mid-angle. Matches the "pulled-out" pie look
1001
+ authors get from Excel's "Vary colors by point" toggle.
1002
+ - 2e9776d: feat(site/playground): chart / media count badges on each slide.
1003
+ `getSlideCharts(slide)` and `getSlideMediaPartNames(pres, slide)`
1004
+ power two new badges (`N chart`, `N media`) showing how many chart
1005
+ shapes and how many media parts (images / audio / video) the slide
1006
+ references — useful for quick deck audits.
1007
+ - 997d507: feat(site/playground): comment badge tooltip carries the comment
1008
+ texts. The `N cmt` badge's `title=` attribute now joins each
1009
+ comment's body text so hovering surfaces the review remarks
1010
+ without opening PowerPoint.
1011
+ - 3ede588: feat(site/playground): additional slide badges — `hidden` (when
1012
+ `show="0"`) and `N cmt` (count of authored review comments). Threads
1013
+ `isSlideHidden` and `getSlideComments` through the slide-snapshot
1014
+ and surfaces both next to the existing `trans` / `anim` badges, so
1015
+ audit views see the full set of slide-level flags at a glance.
1016
+ - 7a489fa: feat(site/playground): layout-type badge tooltip carries the
1017
+ user-visible layout name. Hovering the small `obj` / `title` / etc.
1018
+ badge now reveals `layout: <Name> (type: <token>)` from
1019
+ `getSlideLayoutName(layout)`. Helps identify which authored layout
1020
+ each slide is bound to without leaving the playground.
1021
+ - d7d8571: feat(site/playground): show the slide's layout type (`title`, `obj`,
1022
+ `twoObj`, `blank`, …) as a badge next to the slide title. Reads
1023
+ `<p:sldLayout type="…">` via `getSlideLayout` + `getSlideLayoutType`
1024
+ so deck audits can spot which layout each slide is bound to without
1025
+ opening PowerPoint.
1026
+ - 5861d1e: feat(site/playground): include slide-master count in the
1027
+ "masters · layouts · sections" meta cell. `getPresentationSummary`
1028
+ already returned layout / section counts; the playground now also
1029
+ calls `getSlideMasterCount` so multi-master decks surface that fact
1030
+ in the audit panel.
1031
+ - 9a64bb4: feat(site/playground): expose `getPresentationSummary` data in the
1032
+ meta panel — theme name, layout / section counts, total shape count,
1033
+ and deck-wide flags (hidden slides, charts, comments, animations).
1034
+ Gives deck audits a one-glance overview without scrolling through
1035
+ every slide.
1036
+ - d9ba44d: feat(site/playground): render section dividers in the slide list.
1037
+ Reads `getSlideSections(pres)`, maps each section's first slide to
1038
+ the section's name, and renders a dashed divider above that slide
1039
+ in the slide list. Deck audits can now see the section grouping at
1040
+ a glance.
1041
+ - 62069f7: feat(site/playground): make the per-slide number an anchor link.
1042
+ Each slide's two-digit index in the head row is now an `<a
1043
+ href="#slide-N">` link, so users can right-click → "Copy link
1044
+ address" to share a deep link to a specific slide. Paired with the
1045
+ `id="slide-N"` already on each `<li>`, the link also scrolls the
1046
+ slide into view when clicked.
1047
+ - ffec23d: feat(site/playground): show speaker notes under each slide. The
1048
+ playground now calls `getSlideNotes` for every slide and renders a
1049
+ collapsible `<details>` block when notes exist, so users can
1050
+ inspect the deck author's notes without opening PowerPoint.
1051
+ - b90f1bc: feat(site/playground): show `validatePresentation` results. The
1052
+ playground now runs the validator after parsing and surfaces any
1053
+ issues in a dedicated panel (with severity tint and the offending
1054
+ part name when available). Lets users spot missing rels, dangling
1055
+ slide IDs, etc. without dropping into the test harness.
1056
+ - e078498: feat: `getShapeRunClickAction(shape, p, r)` returns the per-run
1057
+ hlinkClick action with the same `ShapeClickAction` discriminated
1058
+ union the shape-level `getShapeClickAction` uses. Recognises external
1059
+ URLs, slide-jump (`ppaction://hlinksldjump`), and the four
1060
+ nav-preset actions (next / prev / first / last slide). Lets callers
1061
+ treat per-run hyperlinks symmetrically with shape-level ones.
1062
+ - 5015413: feat(site/playground): per-run hyperlinks. Runs carrying `<a:hlinkClick>`
1063
+ now render in the theme's hyperlink color with an underline, and the
1064
+ span is wrapped in an `<a href>` so the preview is clickable. Per-run
1065
+ font / color / formatting overrides still apply on top — the link
1066
+ styling fills the gaps the run didn't author.
1067
+ - cc592d2: feat(site/playground): per-run slide-jump click actions render as
1068
+ in-page anchors. Mirrors the shape-level slide-jump support shipped
1069
+ in the prior batch — `getShapeRunClickAction` resolves to either a
1070
+ URL or `#slide-N` anchor, and the run-level `<a href>` wrapper
1071
+ respects whether the href is in-page (no `target=_blank`) or
1072
+ external.
1073
+ - 2207ed1: feat: scatter, radar, and bubble charts are now modeled as their own
1074
+ `ChartKind`s instead of being folded into `line`. `ChartSeries` gains
1075
+ `xValues` (`<c:xVal>`) and `bubbleSizes` (`<c:bubbleSize>`); `ChartSpec`
1076
+ gains `scatterStyle`, `radarStyle`, `bubbleScale`, and
1077
+ `bubbleSizeRepresents`. Read + render only: the preview draws real
1078
+ scatter (two value axes + markers), radar (polar spokes/rings), and
1079
+ bubble (area-proportional circles) plots, and the write path now rejects
1080
+ these kinds loudly — previously a read-modify-write silently corrupted a
1081
+ scatter chart into a line chart.
1082
+ - 08dc68b: feat(chart): `ChartSeries.lineWidthEmu` and `lineDash` read
1083
+ `<c:ser><c:spPr><a:ln>` per-series stroke width and preset dash.
1084
+ Playground line / area renderer uses the authored stroke width
1085
+ (scaled to px) and projects the preset dash to the same
1086
+ `stroke-dasharray` cadence shape strokes use.
1087
+ - e09e734: feat(chart): per-series marker symbol + size.
1088
+ `ChartSeries.markerSymbol` / `markerSizePt` read `<c:ser><c:marker>`
1089
+ (`<c:symbol val="…"/>` + `<c:size val="N"/>`). Playground line / area
1090
+ renderer emits the matching SVG glyph at each data point — circle /
1091
+ square / diamond / triangle / star / x / plus / dash / dot — sized
1092
+ per the authored point value. `none` hides the markers.
1093
+ - 995825f: feat: `setShapeHyperlink` and `setShapeRunHyperlink` now accept an
1094
+ optional `tooltip` argument that writes a `tooltip="…"` attribute on the
1095
+ emitted `<a:hlinkClick>`. Backwards compatible — calls that omit the new
1096
+ arg behave exactly as before.
1097
+
1098
+ fix: `getShapeHyperlinkTooltip` previously only looked at the shape's
1099
+ `<p:cNvPr><a:hlinkClick>`, missing the run-level tooltip that
1100
+ `setShapeHyperlink` writes. It now scans run-level `<a:rPr>` first
1101
+ (mirroring `getShapeHyperlink`'s read path) and falls back to the
1102
+ shape-click hyperlink — so the reader / writer pair is consistent.
1103
+
1104
+ - 051f4bb: feat: writers for the three stroke attributes that had readers but no
1105
+ setters — `setShapeStrokeCap(shape, 'rnd' | 'sq' | 'flat' | null)`,
1106
+ `setShapeStrokeJoin(shape, 'round' | 'bevel' | 'miter' | null)`, and
1107
+ `setShapeStrokeCompound(shape, 'sng' | 'dbl' | 'thickThin' | 'thinThick' | 'tri' | null)`.
1108
+
1109
+ Cap and compound map to `<a:ln cap=…/>` and `<a:ln cmpd=…/>` attributes;
1110
+ join writes one of the `<a:round/>` / `<a:bevel/>` / `<a:miter/>` child
1111
+ variants. Passing `null` clears the attribute / removes the child so the
1112
+ shape inherits the default. Creates `<a:ln>` if absent.
1113
+
1114
+ - 56da3ee: feat: `setShapeTextBodyRotationDeg(shape, rotationDeg | null)` — companion
1115
+ writer for the existing `getShapeTextBodyRotationDeg` reader. Sets
1116
+ `<a:bodyPr rot="N"/>` (in 60000ths of a degree, per OOXML) so the text
1117
+ body can rotate independently of the shape's own `<p:xfrm rot>`. Passing
1118
+ `null` or `0` clears the attribute so the shape inherits the default.
1119
+ - a65c05c: feat: `setShapeTextColumns(shape, { count, gapEmu? } | null)` — multi-
1120
+ column writer pairing the existing `getShapeTextColumns` reader. Writes
1121
+ `<a:bodyPr numCol="N" [spcCol="EMU"]/>`. Passing `null` clears both
1122
+ attributes so the text body falls back to PowerPoint's default single
1123
+ column. `count` must be `>= 2` (single column is the default — pass
1124
+ `null` instead); the function throws otherwise.
1125
+ - fea7725: feat: `setShapeTextDirection(shape, direction | null)` — companion
1126
+ writer for the existing `getShapeTextDirection` reader. Sets
1127
+ `<a:bodyPr vert="…"/>` with any of the six `ST_TextVerticalType`
1128
+ values (`vert`, `vert270`, `wordArtVert`, `eaVert`, `mongolianVert`,
1129
+ `wordArtVertRtl`); passing `null` or `'horz'` clears the attribute so
1130
+ the shape uses the default horizontal direction.
1131
+ - 4006813: feat: `setTableCellAnchor(cell, 'top' | 'center' | 'bottom' | null)` and
1132
+ `setTableCellMargins(cell, {left?, right?, top?, bottom?} | null)` —
1133
+ writers for two `<a:tcPr>` properties that already had readers
1134
+ (`getTableCellAnchor`, `getTableCellMargins`). The anchor setter maps
1135
+ `top`/`center`/`bottom` to the schema's `t`/`ctr`/`b` values and clears
1136
+ the attribute on `null`. The margins setter writes per-side EMU on
1137
+ `marL`/`marR`/`marT`/`marB`; sides set to `null`/`undefined` are
1138
+ stripped (PowerPoint falls back to its defaults); passing the whole
1139
+ arg as `null` clears every side. Both create `<a:tcPr>` if absent.
1140
+ - 3921802: feat: `setTableCellBorders(cell, sides | null)` — partial-update writer
1141
+ for all 6 cell-border slots (`left`, `right`, `top`, `bottom` + the
1142
+ `tlToBr` / `blToTr` diagonals). Pairs the existing
1143
+ `getTableCellBorders` reader. Sides listed with `null` are removed from
1144
+ `<a:tcPr>`; sides omitted are left untouched. Passing `null` as the
1145
+ whole `sides` arg clears every side. Creates `<a:tcPr>` if absent.
1146
+
1147
+ The diagonals are independent of the four cardinal sides — a
1148
+ strikethrough cell can have only `tlToBr`.
1149
+
1150
+ - c3e01b3: feat: `setTableCellTextDirection(cell, direction | null)` — vertical-
1151
+ text writer for table cells, paired with the existing
1152
+ `getTableCellTextDirection` reader. Same six `ST_TextVerticalType`
1153
+ values as `setShapeTextDirection`. Passing `null` (or `'horz'`) clears
1154
+ the `<a:tcPr vert="…"/>` attribute so the cell uses the default
1155
+ horizontal direction. Creates `<a:tcPr>` if absent.
1156
+ - 328f207: feat: `setTableStyleFlags(table, flags)` — partial-update writer for
1157
+ the six `<a:tblPr>` boolean style flags (`firstRow`, `lastRow`,
1158
+ `firstCol`, `lastCol`, `bandRow`, `bandCol`). Pairs the existing
1159
+ `getTableStyleFlags` reader. Only the keys present in `flags` are
1160
+ touched — omitted keys keep their current state. A flag set to `false`
1161
+ strips the attribute (matching how PowerPoint round-trips defaults).
1162
+ Creates `<a:tblPr>` if absent. Throws when the shape isn't a table
1163
+ graphic frame.
1164
+ - 1ea509b: feat: `setTableStyleId(table, styleId | null)` — writer for
1165
+ `<a:tbl><a:tblPr><a:tableStyleId>`. Pairs the existing `getTableStyleId`
1166
+ reader. Pass the curly-braced GUID (e.g.
1167
+ `'{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}'` for PowerPoint's "Medium
1168
+ Style 2 - Accent 1") or `null` to remove the reference so the table
1169
+ uses the slide's default style. Creates `<a:tblPr>` if absent. Throws
1170
+ when the shape isn't a table graphic frame.
1171
+ - 2438696: feat(site/playground): shape `aria-label` from authored alt text.
1172
+ Each rendered shape with a non-empty alt title (or, as fallback,
1173
+ alt description) now exposes `role="img" aria-label="…"` on the
1174
+ root `<g>`. Screen readers announce decks the same way PowerPoint's
1175
+ Accessibility Inspector reports them, without affecting visuals.
1176
+ - b8e24d6: feat(site/playground): each shape's authored name surfaces as a
1177
+ `data-pptx-shape-name` attribute on its root `<g>` element. Lets
1178
+ DevTools, a11y inspectors, or test selectors target shapes by their
1179
+ PowerPoint name without parsing SVG geometry. Cheap to emit and has
1180
+ no visual impact.
1181
+ - fdd4770: feat: `getShapeTextBodyRotationDeg(shape)` returns the shape's text-body
1182
+ rotation from `<a:bodyPr rot="N"/>` (where N is in 60000ths of a
1183
+ degree). Distinct from the shape's geometry rotation (`<p:xfrm rot>`):
1184
+ this rotates the text body _inside_ the shape without rotating the
1185
+ geometry. The playground renderer pivots the text body around the
1186
+ inset midpoint when the angle is non-zero, matching PowerPoint's
1187
+ behaviour for vertical-label callouts and rotated text frames.
1188
+ - 263bf52: feat: `getSlideBackgroundGradientFill(slide)` returns the gradient
1189
+ stops + path for slides with a `<p:bgPr><a:gradFill>` background.
1190
+ Playground paints gradient slide backgrounds via the same projector
1191
+ that handles shape gradients (linear / radial / rect / shape).
1192
+ - 263bf52: feat: `getSlideBackgroundPatternFill(pres, slide)` returns the pattern
1193
+ preset + theme-resolved foreground / background for slides whose
1194
+ `<p:bgPr>` carries a `<a:pattFill>`. Playground now paints pattern
1195
+ slide backgrounds via the same SVG `<pattern>` tile generator that
1196
+ handles shape pattern fills.
1197
+ - c2bcc39: feat: `getSlideBackground` now handles `<p:bgRef>` (the theme-fill-
1198
+ reference variant of slide background, e.g. `<p:bgRef idx="1003">
1199
+ <a:schemeClr val="bg1"/></p:bgRef>`). Returns the inner color element
1200
+ as a solid fill so renderers paint the slide background even when
1201
+ the deck uses the theme-reference form instead of explicit `<p:bgPr>
1202
+ <a:solidFill>`.
1203
+ - 8b7cab6: feat: `slideHasAnimations(slide)` — per-slide animation predicate.
1204
+ Returns `true` when the slide carries a `<p:timing>` block (at least
1205
+ one authored animation effect). Complements the deck-wide
1206
+ `getPresentationSummary().hasAnimations`. The site playground uses
1207
+ it (plus `getSlideTransition`) to show small `anim` / `trans`
1208
+ badges next to each slide title so deck audits don't need to open
1209
+ PowerPoint.
1210
+ - c0e0dc2: feat(site/playground): shapes with slide-jump click actions
1211
+ (`<a:hlinkClick action="ppaction://hlinksldjump"/>`) render as
1212
+ in-page hash anchors. The renderer resolves the target via
1213
+ `getShapeClickAction` and emits `<a href="#slide-N">`; each slide's
1214
+ `<li>` carries `id="slide-N"` so clicks scroll to the target slide.
1215
+ Plain URL click actions render the same way as shape-level
1216
+ hyperlinks (with `target="_blank"`).
1217
+ - 7410df9: feat: `getSlideMasterPartName(slide)` returns the part-name of the
1218
+ slide master the slide inherits from. Useful for multi-master decks
1219
+ where different slides live under different brand templates and the
1220
+ caller needs to scope theme / fontScheme / clrMap lookups to the
1221
+ correct master.
1222
+ - 1f536ab: feat: `getShapeStrokeEffective(pres, shape)` walks the layout → master
1223
+ placeholder cascade when the shape's own stroke is `'inherit'`. Same
1224
+ discriminant types (solid / none / inherit) as `getShapeStroke`; first
1225
+ non-inherit layer wins. Playground uses it so placeholder outlines
1226
+ authored on the master / layout finally render.
1227
+ - 263bf52: feat: full stroke read-back surface — `getShapeStrokeCap`,
1228
+ `getShapeStrokeJoin`, `getShapeStrokeCompound` plus the existing
1229
+ `getShapeStrokeDash` / `getShapeStrokeArrow`. Renderers now have
1230
+ enough information to reproduce dashed outlines, rounded vs square
1231
+ caps, miter vs bevel joins, and per-end arrow heads.
1232
+
1233
+ The playground composes `stroke-dasharray` from the preset dash
1234
+ patterns (cadence multiplied by stroke width as PowerPoint does),
1235
+ emits SVG `<marker>` defs for triangle / stealth / diamond / oval
1236
+ arrowheads on connectors and shapes, and maps cap / join through.
1237
+
1238
+ - f68bd96: feat(site/playground): table cell borders honor `<a:prstDash>`. The
1239
+ reader already surfaced the dash token; the renderer now projects it
1240
+ to an SVG `stroke-dasharray` (scaled by the border's authored width).
1241
+ Applies to every side, the top-left → bottom-right diagonal, and the
1242
+ bottom-left → top-right diagonal.
1243
+ - a037fa7: feat: `getTableCellAnchor(cell)` returns the cell's vertical text
1244
+ anchor (`<a:tcPr anchor="t|ctr|b"/>`) as `'top' | 'center' |
1245
+ 'bottom' | null`. Playground projects each onto a CSS
1246
+ `justify-content` so cell text sits at the authored vertical
1247
+ position instead of always centering.
1248
+ - e50f1d6: feat(site/playground): table cell text honors authored `<a:tcPr
1249
+ marL/marR/marT/marB>` insets. The renderer previously hard-coded a
1250
+ 4-pixel pad on every side; it now converts each EMU-valued margin to
1251
+ px (falling back to 4px only when the side isn't authored) so cells
1252
+ with custom inner padding line up the way PowerPoint shows them.
1253
+ - 42cf575: feat: `getTableCellMargins(cell)` returns the cell's `<a:tcPr marL
1254
+ marR marT marB>` inset margins in EMU. Each side is `null` when the
1255
+ cell doesn't author it, so renderers know to fall back to
1256
+ PowerPoint's defaults (91440 EMU / 0.1 in horizontal, 45720 EMU /
1257
+ 0.05 in vertical).
1258
+ - ba94f5e: feat: `getTableCellParagraphs(cell)` returns a table cell's text as structured
1259
+ paragraphs — each carrying its alignment and per-run format (`size`, `bold`,
1260
+ `italic`, `color`, `font`, …) — the rich counterpart to `getTableCellText`,
1261
+ which only returns the flat visible string.
1262
+ - f05aa62: feat: `getTableCellTextDirection(cell)` reads `<a:tcPr vert="…"/>` —
1263
+ the same token set as `getShapeTextDirection` (`vert`, `vert270`,
1264
+ `eaVert`, `mongolianVert`, `wordArtVert`, `wordArtVertRtl`).
1265
+ Vertical column headers in tables commonly use `vert270` / `eaVert`
1266
+ so the header label reads bottom-to-top alongside its column.
1267
+ - 263bf52: feat: table span + border read-back.
1268
+
1269
+ - `getTableCellSpan(cell)` returns `{ gridSpan, rowSpan, hMerge, vMerge }`
1270
+ so renderers know which cells own a merged region and which are
1271
+ absorbed into one.
1272
+ - `getTableCellBorders(pres, cell)` returns per-side borders (left,
1273
+ right, top, bottom, plus the two diagonals tlToBr / blToTr) with
1274
+ theme-resolved colors, widths, and dash style.
1275
+
1276
+ Playground table rendering now honours both: merged cells are skipped
1277
+ on their absorbed positions, and per-cell borders render at the
1278
+ authored color / width on top of the default thin grid.
1279
+
1280
+ - 263bf52: feat: `getTableStyleFlags(table)` returns the `<a:tblPr>` boolean
1281
+ toggles — `firstRow` / `lastRow` / `firstCol` / `lastCol` / `bandRow`
1282
+ / `bandCol`. Playground projects each onto a theme-derived tint
1283
+ (accent1 for header / footer rows, 92%-white-mixed accent for bands)
1284
+ when the cell doesn't supply an explicit fill of its own. Header text
1285
+ rendered on the accent gets white text instead of the default body
1286
+ color, matching PowerPoint's built-in table styles.
1287
+ - 243e731: feat: `getTableStyleId(table)` returns the GUID string inside
1288
+ `<a:tbl><a:tblPr><a:tableStyleId>`. PowerPoint references built-in
1289
+ table styles (`{5C22544A-…}` = Medium Style 2 - Accent 1, etc.) and
1290
+ theme-local styles by GUID. Returns `null` when the table doesn't
1291
+ author one.
1292
+ - dfae64a: feat: add `getSlideLayoutShapes(pres, layout)` and `getSlideMasterShapes(pres,
1293
+ layout)` — the non-placeholder decorative shapes (corner bars, divider lines,
1294
+ logos, watermark text) on a slide layout and its master, as render-ready
1295
+ `SlideShapeData`. Unlike the older flat `getSlideLayoutBackgroundShapes`, these
1296
+ include pictures and groups and work with every `getShape*` reader, so a
1297
+ picture logo's bytes resolve (against the layout/master's own relationship
1298
+ table). For reading/rendering — the handles are bound to the layout/master
1299
+ part, not a slide.
1300
+ - 263bf52: feat: `getShapeTextColumns(shape)` returns `{ count, gapEmu? }` for
1301
+ text bodies that author `<a:bodyPr numCol="N" spcCol="EMU"/>`.
1302
+ Playground emits `column-count` / `column-gap` on the foreignObject,
1303
+ so newspaper-style multi-column placeholders flow correctly.
1304
+ - 263bf52: feat: extend `TextFormat` with the remaining commonly-authored
1305
+ `CT_TextCharacterProperties` (ECMA-376 §17.18.83) attributes:
1306
+
1307
+ - `strike` — `true` / `false` / `'sngStrike'` / `'dblStrike'`
1308
+ - `spc` — character spacing in 1/100 pt
1309
+ - `kern` — kerning threshold in half-points
1310
+ - `baseline` — superscript / subscript offset as a unit fraction
1311
+ - `cap` — `'none'` / `'small'` / `'all'`
1312
+ - `highlight` — per-run background color
1313
+
1314
+ All round-trip through `setShapeRunFormat` / `getShapeRunFormat` and
1315
+ flow through `getShapeRunFormatEffective`'s inheritance cascade. The
1316
+ playground renderer honours each of them in the rendered HTML.
1317
+
1318
+ - dc98eb1: feat(site/playground): default text body to the theme's font scheme.
1319
+ `<a:fontScheme><a:majorFont>` becomes the default face for title /
1320
+ ctrTitle placeholders; `<a:minorFont>` covers everything else. The
1321
+ existing per-run `<a:rPr typeface>` override still wins. Templates
1322
+ that brand-themselves to Aptos / Inter / etc. now render with their
1323
+ authored fonts instead of always falling back to Calibri.
1324
+ - 263bf52: feat: Tier B fidelity batch.
1325
+
1326
+ - `getShapeTextDirection(shape)` returns the `<a:bodyPr vert="…"/>`
1327
+ token (`vert`, `vert270`, `wordArtVert`, `eaVert`, `mongolianVert`,
1328
+ `wordArtVertRtl`). Playground projects each onto a CSS
1329
+ `writing-mode` / `text-orientation` declaration so Asian and
1330
+ Mongolian-style vertical text renders without manual transforms.
1331
+ - Playground wraps shapes carrying a `<a:hlinkClick>` in an SVG `<a>`
1332
+ element so the preview is clickable — matches PowerPoint's
1333
+ slide-show behaviour for shape-level hyperlinks.
1334
+ - Group shape rendering now applies the group's own `<a:xfrm rot
1335
+ flipH flipV>` to the whole subtree before the scale + translate
1336
+ that maps internal coords onto slide coords.
1337
+
1338
+ - 4688af3: feat: chart trendline `<c:forward>` / `<c:backward>` extensions.
1339
+ `ChartTrendline.forward` and `backward` carry the N-period
1340
+ extrapolation past the last / before the first data point. The
1341
+ playground renderer projects the linear fit further along the x-axis
1342
+ by `N * step` so projected-future trendlines render the way
1343
+ PowerPoint shows them. Moving-average / log / poly trendlines keep
1344
+ their data-range output since extrapolation isn't meaningful for
1345
+ them.
1346
+ - 57117a7: feat: chart value-axis tick labels honor `<c:valAx><c:txPr><a:bodyPr
1347
+ rot="N"/>`. `ChartSpec.valueAxisLabelRotationDeg` returns the rotation
1348
+ in degrees (converted from OOXML's 60000ths-of-a-degree). The
1349
+ playground renders each value-axis tick label with a
1350
+ `transform=rotate()` around its anchor, symmetric to the
1351
+ `categoryAxisLabelRotationDeg` we already projected.
1352
+ - 3e1c8a1: feat: chart value-axis exposes `<c:scaling><c:logBase val="N"/>`.
1353
+ `ChartAxisScaling.logBase` carries the authored log base (commonly
1354
+ `2`, `10`, or `Math.E`). The reader clamps to PowerPoint's `[2, 1000]`
1355
+ range. Callers that round-trip charts now preserve the log-scale
1356
+ flag; the playground renderer still draws linear (log-scale
1357
+ projection is a follow-up — exposing the field unblocks it).
1358
+
1359
+ ### Patch Changes
1360
+
1361
+ - 499c590: fix: presentation handles now interoperate across the `pptx-kit` and
1362
+ `pptx-kit/node` entry points. The two entries ship as separate bundles,
1363
+ and the opaque handles (`PresentationData`, `SlideData`, …) were keyed by
1364
+ plain `Symbol`s minted per bundle. Loading a deck with
1365
+ `loadPresentationFile` (from `pptx-kit/node`) and then reading it with,
1366
+ say, `getSlides` (from `pptx-kit`) crashed with
1367
+ `Cannot read properties of undefined`. The handle keys now use the global
1368
+ symbol registry (`Symbol.for`), so a handle from either entry is readable
1369
+ by the other — and by companion packages that bundle their own reader copy.
1370
+ - 8fc8f12: fix(site): playground stopped rendering after `SlideCommentData` was made
1371
+ opaque and `getSlideMediaPartNames` lost its `(pres, slide)` two-arg
1372
+ form. The playground was still doing `comment.text` and
1373
+ `getSlideMediaPartNames(pres, slide)`, both of which threw at runtime.
1374
+ Switched to the public `getCommentText(comment)` accessor and the
1375
+ single-arg `getSlideMediaPartNames(slide)` signature.
1376
+ - 3fb5101: fix: `getShapeRunFormatEffective` / `getParagraphPropertiesEffective` no longer
1377
+ inherit the slide master's `bodyStyle` for plain text boxes. A shape without a
1378
+ `<p:ph>` is not a placeholder, so its unsized runs now resolve to no inherited
1379
+ size (consumers apply the ~18pt text-box default) instead of wrongly picking up
1380
+ the master body size (often much larger). Real placeholders — including ones
1381
+ whose `<p:ph>` omits a `type` — still inherit as before. This makes effective
1382
+ text formatting match what PowerPoint and LibreOffice render for text boxes.
1383
+ - cfe8b69: fix: placeholder inheritance now applies the OOXML `ctrTitle`↔`title` and
1384
+ `subTitle`→`body` type equivalence. A `ctrTitle` (centered title) now inherits
1385
+ its layout/master `title` placeholder's `bodyPr` (e.g. `anchor="ctr"`),
1386
+ `lstStyle`, and geometry instead of dropping them — fixing
1387
+ `getShapeBodyPrEffective`, `getShapeBoundsResolved`,
1388
+ `getShapeRunFormatEffective`, and `getParagraphPropertiesEffective` for
1389
+ centered titles and subtitles.
1390
+ - 610ecac: fix(validator): `validatePresentation` now flags duplicate
1391
+ `<p:cNvPr id="N">` values inside a single slide's `<p:spTree>` as
1392
+ errors. PowerPoint requires every shape's non-visual ID to be unique
1393
+ within its slide; duplicates often appear after pasting shapes from
1394
+ another slide without re-allocating IDs. The walk recurses into
1395
+ `<p:grpSp>` so duplicates nested in groups are also caught.
1396
+
1397
+ ## 1.0.0
1398
+
1399
+ ### Major Changes
1400
+
1401
+ - f47b78b: **1.0.0** — first stable release. The public API is now frozen under SemVer.
1402
+
1403
+ **What works at 1.0:**
1404
+
1405
+ - **Read** any `.pptx` produced by PowerPoint, Keynote, Google Slides, or
1406
+ LibreOffice Impress, and save it back without corruption. Unknown
1407
+ extensions are preserved verbatim on round-trip.
1408
+ - **Template editing**: token / text replace across slides and speaker
1409
+ notes, image swap with geometry preserved, slide CRUD with placeholder
1410
+ inheritance from layout / master.
1411
+ - **Authoring on top of an existing master**: 180+ preset shapes, custom
1412
+ text formatting, tables, embedded charts (column / line / bar / pie /
1413
+ doughnut / area) with auto-generated xlsx, solid / gradient / pattern /
1414
+ image fills, shadows and glows, rotation / flip / z-order, hyperlinks
1415
+ and click actions, notes and comments, slide transitions, simple
1416
+ entrance / exit animations.
1417
+ - **Diagnostics**: `validatePresentation` returns invariant violations;
1418
+ every XML part is validated against the ECMA-376 XSDs in CI.
1419
+ - **Bundling**: one ESM build runs in both Node ≥ 20 and modern browsers.
1420
+ Tree-shaking is enforced by a CI test — minimal `load → save` bundle
1421
+ is < 75 KB unminified, full fn-API bundle is ~120 KB.
1422
+
1423
+ **Deferred to post-1.0** (read pass-through preserved on round-trip):
1424
+
1425
+ - Constructing new themes / masters / layouts from scratch.
1426
+ - SmartArt authoring.
1427
+ - Complex animation timing-tree authoring.
1428
+ - OLE / ActiveX authoring.
1429
+ - Document encryption (read + write).
1430
+
1431
+ **Performance (M-series Node 20):** 100-slide synthetic deck saves in
1432
+ ~25 ms, loads in ~20 ms. 100 MB templates fit comfortably under the 2 s
1433
+ load/save targets.
1434
+
1435
+ **Migration:** if you were on the pre-1.0 class API
1436
+ (`Presentation` / `Slide` / `SlideShape` / `SlideLayout`), see the
1437
+ preceding changeset for the rename table. There is no class API at 1.0.
1438
+
1439
+ - 665c979: **BREAKING**: the class-based API (`Presentation`, `Slide`, `SlideShape`,
1440
+ `SlideLayout`) has been removed. Use the free-function API for every
1441
+ capability — one canonical path per operation.
1442
+
1443
+ | Was | Now |
1444
+ | -------------------------------- | ---------------------------------------- |
1445
+ | `Presentation.load(bytes)` | `loadPresentation(bytes)` |
1446
+ | `Presentation.create()` | `createPresentation()` |
1447
+ | `pres.save()` | `savePresentation(pres)` |
1448
+ | `pres.slides` | `getSlides(pres)` |
1449
+ | `pres.slideLayouts` | `getSlideLayouts(pres)` |
1450
+ | `pres.addSlide({ layout })` | `addSlide(pres, { layout })` |
1451
+ | `pres.removeSlide(slide)` | `removeSlide(pres, slide)` |
1452
+ | `pres.moveSlide(slide, i)` | `moveSlide(pres, slide, i)` |
1453
+ | `pres.duplicateSlide(slide)` | `duplicateSlide(pres, slide)` |
1454
+ | `pres.replaceTokens(map)` | `replaceTokensInPresentation(pres, map)` |
1455
+ | `slide.shapes` | `getSlideShapes(slide)` |
1456
+ | `slide.findPlaceholder('title')` | `findSlidePlaceholder(slide, 'title')` |
1457
+ | `slide.addTextBox(opts)` | `addSlideTextBox(slide, opts)` |
1458
+ | `slide.addShape(opts)` | `addSlideShape(slide, opts)` |
1459
+ | `slide.addImage(bytes, opts)` | `addSlideImage(slide, bytes, opts)` |
1460
+ | `slide.addTable(opts)` | `addSlideTable(slide, opts)` |
1461
+ | `slide.addLine(opts)` | `addSlideLine(slide, opts)` |
1462
+ | `slide.setBackground(color)` | `setSlideBackground(slide, color)` |
1463
+ | `slide.setTransition(opts)` | `setSlideTransition(slide, opts)` |
1464
+ | `slide.setNotes(text)` | `setSlideNotes(slide, text)` |
1465
+ | `slide.layout` | `getSlideLayout(slide)` |
1466
+ | `slide.notes` | `getSlideNotes(slide)` |
1467
+ | `slide.text` | `getSlideText(slide)` |
1468
+ | `shape.text` | `getShapeText(shape)` |
1469
+ | `shape.setText(value)` | `setShapeText(shape, value)` |
1470
+ | `shape.position` | `getShapePosition(shape)` |
1471
+ | `shape.setPosition(x, y)` | `setShapePosition(shape, x, y)` |
1472
+ | `shape.setFill(color)` | `setShapeFill(shape, color)` |
1473
+ | `shape.setStroke(opts)` | `setShapeStroke(shape, opts)` |
1474
+ | `shape.setRotation(deg)` | `setShapeRotation(shape, deg)` |
1475
+ | `shape.setHyperlink(url)` | `setShapeHyperlink(shape, url)` |
1476
+ | `layout.name` | `getSlideLayoutName(layout)` |
1477
+
1478
+ Node entry (`pptx-kit/node`) drops the `Presentation` subclass; use
1479
+ `loadPresentationFile` / `savePresentationToFile` instead.
1480
+
1481
+ **Why**: every capability used to have two paths through the public API
1482
+ — a class method and a free function. The duplication hurt
1483
+ discoverability (which one should you use?), made the bundle larger
1484
+ (class consumers dragged the whole prototype in), and forced every
1485
+ breaking change to land in two places. The free-function API is the
1486
+ canonical surface from now on.
1487
+
1488
+ ### Minor Changes
1489
+
1490
+ - b41c502: Comprehensive feature surface for PPTX authoring + editing. This is the
1491
+ first release that covers every L1–L4 capability in the foundation
1492
+ plan. Highlights:
1493
+
1494
+ **Round-trip + template editing (L1 / L2)**
1495
+
1496
+ - `loadPresentation` / `savePresentation` (`Uint8Array` / `ArrayBuffer` / `Blob`).
1497
+ - Node convenience: `loadPresentationFile`, `savePresentationToFile`.
1498
+ - Token replace: `replaceTokensInPresentation`, `replaceTokensInSlide`.
1499
+ - Free-text replace: `replaceTextInPresentation`, `replaceTextInSlide`.
1500
+ - Slide CRUD: `addSlide`, `removeSlide`, `moveSlide`, `duplicateSlide`,
1501
+ `getSlideAt`, `getSlideIndex`, `clearSlideShapes`, `sortSlides`.
1502
+ - Cross-deck: `importSlide` (with image-media propagation).
1503
+ - Cross-slide: `copyShape`.
1504
+ - Diagnostics: `validatePresentation`, `getPresentationSummary`,
1505
+ `listPackageParts`, `readPackagePart`, `getMediaParts`,
1506
+ `setMediaPartBytes`, `compactPackage`.
1507
+
1508
+ **Authoring (L3)**
1509
+
1510
+ - Shapes: `addSlideTextBox`, `addSlideShape` (180+ presets),
1511
+ `addSlideLine`, `addSlideTable`, `addSlideImage`, `addSlideChart`.
1512
+ - Charts: `bar` / `column` / `line` / `pie` / `doughnut` / `area` with
1513
+ embedded xlsx; read/update via `getSlideCharts` / `setChartSpec`.
1514
+ - Tables: per-cell access (`getTableCells`, `setTableCellText`,
1515
+ `setTableCellFill`, `setTableCellTextFormat`,
1516
+ `setTableCellAlignment`); row + column insert/remove.
1517
+ - Slide layout swap: `setSlideLayout`, `findSlideLayout`.
1518
+
1519
+ **Text**
1520
+
1521
+ - Per-shape: `setShapeText`, `setShapeBullets`, `setShapeAlignment`,
1522
+ `setShapeTextFormat`, `setShapeHyperlink`, `setShapeTextAnchor`,
1523
+ `setShapeTextMargins`, `setShapeTextWrap`, `setShapeTextAutoFit`.
1524
+ - Per-paragraph: `setParagraphAlignment`, `setParagraphBullet`,
1525
+ `setParagraphLevel`, `setParagraphSpacing` + read-back pairs.
1526
+ - Per-run: `setShapeRunFormat`, `setShapeRunText`,
1527
+ `getShapeRunFormat`, `getShapeParagraphCount`, `getShapeRunCount`,
1528
+ `getShapeRunText`.
1529
+
1530
+ **Geometry**
1531
+
1532
+ - Position / size / rotation / flip + combined `setShapeBounds` /
1533
+ `getShapeBounds`. Z-order: `bringShapeToFront`, `sendShapeToBack`,
1534
+ `bringShapeForward`, `sendShapeBackward`.
1535
+
1536
+ **Fill / stroke / effects**
1537
+
1538
+ - Fill kinds: solid, gradient, pattern, image, none + `getShapeFill`
1539
+ read-back.
1540
+ - Stroke: color + width + dash + arrowheads + `getShapeStroke` /
1541
+ `getShapeStrokeDash` / `getShapeStrokeArrow` read-back.
1542
+ - Effects: `setShapeShadow`, `setShapeGlow`, `clearShapeEffects` +
1543
+ `getShapeEffect` read-back.
1544
+
1545
+ **Pictures**
1546
+
1547
+ - Crop, opacity, brightness (`lumOff`), contrast (`lumMod`),
1548
+ image replacement, image-as-fill. Read-back pairs for every setter.
1549
+
1550
+ **Slide-level (L4)**
1551
+
1552
+ - Notes (`getSlideNotes` / `setSlideNotes`).
1553
+ - Transitions (every effect + read-back).
1554
+ - Animations (`fadeIn` / `fadeOut` / `appear` / `disappear`) +
1555
+ read-back.
1556
+ - Comments (legacy schema, author dedup, optional position + date).
1557
+ - Backgrounds: solid color or embedded picture; read-back.
1558
+ - Visibility: `setSlideHidden` / `isSlideHidden`.
1559
+ - Slide sections (p14:sectionLst).
1560
+ - Slide size + presets (`SLIDE_SIZE_4_3` / `16_9` / `16_10`).
1561
+ - Slide title shortcut (`getSlideTitle` / `setSlideTitle`).
1562
+ - Click actions: URL / slide jump / preset nav + read-back.
1563
+
1564
+ **Theme + package**
1565
+
1566
+ - `getPresentationTheme` — color scheme (`accent1`–`accent6`, `dark1`,
1567
+ `light1`, `hyperlink`, ...).
1568
+ - `getMediaParts`, `listPackageParts`, `readPackagePart` for audit /
1569
+ export workflows.
1570
+
1571
+ **Tree-shake**
1572
+
1573
+ - The minimal `load`+`save` import is ~60 KB; the full fn-API
1574
+ bundle ~123 KB. CI guard via `test/tree-shake.test.ts`.
1575
+
1576
+ All emitted XML validates against the ECMA-376 strict schemas
1577
+ (pml.xsd, dml-chart.xsd, opc-relationships.xsd, opc-contentTypes.xsd)
1578
+ via Layer-1 tests.
1579
+
1580
+ **Additional helpers** (all tree-shakeable free functions)
1581
+
1582
+ - Properties: `getCoreProperties` / `setCoreProperties`,
1583
+ `getExtendedProperties` / `setExtendedProperties`, plus convenience
1584
+ `getPresentationCreated`, `getPresentationModified`,
1585
+ `incrementRevision`, `touchModified`.
1586
+ - Thumbnail: `getThumbnail` / `setThumbnail` / `removeThumbnail`.
1587
+ - Theme: `getPresentationTheme`, `getPresentationFonts`.
1588
+ - Slide queries: `getSlideCount`, `getSlideLayoutCount`,
1589
+ `getVisibleSlides`, `getHiddenSlides`, `getSlidesWithNotes`,
1590
+ `getSlidesWithComments`, `getSlidesWithImages`,
1591
+ `getSlidesWithCharts`, `getSlidesWithTables`,
1592
+ `getSlidesByLayout`, `findSlideByTitle`, `findSlideByText`,
1593
+ `findSlidesByText`, `findSlideByPartName`,
1594
+ `findSlideLayoutByType`, `findSlideLayoutByPartName`.
1595
+ - Bulk inventories: `getAllNotes`, `getAllComments`, `getAllCharts`,
1596
+ `getAllTables`, `getAllImages`, `getPresentationText`,
1597
+ `getSlideOutline`.
1598
+ - Shape introspection: `getShapeAt`, `getShapeIndex`,
1599
+ `getShapeSlide`, `getShapeXmlString`, `getShapeChartKind`,
1600
+ `getShapeChartSpec`, `getShapeImageFillBytes`,
1601
+ `getShapeImageFormat`, `getShapeImagePartName`,
1602
+ `getShapeAltTitle` / `setShapeAltTitle`,
1603
+ `getShapeDescription` / `setShapeDescription`.
1604
+ - Shape predicates: `isChartShape`, `isTableShape`,
1605
+ `isShapeHidden` / `setShapeHidden`, `isShapePlaceholder`,
1606
+ `hasShapeImage`, `hasShapeText`.
1607
+ - Shape search: `findShapeByText`, `findShapesByText`,
1608
+ `findShapesByKind`, `findChartByKind`,
1609
+ `findChartsBySeriesName`, `findCommentsByAuthor`,
1610
+ `findSlidePlaceholders`, `findSlidePlaceholderByIdx`.
1611
+ - Mutation: `setShapeRunHyperlink`, `getShapeRunHyperlink`,
1612
+ `getSlideBody`, `appendShapeText`,
1613
+ `appendSlideNotes`, `removeSlideNotes`,
1614
+ `swapSlides`, `mergePresentations`, `slidesUsingMediaPart`,
1615
+ `setTableColumnWidth`, `setTableRowHeight`, `getTableColumnWidths`,
1616
+ `getTableRowHeights`, `getTableCellAlignment`, `getTableCellFill`.
1617
+ - Diagnostics: `getSlideXmlString`, `getSlidePartName`,
1618
+ `getSlideLayoutPartName`, `getSlidesByLayout`.