Add evasion of glyph shape for under- and overlines

This commit is contained in:
Martin Haug 2022-02-03 15:21:12 +01:00
parent bd0d0e10d8
commit 9a9c6f22c4
9 changed files with 228 additions and 30 deletions

1
Cargo.lock generated
View File

@ -945,6 +945,7 @@ dependencies = [
"iai",
"image",
"itertools",
"kurbo",
"memmap2",
"miniz_oxide 0.4.4",
"once_cell",

View File

@ -26,6 +26,7 @@ once_cell = "1"
serde = { version = "1", features = ["derive"] }
# Text and font handling
kurbo = "0.8"
ttf-parser = "0.12"
rustybuzz = "0.4"
unicode-bidi = "0.3.5"

View File

@ -42,6 +42,18 @@ impl Frame {
self.elements.push((pos, element));
}
/// The layer the next item will be added on. This corresponds to the number
/// of elements in the frame.
pub fn layer(&self) -> usize {
self.elements.len()
}
/// Insert an element at the given layer in the Frame. This method panics if
/// the layer is greater than the number of layers present.
pub fn insert(&mut self, layer: usize, pos: Point, element: Element) {
self.elements.insert(layer, (pos, element));
}
/// Add a group element.
pub fn push_frame(&mut self, pos: Point, frame: Arc<Self>) {
self.elements.push((pos, Element::Group(Group::new(frame))));

View File

@ -15,6 +15,7 @@ impl<L: LineKind> DecoNode<L> {
thickness: args.named::<Linear>("thickness")?.or_else(|| args.find()),
offset: args.named("offset")?,
extent: args.named("extent")?.unwrap_or_default(),
evade: args.named("evade")?.unwrap_or(true),
};
Ok(args.expect::<Node>("body")?.styled(TextNode::LINES, vec![deco]))
}
@ -36,6 +37,9 @@ pub struct Decoration {
/// Amount that the line will be longer or shorter than its associated text
/// (dependent on scaled font size).
pub extent: Linear,
/// Whether the line skips sections in which it would collide
/// with the glyphs. Does not apply to strikethrough.
pub evade: bool,
}
impl From<DecoLine> for Decoration {
@ -46,6 +50,7 @@ impl From<DecoLine> for Decoration {
thickness: None,
offset: None,
extent: Linear::zero(),
evade: true,
}
}
}

View File

