resvg/
render.rs

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 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) => {
30            render_group(group, ctx, transform, pixmap);
31        }
32        usvg::Node::Path(ref path) => {
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) => {
42            crate::image::render(image, transform, pixmap);
43        }
44        usvg::Node::Text(ref text) => {
45            render_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 fn convert_blend_mode(mode: usvg::BlendMode) -> tiny_skia::BlendMode {
142    match mode {
143        usvg::BlendMode::Normal => tiny_skia::BlendMode::SourceOver,
144        usvg::BlendMode::Multiply => tiny_skia::BlendMode::Multiply,
145        usvg::BlendMode::Screen => tiny_skia::BlendMode::Screen,
146        usvg::BlendMode::Overlay => tiny_skia::BlendMode::Overlay,
147        usvg::BlendMode::Darken => tiny_skia::BlendMode::Darken,
148        usvg::BlendMode::Lighten => tiny_skia::BlendMode::Lighten,
149        usvg::BlendMode::ColorDodge => tiny_skia::BlendMode::ColorDodge,
150        usvg::BlendMode::ColorBurn => tiny_skia::BlendMode::ColorBurn,
151        usvg::BlendMode::HardLight => tiny_skia::BlendMode::HardLight,
152        usvg::BlendMode::SoftLight => tiny_skia::BlendMode::SoftLight,
153        usvg::BlendMode::Difference => tiny_skia::BlendMode::Difference,
154        usvg::BlendMode::Exclusion => tiny_skia::BlendMode::Exclusion,
155        usvg::BlendMode::Hue => tiny_skia::BlendMode::Hue,
156        usvg::BlendMode::Saturation => tiny_skia::BlendMode::Saturation,
157        usvg::BlendMode::Color => tiny_skia::BlendMode::Color,
158        usvg::BlendMode::Luminosity => tiny_skia::BlendMode::Luminosity,
159    }
160}