diff --git a/sparse_strips/vello_common/src/encode.rs b/sparse_strips/vello_common/src/encode.rs index 453e176c8..03b0990f3 100644 --- a/sparse_strips/vello_common/src/encode.rs +++ b/sparse_strips/vello_common/src/encode.rs @@ -8,8 +8,8 @@ use crate::color::palette::css::BLACK; use crate::color::{ColorSpaceTag, HueDirection, Srgb, gradient}; use crate::kurbo::{Affine, Point, Vec2}; use crate::math::{FloatExt, compute_erf7}; -use crate::paint::{Image, ImageSource, IndexedPaint, Paint, PremulColor}; -use crate::peniko::{ColorStop, ColorStops, Extend, Gradient, GradientKind, ImageQuality}; +use crate::paint::{Image, ImageSource, IndexedPaint, Paint, PremulColor, encode_image_with}; +use crate::peniko::{ColorStop, ColorStops, Extend, Gradient, GradientKind}; use alloc::borrow::Cow; use alloc::fmt::Debug; use alloc::vec; @@ -429,60 +429,7 @@ impl private::Sealed for Image {} impl EncodeExt for Image { fn encode_into(&self, paints: &mut Vec, transform: Affine) -> Paint { - let idx = paints.len(); - - let mut sampler = self.sampler; - - if sampler.alpha != 1.0 { - // If the sampler alpha is not 1.0, we need to force alpha compositing. - unimplemented!("Applying opacity to image commands"); - } - - let c = transform.as_coeffs(); - - // Optimize image quality for integer-only translations. - if (c[0] as f32 - 1.0).is_nearly_zero() - && (c[1] as f32).is_nearly_zero() - && (c[2] as f32).is_nearly_zero() - && (c[3] as f32 - 1.0).is_nearly_zero() - && ((c[4] - c[4].floor()) as f32).is_nearly_zero() - && ((c[5] - c[5].floor()) as f32).is_nearly_zero() - && sampler.quality == ImageQuality::Medium - { - sampler.quality = ImageQuality::Low; - } - - // Similarly to gradients, apply a 0.5 offset so we sample at the center of - // a pixel. - let transform = transform.inverse() * Affine::translate((0.5, 0.5)); - - let (x_advance, y_advance) = x_y_advances(&transform); - - let encoded = match &self.image { - ImageSource::Pixmap(pixmap) => { - EncodedImage { - source: ImageSource::Pixmap(pixmap.clone()), - sampler, - // While we could optimize RGB8 images, it's probably not worth the trouble. - has_opacities: true, - transform, - x_advance, - y_advance, - } - } - ImageSource::OpaqueId(image) => EncodedImage { - source: ImageSource::OpaqueId(*image), - sampler, - has_opacities: true, - transform, - x_advance, - y_advance, - }, - }; - - paints.push(EncodedPaint::Image(encoded)); - - Paint::Indexed(IndexedPaint::new(idx)) + encode_image_with(self, paints, transform, false) } } @@ -524,6 +471,30 @@ pub struct EncodedImage { pub x_advance: Vec2, /// The advance in image coordinates for one step in the y direction. pub y_advance: Vec2, + /// Whether the extends in `ImageSampler` should be ignored. + pub ignore_extend: bool, +} + +impl EncodedImage { + /// Return the x extend of the image. + #[inline(always)] + pub fn x_extend(&self) -> Option { + if self.ignore_extend { + None + } else { + Some(self.sampler.x_extend) + } + } + + /// Return the y extend of the image. + #[inline(always)] + pub fn y_extend(&self) -> Option { + if self.ignore_extend { + None + } else { + Some(self.sampler.y_extend) + } + } } /// Computed properties of a linear gradient. diff --git a/sparse_strips/vello_common/src/paint.rs b/sparse_strips/vello_common/src/paint.rs index 4f4c0ed09..2af7f47c3 100644 --- a/sparse_strips/vello_common/src/paint.rs +++ b/sparse_strips/vello_common/src/paint.rs @@ -3,13 +3,20 @@ //! Types for paints. +use crate::encode::{EncodedImage, EncodedPaint, x_y_advances}; +use crate::kurbo::Affine; +use crate::math::FloatExt; use crate::pixmap::Pixmap; use alloc::sync::Arc; +use alloc::vec::Vec; use peniko::{ - Gradient, + Gradient, ImageQuality, color::{AlphaColor, PremulRgba8, Srgb}, }; +#[cfg(not(feature = "std"))] +use peniko::kurbo::common::FloatFuncs as _; + /// A paint that needs to be resolved via its index. // In the future, we might add additional flags, that's why we have // this thin wrapper around u32, so we can change the underlying @@ -139,6 +146,71 @@ impl ImageSource { /// An image. pub type Image = peniko::ImageBrush; +/// Encode the image, optionally ignoring the extends of the image. +pub fn encode_image_with( + image: &Image, + paints: &mut Vec, + transform: Affine, + ignore_extend: bool, +) -> Paint { + let idx = paints.len(); + + let mut sampler = image.sampler; + + if sampler.alpha != 1.0 { + // If the sampler alpha is not 1.0, we need to force alpha compositing. + unimplemented!("Applying opacity to image commands"); + } + + let c = transform.as_coeffs(); + + // Optimize image quality for integer-only translations. + if (c[0] as f32 - 1.0).is_nearly_zero() + && (c[1] as f32).is_nearly_zero() + && (c[2] as f32).is_nearly_zero() + && (c[3] as f32 - 1.0).is_nearly_zero() + && ((c[4] - c[4].floor()) as f32).is_nearly_zero() + && ((c[5] - c[5].floor()) as f32).is_nearly_zero() + && sampler.quality == ImageQuality::Medium + { + sampler.quality = ImageQuality::Low; + } + + // Similarly to gradients, apply a 0.5 offset so we sample at the center of + // a pixel. + let transform = transform.inverse() * Affine::translate((0.5, 0.5)); + + let (x_advance, y_advance) = x_y_advances(&transform); + + let encoded = match &image.image { + ImageSource::Pixmap(pixmap) => { + EncodedImage { + source: ImageSource::Pixmap(pixmap.clone()), + sampler, + // While we could optimize RGB8 images, it's probably not worth the trouble. + has_opacities: true, + transform, + x_advance, + y_advance, + ignore_extend, + } + } + ImageSource::OpaqueId(image) => EncodedImage { + source: ImageSource::OpaqueId(*image), + sampler, + has_opacities: true, + transform, + x_advance, + y_advance, + ignore_extend, + }, + }; + + paints.push(EncodedPaint::Image(encoded)); + + Paint::Indexed(IndexedPaint::new(idx)) +} + /// A premultiplied color. #[derive(Debug, Clone, PartialEq, Copy)] pub struct PremulColor { diff --git a/sparse_strips/vello_cpu/src/fine/common/image.rs b/sparse_strips/vello_cpu/src/fine/common/image.rs index d3fe9fa57..a12fc2767 100644 --- a/sparse_strips/vello_cpu/src/fine/common/image.rs +++ b/sparse_strips/vello_cpu/src/fine/common/image.rs @@ -10,6 +10,8 @@ use vello_common::fearless_simd::{Bytes, Simd, SimdBase, SimdFloat, f32x4, f32x1 use vello_common::pixmap::Pixmap; use vello_common::simd::element_wise_splat; +const PIXEL_OOB_MARKER: f32 = -1.0; + /// A painter for nearest-neighbor images with no skewing. #[derive(Debug)] pub(crate) struct PlainNNImagePainter<'a, S: Simd> { @@ -38,7 +40,7 @@ impl<'a, S: Simd> PlainNNImagePainter<'a, S> { data.x_advances.1, data.y_advances.1, ), - image.sampler.y_extend, + image.y_extend(), data.height, data.height_inv, ); @@ -68,7 +70,7 @@ impl Iterator for PlainNNImagePainter<'_, S> { let x_pos = extend( self.simd, self.cur_x_pos, - self.data.image.sampler.x_extend, + self.data.image.x_extend(), self.data.width, self.data.width_inv, ); @@ -116,7 +118,7 @@ impl Iterator for NNImagePainter<'_, S> { self.data.x_advances.0, self.data.y_advances.0, ), - self.data.image.sampler.x_extend, + self.data.image.x_extend(), self.data.width, self.data.width_inv, ); @@ -129,7 +131,7 @@ impl Iterator for NNImagePainter<'_, S> { self.data.x_advances.1, self.data.y_advances.1, ), - self.data.image.sampler.y_extend, + self.data.image.y_extend(), self.data.height, self.data.height_inv, ); @@ -214,7 +216,7 @@ impl Iterator for FilteredImagePainter<'_, S> { extend( self.simd, x_positions + $offsets[$idx], - self.data.image.sampler.y_extend, + self.data.image.x_extend(), self.data.width, self.data.width_inv, ) @@ -226,7 +228,7 @@ impl Iterator for FilteredImagePainter<'_, S> { extend( self.simd, y_positions + $offsets[$idx], - self.data.image.sampler.y_extend, + self.data.image.y_extend(), self.data.height, self.data.height_inv, ) @@ -335,6 +337,7 @@ pub(crate) struct ImagePainterData<'a, S: Simd> { pub(crate) pixmap: &'a Pixmap, pub(crate) x_advances: (f32, f32), pub(crate) y_advances: (f32, f32), + pub(crate) mask_oob: bool, pub(crate) height: f32x4, pub(crate) height_inv: f32x4, pub(crate) width: f32x4, @@ -362,11 +365,13 @@ impl<'a, S: Simd> ImagePainterData<'a, S> { let x_advances = (image.x_advance.x as f32, image.x_advance.y as f32); let y_advances = (image.y_advance.x as f32, image.y_advance.y as f32); + let mask_oob = image.ignore_extend; Self { cur_pos: start_pos, pixmap, x_advances, + mask_oob, y_advances, image, width, @@ -387,7 +392,7 @@ pub(crate) fn sample( ) -> u8x16 { let idx = x_positions.cvt_u32() + y_positions.cvt_u32() * data.width_u32; - u32x4::from_slice( + let mut res = u32x4::from_slice( simd, &[ data.pixmap.sample_idx(idx[0]).to_u32(), @@ -395,15 +400,25 @@ pub(crate) fn sample( data.pixmap.sample_idx(idx[2]).to_u32(), data.pixmap.sample_idx(idx[3]).to_u32(), ], - ) - .reinterpret_u8() + ); + + if data.mask_oob { + let mask = simd.or_mask32x4( + x_positions.simd_eq(f32x4::splat(simd, PIXEL_OOB_MARKER)), + y_positions.simd_eq(f32x4::splat(simd, PIXEL_OOB_MARKER)), + ); + + res = simd.select_u32x4(mask, u32x4::splat(simd, 0), res); + } + + res.reinterpret_u8() } #[inline(always)] pub(crate) fn extend( simd: S, val: f32x4, - extend: crate::peniko::Extend, + extend: Option, max: f32x4, inv_max: f32x4, ) -> f32x4 { @@ -414,13 +429,13 @@ pub(crate) fn extend( match extend { // Note that max should be exclusive, so subtract a small bias to enforce that. // Otherwise, we might sample out-of-bounds pixels. - crate::peniko::Extend::Pad => val.min(max - bias).max(f32x4::splat(simd, 0.0)), - crate::peniko::Extend::Repeat => val + Some(crate::peniko::Extend::Pad) => val.min(max - bias).max(f32x4::splat(simd, 0.0)), + Some(crate::peniko::Extend::Repeat) => val .msub((val * inv_max).floor(), max) // In certain edge cases, we might still end up with a higher number. .min(max - 1.0), // - crate::peniko::Extend::Reflect => { + Some(crate::peniko::Extend::Reflect) => { let u = val - (val * inv_max * f32x4::splat(simd, 0.5)).floor() * f32x4::splat(simd, 2.0) * max; let s = (u * inv_max).floor(); @@ -438,6 +453,10 @@ pub(crate) fn extend( // In certain edge cases, we might still end up with a higher number. .min(max - 1.0) } + None => { + let mask = simd.and_mask32x4(val.simd_ge(f32x4::splat(simd, 0.0)), val.simd_lt(max)); + simd.select_f32x4(mask, val, f32x4::splat(simd, PIXEL_OOB_MARKER)) + } } } @@ -546,7 +565,7 @@ mod tests { let max_inv = 1.0 / max; let num = f32x4::splat(simd, 127.00001); - let res = extend(simd, num, crate::peniko::Extend::Repeat, max, max_inv); + let res = extend(simd, num, Some(crate::peniko::Extend::Repeat), max, max_inv); assert!(res[0] <= 127.0); } diff --git a/sparse_strips/vello_cpu/src/fine/lowp/image.rs b/sparse_strips/vello_cpu/src/fine/lowp/image.rs index ad9d6c9b4..f59a5e5df 100644 --- a/sparse_strips/vello_cpu/src/fine/lowp/image.rs +++ b/sparse_strips/vello_cpu/src/fine/lowp/image.rs @@ -60,7 +60,7 @@ impl Iterator for BilinearImagePainter<'_, S> { extend( self.simd, x_pos, - self.data.image.sampler.x_extend, + self.data.image.x_extend(), self.data.width, self.data.width_inv, ) @@ -70,7 +70,7 @@ impl Iterator for BilinearImagePainter<'_, S> { extend( self.simd, y_pos, - self.data.image.sampler.y_extend, + self.data.image.y_extend(), self.data.height, self.data.height_inv, ) diff --git a/sparse_strips/vello_cpu/src/render.rs b/sparse_strips/vello_cpu/src/render.rs index ebcb5ef37..1625a4b4d 100644 --- a/sparse_strips/vello_cpu/src/render.rs +++ b/sparse_strips/vello_cpu/src/render.rs @@ -10,8 +10,8 @@ use crate::dispatch::Dispatcher; use crate::dispatch::multi_threaded::MultiThreadedDispatcher; use crate::dispatch::single_threaded::SingleThreadedDispatcher; use crate::kurbo::{PathEl, Point}; +use crate::peniko::{ImageQuality, ImageSampler}; use alloc::boxed::Box; -#[cfg(feature = "text")] use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; @@ -20,9 +20,8 @@ use vello_common::encode::{EncodeExt, EncodedPaint}; use vello_common::fearless_simd::Level; use vello_common::kurbo::{Affine, BezPath, Cap, Join, Rect, Stroke}; use vello_common::mask::Mask; -#[cfg(feature = "text")] use vello_common::paint::ImageSource; -use vello_common::paint::{Paint, PaintType}; +use vello_common::paint::{Image, Paint, PaintType, encode_image_with}; use vello_common::peniko::color::palette::css::BLACK; use vello_common::peniko::{BlendMode, Compose, Fill, Mix}; use vello_common::pixmap::Pixmap; @@ -169,6 +168,38 @@ impl RenderContext { } } + /// Draw an image into the pixmap. + /// + /// This method can be used to draw external bitmaps like PNG images (which first have to + /// be converted into a pixmap) or existing pixmaps into the current render context. + /// + /// By default, the pixmap will be positioned with the top-left corner at the point (0, 0) + /// and the width and height set to the natural dimensions of the image. The positioning and + /// scaling of the image can be changed by applying a transform to the render context. + pub fn draw_pixmap(&mut self, pixmap: Arc, image_quality: ImageQuality) { + let (width, height) = (pixmap.width(), pixmap.height()); + let image = Image { + image: ImageSource::Pixmap(pixmap), + sampler: ImageSampler { + // Those two will be ignored, since we ignore the extend in the encoded image. + x_extend: Default::default(), + y_extend: Default::default(), + quality: image_quality, + alpha: 1.0, + }, + }; + + let paint = encode_image_with(&image, &mut self.encoded_paints, self.transform, true); + self.rect_to_temp_path(&Rect::new(0.0, 0.0, width as f64, height as f64)); + self.dispatcher.fill_path( + &self.temp_path, + self.fill_rule, + self.transform, + paint, + self.aliasing_threshold, + ); + } + /// Fill a path. pub fn fill_path(&mut self, path: &BezPath) { let paint = self.encode_current_paint(); diff --git a/sparse_strips/vello_sparse_tests/snapshots/image_no_extend.png b/sparse_strips/vello_sparse_tests/snapshots/image_no_extend.png new file mode 100644 index 000000000..69341216a --- /dev/null +++ b/sparse_strips/vello_sparse_tests/snapshots/image_no_extend.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bc170064f9382fdd45f98f7ad6163aff1392af8a0f9c594c72786dee893ea4d +size 2314 diff --git a/sparse_strips/vello_sparse_tests/tests/image.rs b/sparse_strips/vello_sparse_tests/tests/image.rs index 33b50f3cb..61c9cce2a 100644 --- a/sparse_strips/vello_sparse_tests/tests/image.rs +++ b/sparse_strips/vello_sparse_tests/tests/image.rs @@ -562,3 +562,17 @@ fn image_with_multiple_clip_layers(ctx: &mut impl Renderer) { ctx.pop_layer(); ctx.pop_layer(); } + +#[vello_test(skip_hybrid)] +fn image_no_extend(ctx: &mut impl Renderer) { + let image_source = rgb_img_10x10(ctx); + let pixmap = match image_source { + ImageSource::Pixmap(p) => p, + ImageSource::OpaqueId(_) => { + unimplemented!() + } + }; + + ctx.set_transform(Affine::translate((10.0, 10.0)) * Affine::scale(8.0)); + ctx.draw_pixmap(pixmap, ImageQuality::High); +} diff --git a/sparse_strips/vello_sparse_tests/tests/renderer.rs b/sparse_strips/vello_sparse_tests/tests/renderer.rs index e2ce9a185..d826cd6d5 100644 --- a/sparse_strips/vello_sparse_tests/tests/renderer.rs +++ b/sparse_strips/vello_sparse_tests/tests/renderer.rs @@ -11,6 +11,7 @@ use vello_common::paint::{ImageSource, PaintType}; use vello_common::peniko::{BlendMode, Fill, FontData}; use vello_common::pixmap::Pixmap; use vello_common::recording::{Recordable, Recorder, Recording}; +use vello_cpu::peniko::ImageQuality; use vello_cpu::{Level, RenderContext, RenderMode, RenderSettings}; use vello_hybrid::Scene; #[cfg(all(target_arch = "wasm32", feature = "webgl"))] @@ -31,6 +32,7 @@ pub(crate) trait Renderer: Sized { fn fill_rect(&mut self, rect: &Rect); fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32); fn stroke_rect(&mut self, rect: &Rect); + fn draw_pixmap(&mut self, pixmap: Arc, quality: ImageQuality); fn glyph_run(&mut self, font: &FontData) -> GlyphRunBuilder<'_, Self::GlyphRenderer>; fn push_layer( &mut self, @@ -99,6 +101,10 @@ impl Renderer for RenderContext { Self::stroke_rect(self, rect); } + fn draw_pixmap(&mut self, pixmap: Arc, quality: ImageQuality) { + Self::draw_pixmap(self, pixmap, quality); + } + fn glyph_run(&mut self, font: &FontData) -> GlyphRunBuilder<'_, Self> { Self::glyph_run(self, font) } @@ -287,6 +293,10 @@ impl Renderer for HybridRenderer { self.scene.stroke_rect(rect); } + fn draw_pixmap(&mut self, _: Arc, _: ImageQuality) { + unimplemented!() + } + fn glyph_run(&mut self, font: &FontData) -> GlyphRunBuilder<'_, Self::GlyphRenderer> { self.scene.glyph_run(font) } @@ -571,6 +581,10 @@ impl Renderer for HybridRenderer { self.scene.stroke_rect(rect); } + fn draw_pixmap(&mut self, _: Arc, _: ImageQuality) { + unimplemented!() + } + fn glyph_run(&mut self, font: &FontData) -> GlyphRunBuilder<'_, Self::GlyphRenderer> { self.scene.glyph_run(font) }