diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce4b58bc4..98288bda2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ env: # This should be limited to packages that are intended for publishing. RUST_NO_STD_PKGS: "-p vello_api" # List of features that depend on the standard library and will be excluded from no_std checks. - FEATURES_DEPENDING_ON_STD: "std,default" + FEATURES_DEPENDING_ON_STD: "std,default,png" # List of packages that can not target Wasm. # `vello_tests` uses `nv-flip`, which doesn't support Wasm. NO_WASM_PKGS: "--exclude vello_tests" diff --git a/Cargo.lock b/Cargo.lock index bcc227fd1..b5360c670 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3179,6 +3179,7 @@ name = "vello_api" version = "0.4.0" dependencies = [ "peniko", + "png", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9636b3fb8..f05001d87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,6 +103,7 @@ smallvec = "1.15.0" static_assertions = "1.1.0" thiserror = "2.0.12" oxipng = { version = "9.1.4", default-features = false } +png = "0.17.16" # The below crates are experimental! vello_api = { path = "sparse_strips/vello_api", default-features = false } @@ -119,7 +120,6 @@ image = { version = "0.25.6", default-features = false } # Used for examples clap = "4.5.35" anyhow = "1.0.97" -png = "0.17.16" pollster = "0.4.0" web-time = "1.1.0" wgpu-profiler = "0.21.0" diff --git a/sparse_strips/vello_api/Cargo.toml b/sparse_strips/vello_api/Cargo.toml index 892e79391..1e17d1a5e 100644 --- a/sparse_strips/vello_api/Cargo.toml +++ b/sparse_strips/vello_api/Cargo.toml @@ -13,11 +13,13 @@ publish = false [dependencies] peniko = { workspace = true } +png = { workspace = true, optional = true } [features] -default = ["std"] +default = ["std", "png"] std = ["peniko/std"] libm = ["peniko/libm"] +png = ["std", "dep:png"] simd = [] [lints] diff --git a/sparse_strips/vello_api/src/lib.rs b/sparse_strips/vello_api/src/lib.rs index 84c8ad171..46169636d 100644 --- a/sparse_strips/vello_api/src/lib.rs +++ b/sparse_strips/vello_api/src/lib.rs @@ -7,6 +7,7 @@ #![forbid(unsafe_code)] #![no_std] +extern crate alloc; pub use peniko; pub use peniko::color; @@ -14,3 +15,4 @@ pub use peniko::kurbo; pub mod execute; pub mod glyph; pub mod paint; +pub mod pixmap; diff --git a/sparse_strips/vello_api/src/paint.rs b/sparse_strips/vello_api/src/paint.rs index ead6cf248..03ebd518c 100644 --- a/sparse_strips/vello_api/src/paint.rs +++ b/sparse_strips/vello_api/src/paint.rs @@ -4,8 +4,10 @@ //! Types for paints. use crate::kurbo::Affine; +use crate::pixmap::Pixmap; +use alloc::sync::Arc; use peniko::color::{AlphaColor, PremulRgba8, Srgb}; -use peniko::{ColorStops, GradientKind}; +use peniko::{ColorStops, GradientKind, ImageQuality}; /// A paint that needs to be resolved via its index. // In the future, we might add additional flags, that's why we have @@ -82,6 +84,21 @@ impl Gradient { } } +/// An image. +#[derive(Debug, Clone)] +pub struct Image { + /// The underlying pixmap of the image. + pub pixmap: Arc, + /// Extend mode in the horizontal direction. + pub x_extend: peniko::Extend, + /// Extend mode in the vertical direction. + pub y_extend: peniko::Extend, + /// Hint for desired rendering quality. + pub quality: ImageQuality, + /// A transform to apply to the image. + pub transform: Affine, +} + /// A kind of paint that can be used for filling and stroking shapes. #[derive(Debug, Clone)] pub enum PaintType { @@ -89,6 +106,8 @@ pub enum PaintType { Solid(AlphaColor), /// A gradient. Gradient(Gradient), + /// An image. + Image(Image), } impl From> for PaintType { @@ -102,3 +121,9 @@ impl From for PaintType { Self::Gradient(value) } } + +impl From for PaintType { + fn from(value: Image) -> Self { + Self::Image(value) + } +} diff --git a/sparse_strips/vello_api/src/pixmap.rs b/sparse_strips/vello_api/src/pixmap.rs new file mode 100644 index 000000000..a62d355e1 --- /dev/null +++ b/sparse_strips/vello_api/src/pixmap.rs @@ -0,0 +1,145 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! A simple pixmap type. + +use alloc::vec; +use alloc::vec::Vec; + +/// A pixmap backed by u8. +#[derive(Debug, Clone)] +pub struct Pixmap { + /// Width of the pixmap in pixels. + pub width: u16, + /// Height of the pixmap in pixels. + pub height: u16, + /// Buffer of the pixmap in RGBA format. + pub buf: Vec, +} + +impl Pixmap { + /// Create a new pixmap with the given width and height in pixels. + pub fn new(width: u16, height: u16) -> Self { + let buf = vec![0; width as usize * height as usize * 4]; + Self { width, height, buf } + } + + /// Return the width of the pixmap. + pub fn width(&self) -> u16 { + self.width + } + + /// Return the height of the pixmap. + pub fn height(&self) -> u16 { + self.height + } + + #[allow( + clippy::cast_possible_truncation, + reason = "cannot overflow in this case" + )] + /// Apply an alpha value to the whole pixmap. + pub fn multiply_alpha(&mut self, alpha: u8) { + for comp in self.data_mut() { + *comp = ((alpha as u16 * *comp as u16) / 255) as u8; + } + } + + /// Create a pixmap from a PNG file. + #[cfg(feature = "png")] + #[allow( + clippy::cast_possible_truncation, + reason = "cannot overflow in this case" + )] + pub fn from_png(data: &[u8]) -> Result { + let mut decoder = png::Decoder::new(data); + decoder.set_transformations(png::Transformations::ALPHA); + + let mut reader = decoder.read_info()?; + let mut img_data = vec![0; reader.output_buffer_size()]; + let info = reader.next_frame(&mut img_data)?; + + let decoded_data = match info.color_type { + // We set a transformation to always convert to alpha. + png::ColorType::Rgb => unreachable!(), + png::ColorType::Grayscale => unreachable!(), + // I believe the above transformation also expands indexed images. + png::ColorType::Indexed => unreachable!(), + png::ColorType::Rgba => img_data, + png::ColorType::GrayscaleAlpha => { + let mut rgba_data = Vec::with_capacity(img_data.len() * 2); + for slice in img_data.chunks(2) { + let gray = slice[0]; + let alpha = slice[1]; + rgba_data.push(gray); + rgba_data.push(gray); + rgba_data.push(gray); + rgba_data.push(alpha); + } + + rgba_data + } + }; + + let premultiplied = decoded_data + .chunks_exact(4) + .flat_map(|d| { + let alpha = d[3] as u16; + let premultiply = |e: u8| ((e as u16 * alpha) / 255) as u8; + + if alpha == 0 { + [0, 0, 0, 0] + } else { + [ + premultiply(d[0]), + premultiply(d[1]), + premultiply(d[2]), + d[3], + ] + } + }) + .collect::>(); + + Ok(Self { + width: info.width as u16, + height: info.height as u16, + buf: premultiplied, + }) + } + + /// Returns a reference to the underlying data as premultiplied RGBA8. + pub fn data(&self) -> &[u8] { + &self.buf + } + + /// Returns a mutable reference to the underlying data as premultiplied RGBA8. + pub fn data_mut(&mut self) -> &mut [u8] { + &mut self.buf + } + + /// Sample a pixel from the pixmap. + #[inline(always)] + pub fn sample(&self, x: u16, y: u16) -> &[u8] { + let idx = 4 * (self.width as usize * y as usize + x as usize); + &self.buf[idx..][..4] + } + + /// Convert from premultiplied to separate alpha. + /// + /// Not fast, but useful for saving to PNG etc. + #[allow( + clippy::cast_possible_truncation, + reason = "cannot overflow in this case" + )] + pub fn unpremultiply(&mut self) { + for rgba in self.buf.chunks_exact_mut(4) { + let alpha = 255.0 / rgba[3] as f32; + + if rgba[3] != 0 { + rgba[0] = (rgba[0] as f32 * alpha + 0.5) as u8; + rgba[1] = (rgba[1] as f32 * alpha + 0.5) as u8; + rgba[2] = (rgba[2] as f32 * alpha + 0.5) as u8; + } + } + } +} diff --git a/sparse_strips/vello_common/Cargo.toml b/sparse_strips/vello_common/Cargo.toml index 328f56ff0..fc30b35df 100644 --- a/sparse_strips/vello_common/Cargo.toml +++ b/sparse_strips/vello_common/Cargo.toml @@ -21,6 +21,7 @@ smallvec = { workspace = true } [features] simd = ["vello_api/simd"] +png = ["vello_api/png"] [lints] workspace = true diff --git a/sparse_strips/vello_common/src/encode.rs b/sparse_strips/vello_common/src/encode.rs index 669323244..f545282cd 100644 --- a/sparse_strips/vello_common/src/encode.rs +++ b/sparse_strips/vello_common/src/encode.rs @@ -5,14 +5,17 @@ use crate::color::Srgb; use crate::color::palette::css::BLACK; +use crate::encode::private::Sealed; use crate::kurbo::{Affine, Point, Vec2}; -use crate::peniko::{ColorStop, Extend, GradientKind}; +use crate::peniko::{ColorStop, Extend, GradientKind, ImageQuality}; +use crate::pixmap::Pixmap; use alloc::borrow::Cow; +use alloc::sync::Arc; use alloc::vec::Vec; use core::f32::consts::PI; use core::iter; use smallvec::SmallVec; -use vello_api::paint::{Gradient, IndexedPaint, Paint}; +use vello_api::paint::{Gradient, Image, IndexedPaint, Paint}; const DEGENERATE_THRESHOLD: f32 = 1.0e-6; const NUDGE_VAL: f32 = 1.0e-7; @@ -210,20 +213,7 @@ impl EncodeExt for Gradient { // the above approach of incrementally updating the position, we need to calculate // how the x/y unit vectors are affected by the transform, and then use this as the // step delta for a step in the x/y direction. - let (x_advance, y_advance) = { - let scale_skew_transform = { - let c = transform.as_coeffs(); - Affine::new([c[0], c[1], c[2], c[3], 0.0, 0.0]) - }; - - let x_advance = scale_skew_transform * Point::new(1.0, 0.0); - let y_advance = scale_skew_transform * Point::new(0.0, 1.0); - - ( - Vec2::new(x_advance.x, x_advance.y), - Vec2::new(y_advance.x, y_advance.y), - ) - }; + let (x_advance, y_advance) = x_y_advances(&transform); let encoded = EncodedGradient { kind, @@ -426,11 +416,57 @@ fn encode_stops(stops: &[ColorStop], start: f32, end: f32, pad: bool) -> Vec (Vec2, Vec2) { + let scale_skew_transform = { + let c = transform.as_coeffs(); + Affine::new([c[0], c[1], c[2], c[3], 0.0, 0.0]) + }; + + let x_advance = scale_skew_transform * Point::new(1.0, 0.0); + let y_advance = scale_skew_transform * Point::new(0.0, 1.0); + + ( + Vec2::new(x_advance.x, x_advance.y), + Vec2::new(y_advance.x, y_advance.y), + ) +} + +impl Sealed for Image {} + +impl EncodeExt for Image { + fn encode_into(&self, paints: &mut Vec) -> Paint { + let idx = paints.len(); + + let transform = self.transform.inverse(); + // TODO: This is somewhat expensive for large images, maybe it's not worth optimizing + // non-opaque images in the first place.. + let has_opacities = self.pixmap.data().chunks(4).any(|c| c[3] != 255); + + let (x_advance, y_advance) = x_y_advances(&transform); + + let encoded = EncodedImage { + pixmap: self.pixmap.clone(), + extends: (self.x_extend, self.y_extend), + quality: self.quality, + has_opacities, + transform, + x_advance, + y_advance, + }; + + paints.push(EncodedPaint::Image(encoded)); + + Paint::Indexed(IndexedPaint::new(idx)) + } +} + /// An encoded paint. #[derive(Debug)] pub enum EncodedPaint { /// An encoded gradient. Gradient(EncodedGradient), + /// An encoded image. + Image(EncodedImage), } impl From for EncodedPaint { @@ -439,6 +475,25 @@ impl From for EncodedPaint { } } +/// An encoded image. +#[derive(Debug)] +pub struct EncodedImage { + /// The underlying pixmap of the image. + pub pixmap: Arc, + /// The extends in the horizontal and vertical direction. + pub extends: (Extend, Extend), + /// The rendering quality of the image. + pub quality: ImageQuality, + /// Whether the image has opacities. + pub has_opacities: bool, + /// A transform to apply to the image. + pub transform: Affine, + /// The advance in image coordinates for one step in the x direction. + pub x_advance: Vec2, + /// The advance in image coordinates for one step in the y direction. + pub y_advance: Vec2, +} + /// Computed properties of a linear gradient. #[derive(Debug)] pub struct LinearKind { diff --git a/sparse_strips/vello_common/src/lib.rs b/sparse_strips/vello_common/src/lib.rs index 0f47b5ed7..3ebc86659 100644 --- a/sparse_strips/vello_common/src/lib.rs +++ b/sparse_strips/vello_common/src/lib.rs @@ -20,7 +20,6 @@ pub mod encode; pub mod flatten; pub mod glyph; pub mod pico_svg; -pub mod pixmap; pub mod strip; pub mod tile; diff --git a/sparse_strips/vello_common/src/pixmap.rs b/sparse_strips/vello_common/src/pixmap.rs deleted file mode 100644 index 4f59d7697..000000000 --- a/sparse_strips/vello_common/src/pixmap.rs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2025 the Vello Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! A simple pixmap type. - -use alloc::vec; -use alloc::vec::Vec; - -/// A pixmap backed by u8. -#[derive(Debug)] -pub struct Pixmap { - /// Width of the pixmap in pixels. - pub width: u16, - /// Height of the pixmap in pixels. - pub height: u16, - /// Buffer of the pixmap in RGBA format. - pub buf: Vec, -} - -impl Pixmap { - /// Create a new pixmap with the given width and height in pixels. - pub fn new(width: u16, height: u16) -> Self { - let buf = vec![0; width as usize * height as usize * 4]; - Self { width, height, buf } - } - - /// Returns the underlying data as premultiplied RGBA8. - pub fn data(&self) -> &[u8] { - &self.buf - } - - /// Convert from premultiplied to separate alpha. - /// - /// Not fast, but useful for saving to PNG etc. - pub fn unpremultiply(&mut self) { - for rgba in self.buf.chunks_exact_mut(4) { - let alpha = 255.0 / rgba[3] as f32; - if alpha != 0.0 { - rgba[0] = (rgba[0] as f32 * alpha).round().min(255.0) as u8; - rgba[1] = (rgba[1] as f32 * alpha).round().min(255.0) as u8; - rgba[2] = (rgba[2] as f32 * alpha).round().min(255.0) as u8; - } - } - } -} diff --git a/sparse_strips/vello_cpu/Cargo.toml b/sparse_strips/vello_cpu/Cargo.toml index e29f55f3b..f2115ef65 100644 --- a/sparse_strips/vello_cpu/Cargo.toml +++ b/sparse_strips/vello_cpu/Cargo.toml @@ -19,6 +19,10 @@ path = "tests/mod.rs" [dependencies] vello_common = { workspace = true } +[features] +default = ["png"] +png = ["vello_common/png"] + [dev-dependencies] automod = "1.0" oxipng = { workspace = true, features = ["freestanding", "parallel"] } diff --git a/sparse_strips/vello_cpu/snapshots/image_bicubic_10x_scale.png b/sparse_strips/vello_cpu/snapshots/image_bicubic_10x_scale.png new file mode 100644 index 000000000..d937f1762 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_bicubic_10x_scale.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:120e923898552c1135db0bf56ca572669977e0f4f32979561a659bf6c2d2c7bb +size 1896 diff --git a/sparse_strips/vello_cpu/snapshots/image_bicubic_10x_scale_2.png b/sparse_strips/vello_cpu/snapshots/image_bicubic_10x_scale_2.png new file mode 100644 index 000000000..aed675a3d --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_bicubic_10x_scale_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:431358c15d2870aeae87edc2e467c72253a592a05595364283f31b7637074760 +size 2604 diff --git a/sparse_strips/vello_cpu/snapshots/image_bicubic_2x_scale.png b/sparse_strips/vello_cpu/snapshots/image_bicubic_2x_scale.png new file mode 100644 index 000000000..a58d7d23d --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_bicubic_2x_scale.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc8fe6cd1596679ed0a26012936ec2c4153988ea8b6f9d74240246aac9f353b1 +size 236 diff --git a/sparse_strips/vello_cpu/snapshots/image_bicubic_5x_scale.png b/sparse_strips/vello_cpu/snapshots/image_bicubic_5x_scale.png new file mode 100644 index 000000000..0a34a4ee9 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_bicubic_5x_scale.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:052b7577732da50b010491a7865135789a15b27a76786d60d1088ce92d3c6ab7 +size 584 diff --git a/sparse_strips/vello_cpu/snapshots/image_bicubic_identity.png b/sparse_strips/vello_cpu/snapshots/image_bicubic_identity.png new file mode 100644 index 000000000..956f8673c --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_bicubic_identity.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c138195844e69298a60ee41f81c624db4f529d5e201d9254b9e554c3658de9e0 +size 151 diff --git a/sparse_strips/vello_cpu/snapshots/image_bicubic_with_rotation.png b/sparse_strips/vello_cpu/snapshots/image_bicubic_with_rotation.png new file mode 100644 index 000000000..dcb9c1b89 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_bicubic_with_rotation.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f64982ff9b2b4668f4f11b851b7dd8f11d1ab994604cb90abe6620cb35083df5 +size 12719 diff --git a/sparse_strips/vello_cpu/snapshots/image_bicubic_with_translation.png b/sparse_strips/vello_cpu/snapshots/image_bicubic_with_translation.png new file mode 100644 index 000000000..dbddf1cc4 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_bicubic_with_translation.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1911f5291e5f90a032c50f7a7a4f687216b88aa65793dc12c170b8b7ab6be626 +size 648 diff --git a/sparse_strips/vello_cpu/snapshots/image_bilinear_10x_scale.png b/sparse_strips/vello_cpu/snapshots/image_bilinear_10x_scale.png new file mode 100644 index 000000000..8239e224f --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_bilinear_10x_scale.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0633d3f7c56e695d09b66852f038be683a627fd31ee626b8256a8585eb52cb0c +size 834 diff --git a/sparse_strips/vello_cpu/snapshots/image_bilinear_10x_scale_2.png b/sparse_strips/vello_cpu/snapshots/image_bilinear_10x_scale_2.png new file mode 100644 index 000000000..05e23fd4e --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_bilinear_10x_scale_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8bcbd506717e24ae98fb5dce94a2e8c4b40807da6e209b0c8080650f9ea7c76 +size 1378 diff --git a/sparse_strips/vello_cpu/snapshots/image_bilinear_2x_scale.png b/sparse_strips/vello_cpu/snapshots/image_bilinear_2x_scale.png new file mode 100644 index 000000000..833b9318a --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_bilinear_2x_scale.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad50015f270247b7a68b57dbfbf8464165c65920fe6910093e38c9b79b5668cf +size 236 diff --git a/sparse_strips/vello_cpu/snapshots/image_bilinear_5x_scale.png b/sparse_strips/vello_cpu/snapshots/image_bilinear_5x_scale.png new file mode 100644 index 000000000..9e830f7e6 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_bilinear_5x_scale.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d29295e2fcd3d508aec6cf11a81353c25d1ba8a3e830e15a64f4c7c4d8068c2 +size 365 diff --git a/sparse_strips/vello_cpu/snapshots/image_bilinear_identity.png b/sparse_strips/vello_cpu/snapshots/image_bilinear_identity.png new file mode 100644 index 000000000..ddc72a02a --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_bilinear_identity.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0766ff15fdf0f6bfe8660d98c06b969a28834dd5b859e7c738da2830bcd3e74e +size 151 diff --git a/sparse_strips/vello_cpu/snapshots/image_bilinear_with_rotation.png b/sparse_strips/vello_cpu/snapshots/image_bilinear_with_rotation.png new file mode 100644 index 000000000..888f9ff71 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_bilinear_with_rotation.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fcc8281282e8f6f89bb1cb4e6879cbb7d4812cbbd9813c23b703d079ce22722e +size 6956 diff --git a/sparse_strips/vello_cpu/snapshots/image_bilinear_with_translation.png b/sparse_strips/vello_cpu/snapshots/image_bilinear_with_translation.png new file mode 100644 index 000000000..b9012be86 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_bilinear_with_translation.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e91518858dc4a1f8fe34bbd9e7eab0246a18d797b935aef468ba049358ad589e +size 365 diff --git a/sparse_strips/vello_cpu/snapshots/image_complex_shape.png b/sparse_strips/vello_cpu/snapshots/image_complex_shape.png new file mode 100644 index 000000000..a14b5dd94 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_complex_shape.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fbd936d24e10f32c54d1b43cffe3de33b6a11e7abe6eaa54ae646ab432333b0 +size 1198 diff --git a/sparse_strips/vello_cpu/snapshots/image_global_alpha.png b/sparse_strips/vello_cpu/snapshots/image_global_alpha.png new file mode 100644 index 000000000..924233844 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_global_alpha.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80361dd8b6b60829419f5d6b413536b38cb707b5a59c752f50b7b1dd76ac53a9 +size 152 diff --git a/sparse_strips/vello_cpu/snapshots/image_luma_image.png b/sparse_strips/vello_cpu/snapshots/image_luma_image.png new file mode 100644 index 000000000..32339b185 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_luma_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39de8f5a43bc96f0309e363293fbb35f37d19a1a199044fc55306292fd174dd8 +size 152 diff --git a/sparse_strips/vello_cpu/snapshots/image_lumaa_image.png b/sparse_strips/vello_cpu/snapshots/image_lumaa_image.png new file mode 100644 index 000000000..3c65194be --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_lumaa_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0bdb672b8d45eb81d023039f34fb54c3e594c094e9b8463b9d2618ba9d9bb18 +size 154 diff --git a/sparse_strips/vello_cpu/snapshots/image_pad_x_pad_y.png b/sparse_strips/vello_cpu/snapshots/image_pad_x_pad_y.png new file mode 100644 index 000000000..8916d0c2d --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_pad_x_pad_y.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:385122f028251918f717348450dd3369b4cd8f5c695523c3335dfc5cbad3d50d +size 141 diff --git a/sparse_strips/vello_cpu/snapshots/image_reflect_x.png b/sparse_strips/vello_cpu/snapshots/image_reflect_x.png new file mode 100644 index 000000000..da8a17c8a --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_reflect_x.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d580452ab65aaf697145866a3f332eb0a6cfe9d9aec379a06ae098d351da1957 +size 149 diff --git a/sparse_strips/vello_cpu/snapshots/image_reflect_x_reflect_y.png b/sparse_strips/vello_cpu/snapshots/image_reflect_x_reflect_y.png new file mode 100644 index 000000000..9e181718b --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_reflect_x_reflect_y.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83892656c4dd506dd4304d2e981f05f8209716dbda1b8c26b3245a53946745e8 +size 157 diff --git a/sparse_strips/vello_cpu/snapshots/image_repeat_x_repeat_y.png b/sparse_strips/vello_cpu/snapshots/image_repeat_x_repeat_y.png new file mode 100644 index 000000000..1b1d68a38 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_repeat_x_repeat_y.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8128b25335d8d0fe38c01430ac8d3faf15688734658418915e552ee8904de7ce +size 152 diff --git a/sparse_strips/vello_cpu/snapshots/image_repeat_y.png b/sparse_strips/vello_cpu/snapshots/image_repeat_y.png new file mode 100644 index 000000000..3ee836144 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_repeat_y.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aeec94e34aa0ab79eeab6a81fb85943dc74b1f7aad2dc1062567b4a6750245c6 +size 148 diff --git a/sparse_strips/vello_cpu/snapshots/image_rgb_image.png b/sparse_strips/vello_cpu/snapshots/image_rgb_image.png new file mode 100644 index 000000000..b8fbf803c --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_rgb_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8742a1ddbbbfadb84df8d7520989fec1472328d0cc6e9c300d421a17277a442d +size 152 diff --git a/sparse_strips/vello_cpu/snapshots/image_rgba_image.png b/sparse_strips/vello_cpu/snapshots/image_rgba_image.png new file mode 100644 index 000000000..ab6244ba8 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_rgba_image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b95b70ff1685b84e98b8b43eca5750b71497757fcc8f007a97d2a979ac5d23e +size 154 diff --git a/sparse_strips/vello_cpu/snapshots/image_with_transform_identity.png b/sparse_strips/vello_cpu/snapshots/image_with_transform_identity.png new file mode 100644 index 000000000..b21d86f61 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_with_transform_identity.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:002bba3c3816a1613590e6ad7cd08229638afa26a1242d027b9a99f5e3ce68b6 +size 150 diff --git a/sparse_strips/vello_cpu/snapshots/image_with_transform_negative_scale.png b/sparse_strips/vello_cpu/snapshots/image_with_transform_negative_scale.png new file mode 100644 index 000000000..646ff726c --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_with_transform_negative_scale.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12d2185bd73c9d35385c5d42df5f8c07257bb19a62ece20d9443777fff2c95a5 +size 152 diff --git a/sparse_strips/vello_cpu/snapshots/image_with_transform_rotate_1.png b/sparse_strips/vello_cpu/snapshots/image_with_transform_rotate_1.png new file mode 100644 index 000000000..ab20ec95f --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_with_transform_rotate_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b26c87471adacba00494788000ef33dedfd22ff694bfe12cabab73366904dca +size 606 diff --git a/sparse_strips/vello_cpu/snapshots/image_with_transform_rotate_2.png b/sparse_strips/vello_cpu/snapshots/image_with_transform_rotate_2.png new file mode 100644 index 000000000..3a8eeae86 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_with_transform_rotate_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2887c89a2e19f9f7e38be1067ceaccf7141d3ef6a87c7ed5c03f9b36c429649a +size 593 diff --git a/sparse_strips/vello_cpu/snapshots/image_with_transform_scale.png b/sparse_strips/vello_cpu/snapshots/image_with_transform_scale.png new file mode 100644 index 000000000..0115d6ce4 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_with_transform_scale.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62e5cf71ed60d187b09b3366e196f4f3e12e5212f09edcb0b79985bd744ee90a +size 152 diff --git a/sparse_strips/vello_cpu/snapshots/image_with_transform_scale_and_translate.png b/sparse_strips/vello_cpu/snapshots/image_with_transform_scale_and_translate.png new file mode 100644 index 000000000..8066816cf --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_with_transform_scale_and_translate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ba1603b224725f26f27529b5f644c6ee3f96efab44ca42d08f63e17fec6d07c +size 156 diff --git a/sparse_strips/vello_cpu/snapshots/image_with_transform_scaling_non_uniform.png b/sparse_strips/vello_cpu/snapshots/image_with_transform_scaling_non_uniform.png new file mode 100644 index 000000000..6834efb7f --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_with_transform_scaling_non_uniform.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea18454ae71ef2addf89ea0ddc7278db9036d9fe92cb40a04b174eea57e9946d +size 149 diff --git a/sparse_strips/vello_cpu/snapshots/image_with_transform_skew_x_1.png b/sparse_strips/vello_cpu/snapshots/image_with_transform_skew_x_1.png new file mode 100644 index 000000000..61f26ebf5 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_with_transform_skew_x_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8254c076f4696a530fb15126ec7d2f4f742db8669a98d606441eed269a1122a +size 167 diff --git a/sparse_strips/vello_cpu/snapshots/image_with_transform_skew_x_2.png b/sparse_strips/vello_cpu/snapshots/image_with_transform_skew_x_2.png new file mode 100644 index 000000000..323164738 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_with_transform_skew_x_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3621a2ec080f47882f1c43436b72ecf80bc493fa56195926cda2850826d4b8e8 +size 173 diff --git a/sparse_strips/vello_cpu/snapshots/image_with_transform_skew_y_1.png b/sparse_strips/vello_cpu/snapshots/image_with_transform_skew_y_1.png new file mode 100644 index 000000000..f90a2e113 --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_with_transform_skew_y_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e4845f4b959226b1a0c43e7b91f6f2d145aa3215fb726ddbed712686fff7ce7 +size 394 diff --git a/sparse_strips/vello_cpu/snapshots/image_with_transform_skew_y_2.png b/sparse_strips/vello_cpu/snapshots/image_with_transform_skew_y_2.png new file mode 100644 index 000000000..4cf719bbd --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_with_transform_skew_y_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd39139982043996e84c1d220ddc58c7c6a58cb818ae46543d4b045bc1a4abf5 +size 383 diff --git a/sparse_strips/vello_cpu/snapshots/image_with_transform_translate.png b/sparse_strips/vello_cpu/snapshots/image_with_transform_translate.png new file mode 100644 index 000000000..ee0ad41ab --- /dev/null +++ b/sparse_strips/vello_cpu/snapshots/image_with_transform_translate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7743573b5b5b45057ac060d2dafa46b89f280948f93de60232e155dbb14c4a1 +size 149 diff --git a/sparse_strips/vello_cpu/src/fine/gradient.rs b/sparse_strips/vello_cpu/src/fine/gradient.rs index bdfa591e0..6cef79fa8 100644 --- a/sparse_strips/vello_cpu/src/fine/gradient.rs +++ b/sparse_strips/vello_cpu/src/fine/gradient.rs @@ -3,13 +3,13 @@ //! Rendering linear gradients. -use crate::fine::{COLOR_COMPONENTS, TILE_HEIGHT_COMPONENTS}; +use crate::fine::{COLOR_COMPONENTS, Painter, TILE_HEIGHT_COMPONENTS}; use vello_common::encode::{EncodedGradient, GradientLike, GradientRange}; use vello_common::kurbo::Point; #[derive(Debug)] pub(crate) struct GradientFiller<'a, T: GradientLike> { - /// The position of the next x that should be processed. + /// The current position that should be processed. cur_pos: Point, /// The index of the current range. range_idx: usize, @@ -114,6 +114,12 @@ impl<'a, T: GradientLike> GradientFiller<'a, T> { } } +impl Painter for GradientFiller<'_, T> { + fn paint(self, target: &mut [u8]) { + self.run(target); + } +} + pub(crate) fn extend(mut val: f32, pad: bool, clamp_range: (f32, f32)) -> f32 { let start = clamp_range.0; let end = clamp_range.1; diff --git a/sparse_strips/vello_cpu/src/fine/image.rs b/sparse_strips/vello_cpu/src/fine/image.rs new file mode 100644 index 000000000..fb129832e --- /dev/null +++ b/sparse_strips/vello_cpu/src/fine/image.rs @@ -0,0 +1,304 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::fine::{COLOR_COMPONENTS, Painter, TILE_HEIGHT_COMPONENTS}; +use vello_common::encode::EncodedImage; +use vello_common::kurbo::{Point, Vec2}; +use vello_common::peniko::{Extend, ImageQuality}; +use vello_common::tile::Tile; + +#[derive(Debug)] +pub(crate) struct ImageFiller<'a> { + /// The current position that should be processed. + cur_pos: Point, + /// The underlying image. + image: &'a EncodedImage, +} + +impl<'a> ImageFiller<'a> { + pub(crate) fn new(image: &'a EncodedImage, start_x: u16, start_y: u16) -> Self { + Self { + // We want to sample values of the pixels at the center, so add an offset of 0.5. + cur_pos: image.transform * Point::new(start_x as f64 + 0.5, start_y as f64 + 0.5), + image, + } + } + + pub(super) fn run(mut self, target: &mut [u8]) { + // We currently have two branches for filling images: The first case is used for + // nearest neighbor filtering and for images with no skewing-transform (this is checked + // by the first two conditions), which allows us to take a faster path. + // The second version is the general case for any other image. + // Once we get to performance optimizations, it's possible that there will be further + // paths (e.g. one for no scaling transform and only integer translation offsets). + if self.image.y_advance.x != 0.0 + || self.image.x_advance.y != 0.0 + || self.image.quality != ImageQuality::Low + { + // Fallback path. + target + .chunks_exact_mut(TILE_HEIGHT_COMPONENTS) + .for_each(|column| { + self.run_complex_column(column); + self.cur_pos += self.image.x_advance; + }); + } else { + // Fast path. Each step in the x/y direction only updates x/y component of the + // current position, since we have no skewing. + // Most importantly, the y position is the same across each column, allowing us + // to precompute it (as well as it's extend). + let mut x_pos = self.cur_pos.x; + let x_advance = self.image.x_advance.x; + let y_advance = self.image.y_advance.y; + + let mut y_positions = [0.0; Tile::HEIGHT as usize]; + + for (idx, pos) in y_positions.iter_mut().enumerate() { + *pos = extend( + // Since we already added a 0.5 offset to sample at the center of the pixel, + // we always floor to get the target pixel. + (self.cur_pos.y + y_advance * idx as f64).floor() as f32, + self.image.extends.1, + self.image.pixmap.height() as f32, + ); + } + + target + .chunks_exact_mut(TILE_HEIGHT_COMPONENTS) + .for_each(|column| { + let extended_x_pos = extend( + // As above, always floor. + x_pos.floor() as f32, + self.image.extends.0, + self.image.pixmap.width() as f32, + ); + self.run_simple_column(column, extended_x_pos, &y_positions); + x_pos += x_advance; + }); + } + } + + fn run_simple_column( + &mut self, + col: &mut [u8], + x_pos: f32, + y_positions: &[f32; Tile::HEIGHT as usize], + ) { + for (pixel, y_pos) in col + .chunks_exact_mut(COLOR_COMPONENTS) + .zip(y_positions.iter()) + { + let sample = match self.image.quality { + ImageQuality::Low => self.image.pixmap.sample(x_pos as u16, *y_pos as u16), + ImageQuality::Medium | ImageQuality::High => unimplemented!(), + }; + + pixel.copy_from_slice(sample); + } + } + + fn run_complex_column(&mut self, col: &mut [u8]) { + let extend_point = |mut point: Point| { + // For the same reason as mentioned above, we always floor. + point.x = extend( + point.x.floor() as f32, + self.image.extends.0, + self.image.pixmap.width() as f32, + ) as f64; + point.y = extend( + point.y.floor() as f32, + self.image.extends.1, + self.image.pixmap.height() as f32, + ) as f64; + + point + }; + + let mut pos = self.cur_pos; + + for pixel in col.chunks_exact_mut(COLOR_COMPONENTS) { + match self.image.quality { + // Nearest neighbor filtering. + // Simply takes the nearest pixel to our current position. + ImageQuality::Low => { + let point = extend_point(pos); + let sample = self.image.pixmap.sample(point.x as u16, point.y as u16); + pixel.copy_from_slice(sample); + } + ImageQuality::Medium | ImageQuality::High => { + // We have two versions of filtering: `Medium` (bilinear filtering) and + // `High` (bicubic filtering). + + // In bilinear filtering, we sample the pixels of the rectangle that spans the + // locations (-0.5, -0.5) and (0.5, 0.5), and weight them by the fractional + // x/y position using simple linear interpolation in both dimensions. + // In bicubic filtering, we instead span a 4x4 grid around the + // center of the location we are sampling, and sample those points + // using a cubic filter to weight each location's contribution. + + let fract = |orig_val: f64| { + // To give some intuition on why we need that shift, based on bilinear + // filtering: If we sample at the position (0.5, 0.5), we are at the center + // of the pixel and thus only want the color of the current pixel. Thus, we take + // 1.0 * 1.0 from the top left pixel (which still lies on our pixel) + // and 0.0 from all other corners (which lie at the start of other pixels). + // + // If we sample at the position (0.4, 0.4), we want 0.1 * 0.1 = 0.01 from + // the top-left pixel, 0.1 * 0.9 = 0.09 from the bottom-left and top-right, + // and finally 0.9 * 0.9 = 0.81 from the bottom right position (which still + // lies on our pixel, and thus has intuitively should have the highest + // contribution). Thus, we need to subtract 0.5 from the position to get + // the correct fractional contribution. + let start = orig_val - 0.5; + let mut res = start.fract() as f32; + + // In case we are in the negative we need to mirror the result. + if res.is_sign_negative() { + res += 1.0; + } + + res + }; + + let x_fract = fract(pos.x); + let y_fract = fract(pos.y); + + let mut f32_color = [0.0_f32; 4]; + + let sample = |p: Point| self.image.pixmap.sample(p.x as u16, p.y as u16); + + if self.image.quality == ImageQuality::Medium { + let cx = [1.0 - x_fract, x_fract]; + let cy = [1.0 - y_fract, y_fract]; + + // Note that the sum of all cx*cy combinations also yields 1.0 again + // (modulo some floating point number impreciseness), ensuring the + // colors stay in range. + + // We sample the corners rectangle that covers our current position. + for (x_idx, x) in [-0.5, 0.5].into_iter().enumerate() { + for (y_idx, y) in [-0.5, 0.5].into_iter().enumerate() { + let color_sample = sample(extend_point(pos + Vec2::new(x, y))); + let w = cx[x_idx] * cy[y_idx]; + + for i in 0..COLOR_COMPONENTS { + f32_color[i] += w * color_sample[i] as f32; + } + } + } + } else { + // Compare to https://github.com/google/skia/blob/84ff153b0093fc83f6c77cd10b025c06a12c5604/src/opts/SkRasterPipeline_opts.h#L5030-L5075. + let cx = weights(x_fract); + let cy = weights(y_fract); + + // Note in particular that it is guaranteed that, similarly to bilinear filtering, + // the sum of all cx*cy is 1. + + // We sample the 4x4 grid around the position we are currently looking at. + for (x_idx, x) in [-1.5, -0.5, 0.5, 1.5].into_iter().enumerate() { + for (y_idx, y) in [-1.5, -0.5, 0.5, 1.5].into_iter().enumerate() { + let color_sample = sample(extend_point(pos + Vec2::new(x, y))); + let c = cx[x_idx] * cy[y_idx]; + + for i in 0..COLOR_COMPONENTS { + f32_color[i] += c * color_sample[i] as f32; + } + } + } + } + + let mut u8_color = [0; 4]; + + for i in 0..COLOR_COMPONENTS { + u8_color[i] = (f32_color[i] + 0.5) as u8; + } + + pixel.copy_from_slice(&u8_color); + } + }; + + pos += self.image.y_advance; + } + } +} + +fn extend(val: f32, extend: Extend, max: f32) -> f32 { + match extend { + Extend::Pad => val.clamp(0.0, max - 1.0), + // TODO: We need to make repeat and reflect more efficient and branch-less. + Extend::Repeat => val.rem_euclid(max), + Extend::Reflect => { + let period = 2.0 * max; + + let val_mod = val.rem_euclid(period); + + if val_mod < max { + val_mod + } else { + (period - 1.0) - val_mod + } + } + } +} + +impl Painter for ImageFiller<'_> { + fn paint(self, target: &mut [u8]) { + self.run(target); + } +} + +/// Calculate the weights for a single fractional value. +const fn weights(fract: f32) -> [f32; 4] { + const MF: [[f32; 4]; 4] = mf_resampler(); + + [ + single_weight(fract, MF[0][0], MF[0][1], MF[0][2], MF[0][3]), + single_weight(fract, MF[1][0], MF[1][1], MF[1][2], MF[1][3]), + single_weight(fract, MF[2][0], MF[2][1], MF[2][2], MF[2][3]), + single_weight(fract, MF[3][0], MF[3][1], MF[3][2], MF[3][3]), + ] +} + +/// Calculate a weight based on the fractional value t and the cubic coefficients. +const fn single_weight(t: f32, a: f32, b: f32, c: f32, d: f32) -> f32 { + t * (t * (t * d + c) + b) + a +} + +/// Mitchell filter with the variables B = 1/3 and C = 1/3. +const fn mf_resampler() -> [[f32; 4]; 4] { + cubic_resampler(1.0 / 3.0, 1.0 / 3.0) +} + +/// Cubic resampling logic is borrowed from Skia. See +/// +/// for some links to understand how this works. In principle, this macro allows us to define a +/// resampler kernel based on two variables B and C which can be between 0 and 1, allowing to +/// change some properties of the cubic interpolation kernel. +/// +/// As mentioned above, cubic resampling consists of sampling the 16 surrounding pixels of the +/// target point and interpolating them with a cubic filter. +/// The generated matrix is 4x4 and represent the coefficients of the cubic function used to +/// calculate weights based on the `x_fract` and `y_fract` of the location we are looking at. +const fn cubic_resampler(b: f32, c: f32) -> [[f32; 4]; 4] { + [ + [ + (1.0 / 6.0) * b, + -(3.0 / 6.0) * b - c, + (3.0 / 6.0) * b + 2.0 * c, + -(1.0 / 6.0) * b - c, + ], + [ + 1.0 - (2.0 / 6.0) * b, + 0.0, + -3.0 + (12.0 / 6.0) * b + c, + 2.0 - (9.0 / 6.0) * b - c, + ], + [ + (1.0 / 6.0) * b, + (3.0 / 6.0) * b + c, + 3.0 - (15.0 / 6.0) * b - 2.0 * c, + -2.0 + (9.0 / 6.0) * b + c, + ], + [0.0, 0.0, -c, (1.0 / 6.0) * b + c], + ] +} diff --git a/sparse_strips/vello_cpu/src/fine/mod.rs b/sparse_strips/vello_cpu/src/fine/mod.rs index f2058bfaa..e0717cb9d 100644 --- a/sparse_strips/vello_cpu/src/fine/mod.rs +++ b/sparse_strips/vello_cpu/src/fine/mod.rs @@ -5,13 +5,15 @@ //! of each pixel and pack it into the pixmap. mod gradient; +mod image; use crate::fine::gradient::GradientFiller; +use crate::fine::image::ImageFiller; use crate::util::scalar::div_255; use alloc::vec; use alloc::vec::Vec; use core::iter; -use vello_common::encode::{EncodedKind, EncodedPaint, GradientLike}; +use vello_common::encode::{EncodedKind, EncodedPaint}; use vello_common::paint::Paint; use vello_common::{ coarse::{Cmd, WideTile}, @@ -122,14 +124,14 @@ impl Fine { let start_x = self.wide_coords.0 * WideTile::WIDTH + x as u16; let start_y = self.wide_coords.1 * Tile::HEIGHT; - fn fill_gradient( + fn fill_complex_paint( color_buf: &mut [u8], blend_buf: &mut [u8], has_opacities: bool, - filler: GradientFiller<'_, T>, + filler: impl Painter, ) { if has_opacities { - filler.run(color_buf); + filler.paint(color_buf); fill::src_over( blend_buf, color_buf.chunks_exact(4).map(|e| [e[0], e[1], e[2], e[3]]), @@ -137,7 +139,7 @@ impl Fine { } else { // Similarly to solid colors we can just override the previous values // if all colors in the gradient are fully opaque. - filler.run(blend_buf); + filler.paint(blend_buf); } } @@ -156,24 +158,28 @@ impl Fine { fill::src_over(blend_buf, iter::repeat(*color)); } - Paint::Indexed(i) => { - let paint = &encoded_paints[i.index()]; + Paint::Indexed(paint) => { + let encoded_paint = &encoded_paints[paint.index()]; - match paint { + match encoded_paint { EncodedPaint::Gradient(g) => match &g.kind { EncodedKind::Linear(l) => { let filler = GradientFiller::new(g, l, start_x, start_y); - fill_gradient(color_buf, blend_buf, g.has_opacities, filler); + fill_complex_paint(color_buf, blend_buf, g.has_opacities, filler); } EncodedKind::Radial(r) => { let filler = GradientFiller::new(g, r, start_x, start_y); - fill_gradient(color_buf, blend_buf, g.has_opacities, filler); + fill_complex_paint(color_buf, blend_buf, g.has_opacities, filler); } EncodedKind::Sweep(s) => { let filler = GradientFiller::new(g, s, start_x, start_y); - fill_gradient(color_buf, blend_buf, g.has_opacities, filler); + fill_complex_paint(color_buf, blend_buf, g.has_opacities, filler); } }, + EncodedPaint::Image(i) => { + let filler = ImageFiller::new(i, start_x, start_y); + fill_complex_paint(color_buf, blend_buf, i.has_opacities, filler); + } } } } @@ -201,13 +207,13 @@ impl Fine { let start_x = self.wide_coords.0 * WideTile::WIDTH + x as u16; let start_y = self.wide_coords.1 * Tile::HEIGHT; - fn strip_gradient( + fn strip_complex_paint( color_buf: &mut [u8], blend_buf: &mut [u8], - filler: GradientFiller<'_, T>, + filler: impl Painter, alphas: &[u8], ) { - filler.run(color_buf); + filler.paint(color_buf); strip::src_over( blend_buf, color_buf.chunks_exact(4).map(|e| [e[0], e[1], e[2], e[3]]), @@ -219,24 +225,28 @@ impl Fine { Paint::Solid(color) => { strip::src_over(blend_buf, iter::repeat(color.to_u8_array()), alphas); } - Paint::Indexed(i) => { - let encoded_paint = &paints[i.index()]; + Paint::Indexed(paint) => { + let encoded_paint = &paints[paint.index()]; match encoded_paint { EncodedPaint::Gradient(g) => match &g.kind { EncodedKind::Linear(l) => { let filler = GradientFiller::new(g, l, start_x, start_y); - strip_gradient(color_buf, blend_buf, filler, alphas); + strip_complex_paint(color_buf, blend_buf, filler, alphas); } EncodedKind::Radial(r) => { let filler = GradientFiller::new(g, r, start_x, start_y); - strip_gradient(color_buf, blend_buf, filler, alphas); + strip_complex_paint(color_buf, blend_buf, filler, alphas); } EncodedKind::Sweep(s) => { let filler = GradientFiller::new(g, s, start_x, start_y); - strip_gradient(color_buf, blend_buf, filler, alphas); + strip_complex_paint(color_buf, blend_buf, filler, alphas); } }, + EncodedPaint::Image(i) => { + let filler = ImageFiller::new(i, start_x, start_y); + strip_complex_paint(color_buf, blend_buf, filler, alphas); + } } } } @@ -366,3 +376,7 @@ pub(crate) mod strip { } } } + +trait Painter { + fn paint(self, target: &mut [u8]); +} diff --git a/sparse_strips/vello_cpu/src/render.rs b/sparse_strips/vello_cpu/src/render.rs index adb9068f8..f6b98159c 100644 --- a/sparse_strips/vello_cpu/src/render.rs +++ b/sparse_strips/vello_cpu/src/render.rs @@ -88,6 +88,10 @@ impl RenderContext { g.encode_into(&mut self.encoded_paints) } + PaintType::Image(mut i) => { + i.transform = self.transform * i.transform; + i.encode_into(&mut self.encoded_paints) + } } } diff --git a/sparse_strips/vello_cpu/tests/assets/luma_image_10x10.png b/sparse_strips/vello_cpu/tests/assets/luma_image_10x10.png new file mode 100644 index 000000000..b981a2e86 Binary files /dev/null and b/sparse_strips/vello_cpu/tests/assets/luma_image_10x10.png differ diff --git a/sparse_strips/vello_cpu/tests/assets/lumaa_image_10x10.png b/sparse_strips/vello_cpu/tests/assets/lumaa_image_10x10.png new file mode 100644 index 000000000..f6e6ae740 Binary files /dev/null and b/sparse_strips/vello_cpu/tests/assets/lumaa_image_10x10.png differ diff --git a/sparse_strips/vello_cpu/tests/assets/rgb_image_10x10.png b/sparse_strips/vello_cpu/tests/assets/rgb_image_10x10.png new file mode 100644 index 000000000..cff75856f Binary files /dev/null and b/sparse_strips/vello_cpu/tests/assets/rgb_image_10x10.png differ diff --git a/sparse_strips/vello_cpu/tests/assets/rgb_image_2x2.png b/sparse_strips/vello_cpu/tests/assets/rgb_image_2x2.png new file mode 100644 index 000000000..65a2a2e3f Binary files /dev/null and b/sparse_strips/vello_cpu/tests/assets/rgb_image_2x2.png differ diff --git a/sparse_strips/vello_cpu/tests/assets/rgb_image_2x3.png b/sparse_strips/vello_cpu/tests/assets/rgb_image_2x3.png new file mode 100644 index 000000000..49d7d6450 Binary files /dev/null and b/sparse_strips/vello_cpu/tests/assets/rgb_image_2x3.png differ diff --git a/sparse_strips/vello_cpu/tests/assets/rgba_image_10x10.png b/sparse_strips/vello_cpu/tests/assets/rgba_image_10x10.png new file mode 100644 index 000000000..94abcd748 Binary files /dev/null and b/sparse_strips/vello_cpu/tests/assets/rgba_image_10x10.png differ diff --git a/sparse_strips/vello_cpu/tests/image.rs b/sparse_strips/vello_cpu/tests/image.rs new file mode 100644 index 000000000..b3290ee5c --- /dev/null +++ b/sparse_strips/vello_cpu/tests/image.rs @@ -0,0 +1,438 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::f64::consts::PI; +use std::path::Path; +use std::sync::Arc; +use vello_common::kurbo::{Affine, Point, Rect}; +use vello_common::paint::Image; +use vello_common::pixmap::Pixmap; +use vello_common::peniko::{Extend, ImageQuality}; +use crate::gradient::tan_45; +use crate::util::{check_ref, crossed_line_star, get_ctx}; + +fn load_image(name: &str) -> Arc { + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("tests/assets/{name}.png")); + Arc::new(Pixmap::from_png(&std::fs::read(path).unwrap()).unwrap()) +} + +fn rgb_img_10x10() -> Arc { + load_image("rgb_image_10x10") +} + +fn rgb_img_2x2() -> Arc { + load_image("rgb_image_2x2") +} + +fn rgb_img_2x3() -> Arc { + load_image("rgb_image_2x3") +} + +fn rgba_img_10x10() -> Arc { + load_image("rgba_image_10x10") +} + +fn luma_img_10x10() -> Arc { + load_image("luma_image_10x10") +} + +fn lumaa_img_10x10() -> Arc { + load_image("lumaa_image_10x10") +} + +macro_rules! repeat { + ($name:expr, $x_repeat:expr, $y_repeat:expr) => { + let mut ctx = get_ctx(100, 100, false); + let rect = Rect::new(10.0, 10.0, 90.0, 90.0); + let im = rgb_img_10x10(); + + ctx.set_paint(Image { + pixmap: im, + x_extend: $x_repeat, + y_extend: $y_repeat, + quality: ImageQuality::Low, + transform: Affine::translate((45.0, 45.0)), + }); + ctx.fill_rect(&rect); + + check_ref(&ctx, $name); + }; +} + +#[test] +fn image_reflect_x_pad_y() { + repeat!("image_reflect_x", Extend::Reflect, Extend::Pad); +} + +#[test] +fn image_pad_x_repeat_y() { + repeat!("image_repeat_y", Extend::Pad, Extend::Repeat); +} + +#[test] +fn image_reflect_x_reflect_y() { + repeat!("image_reflect_x_reflect_y", Extend::Reflect, Extend::Reflect); +} + +#[test] +fn image_repeat_x_repeat_y() { + repeat!("image_repeat_x_repeat_y", Extend::Repeat, Extend::Repeat); +} + +#[test] +fn image_pad_x_pad_y() { + repeat!("image_pad_x_pad_y", Extend::Pad, Extend::Pad); +} + +macro_rules! transform { + ($name:expr, $transform:expr, $p0:expr, $p1: expr, $p2:expr, $p3: expr) => { + let mut ctx = get_ctx(100, 100, false); + let rect = Rect::new($p0, $p1, $p2, $p3); + + let image = Image { + pixmap: rgb_img_10x10(), + x_extend: Extend::Repeat, + y_extend: Extend::Repeat, + quality: ImageQuality::Low, + transform: Affine::IDENTITY, + }; + + ctx.set_transform($transform); + ctx.set_paint(image); + ctx.fill_rect(&rect); + + check_ref(&ctx, $name); + }; +} + +#[test] +fn image_with_transform_identity() { + transform!( + "image_with_transform_identity", + Affine::IDENTITY, + 25.0, + 25.0, + 75.0, + 75.0 + ); +} + +#[test] +fn image_with_transform_translate() { + transform!( + "image_with_transform_translate", + Affine::translate((25.0, 25.0)), + 0.0, + 0.0, + 50.0, + 50.0 + ); +} + +#[test] +fn image_with_transform_scale() { + transform!( + "image_with_transform_scale", + Affine::scale(2.0), + 12.5, + 12.5, + 37.5, + 37.5 + ); +} + +#[test] +fn image_with_transform_negative_scale() { + transform!( + "image_with_transform_negative_scale", + Affine::translate((100.0, 100.0)) * Affine::scale(-2.0), + 12.5, + 12.5, + 37.5, + 37.5 + ); +} + +#[test] +fn image_with_transform_scale_and_translate() { + transform!( + "image_with_transform_scale_and_translate", + Affine::new([2.0, 0.0, 0.0, 2.0, 25.0, 25.0]), + 0.0, + 0.0, + 25.0, + 25.0 + ); +} + +// TODO: The below two test cases fail on Windows CI for some reason. +#[test] +#[ignore] +fn image_with_transform_rotate_1() { + transform!( + "image_with_transform_rotate_1", + Affine::rotate_about(PI / 4.0, Point::new(50.0, 50.0)), + 25.0, + 25.0, + 75.0, + 75.0 + ); +} + +#[test] +#[ignore] +fn image_with_transform_rotate_2() { + transform!( + "image_with_transform_rotate_2", + Affine::rotate_about(-PI / 4.0, Point::new(50.0, 50.0)), + 25.0, + 25.0, + 75.0, + 75.0 + ); +} + +#[test] +fn image_with_transform_scaling_non_uniform() { + transform!( + "image_with_transform_scaling_non_uniform", + Affine::scale_non_uniform(1.0, 2.0), + 25.0, + 12.5, + 75.0, + 37.5 + ); +} + +#[test] +fn image_with_transform_skew_x_1() { + let transform = Affine::translate((-50.0, 0.0)) * Affine::skew(tan_45(), 0.0); + transform!( + "image_with_transform_skew_x_1", + transform, + 25.0, + 25.0, + 75.0, + 75.0 + ); +} + +#[test] +fn image_with_transform_skew_x_2() { + let transform = Affine::translate((50.0, 0.0)) * Affine::skew(-tan_45(), 0.0); + transform!( + "image_with_transform_skew_x_2", + transform, + 25.0, + 25.0, + 75.0, + 75.0 + ); +} + +#[test] +fn image_with_transform_skew_y_1() { + let transform = Affine::translate((0.0, 50.0)) * Affine::skew(0.0, -tan_45()); + transform!( + "image_with_transform_skew_y_1", + transform, + 25.0, + 25.0, + 75.0, + 75.0 + ); +} + +#[test] +fn image_with_transform_skew_y_2() { + let transform = Affine::translate((0.0, -50.0)) * Affine::skew(0.0, tan_45()); + transform!( + "image_with_transform_skew_y_2", + transform, + 25.0, + 25.0, + 75.0, + 75.0 + ); +} + +#[test] +fn image_complex_shape() { + let mut ctx = get_ctx(100, 100, false); + let path = crossed_line_star(); + + let image = Image { + pixmap: rgb_img_10x10(), + x_extend: Extend::Repeat, + y_extend: Extend::Repeat, + quality: ImageQuality::Low, + transform: Affine::IDENTITY, + }; + + ctx.set_paint(image); + ctx.fill_path(&path); + + check_ref(&ctx, "image_complex_shape"); +} + +#[test] +fn image_global_alpha() { + let mut ctx = get_ctx(100, 100, false); + let rect = Rect::new(10.0, 10.0, 90.0 ,90.0); + + let mut pix = rgb_img_10x10(); + Arc::make_mut(&mut pix).multiply_alpha(75); + + let image = Image { + pixmap: pix, + x_extend: Extend::Repeat, + y_extend: Extend::Repeat, + quality: ImageQuality::Low, + transform: Affine::IDENTITY, + }; + + ctx.set_paint(image); + ctx.fill_rect(&rect); + + check_ref(&ctx, "image_global_alpha"); +} + +macro_rules! image_format { + ($name:expr, $image:expr) => { + let mut ctx = get_ctx(100, 100, false); + let rect = Rect::new(10.0, 10.0, 90.0 ,90.0); + + let image = Image { + pixmap: $image, + x_extend: Extend::Repeat, + y_extend: Extend::Repeat, + quality: ImageQuality::Low, + transform: Affine::IDENTITY, + }; + + ctx.set_paint(image); + ctx.fill_rect(&rect); + + check_ref(&ctx, $name); + }; +} + +#[test] +fn image_rgb_image() { + image_format!("image_rgb_image", rgb_img_10x10()); +} + +#[test] +fn image_rgba_image() { + image_format!("image_rgba_image", rgba_img_10x10()); +} + +#[test] +fn image_luma_image() { + image_format!("image_luma_image", luma_img_10x10()); +} + +#[test] +fn image_lumaa_image() { + image_format!("image_lumaa_image", lumaa_img_10x10()); +} + +macro_rules! quality { + ($name:expr, $transform:expr, $image:expr, $quality:expr, $extend:expr) => { + let mut ctx = get_ctx(100, 100, false); + let rect = Rect::new(10.0, 10.0, 90.0, 90.0); + + let image = Image { + pixmap: $image, + x_extend: $extend, + y_extend: $extend, + quality: $quality, + transform: $transform, + }; + + ctx.set_paint(image); + ctx.fill_rect(&rect); + + check_ref(&ctx, $name); + }; +} + +// Outputs of those tests were compared against Blend2D and tiny-skia. + +#[test] +fn image_bilinear_identity() { + quality!("image_bilinear_identity", Affine::IDENTITY, rgb_img_2x2(), ImageQuality::Medium, Extend::Reflect); +} + +#[test] +fn image_bilinear_2x_scale() { + quality!("image_bilinear_2x_scale", Affine::scale(2.0), rgb_img_2x2(), ImageQuality::Medium, Extend::Reflect); +} + +#[test] +fn image_bilinear_5x_scale() { + quality!("image_bilinear_5x_scale", Affine::scale(5.0), rgb_img_2x2(), ImageQuality::Medium, Extend::Reflect); +} + +#[test] +fn image_bilinear_10x_scale() { + quality!("image_bilinear_10x_scale", Affine::scale(10.0), rgb_img_2x2(), ImageQuality::Medium, Extend::Reflect); +} + +#[test] +fn image_bilinear_with_rotation() { + quality!("image_bilinear_with_rotation", Affine::scale(5.0) * Affine::rotate(45.0_f64.to_radians()), rgb_img_2x2(), ImageQuality::Medium, Extend::Reflect); +} + +#[test] +fn image_bilinear_with_translation() { + quality!("image_bilinear_with_translation", Affine::scale(5.0) * Affine::translate((10.0, 10.0)), rgb_img_2x2(), ImageQuality::Medium, Extend::Reflect); +} + +#[test] +fn image_bilinear_10x_scale_2() { + quality!("image_bilinear_10x_scale_2", Affine::scale(10.0), rgb_img_2x3(), ImageQuality::Medium, Extend::Reflect); +} + +// This one looks slightly different from tiny-skia. In tiny-skia, it looks exactly the same as with +// `Nearest`, while in our case it looks overall a bit darker. I'm not 100% sure who is right here, +// but I think ours should be correct, because AFAIK for bicubic scaling, the output image does +// not necessarily need to look the same as with `Nearest` with identity scaling. Would be nice to +// verify this somehow, though. +// +// We also ported the cubic polynomials directly from current Skia, while tiny-skia (seems?) to use +// either an outdated version or a slightly adapted one. +#[test] +fn image_bicubic_identity() { + quality!("image_bicubic_identity", Affine::IDENTITY, rgb_img_2x2(), ImageQuality::High, Extend::Reflect); +} + +#[test] +fn image_bicubic_2x_scale() { + quality!("image_bicubic_2x_scale", Affine::scale(2.0), rgb_img_2x2(), ImageQuality::High, Extend::Reflect); +} + +#[test] +fn image_bicubic_5x_scale() { + quality!("image_bicubic_5x_scale", Affine::scale(5.0), rgb_img_2x2(), ImageQuality::High, Extend::Reflect); +} + +#[test] +fn image_bicubic_10x_scale() { + quality!("image_bicubic_10x_scale", Affine::scale(10.0), rgb_img_2x2(), ImageQuality::High, Extend::Reflect); +} + +#[test] +fn image_bicubic_with_rotation() { + quality!("image_bicubic_with_rotation", Affine::scale(5.0) * Affine::rotate(45.0_f64.to_radians()), rgb_img_2x2(), ImageQuality::High, Extend::Reflect); +} + +#[test] +fn image_bicubic_with_translation() { + quality!("image_bicubic_with_translation", Affine::scale(5.0) * Affine::translate((10.0, 10.0)), rgb_img_2x2(), ImageQuality::High, Extend::Reflect); +} + +#[test] +fn image_bicubic_10x_scale_2() { + quality!("image_bicubic_10x_scale_2", Affine::scale(10.0), rgb_img_2x3(), ImageQuality::High, Extend::Reflect); +} \ No newline at end of file