Skip to content

stet 0.8.4

Choose a tag to compare

@github-actions github-actions released this 04 Oct 18:24
· 148 commits to main since this release

Added

  • rendering_intent on 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 with GraphicsState.overprint_mode in
    0.8.3, the policy is that outside code reads these structs rather than
    writes them.
  • color_lut_components on MeshShadingParams and
    PatchShadingParams
    : the shading's function sampled at the
    color_lut inputs 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
    and DeviceColor::from_cmyk_icc_with_intent in stet-graphics.
    convert_color_readonly_with_intent, convert_color_with_intent and
    convert_image_8bit_with_intent now 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
    ri operator, 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 perceptual gives the colours lcms2-based
    tools such as ImageMagick produce by default. In the library:
    PdfDocument::set_default_rendering_intent in stet-pdf-reader,
    InterpreterBuilder::default_rendering_intent in stet, and
    Context::set_default_rendering_intent / Context::initial_gstate in
    stet-core, taking the new RenderingIntent enum
    (stet_graphics::rendering_intent, re-exported by both).
  • GraphicsState.icc_components in stet-core: the components of
    the current colour in an ICCBased space, kept so setrenderingintent
    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_source and VmMarks.files
    in stet-core: a file
    now belongs to local or global VM, so restore can 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_mode and build_icc_cache_for_list_with_bpc
    in stet-render: the black-point compensation mode for colours the
    renderer converts, which build_icc_cache_for_list leaves at the
    default.
  • Context::newerror in stet-core: whether a job that ended in
    PsError::Stop ended on an error or on quit, which also stops.
  • ViewerOptions and run_viewer_with_options in stet-viewer:
    the viewer's settings as a struct, with the new bpc_mode among 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 and PdfDocument::set_page_area in
    stet-pdf-reader. --box art (or media, crop, bleed, trim)
    renders that box; --box llx,lly,urx,ury renders a rectangle in PDF
    points in the page's unrotated space. The area becomes the page — its
    size is the output size, /Rotate applies to it, and --width,
    --height and --transparent work 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 png or --device pdf.
    PageArea, PdfDocument::page_area_rect and
    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, and ColorKey, a colour-key mask tested on
    samples as encoded — what both front ends now use.
  • transfer on ImageParams and the shading parameter structs
    (AxialShadingParams, RadialShadingParams, MeshShadingParams,
    PatchShadingParams): the transfer function in force, as FillParams
    and StrokeParams already carry it, and TransferState::rgb_tables,
    apply_rgb, rgb_luts, apply_to_rgba and transfer_lookup in
    stet-graphics to 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 as rendering_intent.
  • PdfDevice::in_memory in stet-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 /Title only when a pdfmark gives one.
  • Font copies for devices that read fonts after the page is sent:
    OutputDevice::keeps_text_fonts and Context::font_snapshots in
    stet-core (module font_snapshot), TextParams::font_snapshot in
    stet-graphics. While the device asks, the interpreter copies each font
    text is shown with before any restore can reclaim it or revert glyphs
    added to it, and marks the Text element with its copy. The font's
    font_entity is valid only while the font lives.
  • stet_ops::remove_pdf_authoring_ops: takes pdfmark,
    setdistillerparams and currentdistillerparams back out of
    systemdict, for a context that runs screen jobs after PDF ones.
    register_pdf_authoring_ops may now be called again; it adds nothing to
    the operator table the second time.
  • OutputDevice::resize_page in stet-core: setpagedevice offers
    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_points in stet-pdf: the next page's
    size in points, for a display list drawn at exactly that size, as
    stet_pdf_reader renders a page. set_page_size takes whole device
    pixels, which rounds a page such as A4.
  • Context::set_gstate_object in stet-core: replace a gstate
    object's state so that restore can revert it, backing up what the
    object held if it predates the current save. Write gstate_store
    through it, as dictionaries are written through dict_put_cow.

