pptx-kit 0.0.0 → 0.3.0

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