Repository navigation
stet 0.8.5
Added
-
PdfDocument::set_max_image_pixels: an application's own ceiling on
image size. stet holds every image to limits sized for prepress
(100,000 samples a side, four billion in all) and to nothing lower,
because the right figure depends on the machine. That leaves JPEG 2000
exposed: its decoder needs about 39 bytes of memory per sample the
image's header declares, whatever the size of the stream, so a
legitimate 212-megapixel scan costs 8.5 GB and a hostile 25 KB stream
declaring 60,000 × 60,000 asks for over 100 GB — and nothing in the
file tells the two apart. With a ceiling set, an image over it is left
out of the page with a warning naming its size. The size counted is the
one decoded: for JPEG 2000 the size in the stream's own header, or the
reduced size underImageResolution::Rendered, so a very large image
drawn small is still drawn; for every other image, mask and soft mask,
the size in its dictionary. Off by default. Also new:
DecodeBudget::with_image_pixels. Seedocs/PDF-READER-API.md, "A
ceiling on image size". -
--box-snap, andPdfDocument::page_area_rect_on_pixel_gridin
stet-pdf-reader: a--boxarea whose edges fall between device pixels
is rasterised at its own sub-pixel phase, so it antialiases differently
from the same area cropped out of a full-page render — 20 % of the pixels
of a 751 × 666 crop of a textbook spread at 150 dpi differed, by up to 200
levels.--box-snapwidens the area outward to whole pixels of the crop
box's pixel grid at the chosen resolution, so it renders the page's own
pixels; the output can be up to one pixel larger per side. An area
outside the crop box, a bleed for one, keeps its reach.--device png
only, and not with--width/--height. Contributed by @jungseohaan
(#5). -
PdfDocument::set_image_resolution, andImageResolution. A
display list keeps every image at its stored resolution, so it can be
zoomed into or written to another format; that is
ImageResolution::Full, the default, and it is unchanged. A caller that
builds a list withrender_pageonly to rasterise it at that resolution
can now say so withImageResolution::Rendered, and get what
render_page_to_rgbadoes on its own (see "Changed"). See
docs/PDF-READER-API.md, "Display lists and resolution". -
PdfDocument::from_owned: a document that owns its bytes.
from_bytesborrows the file, so aPdfDocument<'a>cannot outlive the
buffer, and an application that keeps documents open — a viewer, a
server with a cache — had to store each buffer beside its document or
resort to a self-referential struct.from_owned,from_owned_with_icc
andfrom_owned_with_passwordtake the buffer and return a
PdfDocument<'static>. They accept aVec<u8>, aBox<[u8]>or an
Arc<[u8]>(through the newPdfBytes) and copy none of them; anArc
stays shared, so a caller can keep its handle and try again with a
password. Thefrom_bytesconstructors are unchanged. -
PdfDocument::set_annotation_filter: draw some annotations and not
others.set_render_annotationsis all or nothing, which does not
suit an editor that draws review comments itself, as objects the user
can move, but wants form fields left in the page. AnAnnotationFilter
selects by class —AnnotationClass::Markup,Widget,Link,Other,
from the newAnnotationKind::class()— and by what the render is for:
RenderIntent::Viewleaves outHiddenandNoViewannotations,
RenderIntent::Printdraws only those with thePrintflag.
AnnotationFilter::draws(&annotation)is the renderer's own test, for a
caller that draws the rest. The default is every class, for viewing;
set_render_annotations(false)still turns them all off. See
docs/PDF-READER-API.md, "Drawing some annotations and not others". -
stet_render::RegionRender: a region of a page rendered with a layer
set.render_region_preparedand its parallel variants take twelve to
fifteen positional arguments and noLayerSet, so a viewer that draws a
page in tiles could not show or hide layers without interpreting the
page again.RegionRender::new(list, prepared, viewport, w, h, dpi)
takes the options by name —.icc(),.image_cache(),.no_aa(),
.layer_set()— and renders with.render(),.render_parallel(),
.render_parallel_with_progress(),.render_parallel_cancellable()or
.render_band(). With no layer set it gives the pixels the existing
functions give, and those are unchanged. OnePreparedDisplayListand
oneImageCacheserve a page under every layer set: the precomputed
bounds used to leave out a layer hidden by default, which an override
could then not bring back in a tile that the layer alone touched. Also
re-exported from thestetfacade. -
stet-wasm:set_max_vm(interp, bytes)caps PostScript VM in the
browser, as--max-vmdoes on the command line. The default there is
1 GiB, a quarter of the address space, which is more than most pages
embedding the viewer want to grant one document. -
Licence note for the embedded CMYK profile.
default_cmyk.icchas
been the public-domain (CC0)CGATS001Compat-v2-microprofile since
before 0.1.0, but only its filename — Ghostscript's — said anything about
where it came from, and a reader could take it for Ghostscript's
AGPL-licensed profile.LICENSE-CMYK-PROFILEin thestetand
stet-wasmcrates now records its source and terms, and the prebuilt
archives'THIRD-PARTY-NOTICES.txtcarries it.
Changed
-
stet_core::graphics_state::PatternDatahas a new public field,
shading_background: theBackgroundcolour of a shading pattern
(see Fixed). Code that builds aPatternDatawith a struct literal
needs the field. -
stet_core::graphics_state::GraphicsStatehas two new public fields,
clip_chainandclip_chain_stack(see Fixed, "A restored clip
forgot all but its last region"), with a newClipChaintype and the
methodsintersect_clip,set_clipandreset_clipto change the
clip through. Code that builds aGraphicsStatewith a struct literal
needs the two fields;GraphicsState::new()and..updates are
unaffected. As withoverprint_modein 0.8.3, the struct becomes
#[non_exhaustive]in 0.9.0. Code that setsclip_pathdirectly still
works: a state with a path and an empty chain is restored from the path. -
stetexits 1 when a job fails. A PostScript job stopped by an
error, a timeout or a memory limit was reported on standard error as
Job N FAILEDwhile the process exited 0, so a script, amakerule
or a CI step took a failed conversion for a good one. The status is now
1 if any job failed — the remaining files are still processed, as
before, and the closing line counts the failures (Processed 3 jobs, 1 failed). A status a program asks for with.quitwithcodestill wins,
quitis still a success, and the interactive viewer is unaffected.
Anything that ranstetover PostScript known to raise errors and
relied on a zero status needs to allow for the new one. -
A page that draws one gradient thousands of times renders in a tenth
of the time and a third of the memory. Some producers build a gradient
out of thin clipped strips, each a separateshof the same shading:
pdf.jsbug1721218_reduced.pdfdraws 41 shadings 3,531 times on one
letter page, and took 12.6 s and 2.6 GB at 72 dpi. It now takes 1.2 s
and 1.0 GB, and the output is byte for byte the same, as it is for the
first two pages of all 692 PDFs in the test set. Three things were
wrong. The renderer visited every pixel of a shading's bounding box —
the whole page, when it has no/BBox— before asking the clip whether
to paint it; it now finds the stretch of each row the clip lets through
first. Its cache of rasterised clip masks had no limit and held one for
each of the page's thousands of clips; it is now bounded at 32 MB. And
the reader parsed and sampled a shading's function afresh for every use;
the colour stops are now kept for the page. -
A very large JPEG 2000 image drawn small is decoded small. Where a
page is rasterised at once —render_page_to_rgbaand its variants, and
stet --device pngfor PDF input — a JPEG 2000 image the page draws at
half its stored size or less is decoded at a lower resolution level of
its codestream rather than in full and then scaled down. A 212-megapixel
scan (pdf.jsissue19517.pdf) rendered 3,152 pixels wide went from
8.6 GB and 8 s to 0.6 GB and 1.1 s. The level is chosen to be at least
the size drawn and to keep the picture within a quarter of a device
pixel of where the full image would put it; a soft mask is averaged down
to match. Pixels of such pages differ slightly from before: the
wavelet's own smaller image is not the same filter as averaging blocks
of the large one. Images with a/Mask, palette images, images inside
tiling patterns or Type 3 glyphs and images in encrypted files are
decoded in full as before, and so is everything in a display list from
render_page, the viewer and PDF output, which stay independent of
resolution. -
Resolver::data()returns&[u8]borrowed from the resolver, where
it returned&'a [u8]borrowed from the file. A resolver may now own
its bytes, so the longer lifetime can no longer be promised. Code that
uses the slice while it holds the resolver is unaffected; code that kept
the slice after letting the resolver go needs to keep the resolver, or
its own reference to the file. -
What the reader skips, it now reports.
stet-pdf-readercarries on
past most of what is wrong with a page, which is right, but several of
the things it carried on past left no trace: the page simply had
something missing. These are now inPdfDocument::parse_warnings():- An operator that fails. It is skipped and the stream goes on, as
before, with aParsePhase::Contentwarning naming it:operator Do: object 12 0 not found,operator Do: decompression error: …,
operator sh: sampled function has 18 inputs (1 to 16 supported),
operator c: need 6 operands, have 4. So an image that will not
decode and a shading whose function is refused both say so. - A form, pattern, glyph procedure or soft-mask group whose stream
ends in an error, and a page content stream that cannot be
decoded when the page has others that can. - A page tree that loses pages, under the new
ParsePhase::PageTree:
nested more than 256 levels deep, or a node listed twice. Such a
document could open with no pages and no explanation.
A page keeps at most 64 distinct content warnings, then one saying the
list is cut short, and a message repeated by every operator in a stream
costs a hash lookup. Nothing is drawn differently. Thestetcommand
line prints these, so a file that rendered silently with something
missing now says what: of 1,681 test PDFs, 29 gainwarning:lines (at
most 28 each), most of them paths and text operators with the wrong
number of operands. - An operator that fails. It is skipped and the stream goes on, as
-
stet-pdf-readerno longer prints to stderr. A content stream
error, a font or soft mask that would not load, a missing predefined
CMap, a page cut short by the nesting budget and a CCITT image that
needed a fallback were each written straight to the terminal of
whatever application linked the library. They are now recorded in
PdfDocument::parse_warnings()under the newParsePhase::Content,
with the page as the warning's location, when the page is first
rendered; rendering it again adds nothing. Two of them were silenced
after their first occurrence in the whole process, so a second document
with the same fault said nothing; each document now has its own record.
Thestetcommand line prints them itself, so its output is unchanged
apart from the wording of the two CCITT messages. Also new:
Resolver::warnings()andWarningSink::record_once. A lint keeps
the library from printing again. -
An annotation flagged
NoViewis no longer drawn. The flag means
"do not display on screen"; it marks print-only content such as a
watermark that should appear on paper alone. stet honouredHiddenand
ignoredNoView, so such an annotation showed in every render. A render
is now for viewing unless told otherwise, and
set_annotation_filter(AnnotationFilter::new(RenderIntent::Print))gives
the print reading, in which these annotations are drawn. -
The minimum supported Rust version is now 1.92 (was 1.88). The JBIG2,
CCITT and JPEG 2000 decoders stet uses —hayro-jbig2,hayro-ccittand
hayro-jpeg2000— were two to three releases behind, held there by the
old floor: their current versions need 1.92. They are now 0.3.1, 0.4.0
and 0.4.1. A JBIG2 image is also packed to one bit per pixel as it is
decoded, where it used to pass through a one-byte-per-pixel copy first.
The 212-megapixel JPEG 2000 page of pdf.js'sissue19517.pdfpeaks at
8.6 GB where it took 11 GB; that is still far too much, and is not
finished work.
Fixed
-
A gradient mesh from Illustrator raised
typecheckinshfill. A
mesh or patch shading (ShadingType 4 to 7) may take itsDataSource
from a file as well as a string or an array (PLRM 3, 4.9.3), and
Illustrator writes a gradient mesh that way: the vertex data follows
the dictionary in the program, behind a filter oncurrentfile. stet
accepted only the string and the array. A file is now read to its end,
from where it stands, and painted as the same bytes in a string would
be. Reported by @vivozi (#6), with a reproducer and the place in the
source. Two more of the same kind, found beside it:- A sampled (Type 0) function may likewise take its samples from a
positionable file; aReusableStreamDecodefilter, the way in-line
data becomes one, was atypecheckthere too. The samples are read
from position 0, as the manual says, wherever the file was left. setfilepositionraisedioerrorfor aReusableStreamDecode
filter, the one filter that exists to be positioned. A program that
paints one reusable stream twice repositions it in between, and could
not.
- A sampled (Type 0) function may likewise take its samples from a
-
Four ways a damaged PDF could stop the process, found by fuzzing
stet-pdf-readerand present in every build:- An embedded OpenType font whose
CFFtable lies past the end of
the font panicked: the table was sliced at its declared offset and
length without checking either. It is now read as far as the data
goes. New:stet_fonts::truetype::table_data. - A mesh shading (Types 4 to 7) with a short or missing
/Decode
panicked, and one whose coordinates and colour components were
declared zero bits wide never finished reading, taking memory until
none was left. A layout that cannot describe a vertex is now refused,
with a warning from the reader andrangecheckfromshfill. New:
stet_graphics::mesh_shading::mesh_layout_is_readable. - An image smaller than its soft mask, with less data than its
dimensions promise, panicked while being enlarged to the mask's
size. The missing samples are read as zero. - A TrueType glyph whose contours end out of order could claim all
of its points again for every contour, so a 256 KB file asked for
3.7 GB. Such a contour is now empty, and a composite glyph is held to
the 65,535 points the format allows in total.
- An embedded OpenType font whose
-
Three ways a small damaged PDF could take a very long time, found
by fuzzingstet-pdf-reader:- A font that cannot be resolved, used inside nested Type 3 glyphs,
was resolved again, and its substitute read again, for every glyph
shown: a 7 KB file took fifteen seconds. The substitute is now
remembered for the font object and not only for its resource name. - An object stream that cannot be read was decoded again for every
object the cross-reference table placed in it, so opening a file with
a costly broken stream took eleven seconds. It is now tried once. - The limit on a page's nested content counted streams, not their
length. A long form or glyph procedure run thousands of times went
almost uncharged — 23 seconds on 6% of the allowance, and a quarter
of an hour had it all been spent. The charge now grows with the
stream's length beyond 4 KB, which bounds a page at about 4 GB of
nested content; shorter streams, which is every Type 3 glyph and
small form, cost what they did. No page of the 2,690 PDFs the limit
was first measured on reaches it.
- A font that cannot be resolved, used inside nested Type 3 glyphs,
-
A tiling pattern painted under a soft mask ignored the mask. In
stet-pdf-reader, a soft mask set withgscovers what the stream
paints from then on, which the reader tracks as a scope over its
display list. A tiling pattern's cell is interpreted into a display
list of its own, and the scope was left open across that: it was closed
inside the cell, around the cell's own contents, and the fill or stroke
that used the pattern was painted whole. A page that set a mask and
then filled with a tiling pattern — a patterned shape with a feathered
edge, text punched out of a texture — showed the pattern across the
whole shape, and the pattern's cell could lose part of its own content
for every later use. A Type 3 glyph procedure that sets a soft mask of
its own had the same fault, dropping the mask from everything painted
before the glyph. The cell and the glyph procedure are now interpreted
with no scope open, and the page's scope carries on after them. Shading
patterns were not affected. -
A restored clip forgot all but its last region. The clip is the
intersection of every region set sinceinitclip, but the interpreter
kept only the most recent one. Nothing went wrong while clips only
narrowed; once agrestore,restore,setgstateorcliprestore
brought an earlier clip back, it came back as its last region alone, so
a page that clipped twice and then ran any nestedgsave … clip … grestorepainted outside the first clip from then on. Aneoclipalso
came back as a non-zero winding clip, filling its holes. Every region is
now kept, with its rule, and the restored clip is rebuilt from all of
them. Successive rectangles are kept as the one rectangle they share,
whichclippathnow returns in place of the last rectangle set. -
A PostScript shading pattern painted nothing, and a tiling pattern
painted fills only. A pattern is a paint, used by whateverfill,
strokeandshowmark (PLRM 3, 4.9). A shading pattern
(PatternType 2) was accepted bymakepatternandsetpatternand
then drew nothing at all, for any shading type; a tiling pattern was
honoured byfill,eofillandrectfill, while a stroke, text or a
stencil mask set in it came out in a solid colour. Now, for both kinds:- Fills, strokes (
stroke,rectstroke) and text are painted with
the pattern. A patterned stroke is painted as the area the stroke
covers and patterned text as its outlines, so every output device sees
an ordinary pattern fill; in PDF output such text is therefore
outlines rather than text. - A shading pattern stays where
makepatternplaced it, whatever
the current matrix when it is used, paints its shading'sBackground
across what it fills, and reads a mesh'sDataSourcefrom the start
of a positionable file each time one is made. imagemaskis painted with the pattern too, where it had used a
solid colour: the pattern shows through the mask's painted samples.makepatternno longer disturbs the graphics state.PaintProc
ran in the program's own state, so the colour, line width and path it
set stayed set afterwards and the current path was lost; and a cell
made while another pattern was current was painted with that pattern,
which turned an uncoloured pattern's cell into stripes of the wrong
colour. It now runs between an implicitgsaveandgrestore, with
no pattern current.
- Fills, strokes (
-
A JPEG 2000 image whose own header claims an absurd size aborted the
process.stet-pdf-readerchecks an image's/Widthand/Height,
but a JPEG 2000 stream carries its own, and those are the ones the
decoder allocates for. A real 25 KB image with its header rewritten to
2³¹ × 2³¹ samples, in a dictionary still saying 512 × 384, asked for
1.8 × 10¹⁶ bytes: an allocation failure, which no caller can catch. The
size in the stream is now held to the limits every other image is
(100,000 samples a side, 4 × 10⁹ in all), and an image over them is
refused with a warning. Found while measuring where the memory goes on a
212-megapixel JPEG 2000 page. -
A CMap with a byte that is not UTF-8 inside a hex string panicked.
stet-pdf-readerreads CMap streams as text, so such a byte becomes a
three-byte replacement character, and two places then cut the hex string
at a fixed byte offset — inside the character. A/ToUnicodestream with
<0041> <ÿÿÿ>did it, and so did an embedded encoding CMap with a
codespace range of<ÿ>; both are a panic in a release build, on opening
the page. Found by the fuzzer. Such a string is now not a hex string, and
the entry is skipped. -
A PDF function could abort the process, panic, or take all the memory
there is. Functions colour every shading and convert every spot colour,
andstet-pdf-readertook their dictionaries at their word. Found by the
fuzzer, then by reading the rest of the file; each of these is a PDF
under 1 KB:- A sampled (Type 0) function whose
/Sizeis[65535 65535 65535]
asked for its whole table in one reservation, 2.2 × 10¹⁵ bytes, and the
allocator aborted the process — which no caller can catch. One
entry of two billion, or-1, filled memory instead. The table now
holds the samples the stream contains; one the stream does not reach
reads as zero, as it did before. /Size []panicked on an index; an entry of0, or of1with two or
more inputs, and a/BitsPerSampleof 64, overflowed (a panic in a
debug build)./Sizeentries must now be positive, one per input, and
/BitsPerSampleone of the eight values the format allows; a function
that fails either is refused, and what it would have coloured is not
drawn.- A sampled function with more than 16 inputs is refused, as poppler
refuses it: evaluating one point reads 2^m samples. One of the pdf.js
test files,axial_shading_many_inputs.pdf, has 18; it drew a black
page and now draws nothing, as poppler and Ghostscript do. - A calculator (Type 4) function has no loops, but
2 copy 4 copy 8 copy …doubles its operand stack with each pair of tokens: forty of them
took 4 GB. The stack now stops at 100 operands, the figure in the
standard, which poppler and pdf.js also use. idivandmodby a number between −1 and 1 divided by zero and
panicked in a release build, since both work on truncated
integers; they give 0, as division by zero already did.bitshiftby
64 or more, and a function leaving fewer results than it has outputs,
no longer overflow.
Every valid file renders as before: two pages of each of 1,681 PDFs are
byte-identical, bar the one above. - A sampled (Type 0) function whose
-
A PostScript program could raise
--max-vm.MaxLocalVMis a user
parameter, and<< /MaxLocalVM 2000000000 >> setuserparamsreplaced
whatever ceiling the command line had set — so--max-vm 16bounded only
a program that left it alone. The host's ceiling now holds: a program may
lower its limit and raise it again, but not past what the host allows,
andcurrentuserparamsreports the limit in force rather than the
request. The built-in 8 GiB default is a ceiling in the same sense; pass
--max-vmto allow more. Library users get the same guarantee from
Context::max_local_vm, andContext::vm_limit()returns the limit in
force. -
A TrueType or OpenType font among the embedded resources never
loaded.resources/Font/may hold.ttfand.otffiles beside the
Type 1 ones, but they were read from disk even where the resources are
embedded in the binary and there is no disk — a WebAssembly build. None
of the 35 bundled fonts is affected, being Type 1; this matters to a
build that embeds fonts of its own. -
A large image drawn small no longer costs its full size in memory, and
an image with a soft mask no longer costs it once per band. Rendering
converted every image to RGBA at full size before scaling it down, and
an image inside a soft mask or a transparency group — every PDF image
with an/SMaskis one — was converted again by each band that touched
it, with bands running in parallel. A 4500×6442 image with a soft mask,
rendered 800 pixels wide, peaked at 1,075 MB on 24 threads; it now peaks
at about 156 MB on any number, most of which is the decoded image
itself. Four changes, none of which alters a pixel:- images inside soft masks and transparency groups are converted once
per page, like images outside them; - an image that is scaled down is converted a strip of rows at a time,
straight into the scaling filter, and never held at full size; - a page's images are converted in parallel;
- an image drawn several times on a page — a logo, a texture — is
converted once for all the placements that come out as the same
pixels. That image drawn nine times renders in a quarter of the time
on 24 threads and a tenth of it on one.
One cost: converted images inside groups and soft masks are now held
until the page is finished, so a single-threaded render of a page with
many of them that are not drawn small can peak higher than before. - images inside soft masks and transparency groups are converted once
-
A colour key on a 16-bit image cleared the wrong pixels. The
renderer looked up each pixel's key one byte per sample, so with
bits_per_component: 16it tested low bytes and neighbouring pixels'
samples. It now tests each sample's high byte, the value the pixel is
painted with. stet's own PostScript and PDF front ends never produce
such an image (they reduce samples to 8 bits first), so only a display
list built elsewhere was affected.docs/DISPLAY-LIST.mdnow says what
sample layouts and keys a renderer is given ("Sample depth"); it listed
depths of 1, 2 and 4 bits that no display list carries. -
A PDF image declaring 9 to 15 bits per component rendered as noise.
PDF allows 1, 2, 4, 8 and 16; the reader unpacked the first three and
reduced 16-bit samples, but handed any depth in between on as packed
bytes, which were then read as 8-bit samples. Such an image, or soft
mask, is now unpacked bit by bit and reduced to the nearest 8-bit
value, as the other depths are, and a colour key or/Decodeon it
applies as it does to them. Ghostscript draws nothing for these files
and poppler keeps the low 8 bits of each sample. -
A PDF cut off just after its
startxrefkeyword did not open. The
reader took the keyword as a promise of an offset and failed with
"expected integer" when none followed, though a missing keyword was
already handled. Such a file now opens the same way: from the
cross-reference table when one is there, otherwise by scanning for its
objects. Found withissue6069.pdffrom the pdf.js test set. -
A PDF page with a zero-size MediaBox crashed the PNG writer. A page
declaring/MediaBox [0 0 0 0]rendered to a 0 × 0 image, andstet --device pngpanicked writing it. A MediaBox that encloses no area is
now treated as a missing one always was, and replaced by US Letter; a
CropBox with nothing left inside the MediaBox — empty, or lying wholly
outside it — is replaced by the MediaBox. Each replacement is reported
byPdfDocument::parse_warnings, asParsePhase::PageBoxes. The
command line also reports a page too small to have any pixels at the
chosen resolution as an error and exits 1, where it would have
panicked. Found withboundingBox_invalid.pdffrom the pdf.js test set. -
A DeviceN colour space with many colourants could exhaust memory, in
PDF and in PostScript. A tint transform is sampled into a table of
samples ^ colourantsentries, and the colourant count is the file's to
choose. A 1.1 KB PDF with an axial shading in a nine-colourant space took
4.2 GB to open; eight colourants cost 540 to 690 MB for each such space;
and in PostScriptsetcolorspaceran the tint transform procedure once
per entry, 387 million times for nine colourants. Every such table now
shares one ceiling,TintLookupTable::MAX_GRID_POINTS, sized by
TintLookupTable::grid_samples. Up to seven colourants nothing changes.
Eight get six samples a side where they had nine (eight, in a shading).
Beyond eight there is no table: colours are computed from the tint
transform itself, which is exact — a fill or stroke as before, an image
pixel by pixel, an Indexed palette entry by entry — and what is given up
is the spot identity of that colour in--device pdfoutput, which is
written in the alternate space.TintLookupTable::lookup_1dand
lookup_ndalso return zeros for a table they cannot interpolate, where
lookup_ndindexed out of bounds for more than eight inputs. Found with
postscript_type4_many_outputs.pdffrom the pdf.js test set. -
A small PDF could ask for an unbounded amount of nested content.
Form XObjects, tiling patterns, Type 3 glyphs, soft-mask groups and
annotation appearances may nest twenty deep, and nothing limited how many
ran: a form drawing itself eight times, or a Type 3 glyph showing three
copies of itself, ran until the process was killed, and so did nineteen
ordinary forms each drawing the next twice. Two things bound it now. A
stream that is already being interpreted is not entered again — a form
cannot draw itself, so there is nothing to draw — which makes every
self-referencing file finish at once. And each page has a budget for
nested content, charged by depth, 115 times what the busiest of 33,567
real pages used; a page that spends it draws what it had drawn, warns,
and stops. The files that used to be killed now take up to a second and
1.2 GB, which is the display list of what they did draw.
Reuse is unaffected: a form or glyph used any number of times in a row is
drawn each time. -
An AES-256 PDF did not open when its password needed Unicode
preparation. PDF 2.0 has the writer run a password through SASLprep
(RFC 4013) before hashing it: compatibility forms are folded, characters
such as the soft hyphen are dropped. stet hashed the bytes as typed, so
a password entered with a full-width letter, a decomposed accent or a
non-breaking space was refused although it was the right one. The reader
now prepares the password and tries that first, then the bytes as given
for files whose writer skipped the step; both are cut to the 127 bytes
the algorithm reads. The preparation is the newsaslprepfeature of
stet-pdf-reader, on by default; it brings in thestringprepcrate and
its Unicode tables (about 170 KB in thestetbinary), and a build
without it behaves as before. RC4 and AES-128 files are unaffected. -
A PDF's owner password did not open it. An encrypted PDF has two
passwords, and either one decrypts the file. stet tried the password it
was given only as the user password, so--passwordwith the owner
password, orPdfDocument::from_bytes_with_passwordwith it, was refused
as wrong at every revision of the Standard security handler. It is now
tried as each, user first: for RC4 and AES-128 files by decrypting/O
to recover the user password, for AES-256 files against the owner hash
and the key wrapped in/OE. -
A password with an accented letter did not open an RC4 or AES-128
PDF. Those security handlers store the password in PDFDocEncoding —
Latin-1 for accented letters, its own codes for a few characters such as
the euro sign — and a caller passes what the user typed, which is UTF-8.
The two agree only for ASCII, so--password æøåwas refused on a file
whose password is exactly that. The bytes as given are still tried
first; when they are UTF-8 with something beyond ASCII, the
PDFDocEncoding form is tried after them, for the user and the owner
password alike. AES-256 files, which store UTF-8, already worked. -
An encrypted PDF stating an impossible key length panicked. The
/Lengthin an/Encryptdictionary was used as the key size without a
check: zero divided by zero in RC4, and anything over 128 bits read past
the end of a digest. The value is now held to the 40 to 128 bits the RC4
and AES-128 handlers define, so such a file is refused as needing a
password, or opens if its key works at the nearest valid length. -
A
/Matteimage whose soft mask was cut short panicked. An image
premultiplied against a matte colour is divided back out by its soft
mask; when the mask's stream ended early the loop read past its last
sample. Pixels with no mask sample are now left as they are. Found in a
public PDF corpus.
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.