Skip to main content

agg_gui/
svg.rs

1//! SVG rendering support for `agg-gui`.
2//!
3//! This module is the library-owned SVG renderer used by tests, demos, and
4//! applications.  It parses SVG with `usvg`, then emits drawing commands only
5//! through [`crate::draw_ctx::DrawCtx`] so RGBA software, LCD coverage, and
6//! hardware targets all share one render path.
7
8use std::fmt;
9use std::path::{Path, PathBuf};
10use std::sync::{Arc, OnceLock, RwLock};
11
12use agg_rust::math_stroke::{LineCap, LineJoin};
13use agg_rust::trans_affine::TransAffine;
14use usvg::tiny_skia_path::PathSegment;
15
16use crate::draw_ctx::{DrawCtx, FillRule};
17use crate::framebuffer::{unpremultiply_rgba_inplace, Framebuffer};
18use crate::gfx_ctx::GfxCtx;
19use crate::lcd_coverage::LcdBuffer;
20use crate::lcd_gfx_ctx::LcdGfxCtx;
21
22pub use compare::{
23    compare_svg_rgba, SvgCompareResult, SvgCompareThresholds, DEFAULT_ALPHA_TOLERANCE,
24    DEFAULT_MISMATCH_RATIO, DEFAULT_OPAQUE_RGB_TOLERANCE, DEFAULT_TRANSLUCENT_RGB_TOLERANCE,
25    DEFAULT_VISUAL_RGB_TOLERANCE,
26};
27
28#[derive(Clone, Copy, Debug)]
29struct SvgRenderState {
30    opacity: f32,
31    layer_width: f64,
32    layer_height: f64,
33}
34
35impl Default for SvgRenderState {
36    fn default() -> Self {
37        Self {
38            opacity: 1.0,
39            layer_width: 1.0,
40            layer_height: 1.0,
41        }
42    }
43}
44
45/// Errors returned by the SVG renderer.
46#[derive(Debug)]
47pub enum SvgRenderError {
48    /// The SVG data could not be parsed by `usvg`.
49    Parse(usvg::Error),
50    /// A raster image referenced by the SVG could not be decoded.
51    DecodeImage(image::ImageError),
52}
53
54impl fmt::Display for SvgRenderError {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self {
57            SvgRenderError::Parse(err) => write!(f, "failed to parse SVG: {err}"),
58            SvgRenderError::DecodeImage(err) => write!(f, "failed to decode SVG image: {err}"),
59        }
60    }
61}
62
63impl std::error::Error for SvgRenderError {}
64
65impl From<usvg::Error> for SvgRenderError {
66    fn from(err: usvg::Error) -> Self {
67        SvgRenderError::Parse(err)
68    }
69}
70
71impl From<image::ImageError> for SvgRenderError {
72    fn from(err: image::ImageError) -> Self {
73        SvgRenderError::DecodeImage(err)
74    }
75}
76
77/// Options used while parsing SVG documents.
78///
79/// `agg-gui` keeps SVG rendering in the core library, but font selection is
80/// intentionally application-owned. Callers that need SVG text should provide a
81/// `fontdb` built from their own assets.
82#[derive(Clone, Default)]
83pub struct SvgParseOptions {
84    resources_dir: Option<PathBuf>,
85    font_family: Option<String>,
86    fontdb: Option<Arc<usvg::fontdb::Database>>,
87}
88
89impl SvgParseOptions {
90    pub fn new() -> Self {
91        Self::default()
92    }
93
94    /// Resolve relative image references from `resources_dir`.
95    pub fn with_resources_dir(mut self, resources_dir: impl Into<PathBuf>) -> Self {
96        self.resources_dir = Some(resources_dir.into());
97        self
98    }
99
100    /// Set the preferred SVG text family for documents that omit one.
101    pub fn with_font_family(mut self, family: impl Into<String>) -> Self {
102        self.font_family = Some(family.into());
103        self
104    }
105
106    /// Provide a prepared font database for SVG text parsing.
107    pub fn with_fontdb(mut self, fontdb: Arc<usvg::fontdb::Database>) -> Self {
108        self.fontdb = Some(fontdb);
109        self
110    }
111}
112
113static DEFAULT_SVG_PARSE_OPTIONS: OnceLock<RwLock<SvgParseOptions>> = OnceLock::new();
114
115fn default_svg_parse_options_cell() -> &'static RwLock<SvgParseOptions> {
116    DEFAULT_SVG_PARSE_OPTIONS.get_or_init(|| RwLock::new(system_svg_parse_options()))
117}
118
119fn system_svg_parse_options() -> SvgParseOptions {
120    let mut fontdb = usvg::fontdb::Database::new();
121    fontdb.load_system_fonts();
122    font_defaults::configure_generic_font_families(&mut fontdb, None);
123    SvgParseOptions::new().with_fontdb(Arc::new(fontdb))
124}
125
126/// Replace the default SVG parse options used by convenience render helpers.
127///
128/// This keeps SVG rendering in the core library while letting applications own
129/// the font database used for SVG text.
130pub fn set_default_svg_parse_options(options: SvgParseOptions) {
131    *default_svg_parse_options_cell()
132        .write()
133        .expect("default SVG parse options lock poisoned") = options;
134}
135
136/// Build a `usvg` font database from caller-owned font bytes.
137pub fn svg_fontdb_from_font_data<I>(
138    fonts: I,
139    generic_family: Option<&str>,
140) -> Arc<usvg::fontdb::Database>
141where
142    I: IntoIterator<Item = Vec<u8>>,
143{
144    let mut fontdb = usvg::fontdb::Database::new();
145    for bytes in fonts {
146        fontdb.load_font_data(bytes);
147    }
148    font_defaults::configure_generic_font_families(&mut fontdb, generic_family);
149    Arc::new(fontdb)
150}
151
152fn parse_svg_tree(data: &[u8], resources_dir: Option<&Path>) -> Result<usvg::Tree, SvgRenderError> {
153    let mut options = default_svg_parse_options_cell()
154        .read()
155        .expect("default SVG parse options lock poisoned")
156        .clone();
157    if let Some(dir) = resources_dir {
158        options = options.with_resources_dir(dir);
159    }
160    parse_svg(data, &options)
161}
162
163/// Parse an SVG document using caller-supplied parse options.
164pub fn parse_svg(data: &[u8], svg_options: &SvgParseOptions) -> Result<usvg::Tree, SvgRenderError> {
165    let mut options = usvg::Options::default();
166    options.resources_dir = svg_options.resources_dir.clone();
167    if let Some(font_family) = &svg_options.font_family {
168        options.font_family = font_family.clone();
169    }
170    if let Some(fontdb) = &svg_options.fontdb {
171        options.fontdb = Arc::clone(fontdb);
172    }
173    Ok(usvg::Tree::from_data(data, &options)?)
174}
175
176/// Parse an SVG document and render it into `ctx`.
177///
178/// This is a convenience wrapper around [`render_svg_tree`].  Callers that
179/// already cache a `usvg::Tree` should use [`render_svg_tree`] directly.
180pub fn render_svg(data: &[u8], ctx: &mut dyn DrawCtx) -> Result<(), SvgRenderError> {
181    let tree = parse_svg_tree(data, None)?;
182    render_svg_tree(&tree, ctx)
183}
184
185/// Parse an SVG document with explicit options and render it into `ctx`.
186pub fn render_svg_with_options(
187    data: &[u8],
188    ctx: &mut dyn DrawCtx,
189    options: &SvgParseOptions,
190) -> Result<(), SvgRenderError> {
191    let tree = parse_svg(data, options)?;
192    render_svg_tree(&tree, ctx)
193}
194
195/// Parse an SVG document and render it into `ctx` using an explicit output
196/// pixel size for the document viewport.
197pub fn render_svg_at_size(
198    data: &[u8],
199    ctx: &mut dyn DrawCtx,
200    width: u32,
201    height: u32,
202) -> Result<(), SvgRenderError> {
203    let tree = parse_svg_tree(data, None)?;
204    render_svg_tree_at_size(&tree, ctx, width, height)
205}
206
207pub fn render_svg_at_size_with_options(
208    data: &[u8],
209    ctx: &mut dyn DrawCtx,
210    width: u32,
211    height: u32,
212    options: &SvgParseOptions,
213) -> Result<(), SvgRenderError> {
214    let tree = parse_svg(data, options)?;
215    render_svg_tree_at_size(&tree, ctx, width, height)
216}
217
218pub fn render_svg_at_size_with_resources(
219    data: &[u8],
220    ctx: &mut dyn DrawCtx,
221    width: u32,
222    height: u32,
223    resources_dir: &Path,
224) -> Result<(), SvgRenderError> {
225    let tree = parse_svg_tree(data, Some(resources_dir))?;
226    render_svg_tree_at_size(&tree, ctx, width, height)
227}
228
229/// Parse an SVG document and render it into a newly allocated RGBA framebuffer.
230///
231/// This is the library API the SVG regression tests and demo viewer should use
232/// for the `agg-rgba-bitmap render` column.
233pub fn render_svg_to_framebuffer(data: &[u8]) -> Result<Framebuffer, SvgRenderError> {
234    let tree = parse_svg_tree(data, None)?;
235    render_svg_tree_to_framebuffer(&tree)
236}
237
238pub fn render_svg_to_framebuffer_with_options(
239    data: &[u8],
240    options: &SvgParseOptions,
241) -> Result<Framebuffer, SvgRenderError> {
242    let tree = parse_svg(data, options)?;
243    render_svg_tree_to_framebuffer(&tree)
244}
245
246/// Parse an SVG document and render it into an RGBA framebuffer with an
247/// explicit pixel size.
248///
249/// The resvg test suite reference PNGs are not always the SVG document's
250/// intrinsic size, so regression tests and viewers should use this helper when
251/// they need render output to match a reference image one-to-one.
252pub fn render_svg_to_framebuffer_at_size(
253    data: &[u8],
254    width: u32,
255    height: u32,
256) -> Result<Framebuffer, SvgRenderError> {
257    let tree = parse_svg_tree(data, None)?;
258    render_svg_tree_to_framebuffer_at_size(&tree, width, height)
259}
260
261pub fn render_svg_to_framebuffer_at_size_with_options(
262    data: &[u8],
263    width: u32,
264    height: u32,
265    options: &SvgParseOptions,
266) -> Result<Framebuffer, SvgRenderError> {
267    let tree = parse_svg(data, options)?;
268    render_svg_tree_to_framebuffer_at_size(&tree, width, height)
269}
270
271pub fn render_svg_to_framebuffer_at_size_with_resources(
272    data: &[u8],
273    width: u32,
274    height: u32,
275    resources_dir: &Path,
276) -> Result<Framebuffer, SvgRenderError> {
277    let tree = parse_svg_tree(data, Some(resources_dir))?;
278    render_svg_tree_to_framebuffer_at_size(&tree, width, height)
279}
280
281/// Render a parsed SVG tree into a newly allocated RGBA framebuffer.
282pub fn render_svg_tree_to_framebuffer(tree: &usvg::Tree) -> Result<Framebuffer, SvgRenderError> {
283    let width = tree.size().width().ceil().max(1.0) as u32;
284    let height = tree.size().height().ceil().max(1.0) as u32;
285    render_svg_tree_to_framebuffer_at_size(tree, width, height)
286}
287
288/// Render a parsed SVG tree into an RGBA framebuffer with an explicit pixel size.
289pub fn render_svg_tree_to_framebuffer_at_size(
290    tree: &usvg::Tree,
291    width: u32,
292    height: u32,
293) -> Result<Framebuffer, SvgRenderError> {
294    let width = width.max(1);
295    let height = height.max(1);
296    let mut fb = Framebuffer::new(width, height);
297    {
298        let mut ctx = GfxCtx::new(&mut fb);
299        render_svg_tree_at_size(tree, &mut ctx, width, height)?;
300    }
301    Ok(fb)
302}
303
304/// Parse an SVG document and render it into a newly allocated LCD coverage buffer.
305///
306/// This is the library API the SVG regression tests and demo viewer should use
307/// for the `agg-lcd-bitmap render` column.
308pub fn render_svg_to_lcd_buffer(data: &[u8]) -> Result<LcdBuffer, SvgRenderError> {
309    let tree = parse_svg_tree(data, None)?;
310    render_svg_tree_to_lcd_buffer(&tree)
311}
312
313pub fn render_svg_to_lcd_buffer_with_options(
314    data: &[u8],
315    options: &SvgParseOptions,
316) -> Result<LcdBuffer, SvgRenderError> {
317    let tree = parse_svg(data, options)?;
318    render_svg_tree_to_lcd_buffer(&tree)
319}
320
321/// Parse an SVG document and render it into an LCD coverage buffer with an
322/// explicit pixel size.
323pub fn render_svg_to_lcd_buffer_at_size(
324    data: &[u8],
325    width: u32,
326    height: u32,
327) -> Result<LcdBuffer, SvgRenderError> {
328    let tree = parse_svg_tree(data, None)?;
329    render_svg_tree_to_lcd_buffer_at_size(&tree, width, height)
330}
331
332pub fn render_svg_to_lcd_buffer_at_size_with_options(
333    data: &[u8],
334    width: u32,
335    height: u32,
336    options: &SvgParseOptions,
337) -> Result<LcdBuffer, SvgRenderError> {
338    let tree = parse_svg(data, options)?;
339    render_svg_tree_to_lcd_buffer_at_size(&tree, width, height)
340}
341
342pub fn render_svg_to_lcd_buffer_at_size_with_resources(
343    data: &[u8],
344    width: u32,
345    height: u32,
346    resources_dir: &Path,
347) -> Result<LcdBuffer, SvgRenderError> {
348    let tree = parse_svg_tree(data, Some(resources_dir))?;
349    render_svg_tree_to_lcd_buffer_at_size(&tree, width, height)
350}
351
352/// Render a parsed SVG tree into a newly allocated LCD coverage buffer.
353pub fn render_svg_tree_to_lcd_buffer(tree: &usvg::Tree) -> Result<LcdBuffer, SvgRenderError> {
354    let width = tree.size().width().ceil().max(1.0) as u32;
355    let height = tree.size().height().ceil().max(1.0) as u32;
356    render_svg_tree_to_lcd_buffer_at_size(tree, width, height)
357}
358
359/// Render a parsed SVG tree into an LCD coverage buffer with an explicit pixel size.
360pub fn render_svg_tree_to_lcd_buffer_at_size(
361    tree: &usvg::Tree,
362    width: u32,
363    height: u32,
364) -> Result<LcdBuffer, SvgRenderError> {
365    let width = width.max(1);
366    let height = height.max(1);
367    let mut buffer = LcdBuffer::new(width, height);
368    {
369        let mut ctx = LcdGfxCtx::new(&mut buffer);
370        render_svg_tree_at_size(tree, &mut ctx, width, height)?;
371    }
372    Ok(buffer)
373}
374
375/// Render a parsed `usvg::Tree` into `ctx`.
376///
377/// The tree's native SVG coordinate system is Y-down.  This function installs
378/// a root transform that maps it into `agg-gui`'s Y-up convention before any
379/// node commands are emitted.
380pub fn render_svg_tree(tree: &usvg::Tree, ctx: &mut dyn DrawCtx) -> Result<(), SvgRenderError> {
381    let width = tree.size().width().ceil().max(1.0) as u32;
382    let height = tree.size().height().ceil().max(1.0) as u32;
383    render_svg_tree_at_size(tree, ctx, width, height)
384}
385
386/// Render a parsed `usvg::Tree` into `ctx`, fitting its document viewport into
387/// an explicit output pixel size.
388pub fn render_svg_tree_at_size(
389    tree: &usvg::Tree,
390    ctx: &mut dyn DrawCtx,
391    width: u32,
392    height: u32,
393) -> Result<(), SvgRenderError> {
394    let saved_transform = ctx.transform();
395    let mut svg_to_ctx = saved_transform;
396    svg_to_ctx.premultiply(&svg_y_down_to_ctx_y_up(tree, width, height));
397
398    ctx.save();
399    ctx.set_transform(svg_to_ctx);
400    render_group(
401        tree.root(),
402        ctx,
403        SvgRenderState {
404            layer_width: width.max(1) as f64,
405            layer_height: height.max(1) as f64,
406            ..SvgRenderState::default()
407        },
408    )?;
409    ctx.restore();
410    Ok(())
411}
412
413fn svg_y_down_to_ctx_y_up(tree: &usvg::Tree, width: u32, height: u32) -> TransAffine {
414    let sx = width.max(1) as f64 / tree.size().width().max(1.0) as f64;
415    let sy = height.max(1) as f64 / tree.size().height().max(1.0) as f64;
416    TransAffine::new_custom(sx, 0.0, 0.0, -sy, 0.0, height.max(1) as f64)
417}
418
419fn render_group(
420    group: &usvg::Group,
421    ctx: &mut dyn DrawCtx,
422    parent_state: SvgRenderState,
423) -> Result<(), SvgRenderError> {
424    let group_opacity = group.opacity().get();
425    if group_opacity < 1.0 && parent_state.opacity > 0.0 && ctx.supports_compositing_layers() {
426        return render_isolated_group_with_opacity(group, ctx, parent_state, group_opacity);
427    }
428
429    let state = SvgRenderState {
430        opacity: parent_state.opacity * group_opacity,
431        ..parent_state
432    };
433
434    ctx.save();
435    apply_group_clip(ctx, group);
436    for node in group.children() {
437        match node {
438            usvg::Node::Group(group) => render_group(group, ctx, state)?,
439            usvg::Node::Path(path) => render_path(path, ctx, state),
440            usvg::Node::Image(image) => render_image(image, ctx, state)?,
441            usvg::Node::Text(text) => render_text(text, ctx, state)?,
442        }
443    }
444    ctx.restore();
445    Ok(())
446}
447
448fn render_isolated_group_with_opacity(
449    group: &usvg::Group,
450    ctx: &mut dyn DrawCtx,
451    parent_state: SvgRenderState,
452    group_opacity: f32,
453) -> Result<(), SvgRenderError> {
454    let saved_transform = ctx.transform();
455
456    ctx.save();
457    ctx.reset_transform();
458    ctx.push_layer_with_alpha(
459        parent_state.layer_width,
460        parent_state.layer_height,
461        (parent_state.opacity * group_opacity) as f64,
462    );
463    ctx.set_transform(saved_transform);
464
465    let state = SvgRenderState {
466        opacity: 1.0,
467        ..parent_state
468    };
469    ctx.save();
470    apply_group_clip(ctx, group);
471    for node in group.children() {
472        match node {
473            usvg::Node::Group(group) => render_group(group, ctx, state)?,
474            usvg::Node::Path(path) => render_path(path, ctx, state),
475            usvg::Node::Image(image) => render_image(image, ctx, state)?,
476            usvg::Node::Text(text) => render_text(text, ctx, state)?,
477        }
478    }
479    ctx.restore();
480    ctx.pop_layer();
481    ctx.restore();
482    Ok(())
483}
484
485fn apply_group_clip(ctx: &mut dyn DrawCtx, group: &usvg::Group) {
486    if let Some(clip) = group.clip_path() {
487        apply_clip_path(ctx, clip);
488    }
489}
490
491fn apply_clip_path(ctx: &mut dyn DrawCtx, clip: &usvg::ClipPath) {
492    // The bridge currently exposes rectangular clipping only.  This still
493    // covers the common SVG badge pattern and provides a conservative fallback
494    // until arbitrary path masks are wired through the draw backends.
495    let bbox = clip.root().bounding_box();
496    ctx.clip_rect(
497        bbox.x() as f64,
498        bbox.y() as f64,
499        bbox.width() as f64,
500        bbox.height() as f64,
501    );
502
503    if let Some(clip) = clip.clip_path() {
504        apply_clip_path(ctx, clip);
505    }
506}
507
508fn render_text(
509    text: &usvg::Text,
510    ctx: &mut dyn DrawCtx,
511    state: SvgRenderState,
512) -> Result<(), SvgRenderError> {
513    if state.opacity <= 0.0 {
514        return Ok(());
515    }
516
517    ctx.save();
518    apply_transform(ctx, text.abs_transform());
519    let result = render_group(text.flattened(), ctx, state);
520    ctx.restore();
521    result
522}
523
524fn render_path(path: &usvg::Path, ctx: &mut dyn DrawCtx, state: SvgRenderState) {
525    if !path.is_visible() {
526        return;
527    }
528
529    ctx.save();
530    apply_transform(ctx, path.abs_transform());
531
532    match path.paint_order() {
533        usvg::PaintOrder::FillAndStroke => {
534            fill_path(path, ctx, state);
535            stroke_path(path, ctx, state);
536        }
537        usvg::PaintOrder::StrokeAndFill => {
538            stroke_path(path, ctx, state);
539            fill_path(path, ctx, state);
540        }
541    }
542
543    ctx.restore();
544}
545
546fn fill_path(path: &usvg::Path, ctx: &mut dyn DrawCtx, state: SvgRenderState) {
547    let Some(fill) = path.fill() else {
548        return;
549    };
550
551    emit_path(path, ctx);
552    if !apply_fill_paint(
553        ctx,
554        fill.paint(),
555        state.opacity * fill.opacity().get(),
556        Some(path.bounding_box()),
557    ) {
558        return;
559    }
560    ctx.set_fill_rule(map_fill_rule(fill.rule()));
561    ctx.fill();
562}
563
564fn stroke_path(path: &usvg::Path, ctx: &mut dyn DrawCtx, state: SvgRenderState) {
565    let Some(stroke) = path.stroke() else {
566        return;
567    };
568    if !apply_stroke_paint(
569        ctx,
570        stroke.paint(),
571        state.opacity * stroke.opacity().get(),
572        Some(path.stroke_bounding_box()),
573    ) {
574        return;
575    }
576
577    emit_path(path, ctx);
578    ctx.set_line_width(stroke.width().get() as f64);
579    ctx.set_line_cap(map_line_cap(stroke.linecap()));
580    ctx.set_line_join(map_line_join(stroke.linejoin()));
581    ctx.set_miter_limit(stroke.miterlimit().get() as f64);
582    let dashes: Vec<f64> = stroke
583        .dasharray()
584        .map(|items| items.iter().map(|v| *v as f64).collect())
585        .unwrap_or_default();
586    ctx.set_line_dash(&dashes, stroke.dashoffset() as f64);
587    ctx.stroke();
588}
589
590fn apply_fill_paint(
591    ctx: &mut dyn DrawCtx,
592    paint: &usvg::Paint,
593    opacity: f32,
594    object_bbox: Option<usvg::Rect>,
595) -> bool {
596    if let usvg::Paint::Pattern(pattern) = paint {
597        if !ctx.supports_fill_pattern() {
598            return false;
599        }
600        if let Some(pattern) = pattern::render_pattern_paint(pattern, opacity, object_bbox) {
601            ctx.set_fill_pattern(pattern);
602            return true;
603        }
604        return false;
605    }
606
607    paint::apply_fill_paint(ctx, paint, opacity)
608}
609
610fn apply_stroke_paint(
611    ctx: &mut dyn DrawCtx,
612    paint: &usvg::Paint,
613    opacity: f32,
614    object_bbox: Option<usvg::Rect>,
615) -> bool {
616    if let usvg::Paint::Pattern(pattern) = paint {
617        if !ctx.supports_stroke_pattern() {
618            return false;
619        }
620        if let Some(pattern) = pattern::render_pattern_paint(pattern, opacity, object_bbox) {
621            ctx.set_stroke_pattern(pattern);
622            return true;
623        }
624        return false;
625    }
626
627    paint::apply_stroke_paint(ctx, paint, opacity)
628}
629
630fn render_image(
631    image: &usvg::Image,
632    ctx: &mut dyn DrawCtx,
633    state: SvgRenderState,
634) -> Result<(), SvgRenderError> {
635    if !image.is_visible() || state.opacity <= 0.0 {
636        return Ok(());
637    }
638
639    match image.kind() {
640        usvg::ImageKind::JPEG(data)
641        | usvg::ImageKind::PNG(data)
642        | usvg::ImageKind::GIF(data)
643        | usvg::ImageKind::WEBP(data) => {
644            let decoded = image::load_from_memory(data)?;
645            let rgba = decoded.to_rgba8();
646            let (img_w, img_h) = (rgba.width(), rgba.height());
647            if img_w == 0 || img_h == 0 {
648                return Ok(());
649            }
650            let mut pixels = rgba.into_raw();
651            if state.opacity < 1.0 {
652                for px in pixels.chunks_exact_mut(4) {
653                    px[3] = ((px[3] as f32 * state.opacity).clamp(0.0, 255.0)) as u8;
654                }
655            }
656
657            let size = image.size();
658            ctx.save();
659            apply_transform(ctx, image.abs_transform());
660            let t = ctx.transform();
661            let (dst_x, dst_y, dst_w, dst_h) =
662                transformed_rect(&t, size.width() as f64, size.height() as f64);
663            ctx.reset_transform();
664            ctx.draw_image_rgba(&pixels, img_w, img_h, dst_x, dst_y, dst_w, dst_h);
665            ctx.restore();
666        }
667        usvg::ImageKind::SVG(tree) => {
668            let fb = render_svg_tree_to_framebuffer(tree)?;
669            let mut pixels = fb.pixels_flipped();
670            unpremultiply_rgba_inplace(&mut pixels);
671            let size = image.size();
672            ctx.save();
673            apply_transform(ctx, image.abs_transform());
674            let t = ctx.transform();
675            let (dst_x, dst_y, dst_w, dst_h) =
676                transformed_rect(&t, size.width() as f64, size.height() as f64);
677            ctx.reset_transform();
678            ctx.draw_image_rgba(&pixels, fb.width(), fb.height(), dst_x, dst_y, dst_w, dst_h);
679            ctx.restore();
680        }
681    }
682
683    Ok(())
684}
685
686fn emit_path(path: &usvg::Path, ctx: &mut dyn DrawCtx) {
687    ctx.begin_path();
688    for segment in path.data().segments() {
689        match segment {
690            PathSegment::MoveTo(p) => ctx.move_to(p.x as f64, p.y as f64),
691            PathSegment::LineTo(p) => ctx.line_to(p.x as f64, p.y as f64),
692            PathSegment::QuadTo(p1, p2) => {
693                ctx.quad_to(p1.x as f64, p1.y as f64, p2.x as f64, p2.y as f64)
694            }
695            PathSegment::CubicTo(p1, p2, p3) => ctx.cubic_to(
696                p1.x as f64,
697                p1.y as f64,
698                p2.x as f64,
699                p2.y as f64,
700                p3.x as f64,
701                p3.y as f64,
702            ),
703            PathSegment::Close => ctx.close_path(),
704        }
705    }
706}
707
708fn apply_transform(ctx: &mut dyn DrawCtx, transform: usvg::Transform) {
709    let mut current = ctx.transform();
710    let node_transform = to_trans_affine(transform);
711    current.premultiply(&node_transform);
712    ctx.set_transform(current);
713}
714
715pub(super) fn to_trans_affine(transform: usvg::Transform) -> TransAffine {
716    TransAffine::new_custom(
717        transform.sx as f64,
718        transform.ky as f64,
719        transform.kx as f64,
720        transform.sy as f64,
721        transform.tx as f64,
722        transform.ty as f64,
723    )
724}
725
726fn transformed_rect(transform: &TransAffine, width: f64, height: f64) -> (f64, f64, f64, f64) {
727    let corners = [(0.0, 0.0), (width, 0.0), (width, height), (0.0, height)];
728    let mut min_x = f64::INFINITY;
729    let mut min_y = f64::INFINITY;
730    let mut max_x = f64::NEG_INFINITY;
731    let mut max_y = f64::NEG_INFINITY;
732    for (mut x, mut y) in corners {
733        transform.transform(&mut x, &mut y);
734        min_x = min_x.min(x);
735        min_y = min_y.min(y);
736        max_x = max_x.max(x);
737        max_y = max_y.max(y);
738    }
739
740    (min_x, min_y, (max_x - min_x).abs(), (max_y - min_y).abs())
741}
742
743fn map_line_cap(cap: usvg::LineCap) -> LineCap {
744    match cap {
745        usvg::LineCap::Butt => LineCap::Butt,
746        usvg::LineCap::Round => LineCap::Round,
747        usvg::LineCap::Square => LineCap::Square,
748    }
749}
750
751fn map_line_join(join: usvg::LineJoin) -> LineJoin {
752    match join {
753        usvg::LineJoin::Miter | usvg::LineJoin::MiterClip => LineJoin::Miter,
754        usvg::LineJoin::Round => LineJoin::Round,
755        usvg::LineJoin::Bevel => LineJoin::Bevel,
756    }
757}
758
759fn map_fill_rule(rule: usvg::FillRule) -> FillRule {
760    match rule {
761        usvg::FillRule::NonZero => FillRule::NonZero,
762        usvg::FillRule::EvenOdd => FillRule::EvenOdd,
763    }
764}
765
766#[cfg(test)]
767mod clip_tests;
768pub mod compare;
769#[cfg(test)]
770mod gradient_tests;
771#[cfg(test)]
772mod image_tests;
773#[cfg(test)]
774mod opacity_tests;
775#[cfg(test)]
776mod text_tests;
777
778mod font_defaults;
779mod paint;
780mod pattern;
781
782#[cfg(test)]
783mod core_tests;