Repository navigation
stet 0.8.4
Added
rendering_intenton the shading parameter structs
(AxialShadingParams,RadialShadingParams,MeshShadingParams,
PatchShadingParams): the intent the shading's colours were converted
with, for renderers that convert them again. Code that builds these
structs with a struct literal must add the field (or use
..Default::default()); as withGraphicsState.overprint_modein
0.8.3, the policy is that outside code reads these structs rather than
writes them.color_lut_componentsonMeshShadingParamsand
PatchShadingParams: the shading's function sampled at the
color_lutinputs as components of the shading's own colour space —
gray, spot tints, CMYK — before conversion, for writers that emit the
function again. Struct literals must add it (or use
..Default::default()), under the same policy.- CMYK conversions that take a rendering intent:
IccCache::convert_cmyk_with_intent,convert_cmyk_readonly_with_intent
andDeviceColor::from_cmyk_icc_with_intentinstet-graphics.
convert_color_readonly_with_intent,convert_color_with_intentand
convert_image_8bit_with_intentnow honour the intent for CMYK profiles;
the intent-less conversions are relative colorimetric, as before. --default-intent relative|perceptual|saturation|absolute: the
rendering intent pages start with, for PostScript and PDF input, in
place of relative colorimetric. A document that selects an intent — an
rioperator, an ExtGState/RI, an image's/Intent,
setrenderingintent— still gets it; this is a default, not an
override. With most press profiles the intent picks a different CMYK
table, so--default-intent perceptualgives the colours lcms2-based
tools such as ImageMagick produce by default. In the library:
PdfDocument::set_default_rendering_intentinstet-pdf-reader,
InterpreterBuilder::default_rendering_intentinstet, and
Context::set_default_rendering_intent/Context::initial_gstatein
stet-core, taking the newRenderingIntentenum
(stet_graphics::rendering_intent, re-exported by both).GraphicsState.icc_componentsinstet-core: the components of
the current colour in an ICCBased space, kept sosetrenderingintent
can convert them again. Adding it breaks struct literals of
GraphicsState, under the same reader-not-writer policy.FileEntry.global,FileEntry.created_after_save,
FilterState.owns_sourceandVmMarks.filesinstet-core: a file
now belongs to local or global VM, sorestorecan close and reclaim
the local files a save level opened, and a filter knows whether closing
it closes its source. These are interpreter internals that happen to be
public; adding the fields breaks struct literals of the four types,
under the same reader-not-writer policy.SkiaDevice::set_bpc_modeandbuild_icc_cache_for_list_with_bpc
instet-render: the black-point compensation mode for colours the
renderer converts, whichbuild_icc_cache_for_listleaves at the
default.Context::newerrorinstet-core: whether a job that ended in
PsError::Stopended on an error or onquit, which also stops.ViewerOptionsandrun_viewer_with_optionsinstet-viewer:
the viewer's settings as a struct, with the newbpc_modeamong them —
the black-point compensation of the colour cache the display lists were
built with, which the viewer must convert images and overprint with too.- Render a page box, or a region, of a PDF page as the page:
--box
on the command line andPdfDocument::set_page_areain
stet-pdf-reader.--box art(ormedia,crop,bleed,trim)
renders that box;--box llx,lly,urx,uryrenders a rectangle in PDF
points in the page's unrotated space. The area becomes the page — its
size is the output size,/Rotateapplies to it, and--width,
--heightand--transparentwork on it — and only its pixels are
rasterised, so cropping placed artwork from a large page no longer means
rendering the whole page. A box the page does not declare is its crop
box, and areas are clipped to the MediaBox rather than the crop box, so
a bleed renders. For PDF input, with--device pngor--device pdf.
PageArea,PdfDocument::page_area_rectand
PdfError::EmptyPageArea(an area that misses the page) are new. stet_graphics::image_samples:to_8bit, the nearest 8-bit value
of a sample of any depth, andColorKey, a colour-key mask tested on
samples as encoded — what both front ends now use.transferonImageParamsand the shading parameter structs
(AxialShadingParams,RadialShadingParams,MeshShadingParams,
PatchShadingParams): the transfer function in force, asFillParams
andStrokeParamsalready carry it, andTransferState::rgb_tables,
apply_rgb,rgb_luts,apply_to_rgbaandtransfer_lookupin
stet-graphicsto apply one. The renderer now applies the field (see
Fixed); a third-party renderer should apply it to the final RGB of fully
opaque paints. Struct literals must add the field, under the same
reader-not-writer policy asrendering_intent.PdfDevice::in_memoryinstet-pdf: a PDF device whose document is
taken as bytes (take_pdf_bytes_with_context) rather than written to a
file at the end of the job. It derives no output path from page names,
and writes a/Titleonly when a pdfmark gives one.- Font copies for devices that read fonts after the page is sent:
OutputDevice::keeps_text_fontsandContext::font_snapshotsin
stet-core(modulefont_snapshot),TextParams::font_snapshotin
stet-graphics. While the device asks, the interpreter copies each font
text is shown with before anyrestorecan reclaim it or revert glyphs
added to it, and marks theTextelement with its copy. The font's
font_entityis valid only while the font lives. stet_ops::remove_pdf_authoring_ops: takespdfmark,
setdistillerparamsandcurrentdistillerparamsback out of
systemdict, for a context that runs screen jobs after PDF ones.
register_pdf_authoring_opsmay now be called again; it adds nothing to
the operator table the second time.OutputDevice::resize_pageinstet-core:setpagedeviceoffers
a new page size to the current device before replacing it with one from
the device factory. A device that holds the job's pages until the end
takes the size and returns true; the default, false, keeps the old
behaviour. A new device from the factory is told its size the same way.PdfDevice::set_page_size_in_pointsinstet-pdf: the next page's
size in points, for a display list drawn at exactly that size, as
stet_pdf_readerrenders a page.set_page_sizetakes whole device
pixels, which rounds a page such as A4.Context::set_gstate_objectinstet-core: replace agstate
object's state so thatrestorecan revert it, backing up what the
object held if it predates the currentsave. Writegstate_store
through it, as dictionaries are written throughdict_put_cow.
Deprecated
stet_viewer::run_viewer, forrun_viewer_with_options. It has no
way to take the black-point compensation mode, so it keeps the default;
it will be removed in 0.9.0.
Fixed
-
CMYK colours, images and shadings in PDF honour the rendering
intent. stet converted every CMYK source — DeviceCMYK and ICCBased
alike — through its profile's relative colorimetric table whatever the
document asked for, so/Perceptual ri, an ExtGState/RIor an
image's/Intentchanged nothing — and most press profiles (ISO
Coated v2, FOGRA39, Japan Color 2001, SWOP) have perceptual tables that
differ. Each intent now reads the table lcms2, and so Ghostscript, uses:
A2B1for relative colorimetric,A2B0for perceptual,A2B2for
saturation, with black-point compensation as lcms2 applies it
(perceptual and saturation always; a profile with noA2B2gives
saturation itsA2B0, uncompensated). Absolute colorimetric renders as
relative colorimetric, as before. Content that states no intent is
relative colorimetric, as before. Each extra table is built the first
time a page uses it. Anriafterk/Kapplies to the colour
already set, as it does aftersc/scn. Overprint follows the intent
of the element painting; transparency groups composited in CMYK stay
relative colorimetric.This applies where the CMYK profile is a source:
--cmyk-profile, the
system profile, or an ICCBased CMYK colour. In a PDF/X document the
output intent's CMYK is the output, so DeviceCMYK, and anything
converted into the output intent, is shown the same way whatever the
intent; there the intent governs the conversion into the output
intent (ICCBased and Lab colours), as before. The Ghent Workgroup's
output-intent tests (GWG 13.0, 22.1) depend on that. -
CMYK colours, images and shadings in PostScript honour
setrenderingintentthe same way. PostScript has no output intent, so
the CMYK profile is always a source:setcmykcolor, colours in ICCBased,
Indexed, Separation and DeviceN spaces that convert through a CMYK
profile, shadings and images now take the current intent, and
setrenderingintentconverts the current colour again, asridoes in
PDF and as Ghostscript does. -
ICCBased CMYK colours in a PDF/X document ignored the rendering
intent. In a document with an output intent, an ICCBased CMYK colour,
image or shading — FOGRA39 content in an ISO Coated v2 document, say —
is converted into the output intent and shown through it, and that
conversion is where the intent applies. stet made it with moxcms's
perceptual transform whatever the content asked for, including at the
default, relative colorimetric. It now reads the intent's tables on both
sides as lcms2 does without black-point compensation — within 0.06 of
255 ink levels on the press profiles in the corpus — so relative
colorimetric content moves by 3–12 levels on average and up to 69, and
saturation by about 30. A profile stet cannot read itself (one with an
XYZ PCS) goes through moxcms's transform for the intent, about one ink
level from lcms2.IccCache::convert_colorandconvert_image_8bit,
which take no intent, now convert RGB and CMYK sources in a proofing
chain relative colorimetric rather than perceptual. Overprint and CMYK
blending still use an ICCBased CMYK colour's own numbers, as GWG 16.4
expects. -
ICCBased Gray colours in a PDF/X document ignored the rendering intent
and black-point compensation. moxcms could build the conversion into
the output intent only as absolute colorimetric, so that is what every
Gray colour got. Gray now converts as lcms2 does:- by its intent;
- with black-point compensation when
--bpcis on, the default, from
the gray's black to the output intent's black, as lcms2 detects it
for a destination (cmsDetectDestinationBlackPoint, which stet now
implements); - into an ICC v4 output intent, compensated under perceptual and
saturation whatever--bpcsays, as lcms2 does.
The Ghent Workgroup's 16-bit gray image test (GWG 18.3) made its
reference that way. Its X, which showed at 3.4 sRGB levels on average and
up to 36, is now at the 1-level noise of a test that compares an image
with itself. With--bpc offit shows at about 6, which is lcms2's
answer without compensation. Gray profiles with a LUT or a Lab PCS keep
the old conversion; every Gray profile under an output intent in the
test corpus is the usual tone-curve kind. -
ICCBased RGB colours with a pure-gamma profile went wrong in a PDF/X
document wherever a channel was zero. Converting an RGB colour into
the output intent runs it through its profile's tone curves. When a
curve is a single gamma, as in Apple RGB, ColorMatch RGB or any other
γ 1.8 profile, stet evaluated it with moxcms's approximate power
function, which returns garbage at 0. Pure red or a saturated green then
collapsed, up to 240 ink levels from lcms2 (42 on average) on the γ 1.8
profiles GWG 13.0 embeds; no page in the test corpus happens to paint
such a colour. Tone curves now evaluate as lcms2 does: a pure gamma
exactly, a table with lcms2's 16-bit interpolation, and parametric
curves by lcms2's own formulas. The table change moves a few hundred
pixels of sRGB and eciRGB content in the corpus by one level.RGB profiles built on tables also follow lcms2 now: the table comes from
the intent's tag, elseA2B0, else the tone curves and matrix (rather
than any table the profile has), and it is interpolated tetrahedrally
rather than trilinearly. ICC v2lut8Typetables are read too. -
--bpcnow reaches ICCBased RGB and CMYK colours, and Lab, in a PDF/X
document. Converting them into the output intent ignored black-point
compensation, so--bpc on, the default, meant one thing for Gray and
another for everything else. Ghostscript and Acrobat both compensate
this conversion by default. It now compensates as lcms2 does: from the
source's black, as lcms2 detects it, to the output intent's.- Relative colorimetric RGB gets lighter, more open shadows: about 6
sRGB levels on average and up to 40, compressed into the press's
range instead of clipped at its ink limit. This is the default intent,
so it covers most PDF/X files made from RGB artwork. Tagged CMYK moves
by 1.6–9 levels on average. - Perceptual barely moves: into an ICC v2 output intent lcms2 drops
a compensation that small. Into an ICC v4 output intent it compensates
perceptual and saturation whatever the flag, and so does stet. - The Ghent Workgroup's X marks fade: GWG 17.2's, which showed at 6
levels, is gone, and GWG 16.1's are fainter. Every region the change
moves comes closer to Ghostscript's output. - What moves: what is shown, the ink recorded for CMYK blending, and
a Lab colour's ink (only its ink: Lab is shown without the output
intent). - Gray: it now also skips a compensation too small for lcms2 to
apply. - Not compensated: a CMYK profile stet cannot read itself (one with
an XYZ PCS), which still goes through moxcms. --bpc offkeeps the previous output.
- Relative colorimetric RGB gets lighter, more open shadows: about 6
-
ICC v4 profiles built on
lutAToBType/lutBToATypetables now convert
into a PDF/X output intent as lcms2 does, with black-point
compensation. This covers such a profile as the source, such as the
Ghent suite's estprofile.icc, and as the output intent. stet used to
hand these tables to moxcms, and three things went wrong:- Accuracy: moxcms was up to 19 ink levels from lcms2 without
compensation and 31 with it. - Compensation: none was applied.
- Black point: a v4 press profile's relative colorimetric black
point fell back to 400% ink.
stet now evaluates the tables itself, within 0.12 of 255 ink levels of
lcms2 on estprofile, including that black point. The black point is
also what CMYK display through such a profile compensates from.moxcms 0.8.1 misreads a set of curves in these tables that holds an
identity (empty) curve before another: every curve after it comes back
as identity. stet reads them from the profile's bytes. Only GWG 20.5's
image moves in the test corpus, by up to 5 levels, toward Ghostscript. - Accuracy: moxcms was up to 19 ink levels from lcms2 without
-
CMYK shown through an ICC v4 or 8-bit (
lut8Type) profile now matches
lcms2. This covers such a profile as--cmyk-profile, as a PDF/X
output intent, or as ICCBased CMYK. stet used to bake its display
tables from moxcms, which was:- up to 3.5 levels from lcms2 (0.9 on average) through the Ghent
suite's v4 estprofile.icc; - up to 14.7 levels (2.0 on average, 41% of colours more than 2 off)
through an 8-bit profile built from FOGRA39; - wrong altogether on some v4 tables, 150 levels off on average on one
of stet's test profiles; - unable to build a transform from a v4 table whose grid differs per
input, so the profile's CMYK fell back to the uncalibrated PostScript
formula.
stet now bakes these tables through the evaluator its proofing chain
already uses, within half a level of lcms2: the same aslut16Type
profiles, which are unchanged. Nothing in the test corpus moves. - up to 3.5 levels from lcms2 (0.9 on average) through the Ghent
-
CMYK shown through a profile with an XYZ PCS now matches lcms2.
Such profiles include Ghostscript's ownps_cmyk.iccand
gray_to_k.icc. stet read only Lab-PCS tables itself and left these to
moxcms, which was up to 17 levels from lcms2 throughps_cmyk.iccand
4.4 through the ICCBased CMYK in the test corpus's5403.pdf. They are
now within half a level, black-point compensation included:- a black point that needs the profile's
B2A0now uses it; - a three-input table's matrix is now applied, as lcms2 applies it.
5403.pdfmoves by up to 3 levels, toward Ghostscript. - a black point that needs the profile's
-
Profiles with an XYZ PCS now convert into a PDF/X output intent as
lcms2 does, and serve as one. This covers CMYK, RGB (camera and
scanner profiles are usually XYZ-based) and Gray sources, and Lab. They
used to go through moxcms, with no black-point compensation. -
Matrix RGB colours (sRGB, Adobe RGB and the like) in a PDF/X document
convert into the output intent as lcms2 does. stet took their XYZ to
Lab with a D50 white 0.0003 off lcms2's in Z, which tinted every colour
by a few hundredths of a b*. In the test corpus, two pages of the PDFX
output test and one GWG transparency page move by up to 4 levels, on
0.1% of pixels at most. -
An RGB profile with only some of its A2B tables, and no colorant
matrix, is no longer compensated under an intent it has no table for.
This is common in camera profiles, which carryA2B0alone. lcms2
treats such an intent as unsupported and its black as zero; stet took
A2B0's darkest colour instead. -
RGB composited in CMYK, and RGB colour in a DeviceCMYK page group,
convert to CMYK as lcms2 does. Both take the colour stet displays back
to the CMYK profile: the renderer's CMYK buffer, which CMYK blend modes,
non-isolated CMYK groups and overprint simulation read, for a paint with
no CMYK of its own; and a PDF page whose transparency group is
DeviceCMYK, where anrgcolour goes to CMYK and back. moxcms did it,
with no black-point compensation, so with--bpcon (the default)
black landed on 400% ink rather than the profile's black, and it was up
to 150 ink levels wrong through ICC v4 and XYZ-PCS profiles. It now
inverts stet's display into the profile'sB2A1, compensated as--bpc
says, within 0.1 ink levels of lcms2, and the page group shows that ink
as DeviceCMYK is shown. In the test corpus, three PDFs with a DeviceCMYK
page group move by up to 14 levels, their flat colours now Ghostscript's. -
ICCBased RGB colours and images whose profile has A2B tables honour
the rendering intent and black-point compensation, and display as lcms2
does. Scanner, camera and RGB printer profiles carry a table per
intent. stet used the perceptual table whatever the document asked
for, though PDF's default is relative colorimetric, and compensated
nothing. It went through moxcms, which also misreads some ICC v4
tables: up to 92 levels off. These profiles now go through stet's own
evaluators, within a quarter of a level of lcms2, and each image pixel
is exactly the colour of its value. Matrix-shaper profiles (sRGB,
Adobe RGB, Display P3) are unchanged. In the test corpus, one scanner
image in2142.pdfmoves by a level. -
16- and 12-bit image samples are rounded to 8 bits, not truncated.
stet carries 8 bits per sample and cut deeper ones to their high bits,
so every 16-bit image, in PDF and PostScript, and every 12-bit
PostScript image came out up to a level dark before conversion, up to 3
after a CMYK profile. They now take the nearest 8-bit value, within a
level of Ghostscript, which converts them at full precision. 16-bit PDF
soft masks are rounded too. Indexed images keep their indices. In the
test corpus,issue6289.pdfand three pages of the two PDFX output
tests move, by up to 2 levels. -
Colour-key masks mask the samples the file names. A PDF image's
/Maskarray and PostScript ImageType 4'sMaskColorgive ranges of
samples as encoded: at the image's own bit depth, before/Decode.
stet tested them on the samples after expansion to 8 bits and
/Decode, and:- masked the wrong pixels, or none, on a 1-, 2- or 4-bit image or one
with a/Decode: a 4-bit key of 15 missed samples of 15, which stet
held as 255; - kept the low byte of each value of a 16-bit PDF key, so
[32768 33023]became[0 255]and masked every pixel; - scaled a PostScript key on an Indexed image as if its indices were
colours; - wrapped a value past the depth: a key of 300 masked 44.
Keys are now tested on the encoded samples. A PDF key also holds on a
multi-input DeviceN image, under a transfer function, and on a gray
image painted as K under a CMYK output intent, each of which changed
the samples it was tested on. In the test corpus, page 4 of
image-qa.pdfmoves: its DeviceN images now cut out what Ghostscript
does. - masked the wrong pixels, or none, on a 1-, 2- or 4-bit image or one
-
An image with a stencil
/Maskpainted its masked pixels under a
transfer function. The function was applied to the masked pixels
too, so an inverting/TRpainted them white. -
Inline images are painted as image XObjects are. They went through
a second, thinner pipeline. It ignored/Decode, so an inverted
inline image (/D [1 0]) painted inverted, and it also ignored
/Interpolateand transfer functions. It approximated multi-input
DeviceN through a sampled table, and painted all four inks for a
K-only Indexed CMYK image under overprint. Both forms now share one
pipeline. No inline image in the test corpus uses any of these, and
nothing in it moves. -
Multi-input DeviceN images honour
/Decodeand depths other than 8
bits. They were evaluated from the raw stream, read as 8-bit
samples, before expansion and/Decode. -
PostScript transfer functions are rendered.
settransferand
setcolortransferwere recorded for PDF output and otherwise ignored, so
fills, images and shadings painted as if no function were set.
pmaster.psandbirthday.ps, which set a tone curve, came out up to
30 levels from Ghostscript; the pixels more than 8 levels from it fall
from 5% to 1% and from 28% to 3.5%. -
PDF transfer functions apply to images in every colour space, and to
shadings. The reader applied/TRto the samples of images with three
or more components — right for DeviceRGB, wrong for ICCBased RGB and for
CMYK, whose cyan, magenta and yellow took the red, green and blue
functions — and to no Gray, Separation, Indexed or two-component image,
nor to any shading. The renderer now applies the function to the final
RGB of every paint, after colour management, as Ghostscript does. An
image XObject drawn twice no longer keeps the function in force the
first time. -
Transfer functions apply only to fully opaque paints (ISO 32000-1
§11.7.5.2): not to a paint with alpha below 1, a blend mode other than
Normal, or a soft mask, nor inside a transparency group drawn so, nor
inside a soft mask's own mask. stet applied them to every paint, as
poppler does; Ghostscript follows the spec except for the group rule. -
PDF output no longer applies a document's transfer function twice.
The reader baked/TRinto fill colours and also passed it on, so
--device pdffrom a PDF wrote both. Output now carries the function
once, as/TR2, for images and shadings as well as fills and strokes,
and resets it to/Identitywhen the function returns to identity —
before, everything painted after it stayed under it. -
Cached Type 3 glyphs take the transfer function in force where they
are shown, as they take its colour, rather than the one in force when
the glyph was first built. -
An image inside a PDF layer could be drawn as another image on the
page. The renderer's per-page image caches were indexed by position in
the page's top-level list, and an image inside an optional-content group
looked itself up by its position within the group, so it took whichever
top-level image shared that position (CLI, viewer and WASM). The caches
now nest like the layers. Images inside layers are also converted once
per page rather than once per band: a 3000×3000 CMYK image in a layer at
300 dpi went from 2.3 s and 1.5 GB to 1.3 s and 240 MB, the same as
outside one. Images in layers that are hidden are no longer converted. -
Small PostScript pages render as larger ones do. The CLI's PNG
output drew a page small enough to need no banding (Letter below about
80 dpi, or a small EPS) onto what the previous page left behind, unlike
every other page and unlike the library. Over a blank page, blend modes
blended with white paper rather than a transparent page (ISO 32000-1
§11.4.7: red under/Differencecame out cyan); a page sent with
copypageshowed under the next one, which LanguageLevel 3 erases; and
flushpagepainted the page twice, doubling translucent paint. These
pages now render banded, from a clear page. Anti-aliased edges on them
can shift by a level or two, as on larger pages. -
An EPS that calls
showpagerenders as one page in the library.
Interpreter::renderandrender_to_display_listadd ashowpagewhen
an EPS leaves one out, and decided by asking the device whether it had
received a page; the device renders in the background, so it often had
not, and the EPS came back with a second, blank page — every time, once
the page was large enough to band (a 600 pt EPS at 300 dpi). They now
ask whether the EPS'sshowpageran. -
copypagefollows LanguageLevel 3: it passesEndPagereason code
0, asshowpagedoes, counts as ashowpageexecution, and calls
BeginPageafterwards. It was passing the LanguageLevel 2 code, 1, and
skippingBeginPage. -
A page
EndPagedeclines carries over. When a job'sEndPage
returns false, the page is neither sent nor erased (PLRM 6.2.6), so the
next page is drawn on top of it — how n-up imposition gathers pages. It
was erased.PageCountnow counts pages produced, as in Ghostscript, and
numbers the output files; the countBeginPageandEndPagereceive
still counts everyshowpage. -
EndPageruns with reason code 2 when the page device is
deactivated: at the end of a job and whensetpagedevicereplaces it.
AnEndPagethat answers true there sends the page noshowpageended,
such as the last, partly filled sheet of an n-up job.setpagedevice
also erases marks made before it, as the PLRM and Ghostscript do; they
used to carry onto the next page. -
--device pdfno longer leaves an integer on the operand stack at
every page. ItsEndPageconsumed the reason code but not the count. -
Erasing the page paints gray 1 through the transfer function, as the
PLRM defineserasepageand Ghostscript does: under an inverting
function the page is erased to black. That coverserasepageand the
eraseshowpageandcopypageperform, so a job that sets an inverting
function once (film negatives) gets black paper from page 2 on, as in
Ghostscript; page 1 was erased before the function was set.
setpagedevicereinitializes the transfer function before it erases, so
its erase stays white. Pages whose transfer function leaves white white
are unchanged. -
The library renders each PostScript page once.
Interpreter::render
andrender_to_display_listinterpreted onto aSkiaDevicethat
rasterised every page into a sink that threw the pixels away, only to
learn the page's size, and thenrenderrasterised the page again. The
device installed while interpreting now records the size and nothing
else:rendertakes about 40% less time per page at 300 dpi
(0.25 s → 0.14 s ontiger.ps,escher.ps,grade.ps,colorcir.ps),
with the same pixels. The WASM build did the same and is fixed the same
way. -
Interpreter::render_to_pdfreturns the PDF and touches nothing
else. It finished the job as the command line does, writing the
document tooutput.pdfin the working directory — overwriting any file
of that name — and printingPDF written: output.pdfto stderr, then
built the document a second time for the bytes it returned, whose
/Titlewas "output" after that file. It now builds the document once,
in memory, with no/Titleunless a/DOCINFOpdfmark sets one. -
An
Interpreterused for PDF output no longer showspdfmarkto
later screen renders, or fails after ~21 700 PDF renders. Each
render_to_pdfcall registeredpdfmarkand the distiller-parameter
operators afresh and left them insystemdict. A laterrendersaw
systemdict /pdfmark knownas true, so a prologue that asks took its
Distiller branch on screen too — FrameMaker's, for one, then paints its
colours as RGB instead of CMYK. And the operator table grew by three per
call until its 16-bit operator numbers wrapped, after 21 723 calls, and
pdfmarkran another operator. They are now registered once and are in
systemdictfor PDF jobs only. -
setpagedeviceno longer keeps every page device for the life of the
process. It copied each page device dictionary into global VM, which
restoredoes not reclaim, so each call left about 8 KB behind — from
the command line too, and in everyrendercall of anInterpreter,
whose own device setup also ran outside the job'ssave. Page devices
now live in local VM and go with therestorethat ends their job, and
the library sets up its device inside the job. -
PageCountcounts the pages arestorereaches back past. After
save … setpagedevice … showpage restore,currentpagedevicereported
the count from before the page, since the count lived in the page device
dictionary therestorereinstated. It is now the device's count, as in
Ghostscript. -
A file is closed when reading or executing it reaches its end, as
PLRM 3efilesays and Ghostscript does. Onlyclosefileclosed one, so
a file kept what it held for the life of the process: every job the
library ran, which it copies into memory (an 864 KB job rendered 50 times
kept 43 MB), every filerunreads in whole, every font file loaded from
disk, and the descriptor of every file read to the end.read,
readstring,readhexstring,readline,tokenand executing a file
now close it at end of file, andstatusthen answers false — so
setfilepositionon a file read to its end is anioerror, as in
Ghostscript. A filter that reads its source to the end leaves the source
open, as Ghostscript does, except ReusableStreamDecode, which reads it
eagerly and closes it.readstringon a closed file answers() false
rather thanioerror, likeread. -
restorecloses the files made since itssave, as PLRM 3efile
says, and reclaims them, so the file table no longer grows with every
file a job opens — three for each library render call, and every file and
filter a command-line job opened. A file not read to its end, such as a
real file's descriptor or a filter's buffers, was kept until the process
exited. Writes are flushed, asclosefilewould. -
File objects are in local or global VM, like other composites, by
currentglobalwhen they are made.gcheckanswered true for every file,
whiledef,putand the other stores refused every file as local; now
a global file can be stored in global VM and a local one cannot (PLRM
3.7.2), and arestorethat a local file made since itssavewould
outlive on a stack is aninvalidrestore. A filter over a local source is
local even undertrue setglobal, and the standard files are global, as
in Ghostscript. The job's own file is local (Ghostscript's is global), so
currentfile gcheckanswers false. -
PDF output is reproducible. The same job gives the same PDF, apart
from its/CreationDate: a page's/Fontresources were listed in an
order that changed from run to run, and are now in their own order (F0,
F1, …). -
PDF output joins a line's strings into
TJruns in every size of a
font and on every page. Only a font's first instance joined — the same
face at a second size placed each string with its ownTmandTj— and
only for the characters the font's first page used. Across 160 corpus
files, PS → PDF now writes 94% fewerTmoperators (241,429 → 15,708),
which keeps a line's text together for whatever extracts it and shrinks
the content streams. The kerns between a run's strings now come from the
same code as the font's/Widths(or/W), which a viewer advances by;
they were computed from a copy that had drifted from it for re-encoded
Type 1 instances, Type 42 codes without a glyph, and CIDFontType 2 fonts
rebuilt fromGlyphDirectory, where a kern could move the text after it. -
A transparency group without a
/BBoxis bounded by the page.
begintransparencygroupwith no/BBox, and no clip set, took the page
size in points as its device-space bounds, so above 72 dpi the group drew
only in the device's top-left corner — at 300 dpi, the first 24% of the
page across and down — in every output. It now takes the page as
clippathdoes. -
PostScript → PDF writes the text inside transparency groups, soft
masks, layers and pattern tiles. Only a page's top-level text had its
fonts tracked. Text inside a group, soft mask or layer in a font used
nowhere else vanished from the PDF; characters used only there were
missing from the font's/Widths, so viewers drew them on top of one
another; and a page whose only text was in a group drew it twice — as
glyph outlines and as text in a font its resources did not name. A
pattern tile's fonts were tracked after the fonts were embedded, so a
font used only in a tile was named but never written: pdftops output
that fills glyphs with a pattern of text came out with the glyphs empty. -
PostScript → PDF keeps every page of a job that calls
setpagedevice
more than once. Each call replaced the PDF device, and with it every
page so far, so only the pages after the last call reached the PDF.
Ghostscript'sps2writecallssetpagedevicebefore every page, and
documents whose page sizes change call it between them: in a 120-file
corpus sample, 6 PDFs were short — 4 of 12ps2writefiles, one
pdftopsdocument 3 pages of 264.Interpreter::render_to_pdflost
them the same way. An EPS also lost the TrimBox of its bounding box if
its program calledsetpagedevice. -
PDF output writes each page's exact size. The MediaBox was the page
rounded to the device's pixels, so at the command line's 300 dpi an A4
[595 842]page came out 594.96 × 841.92, and an EPS 556.56 square
instead of its 556.49%%HiResBoundingBox; only page sizes that are
whole pixels, such as letter, were exact. PDF → PDF rounded each page to
whole points instead — 595.276 × 841.89 became 595 × 842 — and moved its
content by the difference. Content placement from PostScript is
unchanged. -
restorerevertscurrentgstate, and no longer leaves agstate
naming what it reclaimed.currentgstateinto agstatemade before a
savekept the state from inside the save after therestore, with the
fonts, colour spaces and page device the restore had reclaimed;
reinstating it could panic the interpreter (g setgstate currentpagedeviceafter asetpagedeviceinside the save). PLRM 3.7.3:
restoreundoes changes to such an object, as Ghostscript does. -
gstateobjects work withcopy,eq,==andpstack.
gstate1 gstate2 copyraisedtypecheck; it now copies the state, which
restorereverts like acurrentgstate. Agstatewas noteqto
itself,==raisedundefined, andpstackprinted--nostringval--;
they print-gstate-, as in Ghostscript. -
The page device comes back with the graphics state, and
nulldevice
no longer ends the job's output. PLRM 3e §6.1 makes the device part of
the graphics state; stet restored only the page device dictionary.grestore,grestoreall,restoreandsetgstatenow reactivate the
page device of the state they reinstate, with its page size: after
gsave … setpagedevice … grestorethe next page came out at the inner
device's size, its marks off the page. Switching between two page
devices runs the outgoingEndPage(reason 2) and the incoming
BeginPageand erases the page; switching to or from the null device
runs neither and leaves the page alone (PLRM Examples 6.1, 6.2). The
calls match Ghostscript's, which differs in one case: reinstating
another page device from under the null device, Ghostscript keeps
printing at the old page size while reporting the new one; stet takes
the new one.nulldevicereplaced the output device for the rest of the job, so
aftergsave nulldevice … grestoreno page was written — PNG output
lost every later page, PDF output wrote nothing. Marks made under the
null device, such as a stringshown to measure it, now go nowhere
rather than onto the page.setpagedeviceback from the null device keeps counting pages, as in
Ghostscript; it started again from 0.
-
PDF output keeps the function of a mesh or patch shading. For a
Type 4–7 shading whose colours come from a/Function, the writer wrote
each vertex's function input as its colour and dropped the function. A
gray or spot shading came out as a ramp of that input — a flat 0.8 gray
as black to white, a soft mask that should pass everything as a radial
ball — and in any other colour space the function's curve between
vertices was lost, the colours written converted to RGB. The shading now
names its function again, sampled in its own colour space, so a spot
shading keeps its tints. -
PDF output writes what a pattern tile contains. A tile's resources
were written from a partial copy of the page's, so a pattern whose cell
held a transparency group, a soft mask, a layer or another pattern named
resources it did not have: the group and the nested pattern drew
nothing, the soft mask's content drew unmasked, an uncoloured nested
pattern had no colour space, and the layer was missing from the
document's layers. PDF → PDF lost the whole page of one corpus file,
and charts and nested patterns in others; PostScript that nests
patterns, as Ghostscript'seps2writedoes for nested PDF patterns,
lost the inner one. Each form (group or soft-mask mask) now names its
resources too, rather than relying on the PDF 1.1 rule that a form
without them takes the page's, which inside a tile is not where they
are. -
A clip restored inside a group no longer unbalances the PDF. The
group's form continued the clip scope the page had open, so a clip set
and restored inside it closed the page'sqfrom inside the form;
viewers reported "Restoring state when no valid states to pop". -
A pattern painted inside a transparency group or soft mask is placed
correctly in PDF output. A pattern's matrix maps to the space of the
content stream painting it — for a form, the form's — but every pattern
was given the page's: in PostScript → PDF at 300 dpi a pattern filled
inside a group came out at about a quarter of its size, and at 72 dpi
flipped and shifted. A pattern used both on the page and inside a form is
now written once for each, since viewers disagree on one object shared
between the two (Ghostscript keeps its first placement, poppler places
it per stream). -
A soft mask inside a pattern tile renders wherever the tile lands.
A tile's contents are moved into device space by their transform, their
paths left in pattern space, and the soft mask's raster was bounded by
the raw paths — built where the cell sits in pattern space, not where it
is drawn — so it masked everything out. A tile whose device origin was
at x = 0 rendered nothing; elsewhere it happened to take another route
and rendered. PostScript and PDF alike. -
An uncoloured pattern's tile sets no colour in PDF output. A
PaintType 2 pattern takes its colour from where it is used, and its
content may not set one (ISO 32000-1 § 8.7.3.3), but stet wrote each
mark's captured colour into the tile — usually black. Ghostscript
ignores it; poppler obeys it, so the pattern painted black, or in
whatever colour the PostScript last set, rather than the colour it was
filled with. -
PostScript → PDF no longer crashes on fonts a
restorereclaimed
(--device pdfandInterpreter::render_to_pdf). PDF output builds the
document at the end of the job and read each font out of the
interpreter's memory then, but a font defined inside asave … restore
is gone by that point: much of Ghostscript'seps2writeandps2write
output defines its fonts inside each page'ssave, and an EPS figure
placed on a page brings its fonts inside its own. The job panicked, about
2% of a PostScript corpus sample, in 0.8.x releases. A glyph a page added
to a font inside itssave(PLRM 3e §5.9.2) was silently missing from the
PDF. The interpreter now copies each font as text is shown with it,
before arestorecan change it, and PDF output embeds the copies. -
An EPS that calls
showpagerenders as one page on the command line
and in the library's PDF output. Both added ashowpageof their own
regardless, writing a blank second page (theps_samplesfiles
golfer.ps,printerarea.ps,testprinter.psandtiger.psdid). -
Relative colorimetric CMYK rendered lighter than Ghostscript and
Acrobat with black-point compensation on, the default. Compensation
maps the profile's black to sRGB black, and stet took that black to be
400% ink. lcms2 — inside Ghostscript — takes an output profile's
ink-limited black for relative colorimetric: Lab L*=0 through the
profile's perceptualB2A0table and back throughA2B1, the darkest
colour the press is allowed to make. That is lighter than 400% ink, so
stet compensated too little: K=100 through Ghostscript's
default_cmyk.icc— the CLI's system profile where Ghostscript is
installed — came out RGB(44, 41, 42) where Acrobat and Ghostscript give
(35, 31, 32), which stet now matches. On a 17⁴ grid of CMYK values stet
is now 0.25–0.32 levels from lcms2 on average through ISO Coated v2 300%,
FOGRA39, SWOP v2 and Japan Color 2001 (was 0.7–5.9, up to 24), and
K=100 matches Ghostscript exactly through each. Most CMYK content
through a press profile renders a little darker, in PostScript and
PDF, including PDF/X documents through their output intent; profiles
of input class, such as the library's embedded default, and profiles
whoseB2A0reaches 400% ink are unchanged. lcms2's other black-point
rules come with it: an ICC v4 profile's perceptual and saturation
intents compensate from the fixed v4 perceptual black, and an output
profile with no perceptual table is not compensated under relative
colorimetric. -
--bpcdid not reach PostScript images. The PNG device converts
DeviceCMYK images and overprinted colours as it renders, through a
colour cache it builds for itself with the default options, so
--bpc offchanged fills and left images beside them compensated. -
Inline images ignored the rendering intent, always using relative
colorimetric; they now take the current intent, or their own/Intent. -
Images with an explicit
/Maskskipped their ICC profile. An
ICCBased image with a stencil/Maskshowed its raw samples (RGB) or
went through the default CMYK profile instead of its own (CMYK); both
now convert through their profile, with the image's intent. -
CMYK profiles with ICC v4
mABor 8-bitlut8Typetables were
converted through their perceptual table even for relative
colorimetric, the default intent. They now use the colorimetric table,
like every other profile, so their colours change. -
Black-point compensation for CMYK is applied before the sRGB gamut
clip, as lcms2 does, rather than after. Colours outside sRGB, such as
saturated cyans, move by up to 2 levels. -
PageBoxesdocumented the wrong defaults. It said an absent
BleedBox, TrimBox or ArtBox falls back to the MediaBox; the PDF
specification makes it the crop box. The valuespage_boxesreturns
are unchanged. -
The viewer ignored
--bpc offand--no-iccfor images. It
converts DeviceCMYK and ICCBased images, and overprinted colours, as it
renders, through a cache built with the default black-point
compensation, so those images stayed compensated beside fills that were
not — up to ~20 levels darker for K=100 through Ghostscript's default
CMYK profile. -
Interpreter::renderignored a reconfiguredcontext().icc_cachefor
images. It converted DeviceCMYK images and overprint through the
embedded CMYK profile with the default black-point compensation, so a
caller who set their own profile or compensation got images unlike the
fills beside them. It now converts them as the context does. The default
configuration renders the same as before. -
A PostScript program ending in
quitfailed in thestetlibrary.
Interpreter::render,render_to_display_list,render_to_pdfand
execreturned "PostScript error: stop" and discarded the job's pages,
becausequitstops the job the way an error does. They now treat it as
the clean end of the job, as the CLI always has; a real error is still
an error.
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.