Skip to main content

usvg/text/
flatten.rs

1// Copyright 2022 the Resvg Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use std::mem;
5use std::sync::Arc;
6
7use fontdb::{Database, ID};
8use rustybuzz::ttf_parser;
9use rustybuzz::ttf_parser::{GlyphId, RasterImageFormat, RgbaColor};
10use tiny_skia_path::{NonZeroRect, Size, Transform};
11use xmlwriter::XmlWriter;
12
13use crate::text::colr::GlyphPainter;
14use crate::*;
15
16fn resolve_rendering_mode(text: &Text) -> ShapeRendering {
17    match text.rendering_mode {
18        TextRendering::OptimizeSpeed => ShapeRendering::CrispEdges,
19        TextRendering::OptimizeLegibility => ShapeRendering::GeometricPrecision,
20        TextRendering::GeometricPrecision => ShapeRendering::GeometricPrecision,
21    }
22}
23
24fn push_outline_paths(
25    span: &layout::Span,
26    builder: &mut tiny_skia_path::PathBuilder,
27    new_children: &mut Vec<Node>,
28    rendering_mode: ShapeRendering,
29) {
30    let builder = mem::replace(builder, tiny_skia_path::PathBuilder::new());
31
32    if let Some(path) = builder.finish().and_then(|p| {
33        Path::new(
34            String::new(),
35            span.visible,
36            span.fill.clone(),
37            span.stroke.clone(),
38            span.paint_order,
39            rendering_mode,
40            Arc::new(p),
41            Transform::default(),
42        )
43    }) {
44        new_children.push(Node::Path(Box::new(path)));
45    }
46}
47
48pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZeroRect)> {
49    let mut new_children = vec![];
50
51    let rendering_mode = resolve_rendering_mode(text);
52
53    for span in &text.layouted {
54        if let Some(path) = span.overline.as_ref() {
55            let mut path = path.clone();
56            path.rendering_mode = rendering_mode;
57            new_children.push(Node::Path(Box::new(path)));
58        }
59
60        if let Some(path) = span.underline.as_ref() {
61            let mut path = path.clone();
62            path.rendering_mode = rendering_mode;
63            new_children.push(Node::Path(Box::new(path)));
64        }
65
66        // Instead of always processing each glyph separately, we always collect
67        // as many outline glyphs as possible by pushing them into the span_builder
68        // and only if we encounter a different glyph, or we reach the very end of the
69        // span to we push the actual outline paths into new_children. This way, we don't need
70        // to create a new path for every glyph if we have many consecutive glyphs
71        // with just outlines (which is the most common case).
72        let mut span_builder = tiny_skia_path::PathBuilder::new();
73
74        // For variable fonts, we need to extract the outline with variations applied.
75        // We can't use the cache here since the outline depends on variation values.
76        let has_explicit_variations = !span.variations.is_empty();
77
78        for glyph in &span.positioned_glyphs {
79            // A (best-effort conversion of a) COLR glyph.
80            if let Some(tree) = cache.fontdb_colr(glyph.font, glyph.id) {
81                let mut group = Group {
82                    transform: glyph.colr_transform(),
83                    ..Group::empty()
84                };
85                // TODO: Probably need to update abs_transform of children?
86                group.children.push(Node::Group(Box::new(tree.root)));
87                group.calculate_bounding_boxes();
88
89                new_children.push(Node::Group(Box::new(group)));
90            }
91            // An SVG glyph. Will return the usvg node containing the glyph descriptions.
92            else if let Some(node) = cache.fontdb_svg(glyph.font, glyph.id) {
93                push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode);
94
95                let mut group = Group {
96                    transform: glyph.svg_transform(),
97                    ..Group::empty()
98                };
99                // TODO: Probably need to update abs_transform of children?
100                group.children.push(node);
101                group.calculate_bounding_boxes();
102
103                new_children.push(Node::Group(Box::new(group)));
104            }
105            // A bitmap glyph.
106            else if let Some(img) = cache.fontdb_raster(glyph.font, glyph.id) {
107                push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode);
108
109                let transform = if img.is_sbix {
110                    glyph.sbix_transform(
111                        img.x as f32,
112                        img.y as f32,
113                        img.glyph_bbox.map(|bbox| bbox.x_min).unwrap_or(0) as f32,
114                        img.glyph_bbox.map(|bbox| bbox.y_min).unwrap_or(0) as f32,
115                        img.pixels_per_em as f32,
116                        img.image.size.height(),
117                    )
118                } else {
119                    glyph.cbdt_transform(
120                        img.x as f32,
121                        img.y as f32,
122                        img.pixels_per_em as f32,
123                        img.image.size.height(),
124                    )
125                };
126
127                let mut group = Group {
128                    transform,
129                    ..Group::empty()
130                };
131                group.children.push(Node::Image(Box::new(img.image)));
132                group.calculate_bounding_boxes();
133
134                new_children.push(Node::Group(Box::new(group)));
135            } else {
136                // Only bypass cache if: explicit variations OR (auto opsz AND font has opsz axis)
137                let needs_variations = has_explicit_variations
138                    || (span.font_optical_sizing == crate::FontOpticalSizing::Auto
139                        && cache.has_opsz_axis(glyph.font));
140
141                let outline = if needs_variations {
142                    cache.fontdb.outline_with_variations(
143                        glyph.font,
144                        glyph.id,
145                        &span.variations,
146                        glyph.font_size(),
147                        span.font_optical_sizing,
148                    )
149                } else {
150                    cache.fontdb_outline(glyph.font, glyph.id)
151                };
152
153                if let Some(outline) = outline.and_then(|p| p.transform(glyph.outline_transform()))
154                {
155                    span_builder.push_path(&outline);
156                }
157            }
158        }
159
160        push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode);
161
162        if let Some(path) = span.line_through.as_ref() {
163            let mut path = path.clone();
164            path.rendering_mode = rendering_mode;
165            new_children.push(Node::Path(Box::new(path)));
166        }
167    }
168
169    let mut group = Group {
170        id: text.id.clone(),
171        ..Group::empty()
172    };
173
174    for child in new_children {
175        group.children.push(child);
176    }
177
178    group.calculate_bounding_boxes();
179    let stroke_bbox = group.stroke_bounding_box().to_non_zero_rect()?;
180    Some((group, stroke_bbox))
181}
182
183struct PathBuilder {
184    builder: tiny_skia_path::PathBuilder,
185}
186
187impl ttf_parser::OutlineBuilder for PathBuilder {
188    fn move_to(&mut self, x: f32, y: f32) {
189        self.builder.move_to(x, y);
190    }
191
192    fn line_to(&mut self, x: f32, y: f32) {
193        self.builder.line_to(x, y);
194    }
195
196    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
197        self.builder.quad_to(x1, y1, x, y);
198    }
199
200    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
201        self.builder.cubic_to(x1, y1, x2, y2, x, y);
202    }
203
204    fn close(&mut self) {
205        self.builder.close();
206    }
207}
208
209pub(crate) trait DatabaseExt {
210    fn outline(&self, id: ID, glyph_id: GlyphId) -> Option<tiny_skia_path::Path>;
211    fn outline_with_variations(
212        &self,
213        id: ID,
214        glyph_id: GlyphId,
215        variations: &[crate::FontVariation],
216        font_size: f32,
217        font_optical_sizing: crate::FontOpticalSizing,
218    ) -> Option<tiny_skia_path::Path>;
219    fn has_opsz_axis(&self, id: ID) -> bool;
220    fn raster(&self, id: ID, glyph_id: GlyphId) -> Option<BitmapImage>;
221    fn svg(&self, id: ID, glyph_id: GlyphId) -> Option<Node>;
222    fn colr(&self, id: ID, glyph_id: GlyphId) -> Option<Tree>;
223}
224
225#[derive(Clone)]
226pub(crate) struct BitmapImage {
227    image: Image,
228    x: i16,
229    y: i16,
230    pixels_per_em: u16,
231    glyph_bbox: Option<ttf_parser::Rect>,
232    is_sbix: bool,
233}
234
235impl DatabaseExt for Database {
236    #[inline(never)]
237    fn outline(&self, id: ID, glyph_id: GlyphId) -> Option<tiny_skia_path::Path> {
238        self.with_face_data(id, |data, face_index| -> Option<tiny_skia_path::Path> {
239            let mut font = ttf_parser::Face::parse(data, face_index).ok()?;
240
241            // For variable fonts, we need to set default variation values to get proper outlines
242            if font.is_variable() {
243                for axis in font.variation_axes() {
244                    font.set_variation(axis.tag, axis.def_value);
245                }
246            }
247
248            let mut builder = PathBuilder {
249                builder: tiny_skia_path::PathBuilder::new(),
250            };
251
252            font.outline_glyph(glyph_id, &mut builder)?;
253            builder.builder.finish()
254        })?
255    }
256
257    #[inline(never)]
258    fn outline_with_variations(
259        &self,
260        id: ID,
261        glyph_id: GlyphId,
262        variations: &[crate::FontVariation],
263        font_size: f32,
264        font_optical_sizing: crate::FontOpticalSizing,
265    ) -> Option<tiny_skia_path::Path> {
266        self.with_face_data(id, |data, face_index| -> Option<tiny_skia_path::Path> {
267            let mut font = ttf_parser::Face::parse(data, face_index).ok()?;
268
269            for v in variations {
270                font.set_variation(ttf_parser::Tag::from_bytes(&v.tag), v.value);
271            }
272
273            // Auto-set opsz if font-optical-sizing is auto and not explicitly set
274            if font_optical_sizing == crate::FontOpticalSizing::Auto {
275                let has_explicit_opsz = variations.iter().any(|v| v.tag == *b"opsz");
276                if !has_explicit_opsz {
277                    // Check if font has opsz axis
278                    if let Some(axes) = font.tables().fvar {
279                        let has_opsz_axis = axes
280                            .axes
281                            .into_iter()
282                            .any(|axis| axis.tag == ttf_parser::Tag::from_bytes(b"opsz"));
283                        if has_opsz_axis {
284                            font.set_variation(ttf_parser::Tag::from_bytes(b"opsz"), font_size);
285                        }
286                    }
287                }
288            }
289
290            let mut builder = PathBuilder {
291                builder: tiny_skia_path::PathBuilder::new(),
292            };
293
294            font.outline_glyph(glyph_id, &mut builder)?;
295            builder.builder.finish()
296        })?
297    }
298
299    fn has_opsz_axis(&self, id: ID) -> bool {
300        self.with_face_data(id, |data, face_index| -> Option<bool> {
301            let font = ttf_parser::Face::parse(data, face_index).ok()?;
302            let has_opsz = font.tables().fvar.map_or(false, |axes| {
303                axes.axes
304                    .into_iter()
305                    .any(|axis| axis.tag == ttf_parser::Tag::from_bytes(b"opsz"))
306            });
307            Some(has_opsz)
308        })
309        .flatten()
310        .unwrap_or(false)
311    }
312
313    fn raster(&self, id: ID, glyph_id: GlyphId) -> Option<BitmapImage> {
314        self.with_face_data(id, |data, face_index| -> Option<BitmapImage> {
315            let font = ttf_parser::Face::parse(data, face_index).ok()?;
316            let image = font.glyph_raster_image(glyph_id, u16::MAX)?;
317
318            if image.format == RasterImageFormat::PNG {
319                let bitmap_image = BitmapImage {
320                    image: Image {
321                        id: String::new(),
322                        visible: true,
323                        size: Size::from_wh(image.width as f32, image.height as f32)?,
324                        rendering_mode: ImageRendering::OptimizeQuality,
325                        kind: ImageKind::PNG(Arc::new(image.data.into())),
326                        abs_transform: Transform::default(),
327                        abs_bounding_box: NonZeroRect::from_xywh(
328                            0.0,
329                            0.0,
330                            image.width as f32,
331                            image.height as f32,
332                        )?,
333                    },
334                    x: image.x,
335                    y: image.y,
336                    pixels_per_em: image.pixels_per_em,
337                    glyph_bbox: font.glyph_bounding_box(glyph_id),
338                    // ttf-parser always checks sbix first, so if this table exists, it was used.
339                    is_sbix: font.tables().sbix.is_some(),
340                };
341
342                return Some(bitmap_image);
343            }
344
345            None
346        })?
347    }
348
349    fn svg(&self, id: ID, glyph_id: GlyphId) -> Option<Node> {
350        // TODO: Technically not 100% accurate because the SVG format in a OTF font
351        // is actually a subset/superset of a normal SVG, but it seems to work fine
352        // for Twitter Color Emoji, so might as well use what we already have.
353
354        // TODO: Glyph records can contain the data for multiple glyphs. We should
355        // add a cache so we don't need to reparse the data every time.
356        self.with_face_data(id, |data, face_index| -> Option<Node> {
357            let font = ttf_parser::Face::parse(data, face_index).ok()?;
358            let image = font.glyph_svg_image(glyph_id)?;
359            let tree = Tree::from_data(image.data, &Options::default()).ok()?;
360
361            // Twitter Color Emoji seems to always have one SVG record per glyph,
362            // while Noto Color Emoji sometimes contains multiple ones. It's kind of hacky,
363            // but the best we have for now.
364            let node = if image.start_glyph_id == image.end_glyph_id {
365                Node::Group(Box::new(tree.root))
366            } else {
367                tree.node_by_id(&format!("glyph{}", glyph_id.0))
368                    .log_none(|| {
369                        log::warn!("Failed to find SVG glyph node for glyph {}", glyph_id.0);
370                    })
371                    .cloned()?
372            };
373
374            Some(node)
375        })?
376    }
377
378    fn colr(&self, id: ID, glyph_id: GlyphId) -> Option<Tree> {
379        self.with_face_data(id, |data, face_index| -> Option<Tree> {
380            let face = ttf_parser::Face::parse(data, face_index).ok()?;
381
382            let mut svg = XmlWriter::new(xmlwriter::Options::default());
383
384            svg.start_element("svg");
385            svg.write_attribute("xmlns", "http://www.w3.org/2000/svg");
386            svg.write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
387
388            let mut path_buf = String::with_capacity(256);
389            let gradient_index = 1;
390            let clip_path_index = 1;
391
392            svg.start_element("g");
393
394            let mut glyph_painter = GlyphPainter {
395                face: &face,
396                svg: &mut svg,
397                path_buf: &mut path_buf,
398                gradient_index,
399                clip_path_index,
400                palette_index: 0,
401                transform: ttf_parser::Transform::default(),
402                outline_transform: ttf_parser::Transform::default(),
403                transforms_stack: vec![ttf_parser::Transform::default()],
404            };
405
406            face.paint_color_glyph(
407                glyph_id,
408                0,
409                RgbaColor::new(0, 0, 0, 255),
410                &mut glyph_painter,
411            )?;
412            svg.end_element();
413
414            Tree::from_data(svg.end_document().as_bytes(), &Options::default()).ok()
415        })?
416    }
417}