1use 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#[derive(Debug)]
47pub enum SvgRenderError {
48 Parse(usvg::Error),
50 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#[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 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 pub fn with_font_family(mut self, family: impl Into<String>) -> Self {
102 self.font_family = Some(family.into());
103 self
104 }
105
106 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
126pub 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
136pub 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
163pub 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
176pub 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
185pub 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
195pub 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
229pub 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
246pub 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
281pub 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
288pub 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
304pub 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
321pub 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
352pub 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
359pub 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
375pub 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
386pub 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 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;