Skip to content

fix(render): interpolate gradients in sRGB and honour premultiplied alpha - #31

Open
leshchenko1979 wants to merge 1 commit into
Macawls:mainfrom
leshchenko1979:fix/gradient-srgb
Open

leshchenko1979 wants to merge 1 commit into
Macawls:mainfrom
leshchenko1979:fix/gradient-srgb

Conversation

@leshchenko1979

Copy link
Copy Markdown
Contributor

What

Gradients were rendered in the wrong colour space, and PNG gradients that fade to a transparent stop came out black instead of transparent.

Fixes #27
Fixes #30

Why

#27 — colour space. CSS Images specifies gradient interpolation in gamma-encoded sRGB. The renderer blended in linear light and emitted color-interpolation="linearRGB" in the SVG defs — legal per SVG 1.1, but not what CSS asks for. A red → blue ramp landed on (188,0,188) at the midpoint; Chromium gives (128,0,128).

#30 — premultiplied alpha. image.RGBA stores premultiplied pixels, but both gradient loops wrote the straight colour beside the raw alpha:

r.img.Pix[off]   = uint8(... s.sr*255 ...)
r.img.Pix[off+3] = s.a

With s.a == 0 that is an invalid premultiplied pixel. Composited onto white, a fade to rgba(0,0,255,0) darkened towards black. The interpolator had the sibling bug of blending straight alpha, so a transparent stop pair swung the hue through a desaturated middle instead of holding the visible colour.

How

  • render/background.go: color-interpolation="sRGB" for both <linearGradient> and <radialGradient>.
  • render/png.go: the linear and radial loops now blend in premultiplied sRGB (interpolateStops, replacing interpolateLinear + the linearToSrgbF round-trip) and store RGB scaled by alpha. The dead sRGB LUT is gone.
  • Five golden SVGs regenerated with -update; the diff is 5 lines, all linearRGB → sRGB.

Verification

21-case Chromium comparison harness (% of pixels differing by >8 per channel), before → after:

case before after
09-gradient 19.44 0.00
all other 20 cases unchanged unchanged

Alpha ramp rgba(255,0,0,1) → rgba(0,0,255,0) sampled on a white composite, Ogre vs Chromium:

x t Chromium G Ogre G Chromium a Ogre a
10 0.025 7 6 0.973 0.976
50 0.125 32 32 0.875 0.875
100 0.250 64 64 0.749 0.749
200 0.500 128 127 0.498 0.502
300 0.750 192 191 0.247 0.251
395 0.988 252 251 0.012 0.016

Within 1/255 across the ramp.

New tests in render/gradient_test.go, each confirmed to fail against the previous code:

  • TestSVGLinearGradientUsesSRGB, TestSVGRadialGradientUsesSRGB — fail with the old defs (linearRGB).
  • TestPNGGradientMidpointIsSRGB — fails on the old PNG path with midpoint red = 187, want ~128.
  • TestPNGGradientAlphaIsPremultiplied — every pixel must satisfy channel <= alpha; the transparent end must stay near-white on white.
  • TestGradientInterpolationIsPremultiplied — a red → transparent blue midpoint must keep red and drop blue.

go test ./... and go vet ./... pass.

…lpha

CSS gradients interpolate in gamma-encoded sRGB, but the renderer blended in
linear light and the SVG output asked for `color-interpolation="linearRGB"`,
which is the SVG 1.1 default rather than what CSS specifies. Every gradient
came out too bright: a red-to-blue ramp hit (188,0,188) at the midpoint where
browsers give (128,0,128).

The PNG path had a second defect: `image.RGBA` stores premultiplied pixels, but
the loops wrote the straight colour next to the raw alpha. With `alpha == 0`
that produces an invalid pixel, so a gradient fading to a transparent stop read
back as a fade to black instead of to white. The interpolator also blended
straight alpha, swinging the hue of a partially transparent stop pair.

Both loops (linear and radial) now blend in premultiplied sRGB and store
premultiplied RGB; the SVG defs request sRGB.

Fixes Macawls#27
Fixes Macawls#30
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.

PNG gradients fade to black instead of transparent (premultiplied alpha not honoured) linear-gradient interpolates in linearRGB instead of sRGB

1 participant