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

@otisk/preview

v3.1.0

Published

Browser bindings for the otisk PDF engine: render_page_rgba / page_count for HTML5-canvas previews, and render_document_pdf for sRGB PDF/A-2b documents (wasm-bindgen). For CMYK / PDF/X-4 print output use otisk-server.

Readme

@otisk/preview

Typeset HTML and CSS into pages in the browser or in Node: a pixel-exact preview of each page for a <canvas>, a page count, and a PDF document (sRGB, PDF/A-2b). It is the otisk engine compiled to WebAssembly, so a preview here and a PDF from otisk-server come out of the same layout.

This package makes screen and archive documents. CMYK PDF/X-4 print masters come from otisk-server.

Install

npm install @otisk/preview

The package is an ES module. Call its default export once to load the WebAssembly before anything else.

A page preview in the browser

import init, { render_page_rgba } from "@otisk/preview";

await init(); // fetches otisk_wasm_bg.wasm next to the module

const html = `<h1>Invoice 2026-09-04</h1><p>Thank you for your order.</p>`;
const css = `@page { size: A4; margin: 15mm } h1 { font-size: 24pt }`;

const page = render_page_rgba(html, css, null, null, null, 0, 2); // page 1 at 144 dpi
const canvas = document.querySelector("canvas");
canvas.width = page.width;
canvas.height = page.height;
canvas
  .getContext("2d")
  .putImageData(new ImageData(new Uint8ClampedArray(page.rgba), page.width, page.height), 0, 0);

console.log(`${page.pageCount} pages`, page.diagnostics.map((d) => `${d.code} ${d.message}`));

A PDF in Node

examples/node.mjs in this package is the runnable version:

import { readFileSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import init, { render_document_pdf, page_count } from "@otisk/preview";

const wasm = createRequire(import.meta.url).resolve("@otisk/preview/otisk_wasm_bg.wasm");
await init({ module_or_path: readFileSync(wasm) }); // Node has no fetch for file URLs

const html = `<h1>Hello</h1>`;
const css = `@page { size: A5; margin: 20mm }`;
console.log(page_count(html, css, null, null, null), "page(s)");
writeFileSync("hello.pdf", render_document_pdf(html, css, null, null));

HTML and CSS

Every call takes the document as html and a stylesheet as css:

  • <style> elements in the document apply, in document order, as in a browser. A <style media="screen"> block is skipped (this is a print engine); <style media="print"> applies. One difference from a browser: a <style> inside inline <svg> styles only that SVG.
  • css applies after them, so on equal specificity css wins. Use it for a theme or overrides you keep outside the document. Pass "" when the document carries all its CSS.
  • <link rel="stylesheet"> is not loaded. It raises the notice W0040 instead (an info-level diagnostic, never an error). Inline the file in a <style> element or pass its text as css. If you link a file for a browser preview and pass the same text as css, that is fine; the notice just confirms the link was not fetched.
  • @import works, inside <style> and in css, through the assets resolver below.
  • style="…" attributes apply, above every rule, as in a browser.

html can be a whole document or just a body fragment.

Page size and margins

Page size comes from @page { size: … } (A4 portrait when the CSS sets none). Margins come from one of two places:

| margin_mm | Margins used | |---|---| | null or undefined | the document's @page { margin: … }, or 25 mm on every side if it sets none | | a number | that many millimetres on all four sides, overriding @page |

So if your CSS sets the margins, pass null. A number wins over the CSS, which is what you want when one template should render with different margins, and a surprise when your layout was computed for the @page margins: a page that sizes itself as calc(297mm - 2 * 15mm) overflows if you pass 25. OtiskEngine takes the same value in its constructor.

Fonts and images

The engine bundles Noto Sans (with Latin, Hebrew, Arabic and CJK fallbacks) and uses it for any family it cannot find. Everything else comes through the assets argument: a function (url) => Uint8Array, or an object with a fetch(url) method.

const files = {
  "fonts/Inter-Regular.woff2": interRegularBytes,
  "img/logo.svg": logoBytes,
};
const assets = (url) => files[url]; // return undefined for anything you don't have

render_page_rgba(html, css, null, assets, null, 0, 1);
  • It is called synchronously for every @font-face src, <img src>, url() in CSS and @import, with the URL exactly as written. Load the bytes before you render (it cannot return a Promise).
  • data: URLs never reach it; they are decoded inline.
  • Fonts: TTF, OTF, TTC, WOFF, WOFF2. Images: PNG, JPEG, WebP, SVG.
  • A miss is not an error: the render goes on and reports it (W0004 for a font, W0011 for an image).

To use a font in many renders without @font-face, register it on a handle:

import { OtiskEngine } from "@otisk/preview";

const engine = new OtiskEngine(null);
engine.tryRegisterFont("Inter", interRegularBytes); // throws if the bytes are not a usable font
const page = engine.renderPageRgba(html, css, null, 0, 2); // font-family: Inter just works

Diagnostics

A render does not stop at the first problem. It sets the page with what it has and lists what went wrong in page.diagnostics; each entry has code, severity, message and, for CSS, line and column. Branch on code:

| Code | Meaning | |---|---| | W0003 | an unknown or unsupported CSS property was ignored | | W0004 | a font was not found; the bundled face was used | | W0011 | an image could not be loaded or decoded | | W0040 | (info) a <link rel="stylesheet"> was not loaded |

render_page_rgba throws a string when it cannot return a page at all. render_page_rgba_bounded throws a RasterFailure with kind, message, diagnostics and the page count instead, and refuses a page above a pixel budget (raster_too_large: lower scale and retry). render_document_pdf and page_count do not return diagnostics; render a page preview to see them.

What is supported

Paged media is the point: @page size and margins, margin boxes (running headers and footers, page numbers), page breaks, footnotes, counters and cross-references. Layout: block, inline, flex, grid, tables, floats, multi-column, absolute positioning. Text: full OpenType shaping, kerning and ligatures, hyphenation, justification, text-wrap: balance and pretty, right-to-left scripts. Colour: CSS colours, gradients, shadows, masks, opacity, CMYK and spot colours (previewed through output_intent). Images: PNG, JPEG, WebP, SVG (as images and inline).

Not supported, by design: JavaScript in the document, <link rel="stylesheet">, interactive CSS (:hover, cursor, transitions, animations), and anything that needs a network: every external file comes through assets.

The canvas preview approximates a few things the PDF draws exactly: a transformed element and an opacity group paint without their transform or group alpha, and dashed or dotted borders paint solid.

Common mistakes

  • Passing a number for margin_mm when the CSS sets @page margins. The number wins. Pass null.
  • Expecting <link rel="stylesheet"> to load. Inline it or pass it as css.
  • An async resolver. assets must return bytes, not a Promise. Fetch first, then render.
  • Testing the same HTML in a browser. Browsers give body an 8px margin; otisk gives it none, so the page area is exactly the page minus its margins. A layout sized to fill the page area fits here and overflows by 16px in a browser's print preview. Set body { margin: 0 } if both should agree.
  • Forgetting await init(). Every function throws until the module has loaded.

Licence

Apache-2.0 for this package (see LICENSE and NOTICE). The bundle includes third-party components listed in THIRD-PARTY-NOTICES.md.