Skip to content

Releases: AndyCappDev/stet

stet 0.8.1

Choose a tag to compare

@github-actions github-actions released this 31 Aug 16:46

A malformed-font crash fix. Font data arrives embedded in a PDF or a
PostScript program, so it is attacker-controlled in the same way any PDF
object is, and the affected code shipped in every release to date.

Fixed

  • A malformed CFF font no longer panics the process. The Private DICT
    operator carries [size, offset], and both were bounds-checked with
    offset + size <= data.len(). CFF permits a real-number operand wherever an
    integer is expected, and f64 as usize saturates rather than wrapping, so a
    font declaring a size of 1e49 reached that check as usize::MAX and
    overflowed the add before it could reject anything. Shipped release builds
    are affected
    , not only overflow-checked ones: with checks off the add wraps
    to just below the offset, the <= data.len() bound then passes, and the
    slice panics instead. Three sibling sites had the same shape: the per-FD
    Private DICT of a CID-keyed font, and both local-Subr offsets, which are
    added to their Private DICT offset. DICT operands destined for an offset or
    length are now rejected up front unless they are finite, non-negative, and
    inside what a CFF offset can address.

    Font data is attacker-controlled in the same way any PDF object is — it
    arrives embedded in a PDF or in a PostScript program — so this was reachable
    from an untrusted input. Found by the weekly fuzz job.

Changed

  • Crate descriptions and keywords across all eleven published crates now name
    what distinguishes stet — pure Rust, no C dependencies, prepress-grade CMYK
    and spot colour — rather than restating the crate name. stet-pdf-reader
    was previously described as "PDF parser and renderer", five words that fit
    every PDF crate on the registry.
  • The stet facade no longer describes itself as a "PDF rendering engine". It
    depends on stet-render and stet-pdf (PDF output) and carries
    stet-pdf-reader as a dev-dependency only, so it cannot read a PDF; the old
    wording promised the one thing the crate does not do. PDF reading is
    stet-pdf-reader.

Downloads

stet-<version>-x86_64-unknown-linux-musl.tar.gz is the one most people
want: statically linked, no glibc requirement, no runtime libraries, no GUI.
It is the right choice for servers, CI and containers.

The -gnu Linux build adds the desktop viewer and needs glibc 2.35 or newer
(Debian 12, Ubuntu 22.04 and later) plus X11 or Wayland and OpenGL to open a
window. Rendering to files works without them.

