typst/src/layout/shaping.rs

130 lines
4.7 KiB
Rust

use fontdock::FaceId;
use rustybuzz::UnicodeBuffer;
use ttf_parser::GlyphId;
use super::{Element, Frame, ShapedText};
use crate::env::FontLoader;
use crate::exec::FontProps;
use crate::geom::{Point, Size};
/// Shape text into a frame containing [`ShapedText`] runs.
pub fn shape(text: &str, loader: &mut FontLoader, props: &FontProps) -> Frame {
let mut frame = Frame::new(Size::ZERO);
shape_segment(&mut frame, text, loader, props, props.families.iter(), None);
frame
}
/// Shape text into a frame with font fallback using the `families` iterator.
fn shape_segment<'a>(
frame: &mut Frame,
text: &str,
loader: &mut FontLoader,
props: &FontProps,
mut families: impl Iterator<Item = &'a str> + Clone,
mut first: Option<FaceId>,
) {
// Select the font family.
let (id, fallback) = loop {
// Try to load the next available font family.
match families.next() {
Some(family) => match loader.query(family, props.variant) {
Some(id) => break (id, true),
None => {}
},
// We're out of families, so we don't do any more fallback and just
// shape the tofus with the first face we originally used.
None => match first {
Some(id) => break (id, false),
None => return,
},
}
};
// Register that this is the first available font.
if first.is_none() {
first = Some(id);
}
// Find out some metrics and prepare the shaped text container.
let face = loader.face(id);
let ttf = face.ttf();
let units_per_em = f64::from(ttf.units_per_em().unwrap_or(1000));
let convert = |units| f64::from(units) / units_per_em * props.size;
let top = convert(i32::from(props.top_edge.lookup(ttf)));
let bottom = convert(i32::from(props.bottom_edge.lookup(ttf)));
let mut shaped = ShapedText::new(id, props.size, top, bottom, props.color);
// Fill the buffer with our text.
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
buffer.guess_segment_properties();
// Find out the text direction.
// TODO: Replace this once we do BiDi.
let rtl = matches!(buffer.direction(), rustybuzz::Direction::RightToLeft);
// Shape!
let glyphs = rustybuzz::shape(face.buzz(), &[], buffer);
let info = glyphs.glyph_infos();
let pos = glyphs.glyph_positions();
let mut iter = info.iter().zip(pos).peekable();
while let Some((info, pos)) = iter.next() {
// Do font fallback if the glyph is a tofu.
if info.codepoint == 0 && fallback {
// Flush what we have so far.
if !shaped.glyphs.is_empty() {
place(frame, shaped);
shaped = ShapedText::new(id, props.size, top, bottom, props.color);
}
// Determine the start and end cluster index of the tofu sequence.
let mut start = info.cluster as usize;
let mut end = info.cluster as usize;
while let Some((info, _)) = iter.peek() {
if info.codepoint != 0 {
break;
}
end = info.cluster as usize;
iter.next();
}
// Because Harfbuzz outputs glyphs in visual order, the start
// cluster actually corresponds to the last codepoint in
// right-to-left text.
if rtl {
assert!(end <= start);
std::mem::swap(&mut start, &mut end);
}
// The end cluster index points right before the last character that
// mapped to the tofu sequence. So we have to offset the end by one
// char.
let offset = text[end ..].chars().next().unwrap().len_utf8();
let range = start .. end + offset;
// Recursively shape the tofu sequence with the next family.
shape_segment(frame, &text[range], loader, props, families.clone(), first);
} else {
// Add the glyph to the shaped output.
// TODO: Don't ignore y_advance and y_offset.
let glyph = GlyphId(info.codepoint as u16);
shaped.glyphs.push(glyph);
shaped.offsets.push(shaped.width + convert(pos.x_offset));
shaped.width += convert(pos.x_advance);
}
}
if !shaped.glyphs.is_empty() {
place(frame, shaped)
}
}
/// Place shaped text into a frame.
fn place(frame: &mut Frame, shaped: ShapedText) {
let offset = frame.size.width;
frame.size.width += shaped.width;
frame.size.height = frame.size.height.max(shaped.top - shaped.bottom);
frame.push(Point::new(offset, shaped.top), Element::Text(shaped));
}