npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@betteroffice/pptx

v0.2.0

Published

PowerPoint-faithful PPTX editor and viewer core — full OOXML fidelity, real-time human + AI agent collaboration. Framework-free (Rust/Wasm).

Readme

@betteroffice/pptx

Framework-free core for the BetterOffice PPTX editor — the Rust parser, yrs deck model, slide layout, and display-list engine compiled to WebAssembly, plus the Canvas2D replay host.

bun add @betteroffice/pptx

Most apps want the turnkey React component in @betteroffice/pptx-react. Use this package directly to build custom presentation chrome.

Open and render a slide

import {
  initWasm,
  openPresentation,
  paintSlide,
  sizeCanvasForSlide,
} from '@betteroffice/pptx';

await initWasm();
const bytes = new Uint8Array(await file.arrayBuffer());
const deck = openPresentation(bytes, {
  fonts: [{ family: 'My Sans', bytes: fontBytes }],
});

const frame = deck.layoutSlide(0);
sizeCanvasForSlide(canvas, frame, devicePixelRatio);
await paintSlide(canvas.getContext('2d')!, frame, devicePixelRatio);

Browsers can call initWasm(); Node and SSR pass wasm bytes to initWasm(bytes).

All parsing, edits, collaboration state, text shaping, layout, hit-testing, and display-list emission stay in Rust. The package decodes the typed boundary and replays the resulting primitives on canvas. Font bytes are supplied by the host and registered with the Rust shaper through openPresentation.

Beyond rendering, PresentationHandle covers editing: version-checked batches (readContent / findText / validateEdits / applyEdits), read-only structured export (exportStructured / exportMarkdown), text (insertText / deleteText / formatText / setParagraphAlignment), slides (insertSlide / deleteSlide / moveSlide), shapes (addTextBox / addShape / addPicture / moveShape / resizeShape / setShapeRect), paint order (bringShapeToFront / sendShapeToBack / bringShapeForward / sendShapeBackward), comments (addComment / replyToComment / setCommentStatus / removeComment), hitTest, undo/redo, and save(), which serializes the deck back to .pptx bytes with edits applied — untouched slides keep their exact source part bytes. addPicture accepts common raster/vector MIME types (PNG, JPEG, GIF, BMP, TIFF, WebP, SVG) up to 8 MiB, rejecting anything past either limit before it reaches the deck.

Agent proposals

Stage a group of edits, inspect it, then accept or reject it:

const slide = deck.snapshot().slides[0];
const shape = slide.shapes.find((shape) => shape.textStories.length > 0)!;
const story = shape.textStories[0];
const proposal = deck.propose('editor-agent', 'Make the title concise', [{
  type: 'replaceText',
  storyId: story.id,
  start: 0,
  end: story.paragraphs[0].runs.reduce((length, run) => length + run.text.length, 0),
  text: 'A clear title',
}]);

const preview = deck.previewProposal(proposal.id);
const proposedSlide = deck.layoutProposalSlide(proposal.id, 0);
const diff = deck.layoutProposalDiffSlide(proposal.id, 0);
await paintSlide(canvas.getContext('2d')!, diff.frame, devicePixelRatio, 1, {
  textChanges: diff.textChanges,
});
deck.acceptProposal(proposal.id);
deck.undo();

listProposals() returns agent attribution, notes, edits, before/after targets, and current staleTargets. previewProposal() and layoutProposalSlide() replay against the current deck without changing it. rejectProposal(id) removes a pending group. Acceptance applies the whole group in one undo step and emits one local update; an invalid edit prevents the entire group from being staged or accepted.

layoutProposalDiffSlide() lays removed and inserted text out together, keeping unchanged words as context and retaining fonts and emphasis. Pass its textChanges to paintSlide() for red highlights and strikethrough on deletions, and green highlights and underlines on insertions. Its snapshot and UTF-16 ranges describe a temporary review layout; use the live handle for editing and the ordinary proposed layout for the result after acceptance.

Supported edits replace text within one paragraph, format text, align paragraphs, set shape geometry/fill/stroke/adjustments, and replace speaker notes. Text offsets use UTF-16 and shape coordinates use EMU. Text replacement inherits the style at its start unless style is supplied. Edits run in order, so later text offsets refer to the result of earlier edits in the group.

