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::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
28pub type SvgTree = usvg::Tree;
29
30#[derive(Clone, Copy, Debug)]
31struct SvgRenderState {
32    opacity: f32,
33    layer_width: f64,
34    layer_height: f64,
35    source_cull: Option<SvgSourceRect>,
36}
37
38impl Default for SvgRenderState {
39    fn default() -> Self {
40        Self {
41            opacity: 1.0,
42            layer_width: 1.0,
43            layer_height: 1.0,
44            source_cull: None,
45        }
46    }
47}
48
49#[derive(Clone, Copy, Debug)]
50struct SvgSourceRect {
51    x: f64,
52    y: f64,
53    w: f64,
54    h: f64,
55}
56
57impl SvgSourceRect {
58    fn intersects(self, rect: SvgSourceRect) -> bool {
59        let ax0 = self.x;
60        let ay0 = self.y;
61        let ax1 = self.x + self.w;
62        let ay1 = self.y + self.h;
63        let bx0 = rect.x;
64        let by0 = rect.y;
65        let bx1 = rect.x + rect.w;
66        let by1 = rect.y + rect.h;
67        ax0 < bx1 && ax1 > bx0 && ay0 < by1 && ay1 > by0
68    }
69}
70
71/// Errors returned by the SVG renderer.
72#[derive(Debug)]
73pub enum SvgRenderError {
74    /// The SVG data could not be parsed by `usvg`.
75    Parse(usvg::Error),
76    /// A raster image referenced by the SVG could not be decoded.
77    DecodeImage(image::ImageError),
78}
79
80impl fmt::Display for SvgRenderError {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        match self {
83            SvgRenderError::Parse(err) => write!(f, "failed to parse SVG: {err}"),
84            SvgRenderError::DecodeImage(err) => write!(f, "failed to decode SVG image: {err}"),
85        }
86    }
87}
88
89impl std::error::Error for SvgRenderError {}
90
91impl From<usvg::Error> for SvgRenderError {
92    fn from(err: usvg::Error) -> Self {
93        SvgRenderError::Parse(err)
94    }
95}
96
97impl From<image::ImageError> for SvgRenderError {
98    fn from(err: image::ImageError) -> Self {
99        SvgRenderError::DecodeImage(err)
100    }
101}
102
103/// Options used while parsing SVG documents.
104///
105/// `agg-gui` keeps SVG rendering in the core library, but font selection is
106/// intentionally application-owned. Callers that need SVG text should provide a
107/// `fontdb` built from their own assets.
108#[derive(Clone, Default)]
109pub struct SvgParseOptions {
110    resources_dir: Option<PathBuf>,
111    font_family: Option<String>,
112    fontdb: Option<Arc<usvg::fontdb::Database>>,
113}
114
115impl SvgParseOptions {
116    pub fn new() -> Self {
117        Self::default()
118    }
119
120    /// Resolve relative image references from `resources_dir`.
121    pub fn with_resources_dir(mut self, resources_dir: impl Into<PathBuf>) -> Self {
122        self.resources_dir = Some(resources_dir.into());
123        self
124    }
125
126    /// Set the preferred SVG text family for documents that omit one.
127    pub fn with_font_family(mut self, family: impl Into<String>) -> Self {
128        self.font_family = Some(family.into());
129        self
130    }
131
132    /// Provide a prepared font database for SVG text parsing.
133    pub fn with_fontdb(mut self, fontdb: Arc<usvg::fontdb::Database>) -> Self {
134        self.fontdb = Some(fontdb);
135        self
136    }
137}
138
139static DEFAULT_SVG_PARSE_OPTIONS: OnceLock<RwLock<SvgParseOptions>> = OnceLock::new();
140
141fn default_svg_parse_options_cell() -> &'static RwLock<SvgParseOptions> {
142    DEFAULT_SVG_PARSE_OPTIONS.get_or_init(|| RwLock::new(system_svg_parse_options()))
143}
144
145fn system_svg_parse_options() -> SvgParseOptions {
146    let mut fontdb = usvg::fontdb::Database::new();
147    fontdb.load_system_fonts();
148    font_defaults::configure_generic_font_families(&mut fontdb, None);
149    SvgParseOptions::new().with_fontdb(Arc::new(fontdb))
150}
151
152/// Replace the default SVG parse options used by convenience render helpers.
153///
154/// This keeps SVG rendering in the core library while letting applications own
155/// the font database used for SVG text.
156pub fn set_default_svg_parse_options(options: SvgParseOptions) {
157    *default_svg_parse_options_cell()
158        .write()
159        .expect("default SVG parse options lock poisoned") = options;
160}
161
162/// Build a `usvg` font database from caller-owned font bytes.
163pub fn svg_fontdb_from_font_data<I>(
164    fonts: I,
165    generic_family: Option<&str>,
166) -> Arc<usvg::fontdb::Database>
167where
168    I: IntoIterator<Item = Vec<u8>>,
169{
170    let mut fontdb = usvg::fontdb::Database::new();
171    for bytes in fonts {
172        fontdb.load_font_data(bytes);
173    }
174    font_defaults::configure_generic_font_families(&mut fontdb, generic_family);
175    Arc::new(fontdb)
176}
177
178fn parse_svg_tree(data: &[u8], resources_dir: Option<&Path>) -> Result<usvg::Tree, SvgRenderError> {
179    let mut options = default_svg_parse_options_cell()
180        .read()
181        .expect("default SVG parse options lock poisoned")
182        .clone();
183    if let Some(dir) = resources_dir {
184        options = options.with_resources_dir(dir);
185    }
186    parse_svg(data, &options)
187}
188
189/// Parse an SVG document using caller-supplied parse options.
190pub fn parse_svg(data: &[u8], svg_options: &SvgParseOptions) -> Result<usvg::Tree, SvgRenderError> {
191    let mut options = usvg::Options::default();
192    options.resources_dir = svg_options.resources_dir.clone();
193    if let Some(font_family) = &svg_options.font_family {
194        options.font_family = font_family.clone();
195    }
196    if let Some(fontdb) = &svg_options.fontdb {
197        options.fontdb = Arc::clone(fontdb);
198    }
199    Ok(usvg::Tree::from_data(data, &options)?)
200}
201
202/// Parse an SVG document and render it into `ctx`.
203///
204/// This is a convenience wrapper around [`render_svg_tree`].  Callers that
205/// already cache a `usvg::Tree` should use [`render_svg_tree`] directly.
206pub fn render_svg(data: &[u8], ctx: &mut dyn DrawCtx) -> Result<(), SvgRenderError> {
207    let tree = parse_svg_tree(data, None)?;
208    render_svg_tree(&tree, ctx)
209}
210
211/// Parse an SVG document with explicit options and render it into `ctx`.
212pub fn render_svg_with_options(
213    data: &[u8],
214    ctx: &mut dyn DrawCtx,
215    options: &SvgParseOptions,
216) -> Result<(), SvgRenderError> {
217    let tree = parse_svg(data, options)?;
218    render_svg_tree(&tree, ctx)
219}
220
221/// Parse an SVG document and render it into `ctx` using an explicit output
222/// pixel size for the document viewport.
223pub fn render_svg_at_size(
224    data: &[u8],
225    ctx: &mut dyn DrawCtx,
226    width: u32,
227    height: u32,
228) -> Result<(), SvgRenderError> {
229    let tree = parse_svg_tree(data, None)?;
230    render_svg_tree_at_size(&tree, ctx, width, height)
231}
232
233pub fn render_svg_at_size_with_options(
234    data: &[u8],
235    ctx: &mut dyn DrawCtx,
236    width: u32,
237    height: u32,
238    options: &SvgParseOptions,
239) -> Result<(), SvgRenderError> {
240    let tree = parse_svg(data, options)?;
241    render_svg_tree_at_size(&tree, ctx, width, height)
242}
243
244pub fn render_svg_at_size_with_resources(
245    data: &[u8],
246    ctx: &mut dyn DrawCtx,
247    width: u32,
248    height: u32,
249    resources_dir: &Path,
250) -> Result<(), SvgRenderError> {
251    let tree = parse_svg_tree(data, Some(resources_dir))?;
252    render_svg_tree_at_size(&tree, ctx, width, height)
253}
254
255/// Parse an SVG document and render it into a newly allocated RGBA framebuffer.
256///
257/// This is the library API the SVG regression tests and demo viewer should use
258/// for the `agg-rgba-bitmap render` column.
259pub fn render_svg_to_framebuffer(data: &[u8]) -> Result<Framebuffer, SvgRenderError> {
260    let tree = parse_svg_tree(data, None)?;
261    render_svg_tree_to_framebuffer(&tree)
262}
263
264pub fn render_svg_to_framebuffer_with_options(
265    data: &[u8],
266    options: &SvgParseOptions,
267) -> Result<Framebuffer, SvgRenderError> {
268    let tree = parse_svg(data, options)?;
269    render_svg_tree_to_framebuffer(&tree)
270}
271
272/// Parse an SVG document and render it into an RGBA framebuffer with an
273/// explicit pixel size.
274///
275/// The resvg test suite reference PNGs are not always the SVG document's
276/// intrinsic size, so regression tests and viewers should use this helper when
277/// they need render output to match a reference image one-to-one.
278pub fn render_svg_to_framebuffer_at_size(
279    data: &[u8],
280    width: u32,
281    height: u32,
282) -> Result<Framebuffer, SvgRenderError> {
283    let tree = parse_svg_tree(data, None)?;
284    render_svg_tree_to_framebuffer_at_size(&tree, width, height)
285}
286
287pub fn render_svg_to_framebuffer_at_size_with_options(
288    data: &[u8],
289    width: u32,
290    height: u32,
291    options: &SvgParseOptions,
292) -> Result<Framebuffer, SvgRenderError> {
293    let tree = parse_svg(data, options)?;
294    render_svg_tree_to_framebuffer_at_size(&tree, width, height)
295}
296
297pub fn render_svg_to_framebuffer_at_size_with_resources(
298    data: &[u8],
299    width: u32,
300    height: u32,
301    resources_dir: &Path,
302) -> Result<Framebuffer, SvgRenderError> {
303    let tree = parse_svg_tree(data, Some(resources_dir))?;
304    render_svg_tree_to_framebuffer_at_size(&tree, width, height)
305}
306
307/// Render a parsed SVG tree into a newly allocated RGBA framebuffer.
308pub fn render_svg_tree_to_framebuffer(tree: &usvg::Tree) -> Result<Framebuffer, SvgRenderError> {
309    let width = tree.size().width().ceil().max(1.0) as u32;
310    let height = tree.size().height().ceil().max(1.0) as u32;
311    render_svg_tree_to_framebuffer_at_size(tree, width, height)
312}
313
314/// Render a parsed SVG tree into an RGBA framebuffer with an explicit pixel size.
315pub fn render_svg_tree_to_framebuffer_at_size(
316    tree: &usvg::Tree,
317    width: u32,
318    height: u32,
319) -> Result<Framebuffer, SvgRenderError> {
320    let width = width.max(1);
321    let height = height.max(1);
322    let mut fb = Framebuffer::new(width, height);
323    {
324        let mut ctx = GfxCtx::new(&mut fb);
325        render_svg_tree_at_size(tree, &mut ctx, width, height)?;
326    }
327    Ok(fb)
328}
329
330/// Render a rectangular region of a parsed SVG tree into a newly allocated RGBA framebuffer.
331pub fn render_svg_tree_region_to_framebuffer_at_size(
332    tree: &usvg::Tree,
333    src_x: f64,
334    src_y: f64,
335    src_w: f64,
336    src_h: f64,
337    width: u32,
338    height: u32,
339) -> Result<Framebuffer, SvgRenderError> {
340    let width = width.max(1);
341    let height = height.max(1);
342    let mut fb = Framebuffer::new(width, height);
343    {
344        let mut ctx = GfxCtx::new(&mut fb);
345        render_svg_tree_region_at_size(tree, &mut ctx, src_x, src_y, src_w, src_h, width, height)?;
346    }
347    Ok(fb)
348}
349
350/// Parse an SVG document and render it into a newly allocated LCD coverage buffer.
351///
352/// This is the library API the SVG regression tests and demo viewer should use
353/// for the `agg-lcd-bitmap render` column.
354pub fn render_svg_to_lcd_buffer(data: &[u8]) -> Result<LcdBuffer, SvgRenderError> {
355    let tree = parse_svg_tree(data, None)?;
356    render_svg_tree_to_lcd_buffer(&tree)
357}
358
359pub fn render_svg_to_lcd_buffer_with_options(
360    data: &[u8],
361    options: &SvgParseOptions,
362) -> Result<LcdBuffer, SvgRenderError> {
363    let tree = parse_svg(data, options)?;
364    render_svg_tree_to_lcd_buffer(&tree)
365}
366
367/// Parse an SVG document and render it into an LCD coverage buffer with an
368/// explicit pixel size.
369pub fn render_svg_to_lcd_buffer_at_size(
370    data: &[u8],
371    width: u32,
372    height: u32,
373) -> Result<LcdBuffer, SvgRenderError> {
374    let tree = parse_svg_tree(data, None)?;
375    render_svg_tree_to_lcd_buffer_at_size(&tree, width, height)
376}
377
378pub fn render_svg_to_lcd_buffer_at_size_with_options(
379    data: &[u8],
380    width: u32,
381    height: u32,
382    options: &SvgParseOptions,
383) -> Result<LcdBuffer, SvgRenderError> {
384    let tree = parse_svg(data, options)?;
385    render_svg_tree_to_lcd_buffer_at_size(&tree, width, height)
386}
387
388pub fn render_svg_to_lcd_buffer_at_size_with_resources(
389    data: &[u8],
390    width: u32,
391    height: u32,
392    resources_dir: &Path,
393) -> Result<LcdBuffer, SvgRenderError> {
394    let tree = parse_svg_tree(data, Some(resources_dir))?;
395    render_svg_tree_to_lcd_buffer_at_size(&tree, width, height)
396}
397
398/// Render a parsed SVG tree into a newly allocated LCD coverage buffer.
399pub fn render_svg_tree_to_lcd_buffer(tree: &usvg::Tree) -> Result<LcdBuffer, SvgRenderError> {
400    let width = tree.size().width().ceil().max(1.0) as u32;
401    let height = tree.size().height().ceil().max(1.0) as u32;
402    render_svg_tree_to_lcd_buffer_at_size(tree, width, height)
403}
404
405/// Render a parsed SVG tree into an LCD coverage buffer with an explicit pixel size.
406pub fn render_svg_tree_to_lcd_buffer_at_size(
407    tree: &usvg::Tree,
408    width: u32,
409    height: u32,
410) -> Result<LcdBuffer, SvgRenderError> {
411    let width = width.max(1);
412    let height = height.max(1);
413    let mut buffer = LcdBuffer::new(width, height);
414    {
415        let mut ctx = LcdGfxCtx::new(&mut buffer);
416        render_svg_tree_at_size(tree, &mut ctx, width, height)?;
417    }
418    Ok(buffer)
419}
420
421/// Render a parsed `usvg::Tree` into `ctx`.
422///
423/// The tree's native SVG coordinate system is Y-down.  This function installs
424/// a root transform that maps it into `agg-gui`'s Y-up convention before any
425/// node commands are emitted.
426pub fn render_svg_tree(tree: &usvg::Tree, ctx: &mut dyn DrawCtx) -> Result<(), SvgRenderError> {
427    let width = tree.size().width().ceil().max(1.0) as u32;
428    let height = tree.size().height().ceil().max(1.0) as u32;
429    render_svg_tree_at_size(tree, ctx, width, height)
430}
431
432/// Render a parsed `usvg::Tree` into `ctx`, fitting its document viewport into
433/// an explicit output pixel size.
434pub fn render_svg_tree_at_size(
435    tree: &usvg::Tree,
436    ctx: &mut dyn DrawCtx,
437    width: u32,
438    height: u32,
439) -> Result<(), SvgRenderError> {
440    let saved_transform = ctx.transform();
441    let mut svg_to_ctx = saved_transform;
442    svg_to_ctx.premultiply(&svg_y_down_to_ctx_y_up(tree, width, height));
443
444    ctx.save();
445    ctx.set_transform(svg_to_ctx);
446    render_tree::render_group(
447        tree.root(),
448        ctx,
449        SvgRenderState {
450            layer_width: width.max(1) as f64,
451            layer_height: height.max(1) as f64,
452            ..SvgRenderState::default()
453        },
454    )?;
455    ctx.restore();
456    Ok(())
457}
458
459/// Render a parsed `usvg::Tree` region into `ctx`, mapping the SVG source
460/// rectangle `(src_x, src_y, src_w, src_h)` to the output viewport.
461#[allow(clippy::too_many_arguments)]
462pub fn render_svg_tree_region_at_size(
463    tree: &usvg::Tree,
464    ctx: &mut dyn DrawCtx,
465    src_x: f64,
466    src_y: f64,
467    src_w: f64,
468    src_h: f64,
469    width: u32,
470    height: u32,
471) -> Result<(), SvgRenderError> {
472    let width = width.max(1);
473    let height = height.max(1);
474    let src_w = src_w.max(1.0);
475    let src_h = src_h.max(1.0);
476    let saved_transform = ctx.transform();
477    let mut svg_to_ctx = saved_transform;
478    svg_to_ctx.premultiply(&svg_y_down_region_to_ctx_y_up(
479        src_x, src_y, src_w, src_h, width, height,
480    ));
481
482    ctx.save();
483    ctx.set_transform(svg_to_ctx);
484    render_tree::render_group(
485        tree.root(),
486        ctx,
487        SvgRenderState {
488            layer_width: width as f64,
489            layer_height: height as f64,
490            source_cull: Some(SvgSourceRect {
491                x: src_x,
492                y: src_y,
493                w: src_w,
494                h: src_h,
495            }),
496            ..SvgRenderState::default()
497        },
498    )?;
499    ctx.restore();
500    Ok(())
501}
502
503fn svg_y_down_to_ctx_y_up(tree: &usvg::Tree, width: u32, height: u32) -> TransAffine {
504    let sx = width.max(1) as f64 / tree.size().width().max(1.0) as f64;
505    let sy = height.max(1) as f64 / tree.size().height().max(1.0) as f64;
506    TransAffine::new_custom(sx, 0.0, 0.0, -sy, 0.0, height.max(1) as f64)
507}
508
509fn svg_y_down_region_to_ctx_y_up(
510    src_x: f64,
511    src_y: f64,
512    src_w: f64,
513    src_h: f64,
514    width: u32,
515    height: u32,
516) -> TransAffine {
517    let sx = width as f64 / src_w;
518    let sy = height as f64 / src_h;
519    TransAffine::new_custom(sx, 0.0, 0.0, -sy, -src_x * sx, height as f64 + src_y * sy)
520}
521
522fn emit_path(path: &usvg::Path, ctx: &mut dyn DrawCtx) {
523    ctx.begin_path();
524    for segment in path.data().segments() {
525        match segment {
526            PathSegment::MoveTo(p) => ctx.move_to(p.x as f64, p.y as f64),
527            PathSegment::LineTo(p) => ctx.line_to(p.x as f64, p.y as f64),
528            PathSegment::QuadTo(p1, p2) => {
529                ctx.quad_to(p1.x as f64, p1.y as f64, p2.x as f64, p2.y as f64)
530            }
531            PathSegment::CubicTo(p1, p2, p3) => ctx.cubic_to(
532                p1.x as f64,
533                p1.y as f64,
534                p2.x as f64,
535                p2.y as f64,
536                p3.x as f64,
537                p3.y as f64,
538            ),
539            PathSegment::Close => ctx.close_path(),
540        }
541    }
542}
543
544fn apply_transform(ctx: &mut dyn DrawCtx, transform: usvg::Transform) {
545    let mut current = ctx.transform();
546    let node_transform = to_trans_affine(transform);
547    current.premultiply(&node_transform);
548    ctx.set_transform(current);
549}
550
551pub(super) fn to_trans_affine(transform: usvg::Transform) -> TransAffine {
552    TransAffine::new_custom(
553        transform.sx as f64,
554        transform.ky as f64,
555        transform.kx as f64,
556        transform.sy as f64,
557        transform.tx as f64,
558        transform.ty as f64,
559    )
560}
561
562fn transformed_rect(transform: &TransAffine, width: f64, height: f64) -> (f64, f64, f64, f64) {
563    let corners = [(0.0, 0.0), (width, 0.0), (width, height), (0.0, height)];
564    let mut min_x = f64::INFINITY;
565    let mut min_y = f64::INFINITY;
566    let mut max_x = f64::NEG_INFINITY;
567    let mut max_y = f64::NEG_INFINITY;
568    for (mut x, mut y) in corners {
569        transform.transform(&mut x, &mut y);
570        min_x = min_x.min(x);
571        min_y = min_y.min(y);
572        max_x = max_x.max(x);
573        max_y = max_y.max(y);
574    }
575
576    (min_x, min_y, (max_x - min_x).abs(), (max_y - min_y).abs())
577}
578
579fn transformed_svg_rect(rect: usvg::Rect, transform: usvg::Transform) -> SvgSourceRect {
580    let transform = to_trans_affine(transform);
581    let x = rect.x() as f64;
582    let y = rect.y() as f64;
583    let w = rect.width() as f64;
584    let h = rect.height() as f64;
585    let corners = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)];
586    let mut min_x = f64::INFINITY;
587    let mut min_y = f64::INFINITY;
588    let mut max_x = f64::NEG_INFINITY;
589    let mut max_y = f64::NEG_INFINITY;
590    for (mut x, mut y) in corners {
591        transform.transform(&mut x, &mut y);
592        min_x = min_x.min(x);
593        min_y = min_y.min(y);
594        max_x = max_x.max(x);
595        max_y = max_y.max(y);
596    }
597    SvgSourceRect {
598        x: min_x,
599        y: min_y,
600        w: (max_x - min_x).abs(),
601        h: (max_y - min_y).abs(),
602    }
603}
604
605fn map_line_cap(cap: usvg::LineCap) -> LineCap {
606    match cap {
607        usvg::LineCap::Butt => LineCap::Butt,
608        usvg::LineCap::Round => LineCap::Round,
609        usvg::LineCap::Square => LineCap::Square,
610    }
611}
612
613fn map_line_join(join: usvg::LineJoin) -> LineJoin {
614    match join {
615        usvg::LineJoin::Miter | usvg::LineJoin::MiterClip => LineJoin::Miter,
616        usvg::LineJoin::Round => LineJoin::Round,
617        usvg::LineJoin::Bevel => LineJoin::Bevel,
618    }
619}
620
621fn map_fill_rule(rule: usvg::FillRule) -> FillRule {
622    match rule {
623        usvg::FillRule::NonZero => FillRule::NonZero,
624        usvg::FillRule::EvenOdd => FillRule::EvenOdd,
625    }
626}
627
628#[cfg(test)]
629mod clip_tests;
630pub mod compare;
631#[cfg(test)]
632mod gradient_tests;
633#[cfg(test)]
634mod image_tests;
635#[cfg(test)]
636mod opacity_tests;
637#[cfg(test)]
638mod text_tests;
639
640mod font_defaults;
641mod paint;
642mod pattern;
643mod render_tree;
644use render_tree::render_group;
645
646#[cfg(test)]
647mod core_tests;