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

@office-kit/pptx-preview

v0.21.0

Published

Experimental preview renderer for @office-kit/pptx: render a slide to SVG (browser + Node) or rasterize it to a PNG/RGBA image in Node, with no browser binary.

Readme

@office-kit/pptx-preview

Preview renderer for @office-kit/pptx. Turns a @office-kit/pptx slide model into an SVG (browser + Node) or rasterizes it to a PNG / RGBA image in Node — with no headless browser.

Experimental (0.x). This package lives in the @office-kit/pptx monorepo and also powers the docs-site playground and the fidelity harness. The renderer is an approximation of desktop presentation app / LibreOffice output and is still evolving; the API may change between minor versions, and 0.x semver applies (a minor bump may break). See fidelity below.

Why

@office-kit/pptx core does not render — by design. But "show me this deck" comes up constantly: a docs playground, a thumbnail service, a visual-diff test. The hard requirement is that rendering must work in Node and rasterize to an image without spawning a browser, so it fits CI and serverless. This package lays text out as pure SVG <text> (no <foreignObject>) and paints it with resvg, which has no browser dependency.

Entry points

| Import | Runtime | Use | | ------------------------------- | -------------- | --------------------------------------------------- | | @office-kit/pptx-preview | browser + Node | renderSlideToSvg → an SVG string | | @office-kit/pptx-preview/node | Node only | renderSlideToImage / renderSlideToRgba → pixels |

The browser entry pulls in no Node built-ins (no node:fs, resvg, or fontkit), so it bundles cleanly for the web.

Usage

SVG (browser or Node)

import { renderSlideToSvg } from '@office-kit/pptx-preview';
import { loadPresentation, getSlides } from '@office-kit/pptx';

const pres = await loadPresentation(bytes);
const svg = renderSlideToSvg(pres, getSlides(pres)[0]);
// → '<svg …>…</svg>'  (text laid out via <foreignObject> — the browser wraps it)

Pass { textLayout: 'svg', measureText } to lay text out as pure SVG <text> instead of <foreignObject> (for example when the SVG is shown as an <img> or sanitized). In that mode each run's font is replaced with one of the bundled metric-compatible substitutes (substituteFamily: Carlito, Caladea, Liberation Sans / Serif / Mono). To keep a family the viewer can load — say through @font-face rules you embed in the SVG — pass resolveFamily, and a measureText that knows that font's metrics so line breaks match its glyphs (in a browser, for example, one that wraps CanvasRenderingContext2D.measureText once the font has loaded). Return a single family name; the renderer quotes it in the SVG when CSS requires that:

import { renderSlideToSvg, substituteFamily } from '@office-kit/pptx-preview';

const svg = renderSlideToSvg(pres, slide, {
  textLayout: 'svg',
  measureText,
  resolveFamily: (family) =>
    family === 'Lora' || family === 'Inter' ? family : substituteFamily(family),
});

The bundled substitutes have no Japanese, Chinese or Korean glyphs. Pass eastAsianFamily to write a run's East Asian characters (kana, kanji, Hangul, and CJK and full-width punctuation) in a family the viewer has, in the weights the deck uses; otherwise the glyphs are left to the rasterizer's fallback, which in resvg ignores bold.