@ -5,8 +5,9 @@ use std::convert::TryInto;
use std::fmt::{self, Debug, Formatter};
use std::ops::{BitXor, Range};
use kurbo::{BezPath, Line, ParamCurve, Point as KPoint};
use rustybuzz::{Feature, UnicodeBuffer};
use ttf_parser::Tag;
use ttf_parser::{GlyphId, OutlineBuilder, Tag};
use super::prelude::*;
use super::{DecoLine, Decoration};
@ -812,37 +813,14 @@ impl<'a> ShapedText<'a> {
.collect();
let text = Text { face_id, size, fill, glyphs };
let text_layer = frame.layer();
let width = text.width();
frame.push(pos, Element::Text(text));
// Apply line decorations.
for deco in self.styles.get_cloned(TextNode::LINES) {
let face = fonts.get(face_id);
let metrics = match deco.line {
DecoLine::Underline => face.underline,
DecoLine::Strikethrough => face.strikethrough,
DecoLine::Overline => face.overline,
};
self.add_line_decos(
&mut frame, fonts, &text, face_id, size, fill, pos, width,
);
let extent = deco.extent.resolve(size);
let offset = deco
.offset
.map(|s| s.resolve(size))
.unwrap_or(-metrics.position.resolve(size));
let stroke = Stroke {
paint: deco.stroke.unwrap_or(fill),
thickness: deco
.thickness
.map(|s| s.resolve(size))
.unwrap_or(metrics.thickness.resolve(size)),
};
let subpos = Point::new(pos.x - extent, pos.y + offset);
let target = Point::new(width + 2.0 * extent, Length::zero());
let shape = Shape::stroked(Geometry::Line(target), stroke);
frame.push(subpos, Element::Shape(shape));
}
frame.insert(text_layer, pos, Element::Text(text));
offset += width;
}
@ -855,6 +833,155 @@ impl<'a> ShapedText<'a> {
frame
}
/// Add line decorations to a run of shaped text of a single font.
fn add_line_decos(
&self,
frame: &mut Frame,
fonts: &FontStore,
text: &Text,
face_id: FaceId,
size: Length,
fill: Paint,
pos: Point,
width: Length,
) {
// Apply line decorations.
for deco in self.styles.get_cloned(TextNode::LINES) {
let face = fonts.get(face_id);
let metrics = match deco.line {
DecoLine::Underline => face.underline,
DecoLine::Strikethrough => face.strikethrough,
DecoLine::Overline => face.overline,
};
let evade = deco.evade && deco.line != DecoLine::Strikethrough;
let extent = deco.extent.resolve(size);
let offset = deco
.offset
.map(|s| s.resolve(size))
.unwrap_or(-metrics.position.resolve(size));
let stroke = Stroke {
paint: deco.stroke.unwrap_or(fill),
thickness: deco
.thickness
.map(|s| s.resolve(size))
.unwrap_or(metrics.thickness.resolve(size)),
};
let line_y = pos.y + offset;
let gap_padding = size * 0.08;
let gaps = if evade {
let line = Line::new(
KPoint::new(pos.x.to_raw(), offset.to_raw()),
KPoint::new((pos.x + width).to_raw(), offset.to_raw()),
);
let mut x_advance = pos.x;
let mut intersections = vec![];
for glyph in text.glyphs.iter() {
let local_offset = glyph.x_offset.resolve(size) + x_advance;
let mut builder = KurboOutlineBuilder::new(
face.units_per_em,
size,
local_offset.to_raw(),
);
let bbox = face.ttf().outline_glyph(GlyphId(glyph.id), &mut builder);
x_advance += glyph.x_advance.resolve(size);
let path = match bbox {
Some(bbox) => {
let y_min = -face.to_em(bbox.y_max).resolve(size);
let y_max = -face.to_em(bbox.y_min).resolve(size);
// The line does not intersect the glyph, continue
// with the next one.
if offset < y_min || offset > y_max {
continue;
}
builder.finish()
}
None => continue,
};
// Collect all intersections of segments with the line and sort them.
intersections.extend(
path.segments()
.flat_map(|seg| seg.intersect_line(line))
.map(|is| Length::raw(line.eval(is.line_t).x)),
);
}
intersections.sort();
let mut gaps = vec![];
let mut inside = None;
// Alternate between outside and inside and collect the gaps
// into the gap vector.
for intersection in intersections {
match inside {
Some(start) => {
gaps.push((start, intersection));
inside = None;
}
None => inside = Some(intersection),
}
}
gaps
} else {
vec![]
};
let mut start = pos.x - extent;
let end = pos.x + (width + 2.0 * extent);
let min_width = 0.162 * size;
let mut push_segment = |from: Length, to: Length| {
let origin = Point::new(from, line_y);
let target = Point::new(to - from, Length::zero());
if target.x < min_width {
return;
}
let shape = Shape::stroked(Geometry::Line(target), stroke);
frame.push(origin, Element::Shape(shape));
};
if evade {
for gap in
gaps.into_iter().map(|(a, b)| (a - gap_padding, b + gap_padding))
{
if start >= end {
break;
}
if start >= gap.0 {
start = gap.1;
continue;
}
push_segment(start, gap.0);
start = gap.1;
}
}
if start < end {
push_segment(start, end);
}
}
}
/// Reshape a range of the shaped text, reusing information from this
/// shaping process if possible.
pub fn reshape(
@ -941,3 +1068,55 @@ enum Side {
Left,
Right,
}
struct KurboOutlineBuilder {
path: BezPath,
units_per_em: f64,
font_size: Length,
x_offset: f64,
}
impl KurboOutlineBuilder {
pub fn new(units_per_em: f64, font_size: Length, x_offset: f64) -> Self {
Self {
path: BezPath::new(),
units_per_em,
font_size,
x_offset,
}
}
pub fn finish(self) -> BezPath {
self.path
}
fn p(&self, x: f32, y: f32) -> KPoint {
KPoint::new(self.s(x) + self.x_offset, -self.s(y))
}
fn s(&self, v: f32) -> f64 {
Em::from_units(v, self.units_per_em).resolve(self.font_size).to_raw()
}
}
impl OutlineBuilder for KurboOutlineBuilder {
fn move_to(&mut self, x: f32, y: f32) {
self.path.move_to(self.p(x, y));
}
fn line_to(&mut self, x: f32, y: f32) {
self.path.line_to(self.p(x, y));
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.path.quad_to(self.p(x1, y1), self.p(x, y));
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.path.curve_to(self.p(x1, y1), self.p(x2, y2), self.p(x, y));
}
fn close(&mut self) {
self.path.close_path();
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -10,7 +10,7 @@
#underline(offset: 5pt)[Further below.]
// Different color.
#underline(red)[Critical information is conveyed here.]
#underline(red, evade: false)[Critical information is conveyed here.]
// Inherits font color.
#text(fill: red, underline[Change with the wind.])