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