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, fontdb: &fontdb::Database) -> 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 glyph in &span.positioned_glyphs {
75            // A (best-effort conversion of a) COLR glyph.
76            if let Some(tree) = fontdb.colr(glyph.font, glyph.id) {
77                let mut group = Group {
78                    transform: glyph.colr_transform(),
79                    ..Group::empty()
80                };
81                // TODO: Probably need to update abs_transform of children?
82                group.children.push(Node::Group(Box::new(tree.root)));
83                group.calculate_bounding_boxes();
84
85                new_children.push(Node::Group(Box::new(group)));
86            }
87            // An SVG glyph. Will return the usvg node containing the glyph descriptions.
88            else if let Some(node) = fontdb.svg(glyph.font, glyph.id) {
89                push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode);
90
91                let mut group = Group {
92                    transform: glyph.svg_transform(),
93                    ..Group::empty()
94                };
95                // TODO: Probably need to update abs_transform of children?
96                group.children.push(node);
97                group.calculate_bounding_boxes();
98
99                new_children.push(Node::Group(Box::new(group)));
100            }
101            // A bitmap glyph.
102            else if let Some(img) = fontdb.raster(glyph.font, glyph.id) {
103                push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode);
104
105                let transform = if img.is_sbix {
106                    glyph.sbix_transform(
107                        img.x as f32,
108                        img.y as f32,
109                        img.glyph_bbox.map(|bbox| bbox.x_min).unwrap_or(0) as f32,
110                        img.glyph_bbox.map(|bbox| bbox.y_min).unwrap_or(0) as f32,
111                        img.pixels_per_em as f32,
112                        img.image.size.height(),
113                    )
114                } else {
115                    glyph.cbdt_transform(
116                        img.x as f32,
117                        img.y as f32,
118                        img.pixels_per_em as f32,
119                        img.image.size.height(),
120                    )
121                };
122
123                let mut group = Group {
124                    transform,
125                    ..Group::empty()
126                };
127                group.children.push(Node::Image(Box::new(img.image)));
128                group.calculate_bounding_boxes();
129
130                new_children.push(Node::Group(Box::new(group)));
131            } else if let Some(outline) = fontdb
132                .outline(glyph.font, glyph.id)
133                .and_then(|p| p.transform(glyph.outline_transform()))
134            {
135                span_builder.push_path(&outline);
136            }
137        }
138
139        push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode);
140
141        if let Some(path) = span.line_through.as_ref() {
142            let mut path = path.clone();
143            path.rendering_mode = rendering_mode;
144            new_children.push(Node::Path(Box::new(path)));
145        }
146    }
147
148    let mut group = Group {
149        id: text.id.clone(),
150        ..Group::empty()
151    };
152
153    for child in new_children {
154        group.children.push(child);
155    }
156
157    group.calculate_bounding_boxes();
158    let stroke_bbox = group.stroke_bounding_box().to_non_zero_rect()?;
159    Some((group, stroke_bbox))
160}
161
162struct PathBuilder {
163    builder: tiny_skia_path::PathBuilder,
164}
165
166impl ttf_parser::OutlineBuilder for PathBuilder {
167    fn move_to(&mut self, x: f32, y: f32) {
168        self.builder.move_to(x, y);
169    }
170
171    fn line_to(&mut self, x: f32, y: f32) {
172        self.builder.line_to(x, y);
173    }
174
175    fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
176        self.builder.quad_to(x1, y1, x, y);
177    }
178
179    fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
180        self.builder.cubic_to(x1, y1, x2, y2, x, y);
181    }
182
183    fn close(&mut self) {
184        self.builder.close();
185    }
186}
187
188pub(crate) trait DatabaseExt {
189    fn outline(&self, id: ID, glyph_id: GlyphId) -> Option<tiny_skia_path::Path>;
190    fn raster(&self, id: ID, glyph_id: GlyphId) -> Option<BitmapImage>;
191    fn svg(&self, id: ID, glyph_id: GlyphId) -> Option<Node>;
192    fn colr(&self, id: ID, glyph_id: GlyphId) -> Option<Tree>;
193}
194
195pub(crate) struct BitmapImage {
196    image: Image,
197    x: i16,
198    y: i16,
199    pixels_per_em: u16,
200    glyph_bbox: Option<ttf_parser::Rect>,
201    is_sbix: bool,
202}
203
204impl DatabaseExt for Database {
205    #[inline(never)]
206    fn outline(&self, id: ID, glyph_id: GlyphId) -> Option<tiny_skia_path::Path> {
207        self.with_face_data(id, |data, face_index| -> Option<tiny_skia_path::Path> {
208            let font = ttf_parser::Face::parse(data, face_index).ok()?;
209
210            let mut builder = PathBuilder {
211                builder: tiny_skia_path::PathBuilder::new(),
212            };
213
214            font.outline_glyph(glyph_id, &mut builder)?;
215            builder.builder.finish()
216        })?
217    }
218
219    fn raster(&self, id: ID, glyph_id: GlyphId) -> Option<BitmapImage> {
220        self.with_face_data(id, |data, face_index| -> Option<BitmapImage> {
221            let font = ttf_parser::Face::parse(data, face_index).ok()?;
222            let image = font.glyph_raster_image(glyph_id, u16::MAX)?;
223
224            if image.format == RasterImageFormat::PNG {
225                let bitmap_image = BitmapImage {
226                    image: Image {
227                        id: String::new(),
228                        visible: true,
229                        size: Size::from_wh(image.width as f32, image.height as f32)?,
230                        rendering_mode: ImageRendering::OptimizeQuality,
231                        kind: ImageKind::PNG(Arc::new(image.data.into())),
232                        abs_transform: Transform::default(),
233                        abs_bounding_box: NonZeroRect::from_xywh(
234                            0.0,
235                            0.0,
236                            image.width as f32,
237                            image.height as f32,
238                        )?,
239                    },
240                    x: image.x,
241                    y: image.y,
242                    pixels_per_em: image.pixels_per_em,
243                    glyph_bbox: font.glyph_bounding_box(glyph_id),
244                    // ttf-parser always checks sbix first, so if this table exists, it was used.
245                    is_sbix: font.tables().sbix.is_some(),
246                };
247
248                return Some(bitmap_image);
249            }
250
251            None
252        })?
253    }
254
255    fn svg(&self, id: ID, glyph_id: GlyphId) -> Option<Node> {
256        // TODO: Technically not 100% accurate because the SVG format in a OTF font
257        // is actually a subset/superset of a normal SVG, but it seems to work fine
258        // for Twitter Color Emoji, so might as well use what we already have.
259
260        // TODO: Glyph records can contain the data for multiple glyphs. We should
261        // add a cache so we don't need to reparse the data every time.
262        self.with_face_data(id, |data, face_index| -> Option<Node> {
263            let font = ttf_parser::Face::parse(data, face_index).ok()?;
264            let image = font.glyph_svg_image(glyph_id)?;
265            let tree = Tree::from_data(image.data, &Options::default()).ok()?;
266
267            // Twitter Color Emoji seems to always have one SVG record per glyph,
268            // while Noto Color Emoji sometimes contains multiple ones. It's kind of hacky,
269            // but the best we have for now.
270            let node = if image.start_glyph_id == image.end_glyph_id {
271                Node::Group(Box::new(tree.root))
272            } else {
273                tree.node_by_id(&format!("glyph{}", glyph_id.0))
274                    .log_none(|| {
275                        log::warn!("Failed to find SVG glyph node for glyph {}", glyph_id.0)
276                    })
277                    .cloned()?
278            };
279
280            Some(node)
281        })?
282    }
283
284    fn colr(&self, id: ID, glyph_id: GlyphId) -> Option<Tree> {
285        self.with_face_data(id, |data, face_index| -> Option<Tree> {
286            let face = ttf_parser::Face::parse(data, face_index).ok()?;
287
288            let mut svg = XmlWriter::new(xmlwriter::Options::default());
289
290            svg.start_element("svg");
291            svg.write_attribute("xmlns", "http://www.w3.org/2000/svg");
292            svg.write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
293
294            let mut path_buf = String::with_capacity(256);
295            let gradient_index = 1;
296            let clip_path_index = 1;
297
298            svg.start_element("g");
299
300            let mut glyph_painter = GlyphPainter {
301                face: &face,
302                svg: &mut svg,
303                path_buf: &mut path_buf,
304                gradient_index,
305                clip_path_index,
306                palette_index: 0,
307                transform: ttf_parser::Transform::default(),
308                outline_transform: ttf_parser::Transform::default(),
309                transforms_stack: vec![ttf_parser::Transform::default()],
310            };
311
312            face.paint_color_glyph(
313                glyph_id,
314                0,
315                RgbaColor::new(0, 0, 0, 255),
316                &mut glyph_painter,
317            )?;
318            svg.end_element();
319
320            Tree::from_data(svg.end_document().as_bytes(), &Options::default()).ok()
321        })?
322    }
323}