Skip to content

Commit

Permalink
Split text layouting and text outlining into two separate steps.
Browse files Browse the repository at this point in the history
  • Loading branch information
LaurenzV committed Mar 29, 2024
1 parent 5b97397 commit efddc42
Show file tree
Hide file tree
Showing 11 changed files with 1,463 additions and 1,306 deletions.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions crates/usvg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,18 @@ and can focus just on the rendering part.
#![warn(missing_copy_implementations)]

mod parser;
#[cfg(feature = "text")]
mod text;
mod tree;
mod writer;

pub use parser::*;
#[cfg(feature = "text")]
pub use text::*;
pub use tree::*;

pub use roxmltree;

#[cfg(feature = "text")]
mod text_to_paths;
#[cfg(feature = "text")]
pub use fontdb;

Expand Down
10 changes: 5 additions & 5 deletions crates/usvg/src/parser/converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -804,17 +804,17 @@ fn convert_path(
let mut marker_group = Group::empty();
let mut marker_state = state.clone();

let bbox = tiny_skia_path.compute_tight_bounds().and_then(|r| r.to_non_zero_rect());
let bbox = tiny_skia_path
.compute_tight_bounds()
.and_then(|r| r.to_non_zero_rect());

let fill = fill.clone().map(|mut f| {
f.context_element =
Some(ContextElement::PathNode(path_transform, bbox));
f.context_element = Some(ContextElement::PathNode(path_transform, bbox));
f
});

let stroke = stroke.clone().map(|mut s| {
s.context_element =
Some(ContextElement::PathNode(path_transform, bbox));
s.context_element = Some(ContextElement::PathNode(path_transform, bbox));
s
});

Expand Down
59 changes: 59 additions & 0 deletions crates/usvg/src/parser/paint_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,12 @@ fn node_to_user_coordinates(
// paint servers.
let bbox = text.bounding_box;

// We need to update three things:
// 1. The fills/strokes of the original elements in the usvg tree.
// 2. The fills/strokes of the layouted elements of the text.
// 3. The fills/strokes of the outlined text.

// 1.
for chunk in &mut text.chunks {
for span in &mut chunk.spans {
process_fill(
Expand All @@ -670,6 +676,59 @@ fn node_to_user_coordinates(
}
}

// 2.
#[cfg(feature = "text")]
for span in &mut text.layouted {
process_fill(
&mut span.fill,
text.abs_transform,
context_transform,
context_bbox,
bbox,
cache,
);
process_stroke(
&mut span.stroke,
text.abs_transform,
context_transform,
context_bbox,
bbox,
cache,
);

let mut process_decoration = |path: &mut Path| {
process_fill(
&mut path.fill,
text.abs_transform,
context_transform,
context_bbox,
bbox,
cache,
);
process_stroke(
&mut path.stroke,
text.abs_transform,
context_transform,
context_bbox,
bbox,
cache,
);
};

if let Some(ref mut path) = span.overline {
process_decoration(path);
}

if let Some(ref mut path) = span.underline {
process_decoration(path);
}

if let Some(ref mut path) = span.line_through {
process_decoration(path);
}
}

// 3.
update_paint_servers(
&mut text.flattened,
context_transform,
Expand Down
8 changes: 3 additions & 5 deletions crates/usvg/src/parser/switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,9 @@ pub(crate) fn convert(
let child = node
.children()
.find(|n| is_condition_passed(*n, state.opt))?;
if let Some(g) =
converter::convert_group(node, state, false, cache, parent, &|cache, g| {
converter::convert_element(child, state, cache, g);
})
{
if let Some(g) = converter::convert_group(node, state, false, cache, parent, &|cache, g| {
converter::convert_element(child, state, cache, g);
}) {
parent.children.push(Node::Group(Box::new(g)));
}

Expand Down
3 changes: 2 additions & 1 deletion crates/usvg/src/parser/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,10 @@ pub(crate) fn convert(
stroke_bounding_box: dummy,
abs_stroke_bounding_box: dummy,
flattened: Box::new(Group::empty()),
layouted: vec![],
};

if crate::text_to_paths::convert(&mut text, state.fontdb).is_none() {
if text::convert(&mut text, state.fontdb).is_none() {
return;
}

Expand Down
26 changes: 8 additions & 18 deletions crates/usvg/src/parser/use_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,11 @@ pub(crate) fn convert(
let mut g = clip_element(node, clip_rect, orig_ts, &use_state, cache);

// Make group for `use`.
if let Some(mut g2) = converter::convert_group(
node,
&use_state,
true,
cache,
&mut g,
&|cache, g2| {
if let Some(mut g2) =
converter::convert_group(node, &use_state, true, cache, &mut g, &|cache, g2| {
convert_children(child, new_ts, &use_state, cache, false, g2);
},
) {
})
{
// We must reset transform, because it was already set
// to the group with clip-path.
g.is_context_element = true;
Expand Down Expand Up @@ -252,20 +247,15 @@ fn convert_children(
parent: &mut Group,
) {
let required = !transform.is_identity();
if let Some(mut g) = converter::convert_group(
node,
state,
required,
cache,
parent,
&|cache, g| {
if let Some(mut g) =
converter::convert_group(node, state, required, cache, parent, &|cache, g| {
if state.parent_clip_path.is_some() {
converter::convert_clip_path_elements(node, state, cache, g);
} else {
converter::convert_children(node, state, cache, g);
}
},
) {
})
{
g.is_context_element = is_context_element;
g.transform = transform;
parent.children.push(Node::Group(Box::new(g)));
Expand Down
133 changes: 133 additions & 0 deletions crates/usvg/src/text/flatten.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

use fontdb::{Database, ID};
use rustybuzz::ttf_parser;
use rustybuzz::ttf_parser::GlyphId;
use std::sync::Arc;
use tiny_skia_path::{NonZeroRect, Transform};

use crate::tree::BBox;
use crate::{Group, Node, Path, ShapeRendering, Text, TextRendering};

fn resolve_rendering_mode(text: &Text) -> ShapeRendering {
match text.rendering_mode {
TextRendering::OptimizeSpeed => ShapeRendering::CrispEdges,
TextRendering::OptimizeLegibility => ShapeRendering::GeometricPrecision,
TextRendering::GeometricPrecision => ShapeRendering::GeometricPrecision,
}
}

pub(crate) fn flatten(text: &mut Text, fontdb: &fontdb::Database) -> Option<(Group, NonZeroRect)> {
let mut new_paths = vec![];

let mut stroke_bbox = BBox::default();
let rendering_mode = resolve_rendering_mode(text);

for span in &text.layouted {
if let Some(path) = span.overline.as_ref() {
stroke_bbox = stroke_bbox.expand(path.data.bounds());
let mut path = path.clone();
path.rendering_mode = rendering_mode;
new_paths.push(path);
}

if let Some(path) = span.underline.as_ref() {
stroke_bbox = stroke_bbox.expand(path.data.bounds());
let mut path = path.clone();
path.rendering_mode = rendering_mode;
new_paths.push(path);
}

let mut span_builder = tiny_skia_path::PathBuilder::new();

for glyph in &span.positioned_glyphs {
if let Some(outline) = fontdb.outline(glyph.font, glyph.glyph_id) {
if let Some(outline) = outline.transform(glyph.transform) {
span_builder.push_path(&outline);
}
}
}

if let Some(path) = span_builder.finish().and_then(|p| {
Path::new(
String::new(),
span.visibility,
span.fill.clone(),
span.stroke.clone(),
span.paint_order,
rendering_mode,
Arc::new(p),
Transform::default(),
)
}) {
stroke_bbox = stroke_bbox.expand(path.stroke_bounding_box());
new_paths.push(path);
}

if let Some(path) = span.line_through.as_ref() {
stroke_bbox = stroke_bbox.expand(path.data.bounds());
let mut path = path.clone();
path.rendering_mode = rendering_mode;
new_paths.push(path);
}
}

let mut group = Group {
id: text.id.clone(),
..Group::empty()
};

for path in new_paths {
group.children.push(Node::Path(Box::new(path)));
}

group.calculate_bounding_boxes();
Some((group, stroke_bbox.to_non_zero_rect()?))
}

struct PathBuilder {
builder: tiny_skia_path::PathBuilder,
}

impl ttf_parser::OutlineBuilder for PathBuilder {
fn move_to(&mut self, x: f32, y: f32) {
self.builder.move_to(x, y);
}

fn line_to(&mut self, x: f32, y: f32) {
self.builder.line_to(x, y);
}

fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.builder.quad_to(x1, y1, x, y);
}

fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.builder.cubic_to(x1, y1, x2, y2, x, y);
}

fn close(&mut self) {
self.builder.close();
}
}

pub(crate) trait DatabaseExt {
fn outline(&self, id: ID, glyph_id: GlyphId) -> Option<tiny_skia_path::Path>;
}

impl DatabaseExt for Database {
#[inline(never)]
fn outline(&self, id: ID, glyph_id: GlyphId) -> Option<tiny_skia_path::Path> {
self.with_face_data(id, |data, face_index| -> Option<tiny_skia_path::Path> {
let font = ttf_parser::Face::parse(data, face_index).ok()?;

let mut builder = PathBuilder {
builder: tiny_skia_path::PathBuilder::new(),
};
font.outline_glyph(glyph_id, &mut builder)?;
builder.builder.finish()
})?
}
}

0 comments on commit efddc42

Please sign in to comment.