renderSlideLayoutToSvg(pres, layout, { master }) draws what a slide master editor shows behind the placeholders: the layout's background and decorative shapes over its master's (the master's are left out when the layout hides background graphics), or with master: true the slide master's own background and shapes. Placeholders are not drawn.

Underlines in custom HTML editors

textUnderlineStyle(underline, color) returns inline CSS for a DrawingML underline, using the same patterns as the HTML preview. Pass a resolved CSS color (for example, #154687) because SVG background patterns cannot inherit the surrounding text color. Apply the style to an inline element; keep any strikethrough on an outer element. For words, apply it separately to each non-whitespace segment. false, none, and undefined return an empty string. Pass false as the third argument to let native CSS decorations inherit the text color; image-based patterns still use the supplied color.

PNG / RGBA (Node, no browser)

import { renderSlideToImage, renderSlideToRgba } from '@office-kit/pptx-preview/node';
import { loadPresentationFile, getSlides } from '@office-kit/pptx/node';

const pres = await loadPresentationFile('deck.pptx');
const slide = getSlides(pres)[0];

// PNG-encoded bytes:
const png = renderSlideToImage(pres, slide, { width: 1280 });

// Raw RGBA pixels (+ the same frame PNG-encoded), for SSIM / diffing:
const { image, png: png2 } = renderSlideToRgba(pres, slide, { width: 1280 });
// image: { width, height, data: Uint8Array }  // row-major RGBA

The Node path lays text out as pure <text> and measures it with a fontkit measurer over bundled metric-compatible fonts (Carlito ≈ Calibri, Caladea ≈ Cambria, Liberation ≈ Arial/Times/Courier; OFL / Apache-2.0, see fonts/LICENSES.md). The measurer, resvg's font set, and the SVG family names all reference the same fonts, so wrap/positioning math agrees with the painted glyphs and the result is deterministic (no system fonts).

For Japanese, Chinese or Korean text, pass the faces in fontFiles — the regular and the bold one — and register them with buildFontkitMeasurer({ fonts }) for the measurer. East Asian characters are drawn in the family of the first fontFiles face that covers kana, in the face closest to each run's weight.

Every authored font name is drawn in a bundled substitute unless you pass resolveFamily. Map a deck's font to a family you load through fontFiles (and register with the measurer) to draw the run in that face; East Asian characters keep the kana face above, and resolveFamily decides the rest.

const noto = 'fonts/NotoSansCJKjp-Regular.otf';
const png = renderSlideToImage(pres, slide, {
  fontFiles: [noto],
  measureText: buildFontkitMeasurer({ fonts: [{ family: 'Noto Sans CJK JP', source: noto }] }),
  resolveFamily: (family) => (family === 'Meiryo' ? 'Noto Sans CJK JP' : substituteFamily(family)),
});

Text-layout audit (overflow / soft-wrap detection)

import { auditTextLayout } from '@office-kit/pptx-preview';
import { buildFontkitMeasurer } from '@office-kit/pptx-preview/node';

// Register the fonts the deck actually uses (especially CJK) for accurate widths.
const measureText = buildFontkitMeasurer({
  fonts: [{ family: '游ゴシック', source: 'fonts/YuGothic.ttf' }],
});

const issues = auditTextLayout(pres, { measureText, reportSoftWraps: true });
// → [{ kind: 'overflow-y', slideIndex: 2, shapeName: 'Title 1', overflowPx: 14.3, shapeOverflowPx: 9.5, lineHeightPx: 52.8, approximate: false }, …]

auditTextLayout measures every shape's text body — placeholders, text boxes, autoshapes, shapes inside groups — with the same engine the preview renders with, honouring the effective bodyPr cascade (insets, wrap, anchor, vertical text, columns) and normAutofit shrinking. It returns one issue per finding:

| kind | Meaning | Extra fields | | ----------------- | ----------------------------------------------------------------------------------- | ------------------------------ | | overflow-x | Text ink escapes the box horizontally (typically wrap="none") | overflowPx and the two below | | overflow-y | Wrapped text is taller than the box (or escapes the top when bottom-anchored) | overflowPx and the two below | | soft-wrap | A paragraph wrapped onto more lines than its authored <a:br> count (段落ち) | paragraphIndex, extraLines | | word-break | A wrap split a word (Latin / Greek / Cyrillic letters or digits, or a katakana run) | paragraphIndex, text | | short-last-line | A wrapped paragraph ends on a line of one or two characters (泣き別れ) | paragraphIndex, text | | small-font | The shape draws text below minFontSizePt, after autofit shrinking | sizePt |

overflowPx is measured against the text area (the box minus its insets); shapeOverflowPx is how much ink lies outside the shape itself, both sides added (a centre-anchored body spills above and below; 0 while the spill stays inside the insets, where nobody sees it); lineHeightPx is the tallest line's height, about how far one misjudged wrap moves the text.

Every issue also carries slideIndex, shapeName, and approximate — true when a width had to be estimated (the heuristic measurer, or glyphs the available fonts don't cover), in which case treat borderline verdicts as advisory.

Options:

  • measureText — the measurer. Defaults to the browser-safe heuristic (≈0.55 em per Latin glyph, 1 em per CJK glyph, always approximate); pass buildFontkitMeasurer() in Node for real glyph metrics.
  • tolerancePx — overflow at or below this many px (96 DPI) is ignored. Default 1; raise it if you only care about visibly broken slides.
  • reportSoftWraps — opt into soft-wrap issues. Off by default because wrapping is normal for body text; turn it on when auditing titles or labels meant to stay on one line.
  • reportBadWraps — opt into word-break and short-last-line issues (horizontal text only). A word inside a kanji compound cannot be told apart from its neighbours, so only the scripts listed above are checked. A word-break issue is reported once per word; its text is the word with | at each line break. short-last-line counts characters, not words, so a Latin last line of to is reported and one of the is not.
  • minFontSizePt — report small-font for shapes with text below this size.

Accuracy: the bundled fonts are metric-compatible with the common default fonts, so verdicts for Calibri / Cambria / Arial / Times / Courier decks match real glyph widths; for any other font (custom brand fonts, Japanese fonts), register the real files via buildFontkitMeasurer({ fonts }) — a registered font is used both for runs that name it and as a glyph fallback for CJK text in runs that resolve to a Latin face. Line-break positions can differ from the reference desktop app by a few characters in edge cases (kinsoku, hyphenation), which is what the default 1 px tolerance absorbs. Table cell text is not audited yet.

Fidelity

This is a high-fidelity preview, not a spec-complete presentation renderer. Preset and custom geometry, solid/gradient/pattern/image fills (including the placeholder layout/master cascade), strokes, rotation, effects (shadow, glow, soft edge, reflection), images with adjustments, charts (column, bar, line, area, pie, doughnut, scatter, radar, bubble), tables with per-run cell text and every part of their table style (the 74 built-in table styles included: fills, translucent bands, borders, text, the Themed Styles' background, and the six style options; the background's shadow is not drawn), vertical and multi-column text in both text-layout modes, picture bullets, and template (layout/master) decoration all render. EMF pictures containing solid-filled line/Bézier paths, including rectangular and path clipping with copy semantics, render as transparent SVG images. Other EMF drawing commands, WMF, SmartArt, animations, and 3D fall back to labelled placeholders carrying a machine-readable marker (below). Per-slide closeness to a LibreOffice baseline is measured and gated in CI by the fidelity harness in the monorepo (site/fidelity) — mean fg-SSIM ≈ 0.78 across the corpus, with the residual gaps documented there.

Fallback markers

When a shape cannot be rendered (unsupported format, missing bytes, or unrecognised content type), the renderer emits a labelled placeholder rectangle. The placeholder's top-level <g> element carries a data-pptx-fallback attribute so automated tooling can detect partial renders without string-parsing the label text:

| Value | Trigger | | ---------------- | ----------------------------------------------------------- | | "image" | Image bytes missing (external link) or format not decodable | | "chart" | Chart kind not modelled by this renderer | | "graphicFrame" | Graphic frame with no recognised content (SmartArt, etc.) | | "custGeom" | Shape uses custom geometry (<a:custGeom>) |

Example: svg.querySelectorAll('[data-pptx-fallback]') lists every shape that did not fully render.

Mapping the SVG back to the deck

Slide shapes, and the paragraphs and table cells inside them, carry attributes that tie a click on the SVG back to the presentation. Master and layout decoration carries none.

| Attribute | On | Value | | --------------------- | --------------------------------------------------------- | ---------------------------------------------------- | | data-pptx-shape-id | A <g> wrapping each shape; groups and each member alike | getShapeId(shape) | | data-pptx-paragraph | <p> (foreignObject), each line's <text> (svg mode) | 0-based paragraph index | | data-pptx-cell | A <g> wrapping each table cell's fill and text | row,col, 0-based; paragraphs count within the cell |

const shape = target.closest('[data-pptx-shape-id]')?.getAttribute('data-pptx-shape-id');
const paragraph = target.closest('[data-pptx-paragraph]')?.getAttribute('data-pptx-paragraph');

License

MIT (code). Bundled fonts: OFL-1.1 / Apache-2.0 — see fonts/LICENSES.md.

List numbering in custom editing surfaces

paragraphNumberLabels(paragraphs) accepts an array of { bulletStyle, level } and returns a number label or null for each paragraph. It uses the same per-level counters as the slide renderer, including restarts after a non-numbered paragraph or a change of numbering scheme. Common Arabic, alphabetic and Roman schemes are supported; other schemes currently fall back to Arabic numbers followed by a period. Labels are display data and should not be inserted into the editable paragraph text.

Text regions for editing overlays

shapeTextRect(shape) returns where a shape lays out its text, as fractions of its own extent: the <a:rect> its custom geometry states, or the one ECMA-376 defines for its preset (an ellipse's inscribed rectangle, a triangle's lower middle), following its adjust values. It is null when text takes the whole box. resolveTextBodyRect(bounds, margins, region) places that region in bounds less the body insets, giving the same inner text rectangle the preview uses before wrapping and autofit. Pass {x, y, w, h} and {left, top, right, bottom} in the same unit, normally EMU, with inherited/default margins already resolved. Margins that would collapse a region are dropped; a full box with nonpositive width or height stays degenerate so callers can handle it explicitly.