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

v2.4.0

Published

Browser raster-preview bindings for the otisk PDF engine: render_page_rgba / page_count for HTML5-canvas previews (wasm-bindgen). Preview only — emits no PDF; use otisk-server for print output.

Readme

otisk-wasm — WASM bindings for otisk

Thin wasm-bindgen wrapper around otisk::Engine, so browser (and other wasm32-unknown-unknown) hosts can drive the otisk pipeline.

Two shapes ship from this crate (Issue 483). A default build exports PDF bytes (render_pdf, below). The published @otisk/preview npm package does not — it is built --no-default-features --features raster-preview and carries the canvas-preview surface only. WASM can't run lcms2, so a browser-emitted PDF would announce PDF/X-4 while carrying the preview's RGB approximation; the print PDF comes from the native path (otisk-server / the otisk CLI). See Size budget for what the split costs, and crates/otisk/tests/issue_483_preview_bundle_surface.rs for the gate.

// Issue 30 baseline + Issue 115 widening: an **optional** 4th argument
// crosses a JS-side `AssetResolver` into the engine. The 3-arg call
// `render_pdf(html, css, margin)` remains byte-identical to the
// Issue 31 parity baseline; passing `null` / `undefined` for the 4th
// arg is equivalent. When present, `assets` must be either a bare
// `function(url): Uint8Array` callable or an object with a synchronous
// `fetch(url): Uint8Array` method — every engine-side `@font-face`,
// `<img>`, `background-image`, `@color-profile` lookup is funneled
// through it.
#[wasm_bindgen]
pub fn render_pdf(
    html: &str,
    css: &str,
    margin_mm: f32,
    assets: Option<JsValue>, // bindgen-side: omittable in JS
) -> Result<Vec<u8>, JsValue>;

JS call sites:

// 3-arg legacy form — unchanged, empty resolver.
const pdf = render_pdf(html, css, 10);

// 4-arg widened form — supply a resolver. `null` / `undefined` are
// equivalent to omitting the arg.
const pdf = render_pdf(html, css, 10, {
    fetch(url) { /* return Uint8Array */ }
});

See src/lib.rs and the issue specs at .plans/pdfx4-engine/issues/30-wasm-bindings-crate.md (baseline) and .plans/pdfx4-engine/issues/115-wasm-asset-resolver-crossing.md (resolver crossing) for the full rationale.

Build (browser bundle)

# Full surface — PDF export included. What the demo UI uses.
wasm-pack build crates/otisk-wasm --target web --out-dir ../../examples/web-ui/pkg --release

# Preview-only — exactly what CI publishes as `@otisk/preview` (Issue 483).
wasm-pack build crates/otisk-wasm --target web --out-dir pkg --release \
    -- --no-default-features --features raster-preview

--no-default-features is what drops pdf-export; a plain --features raster-preview is additive and leaves the PDF exports in.

This produces examples/web-ui/pkg/otisk_wasm_bg.wasm plus the otisk_wasm.js glue module. Serve the demo UI with any static server:

python3 -m http.server 4173 --directory examples/web-ui

The pkg/ output directory is git-ignored.

Build (raw .wasm, no JS glue)

For CI gate parity with the issue spec — no wasm-pack required:

cargo build -p otisk-wasm --target wasm32-unknown-unknown --locked --release

Artefact at target/wasm32-unknown-unknown/release/otisk_wasm.wasm.

Size budget

SPECIFICATION.md §4.2: < 5 MB compressed (target; < 10 MB acceptable). gzip is the number that matters — it's what npm reports and what CDNs serve by default; brotli sits well under it but can't be relied on to be negotiated.

The shipped artefact is the @otisk/preview bundle: wasm-pack build --release -- --no-default-features --features raster-preview (the tiny-skia raster backend + the usvg/resvg/fontdb SVG stack, without the PDF emitter). Measured 2026-08-21 on wasm-pack 0.13 / stable, gzip -9 and brotli -q 11:

