krilla_svg/
lib.rs

1/*!
2An extension to krilla that allows rendering SVG files to a PDF file.
3
4It is based on [usvg](https://github.com/linebender/resvg) and passes nearly the whole
5resvg test suite. See the [examples]( https://github.com/LaurenzV/krilla/tree/main/crates/krilla-svg/examples)
6directory for an example on how to use this crate in combination with krilla to convert SVG files
7to PDF.
8*/
9
10#![deny(missing_docs)]
11
12use std::collections::{HashMap, HashSet};
13use std::io::Read;
14use std::sync::Arc;
15
16use fontdb::Database;
17use krilla::color::rgb;
18use krilla::geom::{Rect, Size, Transform};
19use krilla::paint::FillRule;
20use krilla::surface::Surface;
21use krilla::text::Font;
22use krilla::text::GlyphId;
23use usvg::{fontdb, roxmltree, Group, ImageKind, Node, Tree};
24
25use crate::util::RectExt;
26
27mod clip_path;
28mod filter;
29mod group;
30mod image;
31mod mask;
32mod path;
33mod text;
34mod util;
35
36/// Settings that should be applied when converting a SVG.
37#[derive(Copy, Clone, Debug)]
38pub struct SvgSettings {
39    /// Whether text should be embedded as properly selectable text. Otherwise,
40    /// it will be drawn as outlined paths instead.
41    pub embed_text: bool,
42    /// How much filters, which will be converted to bitmaps, should be scaled. Higher values
43    /// mean better quality, but also bigger file sizes.
44    pub filter_scale: f32,
45}
46
47impl Default for SvgSettings {
48    fn default() -> Self {
49        Self {
50            embed_text: true,
51            filter_scale: 4.0,
52        }
53    }
54}
55
56/// An extension trait for the `Surface` struct that allows you to draw SVGs onto a surface.
57pub trait SurfaceExt {
58    /// Draw a `usvg` tree onto a surface with the given size and settings.
59    fn draw_svg(&mut self, tree: &Tree, size: Size, svg_settings: SvgSettings) -> Option<()>;
60}
61
62impl SurfaceExt for Surface<'_> {
63    fn draw_svg(&mut self, tree: &Tree, size: Size, svg_settings: SvgSettings) -> Option<()> {
64        let old_fill = self.get_fill().cloned();
65        let old_stroke = self.get_stroke().cloned();
66
67        let transform = Transform::from_scale(
68            size.width() / tree.size().width(),
69            size.height() / tree.size().height(),
70        );
71        self.push_transform(&transform);
72        self.push_clip_path(
73            &Rect::from_xywh(0.0, 0.0, tree.size().width(), tree.size().height())
74                .unwrap()
75                .to_clip_path(),
76            &FillRule::NonZero,
77        );
78        render_tree(tree, svg_settings, self);
79        self.pop();
80        self.pop();
81
82        self.set_fill(old_fill);
83        self.set_stroke(old_stroke);
84
85        Some(())
86    }
87}
88
89struct ProcessContext {
90    fonts: HashMap<fontdb::ID, Font>,
91    svg_settings: SvgSettings,
92}
93
94impl ProcessContext {
95    fn new(fonts: HashMap<fontdb::ID, Font>, svg_settings: SvgSettings) -> Self {
96        Self {
97            fonts,
98            svg_settings,
99        }
100    }
101}
102
103pub(crate) fn render_tree(tree: &Tree, svg_settings: SvgSettings, surface: &mut Surface) {
104    let mut db = tree.fontdb().clone();
105    let mut fc = get_context_from_group(Arc::make_mut(&mut db), svg_settings, tree.root());
106    group::render(tree.root(), surface, &mut fc);
107}
108
109pub(crate) fn render_node(
110    node: &Node,
111    mut tree_fontdb: Arc<Database>,
112    svg_settings: SvgSettings,
113    surface: &mut Surface,
114) {
115    let mut fc = get_context_from_node(Arc::make_mut(&mut tree_fontdb), svg_settings, node);
116    group::render_node(node, surface, &mut fc);
117}
118
119/// Render an SVG glyph from an OpenType font into a surface. You can plug this method into the
120/// `render_svg_glyph_fn` field of `SerializeSettings` in krilla..
121pub fn render_svg_glyph(
122    data: &[u8],
123    context_color: rgb::Color,
124    glyph: GlyphId,
125    default_size: (f32, f32),
126    surface: &mut Surface,
127) -> Option<()> {
128    let mut data = data;
129    let settings = SvgSettings::default();
130
131    let default_size = usvg::Size::from_wh(default_size.0, default_size.1).unwrap();
132
133    let mut decoded = vec![];
134    if data.starts_with(&[0x1f, 0x8b]) {
135        let mut decoder = flate2::read::GzDecoder::new(data);
136        decoder.read_to_end(&mut decoded).ok()?;
137        data = &decoded;
138    }
139
140    let xml = std::str::from_utf8(data).ok()?;
141    // Incredibly hacky, but hopefully that's enough for SVG glyphs.
142    let has_viewbox = xml.contains("viewBox");
143    let document = roxmltree::Document::parse(xml).ok()?;
144
145    // Reparsing every time might be pretty slow in some cases, because Noto Color Emoji
146    // for example contains hundreds of glyphs in the same SVG document, meaning that we have
147    // to reparse it every time. However, Twitter Color Emoji does have each glyph in a
148    // separate SVG document, and since we use COLRv1 for Noto Color Emoji anyway, this is
149    // good enough.
150    let opts = usvg::Options {
151        style_sheet: Some(format!(
152            "svg {{ color: rgb({}, {}, {}) }}",
153            context_color.red(),
154            context_color.green(),
155            context_color.blue()
156        )),
157        default_size,
158        ..Default::default()
159    };
160    let tree = Tree::from_xmltree(&document, &opts).ok()?;
161
162    let apply_scale = default_size != tree.size() && has_viewbox;
163
164    // From the specification:
165    //
166    // The size of the initial viewport for the SVG document is the em square:
167    // height and width both equal to head.unitsPerEm. If a viewBox
168    // attribute is specified on the <svg> element with width or
169    // height values different from the unitsPerEm value,
170    // this will have the effect of a scale transformation on the SVG “user” coordinate
171    // system.
172    if apply_scale {
173        let scale = (default_size.width() / tree.size().width())
174            .min(default_size.height() / tree.size().height());
175        surface.push_transform(&Transform::from_scale(scale, scale))
176    }
177
178    if let Some(node) = tree.node_by_id(&format!("glyph{}", glyph.to_u32())) {
179        render_node(node, tree.fontdb().clone(), settings, surface)
180    } else {
181        // Twitter Color Emoji SVGs contain the glyph ID on the root element, which isn't saved by
182        // usvg. So in this case, we simply draw the whole document.
183        render_tree(&tree, settings, surface)
184    };
185
186    if apply_scale {
187        surface.pop();
188    }
189
190    Some(())
191}
192
193fn get_context_from_group(
194    tree_fontdb: &mut Database,
195    svg_settings: SvgSettings,
196    group: &Group,
197) -> ProcessContext {
198    let mut ids = HashSet::new();
199    get_ids_from_group_impl(group, &mut ids);
200    let ids = ids.into_iter().collect::<Vec<_>>();
201    let db = convert_fontdb(tree_fontdb, Some(ids));
202
203    ProcessContext::new(db, svg_settings)
204}
205
206fn get_context_from_node(
207    tree_fontdb: &mut Database,
208    svg_settings: SvgSettings,
209    node: &Node,
210) -> ProcessContext {
211    let mut ids = HashSet::new();
212    get_ids_impl(node, &mut ids);
213    let ids = ids.into_iter().collect::<Vec<_>>();
214    let db = convert_fontdb(tree_fontdb, Some(ids));
215
216    ProcessContext::new(db, svg_settings)
217}
218
219fn get_ids_from_group_impl(group: &Group, ids: &mut HashSet<fontdb::ID>) {
220    for child in group.children() {
221        get_ids_impl(child, ids);
222    }
223}
224
225// Collect all used font IDs
226fn get_ids_impl(node: &Node, ids: &mut HashSet<fontdb::ID>) {
227    match node {
228        Node::Text(t) => {
229            for span in t.layouted() {
230                for g in &span.positioned_glyphs {
231                    ids.insert(g.font);
232                }
233            }
234        }
235        Node::Group(group) => {
236            get_ids_from_group_impl(group, ids);
237        }
238        Node::Image(image) => {
239            if let ImageKind::SVG(svg) = image.kind() {
240                get_ids_from_group_impl(svg.root(), ids);
241            }
242        }
243        _ => {}
244    }
245
246    node.subroots(|subroot| get_ids_from_group_impl(subroot, ids));
247}
248
249fn convert_fontdb(db: &mut Database, ids: Option<Vec<fontdb::ID>>) -> HashMap<fontdb::ID, Font> {
250    let mut map = HashMap::new();
251
252    let ids = ids.unwrap_or(db.faces().map(|f| f.id).collect::<Vec<_>>());
253
254    for id in ids {
255        // What we could do is just go through each font and then create a new Font object for each of them.
256        // However, this is somewhat wasteful and expensive, because we have to hash each font, which
257        // can go be multiple MB. So instead, we first construct a font info object, which is much
258        // cheaper, and then check whether we already have a corresponding font object in the cache.
259        // If not, we still need to construct it.
260        if let Some((font_data, index)) = unsafe { db.make_shared_face_data(id) } {
261            if let Some(font) = Font::new(font_data.into(), index) {
262                map.insert(id, font);
263            }
264        }
265    }
266
267    map
268}