mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 15:38:12 +08:00
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:
co-authored by
Keavon Chambers
parent
e725f55043
commit
944d00cac5
@@ -802,8 +802,6 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
|||||||
parent_and_insert_index,
|
parent_and_insert_index,
|
||||||
place_at_origin,
|
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 layer_parent = self.new_layer_parent(true);
|
||||||
let image_size = DVec2::new(image.width as f64, image.height as f64);
|
let image_size = DVec2::new(image.width as f64, image.height as f64);
|
||||||
|
|
||||||
|
|||||||
@@ -2123,7 +2123,9 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
|||||||
_ => None,
|
_ => 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 hash = document.resources.embedded.store(Resource::new(image.to_png()));
|
||||||
|
|
||||||
let resource_id = ResourceId::new();
|
let resource_id = ResourceId::new();
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ pub fn blend_colors(foreground: Color, background: Color, blend_mode: BlendMode,
|
|||||||
blend_mode => apply_blend_mode(foreground, background, blend_mode),
|
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.
|
/// 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]
|
#[test]
|
||||||
fn darker_color_compares_unassociated_channels() {
|
fn darker_color_ignores_backdrop_alpha() {
|
||||||
// The premultiplied backdrop reads as 0.1 gray but is really 0.5 gray, so the 0.4 gray foreground is the darker color
|
// 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 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);
|
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());
|
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]
|
#[test]
|
||||||
fn alpha_only_modes_fade_with_opacity() {
|
fn alpha_only_modes_fade_with_opacity() {
|
||||||
let foreground = Color::from_rgbaf32_unchecked(0.9, 0.9, 0.9, 1.);
|
let foreground = Color::from_rgbaf32_unchecked(0.9, 0.9, 0.9, 1.);
|
||||||
|
|||||||
@@ -123,14 +123,6 @@ pub trait RGBMut: RGB {
|
|||||||
fn set_blue(&mut self, blue: Self::ColorChannel);
|
fn set_blue(&mut self, blue: Self::ColorChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait AssociatedAlpha: RGB + Alpha {
|
|
||||||
fn to_unassociated<Out: UnassociatedAlpha>(&self) -> Out;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait UnassociatedAlpha: RGB + Alpha {
|
|
||||||
fn to_associated<Out: AssociatedAlpha>(&self) -> Out;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Alpha {
|
pub trait Alpha {
|
||||||
type AlphaChannel: LinearChannel;
|
type AlphaChannel: LinearChannel;
|
||||||
const TRANSPARENT: Self;
|
const TRANSPARENT: Self;
|
||||||
|
|||||||
@@ -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 super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float};
|
||||||
use bytemuck::{Pod, Zeroable};
|
use bytemuck::{Pod, Zeroable};
|
||||||
use core::fmt::Debug;
|
use core::fmt::Debug;
|
||||||
@@ -72,7 +72,7 @@ impl Alpha for RGBA16F {
|
|||||||
type AlphaChannel = f32;
|
type AlphaChannel = f32;
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn alpha(&self) -> f32 {
|
fn alpha(&self) -> f32 {
|
||||||
self.alpha.to_f32() / 255.
|
self.alpha.to_f32()
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRANSPARENT: Self = RGBA16F {
|
const TRANSPARENT: Self = RGBA16F {
|
||||||
@@ -83,9 +83,8 @@ impl Alpha for RGBA16F {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self {
|
fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self {
|
||||||
let alpha = alpha * 255.;
|
|
||||||
let mut result = *self;
|
let mut result = *self;
|
||||||
result.alpha = f16::from_f32(alpha * self.alpha());
|
result.alpha = f16::from_f32(self.alpha() * alpha);
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,7 +253,7 @@ impl RGB for Luma {
|
|||||||
|
|
||||||
impl Pixel 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<Color>`]).
|
/// 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).
|
/// 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 {
|
fn from_bytes(bytes: &[u8]) -> Self {
|
||||||
// `Image<Color>` pixel convention is linear-light with associated (premultiplied) alpha.
|
SRGBA8::new(bytes[0], bytes[1], bytes[2], bytes[3]).into()
|
||||||
let srgba = SRGBA8::new(bytes[0], bytes[1], bytes[2], bytes[3]);
|
|
||||||
Color::from(srgba).apply_opacity(bytes[3] as f32 / 255.)
|
|
||||||
}
|
}
|
||||||
fn byte_size() -> usize {
|
fn byte_size() -> usize {
|
||||||
4
|
4
|
||||||
@@ -378,18 +375,7 @@ impl Alpha for Color {
|
|||||||
}
|
}
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self {
|
fn multiplied_alpha(&self, alpha: Self::AlphaChannel) -> Self {
|
||||||
Self {
|
Self { alpha: self.alpha * alpha, ..*self }
|
||||||
red: self.red * alpha,
|
|
||||||
green: self.green * alpha,
|
|
||||||
blue: self.blue * alpha,
|
|
||||||
alpha: self.alpha * alpha,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AssociatedAlpha for Color {
|
|
||||||
fn to_unassociated<Out: super::UnassociatedAlpha>(&self) -> Out {
|
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,12 +429,6 @@ impl Color {
|
|||||||
Color { red, green, blue, alpha }
|
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).
|
/// 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`.
|
/// 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.
|
/// Whole-color "Darker Color" blend: keeps whichever color has the lower mean RGB, with `other`'s alpha.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_darker_color(&self, other: Color) -> Color {
|
pub fn blend_darker_color(&self, other: Color) -> Color {
|
||||||
let background = self.to_unassociated_alpha();
|
let darker = if self.average_rgb_channels() <= other.average_rgb_channels() { *self } else { other };
|
||||||
let darker = if background.average_rgb_channels() <= other.average_rgb_channels() { background } else { other };
|
|
||||||
|
|
||||||
darker.with_alpha(other.alpha)
|
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.
|
/// Whole-color "Lighter Color" blend: keeps whichever color has the higher mean RGB, with `other`'s alpha.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn blend_lighter_color(&self, other: Color) -> Color {
|
pub fn blend_lighter_color(&self, other: Color) -> Color {
|
||||||
let background = self.to_unassociated_alpha();
|
let lighter = if self.average_rgb_channels() >= other.average_rgb_channels() { *self } else { other };
|
||||||
let lighter = if background.average_rgb_channels() >= other.average_rgb_channels() { background } else { other };
|
|
||||||
|
|
||||||
lighter.with_alpha(other.alpha)
|
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.
|
/// 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 {
|
pub fn blend_hue(&self, c_s: Color) -> Color {
|
||||||
let background = self.to_unassociated_alpha();
|
let sat_b = self.chroma_range();
|
||||||
let sat_b = background.chroma_range();
|
let lum_b = self.luminance_rec_601();
|
||||||
let lum_b = background.luminance_rec_601();
|
|
||||||
|
|
||||||
c_s.with_saturation(sat_b).with_luminance(lum_b).with_alpha(c_s.alpha)
|
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.
|
/// 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 {
|
pub fn blend_saturation(&self, c_s: Color) -> Color {
|
||||||
let background = self.to_unassociated_alpha();
|
|
||||||
let sat_s = c_s.chroma_range();
|
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.
|
/// 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 {
|
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)
|
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 {
|
pub fn blend_luminosity(&self, c_s: Color) -> Color {
|
||||||
let lum_s = c_s.luminance_rec_601();
|
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)`.
|
/// 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())
|
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)]
|
#[inline(always)]
|
||||||
pub fn apply_opacity(&self, opacity: f32) -> Self {
|
pub fn to_associated_alpha(&self) -> Self {
|
||||||
Self::from_rgbaf32_unchecked(self.r() * opacity, self.g() * opacity, self.b() * opacity, self.a() * opacity)
|
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)]
|
#[inline(always)]
|
||||||
pub fn to_unassociated_alpha(&self) -> Self {
|
pub fn to_unassociated_alpha(&self) -> Self {
|
||||||
if self.alpha == 0. {
|
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)]
|
#[inline(always)]
|
||||||
pub fn blend_rgb<F: Fn(f32, f32) -> f32>(&self, other: Color, f: F) -> Self {
|
pub fn blend_rgb<F: Fn(f32, f32) -> f32>(&self, other: Color, f: F) -> Self {
|
||||||
let background = self.to_unassociated_alpha();
|
|
||||||
Color {
|
Color {
|
||||||
red: f(background.red, other.red).clamp(0., 1.),
|
red: f(self.red, other.red).clamp(0., 1.),
|
||||||
green: f(background.green, other.green).clamp(0., 1.),
|
green: f(self.green, other.green).clamp(0., 1.),
|
||||||
blue: f(background.blue, other.blue).clamp(0., 1.),
|
blue: f(self.blue, other.blue).clamp(0., 1.),
|
||||||
alpha: other.alpha,
|
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)]
|
#[inline(always)]
|
||||||
pub fn alpha_blend(&self, other: Color) -> Self {
|
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 {
|
Self {
|
||||||
red: self.red * inv_alpha + other.red,
|
red: (other.red * other.alpha + self.red * under_weight) / alpha,
|
||||||
green: self.green * inv_alpha + other.green,
|
green: (other.green * other.alpha + self.green * under_weight) / alpha,
|
||||||
blue: self.blue * inv_alpha + other.blue,
|
blue: (other.blue * other.alpha + self.blue * under_weight) / alpha,
|
||||||
alpha: self.alpha * inv_alpha + other.alpha,
|
alpha,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,14 +144,7 @@ impl<P: Pixel> Image<P> {
|
|||||||
impl Image<Color> {
|
impl Image<Color> {
|
||||||
/// Generate Image from some frontend image data (the canvas pixels as u8s in a flat array)
|
/// 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 {
|
pub fn from_image_data(image_data: &[u8], width: u32, height: u32) -> Self {
|
||||||
let data = image_data
|
let data = image_data.chunks_exact(4).map(|v| SRGBA8::new(v[0], v[1], v[2], v[3]).into()).collect();
|
||||||
.chunks_exact(4)
|
|
||||||
.map(|v| {
|
|
||||||
// `Image<Color>` 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();
|
|
||||||
Image {
|
Image {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -171,7 +164,7 @@ impl Image<Color> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
impl<P: Alpha + RGB + AssociatedAlpha> Image<P>
|
impl<P: Alpha + RGB> Image<P>
|
||||||
where
|
where
|
||||||
P::ColorChannel: Linear,
|
P::ColorChannel: Linear,
|
||||||
<P as Alpha>::AlphaChannel: Linear,
|
<P as Alpha>::AlphaChannel: Linear,
|
||||||
@@ -195,10 +188,9 @@ where
|
|||||||
// Smaller alpha values than this would map to fully transparent
|
// Smaller alpha values than this would map to fully transparent
|
||||||
// anyway, avoid expensive encoding.
|
// anyway, avoid expensive encoding.
|
||||||
if a >= 0.5 / 255. {
|
if a >= 0.5 / 255. {
|
||||||
let undo_premultiply = 1. / a;
|
let r = color.r().to_f32();
|
||||||
let r = color.r().to_f32() * undo_premultiply;
|
let g = color.g().to_f32();
|
||||||
let g = color.g().to_f32() * undo_premultiply;
|
let b = color.b().to_f32();
|
||||||
let b = color.b().to_f32() * undo_premultiply;
|
|
||||||
|
|
||||||
// Compute new sRGB value if necessary.
|
// Compute new sRGB value if necessary.
|
||||||
if r != last_r {
|
if r != last_r {
|
||||||
@@ -287,4 +279,14 @@ mod test {
|
|||||||
|
|
||||||
assert_eq!(image, deserialized);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use dyn_any::DynAny;
|
|||||||
use glam::{DAffine2, DMat2, DVec2};
|
use glam::{DAffine2, DMat2, DVec2};
|
||||||
use graphene_hash::CacheHashWrapper;
|
use graphene_hash::CacheHashWrapper;
|
||||||
use graphene_resource::Resource;
|
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::gradient::{Gradient, GradientForm};
|
||||||
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
|
use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint};
|
||||||
use graphic_types::vector_types::vector::misc::dvec2_to_point;
|
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;
|
return Color::TRANSPARENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The blend formulas read their backdrop premultiplied
|
let mixed = apply_blend_mode(over, under, blend_mode);
|
||||||
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);
|
|
||||||
|
|
||||||
// The mode only mixes where the backdrop has coverage, so its alpha interpolates each source channel from the raw color to the mixed color
|
// 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;
|
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>) -> Color {
|
pub fn black_or_white_for_best_contrast(background: Option<Color>) -> Color {
|
||||||
let Some(bg) = background else { return core_types::consts::LAYER_OUTLINE_STROKE_COLOR };
|
let Some(bg) = background else { return core_types::consts::LAYER_OUTLINE_STROKE_COLOR };
|
||||||
|
|
||||||
let alpha = bg.a();
|
// 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();
|
||||||
// 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.
|
|
||||||
let composited = Color::from_gamma_srgb_channels(gamma_r * alpha, gamma_g * alpha, gamma_b * alpha, 1.);
|
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;
|
let threshold = (1.05 * 0.05f32).sqrt() - 0.05;
|
||||||
@@ -2295,9 +2284,7 @@ fn render_raster_cpu_item_svg(item: ItemRef<'_, Raster<CPU>>, render: &mut SvgRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
if render_params.to_canvas() {
|
if render_params.to_canvas() {
|
||||||
let mut image_copy = image.clone();
|
let id = *render.image_data.entry(CacheHashWrapper(image.clone().into_data())).or_insert_with(generate_uuid);
|
||||||
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);
|
|
||||||
|
|
||||||
render.parent_tag(
|
render.parent_tag(
|
||||||
"foreignObject",
|
"foreignObject",
|
||||||
|
|||||||
@@ -115,9 +115,7 @@ impl RasterGpuToRasterCpuConverter {
|
|||||||
let start = row * row_stride;
|
let start = row * row_stride;
|
||||||
let row_slice = &view[start..start + row_bytes];
|
let row_slice = &view[start..start + row_bytes];
|
||||||
for px in row_slice.chunks_exact(4) {
|
for px in row_slice.chunks_exact(4) {
|
||||||
// `Image<Color>` pixels are stored linear-light with associated (premultiplied) alpha
|
cpu_data.push(SRGBA8::new(px[0], px[1], px[2], px[3]).into());
|
||||||
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.));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -171,14 +171,7 @@ fn decode_image(_: impl Ctx, data: Item<Resource>) -> Item<Raster<CPU>> {
|
|||||||
};
|
};
|
||||||
let image = image.to_rgba32f();
|
let image = image.to_rgba32f();
|
||||||
let image = Image {
|
let image = Image {
|
||||||
data: image
|
data: image.chunks(4).map(|pixel| Color::from_gamma_srgb_channels(pixel[0], pixel[1], pixel[2], pixel[3])).collect(),
|
||||||
.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(),
|
|
||||||
width: image.width(),
|
width: image.width(),
|
||||||
height: image.height(),
|
height: image.height(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@@ -144,12 +144,7 @@ fn make_opaque<T: Adjust<Color>>(
|
|||||||
input: Item<T>,
|
input: Item<T>,
|
||||||
) -> Item<T> {
|
) -> Item<T> {
|
||||||
let mut input = input;
|
let mut input = input;
|
||||||
input.element_mut().adjust(|color| {
|
input.element_mut().adjust(|color| color.with_alpha(1.));
|
||||||
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
|
input
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,11 +497,7 @@ fn invert<T: Adjust<Color>>(
|
|||||||
input: Item<T>,
|
input: Item<T>,
|
||||||
) -> Item<T> {
|
) -> Item<T> {
|
||||||
let mut input = input;
|
let mut input = input;
|
||||||
input.element_mut().adjust(|color| {
|
input.element_mut().adjust(|color| color.map_gamma_rgb(|channel| 1. - channel));
|
||||||
// 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
|
input
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1147,3 +1138,19 @@ mod _graphene_hash_impls {
|
|||||||
SelectiveColorChoice
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -115,13 +115,10 @@ fn color_overlay<T: Adjust<Color>>(
|
|||||||
let opacity = (opacity / 100.).clamp(0., 1.);
|
let opacity = (opacity / 100.).clamp(0., 1.);
|
||||||
|
|
||||||
image.element_mut().adjust(|pixel| {
|
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.
|
Color::from_rgbaf32_unchecked(mix(pixel.r(), overlay.r()), mix(pixel.g(), overlay.g()), mix(pixel.b(), overlay.b()), pixel.a())
|
||||||
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())
|
|
||||||
});
|
});
|
||||||
image
|
image
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ fn gaussian_blur_algorithm(buffer: Image<Color>, radius: f64, gamma: bool) -> Im
|
|||||||
unpremultiply_gamma_to_linear(blurred)
|
unpremultiply_gamma_to_linear(blurred)
|
||||||
} else {
|
} else {
|
||||||
let mut working = buffer;
|
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);
|
let mut blurred = gaussian_separable(working, &kernel, Color::from_rgbaf32_unchecked);
|
||||||
blurred.map_pixels(|px| px.to_unassociated_alpha());
|
blurred.map_pixels(|px| px.to_unassociated_alpha());
|
||||||
blurred
|
blurred
|
||||||
@@ -191,7 +191,7 @@ fn box_blur_algorithm(buffer: Image<Color>, radius: f64, gamma: bool) -> Image<C
|
|||||||
unpremultiply_gamma_to_linear(blurred)
|
unpremultiply_gamma_to_linear(blurred)
|
||||||
} else {
|
} else {
|
||||||
let mut working = buffer;
|
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);
|
let mut blurred = box_separable(working, radius, Color::from_rgbaf32_unchecked);
|
||||||
blurred.map_pixels(|px| px.to_unassociated_alpha());
|
blurred.map_pixels(|px| px.to_unassociated_alpha());
|
||||||
blurred
|
blurred
|
||||||
|
|||||||
@@ -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 (image, mut attributes) = image_frame.into_parts();
|
||||||
let (width, height) = (image.width, image.height);
|
let (width, height) = (image.width, image.height);
|
||||||
|
|
||||||
// Resize the image using the image crate
|
// Resize the image using the image crate, which filters each channel independently, so premultiply to keep transparent texels from bleeding into edges
|
||||||
let data = bytemuck::cast_vec(image.into_data().data);
|
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 image_size = DAffine2::from_scale(DVec2::new(width as f64, height as f64));
|
||||||
let size_px = image_size.transform_vector2(size).as_uvec2();
|
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 = resized.to_rgba32f();
|
||||||
let buffer = buffer.into_raw();
|
let buffer = buffer.into_raw();
|
||||||
let vec = bytemuck::cast_vec(buffer);
|
let vec = bytemuck::cast_vec(buffer);
|
||||||
let image = Image {
|
let mut image = Image {
|
||||||
width: new_width,
|
width: new_width,
|
||||||
height: new_height,
|
height: new_height,
|
||||||
data: vec,
|
data: vec,
|
||||||
base64_string: None,
|
base64_string: None,
|
||||||
};
|
};
|
||||||
|
image.map_pixels(|px: Color| px.to_unassociated_alpha());
|
||||||
// we need to adjust the offset if we truncate the offset calculation
|
// 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);
|
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.to_rgba32f();
|
||||||
let image = Image {
|
let image = Image {
|
||||||
data: image
|
data: image.chunks(4).map(|pixel| Color::from_gamma_srgb_channels(pixel[0], pixel[1], pixel[2], pixel[3])).collect(),
|
||||||
.chunks(4)
|
|
||||||
.map(|pixel| {
|
|
||||||
let alpha = pixel[3];
|
|
||||||
Color::from_gamma_srgb_channels(pixel[0] * alpha, pixel[1] * alpha, pixel[2] * alpha, alpha)
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
width: image.width(),
|
width: image.width(),
|
||||||
height: image.height(),
|
height: image.height(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
Reference in New Issue
Block a user