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    surface: &mut Surface,
126) -> Option<()> {
127    let mut data = data;
128    let settings = SvgSettings::default();
129
130    let mut decoded = vec![];
131    if data.starts_with(&[0x1f, 0x8b]) {
132        let mut decoder = flate2::read::GzDecoder::new(data);
133        decoder.read_to_end(&mut decoded).ok()?;
134        data = &decoded;
135    }
136
137    let xml = std::str::from_utf8(data).ok()?;
138    let document = roxmltree::Document::parse(xml).ok()?;
139
140    // Reparsing every time might be pretty slow in some cases, because Noto Color Emoji
141    // for example contains hundreds of glyphs in the same SVG document, meaning that we have
142    // to reparse it every time. However, Twitter Color Emoji does have each glyph in a
143    // separate SVG document, and since we use COLRv1 for Noto Color Emoji anyway, this is
144    // good enough.
145    let opts = usvg::Options {
146        style_sheet: Some(format!(
147            "svg {{ color: rgb({}, {}, {}) }}",
148            context_color.red(),
149            context_color.green(),
150            context_color.blue()
151        )),
152        ..Default::default()
153    };
154    let tree = Tree::from_xmltree(&document, &opts).ok()?;
155
156    if let Some(node) = tree.node_by_id(&format!("glyph{}", glyph.to_u32())) {
157        render_node(node, tree.fontdb().clone(), settings, surface)
158    } else {
159        // Twitter Color Emoji SVGs contain the glyph ID on the root element, which isn't saved by
160        // usvg. So in this case, we simply draw the whole document.
161        render_tree(&tree, settings, surface)
162    };
163
164    Some(())
165}
166
167fn get_context_from_group(
168    tree_fontdb: &mut Database,
169    svg_settings: SvgSettings,
170    group: &Group,
171) -> ProcessContext {
172    let mut ids = HashSet::new();
173    get_ids_from_group_impl(group, &mut ids);
174    let ids = ids.into_iter().collect::<Vec<_>>();
175    let db = convert_fontdb(tree_fontdb, Some(ids));
176
177    ProcessContext::new(db, svg_settings)
178}
179
180fn get_context_from_node(
181    tree_fontdb: &mut Database,
182    svg_settings: SvgSettings,
183    node: &Node,
184) -> ProcessContext {
185    let mut ids = HashSet::new();
186    get_ids_impl(node, &mut ids);
187    let ids = ids.into_iter().collect::<Vec<_>>();
188    let db = convert_fontdb(tree_fontdb, Some(ids));
189
190    ProcessContext::new(db, svg_settings)
191}
192
193fn get_ids_from_group_impl(group: &Group, ids: &mut HashSet<fontdb::ID>) {
194    for child in group.children() {
195        get_ids_impl(child, ids);
196    }
197}
198
199// Collect all used font IDs
200fn get_ids_impl(node: &Node, ids: &mut HashSet<fontdb::ID>) {
201    match node {
202        Node::Text(t) => {
203            for span in t.layouted() {
204                for g in &span.positioned_glyphs {
205                    ids.insert(g.font);
206                }
207            }
208        }
209        Node::Group(group) => {
210            get_ids_from_group_impl(group, ids);
211        }
212        Node::Image(image) => {
213            if let ImageKind::SVG(svg) = image.kind() {
214                get_ids_from_group_impl(svg.root(), ids);
215            }
216        }
217        _ => {}
218    }
219
220    node.subroots(|subroot| get_ids_from_group_impl(subroot, ids));
221}
222
223fn convert_fontdb(db: &mut Database, ids: Option<Vec<fontdb::ID>>) -> HashMap<fontdb::ID, Font> {
224    let mut map = HashMap::new();
225
226    let ids = ids.unwrap_or(db.faces().map(|f| f.id).collect::<Vec<_>>());
227
228    for id in ids {
229        // What we could do is just go through each font and then create a new Font object for each of them.
230        // However, this is somewhat wasteful and expensive, because we have to hash each font, which
231        // can go be multiple MB. So instead, we first construct a font info object, which is much
232        // cheaper, and then check whether we already have a corresponding font object in the cache.
233        // If not, we still need to construct it.
234        if let Some((font_data, index)) = unsafe { db.make_shared_face_data(id) } {
235            if let Some(font) = Font::new(font_data.into(), index) {
236                map.insert(id, font);
237            }
238        }
239    }
240
241    map
242}