resvg/
render.rs

1// Copyright 2018 the Resvg Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use crate::OptionLog;
5
6pub struct Context {
7    pub max_bbox: tiny_skia::IntRect,
8}
9
10pub 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 in parent.children() {
17        render_node(node, ctx, transform, pixmap);
18    }
19}
20
21pub 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) => {
29            render_group(group, ctx, transform, pixmap);
30        }
31        usvg::Node::Path(ref path) => {
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) => {
41            crate::image::render(image, transform, pixmap);
42        }
43        usvg::Node::Text(ref text) => {
44            render_group(text.flattened(), ctx, transform, pixmap);
45        }
46    }
47}
48
49fn 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
140pub 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}