Changing a targeted shape or its notes makes acceptance throw StaleProposalError with target IDs. Review a fresh preview before calling acceptProposal(id, { force: true }); force still validates targets and ranges. Unrelated peer edits survive acceptance and Undo. Up to 64 proposals, each with 1–256 edits, may be pending.

Pending proposals live in the open session; accepted edits save and synchronize. isProposalsAvailable() supports hosts that load an older WASM build.

Version-checked edit batches

Read the deck with its session version, then apply a batch against that version: every step commits in one transaction, one update and one undo step, or the batch returns a typed refusal and nothing changes.

const read = deck.readContent();
if (!read.ok) throw new Error(read.failure.message);
const story = read.stories[0];
const within = { slideId: story.slideId, shapeId: story.shapeId, storyId: story.storyId };

const result = deck.applyEdits({
  expectVersion: read.version,
  steps: [
    { op: 'replaceText', target: { kind: 'search', within, text: 'Q3' }, text: 'Q4' },
    { op: 'setSlideNotes', target: { slideId: story.slideId }, text: 'Updated for Q4' },
  ],
});
if (!result.ok) console.warn(result.failure.code, result.failure.stepIndex);

A story reads as its paragraphs joined by \n. Offsets are story-local UTF-16 positions, and each entry of paragraphs gives a paragraph's span, its soft line breaks (which also read as \n) and its field results. findText() searches exactly, case-sensitively and within paragraphs; a search target must match exactly once in its story, overlapping occurrences included. Every target and guard resolves against expectVersion, so a later step never sees an earlier step's offsets.

Steps insert, replace and delete text within one paragraph, format text, align paragraphs, replace speaker notes, and set the rectangle, fill or outline of a shape at the top of a slide (fill and outline on preset shapes). An expect guard refuses its step unless the target still reads as expected. Receipts locate each change in the final state. validateEdits() runs every check, staging included, without changing anything. history: 'none' keeps a batch out of undo history and existing undo and redo entries in place; source: 'agent' records provenance only.

Refusals carry a code (stale-version, missing-target, ambiguous-target, content-mismatch, overlapping-steps, unsupported, invalid-step, limit-exceeded), the failing stepIndex and the target; malformed requests throw, and so do NaN or infinite numbers. Slide, shape, story and paragraph ids and versions are session-scoped.

Structured export

exportStructured() returns the committed deck as JSON with the version it was read at, and exportMarkdown() renders that same read as Markdown. Neither flushes editor input or changes anything. exportPptxStructured(bytes), exportPptxMarkdown(bytes) and renderPptxMarkdown(content) do the same headless, with anchors that address the returned snapshot only.

import { exportPptxMarkdown } from '@betteroffice/pptx';

const read = deck.exportStructured({ includeNotes: true });
if (!read.ok) throw new Error(read.failure.message);
console.log(read.content.slides);
const { markdown, anchors } = await exportPptxMarkdown(bytes);

Slides come in deck order with their original index; shapes follow the current shape tree depth first, a group's descendants at the group's position and table cells row by row. This is the authored order, not a reading order inferred from geometry. Paragraphs carry their level, effective alignment, authored bulletJson and the resolved list marker (inherited from the layout, master and text styles and numbered as the renderer numbers them); runs carry formatting marks, links, soft line breaks, fields with their cached result and zero-width placeholders for inline content such as equations. Tables keep their grid, spans and merge continuations with each cell's current story. Pictures, video, audio, charts, SmartArt, embedded objects and unmodelled shape-tree elements become placeholders with their alternative text and relationships. Each slide exports its own shapes, with an inherited-content-omitted diagnostic for layout and master shapes drawn on it.

