Skip to content

stet 0.8.6

Choose a tag to compare

@github-actions github-actions released this 12 Oct 06:04
· 65 commits to main since this release

Added

  • PdfDocument::render_page_cancellable: give up on a page that is
    still being read.
    It is render_page with an AtomicBool to watch,
    for a viewer whose user has scrolled past the page or a service with a
    deadline. Once the flag is set the call returns Ok(None) as soon as
    the operator being carried out finishes — the flag is read before every
    token of the page's content and of each form, pattern, glyph procedure
    and annotation appearance it draws. The result is the whole display
    list or nothing, an abandoned page adds no warnings, and the document
    can go on to read the same page again. One operator is not interrupted,
    so a single very large image still decodes to its end;
    set_max_image_pixels bounds that. Rasterising already had a flag
    (RegionRender::render_parallel_cancellable), so both halves of
    drawing a page can now be stopped. See docs/PDF-READER-API.md,
    "Giving up on a page".

Changed

  • The edge of a clipped shape is smooth. A shape filled with a
    gradient or an image is painted through a clip of its outline, and clips
    were cut to whole pixels, so such shapes had stepped edges beside the
    smooth ones of ordinary fills. A clip's edge is now anti-aliased unless
    another clip runs along it from the other side: regions that are tiled
    together — the pieces of a mesh, the strips of a blend — still meet
    hard, with none of the page showing between them. Edges that run along
    the pixel grid stay hard too, as a clipping rectangle's always were;
    that keeps a table rule drawn through a clip of its own outline at its
    full weight. Rendering with anti-aliasing off (--no-aa, no_aa) is
    unchanged. Roughly one page in seven has such an edge and renders
    differently. A mesh or patch shading clipped to its own outline fills
    that edge: its colour is carried to the edge pixels whose centres lie
    just outside it, which were otherwise left thin and light.

  • Artwork a transparency flattener has cut into pieces has smooth edges
    too.
    A flattened page is made of pieces that tile: each letter of a
    heading an image clipped to the glyph, the background an image clipped
    to everything but the letters. Such edges were the ones kept hard, so
    flattened text was stepped beside smooth artwork. Where two clipped
    pieces meet, the one painted later is now anti-aliased over the colour
    of the one painted first, which keeps its hard edge underneath, so no
    page shows between them. An edge stays hard on both sides where that
    cannot be done safely: the later piece is translucent, blended,
    overprinted or masked, more than one thing is painted through it along
    the edge, nothing is known to be painted behind the edge, or something
    painted later still shows there.

  • A PDF page with thousands of images is read in a fraction of the
    time and memory.
    Two costs grew with the number of images on a page.
    Every image in an ICCBased colour space decoded its profile's stream,
    hashed it and kept a copy of its own, though a document normally names
    one profile object from all of them; the profile is now read once per
    document and shared. And every Do copied the page's whole /XObject
    dictionary to look up one name, so reading was quadratic in the number
    of XObjects; it now copies the one entry (the same for /Font,
    /ExtGState, /Shading, /Pattern, /ColorSpace and /Properties
    lookups). On a one-page InDesign flyer with 5,224 images sharing a
    650 KB CMYK profile, rendering at 150 dpi went from 5.6 s to 1.4 s and
    peak memory from 3.3 GB to 270 MB. Output is unchanged.

  • Pages of many small images, and images that are scaled down, are
    rasterised faster.
    On a page that tracks CMYK for overprint, each
    image looked at every pixel of the band to find the ones it covered,
    so a page of thousands of small images spent nearly all its time
    there; an image now visits only the pixels it can reach. The same page
    as above went from 0.46 s to 0.08 s of rasterising at 150 dpi. And the
    filter that scales an image down to the output resolution worked out
    each output column's span and weights again for every row; it now
    works them out once per image, which more than halves the time spent
    preparing a page whose images are larger than they are printed. Output
    is unchanged in both cases.

  • A PDF content stream is read about a third faster. Reading a page
    allocated for nearly every token. An operator was copied out of the
    stream to be looked up; it is now looked up where it stands. A number
    was checked as UTF-8 and handed to the general parser; an integer of up
    to 18 digits or a real of up to 15 is now read directly, to the same
    value. An operator with a fixed number of operands gathered them into a
    vector; and each operand that was a number built, and threw away, the
    error message it would have needed had it not been one. A colour set in
    a named colour space resolved that space from the PDF's objects every
    time; it is now resolved once per resource dictionary. Saving the
    graphics state for q allocated a copy of each clip path, name and
    colour it holds, and Q freed them; a q now saves into a state a Q
    has finished with, reusing its buffers. And every Q compared the font
    name before and after even when no font was set, which for two empty
    names is far slower than it sounds: the C library's comparison reads
    through the dangling pointer of an empty vector, and the processor has
    to suppress the fault. On a 278 MB content stream of 4.4 million
    strokes, reading went from 5.3 s to 2.5 s. Output is unchanged.

  • Pages built from many clipped gradients and small transparency
    groups are rasterised several times faster.
    Two costs, both paid per
    band of the page. A radial shading worked out the gradient for every
    pixel of its area — the whole page when it has no /BBox — and only
    then asked whether the clip let the pixel through; the clip is now
    asked first, and a row the clip masks out entirely is not visited. And
    a transparency group nested in another was rendered for every band,
    including the bands it does not reach, where it was given an offscreen
    the size of the band and composited back to no effect; a band now
    passes over a nested group outside it, as it already did a page-level
    one. On a Keynote slide with 1,644 radial shadings and 185 nested
    groups, rendering the page to PNG at 150 dpi went from 2.9 s to
    0.7 s, and its rasterising on one thread from 7.1 s to 0.2 s. Output
    is unchanged.

  • A viewport of a page with thousands of clips is rendered three to
    four times faster.
    render_region and its relatives — what a viewer
    calls to draw part of a page at a zoom — rasterised every clip path of
    the page to a mask and intersected it with the clip so far, for every
    band. Rendering a whole page at its own resolution had a shortcut for a
    clip path outside the band; a viewport at any other zoom or offset did
    not. A clip path that lies wholly outside the band is now seen to from
    its bounds at any zoom, offset and CTM, nothing further is rasterised
    once the clip lets nothing through, and two masks are intersected eight
    samples at a time where they are all 0 or all 255, as a clip mask
    nearly always is. On a Keynote slide with 19,479 clips, a full-page
    viewport at 150 dpi went from 3.9 s to 1.2 s on one thread, which is
    how the WebAssembly viewer renders. Output is unchanged.

  • Setting a clip costs what the clip covers, not what the page does, and
    a clip set before is not worked out again.
    A rasterised clip was a
    mask the size of the band, and each clip path copied one out of the
    cache, cleared another and multiplied the two together sample by sample
    — so a page that sets the same page and frame clips before each of
    thousands of small things did the same band-sized intersection
    thousands of times. A clip mask now records the rectangle outside which
    it lets nothing through, and is cleared, copied and intersected only
    within it; a mask in the cache is shared with the clip in force rather
    than copied; and an intersection asked for a second time is kept, as a
    clip path rasterised a second time already was. The same Keynote slide,
    the same viewport: 1.2 s to 0.27 s on one thread. Output is unchanged.

  • A band of a viewport converts and resamples only the part of an
    image that it shows.
    A viewer draws a view in bands, and every band
    an image crossed converted the whole image to RGB and scaled the whole
    of it, to draw a strip: the work of the page once per band. That is
    every image inside a transparency group or under a soft mask, which the
    viewport image cache leaves out, and the scaling of every image that
    is in it. A band now works out which rows and columns of the image fall
    on it — a few rows of an upright image, a few columns of one on its
    side — and converts and resamples those alone, into an image of the
    same size and place so that it is drawn exactly as the whole would be.
    An InDesign page of photographs under soft masks, drawn in thirteen
    bands at 150 dpi on one thread, went from 2.3 s to 0.73 s, and a
    zoomed-in part of it from 2.3 s to 0.28 s; across 175 sample files the
    banded viewport time halved. Output is unchanged.

  • Opening a PDF prints nothing. IccCache::search_system_cmyk_profile
    — which PdfDocument::from_bytes and from_owned call — wrote
    [ICC] Loaded system CMYK profile to stderr every time it found one,
    so an application embedding the reader had that line on its stderr for
    every document it opened. The library no longer says it; the stet
    command does, as before. Whether a profile was found is
    IccCache::default_cmyk_hash being Some.

  • An ICC profile that cannot be used is reported, not printed. A
    profile that would not parse, was in a colour space stet does not
    convert from, or that no transform could be built from, and an image
    conversion that failed, each wrote a line to stderr from inside
    stet-graphics. IccCache now writes nothing; it reports to a sink the
    caller sets with the new IccCache::set_diagnostic_sink, and to nobody
    by default. The PDF reader sets one on every document, so these arrive
    in parse_warnings as ParsePhase::Content warnings for the page that
    uses the profile (ICC profile: Failed to parse profile: …). The stet
    command still prints them: for a PostScript job as before
    ([ICC] Failed to parse profile: …), and for a PDF with the document's
    other warnings (warning: ICC profile: Failed to parse profile: …). The
    cache the renderer builds for itself from a display list has no sink, so
    a failure met only there is no longer printed.

  • Pages are rendered in bands of 128 rows at every resolution. Bands
    used to be sized to fill 2 MB, which below about 160 dpi meant few of
    them or one: a Letter page had seven at 144 dpi and was not banded at
    all at 72, so most threads had nothing to do at the resolutions a
    viewer draws at. Over the first pages of 41 documents the median render
    went from 9.6 ms to about 5 ms at 72 dpi, and from 8.2 ms to 6.0 ms at
    144 dpi on 24 threads; at 144 dpi on 8 threads it is unchanged, and on
    one thread about 2% slower. Pages wider than 1365 pixels — Letter
    above 160 dpi — already used 128 rows and are untouched. Pixels change: a page is not quite
    independent of where its bands fall, so about half of those pages
    differ from before, most by one or two levels in a handful of pixels
    and a few by more in single pixels of a fine pattern or along the edge
    of a shading. The height does not depend on the number of threads, so a
    page still renders the same on every machine.

