Repository navigation
stet 0.8.6
Added
PdfDocument::render_page_cancellable: give up on a page that is
still being read. It isrender_pagewith anAtomicBoolto watch,
for a viewer whose user has scrolled past the page or a service with a
deadline. Once the flag is set the call returnsOk(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_pixelsbounds that. Rasterising already had a flag
(RegionRender::render_parallel_cancellable), so both halves of
drawing a page can now be stopped. Seedocs/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 everyDocopied 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,/ColorSpaceand/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 forqallocated a copy of each clip path, name and
colour it holds, andQfreed them; aqnow saves into a state aQ
has finished with, reusing its buffers. And everyQcompared 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_regionand 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
— whichPdfDocument::from_bytesandfrom_ownedcall — wrote
[ICC] Loaded system CMYK profileto 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; thestet
command does, as before. Whether a profile was found is
IccCache::default_cmyk_hashbeingSome. -
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.IccCachenow writes nothing; it reports to a sink the
caller sets with the newIccCache::set_diagnostic_sink, and to nobody
by default. The PDF reader sets one on every document, so these arrive
inparse_warningsasParsePhase::Contentwarnings for the page that
uses the profile (ICC profile: Failed to parse profile: …). Thestet
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 restoreis no longer told its last page
was dropped. The warning for marks painted after the lastshowpage
counted everything recorded since it, andrestore,grestoreand
initclipeach record the clip they leave. So the commonest ending in
PostScript — and every EPS that callsshowpageitself, rendered
through the library — reported "painted 1 object(s) and then ended
without a matchingshowpage, so that page was dropped" for a page
that had just been output. The warning now counts what was painted,
andExecWarningKind::DroppedFinalPage::objectsis that count. -
An EPS that calls
setpagedeviceand notshowpageshows its page.
It came out blank fromInterpreter::renderandrender_to_pdf, from
the browser build, and from thestetcommand when the bounding box
did not start at the origin. The page was ended after thegrestore
that closes the wrapper stet puts around an EPS, and thatgrestore
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 fromcurrentfile—readline,
readstring, in-line image data — started one character early; with
readlinethat 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,
andtokenon 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 bothtokenandcurrentfile, and Ghostscript does it;
stet left the character behind after/nameand//name. So
(/ab X) tokenleft( X)and now leaves(X), and after
currentfile tokenhad read a literal name,readlinereturned 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 feedingASCIIHexDecodeorASCII85Decode
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/BBoxof[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.PdfDocumentnow keeps resolved fonts, up to 64 MB of
font data with the least recently used going first, and
set_font_providerdiscards 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'sfpgmandprepprograms: 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,TrueTypePdfFontand
CidTrueTypePdfFontinstet_pdf_reader::content::fonthave 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_bytesandfrom_ownedsearched 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_iccconstructor 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.