Every record carries an anchor: range anchors are batch text targets in the story offsets of readContent(), so a session export's range anchor can be a step's target at the version it was read at; notes and comment ranges index their plain text, and records seeded from the file carry provenance (part, SHA-256, element path, sldId, cNvPr id). Session anchors belong to the returned version. A collaboration session opened from an update seeded by an older release, without its source file, may not know which slides are hidden: those slides are exported with hidden: null and a visibility-unknown diagnostic, and reopening the session with its source file restores their visibility. Hidden slides and shapes, speaker notes (plain text) and comments are excluded unless includeHiddenSlides, includeHiddenShapes, includeNotes or includeComments asks for them, and includeFormatting: false drops marks. Everything left out or not represented is listed in diagnostics. maxBlocks (10,000 by default; slides, shapes, paragraphs, notes and comments count) and maxBytes (8 MiB of compact JSON) stop the export at a whole record and set truncated. Unusable limits come back as an invalid-options or limit-exceeded refusal (PptxExportError for the headless functions); malformed options throw.

Markdown renders slide headings, title placeholders as headings, paragraphs with literal list markers, pipe or entity-escaped HTML tables, object placeholders, notes and comments, with a <!-- pptx-export:N --> marker before each block that anchors maps back to its source.

Comments

PowerPoint has two comment systems and a file only ever uses one: legacy reads in every version of PowerPoint plus LibreOffice and Google Slides, while modern carries replies and resolution for PowerPoint 365. A deck commits to one at its first comment, so setCommentFlavor only works while comments() is empty, and replyToComment / setCommentStatus throw on a legacy deck.

Saving patches existing comment XML, preserving identities, formatting, task metadata, anchors and unknown fields. Unedited parts remain byte-identical. Removing a thread removes its known replies; a concurrent reply survives as a new root if its parent was deleted.

deck.addComment(deck.snapshot().slides[0].id, {
  author: 'Ada Lovelace',
  initials: 'AL',
  text: 'Tighten this claim.',
  created: new Date().toISOString(),
  xEmu: 1_828_800,
  yEmu: 914_400,
});

Positions are EMU, like every other coordinate. created is supplied by the caller rather than read from a clock, so peers replaying the same edits converge.

Collaboration

PresentationHandle is a collaboration replica. Pair it with CollaborationProvider and a transport implementing the small CollaborationTransport interface. The provider speaks the standard Yjs sync-v1 wire protocol, performs state-vector handshakes, forwards only local updates, and bounds frames and pending backpressure bytes.

import { CollaborationProvider } from '@betteroffice/pptx';

const provider = new CollaborationProvider(deck, transport, {
  user: { name: "Ada" }, // identity for this peer's presence chip
});
deck.onUpdate((_update, origin) => {
  if (origin === 'remote') repaint();
});
provider.connect();

Development

The generated .wasm binary is intentionally not committed. From the repository root, install wasm-pack 0.15.0 and binaryen, then run bun scripts/build-pptx-wasm.ts. Package builds copy the binary into dist/generated.

JavaScript guide · Changelog · Apache-2.0.

Operation profiles

layoutSlideProfiled returns { layout, profile } with scope, layout, and serialization durations in milliseconds. Profiled mutation methods such as insertTextProfiled and addTextBoxProfiled return { receipt, profile }; undoProfiled reports undo, snapshot, and serialization timings. These methods share the normal operations and add stage timing. See the corpus and browser tests for usage and timing limits.

Host undo and comment controls

handle.setUndoCaptureMode('manual') groups tracked local edits across pauses and operation types until handle.addUndoBoundary(). The getter handle.undoCaptureMode() returns 'auto' or 'manual'. Changing modes closes the current group and preserves history; setting the same mode is a no-op. Auto preserves the existing policy (500 ms capture on native targets, separate transactions in the browser). Remote and agent origins remain outside local undo. These controls group history; edit batches make edits atomic.

handle.anchorCaret(storyId, index) returns a caret anchor, plain data that later edits, undo, redo and remote updates carry along with the text; handle.resolveCaretAnchor(anchor) returns its current UTF-16 offset, or null once the story is gone.

handle.setCommentPosition(commentId, { xEmu, yEmu }) moves an existing root comment on its current slide, preserving identity, author, text, replies, and resolution state through collaboration, undo/redo, and export. Coordinates must be safe integers in EMU. Replies share their root's position. Unknown IDs, reply IDs, and invalid coordinates fail before mutation. Legacy PowerPoint comments quantize exported positions to master units (1/576 inch).