@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yuichiro Yamashita
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,427 @@
1
+ # @office-kit/pptx
2
+
3
+ Generate and edit `.pptx` (PowerPoint / Office Open XML Presentation) files
4
+ from TypeScript — in **Node.js or the browser**, from a single ESM bundle.
5
+
6
+ > **Status: 0.x — pre-1.0, public API still evolving.** The capabilities in
7
+ > the table below are exercised against real PPTX fixtures, and emitted XML is
8
+ > checked against the ECMA-376 schemas with `xmllint` where it is available on
9
+ > the machine running the tests. Until the 1.0 release the public API is not
10
+ > frozen — breaking changes can land in a minor (`0.x`) release, so pin a
11
+ > version or an exact range.
12
+
13
+ ## Why
14
+
15
+ The JavaScript ecosystem has several PPTX libraries, but they typically pick
16
+ one trade-off:
17
+
18
+ - **Node-only** with a Buffer-shaped API → does not work in the browser.
19
+ - **Browser-only** wrapping a fixed template → cannot author from scratch.
20
+ - **Loose XML strings** that "usually open" → break in Keynote / Google Slides
21
+ / the Open XML SDK validator.
22
+
23
+ `@office-kit/pptx` is built around a different stance:
24
+
25
+ - One ESM bundle that runs in **Node and the browser**.
26
+ - A typed object model that mirrors the **OOXML PresentationML** spec
27
+ (ECMA-376 Part 1, §19). When the spec says something is a choice, our types
28
+ say it is a discriminated union.
29
+ - Output that passes Microsoft's
30
+ [Open XML SDK Productivity Tool](https://github.com/dotnet/Open-XML-SDK)
31
+ validator, not just PowerPoint's "open and pray."
32
+ - Two complementary paths: **author from scratch** _or_ **edit a template**.
33
+
34
+ ## Scope
35
+
36
+ The work is split into four levels of completeness. The current `0.x` line
37
+ covers levels 1-3 in full and level 4 in part; the table tracks where each
38
+ capability stands today. Items marked "post-1.0" are not implemented yet:
39
+
40
+ | Level | Capability | 0.x |
41
+ | ----- | ------------------------------------------------------------------- | ------------------------------- |
42
+ | L1 | Read an existing PPTX, save it back without corruption | ✅ |
43
+ | L2 | Template edit — text replacement, image swap, add slide from layout | ✅ |
44
+ | L3 | Authoring — shapes, text, tables, fills, effects, transforms | ✅ |
45
+ | L3 | Authoring on top of existing themes / masters / layouts | ✅ |
46
+ | L3 | Constructing new themes / masters / layouts from scratch | ❌ post-1.0 |
47
+ | L3 | Charts (all common types) with embedded data | ✅ |
48
+ | L4 | Notes, comments, transitions | ✅ |
49
+ | L4 | Simple animations (entrance / exit / emphasis presets) | ✅ |
50
+ | L4 | SmartArt authoring | ❌ post-1.0 (read pass-through) |
51
+ | L4 | Complex animation timing trees | ❌ post-1.0 |
52
+ | L4 | OLE / ActiveX authoring | ❌ post-1.0 (read pass-through) |
53
+ | L4 | Document encryption (read + write) | ❌ post-1.0 |
54
+
55
+ Out-of-scope content is still **preserved on round-trip** — `@office-kit/pptx` will
56
+ never silently strip parts it doesn't model. That's the L1 contract.
57
+
58
+ When NOT to use this:
59
+
60
+ - You need a **pixel-perfect** PPTX rendering (print, archival). The
61
+ companion [`@office-kit/pptx-preview`](packages/preview) package renders slides to
62
+ SVG in the browser and to PNG on the server — its closeness to LibreOffice
63
+ is measured per slide and gated in CI (`site/fidelity`) — but it is a
64
+ high-fidelity preview, not a spec-complete paint engine. For
65
+ pixel-authoritative output, use PowerPoint itself or LibreOffice headless.
66
+ - You need a thin DSL for one-off "report" slides and do not care about
67
+ schema validity. A simpler library will be lighter.
68
+ - You want to convert PPTX to another format (Keynote, ODP). Out of scope
69
+ forever — that's a renderer's job.
70
+
71
+ ## Install
72
+
73
+ ```sh
74
+ npm install @office-kit/pptx
75
+ # or
76
+ pnpm add @office-kit/pptx
77
+ # or
78
+ yarn add @office-kit/pptx
79
+ ```
80
+
81
+ ## One API
82
+
83
+ @office-kit/pptx exposes a single tree-shakeable free-function API. Every
84
+ capability is a named export — `loadPresentation`, `savePresentation`,
85
+ `addSlideTextBox`, `setShapeFill`, etc. Bundlers drop every entry you
86
+ don't import, so the minimal `load → save` bundle is **~60 KB**.
87
+
88
+ ```ts
89
+ import {
90
+ findSlidePlaceholder,
91
+ getSlides,
92
+ loadPresentation,
93
+ savePresentation,
94
+ setShapeText,
95
+ } from '@office-kit/pptx';
96
+
97
+ const pres = await loadPresentation(bytes);
98
+ const title = findSlidePlaceholder(getSlides(pres)[0]!, 'title');
99
+ if (title) setShapeText(title, 'Hello');
100
+ const out = await savePresentation(pres);
101
+ ```
102
+
103
+ ## Driving @office-kit/pptx from an AI agent
104
+
105
+ [`skill/SKILL.md`](skill/SKILL.md) is a self-contained guide for an LLM agent
106
+ authoring presentations with this library: the canonical call for each
107
+ capability, the design rules that keep output from looking template-generated,
108
+ the handful of API footguns worth memorizing, and a QA loop to run before
109
+ declaring a deck done. Its [worked example](skill/examples/business-deck.md) is
110
+ exercised by the test suite, so the code there is known to produce a
111
+ schema-valid deck.
112
+
113
+ CI enforces the tree-shake bound in `test/tree-shake.test.ts`.
114
+
115
+ ## Usage
116
+
117
+ ### Edit a template
118
+
119
+ ```ts
120
+ import {
121
+ findSlidePlaceholder,
122
+ getSlides,
123
+ loadPresentation,
124
+ savePresentation,
125
+ setShapeText,
126
+ } from '@office-kit/pptx';
127
+
128
+ const pres = await loadPresentation(existingPptxBytes);
129
+ const cover = getSlides(pres)[0]!;
130
+ const title = findSlidePlaceholder(cover, 'title');
131
+ if (title) setShapeText(title, 'Q3 Review');
132
+ const body = findSlidePlaceholder(cover, 'body');
133
+ if (body) setShapeText(body, 'Numbers up and to the right.');
134
+
135
+ const out: Uint8Array = await savePresentation(pres);
136
+ // Node: fs.writeFile('out.pptx', out)
137
+ // Browser: new Blob([out], { type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' })
138
+ ```
139
+
140
+ ### Token-based template fill
141
+
142
+ ```ts
143
+ import { loadPresentation, replaceTokensInPresentation, savePresentation } from '@office-kit/pptx';
144
+
145
+ const pres = await loadPresentation(templateBytes);
146
+ // Replaces `{{name}}`, `{{event}}`, `{{date}}` across every slide.
147
+ replaceTokensInPresentation(pres, { name: 'Alice', event: 'Re:Invent', date: '2026-12-01' });
148
+ const out = await savePresentation(pres);
149
+ ```
150
+
151
+ ### Build a deck from scratch (no template file)
152
+
153
+ `createPresentation()` returns an immediately-authorable deck — a slide
154
+ master, the Office theme, and three layouts (`Blank`, `Title Slide`,
155
+ `Title and Content`) — with no slides yet. No `.pptx` template needed.
156
+
157
+ ```ts
158
+ import {
159
+ addContentSlide,
160
+ addTitleSlide,
161
+ createPresentation,
162
+ findSlideLayoutByType,
163
+ addSlide,
164
+ findSlidePlaceholder,
165
+ savePresentation,
166
+ setShapeText,
167
+ } from '@office-kit/pptx';
168
+
169
+ // Defaults to 16:9; pass { size: '4:3' } for the classic ratio.
170
+ const pres = createPresentation();
171
+
172
+ // Sugar helpers pick the right layout by its locale-stable type token.
173
+ addTitleSlide(pres, 'Q3 Business Review');
174
+ addContentSlide(pres, { title: 'Agenda', body: 'Highlights and risks' });
175
+
176
+ // Or bind a layout explicitly. Prefer findSlideLayoutByType — it matches
177
+ // the `type` token (`'title'`, `'obj'`, `'blank'`), which is stable
178
+ // across PowerPoint UI languages. findSlideLayout(pres, 'Blank') matches
179
+ // the user-visible name, which is case-sensitive and localized.
180
+ const titleLayout = findSlideLayoutByType(pres, 'title')!;
181
+ const slide = addSlide(pres, { layout: titleLayout });
182
+ setShapeText(findSlidePlaceholder(slide, 'ctrTitle')!, 'Authored with @office-kit/pptx');
183
+
184
+ const out: Uint8Array = await savePresentation(pres);
185
+ ```
186
+
187
+ ### Build a deck from a blank template
188
+
189
+ ```ts
190
+ import {
191
+ addSlide,
192
+ addSlideImage,
193
+ addSlideTextBox,
194
+ duplicateSlide,
195
+ findSlideLayout,
196
+ findSlidePlaceholder,
197
+ inches,
198
+ loadPresentation,
199
+ moveSlide,
200
+ savePresentation,
201
+ setShapeText,
202
+ } from '@office-kit/pptx';
203
+
204
+ const pres = await loadPresentation(await fetch('/blank.pptx').then((r) => r.arrayBuffer()));
205
+
206
+ const titleLayout = findSlideLayout(pres, 'Title Slide')!;
207
+ const slide1 = addSlide(pres, { layout: titleLayout });
208
+ setShapeText(findSlidePlaceholder(slide1, 'ctrTitle')!, '@office-kit/pptx demo');
209
+ setShapeText(findSlidePlaceholder(slide1, 'subTitle')!, 'an OOXML library for TypeScript');
210
+
211
+ const blank = findSlideLayout(pres, 'Blank')!;
212
+ const slide2 = addSlide(pres, { layout: blank });
213
+ addSlideTextBox(slide2, {
214
+ x: inches(1),
215
+ y: inches(1),
216
+ w: inches(8),
217
+ h: inches(1),
218
+ text: 'Free-form text box',
219
+ });
220
+ addSlideImage(slide2, imageBytes, { x: inches(1), y: inches(3), w: inches(3), h: inches(3) });
221
+
222
+ const dup = duplicateSlide(pres, slide2);
223
+ moveSlide(pres, dup, 0);
224
+
225
+ const out: Uint8Array = await savePresentation(pres);
226
+ ```
227
+
228
+ ### Replace an image in place
229
+
230
+ ```ts
231
+ import {
232
+ getShapeKind,
233
+ getShapeName,
234
+ getSlideShapes,
235
+ getSlides,
236
+ loadPresentation,
237
+ savePresentation,
238
+ setShapeImage,
239
+ } from '@office-kit/pptx';
240
+
241
+ const pres = await loadPresentation(templateBytes);
242
+ for (const slide of getSlides(pres)) {
243
+ for (const shape of getSlideShapes(slide)) {
244
+ if (getShapeKind(shape) === 'picture' && getShapeName(shape) === 'Logo') {
245
+ setShapeImage(shape, newLogoBytes); // format auto-detected; geometry preserved
246
+ }
247
+ }
248
+ }
249
+ const out = await savePresentation(pres);
250
+ ```
251
+
252
+ ### Node convenience entry
253
+
254
+ ```ts
255
+ import { loadPresentationFile, savePresentationToFile } from '@office-kit/pptx/node';
256
+
257
+ const pres = await loadPresentationFile('./template.pptx');
258
+ await savePresentationToFile(pres, './out.pptx');
259
+ ```
260
+
261
+ ### Charts
262
+
263
+ ```ts
264
+ import {
265
+ addSlideChart,
266
+ getSlides,
267
+ loadPresentation,
268
+ savePresentation,
269
+ inches,
270
+ } from '@office-kit/pptx';
271
+
272
+ const pres = await loadPresentation(templateBytes);
273
+ const slide = getSlides(pres)[0];
274
+ addSlideChart(slide!, {
275
+ x: inches(0.5),
276
+ y: inches(0.5),
277
+ w: inches(8),
278
+ h: inches(4.5),
279
+ spec: {
280
+ kind: 'column', // bar | column | line | pie | doughnut | area
281
+ categories: ['Q1', 'Q2', 'Q3', 'Q4'],
282
+ series: [
283
+ { name: 'Revenue', values: [120, 180, 240, 300] },
284
+ { name: 'Cost', values: [80, 90, 130, 160] },
285
+ ],
286
+ title: 'FY26 plan',
287
+ },
288
+ });
289
+
290
+ await savePresentation(pres);
291
+ ```
292
+
293
+ The embedded xlsx that PowerPoint requires for "Edit data" is generated
294
+ automatically. Inline `<c:strCache>` / `<c:numCache>` caches mean the
295
+ chart renders without opening the workbook.
296
+
297
+ ### Animations
298
+
299
+ ```ts
300
+ import { setShapeAnimation, getSlideShapes, getSlides } from '@office-kit/pptx';
301
+
302
+ const slide = getSlides(pres)[0]!;
303
+ const shape = getSlideShapes(slide)[0]!;
304
+ setShapeAnimation(shape, { effect: 'fadeIn', durationMs: 800 });
305
+ // effects: 'fadeIn' | 'fadeOut' | 'appear' | 'disappear'
306
+ ```
307
+
308
+ ### Comments
309
+
310
+ ```ts
311
+ import { addSlideComment, getSlides } from '@office-kit/pptx';
312
+
313
+ const slide = getSlides(pres)[0]!;
314
+ addSlideComment(slide, {
315
+ author: { name: 'Reviewer A' },
316
+ text: 'Punch up the numbers here.',
317
+ position: { x: 1_000_000, y: 1_000_000 }, // optional EMU coords
318
+ });
319
+ ```
320
+
321
+ ### Gradient fills
322
+
323
+ ```ts
324
+ import { setShapeGradientFill } from '@office-kit/pptx';
325
+
326
+ setShapeGradientFill(shape, {
327
+ stops: [
328
+ { offset: 0, color: '#FF0000' },
329
+ { offset: 1, color: '#0000FF' },
330
+ ],
331
+ angleDeg: 90, // top → bottom
332
+ });
333
+ ```
334
+
335
+ ### Validation
336
+
337
+ ```ts
338
+ import { validatePresentation } from '@office-kit/pptx';
339
+
340
+ const issues = validatePresentation(pres);
341
+ for (const i of issues) console.error(i.severity, i.message);
342
+ // Catches missing rels, dangling slide ids, layouts without masters, etc.
343
+ ```
344
+
345
+ ### API surface (current state)
346
+
347
+ Each row lists the free-function entry points. Read/write pairs are
348
+ shown together.
349
+
350
+ | Capability | API |
351
+ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
352
+ | Load / save | `loadPresentation(input)`, `savePresentation(pres)`, `loadPresentationFile(path)` (node), `savePresentationToFile(pres, path)` (node) |
353
+ | Create | `createPresentation({ size?: '16:9' \| '4:3' })` — blank deck with master + theme + `Blank` / `Title Slide` / `Title and Content` layouts |
354
+ | Slide CRUD | `getSlides`, `getSlideAt`, `getSlideIndex`, `addSlide`, `removeSlide`, `moveSlide`, `duplicateSlide`, `clearSlideShapes` |
355
+ | Slide layout | `getSlideLayouts`, `findSlideLayout` (by name — case-sensitive, exact; pass a `RegExp` for case-insensitive), `findSlideLayoutByType` (by locale-stable `type` token — preferred), `getSlideLayout(slide)`, `setSlideLayout(slide, layout)`, `getSlideLayoutName`, `getSlideLayoutType` |
356
+ | Slide metadata | `getSlideTitle` / `setSlideTitle`, `getSlideSize` / `setSlideSize`, `isSlideHidden` / `setSlideHidden`, `getSlideText` |
357
+ | Slide sections | `getSlideSections`, `setSlideSections` (p14 sectionLst) |
358
+ | Placeholders | `findSlidePlaceholder(slide, 'title' \| 'body' \| ...)` |
359
+ | Token / text replace | `replaceTokensInPresentation`, `replaceTokensInSlide`, `replaceTextInPresentation`, `replaceTextInSlide` |
360
+ | Background | `getSlideBackground` / `setSlideBackground` / `clearSlideBackground` |
361
+ | Notes | `getSlideNotes` / `setSlideNotes` |
362
+ | Transitions | `getSlideTransition` / `setSlideTransition` / `clearSlideTransition` |
363
+ | Animations | `getShapeAnimation` / `setShapeAnimation` (`fadeIn` / `fadeOut` / `appear` / `disappear`), `clearSlideAnimations` |
364
+ | Comments | `addSlideComment`, `getSlideComments`, `removeSlideComment`, `getCommentAuthors`, `getCommentText` / `getCommentAuthor` / `getCommentPosition` |
365
+ | Shape authoring | `addSlideTextBox`, `addSlideShape`, `addSlideLine`, `addSlideTable`, `addSlideImage`, `addSlideChart` |
366
+ | Shape lookup | `findShapeByName`, `findShapesByName`, `findShapesByKind`, `findShapeInPresentation`, `getAllShapes`, `getSlideShapes` |
367
+ | Shape text | `setShapeText`, `setShapeBullets`, `setShapeAlignment`, `setShapeTextFormat`, `setShapeHyperlink` / `getShapeHyperlink` |
368
+ | Per-paragraph | `setParagraphAlignment` / `getParagraphAlignment`, `setParagraphLevel` / `getParagraphLevel`, `setParagraphBullet` / `getParagraphBullet`, `setParagraphSpacing` / `getParagraphSpacing`, `setParagraphLineSpacing` / `getParagraphLineSpacing` |
369
+ | Per-run text | `setShapeRunText` / `getShapeRunText`, `setShapeRunFormat` / `getShapeRunFormat`, `getShapeParagraphCount`, `getShapeRunCount` |
370
+ | Text frame | `setShapeTextAnchor` / `getShapeTextAnchor`, `setShapeTextMargins` / `getShapeTextMargins` |
371
+ | Fill | `setShapeFill` / `getShapeFill`, `setShapeGradientFill`, `setShapePatternFill`, `setShapeImageFill`, `setShapeNoFill`, `clearShapeFill` |
372
+ | Stroke | `setShapeStroke` / `getShapeStroke`, `setShapeStrokeDash` / `getShapeStrokeDash`, `setShapeStrokeArrow` / `getShapeStrokeArrow`, `…NoStroke` |
373
+ | Effects | `setShapeShadow` / `setShapeGlow` / `getShapeEffect`, `clearShapeEffects` |
374
+ | Geometry | `setShapePosition`, `setShapeSize`, `setShapeRotation`, `setShapeFlip`, `setShapeBounds` / `getShapeBounds` |
375
+ | Pictures | `setShapeImage`, `setShapeImageCrop` / `getShapeImageCrop`, `setShapeImageOpacity` / `getShapeImageOpacity`, `setShapeImageBrightness`, `…Contrast` |
376
+ | Z-order | `bringShapeToFront`, `sendShapeToBack`, `bringShapeForward`, `sendShapeBackward` |
377
+ | Click actions | `setShapeClickAction` / `getShapeClickAction` (`url` / `slide` / `nextSlide` / `prevSlide` / `firstSlide` / `lastSlide`) |
378
+ | Shape removal | `removeShape` |
379
+ | Tables | `getTableCell` / `getTableCells`, `setTableCellText` / `getTableCellText`, `setTableCellFill` / `clearTableCellFill`, `setTableCellAlignment`, `setTableCellTextFormat`, `insertTableRow` / `removeTableRow`, `insertTableColumn` / `removeTableColumn` |
380
+ | Charts | `addSlideChart`, `getSlideCharts`, `setChartSpec` — kinds: `bar`, `column`, `line`, `pie`, `doughnut`, `area` |
381
+ | Theme | `getPresentationTheme` — color scheme (`accent1`..`accent6`, `dark1`, `light1`, `hyperlink`, ...) |
382
+ | Validation | `validatePresentation(pres)` — invariant checks, returns `ValidationIssue[]` |
383
+ | Units | `inches(n)`, `cm(n)`, `mm(n)`, `pt(n)`, `emu(n)` — return branded `Emu` numbers |
384
+
385
+ ## Compatibility
386
+
387
+ - **Node**: >= 24.16.
388
+ - **Browsers**: current and current-1 of Chrome, Firefox, Safari, Edge.
389
+ - **TypeScript**: >= 5.4 (for strict `satisfies` and `const` type parameters).
390
+ - **Output**: PPTX files checked against the ECMA-376 schemas with `xmllint`
391
+ where it is available, and smoke-tested against PowerPoint (current),
392
+ Keynote (current), Google Slides, and LibreOffice Impress.
393
+
394
+ ## Development
395
+
396
+ ```sh
397
+ git clone --recurse-submodules git@github.com:office-kit/pptx.git
398
+ cd @office-kit/pptx
399
+ pnpm install
400
+ pnpm test
401
+ ```
402
+
403
+ If you already cloned without submodules:
404
+
405
+ ```sh
406
+ git submodule update --init --recursive --depth 1
407
+ ```
408
+
409
+ `references/` holds reference implementations and spec material we read
410
+ while building this library. See `references/README.md`.
411
+
412
+ ## Contributing
413
+
414
+ Before opening an issue or PR, please read `CLAUDE.md` — it documents the
415
+ project's design rules, the "one way to do one thing" policy, and what
416
+ counts as a real bug report vs. a low-effort AI-generated one.
417
+
418
+ PRs are expected to:
419
+
420
+ - Follow the template (`.github/pull_request_template.md`).
421
+ - Include a failing test in the same PR that the change makes pass.
422
+ - Add a changeset (`pnpm changeset`) for user-visible changes.
423
+ - Pass `pnpm typecheck`, `pnpm lint`, and `pnpm test`.
424
+
425
+ ## License
426
+
427
+ [MIT](./LICENSE)