macOS: the binaries are unsigned, so opening a downloaded archive from
Finder trips Gatekeeper ("cannot be opened because the developer cannot be
verified"). Installing with curl avoids it entirely, because the quarantine
attribute is set by the downloading application and curl does not set one:

curl -L <asset-url> | tar xz

If you already downloaded it in a browser, clear the flag with
xattr -dr com.apple.quarantine stet.

Windows: SmartScreen will warn that the publisher is unknown; choose
More info -> Run anyway. Code signing certificates cost money and stet does
not have one.

Rendering is identical across these artifacts with one caveat: the musl
build's libm differs from glibc's in the last bits, which can shift
antialiasing coverage by a pixel or two on heavily curved PostScript
artwork (measured: 265 of 5.4M pixels on one sample, invisible in practice;
all PDF samples tested were byte-identical). Do not generate rendering
baselines with one build and compare them against another.

Verify a download against SHA256SUMS.

stet 0.8.0

Choose a tag to compare

@github-actions github-actions released this 30 Aug 19:06

The release you can download. Every release before this one shipped source
only, so trying stet meant installing a Rust toolchain and compiling a
workspace including a GUI stack — a barrier for exactly the people most
likely to want it, who are replacing a Ghostscript call in a pipeline. This
one attaches prebuilt binaries for Linux, macOS and Windows, and adds the
-o flag such a pipeline needs to say where output goes.

Added

  • Prebuilt binaries on every release. Linux (static musl and glibc+viewer),
    macOS (Apple Silicon and Intel) and Windows, with a SHA256SUMS file.

    The static musl build is the one most people want: no glibc version
    requirement, no runtime libraries, no GUI, and nothing to install alongside
    it — all 56 resources are embedded in the binary. The glibc build adds the
    interactive viewer and needs glibc 2.35 or newer.

    The binaries are unsigned, and the release notes say so rather than letting
    you find out: macOS Gatekeeper quarantines browser downloads (install with
    curl instead), and Windows SmartScreen warns about an unknown publisher.

  • -o / --output for the CLI. Output no longer has to land next to the
    input. The path is a template: a %d in it is replaced by the page number
    and %0Nd zero-pads (p-%03d.png gives p-001.png), while a path without
    a token names a single file, written exactly as given with no extension
    mangling. This is the shape gs -sOutputFile= uses, so an existing
    shell-out pipeline can point at stet without restructuring.

    Naming is decided by the template rather than by the page count, which is
    what makes PostScript and PDF input behave identically — a PostScript page
    count is not knowable in advance, since pages appear as showpage runs.
    Where Ghostscript resolves that by opening the literal path once and
    streaming every page into it (leaving several concatenated images in one
    file, exit 0, no warning), stet stops with an error on the second page,
    names a %03d form to use, and leaves page 1 intact. For PDF input the page
    count is known up front, so the same mistake is refused before anything is
    rendered.

    -o takes one input file, and is rejected for --device viewer and
    --device null, which write no file. --device pdf collects every page
    into one file, so a %d token there is an error rather than being ignored.
    Writing to stdout (-o -) is not implemented and says so.

    Default naming without -o is unchanged: in-0001.png for PostScript,
    doc.png / doc-001.png for PDF.

Fixed

  • stet-cli did not build with --no-default-features. The viewer
    feature was not genuinely optional: render_dropped_pdf names viewer
    channel types in its signature but carried no #[cfg], so a headless build
    failed to compile even though both of its callers sit inside the
    viewer-gated run_viewer_mode. This is the configuration a server, CI or
    container install wants, and the one a static musl binary requires.

  • A headless build's --help advertised a viewer it does not have. It
    listed --device viewer — which exits with "viewer not available" — and
    claimed a bare stet launches the viewer, when it starts the REPL. Both
    lines are now conditional on the feature.

  • A job that requested a non-zero exit status was reported as having
    completed.
    .quitwithcode 1 and the new --output failure both end the
    job through quit, which printed "completed (quit)" regardless of the code
    requested. A non-zero code now prints "FAILED". The process exit status was
    already correct.

Downloads

stet-<version>-x86_64-unknown-linux-musl.tar.gz is the one most people
want: statically linked, no glibc requirement, no runtime libraries, no GUI.
It is the right choice for servers, CI and containers.

The -gnu Linux build adds the desktop viewer and needs glibc 2.35 or newer
(Debian 12, Ubuntu 22.04 and later) plus X11 or Wayland and OpenGL to open a
window. Rendering to files works without them.

macOS: the binaries are unsigned, so opening a downloaded archive from
Finder trips Gatekeeper ("cannot be opened because the developer cannot be
verified"). Installing with curl avoids it entirely, because the quarantine
attribute is set by the downloading application and curl does not set one:

curl -L <asset-url> | tar xz

If you already downloaded it in a browser, clear the flag with
xattr -dr com.apple.quarantine stet.

Windows: SmartScreen will warn that the publisher is unknown; choose
More info -> Run anyway. Code signing certificates cost money and stet does
not have one.

Rendering is identical across these artifacts with one caveat: the musl
build's libm differs from glibc's in the last bits, which can shift
antialiasing coverage by a pixel or two on heavily curved PostScript
artwork (measured: 265 of 5.4M pixels on one sample, invisible in practice;
all PDF samples tested were byte-identical). Do not generate rendering
baselines with one build and compare them against another.

Verify a download against SHA256SUMS.

stet 0.7.0 — PDF-only builds drop the PostScript VM

Choose a tag to compare

@AndyCappDev AndyCappDev released this 30 Aug 06:08

A dependency-hygiene release. A PDF-only consumer no longer compiles the
PostScript interpreter, a mesh-shading memory fault that scaled with core
count is fixed, and --device null works for the scripting use it is
documented for.

Breaking

One change, affecting only consumers who already opt out of stet-render's
default features. This is why the bump is minor rather than patch.

  • stet-render with default-features = false no longer provides
    SkiaDevice or its OutputDevice implementation.
    They now sit behind the
    new default-on ps-device feature. At 0.6.0, opting out of defaults dropped
    only parallel; it now drops the device as well, so a consumer who declared
    stet-render = { version = "0.6", default-features = false } to get a
    single-threaded build will fail to compile against 0.7. Add the features you
    want by name:

    stet-render = { version = "0.7", default-features = false, features = ["ps-device"] }

    Nothing changes for consumers on default features, which is the overwhelming
    majority: default = ["parallel", "ps-device"].

Changed

  • stet-pdf-reader no longer links the PostScript interpreter. Its
    default features pulled stet-render, which depended unconditionally on
    stet-core for a single trait, so a PDF-only consumer compiled the whole
    PostScript VM — contradicting the README's "PDF-only users don't pay for the
    VM". stet-render now gates SkiaDevice and its OutputDevice
    implementation behind a new default-on ps-device feature, and the reader
    takes the crate without it. stet-pdf-reader's dependency closure is now
    stet-fonts, stet-graphics, stet-render and the two tiny-skia forks,
    with PDF → RGBA rendering and parallelism unchanged.

    Consumers on default features are unaffected; see Breaking above for the
    one case that is not.

Fixed

  • --device null aborted PostScript programs that query the page device.
    The flag is documented for "test / scripting use", but it installed the PLRM
    nulldevice operator rather than a page device. That resets the CTM to
    identity and leaves no page-device parameters, so currentpagedevice /OutputDevice get raised undefined and initmatrix had no device matrix
    to restore — stet's own PostScript test suite aborted partway through under
    the flag meant for running it. --device null now installs a real page
    device (a new OutputDevice/null.ps resource) that reports itself as
    /null, carries a page size and resolution like any other device, and still
    produces no output: nothing is rasterized and /EndPage never transmits a
    page. The suite now passes identically on png, pdf and null, and CI
    runs it on null as well so this cannot regress unnoticed.

  • Mesh-shaded PDFs could exhaust memory and fail to render, worse the more
    cores the machine had.
    render_patch_shading triangulated every patch of
    a shading regardless of which band was being drawn, and bands render
    concurrently — one per thread — so the same triangle list was built once per
    core. Cost scaled with core count rather than with the file. A prepress PDF
    with a page-spanning Coons mesh at 300 dpi peaked at 2.1 GB on one thread and
    31.4 GB on sixteen, and on a 24-core machine with less than ~50 GB of RAM it
    did not render at all. Patches and triangles that cannot reach the band being
    drawn are now skipped before they are built. The same file now renders in
    0.94 s at 2.2 GB on 24 threads, and CPU time at 16 threads fell from 99.1 s
    to 5.8 s — the surplus was duplicated work, so throughput improves alongside
    memory. Rendered output is byte-identical.

stet 0.6.0 — hardening sweep, resource governor, decompression ceiling

Choose a tag to compare

@AndyCappDev AndyCappDev released this 27 Aug 06:36

A hardening release. The public Rust API is strictly additive — nothing was
removed and no signature changed — but the interpreter and the PDF reader now
refuse a number of inputs they previously accepted, which is why this is a
minor bump rather than a patch.

Breaking

Nothing here breaks compilation. Every item changes what happens to a file,
so audit these if you render input you do not control the shape of. All were
previously ways to abort the process, produce silently wrong output, or run
without bound; each is documented in full under Security below.

  • Numeric overflow now raises undefinedresult. 1e308 1e308 mul
    returned inf and inf 0 mul then returned NaN; both now error, per PLRM
    and matching Ghostscript at its own boundary. A literal 1e999 no longer
    scans as inf — it declines to be a number and becomes an undefined name.
    A program that relied on either value will now stop.
  • Non-finite path coordinates raise undefinedresult. Reachable through a
    CTM composed past the representable range. Previously drew arbitrary output.
  • VMerror now halts execution. errordict registered the handler under
    a name that could never match, so the interpreter printed the error and
    carried on past the failed allocation. Programs that appeared to survive an
    allocation failure will now stop at it.
  • PostScript VM is capped at 8 GiB by default (--max-vm,
    setuserparams /MaxLocalVM). Jobs above it raise VMerror instead of
    growing until the OS intervenes. Separate from the renderer's image and band
    buffers, so this does not cap rendering resolution.
  • Page size is bounded at 14400 pt (200 in) for PostScript input.
    Resolution is deliberately not capped — 1200 dpi at 11x17 and larger is
    ordinary prepress.
  • Image dimensions are bounded to 100,000 per side and 4e9 pixels total,
    on both the PostScript and PDF paths, with /BitsPerComponent limited to
    1..=16. Sized for prepress, not for the sample corpus: a 60x40 inch page at
    1200 dpi is 3.46 Gpx and is accepted.
  • Decompressed streams are bounded at 512 MiB, raised to whatever an image
    raster or an embedded file declares for itself. A chain of decompression
    filters no longer multiplies without limit.
  • i64::MIN -1 idiv (and mod) raise undefinedresult instead of
    panicking in release.

All 691 sample PDFs render byte-identically across every change above, the
6268-file PostScript corpus has the same 31 failures with an identical failing
set, and both visual suites pass.

Fixed

  • stet-core failed to compile for wasm32-unknown-unknown. The 8 GiB VM
    default is not a large usize on a 32-bit target but a const-evaluation
    error. The default is now computed in u64, falling back to usize::MAX / 4
    where 8 GiB does not fit.
  • currentuserparams reported MaxLocalVM as 0, telling a program there
    was no limit moments before it hit one.

Added

  • --timeout <SECONDS> and --max-vm <MB> CLI options.
  • Context::set_timeout, Context::check_deadline, Context::check_vm_alloc,
    Context::vm_bytes.
  • stet_graphics::image_limitsMAX_IMAGE_DIMENSION, MAX_IMAGE_PIXELS,
    MAX_BITS_PER_COMPONENT, and validators, shared by the PostScript and PDF
    paths so two prepress-calibrated numbers cannot drift apart.
  • stet_pdf_reader::filters::{DecodeBudget, decode_stream_bounded, MAX_DECODED_STREAM_BYTES}. decode_stream is unchanged.
  • PdfError::NestingTooDeep. PdfError is #[non_exhaustive], so this is
    not a breaking change.
  • stet-pdf-reader's parse_object_at_depth,
    parse_object_from_token_at_depth, parse_dict_body_at_depth, and
    MAX_OBJECT_DEPTH. The existing depth-0 entry points are unchanged.
  • scripts/check-cli-docs.sh, wired into CI and .githooks/pre-push: every
    CLI option must appear in --help and in both READMEs. The crates.io page
    had been listing ten of nineteen options.
  • crates/stet-cli/examples/profile_images.rs and profile_alloc.rs
    per-stage memory attribution for the render path.
  • Five cargo-fuzz targets in fuzz/, with seeded corpora and a CI smoke gate.
  • [profile.hardened] — release codegen with overflow checks left on.

Security

Five unbounded-recursion vectors in the PDF reader let a small crafted file
abort the process with a native stack overflow. A stack overflow is not a
panic, so none of these could be contained by catch_unwind — any program
rendering untrusted PDFs was exposed to an uncatchable denial of service.
This is the same vulnerability class as RUSTSEC-2026-0187 in lopdf.

Three were depth-based, and are now capped:

  • Nested arrays and dictionaries in an object body (lexer.rs).
    parse_object_from_token and parse_dict_body are mutually recursive with
    no bound, so [[[[… or <</A<</A… in any object exhausted the stack. Both
    now thread a depth counter and stop at MAX_OBJECT_DEPTH (256), returning
    the new PdfError::NestingTooDeep. The existing parse_object,
    parse_object_from_token, and parse_dict_body signatures are unchanged
    and enter at depth 0; parse_object_at_depth,
    parse_object_from_token_at_depth, and parse_dict_body_at_depth are new.
  • Nested arrays in a content stream (content/mod.rs). Content-stream
    operands go through a separate parser, parse_inline_array, which needed
    its own cap; it shares MAX_OBJECT_DEPTH.
  • Nested procedures in a Type 4 (PostScript calculator) function
    (resources/function.rs). parse_token_sequence recurses once per {
    body; now capped at MAX_CALC_DEPTH (64).

Two were cycle-based, which no depth cap alone can fix — the recursion is
infinite, so file size is irrelevant (both reproduce in under 1 KB):

  • A Type 3 stitching function that reaches itself through /Functions
    (resources/function.rs), directly or through a ring of siblings.
    PdfFunction::parse now carries a set of the object numbers on the current
    path and raises PdfError::CircularReference on re-entry. It is a path set,
    not a seen-set — entries are popped on the way out, so the legitimate shape
    /Functions [7 0 R 7 0 R] still parses and renders.
  • A Type 3 CharProc that shows its own glyph (content/mod.rs), directly
    or through a pair of fonts naming each other. This path incremented the
    interpreter's depth field but never tested it: the only check lived in
    handle_form_xobject. Type 3 glyphs and soft-mask groups — which likewise
    re-enter interpret_stream without passing through the Form XObject path —
    now check it too. The bound, MAX_CONTENT_NESTING, is 20, the value the
    Form XObject and pattern guards already used, so nothing that renders today
    changes.

Added PdfError::NestingTooDeep. PdfError is #[non_exhaustive], so this
is not a breaking change.

Separately, image dictionary integers are now validated before use.

  • /Width and /Height were cast with as u32 and then multiplied in
    u32.
    The product overflowed: 65537 x 65536 is 2^32 + 65536, so
    width * height came back as 65536 and the buffer allocated from it was far
    smaller than the loops that filled it — an "attempt to multiply with
    overflow" panic in debug builds, a silently undersized allocation in
    release. The truncating cast was wrong on its own too: /Width 4294967297
    became a 1-pixel image rather than an error.
  • The loop counts alone were a denial of service. Even where the
    arithmetic survived, an 800-byte file declaring a 65537 x 65536 image spent
    9-19 seconds in release. It now completes in 0.1 s.
  • Both are fixed by validating at the four points where an image dictionary is
    read (image XObject, inline image, /SMask, /Mask): dimensions must be
    positive and at most MAX_IMAGE_DIMENSION (100,000), and their product at
    most MAX_IMAGE_PIXELS (4,000,000,000). The ceiling is sized for prepress
    rather than for the sample corpus: a 40x28 inch press sheet at 600 dpi is
    403M pixels, an A0 poster at 600 dpi 558M, and 60x40 inch grand format at
    1200 dpi 3.46G, all of which a RIP must accept. It stays under 2^32 because
    a dozen sites compute width * height in u32; anything multiplying
    further by a component count uses saturating usize.
  • /BitsPerComponent is validated too, to 1..=16. It reaches
    1u32 << bpc in expand_bits_to_bytes, which panics in debug builds at 32
    or more. That function now also reserves its three-way
    width * height * components product in usize, which overflows a u32
    sooner than the two-way one does.

Filter and font parameters are now validated the same way.

  • /Columns, /Colors, and /BitsPerComponent in /DecodeParms were
    cast straight to usize and multiplied. A zero in any of them drove
    row_bytes to zero and reached slice::chunks(0) — "chunk size must be
    non-zero", which panics in release builds, not only debug. A negative
    became astronomical under the cast and aborted the process on a 2.3-exabyte
    reservation. Both are now range-checked with the row-size products computed
    via checked_mul; a malformed /DecodeParms leaves the stream unchanged
    rather than failing it, which is what the caller would have had if
    /Predictor were absent.
  • PS CIDFont header counts (/CIDCount, /SubrCount, /FDBytes,
    /GDBytes, /SDBytes). /SubrCount was passed to Vec::with_capacity
    before the bounds check that would have rejected it, so a bogus count
    panicked with "capacity overflow" in release as well as debug. Separately,
    FDBytes + GDBytes == 0 made the CID map size zero for any /CIDCount, so
    the "binary data too short" check passed and an 8 TB reservation followed
    from a 700-byte file. Counts are now bounded against the binary segment
    actually present rather than against a fixed ceiling, the byte-widths are
    capped at 8, and the reservation happens after the check.

Neither bound rejects anything real: all 691 sample PDFs were re-...

Read more

stet 0.5.0 — 64-bit integers, Type 3 font fixes, --page

Choose a tag to compare

@AndyCappDev AndyCappDev released this 25 Aug 23:25

Minor release. PostScript integers are now 64-bit, which fixes the standard
LCG idiom that programs use for pseudo-randomness and is the reason this is a
breaking release rather than a patch. Three Type 3 font defects and a
clippath coordinate-space bug are also fixed, and the CLI gains --page.

Breaking

Downstream Rust code that reads PostScript integers needs attention; nothing
in the PostScript language surface changed incompatibly.

  • PsValue::Int now carries i64 instead of i32. A
    match obj.value { PsValue::Int(v) => … } binds an i64, so any use site
    that needs an i32 no longer compiles. DictKey::Int and Token::Int
    widened with it, as did Context::rand_seed.
  • PsObject::as_i32() now range-checks. It returns None for a value
    outside i32, where before it always returned Some for an integer. This
    is the one change with no compiler error behind it — audit call sites that
    treat None as "not an integer". Use the new as_i64() for the full
    range; keep as_i32() where the value is genuinely bounded (array and
    string indices, character codes), since a too-large value should fail those
    callers' range checks rather than wrap into a valid-looking index.
  • PsObject::int() takes impl Into<i64>. Calls are unaffected; only
    code coercing it to a fn(i32) -> PsObject pointer breaks.

Fixed

  • Type 3 fonts supplying only BuildGlyph raised invalidfont. The show
    path required BuildChar unconditionally and pushed the character code.
    PLRM 5.7 lists BuildGlyph as preferred and makes BuildChar required only
    "for LanguageLevel 1 or if BuildGlyph is absent", so such a font is
    well-formed and must be handed the character name from Encoding.
    Ghostscript renders these; stet refused them. xshow/yshow/xyshow had
    the identical defect.
  • stringwidth raised invalidfont on every Type 3 font, BuildChar
    ones included. It branched for font types 2, 0 and 42 and then fell through
    to the Type 1 path, which looks for CharStrings — a Type 3 font has none.
    There is no width table to consult: the width is whatever the build
    procedure hands setcachedevice/setcharwidth, so the procedure now runs
    inside a gsave/grestore with its marks drained, and measuring paints
    nothing.
  • glyphshow raised invalidfont on every Type 3 font. It read
    FontType but never branched on 3, going straight to the CharStrings
    lookup. Per the PLRM it now invokes BuildGlyph with the name directly —
    bypassing Encoding, which is what lets glyphshow reach glyphs no
    character code maps to — or, with only BuildChar, reverse-searches
    Encoding for the name and pushes the array index, retrying with
    /.notdef and raising invalidfont only when neither is encoded.
  • PostScript integers are now 64-bit, matching Ghostscript, which fixes the
    standard LCG idiom PostScript programs use for pseudo-randomness:
    /seed seed 1103515245 mul 12345 add 2147483648 mod def. On 32-bit integers
    the product overflowed, promoted to a real, and mod — which is
    integer-only — raised typecheck. Widening only the real fallback would not
    have fixed it: the product needs 55 bits and a real carries 53, so the seed
    would have come out one too high and every later draw would have diverged
    silently from what other interpreters produce. PLRM Appendix B's 32-bit
    range is listed under "Typical Limits" for interpreters "running on 32-bit
    machines" which "do not necessarily apply to all PostScript
    implementations", so this is not a conformance change. Overflow past the
    64-bit range still promotes to a real. bitshift is correspondingly 64-bit
    wide, and cvi accepts the wider range.
  • clippath returned the page in device space instead of current user
    space
    , so a program that had transformed its coordinate system got a clip
    rectangle dragged along with the transform. The clippath fill idiom for
    painting a background then filled an offset region and left part of the page
    bare — visible in the tiger EPS, whose grey backdrop was displaced by its
    %%BoundingBox origin. The default clip is a fixed region of the device, so
    it is now derived with the default CTM; pathbbox and fill map it back
    through the current CTM, which is what puts it in user space for the caller.
    Ghostscript's values now match exactly under translate, scale and rotate.
  • cvi on a long integer-valued string was off by one. The string scanner
    returned f64, so (22358003463039195) cvi came back as ...196 after the
    round trip through a 53-bit mantissa. Integer literals now stay integral.

Added

  • The CLI reports a page that was painted but never shown. A program that
    paints marks and then ends without a matching showpage leaves them on a
    page the device is never asked to emit, and the page is discarded. That is
    correct — it is what the PLRM specifies and what Ghostscript's file devices
    do — but it was indistinguishable from a broken renderer: no file appeared
    and nothing said why. The warning distinguishes a program that produced no
    output at all from one that lost only its trailing page.
  • Interpreter::warnings() surfaces the same diagnostic to library
    callers, where the silence was worse: render() returned Ok(vec![]), an
    empty page list that reads as a legitimate result. New public types
    ExecWarning and ExecWarningKind in stet::diagnostics; the CLI shares
    the detector, so the two cannot drift. Programs that install nulldevice
    are exempt — that is the PLRM-sanctioned way to ask for no output, so marks
    left unemitted are the point rather than a mistake.
  • --page sets the page size for PostScript/EPS input — a named size
    (letter, legal, tabloid, ledger, executive, a0-a6, b4, b5)
    or WIDTHxHEIGHT in points, with an optional -landscape / -portrait
    suffix that swaps the dimensions. There was previously no way to render a
    plain %!PS program whose artwork is larger than the default page:
    %%BoundingBox sets the page only for EPS — for a non-EPS document DSC
    makes it a description of the artwork's extent, not a page-size request, so
    both stet and Ghostscript fall back to US Letter and clip. --page
    overrides an EPS %%BoundingBox when both apply, and is rejected for PDF
    input, whose pages carry their own size.
  • GlyphCache::by_type3_name, a name-keyed Type 3 glyph cache. glyphshow
    can name a glyph that no character code maps to, which leaves nothing for
    the existing code-keyed cache to key on.

WebAssembly

  • stet-wasm 0.2.0. Its JavaScript API is unchanged, but the browser
    build inherits everything above, so rendering output moves: clippath
    backgrounds fill the page, Type 3 fonts that previously raised
    invalidfont render, and PostScript programs using the standard LCG for
    pseudo-randomness run instead of failing. A minor rather than a patch
    because the pixels change, not because anything you call does.

stet 0.4.1 — systemparams conformance fixes

Choose a tag to compare

@AndyCappDev AndyCappDev released this 15 Aug 09:46

Patch release. Two currentsystemparams values were wrong in every release
up to and including 0.4.0, and PDFs now record which build wrote them. No API
changes; no rendering changes.

Fixed

  • /PrinterName returned (stetIE) instead of (stet). The string was
    allocated with the four bytes of stet but declared six bytes long, so
    reading it ran two bytes into the next allocation — which happened to be
    /RealFormat. Not memory-unsafe (the arena is a single buffer), but it put
    a neighbouring allocation's bytes into a value any PostScript program can
    read, and the value would have changed as soon as allocation order did.

  • /RealFormat returned (IEE) instead of (IEEE) — a missing E in
    the literal, independent of the overrun above. The PLRM specifies this key
    as naming the internal real representation, and Ghostscript reports
    (IEEE).

    Both lengths are now derived from the literal rather than written out
    twice, which is what allowed them to disagree. The other three
    allocate-then-declare sites in Context::new were audited and are correct.
    Regression coverage in unit_tests/interpreter_param_tests.ps asserts
    lengths as well as contents — the contents alone read plausibly, and it was
    the overrun that made them wrong.

Changed

  • PDF /Producer now carries the version, e.g. stet 0.4.1, where it
    previously wrote a bare stet. Every other producer does this —
    Ghostscript writes GPL Ghostscript 10.05.1, Distiller
    Acrobat Distiller 20.0 — and it is the first thing checked when a
    prepress shop is chasing a rendering difference between two files. A
    pdfmark /DOCINFO /Producer override still takes precedence; this
    changes only the default. Note that this alters bytes in the /Info dict
    of every PDF stet writes, at every release.
  • Documented MSRV corrected to Rust 1.88. The README badge had claimed
    1.85 since it was added — a number inferred from edition = "2024" and
    never compiled against. The real floor is 1.88: first-party code uses
    let-chains in 282 places across nine crates, jpeg-encoder declares 1.87,
    and fearless_simd declares 1.86. Nothing about what stet requires has
    changed; only the claim is now true. rust-version = "1.88" is declared in
    [workspace.package] and inherited by all eleven first-party crates, so
    cargo now reports a clear "requires rustc 1.88" instead of failing with a
    confusing edition parse error on an older toolchain.
  • A pinned MSRV 1.88 CI job builds the workspace on exactly that toolchain
    on every push, and scripts/check-release-versions.sh now ties the README
    badge, the README prose, and the CI job's pin to rust-version so the four
    cannot drift apart. The script also asserts every publishable crate
    declares an MSRV, so none can reach crates.io without one.
  • Switched to resolver = "3" (MSRV-aware dependency resolution). Cargo
    now prefers dependency versions compatible with the declared
    rust-version rather than always taking the newest, so a routine
    cargo update can no longer silently break the floor. The lockfile was
    byte-identical on adoption, but this is already doing work: it holds back
    hayro-jpeg2000 0.4.0 (needs 1.92) and moxcms 0.9.0 (needs 1.89).

Note for downstream users

Releases up to and including 0.4.0 published with no rust-version in their
manifests, so crates.io and docs.rs show no MSRV for them and cargo cannot
warn an old toolchain before it fails to compile. Published versions are
immutable; this release is the first to carry the metadata.

stet 0.4.0 — VM reclamation and PostScript conformance

Choose a tag to compare

@AndyCappDev AndyCappDev released this 15 Aug 01:47

Minor release focused on memory and PostScript conformance. restore
now reclaims local VM instead of only reverting values, several
long-standing PLRM 3.7.2/3.7.3 violations in the interpreter's own writes
are fixed, and a 6384-file PostScript corpus sweep drove the job-abort
count from 1158 to 147 — of which 116 fail identically in Ghostscript,
leaving 31 that are genuinely ours.

This is a 0.x minor bump. No public API was removed; Context gained
fields, which is source-breaking only for code constructing one
literally (it has no public constructor other than Context::new).

Highlights

  • restore reclaims local VM. Allocations made above a save's
    high-water mark are released rather than left resident. A
    save/restore loop that peaked at 2108 MB now peaks at 74 MB.
  • restore actually reverts what it is supposed to. Several
    interpreter-internal writes bypassed copy-on-write, so restore had
    no backup to revert to: defineresource, FontDirectory, reverse,
    execstack and dictstack. This was a live PLRM 3.7.3 violation, not
    a theoretical one.
  • Global/local VM enforcement is no longer silently disabled.
    .error did not restore setglobal, so any caught error left the
    interpreter in whatever VM mode the failing code had set.
  • Eleven interpreter defects found by a 6384-file corpus sweep, each
    A/B'd against the previous sweep with no regressions: procedure data
    sources, the Pattern colour space, bind on nested procedures,
    array-form colour spaces, cvi/cvr string conversion, CIDFontType 0
    StartData, 16-bit image samples, EOI-less JPEG, shareddict/scheck,
    self-registering resource files, rectclip, cshow, and charpath on
    Type 3 fonts.

Added

  • charpath support for Type 3 fonts: the glyph procedure runs without
    marking the page and the paths it would have painted become part of
    the current path.
  • The Pattern colour space — setcolorspace/setcolor/currentcolor
    with a pattern, including the uncoloured (PaintType 2) base-space form.
  • shareddict and scheck in systemdict.
  • 16 bits per component for image / imagemask / colorimage.
  • stet_core::vm_audit and --example audit_vm: a machine check for
    dangling references and PLRM 3.7.2 global/local violations.
  • PostScript corpus build and sweep tooling under scripts/.

Fixed

  • restore now releases local VM allocated above the save mark, and
    copy-on-writes the dictionaries and arrays it is required to revert.
  • Every allocation is stamped with its save level and VM mode, so
    restore can tell a surviving reference from a dangling one and raise
    invalidrestore when PLRM requires it.
  • .error restores the VM allocation mode; page-device arrays are
    allocated in the page device's own VM and deep-copied on promotion.
  • filter accepts procedure data sources everywhere, runs them when the
    data is read rather than at filter time, and honours SubFileDecode's
    EOD semantics.
  • bind marks nested procedures read-only per PLRM, and terminates on
    cyclic procedure graphs.
  • setcolorspace accepts array-form base and alternate colour spaces.
  • cvi and cvr convert strings through the scanner, as PLRM specifies.
  • CIDInit's StartData consumes its charstring blob instead of
    leaving it to be scanned as tokens.
  • DCTDecode accepts a JPEG stream that ends before its EOI marker.
  • Resource files that register themselves are loaded once, through a
    shared .LoadResource, so composefont can find a CMap on disk.
  • rectclip takes every rectangle in a multi-rectangle argument, and
    accepts an empty array.
  • cshow hands its procedure the character code, not the CID.
  • CIE decode tables are memoised, and 8-bit image samples are no longer
    copied in unpack_samples — together these took the corpus from 24
    out-of-memory jobs to none.

stet 0.3.0 — PDF→PDF round-trip

Choose a tag to compare

@AndyCappDev AndyCappDev released this 12 Aug 20:28

Minor release adding PDF→PDF round-trip through the PDF output
device. A PDF parsed by stet-pdf-reader into the display list can now
be re-emitted as PDF with its prepress semantics preserved — spot
(Separation/DeviceN) colors, ICCBased spaces, overprint, soft masks,
transparency groups, optional-content layers, and /OutputIntents all
survive the round-trip rather than collapsing to flat process color.

This is a 0.x minor bump. The document-structure IR moved from
stet-core into stet-graphics (still re-exported by stet-core), so
code that reaches those types through stet-core is unaffected.

Highlights

  • PDF→PDF round-trip--device pdf on a PDF input now routes
    through PdfDocumentPdfDevice, so a PDF can be read to the
    display list and written back out as PDF (previously only PS/EPS
    input reached PdfDevice).
  • Prepress color preserved — Separation/DeviceN spot colors (and
    their base spaces, with DeviceGray promotion), ICCBased fill/stroke
    spaces, and ICCBased bases inside Indexed image spaces all round-trip;
    /Catalog /OutputIntents is carried through so the CMYK-driving ICC
    profile is retained.
  • Overprint preserved/OP//op forced on the first paint of
    each content stream, /OPM carried through the display list, and
    overprint state emitted for Image and Shading paints.
  • Transparency & layers emitted from the display list
    DisplayElement::Group as a Form XObject, SoftMasked with per-paint
    alpha/blend, and DisplayElement::OcgGroup with /OCProperties
    optional-content groups.

Added

  • PDF output: Separation/DeviceN spot color + base round-trip,
    Separation/DeviceN shadings and spot imagemasks, and CMYK imagemask
    fill preservation.
  • PDF output: ICCBased fill/stroke color-space round-trip and ICCBased
    base preservation inside Indexed image color spaces.
  • PDF output: /Catalog /OutputIntents round-trip through PDF→PDF.
  • PDF output: Group → Form XObject, SoftMasked + per-paint
    alpha/blend, OcgGroup/OCProperties, per-paint alpha on the
    Image and Shading writer arms, overprint state on Image/Shading, and
    /OPM round-trip.

Changed

  • Document-structure IR lifted from stet-core into stet-graphics
    (re-exported by stet-core).
  • PDF writer: graphics-state tracker restored across q/Q
    boundaries; replayed clips collapsed so a round-trip no longer grows
    the display list; implicit page-box clip skipped on PDF→PDF.
  • stet-pdf-reader: spot tint-transform table cached per content
    stream.
  • README: added a Commercial Support section.

Fixed

  • Removed a dead emit_fill_color_rgb helper, superseded by the
    DeviceColor-aware imagemask fill path (cleared a dead_code warning).

Crates published at 0.3.0

stet, stet-cli, stet-fonts, stet-graphics, stet-core,
stet-ops, stet-engine, stet-render, stet-viewer,
stet-pdf-reader, stet-pdf. The vendored stet-tiny-skia /
stet-tiny-skia-path forks remain at 0.11.4. stet-wasm remains at
0.1.1 (excluded from crates.io, independent cadence).

stet 0.2.1 — PDF/X CMYK rendering correctness

Choose a tag to compare

@AndyCappDev AndyCappDev released this 10 May 03:46

[0.2.1] — 2026-05-09

Patch release focused on PDF/X CMYK rendering correctness against the
Ghent PDF Output Suite (GWG)
test corpus. Fixes a family of bugs where ICCBased / Lab / DeviceN
fills, images, and transparency groups didn't round-trip through the
document's /OutputIntents profile correctly, producing visible
"X" markers in calibration swatches that should render uniform.

This is an additive, non-breaking release. Downstream code does
not need to change. New public API on stet-graphics::IccCache and
a new rendering_intent: u8 field on stet-graphics::ImageParams
are documented below.

Highlights

  • GWG 13.3 — ICCBased RGB paints with /OP true no longer route
    into the custom-spot overprint path; per PDF 1.7 §11.7.4.5 they
    paint as if /OP were false.
  • GWG 16.1 — per-intent PDF/X proofing chain (source A2B → PCS → OI B2A → CMYK) is built for every registered ICCBased RGB
    profile, threaded through op_ri / ExtGState /RI.
  • GWG 16.4 — transparency groups with no /CS (inherit) now
    resolve correctly to the parent's CMYK compositing space when
    the parent is a /CS DeviceCMYK group.
  • GWG 17.2 — ICCBased images now go through the proofing chain
    via convert_image_8bit_with_intent (was bypassing the
    OutputIntent roundtrip and rendering via direct source→sRGB).
  • GWG 22.1 — Lab fills populate DeviceColor::native_cmyk via
    a direct Lab → PCS → OI B2A → CMYK chain (matches Adobe ACE),
    and the OutputIntent install path pre-warms the sRGB→CMYK
    reverse transform so the parallel CMYK buffer never falls back
    to the PLRM (1-r, 1-g, 1-b, 0) formula.
  • WASM vieweropen_pdf now applies the document's
    OutputIntent before storing the cached state, so PDF/X documents
    render in the browser the same way they do in the CLI.

Added — public API (additive, non-breaking)

stet-graphics:

  • IccCache::convert_to_oi_cmyk(hash, components, intent) — run an
    RGB ICC color through the proofing chain at the given intent and
    return the intermediate OutputIntent CMYK.
  • IccCache::convert_lab_to_oi_cmyk(l, a, b, intent) — direct
    Lab → OI CMYK via the OI's per-intent B2A LUT.
  • IccCache::convert_image_8bit_with_intent(hash, samples, pixel_count, intent) — bulk image conversion with explicit
    rendering intent.
  • IccCache::convert_color_with_intent and
    convert_color_readonly_with_intent — per-intent single-color
    conversion.
  • IccCache::prepare_lab_to_oi_cmyk() — pre-build per-intent
    Lab→OI samplers; pair with prepare_reverse_cmyk().
  • IccCache::intent_from_pdf_byte(b: u8) — map PDF rendering-intent
    bytes (0..3) to IccRenderingIntent.
  • pub use moxcms::RenderingIntent as IccRenderingIntent.
  • pub struct LabToCmykSampler (in icc::perceptual) with
    pub fn sample_pdf_lab(l, a, b).
  • New field ImageParams::rendering_intent: u8. Default is 0
    (Perceptual). Per the documented "be a reader, not a writer"
    policy for param structs (CLAUDE.md), this is additive and not
    treated as a SemVer break.

stet-pdf-reader:

  • PdfDocument::apply_output_intent_as_default_cmyk() now also
    pre-warms the sRGB→CMYK reverse and per-intent Lab→OI samplers
    in addition to its previous behaviour. No signature change.
  • Image XObjects with /Intent now propagate the per-image
    rendering intent into ImageParams.rendering_intent, overriding
    the gstate /RI per ISO 32000 §11.3.4.

stet-render:

  • build_icc_cache_for_list now also pre-warms the per-intent
    Lab→OI samplers when proofing is enabled.

Fixed

  • DeviceGray painted in a PDF/X DeviceCMYK page group now routes
    through the K plate (matches DeviceCMYK 0/0/0/(1−g) byte-for-byte).
  • DeviceN images with a non-CMYK alternate space go through the
    overprint path so process plates aren't disturbed.
  • Paired /OP true /op true ExtGStates are now treated as a
    "strict overprint" signal (matches Adobe Illustrator's emit).
  • The custom-spot overprint dispatch and the parallel CMYK buffer's
    is_custom_spot heuristic both now require
    process_cmyk.is_some() so proofing-chain ICCBased RGB stays out.

Verification

  • 11 GWG calibration tests pass: 13.0 / 13.3 / 16.1 / 16.4 / 17.2 /
    17.3 / 22.1 / 23.0 / 1.0 / 3.0 / 12.0 / 8.0.1.
  • cargo test --workspace --release clean.
  • cargo fmt --all -- --check clean. No new clippy errors.
  • wasm-pack build --target web --release crates/stet-wasm clean.

Crates published at 0.2.1

stet, stet-cli, stet-fonts, stet-graphics, stet-core,
stet-ops, stet-engine, stet-render, stet-viewer,
stet-pdf-reader, stet-pdf. The vendored stet-tiny-skia /
stet-tiny-skia-path forks remain at 0.11.4. stet-wasm is
excluded from crates.io and bumped to 0.1.1 independently.

Full changelog: v0.2.0...v0.2.1

stet 0.2.0 — ⚠ breaking release

Choose a tag to compare

@AndyCappDev AndyCappDev released this 01 May 18:18

[0.2.0] — 2026-05-01

This release lands a substantial expansion of the stet-pdf-reader
structural API, the PDF imaging-extension operators (transparency,
soft masks, optional content), and the pdfmark PostScript-to-PDF
authoring bridge. Several public match-surface enums are now
#[non_exhaustive] to lock in additive evolution — the breaking
changes are deliberate and documented per-crate below.

⚠ Breaking changes

This is a breaking release. Cargo treats the 0.1 → 0.2 bump as
incompatible (per the SemVer rules for 0.x), so existing users
pinned at stet = "0.1" won't be auto-upgraded.

The breaking surface is concentrated in two places:

  1. #[non_exhaustive] markers were added to ~40 public
    match-surface enums across stet-graphics, stet-core, and
    stet-pdf-reader. Any downstream match over DisplayElement,
    PsError, Destination, AnnotationKind, the various pdfmark
    record enums, etc. now requires a _ => { ... } wildcard arm.
    See the "Changed — public API breaking changes" subsection below
    for the complete list.

  2. stet-pdf no longer emits PDF/X-3 OutputIntents. PDF output
    is now plain PDF 1.7. PdfDevice::set_output_profile() is
    #[deprecated] as a no-op; existing call sites compile but stop
    producing the (previously broken) PDF/X-3 conformance label.

For a typical downstream renderer that pattern-matches on
DisplayElement, the migration is one wildcard arm per match
site:

 match element {
     DisplayElement::Fill { .. } => { /* … */ }
     DisplayElement::Stroke { .. } => { /* … */ }
     DisplayElement::Image { .. } => { /* … */ }
+    _ => { /* fall through; new variants in 0.2.x are additive */ }
 }

The #[non_exhaustive] ratchet is intentional: it makes future
variant additions non-breaking, so 0.2.x → 0.3.x will be smaller.

Added — stet-pdf-reader structural API

A read-only structural-content API for PDF inspection and tooling.
Every accessor parses lazily on first call and caches its result.

  • metadata()/Info dict (title, author, dates, …) and the
    catalog's /Metadata XMP stream.
  • viewer_preferences() — page layout, page mode, print preferences,
    and reading direction hints.
  • outline() — bookmark tree as OutlineItems with
    destination/action resolution.
  • destinations(), resolve_named_destination(name) — named
    destination table merged from /Catalog /Dests (legacy) and the
    /Names /Dests name tree.
  • page_annotations(page) — typed Annotation list with
    destination/action resolution.
  • form(), form_fields() — AcroForm field tree (text, choice,
    button, signature) with widget cross-references.
  • page_boxes(page) — MediaBox / CropBox / BleedBox / TrimBox /
    ArtBox.
  • embedded_files(), embedded_file_bytes(name)/EmbeddedFiles
    name-tree walker.
  • layers(), layer(ocg_id), configurations(),
    default_configuration(), layer_tree(), layer_set_for(intent)
    — Optional Content Group (OCG) metadata, hierarchy, render-intent
    rules, and a runtime LayerSet for visibility overrides.
  • parse_warnings() — diagnostic sink for non-fatal parse issues
    (broken outlines, bad name trees, malformed /VE expressions, …).
  • New stet inspect <file.pdf> CLI subcommand surfaces the structural
    API at the command line.

See docs/PDF-READER-API.md and docs/PDF-LAYERS.md for full
references.

Added — PDF imaging extensions

Display-list-level support for the PDF transparency and optional-content
imaging models, layered on top of the PostScript interpreter.

  • Alpha and blend modes: setblendmode, setfillalpha,
    setstrokealpha, setalphaisshape. All 16 PDF blend modes.
  • Transparency groups: begintransparencygroup /
    endtransparencygroup with Knockout, Isolated, and group
    colour space (DeviceGray / DeviceRGB / DeviceCMYK / ICC).
  • Soft masks: begintransparencymaskgroup /
    endtransparencymaskgroup with Alpha and Luminosity subtypes,
    transfer functions, and backdrop-colour handling.
  • Optional Content (OCG): setocg / endocg operators wrap
    display-list content in OcgGroup elements with
    OcgVisibility::Single / Membership / Expression predicates.
    LayerSet (in stet-graphics) is the consumer's per-render override
    map; render_page_to_rgba_with_layers honours it.
  • Filters: JBIG2Decode and JPXDecode for embedded image
    streams.

See docs/PDF-EXTENSIONS.md for the full reference and
docs/PDF-LAYERS.md for the runtime layer-visibility model.

Added — pdfmark PostScript-to-PDF authoring

pdfmark operator dispatch in stet-ops (gated behind
register_pdf_authoring_ops so it's only visible to systemdict on the
PDF output path) plus matching emitters in stet-pdf. Five phases of
authoring support:

  • /DOCINFO — document info dictionary (title, author, subject,
    keywords, creator, producer, dates, trapped).
  • /OUT — outline (bookmark) tree authoring with destination /
    action targets.
  • /ANN — Link, Text, FreeText annotations.
  • /DEST, /PAGE, /PAGES — named destinations and per-page-box
    overrides.
  • /VIEWERPREFERENCES, /Metadata — viewer preferences and
    document-level XMP metadata.
  • /Widget and /FORM — AcroForm widget annotations and field-tree
    emission.
  • /EMBED, JavaScript / Named actions, page-level /AA triggers.

See docs/PDFMARK-AUTHORING.md for the full reference.

Added — colour management

  • Hand-rolled colorimetric A2B1 CLUT sampler
    (stet-graphics::icc::perceptual). moxcms 0.8's create_transform
    pipeline over-saturates CMYK→sRGB output relative to lcms2 / Acrobat
    / Ghostscript on midtone colours; this module bypasses it for v2
    lut16Type CMYK profiles and matches lcms2 RelCol output to ±1 RGB
    level on a 17⁴ sweep against ISO Coated v2 300% (ECI). Out-of-gamut
    colours clip to the sRGB boundary (matching lcms2 / GS) so pure
    process primaries remain saturated. BPC is calibrated against the
    sampler's own (1, 1, 1, 1) output so K-heavy CMYK lands at the
    correct darkness. Profiles whose tables are mAB / mft1 fall back
    to the moxcms-driven bake.
  • Soft-mask CMYK-domain blend gate widened to accept Group-wrapped
    flat CMYK fills (GWG 16.11 "Gradient Feather"). The GWG 16.10
    outer-glow protection still rejects on the inner Fill's blend-mode
    check.

Changed — public API breaking changes

These match-surface enums are now #[non_exhaustive] so adding
variants is non-breaking for any consumer that includes a _ => arm.
Existing consumers must add wildcard arms (or update their match
expressions) to keep building.

  • stet-graphics: DisplayElement, ImageColorSpace,
    ShadingColorSpace, SpotColorSpace, LineCap, LineJoin,
    FillRule.
  • stet-core: PsError, FilterKind, RleState.
  • stet-core::pdfmark: PdfMarkRecord, AnnotationSubtype,
    AnnotationTarget, OutlineDestination, OutlineAction,
    GoToTarget, ViewSpec, FieldType, FieldValue, DocDate,
    TrappedState, TzSign, LinkHighlight, TextAnnotationIcon,
    PageOverrideScope.
  • stet-pdf-reader: PdfError, Destination, ViewSpec, Action,
    AnnotationDate, AnnotationKind, AnnotationColor,
    AnnotationKindData, FieldKind, ButtonType, FieldValue,
    TrappedFlag, PageLayout, PageMode, ReadingDirection,
    PrintScaling, Duplex, AfRelationship, ParsePhase,
    LocationHint, Severity, RenderIntent, LayerIntent,
    UsageState, PageElementSubtype, LayerTreeNode, BaseState,
    ListMode, AutoStateEvent.

Param structs (FillParams, StrokeParams, ImageParams, the
pdfmark record structs, Annotation, FormField, Layer, etc.) are
not marked #[non_exhaustive] — adding fields lands additively
and consumers should pattern-match with .. for forward
compatibility.

A scripts/check-non-exhaustive.sh audit runs in the local pre-push
hook; new public enums in the listed files must either carry the
marker or be allow-listed with a one-line justification. See the
"Stable extension points" section of CLAUDE.md and the per-doc
"Stability" sections of docs/DISPLAY-LIST.md,
docs/PDF-READER-API.md, and docs/PDFMARK-AUTHORING.md.

Changed — other

  • stet-pdf: removed the PDF/X-3 OutputIntent emission. The writer
    was emitting soft-mask transparency (prohibited by PDF/X-3) while
    labelling output as PDF/X-3:2003 — a conformance conflict any
    preflight tool would flag. PDF output is now plain PDF 1.7 with no
    PDF/X conformance claim. A correct PDF/X-4 implementation is
    planned.
  • stet-pdf: PdfDevice::set_output_profile() is #[deprecated]
    as a no-op. Retained for forward API compatibility with the planned
    PDF/X-4 work.
  • stet-cli: --width / --height flags for PDF input override
    the page's MediaBox at render time.

Added — documentation

  • docs/PDF-READER-API.md — full reference for the structural API.
  • docs/PDF-LAYERS.md — full reference for the OCG / layer API.
  • docs/PDF-EXTENSIONS.md — full reference for the imaging extension
    operators and the JBIG2 / JPX filters.
  • docs/PDFMARK-AUTHORING.md — full reference for the pdfmark
    authoring bridge.
  • New Rendering Correctness section in the root README covering
    seam-free rendering on adjacent clipped regions and full overprint
    simulation.