1 | // Copyright 2018 the Resvg Authors |
2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | |
4 | use crate::OptionLog; |
5 | |
6 | pub struct Context { |
7 | pub max_bbox: tiny_skia::IntRect, |
8 | } |
9 | |
10 | pub fn render_nodes( |
11 | parent: &usvg::Group, |
12 | ctx: &Context, |
13 | transform: tiny_skia::Transform, |
14 | pixmap: &mut tiny_skia::PixmapMut, |
15 | ) { |
16 | for node: &Node in parent.children() { |
17 | render_node(node, ctx, transform, pixmap); |
18 | } |
19 | } |
20 | |
21 | pub fn render_node( |
22 | node: &usvg::Node, |
23 | ctx: &Context, |
24 | transform: tiny_skia::Transform, |
25 | pixmap: &mut tiny_skia::PixmapMut, |
26 | ) { |
27 | match node { |
28 | usvg::Node::Group(ref group: &Box) => { |
29 | render_group(group, ctx, transform, pixmap); |
30 | } |
31 | usvg::Node::Path(ref path: &Box) => { |
32 | crate::path::render( |
33 | path, |
34 | tiny_skia::BlendMode::SourceOver, |
35 | ctx, |
36 | transform, |
37 | pixmap, |
38 | ); |
39 | } |
40 | usvg::Node::Image(ref image: &Box) => { |
41 | crate::image::render(image, transform, pixmap); |
42 | } |
43 | usvg::Node::Text(ref text: &Box) => { |
44 | render_group(group:text.flattened(), ctx, transform, pixmap); |
45 | } |
46 | } |
47 | } |
48 | |
49 | fn render_group( |
50 | group: &usvg::Group, |
51 | ctx: &Context, |
52 | transform: tiny_skia::Transform, |
53 | pixmap: &mut tiny_skia::PixmapMut, |
54 | ) -> Option<()> { |
55 | let transform = transform.pre_concat(group.transform()); |
56 | |
57 | if !group.should_isolate() { |
58 | render_nodes(group, ctx, transform, pixmap); |
59 | return Some(()); |
60 | } |
61 | |
62 | let bbox = group.layer_bounding_box().transform(transform)?; |
63 | |
64 | let mut ibbox = if group.filters().is_empty() { |
65 | // Convert group bbox into an integer one, expanding each side outwards by 2px |
66 | // to make sure that anti-aliased pixels would not be clipped. |
67 | tiny_skia::IntRect::from_xywh( |
68 | bbox.x().floor() as i32 - 2, |
69 | bbox.y().floor() as i32 - 2, |
70 | bbox.width().ceil() as u32 + 4, |
71 | bbox.height().ceil() as u32 + 4, |
72 | )? |
73 | } else { |
74 | // The bounding box for groups with filters is special and should not be expanded by 2px, |
75 | // because it's already acting as a clipping region. |
76 | let bbox = bbox.to_int_rect(); |
77 | // Make sure our filter region is not bigger than 4x the canvas size. |
78 | // This is required mainly to prevent huge filter regions that would tank the performance. |
79 | // It should not affect the final result in any way. |
80 | crate::geom::fit_to_rect(bbox, ctx.max_bbox)? |
81 | }; |
82 | |
83 | // Make sure our layer is not bigger than 4x the canvas size. |
84 | // This is required to prevent huge layers. |
85 | if group.filters().is_empty() { |
86 | ibbox = crate::geom::fit_to_rect(ibbox, ctx.max_bbox)?; |
87 | } |
88 | |
89 | let shift_ts = { |
90 | // Original shift. |
91 | let mut dx = bbox.x(); |
92 | let mut dy = bbox.y(); |
93 | |
94 | // Account for subpixel positioned layers. |
95 | dx -= bbox.x() - ibbox.x() as f32; |
96 | dy -= bbox.y() - ibbox.y() as f32; |
97 | |
98 | tiny_skia::Transform::from_translate(-dx, -dy) |
99 | }; |
100 | |
101 | let transform = shift_ts.pre_concat(transform); |
102 | |
103 | let mut sub_pixmap = tiny_skia::Pixmap::new(ibbox.width(), ibbox.height()) |
104 | .log_none(|| log::warn!("Failed to allocate a group layer for: {:?}." , ibbox))?; |
105 | |
106 | render_nodes(group, ctx, transform, &mut sub_pixmap.as_mut()); |
107 | |
108 | if !group.filters().is_empty() { |
109 | for filter in group.filters() { |
110 | crate::filter::apply(filter, transform, &mut sub_pixmap); |
111 | } |
112 | } |
113 | |
114 | if let Some(clip_path) = group.clip_path() { |
115 | crate::clip::apply(clip_path, transform, &mut sub_pixmap); |
116 | } |
117 | |
118 | if let Some(mask) = group.mask() { |
119 | crate::mask::apply(mask, ctx, transform, &mut sub_pixmap); |
120 | } |
121 | |
122 | let paint = tiny_skia::PixmapPaint { |
123 | opacity: group.opacity().get(), |
124 | blend_mode: convert_blend_mode(group.blend_mode()), |
125 | quality: tiny_skia::FilterQuality::Nearest, |
126 | }; |
127 | |
128 | pixmap.draw_pixmap( |
129 | ibbox.x(), |
130 | ibbox.y(), |
131 | sub_pixmap.as_ref(), |
132 | &paint, |
133 | tiny_skia::Transform::identity(), |
134 | None, |
135 | ); |
136 | |
137 | Some(()) |
138 | } |
139 | |
140 | pub fn convert_blend_mode(mode: usvg::BlendMode) -> tiny_skia::BlendMode { |
141 | match mode { |
142 | usvg::BlendMode::Normal => tiny_skia::BlendMode::SourceOver, |
143 | usvg::BlendMode::Multiply => tiny_skia::BlendMode::Multiply, |
144 | usvg::BlendMode::Screen => tiny_skia::BlendMode::Screen, |
145 | usvg::BlendMode::Overlay => tiny_skia::BlendMode::Overlay, |
146 | usvg::BlendMode::Darken => tiny_skia::BlendMode::Darken, |
147 | usvg::BlendMode::Lighten => tiny_skia::BlendMode::Lighten, |
148 | usvg::BlendMode::ColorDodge => tiny_skia::BlendMode::ColorDodge, |
149 | usvg::BlendMode::ColorBurn => tiny_skia::BlendMode::ColorBurn, |
150 | usvg::BlendMode::HardLight => tiny_skia::BlendMode::HardLight, |
151 | usvg::BlendMode::SoftLight => tiny_skia::BlendMode::SoftLight, |
152 | usvg::BlendMode::Difference => tiny_skia::BlendMode::Difference, |
153 | usvg::BlendMode::Exclusion => tiny_skia::BlendMode::Exclusion, |
154 | usvg::BlendMode::Hue => tiny_skia::BlendMode::Hue, |
155 | usvg::BlendMode::Saturation => tiny_skia::BlendMode::Saturation, |
156 | usvg::BlendMode::Color => tiny_skia::BlendMode::Color, |
157 | usvg::BlendMode::Luminosity => tiny_skia::BlendMode::Luminosity, |
158 | } |
159 | } |
160 | |