Deprecated

  • stet_viewer::run_viewer, for run_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 /RI or an
    image's /Intent changed 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:
    A2B1 for relative colorimetric, A2B0 for perceptual, A2B2 for
    saturation, with black-point compensation as lcms2 applies it
    (perceptual and saturation always; a profile with no A2B2 gives
    saturation its A2B0, 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. An ri after k/K applies to the colour
    already set, as it does after sc/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
    setrenderingintent
    the 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
    setrenderingintent converts the current colour again, as ri does 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_color and convert_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 --bpc is 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 --bpc says, 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 off it 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, else A2B0, else the tone curves and matrix (rather
    than any table the profile has), and it is interpolated tetrahedrally
    rather than trilinearly. ICC v2 lut8Type tables are read too.

  • --bpc now 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 off keeps the previous output.
  • ICC v4 profiles built on lutAToBType/lutBToAType tables 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.

  • 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 as lut16Type
    profiles, which are unchanged. Nothing in the test corpus moves.

  • CMYK shown through a profile with an XYZ PCS now matches lcms2.
    Such profiles include Ghostscript's own ps_cmyk.icc and
    gray_to_k.icc. stet read only Lab-PCS tables itself and left these to
    moxcms, which was up to 17 levels from lcms2 through ps_cmyk.icc and
    4.4 through the ICCBased CMYK in the test corpus's 5403.pdf. They are
    now within half a level, black-point compensation included:

    • a black point that needs the profile's B2A0 now uses it;
    • a three-input table's matrix is now applied, as lcms2 applies it.

    5403.pdf moves by up to 3 levels, toward Ghostscript.

  • 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 carry A2B0 alone. 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 an rg colour goes to CMYK and back. moxcms did it,
    with no black-point compensation, so with --bpc on (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's B2A1, 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 in 2142.pdf moves 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.pdf and 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
    /Mask array and PostScript ImageType 4's MaskColor give 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.pdf moves: its DeviceN images now cut out what Ghostscript
    does.

  • An image with a stencil /Mask painted its masked pixels under a
    transfer function.
    The function was applied to the masked pixels
    too, so an inverting /TR painted 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
    /Interpolate and 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 /Decode and 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. settransfer and
    setcolortransfer were recorded for PDF output and otherwise ignored, so
    fills, images and shadings painted as if no function were set.
    pmaster.ps and birthday.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 /TR to 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 /TR into fill colours and also passed it on, so
    --device pdf from 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 /Identity when 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 /Difference came out cyan); a page sent with
    copypage showed under the next one, which LanguageLevel 3 erases; and
    flushpage painted 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 showpage renders as one page in the library.
    Interpreter::render and render_to_display_list add a showpage when
    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's showpage ran.

  • copypage follows LanguageLevel 3: it passes EndPage reason code
    0, as showpage does, counts as a showpage execution, and calls
    BeginPage afterwards. It was passing the LanguageLevel 2 code, 1, and
    skipping BeginPage.

  • A page EndPage declines carries over. When a job's EndPage
    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. PageCount now counts pages produced, as in Ghostscript, and
    numbers the output files; the count BeginPage and EndPage receive
    still counts every showpage.

  • EndPage runs with reason code 2 when the page device is
    deactivated
    : at the end of a job and when setpagedevice replaces it.
    An EndPage that answers true there sends the page no showpage ended,
    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 pdf no longer leaves an integer on the operand stack at
    every page.
    Its EndPage consumed the reason code but not the count.

  • Erasing the page paints gray 1 through the transfer function, as the
    PLRM defines erasepage and Ghostscript does: under an inverting
    function the page is erased to black. That covers erasepage and the
    erase showpage and copypage perform, 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.
    setpagedevice reinitializes 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
    and render_to_display_list interpreted onto a SkiaDevice that
    rasterised every page into a sink that threw the pixels away, only to
    learn the page's size, and then render rasterised the page again. The
    device installed while interpreting now records the size and nothing
    else: render takes about 40% less time per page at 300 dpi
    (0.25 s → 0.14 s on tiger.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_pdf returns the PDF and touches nothing
    else.
    It finished the job as the command line does, writing the
    document to output.pdf in the working directory — overwriting any file
    of that name — and printing PDF written: output.pdf to stderr, then
    built the document a second time for the bytes it returned, whose
    /Title was "output" after that file. It now builds the document once,
    in memory, with no /Title unless a /DOCINFO pdfmark sets one.

  • An Interpreter used for PDF output no longer shows pdfmark to
    later screen renders, or fails after ~21 700 PDF renders.
    Each
    render_to_pdf call registered pdfmark and the distiller-parameter
    operators afresh and left them in systemdict. A later render saw
    systemdict /pdfmark known as 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
    pdfmark ran another operator. They are now registered once and are in
    systemdict for PDF jobs only.

  • setpagedevice no longer keeps every page device for the life of the
    process.
    It copied each page device dictionary into global VM, which
    restore does not reclaim, so each call left about 8 KB behind — from
    the command line too, and in every render call of an Interpreter,
    whose own device setup also ran outside the job's save. Page devices
    now live in local VM and go with the restore that ends their job, and
    the library sets up its device inside the job.

  • PageCount counts the pages a restore reaches back past. After
    save … setpagedevice … showpage restore, currentpagedevice reported
    the count from before the page, since the count lived in the page device
    dictionary the restore reinstated. It is now the device's count, as in
    Ghostscript.

  • A file is closed when reading or executing it reaches its end, as
    PLRM 3e file says and Ghostscript does. Only closefile closed 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 file run reads in whole, every font file loaded from
    disk, and the descriptor of every file read to the end. read,
    readstring, readhexstring, readline, token and executing a file
    now close it at end of file, and status then answers false — so
    setfileposition on a file read to its end is an ioerror, 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. readstring on a closed file answers () false
    rather than ioerror, like read.

  • restore closes the files made since its save, as PLRM 3e file
    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, as closefile would.

  • File objects are in local or global VM, like other composites, by
    currentglobal when they are made. gcheck answered true for every file,
    while def, put and 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 a restore that a local file made since its save would
    outlive on a stack is an invalidrestore. A filter over a local source is
    local even under true setglobal, and the standard files are global, as
    in Ghostscript. The job's own file is local (Ghostscript's is global), so
    currentfile gcheck answers false.

  • PDF output is reproducible. The same job gives the same PDF, apart
    from its /CreationDate: a page's /Font resources 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 TJ runs 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 own Tm and Tj — and
    only for the characters the font's first page used. Across 160 corpus
    files, PS → PDF now writes 94% fewer Tm operators (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 from GlyphDirectory, where a kern could move the text after it.

  • A transparency group without a /BBox is bounded by the page.
    begintransparencygroup with 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
    clippath does.

  • 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's ps2write calls setpagedevice before every page, and
    documents whose page sizes change call it between them: in a 120-file
    corpus sample, 6 PDFs were short — 4 of 12 ps2write files, one
    pdftops document 3 pages of 264. Interpreter::render_to_pdf lost
    them the same way. An EPS also lost the TrimBox of its bounding box if
    its program called setpagedevice.

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

  • restore reverts currentgstate, and no longer leaves a gstate
    naming what it reclaimed.
    currentgstate into a gstate made before a
    save kept the state from inside the save after the restore, with the
    fonts, colour spaces and page device the restore had reclaimed;
    reinstating it could panic the interpreter (g setgstate currentpagedevice after a setpagedevice inside the save). PLRM 3.7.3:
    restore undoes changes to such an object, as Ghostscript does.

  • gstate objects work with copy, eq, == and pstack.
    gstate1 gstate2 copy raised typecheck; it now copies the state, which
    restore reverts like a currentgstate. A gstate was not eq to
    itself, == raised undefined, and pstack printed --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, restore and setgstate now reactivate the
      page device of the state they reinstate, with its page size: after
      gsave … setpagedevice … grestore the next page came out at the inner
      device's size, its marks off the page. Switching between two page
      devices runs the outgoing EndPage (reason 2) and the incoming
      BeginPage and 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.
    • nulldevice replaced the output device for the rest of the job, so
      after gsave nulldevice … grestore no page was written — PNG output
      lost every later page, PDF output wrote nothing. Marks made under the
      null device, such as a string shown to measure it, now go nowhere
      rather than onto the page.
    • setpagedevice back 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's eps2write does 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's q from 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 restore reclaimed
    (--device pdf and Interpreter::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 a save … restore
    is gone by that point: much of Ghostscript's eps2write and ps2write
    output defines its fonts inside each page's save, 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 its save (PLRM 3e §5.9.2) was silently missing from the
    PDF. The interpreter now copies each font as text is shown with it,
    before a restore can change it, and PDF output embeds the copies.

  • An EPS that calls showpage renders as one page on the command line
    and in the library's PDF output. Both added a showpage of their own
    regardless, writing a blank second page (the ps_samples files
    golfer.ps, printerarea.ps, testprinter.ps and tiger.ps did).

  • 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 perceptual B2A0 table and back through A2B1, 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
    whose B2A0 reaches 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.

  • --bpc did 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 off changed 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 /Mask skipped their ICC profile. An
    ICCBased image with a stencil /Mask showed 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 mAB or 8-bit lut8Type tables 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.

  • PageBoxes documented 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 values page_boxes returns
    are unchanged.

  • The viewer ignored --bpc off and --no-icc for 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::render ignored a reconfigured context().icc_cache for
    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 quit failed in the stet library.
    Interpreter::render, render_to_display_list, render_to_pdf and
    exec returned "PostScript error: stop" and discarded the job's pages,
    because quit stops 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.