diff --git a/imaging_vello_hybrid/src/lib.rs b/imaging_vello_hybrid/src/lib.rs index bb5b90d..8714d89 100644 --- a/imaging_vello_hybrid/src/lib.rs +++ b/imaging_vello_hybrid/src/lib.rs @@ -166,7 +166,7 @@ use imaging::RgbaImage; use imaging::record::{Scene, ValidateError, replay}; use imaging::render::{ GpuReadbackError, ImageBufferFormat, ImageBufferTarget, ImageRenderer, ImageRendererError, - ImageTargetError, RenderContentError, RenderSource, RenderUnsupportedError, + ImageTargetError, RenderContentError, RenderSource, }; pub use imaging_wgpu::wgpu; use imaging_wgpu::{TextureRenderer, TextureRendererError, TextureTargetError, TextureViewTarget}; @@ -187,12 +187,8 @@ pub enum Error { InvalidScene(ValidateError), /// An image brush was encountered on a sink path that has no renderer-backed image resolver. UnsupportedImageBrush, - /// A filter configuration could not be translated. - UnsupportedFilter, /// Masks are not supported by this backend yet. UnsupportedMask, - /// Blurred rounded rect draws are not supported by this backend yet. - UnsupportedBlurredRoundedRect, /// Vello hybrid returned a render error. Render(RenderError), /// An internal invariant was violated. @@ -570,16 +566,6 @@ fn map_texture_renderer_error(error: Error) -> TextureRendererError { Error::InvalidScene(error) => { TextureRendererError::Content(RenderContentError::InvalidScene(error)) } - Error::UnsupportedImageBrush => { - TextureRendererError::Unsupported(RenderUnsupportedError::ImageBrush) - } - Error::UnsupportedFilter => { - TextureRendererError::Unsupported(RenderUnsupportedError::Filter) - } - Error::UnsupportedMask => TextureRendererError::Unsupported(RenderUnsupportedError::Mask), - Error::UnsupportedBlurredRoundedRect => { - TextureRendererError::Unsupported(RenderUnsupportedError::BlurredRoundedRect) - } Error::Internal("render width too large" | "render height too large") => { TextureRendererError::Target(TextureTargetError::DimensionsTooLarge) } @@ -640,8 +626,10 @@ fn map_readback_image_error(error: ReadbackError) -> ImageRendererError { #[cfg(test)] mod tests { use super::*; - use imaging::{Painter, record::Scene, render::ImageTargetError}; - use kurbo::Rect; + use imaging::{ + BlurredRoundedRect, Composite, Filter, Painter, record::Scene, render::ImageTargetError, + }; + use kurbo::{Affine, Rect}; use peniko::{Blob, Brush, Color, ImageAlphaType, ImageBrush, ImageData, ImageFormat}; use pollster::block_on; use std::sync::Arc; @@ -921,4 +909,64 @@ mod tests { assert_eq!(image.width, 20); assert_eq!(image.height, 20); } + #[test] + fn blurred_rounded_rect_blurs_beyond_source_rect() { + let Ok((device, queue)) = try_init_device_and_queue() else { + return; + }; + let mut renderer = VelloHybridRenderer::new(device, queue); + + let mut scene = Scene::new(); + { + let mut painter = Painter::new(&mut scene); + painter.blurred_rounded_rect(BlurredRoundedRect { + transform: Affine::IDENTITY, + rect: Rect::new(8.0, 8.0, 24.0, 24.0), + color: Color::from_rgba8(0x40, 0x80, 0xc0, 0xff), + radius: 6.0, + std_dev: 3.0, + composite: Composite::default(), + }); + } + + let native = renderer.encode_scene(&scene, 32, 32).unwrap(); + let image = renderer.render(&native, 32, 32).unwrap(); + + let alpha_at = |x: usize, y: usize| -> u8 { image.data[(y * 32 + x) * 4 + 3] }; + assert_eq!(alpha_at(2, 2), 0); + assert!(alpha_at(6, 16) > 0); + assert!(alpha_at(16, 16) > alpha_at(6, 16)); + } + + #[test] + fn filtered_group_blurs_beyond_source_rect() { + let Ok((device, queue)) = try_init_device_and_queue() else { + return; + }; + let mut renderer = VelloHybridRenderer::new(device, queue); + + let mut scene = Scene::new(); + { + let mut painter = Painter::new(&mut scene); + painter.with_group( + imaging::GroupRef::new().with_filters(&[Filter::blur(3.0)]), + |group| { + group + .fill( + Rect::new(8.0, 8.0, 24.0, 24.0), + Color::from_rgb8(0x20, 0x90, 0x60), + ) + .draw(); + }, + ); + } + + let native = renderer.encode_scene(&scene, 32, 32).unwrap(); + let image = renderer.render(&native, 32, 32).unwrap(); + + let alpha_at = |x: usize, y: usize| -> u8 { image.data[(y * 32 + x) * 4 + 3] }; + assert_eq!(alpha_at(2, 2), 0); + assert!(alpha_at(6, 16) > 0); + assert!(alpha_at(16, 16) > alpha_at(6, 16)); + } } diff --git a/imaging_vello_hybrid/src/scene_sink.rs b/imaging_vello_hybrid/src/scene_sink.rs index 04998d7..f853ee8 100644 --- a/imaging_vello_hybrid/src/scene_sink.rs +++ b/imaging_vello_hybrid/src/scene_sink.rs @@ -9,6 +9,8 @@ use imaging::{ }; use kurbo::{Affine, Shape as _}; use peniko::{Brush, BrushRef, ImageBrush, Style}; +use std::sync::Arc; +use vello_common::filter_effects::{EdgeMode, Filter as VelloFilter, FilterGraph, FilterPrimitive}; use vello_common::glyph::Glyph as VelloGlyph; /// Borrowed adapter that streams `imaging` commands into an existing [`vello_hybrid::Scene`]. @@ -212,8 +214,73 @@ impl<'a> VelloHybridSceneSink<'a> { } } - fn draw_blurred_rounded_rect(&mut self, _draw: BlurredRoundedRect) { - self.set_error_once(Error::UnsupportedBlurredRoundedRect); + fn draw_blurred_rounded_rect(&mut self, draw: BlurredRoundedRect) { + #[expect( + clippy::cast_possible_truncation, + reason = "filter sigma is stored as f32" + )] + let blur_sigma = draw.std_dev as f32; + self.scene.set_transform(draw.transform); + self.scene.set_fill_rule(peniko::Fill::NonZero); + self.scene.push_layer( + None, + Some(draw.composite.blend), + Some(draw.composite.alpha), + None, + self.filters_to_vello(&[imaging::Filter::blur(blur_sigma)]), + ); + self.scene.set_blend_mode(Composite::default().blend); + self.scene.set_paint(Brush::Solid( + draw.color.multiply_alpha(Composite::default().alpha), + )); + let path = draw + .rect + .to_rounded_rect(draw.radius) + .to_path(self.tolerance); + self.scene.fill_path(&path); + self.scene.pop_layer(); + } + + fn filters_to_vello(&self, filters: &[imaging::Filter]) -> Option { + if filters.is_empty() { + return None; + } + + let mut graph = FilterGraph::new(); + let mut last = None; + for filter in filters { + let primitive = match *filter { + imaging::Filter::Flood { color } => FilterPrimitive::Flood { color }, + imaging::Filter::Blur { + std_deviation_x, + std_deviation_y, + } => FilterPrimitive::GaussianBlur { + std_deviation: std_deviation_x.max(std_deviation_y), + edge_mode: EdgeMode::None, + }, + imaging::Filter::DropShadow { + dx, + dy, + std_deviation_x, + std_deviation_y, + color, + } => FilterPrimitive::DropShadow { + dx, + dy, + std_deviation: std_deviation_x.max(std_deviation_y), + color, + edge_mode: EdgeMode::None, + }, + imaging::Filter::Offset { dx, dy } => FilterPrimitive::Offset { dx, dy }, + }; + last = Some(graph.add(primitive, None)); + } + if let Some(output) = last { + graph.set_output(output); + } + Some(VelloFilter { + graph: Arc::new(graph), + }) } } @@ -249,10 +316,6 @@ impl PaintSink for VelloHybridSceneSink<'_> { self.set_error_once(Error::UnsupportedMask); return; } - if !group.filters.is_empty() { - self.set_error_once(Error::UnsupportedFilter); - return; - } let clip_path = group.clip.map(|clip| { let (xf, path, fill_rule) = self.clip_to_path(clip); self.scene.set_transform(xf); @@ -262,8 +325,9 @@ impl PaintSink for VelloHybridSceneSink<'_> { let blend = Some(group.composite.blend); let opacity = Some(group.composite.alpha); + let filter = self.filters_to_vello(group.filters); self.scene - .push_layer(clip_path.as_ref(), blend, opacity, None, None); + .push_layer(clip_path.as_ref(), blend, opacity, None, filter); self.group_depth += 1; } @@ -388,12 +452,13 @@ mod tests { } #[test] - fn hybrid_scene_sink_rejects_filters() { + fn hybrid_scene_sink_supports_filters() { let mut scene = vello_hybrid::Scene::new(32, 32); scene.reset(); let mut sink = VelloHybridSceneSink::new(&mut scene); sink.push_group(GroupRef::new().with_filters(&[Filter::blur(2.0)])); - assert!(matches!(sink.finish(), Err(Error::UnsupportedFilter))); + sink.pop_group(); + assert!(matches!(sink.finish(), Ok(()))); } #[test]