Fixed

  • A gradient mesh no longer shows specks of what is under it. The
    patches of a mesh are each drawn as triangles of their own, and where
    two patches did not divide a shared edge alike a pixel could fall
    between them and be painted by neither: a white or grey speck inside
    the mesh, in a different place at every zoom. Illustrator's gradient
    meshes showed it most. Such a pixel is now painted from the nearest
    triangle. The outline of a mesh is unchanged, and a gap a pixel or
    more wide is left as drawn.

  • A program ending showpage restore is no longer told its last page
    was dropped.
    The warning for marks painted after the last showpage
    counted everything recorded since it, and restore, grestore and
    initclip each record the clip they leave. So the commonest ending in
    PostScript — and every EPS that calls showpage itself, rendered
    through the library — reported "painted 1 object(s) and then ended
    without a matching showpage, so that page was dropped" for a page
    that had just been output. The warning now counts what was painted,
    and ExecWarningKind::DroppedFinalPage::objects is that count.

  • An EPS that calls setpagedevice and not showpage shows its page.
    It came out blank from Interpreter::render and render_to_pdf, from
    the browser build, and from the stet command when the bounding box
    did not start at the origin. The page was ended after the grestore
    that closes the wrapper stet puts around an EPS, and that grestore
    takes the EPS's page device, and what was painted on it, away. The
    page is now ended first.

  • A number or name ended by CR LF takes both characters (issue #7).
    The scanner took the carriage return and left the line feed in the
    file, so a program that then read from currentfile — readline,
    readstring, in-line image data — started one character early; with
    readline that is an empty first line. PLRM 3.2.2 makes the pair one
    newline. Fixed in the scanner for files, the one for filtered files,
    and token on a string, which now leaves what follows the pair.

  • A literal or immediately evaluated name takes the white space that
    ends it
    , as an executable name and a number do. PLRM says "a name or
    a number" of both token and currentfile, and Ghostscript does it;
    stet left the character behind after /name and //name. So
    (/ab X) token left ( X) and now leaves (X), and after
    currentfile token had read a literal name, readline returned an
    empty line or one beginning with a space. A delimiter that ends a name
    is still left for the token it begins.

  • The program goes on after a mesh whose data a procedure reads from
    comment lines.
    Illustrator stores a gradient mesh's data in % lines
    after the shading, read a line at a time by a procedure behind
    ASCII85Decode. With CR LF line endings the empty line above ended
    the data at once and every mesh painted nothing, without an error. And
    with any line endings, stet called the procedure until it returned an
    empty string, which such a procedure does only at the end of the file:
    the rest of the program was taken as data and nothing after the first
    mesh was run. A procedure feeding ASCIIHexDecode or ASCII85Decode
    is now asked for data only up to the filter's end-of-data mark.

  • A tiling pattern whose cell is not at its tile's origin is painted.
    The renderer drew the tiles whose origins lay within a step of the shape
    being filled. A pattern's cell is its /BBox, which can sit anywhere: a
    chart whose fills used a /BBox of [144 472 429 540] with a 68-unit
    step has its cell seven steps from the origin, and those fills came out
    blank. Every tile whose cell reaches the shape is now drawn, which also
    covers cells larger than the step.

  • A pattern tile is no longer drawn a pixel out on one side of a band
    seam.
    A tile made of anything but plain fills, strokes and images is
    drawn once and stamped; the stamp's position was truncated towards the
    band's own top-left corner, so a tile crossing two bands could sit one
    pixel differently in each. It is now placed on the page's pixel grid.

  • Pattern fills cost what the shape covers, not what the page does.
    Each one drew its tiles into a buffer the size of the band and painted
    the whole buffer through the shape. The buffer is now the rows the shape
    reaches. A page of ten thousand small pattern-filled shapes at 72 dpi
    went from 0.94 s to 0.17 s on one thread and from 190 ms to 43 ms on 24.
    Because a tile's position is rounded in single precision relative to
    that buffer, a pattern fill can differ from before by a level or two
    along its edges, and occasionally by a row or pixel of the tile's own
    detail.

  • A font is resolved once for a document, not once for every page.
    Each page read the font program and built the encoding, widths and
    character maps of every font it used, however many pages before it had
    done the same. PdfDocument now keeps resolved fonts, up to 64 MB of
    font data with the least recently used going first, and
    set_font_provider discards them. Over 607 pages of 131 documents,
    reading every page a second time took 17% less; a ten-page document
    whose time was nearly all fonts went from 117 ms to 6 ms. Display
    lists, pixels and warnings are unchanged: a page that takes a font an
    earlier page resolved reports the warnings resolving it raised as its
    own, and a font that failed to resolve is tried again by each page.

  • Bands no longer wait on each other to build paths. The renderer
    grew each path it built a few points at a time, and with every band
    doing that for every glyph the threads spent their time queued in the
    allocator: on a page of text in 25 bands, 31 s of system time against
    16 s of work over 200 renders, and 0.3 s once each path is sized before
    it is filled. That was what made more, smaller bands slower instead of
    faster. Pages render byte for byte as before.

  • Pages set in embedded TrueType fonts are several times faster to
    read.
    Every glyph outline set up the font's hinting afresh, which
    runs the font's fpgm and prep programs: far more work than drawing
    the glyph. It is now set up once per font. On office documents set in
    an embedded Calibri, the first page went from 370–640 ms to 100–140 ms
    end to end; the pages come out byte for byte the same. This is the PDF
    reader only — the PostScript interpreter's Type 42 path was not
    affected. To hold what is set up, TrueTypePdfFont and
    CidTrueTypePdfFont in stet_pdf_reader::content::font have a new
    private field, so code that builds either with a struct literal no
    longer compiles. Reading their fields is unaffected.

  • Text is faster to read: a glyph's outline is drawn once per page.
    The PDF reader asked the font for an outline — a hinting program or a
    charstring to run — every time a glyph was shown, and a page of prose
    shows a few dozen glyphs thousands of times. It now keeps each font's
    outlines for the rest of the page. Building the display list for the
    first page of six office documents went from 11–19 ms to 6–12 ms; the
    lists are the same.

  • Painting through a clipping rectangle no longer builds a mask per
    element.
    With a rectangular clip smaller than the page — how word
    processors write every page — the renderer allocated and filled a
    surface-sized mask of the rectangle for every element it painted, so
    once per glyph. The mask is made once and kept while the clip stands.
    With the outline change above, the first page of six office documents
    at 144 dpi went from 18–32 ms to 13–26 ms, and from 29–56 ms to
    25–40 ms on one thread. Pages render byte for byte as before.

  • Opening a second PDF no longer pays for the system CMYK profile
    again.
    PdfDocument::from_bytes and from_owned searched for the
    profile, read it and built its transforms on every call: about 30 ms,
    which was most of the cost of opening a small document. That is done
    once per process and each document takes its own copy. Opening after
    the first went from 32 ms to 0.6 ms on a typical file. A document's
    cache is still its own — nothing one registers reaches another — and a
    profile installed while the process runs is not noticed; pass a cache
    to a _with_icc constructor for that.

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