From 05917009f44890b71278e04ac610fc325b0c5681 Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Fri, 17 Apr 2026 22:00:17 -0400 Subject: [PATCH] don't return err on unsupported --- imaging/src/render.rs | 40 -------- imaging_skia/src/lib.rs | 24 +---- imaging_skia/src/sinks.rs | 14 +-- imaging_vello/src/lib.rs | 29 +----- imaging_vello/src/scene_sink.rs | 77 ++++++++------- imaging_vello_cpu/src/lib.rs | 13 +-- imaging_vello_hybrid/README.md | 4 +- imaging_vello_hybrid/src/lib.rs | 21 +---- imaging_vello_hybrid/src/scene_sink.rs | 125 +++++++++++++++++-------- imaging_wgpu/src/lib.rs | 4 - 10 files changed, 141 insertions(+), 210 deletions(-) diff --git a/imaging/src/render.rs b/imaging/src/render.rs index 5a2d9d1..c77aa32 100644 --- a/imaging/src/render.rs +++ b/imaging/src/render.rs @@ -90,42 +90,6 @@ impl core::error::Error for RenderContentError { } } -/// Shared unsupported-feature failures surfaced by renderer traits. -#[derive(Debug)] -pub enum RenderUnsupportedError { - /// An image brush was encountered on a backend that cannot render it. - ImageBrush, - /// A filter configuration could not be translated for the active backend. - Filter, - /// A mask mode or masking primitive is unsupported by the active backend. - Mask, - /// Glyph rendering requested a blend mode or transform unsupported by the active backend. - Glyph, - /// A blurred rounded-rect primitive is unsupported by the active backend. - BlurredRoundedRect, - /// The source used clip/group nesting the active backend cannot represent. - UnbalancedLayerStack, -} - -impl core::fmt::Display for RenderUnsupportedError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::ImageBrush => f.write_str("backend does not support image brushes"), - Self::Filter => f.write_str("backend does not support this filter"), - Self::Mask => f.write_str("backend does not support this mask"), - Self::Glyph => f.write_str("backend does not support this glyph operation"), - Self::BlurredRoundedRect => { - f.write_str("backend does not support this blurred rounded rect operation") - } - Self::UnbalancedLayerStack => { - f.write_str("scene uses clip/group nesting unsupported by this backend") - } - } - } -} - -impl core::error::Error for RenderUnsupportedError {} - /// Shared GPU readback failures surfaced by image renderers. #[derive(Debug)] pub enum GpuReadbackError { @@ -222,8 +186,6 @@ pub enum ImageRendererError { Content(RenderContentError), /// Caller target-related failure. Target(ImageTargetError), - /// Unsupported-feature failure. - Unsupported(RenderUnsupportedError), /// GPU readback failure. Readback(GpuReadbackError), /// Backend-specific rendering error. @@ -243,7 +205,6 @@ impl core::fmt::Display for ImageRendererError { match self { Self::Content(error) => core::fmt::Display::fmt(error, f), Self::Target(error) => core::fmt::Display::fmt(error, f), - Self::Unsupported(error) => core::fmt::Display::fmt(error, f), Self::Readback(error) => core::fmt::Display::fmt(error, f), Self::Backend(error) => core::fmt::Display::fmt(error, f), } @@ -255,7 +216,6 @@ impl core::error::Error for ImageRendererError { match self { Self::Content(error) => Some(error), Self::Target(error) => Some(error), - Self::Unsupported(error) => Some(error), Self::Readback(error) => Some(error), Self::Backend(error) => Some(error.as_ref()), } diff --git a/imaging_skia/src/lib.rs b/imaging_skia/src/lib.rs index 5ed75ac..08e8713 100644 --- a/imaging_skia/src/lib.rs +++ b/imaging_skia/src/lib.rs @@ -172,7 +172,7 @@ use imaging::{ record::{Scene, ValidateError, replay}, render::{ ImageBufferFormat, ImageBufferTarget, ImageRenderer, ImageRendererError, ImageTargetError, - RenderContentError, RenderSource, RenderUnsupportedError, + RenderContentError, RenderSource, }, }; use kurbo::{Affine, Shape as _}; @@ -219,12 +219,6 @@ pub enum Error { UnsupportedGpuTextureFormat, /// The target image buffer format cannot be represented through Skia. UnsupportedImageTargetFormat, - /// An image brush was encountered; this backend does not support it. - UnsupportedImageBrush, - /// A filter configuration could not be translated. - UnsupportedFilter, - /// A glyph run used a per-glyph transform unsupported by this backend. - UnsupportedGlyphTransform, /// Font bytes could not be loaded by Skia. InvalidFontData, /// A glyph identifier could not be represented by Skia's glyph type. @@ -1156,13 +1150,6 @@ fn map_image_renderer_error(error: Error) -> ImageRendererError { Error::InvalidScene(error) => { ImageRendererError::Content(RenderContentError::InvalidScene(error)) } - Error::UnsupportedImageBrush => { - ImageRendererError::Unsupported(RenderUnsupportedError::ImageBrush) - } - Error::UnsupportedFilter => ImageRendererError::Unsupported(RenderUnsupportedError::Filter), - Error::UnsupportedGlyphTransform => { - ImageRendererError::Unsupported(RenderUnsupportedError::Glyph) - } Error::InvalidFontData => ImageRendererError::Content(RenderContentError::InvalidFontData), Error::InvalidGlyphId => ImageRendererError::Content(RenderContentError::InvalidGlyphId), #[cfg(feature = "gpu")] @@ -1207,15 +1194,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::UnsupportedGlyphTransform => { - TextureRendererError::Unsupported(RenderUnsupportedError::Glyph) - } Error::Internal("render width too large" | "render height too large") => { TextureRendererError::Target(TextureTargetError::DimensionsTooLarge) } diff --git a/imaging_skia/src/sinks.rs b/imaging_skia/src/sinks.rs index 50ceb34..ba90d5d 100644 --- a/imaging_skia/src/sinks.rs +++ b/imaging_skia/src/sinks.rs @@ -258,9 +258,6 @@ fn push_group_impl(canvas: &sk::Canvas, state: &mut StreamState, group: GroupRef } else { build_filter_chain(group.filters) }; - if !group.filters.is_empty() && filter.is_none() { - state.set_error_once(Error::UnsupportedFilter); - } let clip_path = group.clip.and_then(|clip| clip_path(canvas, state, clip)); let mut restores = 0_u8; @@ -566,13 +563,10 @@ fn draw_masked_group( paint.set_anti_alias(true); paint.set_blend_mode(map_blend_mode(&masked.composite.blend)); paint.set_alpha_f(masked.composite.alpha); - if !masked.filters.is_empty() { - if let Some(filter) = build_filter_chain(&masked.filters) { - paint.set_image_filter(filter); - } else { - state.set_error_once(Error::UnsupportedFilter); - return; - } + if !masked.filters.is_empty() + && let Some(filter) = build_filter_chain(&masked.filters) + { + paint.set_image_filter(filter); } let clip_path = masked diff --git a/imaging_vello/src/lib.rs b/imaging_vello/src/lib.rs index e789785..1e50aa9 100644 --- a/imaging_vello/src/lib.rs +++ b/imaging_vello/src/lib.rs @@ -123,7 +123,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, }; use imaging_wgpu::{TextureRenderer, TextureRendererError, TextureTargetError, TextureViewTarget}; use kurbo::Rect; @@ -146,16 +146,6 @@ pub use scene_sink::VelloSceneSink; pub enum Error { /// The scene is invalid (unbalanced stacks). InvalidScene(ValidateError), - /// An image brush was encountered; this backend does not support it. - UnsupportedImageBrush, - /// A filter configuration could not be translated. - UnsupportedFilter, - /// A mask mode or masking primitive is not supported by this backend. - UnsupportedMask, - /// Glyph draws with non-default blend modes are not supported by this backend yet. - UnsupportedGlyphBlend, - /// Blurred rounded rect draws with non-default blend modes are not supported by this backend yet. - UnsupportedBlurredRoundedRectBlend, /// The clip/group stack was not well-nested for this backend. /// /// Vello uses a single layer stack for both clipping and blending; `imaging` tracks these as @@ -472,22 +462,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::UnsupportedGlyphBlend => { - TextureRendererError::Unsupported(RenderUnsupportedError::Glyph) - } - Error::UnsupportedBlurredRoundedRectBlend => { - TextureRendererError::Unsupported(RenderUnsupportedError::BlurredRoundedRect) - } - Error::UnbalancedLayerStack => { - TextureRendererError::Unsupported(RenderUnsupportedError::UnbalancedLayerStack) - } Error::Internal("render width too large" | "render height too large") => { TextureRendererError::Target(TextureTargetError::DimensionsTooLarge) } @@ -522,7 +496,6 @@ fn map_texture_to_image_error(error: TextureRendererError) -> ImageRendererError )) } }, - TextureRendererError::Unsupported(error) => ImageRendererError::Unsupported(error), TextureRendererError::Backend(error) => ImageRendererError::Backend(error), } } diff --git a/imaging_vello/src/scene_sink.rs b/imaging_vello/src/scene_sink.rs index 91c5285..0bd84f3 100644 --- a/imaging_vello/src/scene_sink.rs +++ b/imaging_vello/src/scene_sink.rs @@ -4,8 +4,8 @@ use super::Error; use crate::vello::{self, Glyph as VelloGlyph}; use imaging::{ - BlurredRoundedRect, ClipRef, Composite, FillRef, GeometryRef, GlyphRunRef, GroupRef, MaskMode, - PaintSink, StrokeRef, + BlurredRoundedRect, ClipRef, Composite, FillRef, GeometryRef, GlyphRunRef, GroupRef, PaintSink, + StrokeRef, record::{Scene, replay_transformed}, }; use kurbo::{Affine, Rect}; @@ -27,7 +27,6 @@ pub struct VelloSceneSink<'a> { #[derive(Clone, Debug)] struct PendingMask { scene: Scene, - mode: MaskMode, transform: Affine, } @@ -120,15 +119,22 @@ impl<'a> VelloSceneSink<'a> { glyph_run: GlyphRunRef<'_>, glyphs: &mut dyn Iterator, ) { - if glyph_run.composite.blend != peniko::BlendMode::default() { - self.set_error_once(Error::UnsupportedGlyphBlend); - return; - } - let Some(paint) = self.brush_to_brush(glyph_run.brush, glyph_run.composite) else { return; }; + let blended = glyph_run.composite.blend != peniko::BlendMode::default(); + if blended { + self.scene.push_layer( + Fill::NonZero, + glyph_run.composite.blend, + 1.0, + Affine::IDENTITY, + &self.surface_clip, + ); + self.push_group_frame(None); + } + let builder = self .scene .draw_glyphs(glyph_run.font) @@ -145,12 +151,26 @@ impl<'a> VelloSceneSink<'a> { y: glyph.y, }); builder.draw(glyph_run.style, glyphs); + + if blended { + if self.pop_group_frame().is_none() { + return; + } + self.scene.pop_layer(); + } } fn draw_blurred_rounded_rect(&mut self, draw: BlurredRoundedRect) { - if draw.composite.blend != peniko::BlendMode::default() { - self.set_error_once(Error::UnsupportedBlurredRoundedRectBlend); - return; + let blended = draw.composite.blend != peniko::BlendMode::default(); + if blended { + self.scene.push_layer( + Fill::NonZero, + draw.composite.blend, + 1.0, + draw.transform, + &draw.rect, + ); + self.push_group_frame(None); } self.scene.draw_blurred_rounded_rect( draw.transform, @@ -159,6 +179,12 @@ impl<'a> VelloSceneSink<'a> { draw.radius, draw.std_dev, ); + if blended { + if self.pop_group_frame().is_none() { + return; + } + self.scene.pop_layer(); + } } fn replay_masked_subscene(&mut self, scene: &Scene, transform: Affine) { @@ -213,18 +239,6 @@ impl PaintSink for VelloSceneSink<'_> { if self.error.is_some() { return; } - if !group.filters.is_empty() { - self.set_error_once(Error::UnsupportedFilter); - return; - } - if group - .mask - .as_ref() - .is_some_and(|mask| mask.mask.mode != MaskMode::Luminance) - { - self.set_error_once(Error::UnsupportedMask); - return; - } if let Some(clip) = group.clip { match clip { @@ -308,7 +322,6 @@ impl PaintSink for VelloSceneSink<'_> { } self.push_group_frame(group.mask.map(|mask| PendingMask { scene: mask.mask.scene.clone(), - mode: mask.mask.mode, transform: mask.transform, })); } @@ -321,11 +334,6 @@ impl PaintSink for VelloSceneSink<'_> { return; }; if let Some(mask) = mask { - debug_assert_eq!( - mask.mode, - MaskMode::Luminance, - "only luminance masks should reach Vello group-mask replay" - ); self.scene.push_luminance_mask_layer( Fill::NonZero, 1.0, @@ -539,11 +547,12 @@ mod tests { } #[test] - fn vello_scene_sink_rejects_filters() { + fn vello_scene_sink_ignores_filters() { let mut scene = vello::Scene::new(); let mut sink = VelloSceneSink::new(&mut scene, Rect::new(0.0, 0.0, 32.0, 32.0)); 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] @@ -560,13 +569,15 @@ mod tests { } #[test] - fn vello_scene_sink_rejects_alpha_masks() { + fn vello_scene_sink_falls_back_for_alpha_masks() { let mut mask = Scene::new(); mask.fill(FillRef::new(Rect::new(0.0, 0.0, 16.0, 16.0), Color::WHITE)); let mut scene = vello::Scene::new(); let mut sink = VelloSceneSink::new(&mut scene, Rect::new(0.0, 0.0, 32.0, 32.0)); sink.push_group(GroupRef::new().with_mask(MaskRef::new(MaskMode::Alpha, &mask))); - assert!(matches!(sink.finish(), Err(Error::UnsupportedMask))); + sink.fill(FillRef::new(Rect::new(4.0, 4.0, 20.0, 20.0), Color::BLACK)); + sink.pop_group(); + assert!(matches!(sink.finish(), Ok(()))); } } diff --git a/imaging_vello_cpu/src/lib.rs b/imaging_vello_cpu/src/lib.rs index 7effaeb..cac65ad 100644 --- a/imaging_vello_cpu/src/lib.rs +++ b/imaging_vello_cpu/src/lib.rs @@ -73,7 +73,7 @@ use imaging::{ record::{Scene, ValidateError, replay, replay_transformed}, render::{ ImageBufferFormat, ImageBufferTarget, ImageRenderer, ImageRendererError, ImageTargetError, - RenderContentError, RenderSource, RenderUnsupportedError, + RenderContentError, RenderSource, }, }; use kurbo::{Affine, Rect, Shape as _}; @@ -89,10 +89,6 @@ use vello_cpu::{Pixmap, RenderContext, RenderMode, RenderSettings}; pub enum Error { /// The scene is invalid (unbalanced stacks). InvalidScene(ValidateError), - /// An image brush was encountered; this backend does not support it. - UnsupportedImageBrush, - /// A filter configuration could not be translated. - UnsupportedFilter, /// An internal invariant was violated. Internal(&'static str), } @@ -391,9 +387,6 @@ impl VelloCpuRenderer { } if let Some(out) = last { graph.set_output(out); - } else { - self.set_error_once(Error::UnsupportedFilter); - return None; } Some(VelloFilter { graph: Arc::new(graph), @@ -571,10 +564,6 @@ fn map_image_renderer_error(error: Error) -> ImageRendererError { Error::InvalidScene(error) => { ImageRendererError::Content(RenderContentError::InvalidScene(error)) } - Error::UnsupportedImageBrush => { - ImageRendererError::Unsupported(RenderUnsupportedError::ImageBrush) - } - Error::UnsupportedFilter => ImageRendererError::Unsupported(RenderUnsupportedError::Filter), Error::Internal("image target dimensions do not match renderer output") => { ImageRendererError::Target(ImageTargetError::InvalidTarget( "image target dimensions do not match renderer output", diff --git a/imaging_vello_hybrid/README.md b/imaging_vello_hybrid/README.md index c9769a8..bfc8bc3 100644 --- a/imaging_vello_hybrid/README.md +++ b/imaging_vello_hybrid/README.md @@ -15,8 +15,8 @@ This backend supports both headless image rendering and host-owned `wgpu` textur limited to non-image brushes. - Use `VelloHybridSceneSink::new` for solid/gradient-only native scene recording. - Use `VelloHybridSceneSink::with_renderer` for native scene recording that needs image brushes. -- Group-level filters are currently not supported by `vello_hybrid`; `imaging_vello_hybrid` - returns `Error::UnsupportedFilter` if a scene uses them. +- Group-level filters and masks currently degrade to best-effort rendering on the native-scene + sink paths instead of aborting the frame. - Workaround for vello#1408: `Compose::Copy` with a fully transparent solid paint is mapped to `Compose::Clear` to avoid a vello_hybrid optimization that skips generating strips for invisible paints. diff --git a/imaging_vello_hybrid/src/lib.rs b/imaging_vello_hybrid/src/lib.rs index bb5b90d..daf5a9d 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}; @@ -185,14 +185,6 @@ pub use scene_sink::VelloHybridSceneSink; pub enum Error { /// The scene is invalid (unbalanced stacks). 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 +562,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) } @@ -614,7 +596,6 @@ fn map_texture_to_image_error(error: TextureRendererError) -> ImageRendererError )) } }, - TextureRendererError::Unsupported(error) => ImageRendererError::Unsupported(error), TextureRendererError::Backend(error) => ImageRendererError::Backend(error), } } diff --git a/imaging_vello_hybrid/src/scene_sink.rs b/imaging_vello_hybrid/src/scene_sink.rs index 04998d7..d463ef9 100644 --- a/imaging_vello_hybrid/src/scene_sink.rs +++ b/imaging_vello_hybrid/src/scene_sink.rs @@ -8,7 +8,7 @@ use imaging::{ StrokeRef, }; use kurbo::{Affine, Shape as _}; -use peniko::{Brush, BrushRef, ImageBrush, Style}; +use peniko::{Brush, BrushRef, Color, ImageAlphaType, ImageBrush, ImageFormat, Style}; use vello_common::glyph::Glyph as VelloGlyph; /// Borrowed adapter that streams `imaging` commands into an existing [`vello_hybrid::Scene`]. @@ -110,17 +110,8 @@ impl<'a> VelloHybridSceneSink<'a> { } fn resolve_image_brush(&mut self, image: &ImageBrush) -> Option { - let Some(image_upload) = self.image_upload.as_mut() else { - self.set_error_once(Error::UnsupportedImageBrush); - return None; - }; - match image_upload.resolve_image_brush(image) { - Ok(image) => Some(image), - Err(err) => { - self.set_error_once(err); - None - } - } + let image_upload = self.image_upload.as_mut()?; + image_upload.resolve_image_brush(image).ok() } fn geometry_to_path(&self, geom: GeometryRef<'_>) -> kurbo::BezPath { @@ -213,7 +204,25 @@ impl<'a> VelloHybridSceneSink<'a> { } fn draw_blurred_rounded_rect(&mut self, _draw: BlurredRoundedRect) { - self.set_error_once(Error::UnsupportedBlurredRoundedRect); + let draw = _draw; + self.scene.set_transform(draw.transform); + self.scene.set_blend_mode(draw.composite.blend); + self.scene.set_paint(Brush::Solid( + draw.color.multiply_alpha(draw.composite.alpha), + )); + self.scene.fill_rect(&draw.rect); + } + + fn fallback_brush( + &self, + brush: BrushRef<'_>, + composite: Composite, + ) -> vello_common::paint::PaintType { + match brush.to_owned().multiply_alpha(composite.alpha) { + Brush::Solid(color) => Brush::Solid(color), + Brush::Gradient(gradient) => Brush::Gradient(gradient), + Brush::Image(image) => Brush::Solid(average_image_color(&image)), + } } } @@ -245,14 +254,6 @@ impl PaintSink for VelloHybridSceneSink<'_> { if self.error.is_some() { return; } - if group.mask.is_some() { - 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); @@ -284,9 +285,9 @@ impl PaintSink for VelloHybridSceneSink<'_> { return; } - let Some(paint) = self.brush_to_paint(draw.brush, draw.composite) else { - return; - }; + let paint = self + .brush_to_paint(draw.brush, draw.composite) + .unwrap_or_else(|| self.fallback_brush(draw.brush, draw.composite)); self.scene.set_transform(draw.transform); self.scene.set_fill_rule(draw.fill_rule); self.scene @@ -295,7 +296,7 @@ impl PaintSink for VelloHybridSceneSink<'_> { let (blend, paint) = match (&paint, draw.composite.blend.compose) { (Brush::Solid(c), peniko::Compose::Copy) if c.components[3] == 0.0 => ( peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::Clear), - Brush::Solid(peniko::Color::from_rgba8(0, 0, 0, 255)), + Brush::Solid(Color::from_rgba8(0, 0, 0, 255)), ), _ => (draw.composite.blend, paint), }; @@ -319,9 +320,9 @@ impl PaintSink for VelloHybridSceneSink<'_> { return; } - let Some(paint) = self.brush_to_paint(draw.brush, draw.composite) else { - return; - }; + let paint = self + .brush_to_paint(draw.brush, draw.composite) + .unwrap_or_else(|| self.fallback_brush(draw.brush, draw.composite)); self.scene.set_transform(draw.transform); self.scene.set_stroke(draw.stroke.clone()); self.scene @@ -330,7 +331,7 @@ impl PaintSink for VelloHybridSceneSink<'_> { let (blend, paint) = match (&paint, draw.composite.blend.compose) { (Brush::Solid(c), peniko::Compose::Copy) if c.components[3] == 0.0 => ( peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::Clear), - Brush::Solid(peniko::Color::from_rgba8(0, 0, 0, 255)), + Brush::Solid(Color::from_rgba8(0, 0, 0, 255)), ), _ => (draw.composite.blend, paint), }; @@ -368,6 +369,53 @@ impl PaintSink for VelloHybridSceneSink<'_> { } } +fn average_image_color(image: &ImageBrush) -> Color { + let bytes = image.image.data.as_ref(); + let mut r_sum = 0_u64; + let mut g_sum = 0_u64; + let mut b_sum = 0_u64; + let mut a_sum = 0_u64; + let mut count = 0_u64; + + for px in bytes.chunks_exact(4) { + let (r, g, b, a) = match image.image.format { + ImageFormat::Rgba8 => (px[0], px[1], px[2], px[3]), + ImageFormat::Bgra8 => (px[2], px[1], px[0], px[3]), + _ => return Color::TRANSPARENT, + }; + + let (r, g, b) = match image.image.alpha_type { + ImageAlphaType::Alpha => (u32::from(r), u32::from(g), u32::from(b)), + ImageAlphaType::AlphaPremultiplied if a == 0 => (0, 0, 0), + ImageAlphaType::AlphaPremultiplied => { + let alpha = u32::from(a); + ( + (u32::from(r) * 255 + alpha / 2) / alpha, + (u32::from(g) * 255 + alpha / 2) / alpha, + (u32::from(b) * 255 + alpha / 2) / alpha, + ) + } + }; + + r_sum += u64::from(r); + g_sum += u64::from(g); + b_sum += u64::from(b); + a_sum += u64::from(a); + count += 1; + } + + if count == 0 { + return Color::TRANSPARENT; + } + + Color::from_rgba8( + u8::try_from(r_sum / count).expect("average red stays within u8"), + u8::try_from(g_sum / count).expect("average green stays within u8"), + u8::try_from(b_sum / count).expect("average blue stays within u8"), + u8::try_from(a_sum / count).expect("average alpha stays within u8"), + ) +} + #[cfg(test)] mod tests { use super::*; @@ -388,16 +436,17 @@ mod tests { } #[test] - fn hybrid_scene_sink_rejects_filters() { + fn hybrid_scene_sink_ignores_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] - fn hybrid_scene_sink_rejects_image_brushes_without_resolver() { + fn hybrid_scene_sink_falls_back_for_image_brushes_without_resolver() { let mut scene = vello_hybrid::Scene::new(32, 32); scene.reset(); let mut sink = VelloHybridSceneSink::new(&mut scene); @@ -409,20 +458,20 @@ mod tests { height: 2, })); sink.fill(FillRef::new(kurbo::Rect::new(0.0, 0.0, 8.0, 8.0), &image)); - assert!(matches!(sink.finish(), Err(Error::UnsupportedImageBrush))); + assert!(matches!(sink.finish(), Ok(()))); } #[test] - fn hybrid_scene_sink_rejects_masks() { + fn hybrid_scene_sink_ignores_masks() { let mut mask = record::Scene::new(); mask.fill(FillRef::new( kurbo::Rect::new(0.0, 0.0, 8.0, 8.0), - peniko::Color::WHITE, + Color::WHITE, )); let mut content = record::Scene::new(); content.fill(FillRef::new( kurbo::Rect::new(1.0, 1.0, 7.0, 7.0), - peniko::Color::BLACK, + Color::BLACK, )); let mut scene = vello_hybrid::Scene::new(32, 32); @@ -431,9 +480,9 @@ mod tests { sink.push_group(GroupRef::new().with_mask(MaskRef::new(MaskMode::Luminance, &mask))); sink.fill(FillRef::new( kurbo::Rect::new(1.0, 1.0, 7.0, 7.0), - peniko::Color::BLACK, + Color::BLACK, )); sink.pop_group(); - assert!(matches!(sink.finish(), Err(Error::UnsupportedMask))); + assert!(matches!(sink.finish(), Ok(()))); } } diff --git a/imaging_wgpu/src/lib.rs b/imaging_wgpu/src/lib.rs index 4af3cbf..de2298e 100644 --- a/imaging_wgpu/src/lib.rs +++ b/imaging_wgpu/src/lib.rs @@ -98,8 +98,6 @@ pub enum TextureRendererError { Content(imaging::render::RenderContentError), /// Caller target-related failure. Target(TextureTargetError), - /// Unsupported-feature failure. - Unsupported(imaging::render::RenderUnsupportedError), /// Backend-specific rendering error. Backend(Box), } @@ -117,7 +115,6 @@ impl core::fmt::Display for TextureRendererError { match self { Self::Content(error) => core::fmt::Display::fmt(error, f), Self::Target(error) => core::fmt::Display::fmt(error, f), - Self::Unsupported(error) => core::fmt::Display::fmt(error, f), Self::Backend(error) => core::fmt::Display::fmt(error, f), } } @@ -128,7 +125,6 @@ impl core::error::Error for TextureRendererError { match self { Self::Content(error) => Some(error), Self::Target(error) => Some(error), - Self::Unsupported(error) => Some(error), Self::Backend(error) => Some(error.as_ref()), } }