Skip to content

Add --transparent: keep unpainted page areas transparent in PNG output - #3

Open
jungseohaan wants to merge 3 commits into
AndyCappDev:mainfrom
jungseohaan:transparent-background
Open

jungseohaan wants to merge 3 commits into
AndyCappDev:mainfrom
jungseohaan:transparent-background

Conversation

@jungseohaan

Copy link
Copy Markdown

Adds --transparent for --device png: unpainted page areas stay transparent instead of being composited onto white paper.

Why: I use stet to rasterize placed artwork (EPS, Illustrator, PDF) that ends up layered over other page content, so it needs a clear background. Today that means Ghostscript's pngalpha device; stet already renders these files well and faster, but always on white.

How: the renderer already starts from a transparent backdrop and composites onto white only as its last step (composite_onto_white), and ErasePage replays as a transparent clear. With the option set, that last step converts premultiplied pixels to straight alpha instead. The full-page (non-banded) path now starts from a transparent pixmap in that mode, so both paths agree.

  • CLI: --transparent, rejected for devices other than png; documented in --help, both READMEs, and the changelog.
  • API: SkiaDevice::set_transparent_background and render_to_rgba_with_background in stet-render. render_to_rgba and render_to_rgba_with_layers keep their signatures and white paper.
  • Not changed: the viewport audit path (--device viewport-png) and the viewer.

Testing:

  • Two unit tests: a half-painted page is white by default and alpha 0 with the option; unpremultiply values.
  • cargo test -p stet-render -p stet-cli, cargo fmt --check, scripts/check-cli-docs.sh pass; no new clippy warnings.
  • Real Illustrator/EPS files compared with Ghostscript 10.08 pngalpha at 144 dpi: alpha-channel RMSE 0 (EPS), 0.12% (PDF-compatible AI), 0.64% (DOS-EPS AI, edge anti-aliasing); transparent-pixel share 99.5952% vs 99.5949%.

Happy to rename the flag or restructure the API if you prefer something else.

SEOHAN JUNG added 2 commits September 22, 2026 22:11
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.

@AndyCappDev AndyCappDev left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. It's a well-scoped change and the approach is right: the renderer already works on a transparent backdrop, so skipping the final composite is the natural place to hook in. fmt, clippy, check-cli-docs.sh and the new tests all pass here, and the unpremultiply maths checks out.

One bug to fix before merging, a request for a test, and a couple of smaller points.

Pages after the first come out opaque white on the full-page path

SkiaDevice::erase_page (skia_device.rs, just above show_page) still fills the pixmap with Color::WHITE. The interpreter calls it directly after every showpage. When a page is small enough that select_band_height skips banding (roughly two bands or fewer), rendering goes through 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 showpage

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 too, so this hits your use case.

The fix is for erase_page to honour the flag 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.

Test

The new test goes through 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.

Smaller points

  • API shape. render_to_rgba_with_background takes eight positional arguments, two of them adjacent-ish bools, and the name says "background" while the parameter is a yes/no. Since this becomes public API in a published crate, would you mind swapping the bool for a small enum, e.g. PageBackground { White, Transparent }, on both the function and SkiaDevice::set_page_background? Call sites then read unambiguously.
  • 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; …]). Transparent would be consistent with what the caller asked for.

Licensing

stet is dual licensed Apache-2.0 OR MIT. The repo didn't previously say what contributions come in under (I've just added the standard Rust clause to the README and CONTRIBUTING). Since this PR predates that, could you confirm in a comment that you're happy for it to be licensed under Apache-2.0 OR MIT?

Not for this PR

Two things I'll handle on my side rather than ask of you:

  • The stet facade's Interpreter::render still calls render_to_rgba, so library users of the facade can't reach this option yet.
  • Without --transparent, the full-page and banded paths currently start from different backdrops (white vs. transparent), which shows up with non-Normal blend modes. That predates your change, and resolving it may move visual baselines.

Also, a small thing: the commits are authored as seohan@SEOHANui-MacBookPro.local, which GitHub can't link to your account. I'll squash-merge so the credit lands on your GitHub profile. If you'd rather keep the individual commits, re-authoring them with your GitHub email would do it.

@b26354nz

Copy link
Copy Markdown

I have the same requirement for a transparent background. I've developed an application for printing decals on an old ALPS printer - which can print an opaque white underlay on clear decal film - and am using stet-cli to convert pdf to png. I'm using the alpha from the png to create the white spot-colour layer. My slightly naive workaround was to modify composite_onto_white() into composite_onto_black() then change the calling sites for the smallest possible code modification. I was considering generalising with a command-line argument for a background colour (which would be 0,0,0,0 for a transparent background) but could be used to composite over any colour; however, the --transparent arg would suffice for my use case.

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.
@AndyCappDev

Copy link
Copy Markdown
Owner

@jungseohaan: 660a0cf (--cmyk-intent) has landed on this PR. A PR follows its branch, so anything pushed to transparent-background becomes part of it. I'm guessing that wasn't intended?

The finding behind it is a good one. The K100 difference against lcms2 with Japan Color 2001 Coated is real and worth fixing. But it's a separate change to colour management and needs its own review. In particular, stet already reads PDF rendering intents per object (see intent_from_pdf_byte in stet-graphics/src/icc.rs), and I'd like to work out how a global switch should interact with those before settling on a CLI flag. Could you move it to its own branch and open a separate PR? Something like:

git branch cmyk-intent 660a0cf
git push origin cmyk-intent            # then open a PR from it
# once that's pushed, drop the commit from this branch:
git checkout transparent-background
git reset --hard HEAD~1
git push --force-with-lease origin transparent-background

That keeps this PR to --transparent plus the review fixes.

@b26354nz: thanks, that's a great use case. Deriving the white-ink layer from the alpha channel is exactly what the straight-alpha output here is meant to allow. For PDF input, as in your workflow, the current branch already behaves correctly: PDF pages always go through the banded renderer, and the erase_page bug in my review only affects the PostScript/EPS path. A general background colour is a reasonable follow-up, and it's one reason I suggested an enum rather than a bool for the API, since a colour variant can be added later without breaking anything.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants