Skip to content

SVG: write translucent paints as color plus opacity instead of rgba() - #386

Merged
tdewolff merged 1 commit into
tdewolff:masterfrom
aldernero:svg-rgba-opacity
Aug 3, 2026
Merged

SVG: write translucent paints as color plus opacity instead of rgba()#386
tdewolff merged 1 commit into
tdewolff:masterfrom
aldernero:svg-rgba-opacity

Conversation

@aldernero

@aldernero aldernero commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #385.

The SVG renderer wrote translucent paints as fill:rgba(58,130,246,.69803922). SVG 1.1 does not allow a function where a <color> is expected, so renderers implementing SVG 1.1 rather than CSS Color 4 discard the declaration and fall back to the initial fill — black. Browsers accept it, so it only shows up outside a browser (Inkscape, librsvg, most print/CAD toolchains).

Fix

splitAlpha un-premultiplies a color into an opaque #rrggbb plus an opacity in [0,1]. writePaint returns both, and each call site writes the opacity to the matching property — fill-opacity, stroke-opacity, or stop-opacity — only when it is not 1.0. Output for the issue's reproducer:

-<path d="M10 90H90V10H10z" style="fill:rgba(58,130,246,.69803922);stroke:#000"/>
+<path d="M10 90H90V10H10z" style="fill:#3a82f7;fill-opacity:.69803922;stroke:#000"/>

Covered: path fill and stroke in both attribute and style form, the explicitly-drawn stroke path (strokeUnsupported), text fill in RenderText and all three branches of writeFontStyle, and linear/radial gradient stops (stop-color had the same problem).

Not writing the alpha twice

#263 fixed this in Dec 2023 and 7d6baa9 removed it in Jul 2024 as a drive-by while working on #307. I could not find a recorded failure case, but the obvious hazard is emitting the alpha in both rgba() and fill-opacity, which darkens the result. That cannot happen here: splitAlpha returns a color with A = 255, so the alpha exists in exactly one place. TestSplitAlpha pins that, and every case in TestPaintOpacity additionally asserts rgba( never appears in the output, so this cannot regress a third time.

The SVG parser in svg.go already handles fill-opacity, stroke-opacity and stop-opacity, so round-tripping is unaffected.

Verification

inkscape out.svg -o out.png on the reproducer from the issue now samples srgba(57,130,248,0.698039) inside the square, instead of solid black. go test ./renderers/svg/ ./renderers/pdf/ . passes.

One leftover: examples/go-chart/output.svg is a committed render that still contains one rgba(...) from before this change. I left it alone rather than regenerate the example's four output files in this PR — happy to add that if you'd prefer.

🤖 Generated with Claude Code

SVG 1.1 does not allow a function like rgba() where a <color> is
expected, so renderers implementing SVG 1.1 rather than CSS Color 4
discard the value and fall back to the initial fill, black. Split the
alpha off into a separate fill-opacity/stroke-opacity/stop-opacity
property and write the color as plain hex, which both SVG 1.1 renderers
and browsers understand.

The alpha is never written twice: splitAlpha un-premultiplies the color
and returns an opaque one, so the value in the *-opacity property is the
only place the alpha appears. This was the hazard that made 7d6baa9
remove the previous fix for this (tdewolff#263).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tdewolff
tdewolff merged commit 8e86b9a into tdewolff:master Aug 3, 2026
1 check passed
@tdewolff

tdewolff commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Fantastic, thank you for the PR!

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.

SVG renderer emits rgba() for fill/stroke, which SVG 1.1 renderers paint black (regression of #263)

2 participants