Transparent PNG output and a perceptual CMYK source intent - #4
jungseohaan wants to merge 5 commits into
Conversation
Rendering already starts from a transparent backdrop and composites onto white paper as its last step. With --transparent (or SkiaDevice::set_transparent_background / render_to_rgba_with_background) that step converts premultiplied pixels to straight alpha instead, so placed artwork keeps its unpainted areas clear. The full-page path starts from a transparent pixmap in that mode, matching the banded path. render_to_rgba and render_to_rgba_with_layers keep their signatures and white paper; the viewport audit path is unchanged.
The CLUT bake samples A2B1, matching lcms2's RelCol. For a print profile that is markedly lighter in the blacks than A2B0, which is what lcms2, Ghostscript and ImageMagick use by default: with Japan Color 2001 Coated, K100 comes out (51,45,43) instead of (35,25,22), and placed artwork ends up lighter than the same black drawn as text by another tool. --cmyk-intent perceptual (IccCacheOptions::cmyk_source_table) samples A2B0 instead. Checked against ImageMagick + lcms2 with the same profile: K100 and K50 match exactly, and a 60-patch CMYK sweep agrees to within 1 RGB level (mean 0.3). The default stays relative, and a profile with no perceptual table falls back to the colorimetric one.
render_to_rgba_with_background is the library half of --transparent, and LIBRARY-USAGE.md never mentioned it: a caller reading that document would find only render_to_rgba, which always composites onto white paper. The new section states the rule the renderer already follows — every page starts transparent and is composited onto paper last — and shows both input paths, since the function takes a display list and the PostScript interpreter and the PDF reader produce the same type.
AndyCappDev
left a comment
There was a problem hiding this comment.
Thanks for rebasing this onto 0.8.2 and for the LIBRARY-USAGE.md section, which is a useful addition. Authoring the commits with your GitHub email helps too: the credit will now land on your profile.
I think my review on #3 may not have reached you, since none of it comes up here, so I've repeated it below in full. Since the branch is based on 0.8.2, which carries the contribution clause in README.md and CONTRIBUTING.md, the licensing question from that review is settled and there's nothing to confirm. Could you close #3 so we can carry on here?
1. Split out --cmyk-intent
Please move --cmyk-intent to its own PR and keep this one to --transparent. The finding behind it is a good one: the K100 difference against lcms2 with Japan Color 2001 Coated is real. But it's a separate change to colour management and needs its own design discussion, which I'd rather not have block --transparent.
In particular, PDF content and PostScript both carry a per-object rendering intent, and I want to work out how a global switch should relate to that before settling on a CLI flag or a library API. I haven't decided on an approach yet, so let's take that up in the new PR.
To split the branch, cut cmyk-intent from upstream main and cherry-pick 1fcae24 onto it. CHANGELOG.md will conflict, because the commit expects the --transparent entry above it; keep just the --cmyk-intent entry. Then drop the commit from this branch:
git checkout -b cmyk-intent 688cc9e
git cherry-pick 1fcae24 # resolve CHANGELOG.md, then: git cherry-pick --continue
git push origin cmyk-intent # and open a PR from it
git checkout upstream-transparent
git rebase --onto 8665ddc 1fcae24
git push --force-with-lease origin upstream-transparentThe rest of the review is about --transparent itself.
2. Pages after the first come out opaque white on the full-page path
SkiaDevice::erase_page (just above show_page in skia_device.rs) still fills the pixmap with Color::WHITE. The interpreter calls it after every showpage. When a page is small enough that select_band_height skips banding (roughly two bands or fewer), rendering takes the full-page path, and every page after the first is drawn onto opaque white paper.
Repro, a 3-page 200×200 file:
%!PS
<< /PageSize [200 200] >> setpagedevice
0 0 1 setrgbcolor 20 20 60 60 rectfill showpage
1 0 0 setrgbcolor 20 20 60 60 rectfill showpage
0 1 0 setrgbcolor 20 20 60 60 rectfill showpageWith this branch, stet --device png --transparent --dpi 72 multi.ps gives an unpainted corner of (0,0,0,0) on page 1 but (255,255,255,255) on pages 2 and 3. At 300 dpi, which takes the banded path, all three pages are correct. Small EPS artwork usually takes the full-page path, so this affects your use case directly.
The fix is for erase_page to honour the setting the same way ensure_full_pixmap now does. A small fn paper_color(&self) -> Color used at both sites would keep them from drifting apart.
3. A test that goes through SkiaDevice
The new test calls render_to_rgba_with_background, which is always banded, so nothing exercises SkiaDevice, where the bug above lives. Please add a test that drives two pages through SkiaDevice at a size that takes the full-page path (e.g. 200×200 at 72 dpi) and asserts that an unpainted pixel on page 2 has alpha 0.
4. An enum instead of a bool
render_to_rgba_with_background becomes public API in a published crate, and it takes eight positional arguments, including two bools. The name also says "background" while the parameter is a yes/no. Please swap the bool for a small enum, e.g. PageBackground { White, Transparent }, on both the function and the SkiaDevice setter (set_page_background). Call sites then read unambiguously, and a colour variant can be added later without a breaking change. @b26354nz asked for that on #3.
5. Transparent error fallbacks
In transparent mode, the zero-size and render-error returns in render_to_rgba_with_background still hand back opaque white (vec![0xFF; …]). They should return transparent pixels to match what the caller asked for.
Smaller points
LIBRARY-USAGE.mdopens with "Rendering always starts from a transparent backdrop". That's true of the banded path, but without--transparentthe full-page path starts from white. That inconsistency predates this PR and I'll resolve it separately, so please phrase the doc as "composites onto white paper as its last step" rather than describing the backdrop. The code examples will also need updating once the enum lands.- Clippy on rustc 1.93. Thanks for flagging the two warnings on
main. I'll look at both, especially the macOS-only one, since it points to a gap in what CI lints. CHANGELOG.mdconflict. It conflicts with unreleased work on my side that isn't on GitHub yet. Leave it as it is; I'll resolve it when I merge.
Once --cmyk-intent is split out and 2–5 are in, I'll squash-merge this and cut 0.8.3 right away, so --transparent reaches the release binaries and crates.io.
Review of AndyCappDev#4 found the flag reaching only part of the device. SkiaDevice::erase_page, which the interpreter calls after every showpage, still filled with white. A page small enough for select_band_height to skip banding renders through the full-page path, so with --transparent every page after the first came back on opaque white paper — a three-page 200x200 file at 72 dpi gives (0,0,0,0) in an unpainted corner of page 1 and (255,255,255,255) on pages 2 and 3. Small EPS artwork takes that path too. Both sites now read one paper_color(), so neither can be changed without the other. A test drives the full-page path and asserts an erased page stays clear; it fails against the old erase_page. The flag is a PageBackground { White, Transparent } on render_to_rgba_with_background and SkiaDevice::set_page_background, rather than a bool whose meaning a call site could not show. The CLI keeps --transparent and converts at its boundary. The zero-size and render-failure returns handed back opaque white whatever was asked for; they now answer with the background the caller named.
Review of AndyCappDev#4 replaced the transparent-background bool with an enum, and render_region_prepared_with_background arrived after that review was written. It follows the same shape, including the blank answer a zero-sized region gives: white paper is opaque, a transparent region is clear.
|
@jungseohaan I saw Was closing intentional? If you'd like to reopen this, or open a fresh PR, I'm happy to merge If I don't hear back in a few days, I'll carry on myself:
Either way, thanks for the work and the careful testing behind it. |
Placed artwork (EPS, AI, PDF laid over other page content) needs its unpainted areas clear, not white. Pages render onto a backdrop that is composited onto white paper as the last step; with --transparent, or PageBackground::Transparent on render_to_rgba_with_background and SkiaDevice::set_page_background, that step instead converts premultiplied pixels to straight alpha. Every site that clears a page reads one paper_color(), so erase_page after showpage keeps later pages clear on the full-page path as well as the banded one, and the zero-size and render-failure returns answer with the background the caller asked for. render_to_rgba and render_to_rgba_with_layers keep their signatures and white paper. Squashed from PR #4 (aa1e7bd, 8665ddc, a171516, ffa4198). 1fcae24 (--cmyk-intent) is left out: it is an independent colour-management change that interacts with per-object rendering intents and belongs in its own PR.
Transparent page backgrounds for placed artwork (--transparent, and PageBackground through the stet facade, stet-render and stet-pdf-reader), contributed in #4; CIDFontType 0 fonts built with StartData rendered and embedded in PDF output as CFF; TrueType CID fonts honouring CIDMap; vertical CID text per the PLRM; setoverprintmode and spot-colour overprint in PostScript; the URW++ fonts under the OFL; and CLI fixes for a bare `stet`, `-o`, and the REPL banner. cargo-semver-checks reports one struct-literal break, accepted and changelogged as in 0.8.2: GraphicsState's overprint_mode field.
|
@jungseohaan Your work went in as submitted, squashed into one commit under your name (e83a0ba), including the On top of it I added the same option to the library, so you don't have to drop down to
Examples are in docs/LIBRARY-USAGE.md. Prebuilt binaries for Linux, macOS and Windows are on the release page, and the crates are on crates.io. Rendering only part of a large artboard, and the perceptual/relative CMYK question, are both on my list as separate pieces of work. The region rendering comes next. If you have a test file for either, especially a Japan Color 2001 Coated job where the difference shows, I'd be glad to have it. Thanks again. This was stet's first external code contribution, and it was a useful one. |
Two options that placed artwork needs, plus the library API behind the first.
--transparent(--device png)Rendering already starts from a transparent backdrop and composites onto
white paper as its last step.
--transparentconverts premultiplied pixelsto straight alpha instead of taking that step, so a page's unpainted areas
stay clear and the artwork can sit over other content. The full-page path
starts from a transparent pixmap in that mode, matching the banded path.
render_to_rgbaandrender_to_rgba_with_layerskeep their signatures andtheir white paper; the viewport audit path is unchanged. The library half is
render_to_rgba_with_background(andSkiaDevice::set_transparent_background),which takes a display list and so serves both the PostScript interpreter and
the PDF reader.
--cmyk-intent <perceptual|relative>Selects which table of the source CMYK profile converts colour. Colorimetric
(A2B1) stays the default, since the perceptual table changes every CMYK
pixel; a profile with no perceptual table falls back to the colorimetric one.
The library entry point is
IccCacheOptions::cmyk_source_table.Why
These came out of a print-to-HWPX converter that rasterizes linked EPS,
Illustrator and PDF artwork for placement in a document. It needs unpainted
areas clear (Ghostscript's
pngalphadevice used to provide that) and printblacks that match the swatch colours ImageMagick produces. It currently
pins a fork; upstreaming these retires the pin.
Checks
cargo fmt --all -- --checkpasses.cargo test --workspace: 1390 passed, 0 failed.cargo clippy --workspace --all-targets -- -D warningsreports two errors,both of which reproduce unchanged on
mainwith rustc 1.93: an unfulfilled#[expect(clippy::drain_collect)]instet-fonts/src/type2_charstring.rs,and a
collapsible_ifinstet-graphics/src/icc.rsinside a#[cfg(any(target_os = "macos", target_os = "windows"))]block that LinuxCI never compiles. Neither is in the files this branch touches.
ps_samplesI rendered both ways come out pixel-identical to the pre-rebase build.
I have not run
pdf_visual_test.sh— it needs the corpus the project doesnot ship. Happy to if you would like it before merging.