A PowerPoint renderer and layout-aware editor that runs entirely in the browser.
Open a .pptx, see it rendered faithfully, edit every element on the slide, switch layouts and
themes and have them genuinely cascade, then export a file that still opens in real PowerPoint with
everything you did not touch left byte-for-byte intact.
Status: pre-alpha, under active construction. Twelve packages are on npm as
@pptx-studio/*, everylatestwith a provenance attestation a consumer can verify withnpm audit signatures; the API will change before 1.0. Sub-phases 0.1 (repository skeleton), 0.2 (ZIP reader, decompression budgets, OPC part-name grammar), 0.3 (part store, content types, relationships, ZIP32 writer), 0.4 (XML tokenizer and node model), 0.5 (serializer and the byte-identical round-trip gate), 0.6 (schema order, Markup Compatibility, invertible edits), 0.7 (ground-truth experiments) and 0.8 (inspect, the Worker boundary, the benchmark) are complete, which closes Gate 0: drop a.pptxon a page and get a live explorer of its internals — every part, content type, relationship edge and a feature census — computed in a Web Worker, entirely in the tab. A 188 MiB, 300-slide deck reads in about a third of a second.Phase 1 is complete too — 1.1 (the corpus), 1.2 (the repair firewall), 1.3 (the writer), 1.4 (the round-trip oracle), 1.5 (
bisectand the PowerPoint oracle) and 1.6 (the CI gate and the badge above) — which closes Gate 1: drop a deck carrying charts, SmartArt, animations, OLE objects and macros onto the page, press Save, and the file your browser downloads opens in real PowerPoint 365 with no repair prompt and the same ten slides and the same shapes on each of them. Measured, not asserted:pnpm gate1drives the actual page in Chromium, captures the actual download, and asks PowerPoint through COM withOpenAndRepairoff, which is the only setting under which a repair is an error rather than a silent success.The table below tracks what actually works, and it will not be marked green ahead of the code.
Every existing web PowerPoint tool sits at one of two extremes.
Server-side viewers render well and hand you a picture. You cannot edit a picture.
Browser editors that import .pptx — Canva, PPTist, PPTXjs — flatten each slide into a fixed list
of absolutely-positioned boxes the moment they parse it. That flattening is exactly why their
slides feel fixed: once a shape's missing a:xfrm has been defaulted to a concrete number, the
information that the shape never had explicit geometry is destroyed, and with it any ability to
switch layouts, swap themes, or cascade anything at all. They also regenerate the file on export,
so charts, SmartArt, animations and OLE objects are lost on the way out.
PPTX Studio takes the opposite position on both:
- The OPC package is the document. Our model is a projection of it. Parts we do not edit are streamed back out byte-for-byte, so preservation is the default state rather than a feature that has to be implemented per content type.
- Inheritance is never flattened. A property that is absent stays absent.
undefinedmeans "ask the resolver", and the resolver walks slide → layout → master → theme every time. That one decision is what makes Change Layout, theme swaps and per-property Reset possible at all.
Read SCOPE.md for what this deliberately is not.
| Area | Status |
|---|---|
| Repository, toolchain, architecture guards | ✅ 0.1 |
| OPC container: ZIP reader, budgets, part names | ✅ 0.2 |
| OPC container: parts, content types, relationships | ✅ 0.3 |
| Byte-preserving XML tokenizer and node model | ✅ 0.4 |
| XML serializer and the byte-identical round trip | ✅ 0.5 |
| Schema order, Markup Compatibility, invertible edits | ✅ 0.6 |
| Ground truth: embedded fonts, colour transforms | ✅ 0.7 |
Feature census, cli inspect, the Worker boundary |
✅ 0.8 |
| The corpus: 55 licensed decks, three producers | ✅ 1.1 |
The repair firewall: 34 rules, cli validate |
✅ 1.2 |
| Writer: dirty-part export, media GC, prepare hooks | ✅ 1.3 |
The round-trip oracle and cli roundtrip |
✅ 1.4 |
cli bisect and the PowerPoint oracle |
✅ 1.5 |
| The CI round-trip gate and badge | ✅ 1.6 |
| Open, re-save and download in the browser | ✅ Gate 1 |
| The 187 preset shape definitions | ✅ 2.1 |
| The formula evaluator: 17 operators, built-in guides | ✅ 2.2 |
| Arc math: the unskew, winding, the whole-turn split | ✅ 2.3 |
Path emit, and custGeom as the same thing |
✅ 2.4 |
| Adjust handles: drag one, get the value to write | ✅ 2.5 |
| Colour: six bases, 28 transforms, theme and clrMap | ✅ 2.6 |
| Gradients and the 54 pattern tiles, both measured | ✅ 2.7 |
| Dashes, joins, arrowheads and effects, all measured | ✅ 2.8 |
| The document model, and the inheritance walk | ✅ 2.9 |
| Both renderers: transforms, groups, flip before rot | ✅ 2.10 |
| The debug overlay and the 187-preset gallery | ✅ 2.11 |
| Image fills: crop, tile, and the blip colour effects | ✅ 2.12 |
| Picture shapes, and the border drawn outside them | ✅ 2.13 |
| Gate 2 side by side, with per-slide diff heatmaps | ✅ 2.14 |
| Painted: fills, strokes, gradients, effects, groups | ✅ Gate 2 |
| Text parse and the 10-source cascade | ✅ 3.1 |
| Measurement and the line model | ✅ 3.2 |
| Line breaking | ✅ 3.3 |
| Autofit, view and edit modes | ✅ 3.4 |
| Bullets, fields, script runs | ✅ 3.5 |
| Anchors, insets, vertical text | ✅ 3.6 |
| Font substitution and the guard | ✅ 3.7 |
| Text in both renderers | ✅ 3.8 |
| The fidelity harness, and the Linux baseline | ✅ 3.9 |
First npm release: render-svg and cli render |
✅ 3.10 |
| A hundred slides at any zoom, through the page | ✅ 3.11 |
| The public website, from npm, on GitHub Pages | ✅ 3.12 |
| A 100-slide deck at any zoom, entirely client-side | ✅ Gate 3 |
| Table parse: the grid, and the spans that are the merge | ✅ 4.1 |
| Tables and SmartArt | ⬜ Phase 4 |
| Select, move, resize, rotate | ⬜ Phase 5 |
| Text editing | ⬜ Phase 6 |
| Layout switching, theme verbs, backgrounds | ⬜ Phase 7 |
| Font embedding, custom font upload and export | ⬜ Phase 8 |
| Charts | ⬜ Phases 9–10 |
| Accessibility, virtualization, 1.0 | ⬜ Phase 12 |
packages/
opc/ OPC container: zip, parts, content types, relationships
xml/ byte-preserving XML tokenizer, XNode, serializer
census/ what is inside a package: parts, relationship graph, feature census
geometry/ preset and custom geometry: 187 shapes, the evaluator, arcs, paths, handles
paint/ colour, fills, strokes and effects, every rule measured against PowerPoint
model/ sheets, the inheritance chain, and the resolver everything else reads
render-svg/ slides to SVG: group transforms, flip order, grpFill, as a string
render-dom/ the live renderer, over the same node tree as render-svg
validate/ the repair firewall: the 34 rules a .pptx must not break
writer/ export: dirty-part-only serialization, media GC, prepare hooks
cli/ the Node entry point — inspect, validate, roundtrip, bisect, render
apps/
studio/ drop a .pptx on a page; the parse Worker boundary lives here
website/ the public site: docs and a live demo of every package, installed from npm
tools/
repo/ the rules about the repo: structure, links, layering, legal, the plan table
release/ the candidate gate on this commit's tarballs, and what npm is asked
eslint-rules/ local ESLint rules with no upstream equivalent
schema-codegen/ the ECMA-376 element-order table generator
geometry-codegen/ the preset-shape transcoder, and the 187-name cross-check
ground-truth/ experiments that ask real PowerPoint what it actually does
bench/ synthetic decks, and the browser benchmark that reads them
corpus/ the corpus generators, its five rules, and the roster
corpus/
decks/ 45 synthetic probe decks, one feature each but a43's five and a46's hundred
authored/ 9 decks PowerPoint wrote, so the corpus has a second producer
written/ 1 deck our own writer round-tripped
reject/ 17 packages PowerPoint refuses, one measured finding each
ground-truth/ what PowerPoint answered, as committed fixtures
bench/ benchmark deck recipes, their hashes, and recorded timings
docs/adr/ architecture decision records
docs/plan/ the 13 phases, and where the build has got to
docs/plan/README.md is generated from docs/plan/phases.json: which
sub-phases have a record, which gates are closed, and the debt carried across them.
Further packages arrive with the phases that need them. Every one of them is already declared in
tools/repo/layering/layers.ts with its layer and runtime, because a
guard that arrives after the code it guards has already missed the first violation.
Three constraints hold this design together, and all three are checked by CI rather than by convention:
- The core never touches Node.
@pptx-studio/*runs in a browser tab and in a Web Worker.node:*imports and Node globals are banned by ESLint; a"node"condition in an exports map is banned by the layering checker; and the core test suite runs in real Chromium rather than jsdom, because under jsdomnode:fsresolves and the leak stays invisible until someone opens a tab. - Dependencies run one way only.
opc/xml→geometry/paint/text/fonts→model→ renderers →react. Enforced at the manifest level bypnpm layering, which also catches cycles, upward edges, and React appearing anywhere below the one package allowed to know it exists. - No top-level await. A single one makes a module asynchronous and therefore un-requireable from CommonJS, for every consumer, forever. A local ESLint rule rejects it.
Run all three with pnpm check.
That is Gate 1, and it is one command:
pnpm build
pnpm gate1 # a real browser, a real download, and real PowerPoint
pnpm gate1 --no-powerpoint # everything a machine without Office can doIt serves apps/studio, drives the page in Chromium, presses Save twice — once
with no edit and once with one — captures what the browser actually downloads, reads it back, and
then asks PowerPoint through COM whether it opens. Measured on 2026-09-02, PowerPoint 365 build
16.0.20326, on corpus/decks/a43-kitchen-sink.pptm — ten slides carrying four
charts, three SmartArt diagrams, three OLE objects, three animation timelines and a VBA project:
| save | parts | rewritten | streamed | differs in | PowerPoint |
|---|---|---|---|---|---|
| unchanged | 55 | 0 | 55 | nothing | opened, no repair |
| with a stamp | 55 | 1 | 54 | /docProps/core.xml |
opened, no repair |
The first row is the gate. Zero parts rewritten is the architecture in one number: a part
nobody edited is copied out of the source archive still compressed, so there is no category of
content that can be silently dropped on the way out. PowerPoint reported the same ten slides with
the same shape counts — [7, 2, 2, 3, 2, 2, 2, 3, 3, 3] — for the downloaded file as for the
original.
The second row is why the demo has two buttons. Setting cp:lastModifiedBy is the smallest edit in
the package and changes nothing on any slide, but it is the only path in which a part is serialised
afresh — and a demo that can only ever emit the bytes it read has not exercised the writer.
OpenAndRepair is passed off, which matters more than it sounds. It defaults to on, and under
automation the repair is silent: a file PowerPoint would prompt about opens successfully, already
repaired, and reports success. With it off the repair becomes a catchable error, which is the only
way that answer is worth anything.
The badge at the top says 55/55 decks, and it means something narrow and checkable: every
licensed deck in corpus/ is read, written back out, and compared against the original
— 1737 parts in all, 1013 as canonical XML, 676 as relationship graphs with the ids treated as
opaque labels, and 48 by SHA-256.
It is not a byte comparison and must not become one. Entry order, deflate level, timestamps and
attribute order all differ legitimately between two archives holding one document; nine of the
PowerPoint-authored decks come out exactly 1832 bytes smaller with every stored byte of every entry
identical, because Office writes a 0xA220 growth-hint extra field on five entries and we do not.
It does not say the decks open in PowerPoint. No hosted runner has Office on it — GitHub's
Windows images ship Visual Studio's Office development workload, which cannot open a file. That
question is answered by pptx-studio bisect --oracle powerpoint, on a machine with PowerPoint, by
hand.
The number is committed to .github/badges/roundtrip.json
rather than published from a workflow run, because it is a fact about the contents of this
repository and nothing about a run decides it. pnpm roundtrip recomputes it and fails if the two
disagree, so the badge cannot claim 55/55 unless the last run to touch it measured 55/55 — and no
workflow here needs write access to anything.
Requires Node ≥ 24.11 and pnpm ≥ 11.
pnpm install
pnpm browsers # one-off: Chromium for the core test suite
pnpm check # structure, layering, legal, references, corpus, format, build,
# lint, typecheck, round trip, fidelity, Gate 2, Gate 3, package QA, testIndividual steps: pnpm structure, pnpm layering, pnpm legal, pnpm references,
pnpm corpus, pnpm lint, pnpm typecheck, pnpm build, pnpm roundtrip, pnpm fidelity,
pnpm gate2, pnpm gate3, pnpm pkg:qa, pnpm test.
To look inside a deck:
node packages/cli/dist/cli.js inspect deck.pptx # after pnpm build
node packages/cli/dist/cli.js validate deck.pptx # the 33 must-not-break rules
node packages/cli/dist/cli.js render deck.pptx --out svg/ # every slide as SVG, no browser
node packages/cli/dist/cli.js roundtrip deck.pptx # is it still the same deck
node packages/cli/dist/cli.js bisect a.pptx b.pptx # which change broke it
node tools/bench/serve.ts --decks <dir> # then drop one on the pagetools/bench/ also generates the decks the benchmark reads — up to 300 slides and 200 MB, from a
recipe, deterministically. See tools/bench/README.md.
pnpm test and pnpm typecheck read package source, not dist, so neither needs a build
first. Publishing reads dist.
dewiride-open-source.github.io/Dewiride-PPTX-Studio
is built from website/: the documentation for every published package and a live
demo of each, running entirely in the tab on the sample decks or on a .pptx you drop on it.
It sits outside pnpm-workspace.yaml on purpose and installs @pptx-studio/* from the public
registry with npm. Nothing else in this repository does: everything here resolves through the pnpm
link farm, so nothing else can tell whether a published tarball actually works. Three things build
it: the release gate, against this commit's own tarballs; the daily canary, against latest with
no lockfile; and website.yml, against the ranges committed here, after every release — so the
site is what the last release proved and never ahead of it. The ranges are the caret of each
workspace version, written by pnpm version-packages and refused by the gate when they are not.
cd website
npm install
npm run devContributions are welcome. See CONTRIBUTING.md — this project uses the
Developer Certificate of Origin (a Signed-off-by line), not a CLA.
If you are looking for somewhere to start, the geometry presets in Phase 2 are unusually well-suited to a first contribution: each one has an objective visual test and a number that moves.
Apache-2.0. See NOTICE for third-party attributions and
LEGAL.md for the reimplementation basis and the clean-room record.
Not affiliated with or endorsed by Microsoft. PowerPoint is a trademark of Microsoft Corporation.