PostScript in the browser

The interpreter core compiles to WebAssembly, and web/pscat.js is a dependency-free ES module that renders — and executes — PostScript in a page. The playground is this library and nothing else.

Setup

git clone https://github.com/PSCatLabs/pscat
cd pscat
rustup target add wasm32-unknown-unknown
./scripts/build_wasm.sh          # → web/pscat.wasm (~7.5 MB; mostly the base fonts and lib/)
python3 -m http.server -d web    # open http://localhost:8000

Copy web/pscat.js and web/pscat.wasm side by side into any site — no bundler, no dependency. Pscat.load() fetches the module from next to the JS file by default. From 0.6.2 the same module is also the pscat npm package, with a higher-level, typed API (createRenderer()); see the browser guide.

Run to completion

import { Pscat } from './pscat.js';

const ps = await Pscat.load();
const status = ps.run(source, 612, 792);   // 0 = done, -1 = error
if (status === -1) console.error(ps.error);
ps.paintTo(canvas);                        // putImageData onto a <canvas>

Watch it draw

The wasm exports expose the same step-budget execution the desktop window uses — begin queues the program, step(n) runs up to n objects. Drive it from requestAnimationFrame and the page renders the program drawing itself:

ps.begin(source, 612, 792);
function frame() {
  const more = ps.step(300);        // objects per frame — the speed knob
  ps.paintTo(canvas);
  if (more === 1) requestAnimationFrame(frame);
  else if (more === -1) console.error(ps.error);
}
requestAnimationFrame(frame);

API

MemberWhat it does
Pscat.load(url?)fetch + instantiate (browser)
Pscat.instantiate(bytes)instantiate from bytes in hand (node)
begin(src, w?, h?, scale?)fresh page (default 612×792 points), program queued; src is a string or bytes; scale is pixels per point (dpi / 72, default 1)
step(n?)run ≤ n objects → 1 more work · 0 done · −1 error
run(src, w?, h?, scale?)begin + drive to completion → 0 or −1
addFont(name, bytes)register a TrueType/OpenType file for findfont under a font pack's file stem ('EBGaramond-Regular'; /Garamond then finds it too). Only the Liberation base fonts are built in.
paintTo(canvas)blit the live page (resizes the canvas to match)
pixels()copied RGBA bytes — node-friendly
width, heightcurrent page size in pixels
errorlast error report ('' when clean) — standard PostScript error names
outputwhat the program has printed since begin with =, ==, stack, pstack or print ('' when nothing); readable mid-run, kept to 64 KB

Errors are REPL-style: the machine unwinds but the page and operand stack survive, and the instance stays usable for the next run.

Under the hood

No wasm-bindgen — the export surface (src/wasm.rs) is a hand-rolled C ABI over byte buffers: ps_alloc/ps_dealloc, ps_begin/ps_begin_scaled, ps_step, ps_pixels, ps_error_*, ps_output_*, ps_add_font, ps_run. The JS class is a thin wrapper you could re-implement in any language with a wasm runtime. It works under node the same way — the repo's smoke test instantiates the module in node, renders, and counts pixels through this exact library.

What the wasm build doesn't have: a window, a filesystem (run-ing other files, spool mode), or a clock (usertime/realtime read 0 — the one documented deviation). Everything else — the full language, fonts, filters, images, multi-page — is the same interpreter, compiled from the same sources.