#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(missing_debug_implementations)]
#![warn(missing_copy_implementations)]
#![allow(clippy::many_single_char_names)]
#![allow(clippy::collapsible_else_if)]
#![allow(clippy::too_many_arguments)]
#![allow(clippy::neg_cmp_op_on_partial_ord)]
#![allow(clippy::identity_op)]
#![allow(clippy::question_mark)]
#![allow(clippy::upper_case_acronyms)]
pub use fontdb;
use std::cell::RefCell;
use std::collections::HashMap;
use std::num::NonZeroU16;
use std::rc::Rc;
use fontdb::{Database, ID};
use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv};
use rustybuzz::ttf_parser;
use ttf_parser::GlyphId;
use unicode_script::UnicodeScript;
use usvg_tree::*;
pub fn convert_text(root: &mut Group, fontdb: &fontdb::Database) {
for node in &mut root.children {
if let Node::Text(ref mut text) = node {
if let Some((node, bbox, stroke_bbox)) = convert_node(text, fontdb) {
text.bounding_box = Some(bbox);
text.stroke_bounding_box = Some(stroke_bbox);
text.flattened = Some(Box::new(node));
}
}
if let Node::Group(ref mut g) = node {
convert_text(g, fontdb);
}
node.subroots_mut(|subroot| convert_text(subroot, fontdb))
}
}
fn convert_node(
text: &Text,
fontdb: &fontdb::Database,
) -> Option<(Group, NonZeroRect, NonZeroRect)> {
let (new_paths, bbox, stroke_bbox) = text_to_paths(text, fontdb)?;
let mut group = Group {
id: text.id.clone(),
..Group::default()
};
let rendering_mode = resolve_rendering_mode(text);
for mut path in new_paths {
fix_obj_bounding_box(&mut path, bbox);
path.rendering_mode = rendering_mode;
group.children.push(Node::Path(Box::new(path)));
}
group.calculate_abs_transforms(Transform::identity());
Some((group, bbox, stroke_bbox))
}
trait DatabaseExt {
fn load_font(&self, id: ID) -> Option<ResolvedFont>;
fn outline(&self, id: ID, glyph_id: GlyphId) -> Option<tiny_skia_path::Path>;
fn has_char(&self, id: ID, c: char) -> bool;
}
impl DatabaseExt for Database {
#[inline(never)]
fn load_font(&self, id: ID) -> Option<ResolvedFont> {
self.with_face_data(id, |data, face_index| -> Option<ResolvedFont> {
let font = ttf_parser::Face::parse(data, face_index).ok()?;
let units_per_em = NonZeroU16::new(font.units_per_em())?;
let ascent = font.ascender();
let descent = font.descender();
let x_height = font
.x_height()
.and_then(|x| u16::try_from(x).ok())
.and_then(NonZeroU16::new);
let x_height = match x_height {
Some(height) => height,
None => {
u16::try_from((f32::from(ascent - descent) * 0.45) as i32)
.ok()
.and_then(NonZeroU16::new)?
}
};
let line_through = font.strikeout_metrics();
let line_through_position = match line_through {
Some(metrics) => metrics.position,
None => x_height.get() as i16 / 2,
};
let (underline_position, underline_thickness) = match font.underline_metrics() {
Some(metrics) => {
let thickness = u16::try_from(metrics.thickness)
.ok()
.and_then(NonZeroU16::new)
.unwrap_or_else(|| NonZeroU16::new(units_per_em.get() / 12).unwrap());
(metrics.position, thickness)
}
None => (
-(units_per_em.get() as i16) / 9,
NonZeroU16::new(units_per_em.get() / 12).unwrap(),
),
};
let mut subscript_offset = (units_per_em.get() as f32 / 0.2).round() as i16;
let mut superscript_offset = (units_per_em.get() as f32 / 0.4).round() as i16;
if let Some(metrics) = font.subscript_metrics() {
subscript_offset = metrics.y_offset;
}
if let Some(metrics) = font.superscript_metrics() {
superscript_offset = metrics.y_offset;
}
Some(ResolvedFont {
id,
units_per_em,
ascent,
descent,
x_height,
underline_position,
underline_thickness,
line_through_position,
subscript_offset,
superscript_offset,
})
})?
}
#[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()
})?
}
#[inline(never)]
fn has_char(&self, id: ID, c: char) -> bool {
let res = self.with_face_data(id, |font_data, face_index| -> Option<bool> {
let font = ttf_parser::Face::parse(font_data, face_index).ok()?;
font.glyph_index(c)?;
Some(true)
});
res == Some(Some(true))
}
}
#[derive(Clone, Copy, Debug)]
struct ResolvedFont {
id: ID,
units_per_em: NonZeroU16,
ascent: i16,
descent: i16,
x_height: NonZeroU16,
underline_position: i16,
underline_thickness: NonZeroU16,
line_through_position: i16,
subscript_offset: i16,
superscript_offset: i16,
}
impl ResolvedFont {
#[inline]
fn scale(&self, font_size: f32) -> f32 {
font_size / self.units_per_em.get() as f32
}
#[inline]
fn ascent(&self, font_size: f32) -> f32 {
self.ascent as f32 * self.scale(font_size)
}
#[inline]
fn descent(&self, font_size: f32) -> f32 {
self.descent as f32 * self.scale(font_size)
}
#[inline]
fn height(&self, font_size: f32) -> f32 {
self.ascent(font_size) - self.descent(font_size)
}
#[inline]
fn x_height(&self, font_size: f32) -> f32 {
self.x_height.get() as f32 * self.scale(font_size)
}
#[inline]
fn underline_position(&self, font_size: f32) -> f32 {
self.underline_position as f32 * self.scale(font_size)
}
#[inline]
fn underline_thickness(&self, font_size: f32) -> f32 {
self.underline_thickness.get() as f32 * self.scale(font_size)
}
#[inline]
fn line_through_position(&self, font_size: f32) -> f32 {
self.line_through_position as f32 * self.scale(font_size)
}
#[inline]
fn subscript_offset(&self, font_size: f32) -> f32 {
self.subscript_offset as f32 * self.scale(font_size)
}
#[inline]
fn superscript_offset(&self, font_size: f32) -> f32 {
self.superscript_offset as f32 * self.scale(font_size)
}
fn dominant_baseline_shift(&self, baseline: DominantBaseline, font_size: f32) -> f32 {
let alignment = match baseline {
DominantBaseline::Auto => AlignmentBaseline::Auto,
DominantBaseline::UseScript => AlignmentBaseline::Auto, DominantBaseline::NoChange => AlignmentBaseline::Auto, DominantBaseline::ResetSize => AlignmentBaseline::Auto, DominantBaseline::Ideographic => AlignmentBaseline::Ideographic,
DominantBaseline::Alphabetic => AlignmentBaseline::Alphabetic,
DominantBaseline::Hanging => AlignmentBaseline::Hanging,
DominantBaseline::Mathematical => AlignmentBaseline::Mathematical,
DominantBaseline::Central => AlignmentBaseline::Central,
DominantBaseline::Middle => AlignmentBaseline::Middle,
DominantBaseline::TextAfterEdge => AlignmentBaseline::TextAfterEdge,
DominantBaseline::TextBeforeEdge => AlignmentBaseline::TextBeforeEdge,
};
self.alignment_baseline_shift(alignment, font_size)
}
fn alignment_baseline_shift(&self, alignment: AlignmentBaseline, font_size: f32) -> f32 {
match alignment {
AlignmentBaseline::Auto => 0.0,
AlignmentBaseline::Baseline => 0.0,
AlignmentBaseline::BeforeEdge | AlignmentBaseline::TextBeforeEdge => {
self.ascent(font_size)
}
AlignmentBaseline::Middle => self.x_height(font_size) * 0.5,
AlignmentBaseline::Central => self.ascent(font_size) - self.height(font_size) * 0.5,
AlignmentBaseline::AfterEdge | AlignmentBaseline::TextAfterEdge => {
self.descent(font_size)
}
AlignmentBaseline::Ideographic => self.descent(font_size),
AlignmentBaseline::Alphabetic => 0.0,
AlignmentBaseline::Hanging => self.ascent(font_size) * 0.8,
AlignmentBaseline::Mathematical => self.ascent(font_size) * 0.5,
}
}
}
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();
}
}
#[derive(Clone, Copy, PartialEq)]
struct ByteIndex(usize);
impl ByteIndex {
fn new(i: usize) -> Self {
ByteIndex(i)
}
fn value(&self) -> usize {
self.0
}
fn code_point_at(&self, text: &str) -> usize {
text.char_indices()
.take_while(|(i, _)| *i != self.0)
.count()
}
fn char_from(&self, text: &str) -> char {
text[self.0..].chars().next().unwrap()
}
}
fn resolve_rendering_mode(text: &Text) -> ShapeRendering {
match text.rendering_mode {
TextRendering::OptimizeSpeed => ShapeRendering::CrispEdges,
TextRendering::OptimizeLegibility => ShapeRendering::GeometricPrecision,
TextRendering::GeometricPrecision => ShapeRendering::GeometricPrecision,
}
}
fn chunk_span_at(chunk: &TextChunk, byte_offset: ByteIndex) -> Option<&TextSpan> {
chunk
.spans
.iter()
.find(|&span| span_contains(span, byte_offset))
}
fn span_contains(span: &TextSpan, byte_offset: ByteIndex) -> bool {
byte_offset.value() >= span.start && byte_offset.value() < span.end
}
fn resolve_baseline(span: &TextSpan, font: &ResolvedFont, writing_mode: WritingMode) -> f32 {
let mut shift = -resolve_baseline_shift(&span.baseline_shift, font, span.font_size.get());
if writing_mode == WritingMode::LeftToRight {
if span.alignment_baseline == AlignmentBaseline::Auto
|| span.alignment_baseline == AlignmentBaseline::Baseline
{
shift += font.dominant_baseline_shift(span.dominant_baseline, span.font_size.get());
} else {
shift += font.alignment_baseline_shift(span.alignment_baseline, span.font_size.get());
}
}
shift
}
type FontsCache = HashMap<Font, Rc<ResolvedFont>>;
fn text_to_paths(
text_node: &Text,
fontdb: &fontdb::Database,
) -> Option<(Vec<Path>, NonZeroRect, NonZeroRect)> {
let mut fonts_cache: FontsCache = HashMap::new();
for chunk in &text_node.chunks {
for span in &chunk.spans {
if !fonts_cache.contains_key(&span.font) {
if let Some(font) = resolve_font(&span.font, fontdb) {
fonts_cache.insert(span.font.clone(), Rc::new(font));
}
}
}
}
let mut bbox = BBox::default();
let mut stroke_bbox = BBox::default();
let mut char_offset = 0;
let mut last_x = 0.0;
let mut last_y = 0.0;
let mut new_paths = Vec::new();
for chunk in &text_node.chunks {
let (x, y) = match chunk.text_flow {
TextFlow::Linear => (chunk.x.unwrap_or(last_x), chunk.y.unwrap_or(last_y)),
TextFlow::Path(_) => (0.0, 0.0),
};
let mut clusters = outline_chunk(chunk, &fonts_cache, fontdb);
if clusters.is_empty() {
char_offset += chunk.text.chars().count();
continue;
}
apply_writing_mode(text_node.writing_mode, &mut clusters);
apply_letter_spacing(chunk, &mut clusters);
apply_word_spacing(chunk, &mut clusters);
apply_length_adjust(chunk, &mut clusters);
let mut curr_pos = resolve_clusters_positions(
text_node,
chunk,
char_offset,
text_node.writing_mode,
&fonts_cache,
&mut clusters,
);
let mut text_ts = Transform::default();
if text_node.writing_mode == WritingMode::TopToBottom {
if let TextFlow::Linear = chunk.text_flow {
text_ts = text_ts.pre_rotate_at(90.0, x, y);
}
}
for span in &chunk.spans {
let font = match fonts_cache.get(&span.font) {
Some(v) => v,
None => continue,
};
let decoration_spans = collect_decoration_spans(span, &clusters);
let mut span_ts = text_ts;
span_ts = span_ts.pre_translate(x, y);
if let TextFlow::Linear = chunk.text_flow {
let shift = resolve_baseline(span, font, text_node.writing_mode);
span_ts = span_ts.pre_translate(0.0, shift);
}
if let Some(decoration) = span.decoration.underline.clone() {
let offset = match text_node.writing_mode {
WritingMode::LeftToRight => -font.underline_position(span.font_size.get()),
WritingMode::TopToBottom => font.height(span.font_size.get()) / 2.0,
};
if let Some(path) =
convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
{
bbox = bbox.expand(path.data.bounds());
stroke_bbox = stroke_bbox.expand(path.data.bounds());
new_paths.push(path);
}
}
if let Some(decoration) = span.decoration.overline.clone() {
let offset = match text_node.writing_mode {
WritingMode::LeftToRight => -font.ascent(span.font_size.get()),
WritingMode::TopToBottom => -font.height(span.font_size.get()) / 2.0,
};
if let Some(path) =
convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
{
bbox = bbox.expand(path.data.bounds());
stroke_bbox = stroke_bbox.expand(path.data.bounds());
new_paths.push(path);
}
}
if let Some((path, span_bbox)) = convert_span(span, &mut clusters, span_ts) {
bbox = bbox.expand(span_bbox);
if let Some(s_bbox) = path.calculate_stroke_bounding_box() {
stroke_bbox = stroke_bbox.expand(s_bbox)
}
new_paths.push(path);
}
if let Some(decoration) = span.decoration.line_through.clone() {
let offset = match text_node.writing_mode {
WritingMode::LeftToRight => -font.line_through_position(span.font_size.get()),
WritingMode::TopToBottom => 0.0,
};
if let Some(path) =
convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
{
bbox = bbox.expand(path.data.bounds());
stroke_bbox = stroke_bbox.expand(path.data.bounds());
new_paths.push(path);
}
}
}
char_offset += chunk.text.chars().count();
if text_node.writing_mode == WritingMode::TopToBottom {
if let TextFlow::Linear = chunk.text_flow {
std::mem::swap(&mut curr_pos.0, &mut curr_pos.1);
}
}
last_x = x + curr_pos.0;
last_y = y + curr_pos.1;
}
let bbox = bbox.to_non_zero_rect()?;
let stroke_bbox = stroke_bbox.to_non_zero_rect().unwrap_or(bbox);
Some((new_paths, bbox, stroke_bbox))
}
fn resolve_font(font: &Font, fontdb: &fontdb::Database) -> Option<ResolvedFont> {
let mut name_list = Vec::new();
for family in &font.families {
name_list.push(match family.as_str() {
"serif" => fontdb::Family::Serif,
"sans-serif" => fontdb::Family::SansSerif,
"cursive" => fontdb::Family::Cursive,
"fantasy" => fontdb::Family::Fantasy,
"monospace" => fontdb::Family::Monospace,
_ => fontdb::Family::Name(family),
});
}
name_list.push(fontdb::Family::Serif);
let stretch = match font.stretch {
FontStretch::UltraCondensed => fontdb::Stretch::UltraCondensed,
FontStretch::ExtraCondensed => fontdb::Stretch::ExtraCondensed,
FontStretch::Condensed => fontdb::Stretch::Condensed,
FontStretch::SemiCondensed => fontdb::Stretch::SemiCondensed,
FontStretch::Normal => fontdb::Stretch::Normal,
FontStretch::SemiExpanded => fontdb::Stretch::SemiExpanded,
FontStretch::Expanded => fontdb::Stretch::Expanded,
FontStretch::ExtraExpanded => fontdb::Stretch::ExtraExpanded,
FontStretch::UltraExpanded => fontdb::Stretch::UltraExpanded,
};
let style = match font.style {
FontStyle::Normal => fontdb::Style::Normal,
FontStyle::Italic => fontdb::Style::Italic,
FontStyle::Oblique => fontdb::Style::Oblique,
};
let query = fontdb::Query {
families: &name_list,
weight: fontdb::Weight(font.weight),
stretch,
style,
};
let id = fontdb.query(&query);
if id.is_none() {
log::warn!("No match for '{}' font-family.", font.families.join(", "));
}
fontdb.load_font(id?)
}
fn convert_span(
span: &TextSpan,
clusters: &mut [OutlinedCluster],
text_ts: Transform,
) -> Option<(Path, NonZeroRect)> {
let mut path_builder = tiny_skia_path::PathBuilder::new();
let mut bboxes_builder = tiny_skia_path::PathBuilder::new();
for cluster in clusters {
if !cluster.visible {
continue;
}
if span_contains(span, cluster.byte_idx) {
let path = cluster
.path
.take()
.and_then(|p| p.transform(cluster.transform));
if let Some(path) = path {
path_builder.push_path(&path);
}
let mut advance = cluster.advance;
if advance <= 0.0 {
advance = 1.0;
}
if let Some(r) = NonZeroRect::from_xywh(0.0, -cluster.ascent, advance, cluster.height())
{
if let Some(r) = r.transform(cluster.transform) {
bboxes_builder.push_rect(r.to_rect());
}
}
}
}
let mut path = path_builder.finish()?;
path = path.transform(text_ts)?;
let mut bboxes = bboxes_builder.finish()?;
bboxes = bboxes.transform(text_ts)?;
let mut fill = span.fill.clone();
if let Some(ref mut fill) = fill {
fill.rule = FillRule::NonZero;
}
let bbox = bboxes.compute_tight_bounds()?.to_non_zero_rect()?;
let path = Path {
id: String::new(),
visibility: span.visibility,
fill,
stroke: span.stroke.clone(),
paint_order: span.paint_order,
rendering_mode: ShapeRendering::default(),
data: Rc::new(path),
abs_transform: Transform::default(),
bounding_box: None,
stroke_bounding_box: None,
};
Some((path, bbox))
}
fn collect_decoration_spans(span: &TextSpan, clusters: &[OutlinedCluster]) -> Vec<DecorationSpan> {
let mut spans = Vec::new();
let mut started = false;
let mut width = 0.0;
let mut transform = Transform::default();
for cluster in clusters {
if span_contains(span, cluster.byte_idx) {
if started && cluster.has_relative_shift {
started = false;
spans.push(DecorationSpan { width, transform });
}
if !started {
width = cluster.advance;
started = true;
transform = cluster.transform;
} else {
width += cluster.advance;
}
} else if started {
spans.push(DecorationSpan { width, transform });
started = false;
}
}
if started {
spans.push(DecorationSpan { width, transform });
}
spans
}
fn convert_decoration(
dy: f32,
span: &TextSpan,
font: &ResolvedFont,
mut decoration: TextDecorationStyle,
decoration_spans: &[DecorationSpan],
transform: Transform,
) -> Option<Path> {
debug_assert!(!decoration_spans.is_empty());
let thickness = font.underline_thickness(span.font_size.get());
let mut builder = tiny_skia_path::PathBuilder::new();
for dec_span in decoration_spans {
let rect = match NonZeroRect::from_xywh(0.0, -thickness / 2.0, dec_span.width, thickness) {
Some(v) => v,
None => {
log::warn!("a decoration span has a malformed bbox");
continue;
}
};
let ts = dec_span.transform.pre_translate(0.0, dy);
let mut path = tiny_skia_path::PathBuilder::from_rect(rect.to_rect());
path = match path.transform(ts) {
Some(v) => v,
None => continue,
};
builder.push_path(&path);
}
let mut path_data = builder.finish()?;
path_data = path_data.transform(transform)?;
let mut path = Path::new(Rc::new(path_data));
path.visibility = span.visibility;
path.fill = decoration.fill.take();
path.stroke = decoration.stroke.take();
Some(path)
}
fn fix_obj_bounding_box(path: &mut Path, bbox: NonZeroRect) {
if let Some(ref mut fill) = path.fill {
if let Some(new_paint) = paint_server_to_user_space_on_use(fill.paint.clone(), bbox) {
fill.paint = new_paint;
}
}
if let Some(ref mut stroke) = path.stroke {
if let Some(new_paint) = paint_server_to_user_space_on_use(stroke.paint.clone(), bbox) {
stroke.paint = new_paint;
}
}
}
fn paint_server_to_user_space_on_use(paint: Paint, bbox: NonZeroRect) -> Option<Paint> {
if paint.units() != Some(Units::ObjectBoundingBox) {
return None;
}
let ts = Transform::from_bbox(bbox);
let paint = match paint {
Paint::Color(_) => paint,
Paint::LinearGradient(ref lg) => {
let transform = lg.transform.post_concat(ts);
Paint::LinearGradient(Rc::new(LinearGradient {
x1: lg.x1,
y1: lg.y1,
x2: lg.x2,
y2: lg.y2,
base: BaseGradient {
id: String::new(),
units: Units::UserSpaceOnUse,
transform,
spread_method: lg.spread_method,
stops: lg.stops.clone(),
},
}))
}
Paint::RadialGradient(ref rg) => {
let transform = rg.transform.post_concat(ts);
Paint::RadialGradient(Rc::new(RadialGradient {
cx: rg.cx,
cy: rg.cy,
r: rg.r,
fx: rg.fx,
fy: rg.fy,
base: BaseGradient {
id: String::new(),
units: Units::UserSpaceOnUse,
transform,
spread_method: rg.spread_method,
stops: rg.stops.clone(),
},
}))
}
Paint::Pattern(ref patt) => {
let transform = patt.borrow().transform.post_concat(ts);
Paint::Pattern(Rc::new(RefCell::new(Pattern {
id: String::new(),
units: Units::UserSpaceOnUse,
content_units: patt.borrow().content_units,
transform,
rect: patt.borrow().rect,
view_box: patt.borrow().view_box,
root: patt.borrow().root.clone(),
})))
}
};
Some(paint)
}
#[derive(Clone, Copy)]
struct DecorationSpan {
width: f32,
transform: Transform,
}
#[derive(Clone)]
struct Glyph {
id: GlyphId,
byte_idx: ByteIndex,
dx: i32,
dy: i32,
width: i32,
font: Rc<ResolvedFont>,
}
impl Glyph {
fn is_missing(&self) -> bool {
self.id.0 == 0
}
}
#[derive(Clone)]
struct OutlinedCluster {
byte_idx: ByteIndex,
codepoint: char,
width: f32,
advance: f32,
ascent: f32,
descent: f32,
x_height: f32,
has_relative_shift: bool,
path: Option<tiny_skia_path::Path>,
transform: Transform,
visible: bool,
}
impl OutlinedCluster {
fn height(&self) -> f32 {
self.ascent - self.descent
}
}
struct GlyphClusters<'a> {
data: &'a [Glyph],
idx: usize,
}
impl<'a> GlyphClusters<'a> {
fn new(data: &'a [Glyph]) -> Self {
GlyphClusters { data, idx: 0 }
}
}
impl<'a> Iterator for GlyphClusters<'a> {
type Item = (std::ops::Range<usize>, ByteIndex);
fn next(&mut self) -> Option<Self::Item> {
if self.idx == self.data.len() {
return None;
}
let start = self.idx;
let cluster = self.data[self.idx].byte_idx;
for g in &self.data[self.idx..] {
if g.byte_idx != cluster {
break;
}
self.idx += 1;
}
Some((start..self.idx, cluster))
}
}
fn outline_chunk(
chunk: &TextChunk,
fonts_cache: &FontsCache,
fontdb: &fontdb::Database,
) -> Vec<OutlinedCluster> {
let mut glyphs = Vec::new();
for span in &chunk.spans {
let font = match fonts_cache.get(&span.font) {
Some(v) => v.clone(),
None => continue,
};
let tmp_glyphs = shape_text(
&chunk.text,
font,
span.small_caps,
span.apply_kerning,
fontdb,
);
if glyphs.is_empty() {
glyphs = tmp_glyphs;
continue;
}
if glyphs.len() != tmp_glyphs.len() {
log::warn!("Text layouting failed.");
return Vec::new();
}
for (i, glyph) in tmp_glyphs.iter().enumerate() {
if span_contains(span, glyph.byte_idx) {
glyphs[i] = glyph.clone();
}
}
}
let mut clusters = Vec::new();
for (range, byte_idx) in GlyphClusters::new(&glyphs) {
if let Some(span) = chunk_span_at(chunk, byte_idx) {
clusters.push(outline_cluster(
&glyphs[range],
&chunk.text,
span.font_size.get(),
fontdb,
));
}
}
clusters
}
fn shape_text(
text: &str,
font: Rc<ResolvedFont>,
small_caps: bool,
apply_kerning: bool,
fontdb: &fontdb::Database,
) -> Vec<Glyph> {
let mut glyphs = shape_text_with_font(text, font.clone(), small_caps, apply_kerning, fontdb)
.unwrap_or_default();
let mut used_fonts = vec![font.id];
'outer: loop {
let mut missing = None;
for glyph in &glyphs {
if glyph.is_missing() {
missing = Some(glyph.byte_idx.char_from(text));
break;
}
}
if let Some(c) = missing {
let fallback_font = match find_font_for_char(c, &used_fonts, fontdb) {
Some(v) => Rc::new(v),
None => break 'outer,
};
let fallback_glyphs = shape_text_with_font(
text,
fallback_font.clone(),
small_caps,
apply_kerning,
fontdb,
)
.unwrap_or_default();
let all_matched = fallback_glyphs.iter().all(|g| !g.is_missing());
if all_matched {
glyphs = fallback_glyphs;
break 'outer;
}
if glyphs.len() != fallback_glyphs.len() {
break 'outer;
}
for i in 0..glyphs.len() {
if glyphs[i].is_missing() && !fallback_glyphs[i].is_missing() {
glyphs[i] = fallback_glyphs[i].clone();
}
}
used_fonts.push(fallback_font.id);
} else {
break 'outer;
}
}
for glyph in &glyphs {
if glyph.is_missing() {
let c = glyph.byte_idx.char_from(text);
log::warn!(
"No fonts with a {}/U+{:X} character were found.",
c,
c as u32
);
}
}
glyphs
}
fn shape_text_with_font(
text: &str,
font: Rc<ResolvedFont>,
small_caps: bool,
apply_kerning: bool,
fontdb: &fontdb::Database,
) -> Option<Vec<Glyph>> {
fontdb.with_face_data(font.id, |font_data, face_index| -> Option<Vec<Glyph>> {
let rb_font = rustybuzz::Face::from_slice(font_data, face_index)?;
let bidi_info = unicode_bidi::BidiInfo::new(text, Some(unicode_bidi::Level::ltr()));
let paragraph = &bidi_info.paragraphs[0];
let line = paragraph.range.clone();
let mut glyphs = Vec::new();
let (levels, runs) = bidi_info.visual_runs(paragraph, line);
for run in runs.iter() {
let sub_text = &text[run.clone()];
if sub_text.is_empty() {
continue;
}
let hb_direction = if levels[run.start].is_rtl() {
rustybuzz::Direction::RightToLeft
} else {
rustybuzz::Direction::LeftToRight
};
let mut buffer = rustybuzz::UnicodeBuffer::new();
buffer.push_str(sub_text);
buffer.set_direction(hb_direction);
let mut features = Vec::new();
if small_caps {
features.push(rustybuzz::Feature::new(
rustybuzz::Tag::from_bytes(b"smcp"),
1,
..,
));
}
if !apply_kerning {
features.push(rustybuzz::Feature::new(
rustybuzz::Tag::from_bytes(b"kern"),
0,
..,
));
}
let output = rustybuzz::shape(&rb_font, &features, buffer);
let positions = output.glyph_positions();
let infos = output.glyph_infos();
for (pos, info) in positions.iter().zip(infos) {
let idx = run.start + info.cluster as usize;
debug_assert!(text.get(idx..).is_some());
glyphs.push(Glyph {
byte_idx: ByteIndex::new(idx),
id: GlyphId(info.glyph_id as u16),
dx: pos.x_offset,
dy: pos.y_offset,
width: pos.x_advance,
font: font.clone(),
});
}
}
Some(glyphs)
})?
}
fn outline_cluster(
glyphs: &[Glyph],
text: &str,
font_size: f32,
db: &fontdb::Database,
) -> OutlinedCluster {
debug_assert!(!glyphs.is_empty());
let mut builder = tiny_skia_path::PathBuilder::new();
let mut width = 0.0;
let mut x: f32 = 0.0;
for glyph in glyphs {
let sx = glyph.font.scale(font_size);
if let Some(outline) = db.outline(glyph.font.id, glyph.id) {
let mut ts = Transform::from_scale(1.0, -1.0);
ts = ts.pre_scale(sx, sx);
ts = ts.pre_translate(x + glyph.dx as f32, glyph.dy as f32);
if let Some(outline) = outline.transform(ts) {
builder.push_path(&outline);
}
}
x += glyph.width as f32;
let glyph_width = glyph.width as f32 * sx;
if glyph_width > width {
width = glyph_width;
}
}
let byte_idx = glyphs[0].byte_idx;
let font = glyphs[0].font.clone();
OutlinedCluster {
byte_idx,
codepoint: byte_idx.char_from(text),
width,
advance: width,
ascent: font.ascent(font_size),
descent: font.descent(font_size),
x_height: font.x_height(font_size),
has_relative_shift: false,
path: builder.finish(),
transform: Transform::default(),
visible: true,
}
}
fn find_font_for_char(
c: char,
exclude_fonts: &[fontdb::ID],
fontdb: &fontdb::Database,
) -> Option<ResolvedFont> {
let base_font_id = exclude_fonts[0];
for face in fontdb.faces() {
if exclude_fonts.contains(&face.id) {
continue;
}
let base_face = fontdb.face(base_font_id)?;
if base_face.style != face.style
&& base_face.weight != face.weight
&& base_face.stretch != face.stretch
{
continue;
}
if !fontdb.has_char(face.id, c) {
continue;
}
let base_family = base_face
.families
.iter()
.find(|f| f.1 == fontdb::Language::English_UnitedStates)
.unwrap_or(&base_face.families[0]);
let new_family = face
.families
.iter()
.find(|f| f.1 == fontdb::Language::English_UnitedStates)
.unwrap_or(&base_face.families[0]);
log::warn!("Fallback from {} to {}.", base_family.0, new_family.0);
return fontdb.load_font(face.id);
}
None
}
fn resolve_clusters_positions(
text: &Text,
chunk: &TextChunk,
char_offset: usize,
writing_mode: WritingMode,
fonts_cache: &FontsCache,
clusters: &mut [OutlinedCluster],
) -> (f32, f32) {
match chunk.text_flow {
TextFlow::Linear => {
resolve_clusters_positions_horizontal(text, chunk, char_offset, writing_mode, clusters)
}
TextFlow::Path(ref path) => resolve_clusters_positions_path(
text,
chunk,
char_offset,
path,
writing_mode,
fonts_cache,
clusters,
),
}
}
fn resolve_clusters_positions_horizontal(
text: &Text,
chunk: &TextChunk,
offset: usize,
writing_mode: WritingMode,
clusters: &mut [OutlinedCluster],
) -> (f32, f32) {
let mut x = process_anchor(chunk.anchor, clusters_length(clusters));
let mut y = 0.0;
for cluster in clusters {
let cp = offset + cluster.byte_idx.code_point_at(&chunk.text);
if let (Some(dx), Some(dy)) = (text.dx.get(cp), text.dy.get(cp)) {
if writing_mode == WritingMode::LeftToRight {
x += dx;
y += dy;
} else {
y -= dx;
x += dy;
}
cluster.has_relative_shift = !dx.approx_zero_ulps(4) || !dy.approx_zero_ulps(4);
}
cluster.transform = cluster.transform.pre_translate(x, y);
if let Some(angle) = text.rotate.get(cp).cloned() {
if !angle.approx_zero_ulps(4) {
cluster.transform = cluster.transform.pre_rotate(angle);
cluster.has_relative_shift = true;
}
}
x += cluster.advance;
}
(x, y)
}
fn resolve_clusters_positions_path(
text: &Text,
chunk: &TextChunk,
char_offset: usize,
path: &TextPath,
writing_mode: WritingMode,
fonts_cache: &FontsCache,
clusters: &mut [OutlinedCluster],
) -> (f32, f32) {
let mut last_x = 0.0;
let mut last_y = 0.0;
let mut dy = 0.0;
let chunk_offset = match writing_mode {
WritingMode::LeftToRight => chunk.x.unwrap_or(0.0),
WritingMode::TopToBottom => chunk.y.unwrap_or(0.0),
};
let start_offset =
chunk_offset + path.start_offset + process_anchor(chunk.anchor, clusters_length(clusters));
let normals = collect_normals(text, chunk, clusters, &path.path, char_offset, start_offset);
for (cluster, normal) in clusters.iter_mut().zip(normals) {
let (x, y, angle) = match normal {
Some(normal) => (normal.x, normal.y, normal.angle),
None => {
cluster.visible = false;
continue;
}
};
cluster.has_relative_shift = true;
let orig_ts = cluster.transform;
let half_width = cluster.width / 2.0;
cluster.transform = Transform::default();
cluster.transform = cluster.transform.pre_translate(x - half_width, y);
cluster.transform = cluster.transform.pre_rotate_at(angle, half_width, 0.0);
let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text);
dy += text.dy.get(cp).cloned().unwrap_or(0.0);
let baseline_shift = chunk_span_at(chunk, cluster.byte_idx)
.map(|span| {
let font = match fonts_cache.get(&span.font) {
Some(v) => v,
None => return 0.0,
};
-resolve_baseline(span, font, writing_mode)
})
.unwrap_or(0.0);
if !dy.approx_zero_ulps(4) || !baseline_shift.approx_zero_ulps(4) {
let shift = kurbo::Vec2::new(0.0, (dy - baseline_shift) as f64);
cluster.transform = cluster
.transform
.pre_translate(shift.x as f32, shift.y as f32);
}
if let Some(angle) = text.rotate.get(cp).cloned() {
if !angle.approx_zero_ulps(4) {
cluster.transform = cluster.transform.pre_rotate(angle);
}
}
cluster.transform = cluster.transform.pre_concat(orig_ts);
last_x = x + cluster.advance;
last_y = y;
}
(last_x, last_y)
}
fn clusters_length(clusters: &[OutlinedCluster]) -> f32 {
clusters.iter().fold(0.0, |w, cluster| w + cluster.advance)
}
fn process_anchor(a: TextAnchor, text_width: f32) -> f32 {
match a {
TextAnchor::Start => 0.0, TextAnchor::Middle => -text_width / 2.0,
TextAnchor::End => -text_width,
}
}
struct PathNormal {
x: f32,
y: f32,
angle: f32,
}
fn collect_normals(
text: &Text,
chunk: &TextChunk,
clusters: &[OutlinedCluster],
path: &tiny_skia_path::Path,
char_offset: usize,
offset: f32,
) -> Vec<Option<PathNormal>> {
let mut offsets = Vec::with_capacity(clusters.len());
let mut normals = Vec::with_capacity(clusters.len());
{
let mut advance = offset;
for cluster in clusters {
let half_width = cluster.width / 2.0;
let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text);
advance += text.dx.get(cp).cloned().unwrap_or(0.0);
let offset = advance + half_width;
if offset < 0.0 {
normals.push(None);
}
offsets.push(offset as f64);
advance += cluster.advance;
}
}
let mut prev_mx = path.points()[0].x;
let mut prev_my = path.points()[0].y;
let mut prev_x = prev_mx;
let mut prev_y = prev_my;
fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez {
let line = kurbo::Line::new(
kurbo::Point::new(px as f64, py as f64),
kurbo::Point::new(x as f64, y as f64),
);
let p1 = line.eval(0.33);
let p2 = line.eval(0.66);
kurbo::CubicBez {
p0: line.p0,
p1,
p2,
p3: line.p1,
}
}
let mut length: f64 = 0.0;
for seg in path.segments() {
let curve = match seg {
tiny_skia_path::PathSegment::MoveTo(p) => {
prev_mx = p.x;
prev_my = p.y;
prev_x = p.x;
prev_y = p.y;
continue;
}
tiny_skia_path::PathSegment::LineTo(p) => {
create_curve_from_line(prev_x, prev_y, p.x, p.y)
}
tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez {
p0: kurbo::Point::new(prev_x as f64, prev_y as f64),
p1: kurbo::Point::new(p1.x as f64, p1.y as f64),
p2: kurbo::Point::new(p.x as f64, p.y as f64),
}
.raise(),
tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez {
p0: kurbo::Point::new(prev_x as f64, prev_y as f64),
p1: kurbo::Point::new(p1.x as f64, p1.y as f64),
p2: kurbo::Point::new(p2.x as f64, p2.y as f64),
p3: kurbo::Point::new(p.x as f64, p.y as f64),
},
tiny_skia_path::PathSegment::Close => {
create_curve_from_line(prev_x, prev_y, prev_mx, prev_my)
}
};
let arclen_accuracy = {
let base_arclen_accuracy = 0.5;
let (sx, sy) = text.abs_transform.get_scale();
base_arclen_accuracy / (sx * sy).sqrt().max(1.0)
};
let curve_len = curve.arclen(arclen_accuracy as f64);
for offset in &offsets[normals.len()..] {
if *offset >= length && *offset <= length + curve_len {
let mut offset = curve.inv_arclen(offset - length, arclen_accuracy as f64);
debug_assert!((-1.0e-3..=1.0 + 1.0e-3).contains(&offset));
offset = offset.min(1.0).max(0.0);
let pos = curve.eval(offset);
let d = curve.deriv().eval(offset);
let d = kurbo::Vec2::new(-d.y, d.x); let angle = d.atan2().to_degrees() - 90.0;
normals.push(Some(PathNormal {
x: pos.x as f32,
y: pos.y as f32,
angle: angle as f32,
}));
if normals.len() == offsets.len() {
break;
}
}
}
length += curve_len;
prev_x = curve.p3.x as f32;
prev_y = curve.p3.y as f32;
}
for _ in 0..(offsets.len() - normals.len()) {
normals.push(None);
}
normals
}
fn apply_letter_spacing(chunk: &TextChunk, clusters: &mut [OutlinedCluster]) {
if !chunk
.spans
.iter()
.any(|span| !span.letter_spacing.approx_zero_ulps(4))
{
return;
}
let num_clusters = clusters.len();
for (i, cluster) in clusters.iter_mut().enumerate() {
let script = cluster.codepoint.script();
if script_supports_letter_spacing(script) {
if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) {
if i != num_clusters - 1 {
cluster.advance += span.letter_spacing;
}
if !cluster.advance.is_valid_length() {
cluster.width = 0.0;
cluster.advance = 0.0;
cluster.path = None;
}
}
}
}
}
fn script_supports_letter_spacing(script: unicode_script::Script) -> bool {
use unicode_script::Script;
!matches!(
script,
Script::Arabic
| Script::Syriac
| Script::Nko
| Script::Manichaean
| Script::Psalter_Pahlavi
| Script::Mandaic
| Script::Mongolian
| Script::Phags_Pa
| Script::Devanagari
| Script::Bengali
| Script::Gurmukhi
| Script::Modi
| Script::Sharada
| Script::Syloti_Nagri
| Script::Tirhuta
| Script::Ogham
)
}
fn apply_word_spacing(chunk: &TextChunk, clusters: &mut [OutlinedCluster]) {
if !chunk
.spans
.iter()
.any(|span| !span.word_spacing.approx_zero_ulps(4))
{
return;
}
for cluster in clusters {
if is_word_separator_characters(cluster.codepoint) {
if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) {
cluster.advance += span.word_spacing;
}
}
}
}
fn is_word_separator_characters(c: char) -> bool {
matches!(
c as u32,
0x0020 | 0x00A0 | 0x1361 | 0x010100 | 0x010101 | 0x01039F | 0x01091F
)
}
fn apply_length_adjust(chunk: &TextChunk, clusters: &mut [OutlinedCluster]) {
let is_horizontal = matches!(chunk.text_flow, TextFlow::Linear);
for span in &chunk.spans {
let target_width = match span.text_length {
Some(v) => v,
None => continue,
};
let mut width = 0.0;
let mut cluster_indexes = Vec::new();
for i in span.start..span.end {
if let Some(index) = clusters.iter().position(|c| c.byte_idx.value() == i) {
cluster_indexes.push(index);
}
}
cluster_indexes.sort();
cluster_indexes.dedup();
for i in &cluster_indexes {
width += clusters[*i].width;
}
if cluster_indexes.is_empty() {
continue;
}
if span.length_adjust == LengthAdjust::Spacing {
let factor = if cluster_indexes.len() > 1 {
(target_width - width) / (cluster_indexes.len() - 1) as f32
} else {
0.0
};
for i in cluster_indexes {
clusters[i].advance = clusters[i].width + factor;
}
} else {
let factor = target_width / width;
if factor < 0.001 {
continue;
}
for i in cluster_indexes {
clusters[i].transform = clusters[i].transform.pre_scale(factor, 1.0);
if !is_horizontal {
clusters[i].advance *= factor;
clusters[i].width *= factor;
}
}
}
}
}
fn apply_writing_mode(writing_mode: WritingMode, clusters: &mut [OutlinedCluster]) {
if writing_mode != WritingMode::TopToBottom {
return;
}
for cluster in clusters {
let orientation = unicode_vo::char_orientation(cluster.codepoint);
if orientation == unicode_vo::Orientation::Upright {
let dy = cluster.width - cluster.height();
let mut ts = Transform::default();
ts = ts.pre_translate(cluster.width / 2.0, 0.0);
ts = ts.pre_rotate(-90.0);
ts = ts.pre_translate(-cluster.width / 2.0, -dy);
if let Some(path) = cluster.path.take() {
cluster.path = path.transform(ts);
}
cluster.ascent = cluster.width / 2.0;
cluster.descent = -cluster.width / 2.0;
} else {
cluster.transform = cluster.transform.pre_translate(0.0, cluster.x_height / 2.0);
}
}
}
fn resolve_baseline_shift(baselines: &[BaselineShift], font: &ResolvedFont, font_size: f32) -> f32 {
let mut shift = 0.0;
for baseline in baselines.iter().rev() {
match baseline {
BaselineShift::Baseline => {}
BaselineShift::Subscript => shift -= font.subscript_offset(font_size),
BaselineShift::Superscript => shift += font.superscript_offset(font_size),
BaselineShift::Number(n) => shift += n,
}
}
shift
}