@office-kit/pptx 0.11.1

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