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 crate::GlyphId;
8use fontdb::{Database, ID};
9use skrifa::MetadataProvider;
10use skrifa::Tag;
11use skrifa::bitmap::{BitmapData, BitmapFormat};
12use skrifa::outline::{DrawSettings, OutlinePen};
13use skrifa::prelude::LocationRef;
14use skrifa::raw::TableProvider as _;
15use skrifa::raw::types::BoundingBox;
16use svgtypes::Color;
17use tiny_skia_path::{NonZeroRect, Size, Transform};
18use xmlwriter::XmlWriter;
19
20use crate::text::OPSZ;
21use crate::text::colr::GlyphPainter;
22use crate::*;
23
24fn resolve_rendering_mode(text: &Text) -> ShapeRendering {
25    match text.rendering_mode {
26        TextRendering::OptimizeSpeed => ShapeRendering::CrispEdges,
27        TextRendering::OptimizeLegibility => ShapeRendering::GeometricPrecision,
28        TextRendering::GeometricPrecision => ShapeRendering::GeometricPrecision,
29    }
30}
31
32/// Returns the effective variation settings for a glyph: the span's explicit
33/// variations plus an automatically computed `opsz` value when
34/// `font-optical-sizing: auto` is in effect and the font has an `opsz` axis
35/// that wasn't set explicitly. This matches browser behavior
36/// (CSS font-optical-sizing: auto).
37fn effective_variations(
38    cache: &mut Cache,
39    span: &layout::Span,
40    glyph: &layout::PositionedGlyph,
41) -> Vec<FontVariation> {
42    let mut variations = span.variations.clone();
43    if span.font_optical_sizing == crate::FontOpticalSizing::Auto
44        && !variations.iter().any(|v| &v.tag == b"opsz")
45        && cache.has_opsz_axis(glyph.font)
46    {
47        variations.push(FontVariation::new(*b"opsz", glyph.font_size()));
48    }
49    variations
50}
51
52fn push_outline_paths(
53    span: &layout::Span,
54    builder: &mut tiny_skia_path::PathBuilder,
55    new_children: &mut Vec<Node>,
56    rendering_mode: ShapeRendering,
57    abs_transform: Transform,
58) {
59    let builder = mem::replace(builder, tiny_skia_path::PathBuilder::new());
60
61    if let Some(path) = builder.finish().and_then(|p| {
62        Path::new(
63            String::new(),
64            span.visible,
65            span.fill.clone(),
66            span.stroke.clone(),
67            span.paint_order,
68            rendering_mode,
69            Arc::new(p),
70            abs_transform,
71        )
72    }) {
73        new_children.push(Node::Path(Box::new(path)));
74    }
75}
76
77pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZeroRect)> {
78    let mut new_children = vec![];
79
80    let abs_transform = text.abs_transform;
81    let rendering_mode = resolve_rendering_mode(text);
82
83    for span in &text.layouted {
84        if let Some(path) = span.overline.as_ref() {
85            let mut path = path.clone();
86            path.rendering_mode = rendering_mode;
87            new_children.push(Node::Path(Box::new(path)));
88        }
89
90        if let Some(path) = span.underline.as_ref() {
91            let mut path = path.clone();
92            path.rendering_mode = rendering_mode;
93            new_children.push(Node::Path(Box::new(path)));
94        }
95
96        // Instead of always processing each glyph separately, we always collect
97        // as many outline glyphs as possible by pushing them into the span_builder
98        // and only if we encounter a different glyph, or we reach the very end of the
99        // span to we push the actual outline paths into new_children. This way, we don't need
100        // to create a new path for every glyph if we have many consecutive glyphs
101        // with just outlines (which is the most common case).
102        let mut span_builder = tiny_skia_path::PathBuilder::new();
103
104        for glyph in &span.positioned_glyphs {
105            let variations = effective_variations(cache, span, glyph);
106
107            // A (best-effort conversion of a) COLR glyph.
108            if let Some(tree) = cache.fontdb_colr(glyph.font, glyph.id, &variations) {
109                let mut group = Group {
110                    transform: glyph.colr_transform(),
111                    ..Group::empty()
112                };
113                // TODO: Probably need to update abs_transform of children? Same
114                // for SVG and bitmap glyphs.
115                group.children.push(Node::Group(Box::new(tree.root)));
116                group.calculate_bounding_boxes();
117
118                new_children.push(Node::Group(Box::new(group)));
119            }
120            // An SVG glyph. Will return the usvg node containing the glyph descriptions.
121            else if let Some(node) = cache.fontdb_svg(glyph.font, glyph.id) {
122                push_outline_paths(
123                    span,
124                    &mut span_builder,
125                    &mut new_children,
126                    rendering_mode,
127                    abs_transform,
128                );
129
130                let mut group = Group {
131                    transform: glyph.svg_transform(),
132                    ..Group::empty()
133                };
134                group.children.push(node);
135                group.calculate_bounding_boxes();
136
137                new_children.push(Node::Group(Box::new(group)));
138            }
139            // A bitmap glyph.
140            else if let Some(img) = cache.fontdb_raster(glyph.font, glyph.id) {
141                push_outline_paths(
142                    span,
143                    &mut span_builder,
144                    &mut new_children,
145                    rendering_mode,
146                    abs_transform,
147                );
148
149                let transform = if img.is_sbix {
150                    glyph.sbix_transform(
151                        img.x as f32,
152                        img.y as f32,
153                        img.glyph_bbox.map(|bbox| bbox.x_min).unwrap_or(0) as f32,
154                        img.glyph_bbox.map(|bbox| bbox.y_min).unwrap_or(0) as f32,
155                        img.pixels_per_em as f32,
156                        img.image.size.height(),
157                    )
158                } else {
159                    glyph.cbdt_transform(img.x as f32, img.y as f32, img.pixels_per_em as f32)
160                };
161
162                let mut group = Group {
163                    transform,
164                    ..Group::empty()
165                };
166                group.children.push(Node::Image(Box::new(img.image)));
167                group.calculate_bounding_boxes();
168
169                new_children.push(Node::Group(Box::new(group)));
170            } else {
171                let outline = cache.fontdb_outline(glyph.font, glyph.id, &variations);
172
173                if let Some(outline) = outline.and_then(|p| p.transform(glyph.outline_transform()))
174                {
175                    span_builder.push_path(&outline);
176                }
177            }
178        }
179
180        push_outline_paths(
181            span,
182            &mut span_builder,
183            &mut new_children,
184            rendering_mode,
185            abs_transform,
186        );
187
188        if let Some(path) = span.line_through.as_ref() {
189            let mut path = path.clone();
190            path.rendering_mode = rendering_mode;
191            new_children.push(Node::Path(Box::new(path)));
192        }
193    }
194
195    let mut group = Group {
196        id: text.id.clone(),
197        ..Group::empty()
198    };
199
200    for child in new_children {
201        group.children.push(child);
202    }
203
204    group.calculate_bounding_boxes();
205    let stroke_bbox = group.stroke_bounding_box().to_non_zero_rect()?;
206    Some((group, stroke_bbox))
207}
208
209#[derive(Default)]
210struct PathBuilder {
211    builder: tiny_skia_path::PathBuilder,
212}
213
214impl OutlinePen for PathBuilder {
215    fn move_to(&mut self, x: f32, y: f32) {
216        self.builder.move_to(x, y);
217    }
218
219    fn line_to(&mut self, x: f32, y: f32) {
220        self.builder.line_to(x, y);
221    }
222
223    fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
224        self.builder.quad_to(cx0, cy0, x, y);
225    }
226
227    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
228        self.builder.cubic_to(cx0, cy0, cx1, cy1, x, y);
229    }
230
231    fn close(&mut self) {
232        self.builder.close();
233    }
234}
235
236pub(crate) trait DatabaseExt {
237    fn outline(
238        &self,
239        id: ID,
240        glyph_id: GlyphId,
241        variations: &[crate::FontVariation],
242    ) -> Option<tiny_skia_path::Path>;
243    fn has_opsz_axis(&self, id: ID) -> bool;
244    fn raster(&self, id: ID, glyph_id: GlyphId) -> Option<BitmapImage>;
245    fn svg(&self, id: ID, glyph_id: GlyphId) -> Option<Node>;
246    fn colr(&self, id: ID, glyph_id: GlyphId, variations: &[crate::FontVariation]) -> Option<Tree>;
247}
248
249#[derive(Clone)]
250pub(crate) struct BitmapImage {
251    image: Image,
252    x: i16,
253    y: i16,
254    pixels_per_em: u16,
255    glyph_bbox: Option<BoundingBox<i16>>,
256    is_sbix: bool,
257}
258
259impl DatabaseExt for Database {
260    #[inline(never)]
261    fn outline(
262        &self,
263        id: ID,
264        glyph_id: GlyphId,
265        variations: &[crate::FontVariation],
266    ) -> Option<tiny_skia_path::Path> {
267        self.with_face_data(id, |data, face_index| -> Option<tiny_skia_path::Path> {
268            let font = skrifa::FontRef::from_index(data, face_index).ok()?;
269            let outline = font.outline_glyphs().get(glyph_id.into())?;
270
271            let mut builder = PathBuilder::default();
272
273            let size = skrifa::prelude::Size::unscaled();
274            // An empty variation list resolves to the default value of every
275            // variation axis, which is what we want for non-variable fonts and
276            // for variable fonts used without variations.
277            let location = font.axes().location(
278                variations
279                    .iter()
280                    .map(|v| (Tag::from_be_bytes(v.tag), v.value)),
281            );
282            outline
283                .draw(DrawSettings::unhinted(size, &location), &mut builder)
284                .ok()?;
285
286            builder.builder.finish()
287        })?
288    }
289
290    fn has_opsz_axis(&self, id: ID) -> bool {
291        self.with_face_data(id, |data, face_index| -> Option<bool> {
292            let font = skrifa::FontRef::from_index(data, face_index).ok()?;
293            Some(font.axes().get_by_tag(OPSZ).is_some())
294        })
295        .flatten()
296        .unwrap_or(false)
297    }
298
299    fn raster(&self, id: ID, glyph_id: GlyphId) -> Option<BitmapImage> {
300        self.with_face_data(id, |data, face_index| -> Option<BitmapImage> {
301            let font = skrifa::FontRef::from_index(data, face_index).ok()?;
302            let bitmap_strikes = font.bitmap_strikes();
303
304            // We set size to unscaled to get the largest image available
305            let size = skrifa::prelude::Size::unscaled();
306            let location = LocationRef::default();
307            let image = bitmap_strikes.glyph_for_size(size, glyph_id.into())?;
308
309            match image.data {
310                BitmapData::Png(data) => {
311                    let metrics = font.glyph_metrics(size, location);
312                    let bounding_box = metrics.bounds(glyph_id.into()).map(|bbox| BoundingBox {
313                        x_min: bbox.x_min as i16,
314                        y_min: bbox.y_min as i16,
315                        x_max: bbox.x_max as i16,
316                        y_max: bbox.y_max as i16,
317                    });
318
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(data.to_vec())),
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.inner_bearing_x as i16,
335                        y: image.inner_bearing_y as i16,
336                        pixels_per_em: image.ppem_x as u16,
337                        glyph_bbox: bounding_box,
338                        is_sbix: bitmap_strikes.format() == Some(BitmapFormat::Sbix),
339                    };
340
341                    Some(bitmap_image)
342                }
343                // TODO: implement other bitmap formats
344                BitmapData::Bgra(_) | BitmapData::Mask(_) => None,
345            }
346        })?
347    }
348
349    fn svg(&self, id: ID, glyph_id: GlyphId) -> Option<Node> {
350        // SEE: https://docs.rs/read-fonts/latest/read_fonts/tables/svg/type.Svg.html
351
352        // TODO: Technically not 100% accurate because the SVG format in a OTF font
353        // is actually a subset/superset of a normal SVG, but it seems to work fine
354        // for Twitter Color Emoji, so might as well use what we already have.
355
356        // TODO: Glyph records can contain the data for multiple glyphs. We should
357        // add a cache so we don't need to reparse the data every time.
358        self.with_face_data(id, |data, face_index| -> Option<Node> {
359            let font = skrifa::FontRef::from_index(data, face_index).ok()?;
360            let svg_table = font.svg().ok()?;
361            let image_data = svg_table.glyph_data(glyph_id.into()).ok()??;
362            let tree = Tree::from_data(image_data, &Options::default()).ok()?;
363
364            // Twitter Color Emoji seems to always have one SVG record per glyph,
365            // while Noto Color Emoji sometimes contains multiple ones. It's kind of hacky,
366            // but the best we have for now.
367            let document_list = svg_table.svg_document_list().ok()?;
368            let doc_record = document_list.document_records().iter().find(|r| {
369                (r.start_glyph_id.get().to_u32()..=r.end_glyph_id.get().to_u32())
370                    .contains(&glyph_id.0)
371            })?;
372            let node = if doc_record.start_glyph_id == doc_record.end_glyph_id {
373                Node::Group(Box::new(tree.root))
374            } else {
375                tree.node_by_id(&format!("glyph{}", glyph_id.0))
376                    .log_none(|| {
377                        log::warn!("Failed to find SVG glyph node for glyph {}", glyph_id.0);
378                    })
379                    .cloned()?
380            };
381
382            Some(node)
383        })?
384    }
385
386    fn colr(&self, id: ID, glyph_id: GlyphId, variations: &[crate::FontVariation]) -> Option<Tree> {
387        self.with_face_data(id, |data, face_index| -> Option<Tree> {
388            let font = skrifa::FontRef::from_index(data, face_index).ok()?;
389
390            let location = font.axes().location(
391                variations
392                    .iter()
393                    .map(|v| (Tag::from_be_bytes(v.tag), v.value)),
394            );
395
396            let mut svg = XmlWriter::new(xmlwriter::Options::default());
397
398            svg.start_element("svg");
399            svg.write_attribute("xmlns", "http://www.w3.org/2000/svg");
400            svg.write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
401
402            let mut path_buf = String::with_capacity(256);
403            let gradient_index = 1;
404            let clip_path_index = 1;
405
406            svg.start_element("g");
407
408            let mut glyph_painter = GlyphPainter {
409                font: &font,
410                location: LocationRef::from(&location),
411                svg: &mut svg,
412                path_buf: &mut path_buf,
413                gradient_index,
414                clip_path_index,
415                foreground_color: Color::new_rgba(0, 0, 0, 255),
416                transform: skrifa::color::Transform::default(),
417                outline_transform: skrifa::color::Transform::default(),
418                transforms_stack: vec![skrifa::color::Transform::default()],
419                clip_stack: Vec::new(),
420            };
421
422            font.color_glyphs()
423                .get(glyph_id.into())?
424                .paint(&location, &mut glyph_painter)
425                .ok()?;
426            svg.end_element();
427
428            Tree::from_data(svg.end_document().as_bytes(), &Options::default()).ok()
429        })?
430    }
431}