Skip to content

Repository files navigation

stet

A modern, open-source PostScript and PDF rendering engine written in pure Rust.

Version 0.8.1 License Apache-2.0 OR MIT Rust 1.88+

About

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.

Getting started

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 online

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.

Install

Download a prebuilt binary

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 --version

On macOS, uname -m tells you which to take: arm64aarch64-apple-darwin, x86_64x86_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 --version

SmartScreen 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" }

Install with cargo

cargo install stet-cli

Builds 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-features

Build from source

To work from a checkout, see CONTRIBUTING.md.

Use it as a library

To call stet from your own Rust program rather than the command line, see Library Usage.

CLI 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 summary

See the Viewer Guide for keyboard/mouse controls, zoom presets, minimap navigation, and drag-and-drop.

Options

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, a0a6, 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

Choosing where output goes

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 PDF

The 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.

Page size

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 dimensions

EPS 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.

stet inspect <file.pdf>

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.

Display List Architecture

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.

Features

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: /Info dict + XMP /Metadata stream, 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 /Dests and the modern /Names /Dests name 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 + /Usage hints, hierarchy + alternate configurations + /RBGroups, runtime LayerSet overrides, OCMD /P policies (AllOn / AnyOn / AllOff / AnyOff), /VE boolean expressions (/And / /Or / /Not), and intent-driven rendering (RenderIntent::View / Print / Export) that honours /AS automatic-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

Rendering Correctness

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.

Library Usage

[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 DPI

The 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.

Minimum Supported Rust Version

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.

Crate Overview

                         ┌──────────┐
                         │ 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.

Contributing

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.

Commercial Support

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.

Acknowledgements

  • hayro — PDF renderer by Laurenz Stampfl. stet-pdf-reader uses the project's hayro-jpeg2000, hayro-jbig2, and hayro-ccitt crates 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-path are modified forks (see each crate's README for the specific changes).
  • moxcms — pure-Rust ICC colour management. stet-graphics uses it for CMYK↔sRGB conversion, image bulk transforms, and black-point compensation.

License

Apache-2.0 OR MIT

About

A PDF and PostScript Level 3 interpreter/renderer written in pure Rust.

Topics

Resources

Contributing

Stars

14 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages