office-kit-pptx · diff
git:20260919.96a37b5 to git:20260920.f183d04
69 added, 320 removed. Audit A to A.
---
name: office-kit-pptx
- description: Author and edit PowerPoint (.pptx) files from TypeScript/JavaScript with @office-kit/pptx — in Node or the browser. Use when an agent must generate a presentation from scratch, fill a template, or programmatically edit slides, shapes, text, tables, charts, and images, and needs the output to open cleanly in PowerPoint, Keynote, Google Slides, and LibreOffice.
+ description: Create and revise editable PowerPoint presentations with typed TSX and a live slide preview. Use for a new slide deck, changes to an existing PPTX or template, or iterative presentation design with office-kit. Handles project setup, preview, checking and PPTX export.
---
- # Authoring PPTX with @office-kit/pptx
-
- `@office-kit/pptx` generates **schema-valid** Office Open XML PresentationML. Every
- authoring call maps to a specific ECMA-376 element, so a deck you build here
- opens and is **fully editable** in PowerPoint — not a flattened image, and not
- "valid enough to usually open." That is the bar: _if a file can't be opened and
- edited in PowerPoint, it isn't done._
-
- This guide is the fast path for an LLM agent. It covers the canonical call for
- each capability, the design rules that separate a real deck from an
- AI-template-looking one, the handful of footguns worth memorizing, and a QA
- loop you must run before declaring success.
+ # Create slides with office-kit
- ## When to use this
+ Turn the user's brief into an editable PPTX. You operate the authoring tools;
+ the user describes the content and reviews slides in the browser. Make content
+ and layout changes in TSX. The browser viewer has no editing controls.
- - **Create from scratch** — build a deck programmatically and emit `.pptx`.
- - **Fill a template** — load an existing `.pptx`, replace text/images, add
- slides from its layouts, emit the result.
- - **Edit** — mutate shapes, text, tables, charts on existing slides.
+ ## Start or resume a project
- If you need a _pixel-perfect render_ (print/archival) or format conversion
- (Keynote/ODP), this is the wrong tool — use PowerPoint/LibreOffice headless.
+ For an existing office-kit slide project, read its `CLAUDE.md`, `package.json`
+ and presentation source, then use its installed dependencies and scripts.
+ Do not initialize over existing work or upgrade packages just to edit a deck.
- ## Setup
+ For a new deck, choose a new child directory in the user's workspace (for example,
+ `my-slides`). Run the setup yourself; do not hand the user a list of setup commands.
+ Prerequisites are Node.js 22.18+ and npm. If either is missing, explain the missing
+ prerequisite instead of attempting system-wide installation.
```sh
- npm install @office-kit/pptx
- ```
-
- ```ts
- import {
- createPresentation,
- addTitleSlide,
- addContentSlide,
- addBlankSlide,
- addSlideTextBox,
- addSlideShape,
- addSlideTable,
- addSlideChart,
- addSlideImage,
- findSlidePlaceholder,
- setShapeText,
- setShapeRunFormat,
- setShapeFill,
- savePresentation,
- } from '@office-kit/pptx';
- // Node-only convenience (reads/writes files):
- import { loadPresentationFile, savePresentationToFile } from '@office-kit/pptx/node';
- ```
-
- `savePresentation(pres)` returns a `Uint8Array`. In Node, write it with
- `fs.writeFile`; in the browser, wrap it in a `Blob`.
-
- ## Mental model (read this once)
-
- 1. **One free-function API.** Every capability is a named export that takes the
- thing it operates on (`PresentationData`, `SlideData`, a shape handle) as its
- first argument. There are no classes to instantiate and no fluent chains.
- Import only what you use.
- 2. **Units are EMU.** Positions/sizes are English Metric Units (914400 per inch).
- Always go through the unit helpers — `inches(1)`, `cm(2)`, `mm(5)`, `pt(18)`,
- `emu(n)` — never raw numbers.
- 3. **Build, then format.** Add a shape/slide; it returns a handle. Pass that
- handle to formatting functions (`setShape*`). Order of formatting calls does
- not matter — the library inserts each XML child at its schema-mandated slot.
- 4. **Colors** are `#RRGGBB`, the 3-digit shorthand `#RGB`, bare `RRGGBB`, or a
- theme token (`accent1`…`accent6`, `tx1`, `bg1`, `dk1`, `lt1`, `hlink`). An
- unrecognized color **throws** — it is never silently emitted. One exception:
- chart series colors accept the hex forms but **not** theme tokens (a series
- must resolve to a concrete sRGB value).
-
- ## Core workflow — build a deck from scratch
-
- `createPresentation()` returns an immediately-authorable deck (slide master,
- Office theme, and `Blank` / `Title Slide` / `Title and Content` layouts) with
- no slides. Defaults to 16:9; pass `{ size: '4:3' }` for the classic ratio.
-
- ```ts
- const pres = createPresentation();
-
- // Title slide — sugar picks the right layout by its locale-stable type token.
- const cover = addTitleSlide(pres, 'FY26 Business Review');
- const subtitle = findSlidePlaceholder(cover, 'subTitle');
- if (subtitle) setShapeText(subtitle, 'Strategy, results, and the road ahead');
-
- // Title + body content slide.
- const agenda = addContentSlide(pres, { title: 'Agenda' });
- const body = findSlidePlaceholder(agenda, 'body');
- // IMPORTANT: bullet *content* is multi-line text + a bullet style, NOT a list arg.
- if (body) setShapeText(body, 'Highlights\nFinancials\nRoadmap\nRisks', { bullets: 'bullet' });
-
- const out = await savePresentation(pres);
+ npx --yes --registry=https://registry.npmjs.org @office-kit/pptx-dev@latest init my-slides
+ cd my-slides
+ npm install --registry=https://registry.npmjs.org
```
- Slide constructors:
-
- | Goal | Call |
- | --------------------------------- | ----------------------------------------------------------------- |
- | Title slide | `addTitleSlide(pres, title)` |
- | Title + body (bulleted) | `addContentSlide(pres, { title, body })` |
- | Section divider | `addSectionHeaderSlide(pres, title)` |
- | Empty canvas for free-form layout | `addBlankSlide(pres)` |
- | Bind a layout explicitly | `addSlide(pres, { layout: findSlideLayoutByType(pres, 'obj')! })` |
-
- Prefer `findSlideLayoutByType(pres, 'title' | 'obj' | 'secHead' | 'blank')` —
- the `type` token is stable across PowerPoint UI languages. `findSlideLayout`
- matches the localized, case-sensitive display name.
-
- ## Capability cheat-sheet (the canonical call)
-
- Content on a blank slide (all positions via `inches`/`cm`/`pt`):
-
- ```ts
- const slide = addBlankSlide(pres);
-
- // Text box.
- const tb = addSlideTextBox(slide, {
- x: inches(0.6),
- y: inches(0.4),
- w: inches(8),
- h: inches(1),
- text: 'A strong year',
- });
- setShapeRunFormat(tb, 0, 0, { bold: true, size: 32, color: '#1F2937', font: 'Arial' });
-
- // Preset shape with centered text + gradient + shadow.
- const card = addSlideShape(slide, {
- preset: 'roundRect',
- x: inches(0.6),
- y: inches(1.6),
- w: inches(3.6),
- h: inches(2.2),
- text: 'Revenue +38%',
- textAnchor: 'ctr',
- });
- setShapeGradientFill(card, {
- stops: [
- { offset: 0, color: '#2563EB' },
- { offset: 1, color: '#1E3A8A' },
- ],
- angleDeg: 90,
- });
- setShapeShadow(card, {
- color: '#000000',
- blurEmu: pt(8),
- offsetEmu: pt(3),
- angleDeg: 90,
- opacity: 0.35,
- });
-
- // Table (firstRow + bandRow give the banded header look).
- addSlideTable(slide, {
- x: inches(0.6),
- y: inches(1.6),
- w: inches(8.2),
- h: inches(2.5),
- rows: [
- ['Metric', 'FY25', 'FY26'],
- ['Revenue', '$120M', '$166M'],
- ['Margin', '64%', '67%'],
- ],
- firstRow: true,
- bandRow: true,
- });
-
- // Chart (embedded workbook + caches are generated automatically).
- addSlideChart(slide, {
- x: inches(0.6),
- y: inches(1.1),
- w: inches(8.2),
- h: inches(4.2),
- spec: {
- kind: 'column', // bar | column | line | pie | doughnut | area
- categories: ['Q1', 'Q2', 'Q3', 'Q4'],
- series: [
- { name: 'Revenue', values: [120, 138, 152, 166] },
- { name: 'Plan', values: [115, 130, 148, 160] },
- ],
- title: 'Revenue vs plan ($M)',
- dataLabels: { showValue: true, showCategory: false, showSeriesName: false, showPercent: false },
- // Axis tick labels: `categoryAxisTickLabelPos` / `valueAxisTickLabelPos` / `secondaryValueAxis.tickLabelPos`
- // ('none' | 'low' | 'high' | 'nextTo'). Series outline: `lineColor`; marker fill / outline: `markerColor` / `markerLineColor` (fill defaults to the series color, outline to the fill); label leader lines: `dataLabels.showLeaderLines`.
- // Axis line / gridline widths (EMU, 12700 = 1 pt): `valueAxisLineWidthEmu`, `valueAxisMajorGridlineWidthEmu`,
- // the `categoryAxis…` mirrors, `secondaryValueAxis.lineWidthEmu` / `majorGridlineWidthEmu`. Unset = PowerPoint's 0.75 pt.
- // At least one series must stay on the primary axis when others set `secondaryAxis: true`.
- },
- });
+ Use the published packages. No clone of the office-kit repository, pnpm build,
+ local tarballs or peer-dependency overrides are needed. If setup fails, report
+ the actual error and resolve it before proceeding. The initializer refuses an
+ existing directory; reuse a presentation project or choose a new directory.
+ Read the generated `CLAUDE.md` before authoring. It is not necessarily loaded
+ automatically when Claude was started in the parent directory.
- // Image (format auto-detected from the bytes; `fit` controls letterbox/crop).
- addSlideImage(slide, pngBytes, {
- x: inches(0.6),
- y: inches(1.1),
- w: inches(8.2),
- h: inches(4.2),
- fit: 'contain',
- });
- ```
+ ## Author and revise
- Formatting and slide features (one canonical call each):
+ Read [the TSX reference](references/tsx.md) before writing the deck. It covers
+ native elements, charts, tables, template selection and the Raw escape hatch.
+ The default export of `deck.tsx` is a `Presentation`. Use ordinary TypeScript
+ functions and data to reuse content; do not add React, Vue or a web UI.
- | Capability | Call |
- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
- | Whole-shape text | `setShapeText(shape, text, { bullets? })` (split lines with `\n`) |
- | Mixed-format paragraphs | `setShapeParagraphs(shape, [{ align?, runs: [{ text, format? }], endFormat? }])` (several runs per paragraph; `endFormat: { size }` gives a run-less paragraph its font size, read back with `getParagraphEndFormat`) |
- | One run's format | `setShapeRunFormat(shape, p, r, { bold, italic, underline, size, color, font, highlight, ... })` |
- | Paragraph align / level | `setParagraphAlignment(shape, p, 'ctr')`, `setParagraphLevel(shape, p, 1)` |
- | Paragraph spacing / leading | `setParagraphSpacing(shape, p, { beforePts, afterPts })`, `setParagraphLineSpacing(shape, p, { kind: 'pct', value: 1.5 })` |
- | Solid / gradient / pattern fill | `setShapeFill(shape, '#2563EB')`, `setShapeGradientFill(...)`, `setShapePatternFill(...)` |
- | Outline | `setShapeStroke(shape, { color, widthEmu })` + `setShapeStrokeDash/Arrow/Cap/Join` |
- | Effects | `setShapeShadow(shape, {...})`, `setShapeGlow(shape, {...})` |
- | Table cell text / merge | `setTableCellParagraphs(getTableCell(table, r, c), [{ align?, runs, endFormat? }])`, `mergeTableCells(table, { row, col, rowSpan, colSpan }, { coveredText: 'drop' })` (`'drop'` removes the covered cells' text; default keeps it) |
- | Geometry | `setShapePosition/Size/Rotation/Flip/Bounds`, `bringShapeToFront`, `sendShapeToBack` |
- | Picture corrections | `setShapeImageCrop/Opacity/Brightness/Contrast` (brightness/contrast in `[-1, 1]`) |
- | Hyperlink / click action | `setShapeHyperlink(shape, url)`, `setShapeClickAction(shape, { kind: 'nextSlide' })` |
- | Slide background | `setSlideBackground(slide, '#102030')`, `setSlideBackgroundImage(slide, bytes)` |
- | Transition | `setSlideTransition(slide, { effect: 'fade' })` — key is **`effect`**, not `type` |
- | Animation | `setShapeAnimation(shape, { effect: 'fadeIn' })` (`fadeIn`/`fadeOut`/`appear`/`disappear`) |
- | Speaker notes | `setSlideNotes(slide, '...')` |
- | Comments | `addSlideComment(slide, { author: { name }, text })` |
- | Sections | `setSlideSections(pres, [{ name, slides: [...] }])` |
+ - Use the supplied brief, audience and source material. Ask only for information
+ needed to proceed; label illustrative data when real figures are unavailable.
+ - Choose a consistent palette and typography, with clear hierarchy and enough
+ space for text. Inspect chart labels, table density, contrast and overflow.
+ - Keep text, shapes, charts and tables as native objects. Do not rasterize whole
+ slides to make a preview look correct.
+ - Keep assets and imported components under the slide project directory so they
+ are watched. Load files relative to their source module with
+ `readFile(new URL('./asset.png', import.meta.url))`.
+ - For an existing PPTX, copy it into the project, inspect it with
+ `npx --no-install office-pptx inspect template.pptx`, then use source bytes and
+ `mode="edit"`. Preserve unmentioned slides, masters, layouts and unknown parts.
+ Use compose mode only when the request calls for a new slide sequence. Keep
+ the source file separate from the exported `deck.pptx`.
+ - Prefer typed elements. When a needed capability is only in the core library,
+ use `Raw` with public core APIs and consult [the core reference](references/core-api.md)
+ and installed type declarations. Do not invent props or describe Raw support
+ as complete declarative coverage.
- ## Fill a template instead
+ ## Preview for the user
- ```ts
- import {
- loadPresentation,
- getSlides,
- findSlidePlaceholder,
- setShapeText,
- replaceTokensInPresentation,
- setShapeImage,
- getSlideShapes,
- getShapeKind,
- getShapeName,
- savePresentation,
- } from '@office-kit/pptx';
+ Run `npm run dev` in the slide project using your terminal tool's background-task
+ support. Wait for `Preview: ...`, open that exact URL if browser tools are
+ available, and give it to the user. Keep the server running while iterating and
+ reuse it on follow-up edits. If the port is busy, use `npm run dev -- --port 0`
+ and read the assigned URL. Do not stop unrelated processes.
- const pres = await loadPresentation(templateBytes);
+ Saving TSX updates the preview. The user selects slides in the vertical thumbnail
+ strip, uses Fit/zoom, or chooses Present for presentation mode. Review the slides
+ with available browser/image tools and fix problems in TSX. If visual inspection
+ is unavailable, state that limitation rather than claiming a visual check.
- // Placeholder text:
- const title = findSlidePlaceholder(getSlides(pres)[0]!, 'title');
- if (title) setShapeText(title, 'Q3 Review');
+ A failed build leaves the last successful preview visible with an error. Fix the
+ error before treating the visible slides or Download PPTX as current output.
+ The viewer does not contain an AI chat; the conversation stays in Claude Code.
- // Token fill across every slide ({{name}}, {{date}}, ...):
- replaceTokensInPresentation(pres, { name: 'Alice', date: '2026-12-01' });
+ ## Check and deliver
- // Swap an image in place (geometry preserved):
- for (const s of getSlides(pres))
- for (const sh of getSlideShapes(s))
- if (getShapeKind(sh) === 'picture' && getShapeName(sh) === 'Logo')
- setShapeImage(sh, newLogoBytes);
+ Run both commands after the final edits:
- const out = await savePresentation(pres);
+ ```sh
+ npm run check
+ npm run build
```
- Out-of-scope parts are preserved on round-trip — the library never silently
- strips content it doesn't model.
-
- ## Design rules (so it doesn't look AI-generated)
-
- A schema-valid deck can still look like a template. Before building, commit to a
- look and apply it consistently:
-
- - **Pick a bold, content-informed palette** and let _one_ color dominate
- (~60–70% of visual weight), with one accent. Don't scatter six accent colors
- per slide. Define the palette once as hex constants and reuse it.
- - **Every slide needs a visual element** — a chart, table, image, colored
- shape, or a strong type hierarchy. Avoid bullet-only text slides; convert
- lists into two-column layouts, icon/number rows, or small-multiple cards.
- - **Don't repeat the same layout** slide after slide. Alternate: full-bleed
- title, two-column, chart-led, table-led, quote/stat callout.
- - **Typography**: titles 32–44pt bold, body 14–18pt, one or two font families.
- Keep a ≥0.5in (`inches(0.5)`) margin from the slide edge; don't crowd edges.
- - **Numbers deserve charts**, not sentences. A trend → line; parts of a whole →
- pie/doughnut; comparison across categories → column/bar.
- - **Avoid the tells**: a thin accent line under every title, dense walls of
- bullets, clip-art, and four different accent colors all read as "generated."
-
- Sizing reference (16:9 deck): the canvas is `inches(13.333) × inches(7.5)`.
- Keep content within `x ∈ [0.5, 12.83]`, `y ∈ [0.5, 7.0]` inches.
-
- ## Footguns (memorize these — each is a real, easy mistake)
-
- - **Bullets are content + style, not a list argument.** To make a bulleted
- list: `setShapeText(shape, 'A\nB\nC', { bullets: 'bullet' })`.
- `setShapeBullets(shape, style)` sets the _bullet glyph style_ (`'bullet'` |
- `'number'` | `'none'` | `{ char }` | `{ autoNum }`) on existing paragraphs —
- it is NOT how you set the text.
- - **`setShapeFill(shape, color)` takes a color string**, e.g.
- `setShapeFill(card, '#059669')` — not an object.
- - **Transitions key on `effect`**: `setSlideTransition(slide, { effect: 'fade' })`.
- `{ type: 'fade' }` is wrong. `effect: 'none'` emits no transition (use
- `clearSlideTransition` to remove one).
- - **Multi-line text** in a text box, shape, or table cell uses `\n` between
- lines — each becomes its own paragraph. A literal newline inside one run is
- not a line break.
- - **Authorable chart kinds** are `bar`, `column`, `line`, `pie`, `doughnut`,
- `area`. `scatter`/`radar`/`bubble` are read-only today (authoring them
- throws). `pie`/`doughnut` take exactly one series.
- - **Find placeholders by type token**, not display name:
- `findSlidePlaceholder(slide, 'title' | 'body' | 'ctrTitle' | 'subTitle')`.
-
- ## QA protocol — run this before saying "done"
-
- 1. **Structural validation.** `const issues = validatePresentation(pres);` —
- treat any `severity === 'error'` as a blocker (missing rels, dangling slide
- ids, layouts without masters, etc.). Fix and re-run until clean.
- 2. **Round-trip.** `loadPresentation(await savePresentation(pres))` must not
- throw and must report the same slide count.
- 3. **Schema validity.** If you have `xmllint` + the ECMA-376 XSDs, validate each
- emitted part. The library's own test suite gates this; if you author novel
- combinations, validate them too.
- 4. **Visual check.** Render to an image and _look at it_ — overflowing text,
- collisions, off-canvas shapes, and unreadable color contrast do not show up
- in schema validation. Use `@office-kit/pptx-preview` (SVG in the browser, PNG on the
- server) or open the file in PowerPoint/LibreOffice. `findShapesOutsideCanvas`
- and `findOverlappingShapePairs` catch layout problems programmatically.
- 5. **Content check.** Grep the saved deck's text (`getPresentationText(pres)`)
- for leftover placeholder tokens (`{{`, "Lorem", "TODO") before shipping.
-
- ## Known limitations (don't fight these — design around them)
-
- Read-pass-through or post-1.0, _not_ authorable today:
-
- - `scatter` / `radar` / `bubble` charts, combo charts, and secondary value axes.
- - Multiple independently-formatted runs **within one paragraph** (inline rich
- text like "make _this word_ bold"); each paragraph is authored as one run.
- Use per-shape or per-paragraph formatting, or split across shapes.
- - Constructing new themes / masters / layouts from scratch (author _on top of_
- the ones `createPresentation` or a template provides).
- - SmartArt authoring, complex multi-step animation timing trees, OLE/ActiveX,
- and document encryption.
- - Modern threaded comments (legacy `<p:cm>` comments are read + written).
-
- ## Worked example
+ Type checking is separate from the live rebuild. Fix errors and review all slides,
+ including template slides retained in edit mode. The SVG preview is a rendering
+ aid, not a guarantee of PowerPoint fidelity or animation/media playback.
- A complete, validated multi-slide business deck is in
- [`examples/business-deck.md`](examples/business-deck.md). It is exercised by the
- library's test suite (`test/skill-example.test.ts`), so the code there is known
- to produce a schema-valid deck.
+ Deliver the preview URL, the generated `deck.pptx` path and the editable project
+ path. State any unverified rendering or unsupported requested feature. On the
+ next revision, edit the same project, rerun the checks and rebuild the output.