| build | raw | gzip | brotli | |---|---|---|---| | default features (bindgen + pdf-export) | 9.50 MB | 4.64 MB | 2.99 MB | | default + raster-preview (what was published before Issue 483) | 9.53 MB | 4.66 MB | 3.01 MB | | --no-default-features --features raster-preview (pre-GH-#33) | 7.36 MB | 3.44 MB | 2.70 MB | | --no-default-features --features raster-preview (with WebP, pre-GH-#62) | 7.42 MB | 3.46 MB | 2.70 MB | | --no-default-features --features raster-preview (published, with WOFF2) | 7.69 MB | 3.57 MB ✅ | 2.79 MB |

The last row is newer than the rest: measured 2026-09-10 on wasm-pack 0.15 / rustc 1.98.1, with the same flags and the same gzip -9 / brotli -q 11. Its base was re-measured on that toolchain too (see the WOFF2 table below), so the delta it reports is like-for-like even though the rows above it were taken on wasm-pack 0.13.

Two things that table says and the previous one could not:

  • The raster backend is nearly free — 20 KB gzip on top of the default build. tiny-skia reuses the geometry the layout already computed.
  • The PDF emitter is not — dropping it takes 2.18 MB raw / 1.22 MB gzip (−26 %) out of the published bundle, which moves @otisk/preview from 4.66 MB to 3.44 MB against the §4.2 budget. Issue 483 was a correctness decision, not a size one, but the size followed it.

WOFF2 is the biggest single feature cost so far (GH #62) — the last row above. Three builds on this recipe, all measured 2026-09-10 on wasm-pack 0.15 / rustc 1.98.1, so the deltas are like-for-like:

| commit | what is in it | gzip | Δ vs base | |---|---|---|---| | eba810a4 | before the decoder | 3 476 780 B | — | | 4f25c9b4 | decoder present, to_sfnt still refuses wOF2 | 3 477 047 B | +267 B | | 36965291 | to_sfnt + FontFace::validate routed at the decoder | 3 573 147 B | +96 367 B / +94.1 KB | | this branch | + the hardening of the adversarial review | 3 576 570 B | +99 790 B / +97.5 KB |

The middle row is the point of the split: an unreachable decoder is dead code the linker drops, so the format work cost nothing until a door called it. Wiring the doors is what pays for brotli plus the glyf / loca / hmtx reconstruction — GH #29 measured +79 KB for brotli-decompressor alone, so the reconstruction itself is ~15 KB on top.

The decoder-side estimate written here before the wiring landed was +94.7 KB; the measured number was +94.1 KB, 606 B under it. The last row is the review hardening — a tighter decompression ceiling and a Cow over the table slices, a checked_add on loca offsets, a bound on the checkSumAdjustment write, a layout window for a transformed glyf's declared length, and four refusals for undefined containers — which costs +3 423 B gzip (+0.10 %), raw 7 690 650 B, brotli 2 786 586 B. The bundle sits at 72 % of SPECIFICATION.md §4.2's 5 MB budget — as GH #29 said when it refused WOFF2 on outline-fidelity grounds, size was never the decider.

PDF-as-image costs +27 KB gzip (GH #79). Reading a source PDF for <img src="logo.pdf"> has to happen on both backends, not just the print one: <img> sizing, object-fit and the aspect ratio are computed in layout, so a PDF the print path can size and the preview path cannot is a layout divergence (Issue 297). Measured 2026-09-13 on wasm-pack 0.15 / rustc 1.98.1, same recipe, one build immediately before the change and one immediately after:

| build | raw | gzip | brotli | |---|---|---|---| | before otisk-pdfsrc | 7 749 614 B | 3 598 096 B | 2 802 748 B | | with otisk-pdfsrc | 7 816 322 B | 3 625 847 B | 2 822 740 B | | Δ | +66 708 B (+0.86 %) | +27 751 B (+0.77 %) | +19 992 B (+0.71 %) |

That leaves the bundle at 72.5 % of SPECIFICATION.md §4.2's 5 MB budget, up from 72.0 %.

The number is small because the reader is small, and the reader is small because it is not lopdf. lopdf 0.36 is already a workspace dependency (behind compressed-objstm), and reaching for it was the obvious move — but it does not build for this target at all:

$ cargo build -p otisk-pdf --features compressed-objstm \
      --target wasm32-unknown-unknown
error: The wasm32-unknown-unknown targets are not supported by default;
       you may need to enable the "wasm_js" configuration flag.
  --> getrandom-0.3.4/src/backends.rs:194:17

lopdfrand 0.9getrandom 0.3, which hard-errors without a JS backend cfg. Enabling that backend would put js-sys-shaped randomness into a renderer whose contract is hermetic determinism, and it would land on the cargo build --workspace --exclude otisk-server --target wasm32-unknown-unknown gate that runs on every commit. So the reader is otisk-pdfsrc: thiserror plus miniz_oxide (already linked for the image tier's ICC recovery), and nothing else. The 27 KB is the parser itself.

WebP costs almost nothing (GH #33). Enabling the image crate's webp codec in otisk-image moved the published bundle by +68 314 B raw / +12 795 B gzip (+0.37 %) / +863 B brotli — 3 444 838 → 3 457 633 B gzip, measured on 2026-09-09 with the same recipe, one build immediately before the change and one immediately after.

The reason it is that cheap is that the decoder was already in the bundle: resvg (pulled by otisk-page's raster-preview feature, for the SVG preview path) depends on image-webp directly, so a nested SVG <image href="*.webp"> already decoded while a top-level HTML <img> did not. Cargo unifies to one copy of image-webp, so what the webp feature adds is the image crate's thin ImageDecoder shim over it, not a codec. At the time it was measured the bundle sat at 69 % of the §4.2 5 MB budget, unchanged to the percentage point; the WOFF2 row above moved that to 71 %.

The size comes from the Cargo release profile, not wasm-optwasm-pack already runs wasm-opt -O, and forcing -Oz moves gzip by < 0.1%. The lever is the workspace [profile.release] (root Cargo.toml): opt-level = "s", lto = true, codegen-units = 1, strip = true. Without it (cargo defaults: no LTO, codegen-units = 16, symbols retained) the raster-preview bundle is ~5.09 MB gzip — over budget. The native otisk binary opts out of these size settings via its own otisk-release profile (see the profile rationale in the root Cargo.toml), so shrinking the bundle never costs native throughput.

Host-target build

The crate is also exposed as an rlib so cargo build -p otisk-wasm on the host triple works (useful for cargo doc, future host-side integration tests, and IDE indexing). The cdylib artefact only matters for the wasm32-unknown-unknown target.

parity_export feature (Issue 31)

The crate ships a second WASM entry point — a raw C-ABI render_pdf_raw — gated behind the parity_export feature. The Phase 2 parity harness (crates/otisk/tests/wasm_parity.rs) builds the artefact with --no-default-features --features parity_export and drives it through wasmtime without the wasm-bindgen JS glue. This surface is internal — the wire format is not a stability contract, and browser hosts should always use the default render_pdf.

cargo build -p otisk-wasm --target wasm32-unknown-unknown \
    --no-default-features --features parity_export --release