diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 4925fa070d8..39ce7899e53 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -802,8 +802,6 @@ impl MessageHandler> for DocumentMes parent_and_insert_index, place_at_origin, } => { - // All the image's pixels have been converted to 0..=1, linear, and premultiplied by `Color::from_rgba8_srgb` - let layer_parent = self.new_layer_parent(true); let image_size = DVec2::new(image.width as f64, image.height as f64); diff --git a/editor/src/messages/portfolio/document_migration.rs b/editor/src/messages/portfolio/document_migration.rs index c83e51518ab..792aa807233 100644 --- a/editor/src/messages/portfolio/document_migration.rs +++ b/editor/src/messages/portfolio/document_migration.rs @@ -2123,7 +2123,9 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId], _ => None, }); - if let Some(image) = image { + if let Some(mut image) = image { + // Legacy embedded pixel data is premultiplied, so restore straight alpha before encoding it + image.data.iter_mut().for_each(|pixel| *pixel = pixel.to_unassociated_alpha()); let hash = document.resources.embedded.store(Resource::new(image.to_png())); let resource_id = ResourceId::new(); diff --git a/node-graph/libraries/no-std-types/src/blending.rs b/node-graph/libraries/no-std-types/src/blending.rs index cd4cd1116ec..ea3a5763bb5 100644 --- a/node-graph/libraries/no-std-types/src/blending.rs +++ b/node-graph/libraries/no-std-types/src/blending.rs @@ -201,7 +201,7 @@ pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode, blend_mode => apply_blend_mode(foreground, background, blend_mode), }; - background.alpha_blend(target_color.apply_opacity(opacity)) + background.alpha_blend(target_color.with_alpha(target_color.a() * opacity)) } /// Mixes the two colors by the blend mode's own formula, leaving the alpha compositing to the caller. @@ -283,10 +283,10 @@ mod tests { } #[test] - fn darker_color_compares_unassociated_channels() { - // The premultiplied backdrop reads as 0.1 gray but is really 0.5 gray, so the 0.4 gray foreground is the darker color + fn darker_color_ignores_backdrop_alpha() { + // The backdrop's low alpha doesn't darken its color, so the 0.4 gray foreground is the darker color let foreground = Color::from_rgbaf32_unchecked(0.4, 0.4, 0.4, 1.); - let background = Color::from_rgbaf32_unchecked(0.1, 0.1, 0.1, 0.2); + let background = Color::from_rgbaf32_unchecked(0.5, 0.5, 0.5, 0.2); let blended = apply_blend_mode(foreground, background, BlendMode::DarkerColor); @@ -294,6 +294,18 @@ mod tests { assert!((blended.a() - 1.).abs() < 1e-5, "alpha was {}", blended.a()); } + #[test] + fn source_over_weights_straight_colors_by_alpha() { + let over = Color::from_rgbaf32_unchecked(1., 0., 0., 0.5); + let under = Color::from_rgbaf32_unchecked(0., 0., 1., 1.); + + let blended = under.alpha_blend(over); + + assert!((blended.r() - 0.5).abs() < 1e-5, "red was {}", blended.r()); + assert!((blended.b() - 0.5).abs() < 1e-5, "blue was {}", blended.b()); + assert!((blended.a() - 1.).abs() < 1e-5, "alpha was {}", blended.a()); + } + #[test] fn alpha_only_modes_fade_with_opacity() { let foreground = Color::from_rgbaf32_unchecked(0.9, 0.9, 0.9, 1.); diff --git a/node-graph/libraries/no-std-types/src/color/color_traits.rs b/node-graph/libraries/no-std-types/src/color/color_traits.rs index 945cd9e7add..5870b52516b 100644 --- a/node-graph/libraries/no-std-types/src/color/color_traits.rs +++ b/node-graph/libraries/no-std-types/src/color/color_traits.rs @@ -123,14 +123,6 @@ pub trait RGBMut: RGB { fn set_blue(&mut self, blue: Self::ColorChannel); } -pub trait AssociatedAlpha: RGB + Alpha { - fn to_unassociated(&self) -> Out; -} - -pub trait UnassociatedAlpha: RGB + Alpha { - fn to_associated(&self) -> Out; -} - pub trait Alpha { type AlphaChannel: LinearChannel; const TRANSPARENT: Self; diff --git a/node-graph/libraries/no-std-types/src/color/color_types.rs b/node-graph/libraries/no-std-types/src/color/color_types.rs index 13bbe379121..b35038ec54f 100644 --- a/node-graph/libraries/no-std-types/src/color/color_types.rs +++ b/node-graph/libraries/no-std-types/src/color/color_types.rs @@ -1,4 +1,4 @@ -use super::color_traits::{Alpha, AlphaMut, AssociatedAlpha, Luminance, Pixel, RGB, RGBMut, Rec709Primaries, SRGB}; +use super::color_traits::{Alpha, AlphaMut, Luminance, Pixel, RGB, RGBMut, Rec709Primaries, SRGB}; use super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float}; use bytemuck::{Pod, Zeroable}; use core::fmt::Debug; @@ -72,7 +72,7 @@ impl Alpha for RGBA16F { type AlphaChannel = f32; #[inline(always)] fn alpha(&self) -> f32 { - self.alpha.to_f32() / 255. + self.alpha.to_f32() } const TRANSPARENT: Self = RGBA16F { @@ -83,9 +83,8 @@ impl Alpha for RGBA16F { }; fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self { - let alpha = alpha * 255.; let mut result = *self; - result.alpha = f16::from_f32(alpha * self.alpha()); + result.alpha = f16::from_f32(self.alpha() * alpha); result } } @@ -254,7 +253,7 @@ impl RGB for Luma { impl Pixel for Luma {} -/// Linear-light sRGB color with `f32` channels (alpha unassociated for swatch/UI colors, associated/premultiplied for pixel data inside [`Image`]). +/// Linear-light sRGB color with `f32` channels and unassociated (straight) alpha. /// /// Channels range from `0.` to `f32::MAX`, encoding brightness proportional to light intensity (cd/m² nits in HDR, or `0..=1` mapped to white for SDR). /// @@ -359,9 +358,7 @@ impl Pixel for Color { } fn from_bytes(bytes: &[u8]) -> Self { - // `Image` pixel convention is linear-light with associated (premultiplied) alpha. - let srgba = SRGBA8::new(bytes[0], bytes[1], bytes[2], bytes[3]); - Color::from(srgba).apply_opacity(bytes[3] as f32 / 255.) + SRGBA8::new(bytes[0], bytes[1], bytes[2], bytes[3]).into() } fn byte_size() -> usize { 4 @@ -378,18 +375,7 @@ impl Alpha for Color { } #[inline(always)] fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self { - Self { - red: self.red * alpha, - green: self.green * alpha, - blue: self.blue * alpha, - alpha: self.alpha * alpha, - } - } -} - -impl AssociatedAlpha for Color { - fn to_unassociated(&self) -> Out { - todo!() + Self { alpha: self.alpha * alpha, ..*self } } } @@ -443,12 +429,6 @@ impl Color { Color { red, green, blue, alpha } } - /// Construct a `Color` from unassociated (straight) RGBA channels, premultiplying the RGB channels by alpha. - #[inline(always)] - pub fn new_from_unassociated_rgba(red: f32, green: f32, blue: f32, alpha: f32) -> Color { - Color::from_rgbaf32_unchecked(red * alpha, green * alpha, blue * alpha, alpha) - } - /// Create a linear-light `Color` from HSL coordinates (all between 0 and 1). /// HSL is defined on sRGB display values, so the RGB produced by the HSL math is gamma-encoded and decoded to linear before being wrapped in `Color`. /// @@ -707,8 +687,7 @@ impl Color { /// Whole-color "Darker Color" blend: keeps whichever color has the lower mean RGB, with `other`'s alpha. #[inline(always)] pub fn blend_darker_color(&self, other: Color) -> Color { - let background = self.to_unassociated_alpha(); - let darker = if background.average_rgb_channels() <= other.average_rgb_channels() { background } else { other }; + let darker = if self.average_rgb_channels() <= other.average_rgb_channels() { *self } else { other }; darker.with_alpha(other.alpha) } @@ -740,8 +719,7 @@ impl Color { /// Whole-color "Lighter Color" blend: keeps whichever color has the higher mean RGB, with `other`'s alpha. #[inline(always)] pub fn blend_lighter_color(&self, other: Color) -> Color { - let background = self.to_unassociated_alpha(); - let lighter = if background.average_rgb_channels() >= other.average_rgb_channels() { background } else { other }; + let lighter = if self.average_rgb_channels() >= other.average_rgb_channels() { *self } else { other }; lighter.with_alpha(other.alpha) } @@ -824,25 +802,23 @@ impl Color { /// Whole-color "Hue" blend: source hue with this color's saturation and Rec.601 luma, with `c_s`'s alpha. pub fn blend_hue(&self, c_s: Color) -> Color { - let background = self.to_unassociated_alpha(); - let sat_b = background.chroma_range(); - let lum_b = background.luminance_rec_601(); + let sat_b = self.chroma_range(); + let lum_b = self.luminance_rec_601(); c_s.with_saturation(sat_b).with_luminance(lum_b).with_alpha(c_s.alpha) } /// Whole-color "Saturation" blend: this color's hue/luma with source saturation, with `c_s`'s alpha. pub fn blend_saturation(&self, c_s: Color) -> Color { - let background = self.to_unassociated_alpha(); let sat_s = c_s.chroma_range(); - let lum_b = background.luminance_rec_601(); + let lum_b = self.luminance_rec_601(); - background.with_saturation(sat_s).with_luminance(lum_b).with_alpha(c_s.alpha) + self.with_saturation(sat_s).with_luminance(lum_b).with_alpha(c_s.alpha) } /// Whole-color "Color" blend: source hue/saturation with this color's luma, with `c_s`'s alpha. pub fn blend_color(&self, c_s: Color) -> Color { - let lum_b = self.to_unassociated_alpha().luminance_rec_601(); + let lum_b = self.luminance_rec_601(); c_s.with_luminance(lum_b).with_alpha(c_s.alpha) } @@ -851,7 +827,7 @@ impl Color { pub fn blend_luminosity(&self, c_s: Color) -> Color { let lum_s = c_s.luminance_rec_601(); - self.to_unassociated_alpha().with_luminance(lum_s).with_alpha(c_s.alpha) + self.with_luminance(lum_s).with_alpha(c_s.alpha) } /// All four channels as `(red, green, blue, alpha)`. @@ -990,13 +966,13 @@ impl Color { Self::from_rgbaf32_unchecked(f(self.r()), f(self.g()), f(self.b()), self.a()) } - /// Multiply all four channels (including alpha) by `opacity`, applying an additional premultiplication factor to this Color. + /// Multiply RGB by alpha, giving the associated (premultiplied) form for compositing and filtering. #[inline(always)] - pub fn apply_opacity(&self, opacity: f32) -> Self { - Self::from_rgbaf32_unchecked(self.r() * opacity, self.g() * opacity, self.b() * opacity, self.a() * opacity) + pub fn to_associated_alpha(&self) -> Self { + self.map_rgb(|channel| channel * self.alpha) } - /// Divide RGB by alpha to recover unassociated (straight-alpha) channels; no-op if alpha is zero. + /// Divide RGB by alpha, undoing [`Self::to_associated_alpha`]; no-op if alpha is zero. #[inline(always)] pub fn to_unassociated_alpha(&self) -> Self { if self.alpha == 0. { @@ -1011,27 +987,30 @@ impl Color { } } - /// Apply a per-channel blend function to this color (unmultiplied) and `other`, returning a color with `other`'s alpha; channels are clamped to 0..1. + /// Apply a per-channel blend function to this color and `other`, returning a color with `other`'s alpha; channels are clamped to 0..1. #[inline(always)] pub fn blend_rgb f32>(&self, other: Color, f: F) -> Self { - let background = self.to_unassociated_alpha(); Color { - red: f(background.red, other.red).clamp(0., 1.), - green: f(background.green, other.green).clamp(0., 1.), - blue: f(background.blue, other.blue).clamp(0., 1.), + red: f(self.red, other.red).clamp(0., 1.), + green: f(self.green, other.green).clamp(0., 1.), + blue: f(self.blue, other.blue).clamp(0., 1.), alpha: other.alpha, } } - /// Porter-Duff "source over" composite of `other` over `self`. Both colors must use associated (premultiplied) alpha. + /// Porter-Duff "source over" composite of `other` over `self`. #[inline(always)] pub fn alpha_blend(&self, other: Color) -> Self { - let inv_alpha = 1. - other.alpha; + let under_weight = self.alpha * (1. - other.alpha); + let alpha = other.alpha + under_weight; + if alpha == 0. { + return Self::TRANSPARENT; + } Self { - red: self.red * inv_alpha + other.red, - green: self.green * inv_alpha + other.green, - blue: self.blue * inv_alpha + other.blue, - alpha: self.alpha * inv_alpha + other.alpha, + red: (other.red * other.alpha + self.red * under_weight) / alpha, + green: (other.green * other.alpha + self.green * under_weight) / alpha, + blue: (other.blue * other.alpha + self.blue * under_weight) / alpha, + alpha, } } diff --git a/node-graph/libraries/raster-types/src/image.rs b/node-graph/libraries/raster-types/src/image.rs index 16d4d5955a7..e87117a7b3a 100644 --- a/node-graph/libraries/raster-types/src/image.rs +++ b/node-graph/libraries/raster-types/src/image.rs @@ -144,14 +144,7 @@ impl Image

{ impl Image { /// Generate Image from some frontend image data (the canvas pixels as u8s in a flat array) pub fn from_image_data(image_data: &[u8], width: u32, height: u32) -> Self { - let data = image_data - .chunks_exact(4) - .map(|v| { - // `Image` pixels are stored linear-light with premultiplied alpha - let srgba = SRGBA8::new(v[0], v[1], v[2], v[3]); - Color::from(srgba).apply_opacity(v[3] as f32 / 255.) - }) - .collect(); + let data = image_data.chunks_exact(4).map(|v| SRGBA8::new(v[0], v[1], v[2], v[3]).into()).collect(); Image { width, height, @@ -171,7 +164,7 @@ impl Image { } use super::*; -impl Image

+impl Image

where P::ColorChannel: Linear,

::AlphaChannel: Linear, @@ -195,10 +188,9 @@ where // Smaller alpha values than this would map to fully transparent // anyway, avoid expensive encoding. if a >= 0.5 / 255. { - let undo_premultiply = 1. / a; - let r = color.r().to_f32() * undo_premultiply; - let g = color.g().to_f32() * undo_premultiply; - let b = color.b().to_f32() * undo_premultiply; + let r = color.r().to_f32(); + let g = color.g().to_f32(); + let b = color.b().to_f32(); // Compute new sRGB value if necessary. if r != last_r { @@ -287,4 +279,14 @@ mod test { assert_eq!(image, deserialized); } + + #[test] + fn image_data_round_trips_translucent_pixels() { + use super::*; + let bytes = [255, 0, 0, 128, 0, 255, 0, 1, 255, 255, 255, 41, 10, 20, 30, 255]; + + let image = Image::from_image_data(&bytes, 4, 1); + + assert_eq!(image.to_flat_u8().0, bytes); + } } diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 932bf144484..e17a68c241c 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -22,7 +22,7 @@ use dyn_any::DynAny; use glam::{DAffine2, DMat2, DVec2}; use graphene_hash::CacheHashWrapper; use graphene_resource::Resource; -use graphic_types::raster_types::{BitmapMut, CPU, GPU, Image, Raster, Texture}; +use graphic_types::raster_types::{CPU, GPU, Image, Raster, Texture}; use graphic_types::vector_types::gradient::{Gradient, GradientForm}; use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint}; use graphic_types::vector_types::vector::misc::dvec2_to_point; @@ -130,9 +130,7 @@ fn composite_paint_over(over: Color, under: Color, blend_mode: BlendMode) -> Col return Color::TRANSPARENT; } - // The blend formulas read their backdrop premultiplied - let premultiplied_under = Color::from_rgbaf32_unchecked(under.r() * under_alpha, under.g() * under_alpha, under.b() * under_alpha, under_alpha); - let mixed = apply_blend_mode(over, premultiplied_under, blend_mode); + let mixed = apply_blend_mode(over, under, blend_mode); // The mode only mixes where the backdrop has coverage, so its alpha interpolates each source channel from the raw color to the mixed color let source_channel = |over_channel: f32, mixed_channel: f32| over_channel * (1. - under_alpha) + mixed_channel * under_alpha; @@ -428,17 +426,8 @@ fn singular_values(transform: DAffine2) -> (f64, f64) { pub fn black_or_white_for_best_contrast(background: Option) -> Color { let Some(bg) = background else { return core_types::consts::LAYER_OUTLINE_STROKE_COLOR }; - let alpha = bg.a(); - - // Un-premultiply, then encode to gamma sRGB to do the composite in display space. - let (gamma_r, gamma_g, gamma_b) = if alpha > f32::EPSILON { - let [r, g, b, _] = Color::from_rgbaf32_unchecked(bg.r() / alpha, bg.g() / alpha, bg.b() / alpha, alpha).to_gamma_srgb_channels(); - (r, g, b) - } else { - (0., 0., 0.) - }; - - // Composite over black in sRGB space (premultiplied by alpha), then decode to linear for the luminance test. + // Composite over black in gamma sRGB space, then decode to linear for the luminance test. + let [gamma_r, gamma_g, gamma_b, alpha] = bg.to_gamma_srgb_channels(); let composited = Color::from_gamma_srgb_channels(gamma_r * alpha, gamma_g * alpha, gamma_b * alpha, 1.); let threshold = (1.05 * 0.05f32).sqrt() - 0.05; @@ -2295,9 +2284,7 @@ fn render_raster_cpu_item_svg(item: ItemRef<'_, Raster>, render: &mut SvgRe } if render_params.to_canvas() { - let mut image_copy = image.clone(); - image_copy.data_mut().map_pixels(|p| p.to_unassociated_alpha()); - let id = *render.image_data.entry(CacheHashWrapper(image_copy.into_data())).or_insert_with(generate_uuid); + let id = *render.image_data.entry(CacheHashWrapper(image.clone().into_data())).or_insert_with(generate_uuid); render.parent_tag( "foreignObject", diff --git a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs index 1411a57f6b3..93fb12da34d 100644 --- a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs +++ b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs @@ -115,9 +115,7 @@ impl RasterGpuToRasterCpuConverter { let start = row * row_stride; let row_slice = &view[start..start + row_bytes]; for px in row_slice.chunks_exact(4) { - // `Image` pixels are stored linear-light with associated (premultiplied) alpha - let srgba = SRGBA8::new(px[0], px[1], px[2], px[3]); - cpu_data.push(Color::from(srgba).apply_opacity(px[3] as f32 / 255.)); + cpu_data.push(SRGBA8::new(px[0], px[1], px[2], px[3]).into()); } } diff --git a/node-graph/nodes/gstd/src/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index 5ed9eb366a2..269c1dc380e 100644 --- a/node-graph/nodes/gstd/src/platform_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -171,14 +171,7 @@ fn decode_image(_: impl Ctx, data: Item) -> Item> { }; let image = image.to_rgba32f(); let image = Image { - data: image - .chunks(4) - .map(|pixel| { - // Decoded bytes are unassociated gamma sRGB; premultiply in gamma then lift to linear - let a = pixel[3]; - Color::from_gamma_srgb_channels(pixel[0] * a, pixel[1] * a, pixel[2] * a, a) - }) - .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() diff --git a/node-graph/nodes/raster/src/adjustments.rs b/node-graph/nodes/raster/src/adjustments.rs index 4367089d61f..e86cf04fe9e 100644 --- a/node-graph/nodes/raster/src/adjustments.rs +++ b/node-graph/nodes/raster/src/adjustments.rs @@ -144,12 +144,7 @@ fn make_opaque>( input: Item, ) -> Item { 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>( input: Item, ) -> Item { 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}"); + } +} diff --git a/node-graph/nodes/raster/src/blending_nodes.rs b/node-graph/nodes/raster/src/blending_nodes.rs index 7ed3c829caf..0340b068834 100644 --- a/node-graph/nodes/raster/src/blending_nodes.rs +++ b/node-graph/nodes/raster/src/blending_nodes.rs @@ -115,13 +115,10 @@ fn color_overlay>( 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 } diff --git a/node-graph/nodes/raster/src/filter.rs b/node-graph/nodes/raster/src/filter.rs index bfe2001f2ee..58375b601b0 100644 --- a/node-graph/nodes/raster/src/filter.rs +++ b/node-graph/nodes/raster/src/filter.rs @@ -177,7 +177,7 @@ fn gaussian_blur_algorithm(buffer: Image, 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, radius: f64, gamma: bool) -> Image(_: impl Ctx, resource: Item) -> Item> }; 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()