A modern, open-source PostScript and PDF rendering engine written in pure Rust.
stet interprets PostScript (Level 3) and parses PDF files, rendering both to PNG images, PDF documents, or display lists through a unified pipeline. It can also render them in an interactive desktop viewer or a browser-based WASM viewer.
The PostScript interpreter and PDF reader are independent — use either or both — but they produce the same display list type, so every output device and rendering path works with both sources.
Three ways in. Pick the row that matches what you are doing:
| I want to… | Start here | More |
|---|---|---|
| See what it does, right now | Open the browser sampler and drop a PS, EPS or PDF file on the page. Nothing to install, nothing uploaded — it renders client-side. | Try it online |
| Render files on my machine or in a pipeline | Download a prebuilt binary — no Rust toolchain needed — or cargo install stet-cli. |
Install · CLI usage |
| Build rendering into my own program | Add the stet crate to your Cargo.toml. Pure Rust: no C dependencies and no system libraries to install. |
Library usage |
Try it at https://andycappdev.github.io/stet/ — drop a PS, EPS, or PDF file onto the page and stet renders it client-side, no install required.
This is a capability sampler, not a production viewer:
- No system fonts. A browser WASM sandbox can't reach the OS font directories, so font coverage is limited to the 35 URW fonts embedded in the binary plus whatever the source PDF embeds. Documents that expect a specific unembedded font will fall back to a URW substitute.
- Fixed zoom stops (fit, 75, 150, 300, 600 DPI) rather than arbitrary zoom — re-rasterizing at every scroll was too slow in single-threaded WASM to feel responsive.
- Single-threaded WASM. No rayon parallelism. Rendering is ~2-4× slower than native stet.
For production work, use a native binary — see Install.
No Rust toolchain required. Archives for Linux, macOS and Windows are
attached to each release,
alongside a SHA256SUMS file to check them against.
| Platform | Archive | Notes |
|---|---|---|
| Linux — servers, CI, containers | x86_64-unknown-linux-musl |
Statically linked. No glibc version requirement, no runtime libraries, no GUI. Start here on a server. |
| Linux — desktop | x86_64-unknown-linux-gnu |
Includes the interactive viewer. Needs glibc 2.35+ (Debian 12, Ubuntu 22.04 or newer) and X11/Wayland + OpenGL to open a window; rendering to files needs neither. |
| macOS — Apple Silicon | aarch64-apple-darwin |
|
| macOS — Intel | x86_64-apple-darwin |
|
| Windows | x86_64-pc-windows-msvc |
.zip. Statically linked CRT, so no redistributable to install. |
Everything is embedded in the binary — 35 fonts, init scripts, encodings and ICC profiles — so the archive holds the executable and the licences, and there is nothing to install alongside it.
Linux and macOS. Pick the archive from the table above and drop its name in — the rest of the command is the same for all four:
ASSET=stet-0.8.1-x86_64-unknown-linux-musl.tar.gz # see the table above
curl -L "https://github.com/AndyCappDev/stet/releases/download/v0.8.1/$ASSET" | tar xz
cd "${ASSET%.tar.gz}"
./stet --versionOn macOS, uname -m tells you which to take: arm64 → aarch64-apple-darwin,
x86_64 → x86_64-apple-darwin.
Use curl on macOS rather than a browser. The binaries are unsigned, and
Gatekeeper quarantines whatever a browser downloads, so a double-click gives
"cannot be opened because the developer cannot be verified". The quarantine
attribute is set by the downloading application and curl sets none, which
sidesteps it entirely. If you already downloaded through a browser, clear the
flag with xattr -dr com.apple.quarantine stet.
Windows. PowerShell aliases curl to Invoke-WebRequest, which takes
different arguments — so call curl.exe explicitly. Windows 10 1803 and later
ship both it and tar:
curl.exe -L -O https://github.com/AndyCappDev/stet/releases/download/v0.8.1/stet-0.8.1-x86_64-pc-windows-msvc.zip
tar -xf stet-0.8.1-x86_64-pc-windows-msvc.zip
cd stet-0.8.1-x86_64-pc-windows-msvc
.\stet.exe --versionSmartScreen warns that the publisher is unknown on first run; choose More info → Run anyway. Code-signing certificates cost money and stet does not have one.
Verify the download. Each release carries a SHA256SUMS file listing
every archive. The quick commands above stream the archive straight into
tar, so nothing is left on disk to check against it — to verify, save the
file first:
ASSET=stet-0.8.1-x86_64-unknown-linux-musl.tar.gz # see the table above
URL=https://github.com/AndyCappDev/stet/releases/download/v0.8.1/
curl -L -O "$URL$ASSET"
curl -L -O "${URL}SHA256SUMS"
grep "$ASSET" SHA256SUMS | sha256sum -c - # prints "<file>: OK"
tar xzf "$ASSET"macOS ships no sha256sum; substitute shasum -a 256 -c - in that line.
On Windows, PowerShell has no sha256sum either, and Get-FileHash returns
an upper-case digest while SHA256SUMS is lower-case, so compare them
case-folded:
$name = "stet-0.8.1-x86_64-pc-windows-msvc.zip"
curl.exe -L -O https://github.com/AndyCappDev/stet/releases/download/v0.8.1/SHA256SUMS
$want = ((Select-String -Path SHA256SUMS -SimpleMatch $name).Line -split '\s+')[0]
$got = (Get-FileHash $name -Algorithm SHA256).Hash.ToLower()
if ($got -eq $want) { "OK" } else { "MISMATCH" }cargo install stet-cliBuilds the stet binary with the interactive viewer included. Needs a Rust
toolchain (1.88+), and compiles the workspace including the GUI stack, so it
takes appreciably longer than downloading a binary.
For a headless build with no viewer and no GUI dependencies:
cargo install stet-cli --no-default-featuresTo work from a checkout, see CONTRIBUTING.md.
To call stet from your own Rust program rather than the command line, see Library Usage.
stet auto-detects the input format — PostScript, EPS, or PDF — so the same commands work against any of them.
stet --device png document.ps # PostScript → PNG
stet --device pdf document.ps # PostScript → PDF
stet --device png document.pdf # PDF → PNG
stet document.ps # Interactive viewer
stet # REPL (viewer opens on first showpage)
stet inspect document.pdf # Print PDF structural summarySee the Viewer Guide for keyboard/mouse controls, zoom presets, minimap navigation, and drag-and-drop.
| Option | Description |
|---|---|
--device <TYPE> |
Output: png, pdf, viewer (default), null |
-o, --output <PATH> |
Write output to PATH instead of alongside the input. A %d token becomes the page number (%03d zero-pads); without one, PATH is a single file and a second page is an error. One input file at a time. See Choosing where output goes. |
--dpi <DPI> |
Resolution (overrides device default; all built-in devices default to 300) |
--pages <RANGE> |
Page filter: 1, 1-5, 2,4,6 |
--page <SIZE> |
Page size for PostScript/EPS input: a named size (letter, legal, tabloid, ledger, executive, a0–a6, b4, b5) or WIDTHxHEIGHT in points, e.g. 620x1000. Add -landscape / -portrait to orient a named size. See Page size. |
--width <PX> / --height <PX> |
Scale PDF output to a pixel size (PDF input only; not combinable with --dpi or --page) |
--threads <N> |
Worker-thread count (default: 75 % of cores in viewer mode, 8 otherwise) |
--no-icc |
Disable ICC color management entirely |
--no-aa |
Disable anti-aliasing |
--output-profile <FILE> |
Generic ICC output profile (also used as source CMYK when --cmyk-profile is absent) |
--cmyk-profile <FILE> |
Pin the source CMYK ICC profile for CMYK→sRGB conversion |
--use-output-intent |
Honour the PDF's embedded OutputIntent as the source CMYK profile (default) |
--no-output-intent |
Ignore the PDF's embedded OutputIntent and use the system CMYK profile |
--bpc <on|off|auto> |
Black-point compensation (default: auto, currently equivalent to on) |
--password <PW> |
Password for encrypted PDF input |
--timeout <SECONDS> |
Abort a job running longer than this. No limit by default — PostScript is Turing-complete and legitimate jobs run for minutes. Set one for untrusted input |
--max-vm <MB> |
Ceiling on PostScript VM — strings, arrays, dictionaries (default 8192). Exceeding it raises VMerror instead of aborting. Separate from the renderer's image and band buffers, so it does not cap rendering resolution |
By default stet writes next to the input: doc.pdf becomes doc.png, or
doc-001.png, doc-002.png, … when it has several pages. -o / --output
puts the result somewhere you choose, which is what a shell-out pipeline
built around gs -sOutputFile= expects:
stet --device png -o /tmp/preview.png --pages 1 doc.pdf # exactly this path
stet --device png -o '/tmp/p-%03d.png' doc.pdf # p-001.png, p-002.png, …
stet --device pdf -o out.pdf in.ps # PostScript → chosen PDFThe path is a template, and the %d in it — not the number of pages — is
what decides the naming. %d is replaced by the page number and %0Nd
zero-pads it to N digits, so p-%03d.png yields p-001.png. A template
with no token names one file: the single-page case most people want, written
to exactly the path given, with no extension mangling.
Deciding from the template rather than the page count is what lets
PostScript and PDF behave identically here. A PostScript page count is not
knowable in advance — pages appear as showpage runs — so page 1's name has
to be committed before anyone knows whether a page 2 is coming. Ghostscript
resolves that by opening the literal path once and streaming every page into
it, which silently leaves several concatenated images in one file (measured
against gs 10.05.1: -sOutputFile=out.png on a three-page job gives one
out.png holding three PNGs, exit 0, no warning; most viewers stop at the
first and the other two are invisible). stet accepts the same templates but
stops with an error on the second page instead, naming a %03d form to
use, and leaves page 1 on disk. For PDF input the page count is known, so
the same mistake is caught before anything is rendered.
-o takes one input file — with several, which output each belongs to is
ambiguous — and does not apply to --device viewer or --device null,
which write no file. --device pdf collects every page into a single PDF, so
a %d token there is rejected rather than silently ignored. Writing to
stdout (-o -) is not supported yet.
A PostScript program is rendered onto whatever page the device provides,
which defaults to US Letter. %%BoundingBox does not change that for a
plain %!PS document — DSC defines it as a description of the artwork's
extent, not a page-size request — so a program drawing outside Letter is
clipped unless it calls setpagedevice itself. Ghostscript behaves the same
way. --page is how you supply the size from outside:
stet --device png --page 620x1000 broadside.ps # explicit, in points
stet --device png --page a4 report.ps # named size
stet --device png --page a4-landscape report.ps # swap the dimensionsEPS is the exception: an EPSF header line or a .eps extension makes stet
honour %%BoundingBox automatically, and --page overrides it when both
apply. For PDF input the page size comes from the document, so --page is
rejected there — use --width / --height to scale the output instead.
Prints a human-readable summary of the document structure: metadata, page count and dimensions, outline (bookmark) tree, named destinations, per-page annotation counts by subtype, AcroForm field summary, embedded file attachments, and any parse warnings. Read-only; never writes to the file. See the PDF Reader API guide for the underlying library API.
$ stet inspect document.pdf
document.pdf
Metadata:
Title: Annual Report 2026
Author: Scott Bowman
Producer: stet 0.8.1
Created: 2026-04-27 12:00:00 UTC
Pages: 4
Page 1 size: 612.0 × 792.0 pt (8.50 × 11.00 in)
Outline (3 entries):
- Chapter 1 → page 1 (fit)
- Section 1.1 → page 2 (xyz)
- Chapter 2 → page 3 (fit)
Annotations: 3
Page 1: 2 Link
Page 3: 1 Highlight
Form: 4 terminal fields (4 widgets)
By kind: Button: 1, Text: 3
Pass --password <pw> for encrypted documents.
Unlike rendering engines that interpret and rasterize in a single pass, stet decouples the two: interpreters produce an intermediate display list, and rendering is a separate step that consumes it.
The display list is a public, iterable Rust data structure — a flat sequence of painting operations (fills, strokes, images, shadings, text, groups, clips) that you can walk directly. Emit SVG, extract structured text or metadata, diff two documents, transform the list before rendering, feed it to an analysis or ML pipeline, or build a custom renderer for a non-standard target. Few rendering engines expose this layer; stet treats it as the interchange format between parser and consumer — use it for anything you want.
This decoupling also enables viewport rendering at arbitrary zoom without re-interpretation, pipelined multi-page rendering, trivial cancellation between render bands, multiple output formats from a single interpretation pass, and display list caching for repeated renders at different resolutions. See the Architecture Guide and Display List Reference for details.
PDF Reader
- PDF 1.0–2.0 parsing with cross-reference tables and streams
- Encryption: RC4, AES-128, AES-256
- Filters: Flate, LZW, ASCII85, ASCIIHex, RunLength, DCT, JPXDecode (JPEG 2000), CCITTFax, JBIG2
- All PDF color spaces including ICCBased, Separation, DeviceN, Indexed
- Transparency groups (isolated, knockout), soft masks, blend modes
- Font rendering: Type 1, TrueType, CFF, CID with CMap/encoding support
- Annotations (form fields, stamps)
- No dependency on the PostScript interpreter — usable standalone
PDF Structural API (read-only, on top of the reader)
- Document metadata:
/Infodict + XMP/Metadatastream, with PDFDocEncoding / UTF-16BE / UTF-8-BOM string decoding and PDF-date parsing - Outline (bookmarks) tree with cycle protection and 64-level depth cap
- Typed [
Destination] and [Action] (URI, GoTo, GoToR, Named, JavaScript, SubmitForm, …) - Named-destination table merging legacy
/Destsand the modern/Names /Destsname tree - Per-page annotations as structured data — Link, Text, Highlight/Underline/Squiggly/StrikeOut, FreeText, Line, Square/Circle, Polygon/PolyLine, Ink, Stamp, Caret, FileAttachment, Popup
- AcroForm field tree with Button/Text/Choice/Signature kinds, Ff-bit decoding, dotted-path field names, widget cross-references back to annotations
- All 5 PDF page boxes (MediaBox, CropBox, BleedBox, TrimBox, ArtBox) plus rotation, user unit, presentation hints
- Embedded files (file attachments) with on-demand byte access and AfRelationship hints
- Full Optional Content (layer) model: per-layer metadata +
/Usagehints, hierarchy + alternate configurations +/RBGroups, runtimeLayerSetoverrides, OCMD/Ppolicies (AllOn/AnyOn/AllOff/AnyOff),/VEboolean expressions (/And//Or//Not), and intent-driven rendering (RenderIntent::View/Print/Export) that honours/ASautomatic-state rules - Parse warnings (
ParseWarning,ParsePhase,Severity) for cycles, dropped entries, and structural truncations
PostScript Interpreter
- Full PostScript Level 3 — 388 operators in
systemdict - Type 1, CFF/Type 2, TrueType, CID, and Type 3 font rendering
- All 7 shading types (axial, radial, Gouraud mesh, Coons/tensor patch)
- CIE color spaces (CIEBasedABC, CIEBasedA, CIEBasedDEF, CIEBasedDEFG)
- ICC color management with system CMYK profile auto-detection
- Filters: ASCII85, ASCIIHex, Flate, LZW, RunLength, DCT (JPEG), CCITTFax, JBIG2, JPX (JPEG 2000), eexec, SubFile
- PDF-imaging extensions: constant alpha + 16 blend modes, transparency groups, soft masks, optional-content layers — all driven from PostScript with stet-specific operators (reference)
- Resource system with embedded fonts (35 URW equivalents of the standard PS fonts)
- Interactive REPL with
executive
Rendering & Output
- RGBA rasterization via
stet-tiny-skia(banded, multi-threaded) - PDF output with native CMYK, spot colors, ICC profiles, transfer functions, halftone screens, overprint, and font embedding
- PNG file output
- Viewport rendering: render any region at any zoom from a stored display list
- Interactive desktop viewer (egui) with zoom, pan, minimap, drag-and-drop
- WASM viewer for browser-based rendering
- Display list as a public API for building custom output devices
Print Production
- Native CMYK color preservation (no lossy RGB round-trip)
- Separation and DeviceN (spot color) support with tint transforms
- Overprint and overprint mode (OPM) for both rasterizer simulation and PDF output
- Transfer functions, halftone screens, black generation, and undercolor removal carried per display element
- Rendering intent preservation
- Trim box support
Two issues that plague most PDF and PostScript renderers are handled correctly here:
- No seams on shared clip edges. Anti-aliased clip masks produce visible seams where adjacent clipped regions meet — the background bleeds through the softened edge. stet uses binary clip coverage instead, so AGM (Adobe Graphics Manager) EPS exports and other heavily-clipped artwork render without artifacts.
- Overprint simulation. Overprint is a print-workflow feature where the painter combines with the canvas below instead of replacing it. Correct simulation requires honest CMYK blend math, knockout group handling, and spot channel preservation — complexity that most on-screen renderers skip for historical reasons, treating overprint as a print-only concern. The result is that documents relying on it render visibly wrong everywhere: Firefox/pdf.js, Okular/poppler, Chromium/pdfium, and most others all show incorrect colours on PDF/X-4 files and the Ghent Workgroup (GWG) conformance test suite. stet simulates overprint rather than ignoring it, and support is actively being hardened: the common cases and most GWG conformance files render correctly today, but it is not yet bug-for-bug Acrobat parity on every edge case.
If you're doing prepress, proofing, or any color-separated output, these matter more than raw rendering speed.
[dependencies]
stet = "0.8"let mut interp = stet::Interpreter::new();
let pages = interp.render(include_bytes!("document.ps"), 300.0)?;
// pages[0].rgba — RGBA pixel data (4 bytes/pixel, row-major)
// pages[0].width — pixel width at 300 DPIThe stet crate embeds all required resources (35 fonts, init scripts,
encodings, ICC color profiles) so there are no external files to ship.
Output formats, diagnostics, viewport rendering, the PDF reader, custom output devices, feature flags, and the pre-1.0 upgrade notes are in docs/LIBRARY-USAGE.md.
stet requires Rust 1.88 or newer. This is verified on every push by a CI job that compiles the workspace on exactly that toolchain, so the number above is a tested fact rather than an estimate.
The MSRV is not covered by semantic versioning: it may be raised in any minor release. Any change to it is called out in CHANGELOG.md.
┌──────────┐
│ stet-cli │ Binary: file I/O, REPL, arg parsing
└────┬─────┘
│
┌───────────────┼───────────────┐
v v v
┌──────────┐ ┌───────────┐ ┌─────────────┐
│ stet │ │stet-viewer│ │stet-pdf- │
│ (facade) │ │ (egui) │ │ reader │
└────┬─────┘ └─────┬─────┘ └──────┬──────┘
│ │ │
┌───────┼───────┐ │ │
v v v v │
┌────────┐┌──────┐┌───────────┐ │
│stet-pdf││stet- ││stet-render│ │
│(output)││engine││(tiny-skia)│ │
└───┬────┘└──┬───┘└─────┬─────┘ │
│ v │ │
│ ┌─────────┐ │ │
│ │stet-ops │ │ │
│ └────┬────┘ │ │
│ v │ │
├──►┌─────────┐◄────┘ │
│ │stet-core│ │
│ └────┬────┘ │
│ v │
│ ┌────────────┐◄───────────────────────┘
└──►│stet- │
│ graphics │
└─────┬──────┘
v
┌───────────┐
│stet-fonts │
└───────────┘
| Crate | Role |
|---|---|
stet |
Batteries-included library API (facade) |
stet-core |
Interpreter infrastructure: types, VM, tokenizer |
stet-ops |
PostScript operator implementations (388 registered in systemdict) |
stet-engine |
Execution engine (eval loop) |
stet-fonts |
Font parsing: Type 1, CFF/Type 2, TrueType |
stet-graphics |
Display list, color types, ICC color management |
stet-render |
Rasterization backend, PNG output. Depends on stet-core only for the default ps-device feature; without it a consumer rasterizes display lists with no PostScript VM linked |
stet-tiny-skia |
Modified tiny-skia fork with stet-specific optimizations (BSD-3-Clause) |
stet-tiny-skia-path |
Companion path/stroker crate for stet-tiny-skia (BSD-3-Clause) |
stet-pdf |
PDF output device (PS → PDF) |
stet-pdf-reader |
PDF input parser (PDF → display lists) |
stet-viewer |
Interactive egui/winit desktop viewer |
stet-cli |
Command-line interface and REPL |
stet-wasm |
WebAssembly bindings for browser rendering |
See the Architecture Guide for a detailed explanation of how these crates work together.
cargo test runs with no extra setup. Building from a checkout, the git
hooks, tag naming, the WASM viewer build, and the PDF visual-regression
corpus are in CONTRIBUTING.md.
stet is built and maintained by Scott Bowman and is free under Apache-2.0 OR MIT with no obligations. If your organization runs it in production, commercial support and consulting are available — priority fixes, integration help, and format-coverage work, including guarantees around the specific PostScript, EPS/AI, or PDF edge cases your pipeline depends on. Reach out at scott@bowmans.org.
- hayro — PDF renderer by
Laurenz Stampfl.
stet-pdf-readeruses the project'shayro-jpeg2000,hayro-jbig2, andhayro-ccittcrates for JPEG 2000, JBIG2, and CCITT-Fax stream decoding — the PDF filters that have no other pure-Rust implementation. Big thanks to the hayro project for factoring these out as reusable crates. - tiny-skia by
Yevhenii Reizner — a Skia subset
ported to Rust.
stet-tiny-skia/stet-tiny-skia-pathare modified forks (see each crate's README for the specific changes). - moxcms — pure-Rust ICC colour
management.
stet-graphicsuses it for CMYK↔sRGB conversion, image bulk transforms, and black-point compensation.
Apache-2.0 OR MIT