Switch the Color struct back to storing unassociated alpha (#4518)

* Switch Color struct back to storing unassociated alpha

* Address review feedback

* Update the Invert node and legacy image migration for straight alpha and add round-trip tests

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Dennis Kobert
2026-09-12 08:22:54 +00:00
committed by GitHub
parent e725f55043
commit 944d00cac5
13 changed files with 103 additions and 139 deletions

View File

@@ -144,12 +144,7 @@ fn make_opaque<T: Adjust<Color>>(
input: Item<T>,
) -> Item<T> {
let mut input = input;
input.element_mut().adjust(|color| {
if color.a() == 0. {
return color.with_alpha(1.);
}
Color::from_rgbaf32_unchecked(color.r() / color.a(), color.g() / color.a(), color.b() / color.a(), 1.)
});
input.element_mut().adjust(|color| color.with_alpha(1.));
input
}
@@ -502,11 +497,7 @@ fn invert<T: Adjust<Color>>(
input: Item<T>,
) -> Item<T> {
let mut input = input;
input.element_mut().adjust(|color| {
// Invert in gamma space relative to alpha
let [r, g, b, a] = color.to_gamma_srgb_channels();
Color::from_gamma_srgb_channels(a - r, a - g, a - b, a)
});
input.element_mut().adjust(|color| color.map_gamma_rgb(|channel| 1. - channel));
input
}
@@ -1147,3 +1138,19 @@ mod _graphene_hash_impls {
SelectiveColorChoice
);
}
#[cfg(all(feature = "std", test))]
mod test {
use super::*;
#[test]
fn invert_flips_straight_channels_and_keeps_alpha() {
let color = Color::from_gamma_srgb_channels(1., 0.25, 0., 0.5);
let inverted = invert((), Item::new_from_element(color)).into_element();
let [r, g, b, a] = inverted.to_gamma_srgb_channels();
assert!((r - 0.).abs() < 1e-5 && (g - 0.75).abs() < 1e-5 && (b - 1.).abs() < 1e-5, "inverted channels were {r} {g} {b}");
assert!((a - 0.5).abs() < 1e-5, "alpha was {a}");
}
}

View File

@@ -115,13 +115,10 @@ fn color_overlay<T: Adjust<Color>>(
let opacity = (opacity / 100.).clamp(0., 1.);
image.element_mut().adjust(|pixel| {
let image = pixel.map_rgb(|channel| channel * (1. - opacity));
let overlay = apply_blend_mode(color, *pixel, blend_mode);
let mix = |image: f32, overlay: f32| image + (overlay - image) * opacity;
// The apply blend mode function divides rgb by the alpha channel for the background. This undoes that.
let associated_pixel = Color::from_rgbaf32_unchecked(pixel.r() * pixel.a(), pixel.g() * pixel.a(), pixel.b() * pixel.a(), pixel.a());
let overlay = apply_blend_mode(color, associated_pixel, blend_mode).map_rgb(|channel| channel * opacity);
Color::from_rgbaf32_unchecked(image.r() + overlay.r(), image.g() + overlay.g(), image.b() + overlay.b(), pixel.a())
Color::from_rgbaf32_unchecked(mix(pixel.r(), overlay.r()), mix(pixel.g(), overlay.g()), mix(pixel.b(), overlay.b()), pixel.a())
});
image
}

View File

@@ -177,7 +177,7 @@ fn gaussian_blur_algorithm(buffer: Image<Color>, radius: f64, gamma: bool) -> Im
unpremultiply_gamma_to_linear(blurred)
} else {
let mut working = buffer;
working.map_pixels(|px| px.apply_opacity(px.a()));
working.map_pixels(|px| px.to_associated_alpha());
let mut blurred = gaussian_separable(working, &kernel, Color::from_rgbaf32_unchecked);
blurred.map_pixels(|px| px.to_unassociated_alpha());
blurred
@@ -191,7 +191,7 @@ fn box_blur_algorithm(buffer: Image<Color>, radius: f64, gamma: bool) -> Image<C
unpremultiply_gamma_to_linear(blurred)
} else {
let mut working = buffer;
working.map_pixels(|px| px.apply_opacity(px.a()));
working.map_pixels(|px| px.to_associated_alpha());
let mut blurred = box_separable(working, radius, Color::from_rgbaf32_unchecked);
blurred.map_pixels(|px| px.to_unassociated_alpha());
blurred

View File

@@ -48,8 +48,10 @@ pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Item
let (image, mut attributes) = image_frame.into_parts();
let (width, height) = (image.width, image.height);
// Resize the image using the image crate
let data = bytemuck::cast_vec(image.into_data().data);
// Resize the image using the image crate, which filters each channel independently, so premultiply to keep transparent texels from bleeding into edges
let mut image = image.into_data();
image.map_pixels(|px| px.to_associated_alpha());
let data = bytemuck::cast_vec(image.data);
let image_size = DAffine2::from_scale(DVec2::new(width as f64, height as f64));
let size_px = image_size.transform_vector2(size).as_uvec2();
@@ -77,12 +79,13 @@ pub fn sample_image(ctx: impl ExtractFootprint + Clone + Send, image_frame: Item
let buffer = resized.to_rgba32f();
let buffer = buffer.into_raw();
let vec = bytemuck::cast_vec(buffer);
let image = Image {
let mut image = Image {
width: new_width,
height: new_height,
data: vec,
base64_string: None,
};
image.map_pixels(|px: Color| px.to_unassociated_alpha());
// we need to adjust the offset if we truncate the offset calculation
let new_transform = image_frame_transform * DAffine2::from_translation(offset) * DAffine2::from_scale(size);
@@ -257,13 +260,7 @@ pub fn image<'a: 'n>(_: impl Ctx, resource: Item<Resource>) -> Item<Raster<CPU>>
};
let image = image.to_rgba32f();
let image = Image {
data: image
.chunks(4)
.map(|pixel| {
let alpha = pixel[3];
Color::from_gamma_srgb_channels(pixel[0] * alpha, pixel[1] * alpha, pixel[2] * alpha, alpha)
})
.collect(),
data: image.chunks(4).map(|pixel| Color::from_gamma_srgb_channels(pixel[0], pixel[1], pixel[2], pixel[3])).collect(),
width: image.width(),
height: image.height(),
..Default::default()