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
| Member | What 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, height | current page size in pixels |
error | last error report ('' when clean) — standard PostScript error names |
output | what 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.