Reshaping with unsafe-to-break

Co-Authored-By: Martin <mhaug@live.de>
This commit is contained in:
Laurenz 2021-04-05 22:32:09 +02:00
parent a86cf7bd8c
commit de20a21a58
13 changed files with 455 additions and 256 deletions

View File

@ -26,7 +26,7 @@ fontdock = { path = "../fontdock", default-features = false }
image = { version = "0.23", default-features = false, features = ["jpeg", "png"] }
miniz_oxide = "0.3"
pdf-writer = { path = "../pdf-writer" }
rustybuzz = "0.3"
rustybuzz = { git = "https://github.com/laurmaedje/rustybuzz" }
ttf-parser = "0.9"
unicode-bidi = "0.3"
unicode-xid = "0.2"

View File

@ -4,12 +4,19 @@ use std::fmt::{self, Display, Formatter};
use fontdock::FaceFromVec;
use crate::geom::Length;
/// An owned font face.
pub struct FaceBuf {
data: Box<[u8]>,
index: u32,
ttf: ttf_parser::Face<'static>,
buzz: rustybuzz::Face<'static>,
units_per_em: f64,
ascender: f64,
cap_height: f64,
x_height: f64,
descender: f64,
}
impl FaceBuf {
@ -36,6 +43,22 @@ impl FaceBuf {
// lifetime.
&self.buzz
}
/// Look up a vertical metric at a given font size.
pub fn vertical_metric(&self, size: Length, metric: VerticalFontMetric) -> Length {
self.convert(size, match metric {
VerticalFontMetric::Ascender => self.ascender,
VerticalFontMetric::CapHeight => self.cap_height,
VerticalFontMetric::XHeight => self.x_height,
VerticalFontMetric::Baseline => 0.0,
VerticalFontMetric::Descender => self.descender,
})
}
/// Convert from font units to a length at a given font size.
pub fn convert(&self, size: Length, units: impl Into<f64>) -> Length {
units.into() / self.units_per_em * size
}
}
impl FaceFromVec for FaceBuf {
@ -47,11 +70,26 @@ impl FaceFromVec for FaceBuf {
let slice: &'static [u8] =
unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) };
let ttf = ttf_parser::Face::from_slice(slice, index).ok()?;
let buzz = rustybuzz::Face::from_slice(slice, index)?;
// Look up some metrics we may need often.
let units_per_em = ttf.units_per_em().unwrap_or(1000);
let ascender = ttf.typographic_ascender().unwrap_or(ttf.ascender());
let cap_height = ttf.capital_height().filter(|&h| h > 0).unwrap_or(ascender);
let x_height = ttf.x_height().filter(|&h| h > 0).unwrap_or(ascender);
let descender = ttf.typographic_descender().unwrap_or(ttf.descender());
Some(Self {
data,
index,
ttf: ttf_parser::Face::from_slice(slice, index).ok()?,
buzz: rustybuzz::Face::from_slice(slice, index)?,
ttf,
buzz,
units_per_em: f64::from(units_per_em),
ascender: f64::from(ascender),
cap_height: f64::from(cap_height),
x_height: f64::from(x_height),
descender: f64::from(descender),
})
}
}
@ -77,38 +115,6 @@ pub enum VerticalFontMetric {
Descender,
}
impl VerticalFontMetric {
/// Look up the metric in the given font face.
pub fn lookup(self, face: &ttf_parser::Face) -> i16 {
match self {
VerticalFontMetric::Ascender => lookup_ascender(face),
VerticalFontMetric::CapHeight => face
.capital_height()
.filter(|&h| h > 0)
.unwrap_or_else(|| lookup_ascender(face)),
VerticalFontMetric::XHeight => face
.x_height()
.filter(|&h| h > 0)
.unwrap_or_else(|| lookup_ascender(face)),
VerticalFontMetric::Baseline => 0,
VerticalFontMetric::Descender => lookup_descender(face),
}
}
}
/// The ascender of the face.
fn lookup_ascender(face: &ttf_parser::Face) -> i16 {
// We prefer the typographic ascender over the Windows ascender because
// it can be overly large if the font has large glyphs.
face.typographic_ascender().unwrap_or_else(|| face.ascender())
}
/// The descender of the face.
fn lookup_descender(face: &ttf_parser::Face) -> i16 {
// See `lookup_ascender` for reason.
face.typographic_descender().unwrap_or_else(|| face.descender())
}
impl Display for VerticalFontMetric {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(match self {

View File

@ -40,62 +40,45 @@ impl Frame {
#[derive(Debug, Clone, PartialEq)]
pub enum Element {
/// Shaped text.
Text(ShapedText),
Text(Text),
/// A geometric shape.
Geometry(Geometry),
/// A raster image.
Image(Image),
}
/// A shaped run of text.
/// A run of shaped text.
#[derive(Debug, Clone, PartialEq)]
pub struct ShapedText {
/// The font face the text was shaped with.
pub face: FaceId,
pub struct Text {
/// The font face the glyphs are contained in.
pub face_id: FaceId,
/// The font size.
pub size: Length,
/// The width.
pub width: Length,
/// The extent to the top.
pub top: Length,
/// The extent to the bottom.
pub bottom: Length,
/// The glyph fill color / texture.
pub color: Fill,
/// The shaped glyphs.
pub glyphs: Vec<GlyphId>,
/// The horizontal offsets of the glyphs. This is indexed parallel to
/// `glyphs`. Vertical offsets are not yet supported.
pub offsets: Vec<Length>,
/// The glyphs.
pub glyphs: Vec<Glyph>,
}
impl ShapedText {
/// Create a new shape run with `width` zero and empty `glyphs` and `offsets`.
pub fn new(
face: FaceId,
size: Length,
top: Length,
bottom: Length,
color: Fill,
) -> Self {
Self {
face,
size,
width: Length::ZERO,
top,
bottom,
glyphs: vec![],
offsets: vec![],
color,
}
}
/// A glyph in a run of shaped text.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Glyph {
/// The glyph's ID in the face.
pub id: GlyphId,
/// The advance width of the glyph.
pub x_advance: Length,
/// The horizontal offset of the glyph.
pub x_offset: Length,
}
impl Text {
/// Encode the glyph ids into a big-endian byte buffer.
pub fn encode_glyphs_be(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(2 * self.glyphs.len());
for &GlyphId(g) in &self.glyphs {
bytes.push((g >> 8) as u8);
bytes.push((g & 0xff) as u8);
for glyph in &self.glyphs {
let id = glyph.id.0;
bytes.push((id >> 8) as u8);
bytes.push((id & 0xff) as u8);
}
bytes
}

View File

@ -1,4 +1,3 @@
use std::cmp::Ordering;
use std::fmt::{self, Debug, Formatter};
use std::mem;
@ -7,6 +6,7 @@ use xi_unicode::LineBreakIterator;
use super::*;
use crate::exec::FontProps;
use crate::util::RangeExt;
type Range = std::ops::Range<usize>;
@ -74,7 +74,6 @@ impl ParNode {
/// A paragraph representation in which children are already layouted and text
/// is separated into shapable runs.
#[derive(Debug)]
struct ParLayout<'a> {
/// The top-level direction.
dir: Dir,
@ -87,12 +86,11 @@ struct ParLayout<'a> {
}
/// A prepared item in a paragraph layout.
#[derive(Debug)]
enum ParItem<'a> {
/// Spacing between other items.
Spacing(Length),
/// A shaped text run with consistent direction.
Text(ShapeResult<'a>, Align),
Text(ShapedText<'a>, Align),
/// A layouted child node.
Frame(Frame, Align),
}
@ -151,24 +149,22 @@ impl<'a> ParLayout<'a> {
// TODO: Provide line break opportunities on alignment changes.
for (end, mandatory) in LineBreakIterator::new(self.bidi.text) {
let mut line = LineLayout::new(&self, start .. end, ctx);
let mut size = line.measure().0;
if !stack.areas.current.fits(size) {
if !stack.areas.current.fits(line.size) {
if let Some((last_line, last_end)) = last.take() {
stack.push(last_line);
start = last_end;
line = LineLayout::new(&self, start .. end, ctx);
size = line.measure().0;
}
}
if !stack.areas.current.height.fits(size.height)
if !stack.areas.current.height.fits(line.size.height)
&& !stack.areas.in_full_last()
{
stack.finish_area();
stack.finish_area(ctx);
}
if mandatory || !stack.areas.current.width.fits(size.width) {
if mandatory || !stack.areas.current.width.fits(line.size.width) {
stack.push(line);
start = end;
last = None;
@ -185,7 +181,7 @@ impl<'a> ParLayout<'a> {
stack.push(line);
}
stack.finish()
stack.finish(ctx)
}
/// Find the index of the item whose range contains the `text_offset`.
@ -200,7 +196,7 @@ impl ParItem<'_> {
pub fn measure(&self) -> (Size, Length) {
match self {
Self::Spacing(amount) => (Size::new(*amount, Length::ZERO), Length::ZERO),
Self::Text(shaped, _) => shaped.measure(),
Self::Text(shaped, _) => (shaped.size, shaped.baseline),
Self::Frame(frame, _) => (frame.size, frame.baseline),
}
}
@ -239,6 +235,8 @@ struct LineLayout<'a> {
items: &'a [ParItem<'a>],
last: Option<ParItem<'a>>,
ranges: &'a [Range],
size: Size,
baseline: Length,
}
impl<'a> LineLayout<'a> {
@ -265,7 +263,7 @@ impl<'a> LineLayout<'a> {
let end = line.end - range.start;
// Trim whitespace at the end of the line.
let end = start + shaped.text()[start .. end].trim_end().len();
let end = start + shaped.text[start .. end].trim_end().len();
line.end = range.start + end;
if start != end || rest.is_empty() {
@ -291,28 +289,32 @@ impl<'a> LineLayout<'a> {
items = rest;
}
Self { par, line, first, items, last, ranges }
}
/// Measure the size of the line without actually building its frame.
fn measure(&self) -> (Size, Length) {
let mut width = Length::ZERO;
let mut top = Length::ZERO;
let mut bottom = Length::ZERO;
for item in self.iter() {
for item in first.iter().chain(items).chain(&last) {
let (size, baseline) = item.measure();
width += size.width;
top = top.max(baseline);
bottom = bottom.max(size.height - baseline);
}
(Size::new(width, top + bottom), top)
Self {
par,
line,
first,
items,
last,
ranges,
size: Size::new(width, top + bottom),
baseline: top,
}
}
/// Build the line's frame.
fn build(&self, width: Length) -> Frame {
let (size, baseline) = self.measure();
fn build(&self, ctx: &mut LayoutContext, width: Length) -> Frame {
let (size, baseline) = (self.size, self.baseline);
let full_size = Size::new(size.width.max(width), size.height);
let mut output = Frame::new(full_size, baseline);
@ -325,7 +327,9 @@ impl<'a> LineLayout<'a> {
offset += amount;
return;
}
ParItem::Text(ref shaped, align) => (shaped.build(), align),
ParItem::Text(ref shaped, align) => {
(shaped.build(&mut ctx.env.fonts), align)
}
ParItem::Frame(ref frame, align) => (frame.clone(), align),
};
@ -400,18 +404,7 @@ impl<'a> LineLayout<'a> {
/// Find the range that contains the position.
fn find_range(ranges: &[Range], pos: usize) -> Option<usize> {
ranges.binary_search_by(|r| cmp(r, pos)).ok()
}
/// Comparison function for a range and a position used in binary search.
fn cmp(range: &Range, pos: usize) -> Ordering {
if pos < range.start {
Ordering::Greater
} else if pos < range.end {
Ordering::Equal
} else {
Ordering::Less
}
ranges.binary_search_by(|r| r.locate(pos)).ok()
}
/// Stacks lines into paragraph frames.
@ -435,19 +428,17 @@ impl<'a> LineStack<'a> {
}
fn push(&mut self, line: LineLayout<'a>) {
let size = line.measure().0;
self.size.width = self.size.width.max(size.width);
self.size.height += size.height;
self.size.width = self.size.width.max(line.size.width);
self.size.height += line.size.height;
if !self.lines.is_empty() {
self.size.height += self.line_spacing;
}
self.areas.current.height -= size.height + self.line_spacing;
self.areas.current.height -= line.size.height + self.line_spacing;
self.lines.push(line);
}
fn finish_area(&mut self) {
fn finish_area(&mut self, ctx: &mut LayoutContext) {
let expand = self.areas.expand.horizontal;
let full = self.areas.full.width;
self.size.width = expand.resolve(self.size.width, full);
@ -457,7 +448,7 @@ impl<'a> LineStack<'a> {
let mut first = true;
for line in mem::take(&mut self.lines) {
let frame = line.build(self.size.width);
let frame = line.build(ctx, self.size.width);
let height = frame.size.height;
if first {
@ -474,8 +465,8 @@ impl<'a> LineStack<'a> {
self.size = Size::ZERO;
}
fn finish(mut self) -> Vec<Frame> {
self.finish_area();
fn finish(mut self, ctx: &mut LayoutContext) -> Vec<Frame> {
self.finish_area(ctx);
self.finished
}
}

View File

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::fmt::{self, Debug, Formatter};
use std::ops::Range;
@ -5,86 +6,179 @@ use fontdock::FaceId;
use rustybuzz::UnicodeBuffer;
use ttf_parser::GlyphId;
use super::{Element, Frame, ShapedText};
use super::{Element, Frame, Glyph, Text};
use crate::env::FontLoader;
use crate::exec::FontProps;
use crate::font::FaceBuf;
use crate::geom::{Dir, Length, Point, Size};
use crate::util::SliceExt;
/// Shape text into a frame containing [`ShapedText`] runs.
pub fn shape<'a>(
text: &'a str,
dir: Dir,
loader: &mut FontLoader,
props: &'a FontProps,
) -> ShapeResult<'a> {
let iter = props.families.iter();
let mut results = vec![];
shape_segment(&mut results, text, dir, loader, props, iter, None);
/// The result of shaping text.
///
/// This type contains owned or borrowed shaped text runs, which can be
/// measured, used to reshape substrings more quickly and converted into a
/// frame.
pub struct ShapedText<'a> {
/// The text that was shaped.
pub text: &'a str,
/// The text direction.
pub dir: Dir,
/// The properties used for font selection.
pub props: &'a FontProps,
/// The font size.
pub size: Size,
/// The baseline from the top of the frame.
pub baseline: Length,
/// The shaped glyphs.
pub glyphs: Cow<'a, [ShapedGlyph]>,
}
let mut top = Length::ZERO;
let mut bottom = Length::ZERO;
for result in &results {
top = top.max(result.top);
bottom = bottom.max(result.bottom);
}
/// A single glyph resulting from shaping.
#[derive(Debug, Copy, Clone)]
pub struct ShapedGlyph {
/// The font face the glyph is contained in.
pub face_id: FaceId,
/// The glyph's ID in the face.
pub id: GlyphId,
/// The advance width of the glyph.
pub x_advance: i32,
/// The horizontal offset of the glyph.
pub x_offset: i32,
/// The start index of the glyph in the source text.
pub text_index: usize,
/// Whether splitting the shaping result before this glyph would yield the
/// same results as shaping the parts to both sides of `text_index`
/// separately.
pub safe_to_break: bool,
}
let mut frame = Frame::new(Size::new(Length::ZERO, top + bottom), top);
impl<'a> ShapedText<'a> {
/// Build the shaped text's frame.
pub fn build(&self, loader: &mut FontLoader) -> Frame {
let mut frame = Frame::new(self.size, self.baseline);
let mut x = Length::ZERO;
for shaped in results {
let offset = frame.size.width;
frame.size.width += shaped.width;
for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
let face = loader.face(face_id);
if !shaped.glyphs.is_empty() {
frame.push(Point::new(offset, top), Element::Text(shaped));
let pos = Point::new(x, self.baseline);
let mut text = Text {
face_id,
size: self.props.size,
color: self.props.color,
glyphs: vec![],
};
for glyph in group {
let x_advance = face.convert(self.props.size, glyph.x_advance);
let x_offset = face.convert(self.props.size, glyph.x_offset);
text.glyphs.push(Glyph { id: glyph.id, x_advance, x_offset });
x += x_advance;
}
frame.push(pos, Element::Text(text));
}
frame
}
ShapeResult { frame, text, dir, props }
}
#[derive(Clone)]
pub struct ShapeResult<'a> {
frame: Frame,
text: &'a str,
dir: Dir,
props: &'a FontProps,
}
impl<'a> ShapeResult<'a> {
/// Reshape a range of the shaped text, reusing information from this
/// shaping process if possible.
pub fn reshape(
&self,
range: Range<usize>,
&'a self,
text_range: Range<usize>,
loader: &mut FontLoader,
) -> ShapeResult<'_> {
if range.start == 0 && range.end == self.text.len() {
self.clone()
) -> ShapedText<'a> {
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
let (size, baseline) = measure(glyphs, loader, self.props);
Self {
text: &self.text[text_range],
dir: self.dir,
props: self.props,
size,
baseline,
glyphs: Cow::Borrowed(glyphs),
}
} else {
shape(&self.text[range], self.dir, loader, self.props)
shape(&self.text[text_range], self.dir, loader, self.props)
}
}
pub fn text(&self) -> &'a str {
self.text
/// Find the subslice of glyphs that represent the given text range if both
/// sides are safe to break.
fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> {
let mut start = self.find_safe_to_break(text_range.start)?;
let mut end = self.find_safe_to_break(text_range.end)?;
if !self.dir.is_positive() {
std::mem::swap(&mut start, &mut end);
}
// TODO: Expand to left and right if necessary because
// find_safe_to_break may find any glyph with the text_index.
Some(&self.glyphs[start .. end])
}
pub fn measure(&self) -> (Size, Length) {
(self.frame.size, self.frame.baseline)
}
/// Find the glyph slice offset at the text index if it's safe to break.
fn find_safe_to_break(&self, text_index: usize) -> Option<usize> {
let ltr = self.dir.is_positive();
pub fn build(&self) -> Frame {
self.frame.clone()
// Handle edge cases.
let len = self.glyphs.len();
if text_index == 0 {
return Some(if ltr { 0 } else { len });
} else if text_index == self.text.len() {
return Some(if ltr { len } else { 0 });
}
// TODO: Do binary search. Take care that RTL needs reversed ordering.
let idx = self
.glyphs
.iter()
.position(|g| g.text_index == text_index)
.filter(|&i| self.glyphs[i].safe_to_break)?;
// RTL needs offset one because the the start of the range should
// be exclusive and the end inclusive.
Some(if ltr { idx } else { idx + 1 })
}
}
impl Debug for ShapeResult<'_> {
impl Debug for ShapedText<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Shaped({:?})", self.text)
}
}
/// Shape text into a frame with font fallback using the `families` iterator.
/// Shape text into [`ShapedText`].
pub fn shape<'a>(
text: &'a str,
dir: Dir,
loader: &mut FontLoader,
props: &'a FontProps,
) -> ShapedText<'a> {
let mut glyphs = vec![];
let families = props.families.iter();
if !text.is_empty() {
shape_segment(&mut glyphs, 0, text, dir, loader, props, families, None);
}
let (size, baseline) = measure(&glyphs, loader, props);
ShapedText {
text,
dir,
props,
size,
baseline,
glyphs: Cow::Owned(glyphs),
}
}
/// Shape text with font fallback using the `families` iterator.
fn shape_segment<'a>(
results: &mut Vec<ShapedText>,
glyphs: &mut Vec<ShapedGlyph>,
base: usize,
text: &str,
dir: Dir,
loader: &mut FontLoader,
@ -93,7 +187,7 @@ fn shape_segment<'a>(
mut first: Option<FaceId>,
) {
// Select the font family.
let (id, fallback) = loop {
let (face_id, fallback) = loop {
// Try to load the next available font family.
match families.next() {
Some(family) => match loader.query(family, props.variant) {
@ -111,22 +205,7 @@ fn shape_segment<'a>(
// 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);
// For empty text, we want a zero-width box with the correct height.
if text.is_empty() {
results.push(shaped);
return;
first = Some(face_id);
}
// Fill the buffer with our text.
@ -139,59 +218,117 @@ fn shape_segment<'a>(
});
// 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();
let buffer = rustybuzz::shape(loader.face(face_id).buzz(), &[], buffer);
let infos = buffer.glyph_infos();
let pos = buffer.glyph_positions();
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() {
results.push(shaped);
shaped = ShapedText::new(id, props.size, top, bottom, props.color);
}
// Collect the shaped glyphs, reshaping with the next font if necessary.
let mut i = 0;
while i < infos.len() {
let info = &infos[i];
let cluster = info.cluster as usize;
// 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 !dir.is_positive() {
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;
let part = &text[range];
// Recursively shape the tofu sequence with the next family.
shape_segment(results, part, dir, loader, props, families.clone(), first);
} else {
if info.codepoint != 0 || !fallback {
// 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);
glyphs.push(ShapedGlyph {
face_id,
id: GlyphId(info.codepoint as u16),
x_advance: pos[i].x_advance,
x_offset: pos[i].x_offset,
text_index: base + cluster,
safe_to_break: !info.unsafe_to_break(),
});
} else {
// Do font fallback if the glyph is a tofu.
//
// First, search for the end of the tofu sequence.
let k = i;
while infos.get(i + 1).map_or(false, |info| info.codepoint == 0) {
i += 1;
}
// Determine the source text range for the tofu sequence.
let range = {
// Examples
//
// Here, _ is a tofu.
// Note that the glyph cluster length is greater than 1 char!
//
// Left-to-right clusters:
// h a l i h a l l o
// A _ _ C E
// 0 2 4 6 8
//
// Right-to-left clusters:
// O L L A H I L A H
// E C _ _ A
// 8 6 4 2 0
let ltr = dir.is_positive();
let first = if ltr { k } else { i };
let start = infos[first].cluster as usize;
let last = if ltr { i.checked_add(1) } else { k.checked_sub(1) };
let end = last
.and_then(|last| infos.get(last))
.map(|info| info.cluster as usize)
.unwrap_or(text.len());
start .. end
};
// Recursively shape the tofu sequence with the next family.
shape_segment(
glyphs,
base + range.start,
&text[range],
dir,
loader,
props,
families.clone(),
first,
);
}
i += 1;
}
}
/// Measure the size and baseline of a run of shaped glyphs with the given
/// properties.
fn measure(
glyphs: &[ShapedGlyph],
loader: &mut FontLoader,
props: &FontProps,
) -> (Size, Length) {
let mut top = Length::ZERO;
let mut bottom = Length::ZERO;
let mut width = Length::ZERO;
let mut vertical = |face: &FaceBuf| {
top = top.max(face.vertical_metric(props.size, props.top_edge));
bottom = bottom.max(-face.vertical_metric(props.size, props.bottom_edge));
};
if glyphs.is_empty() {
// When there are no glyphs, we just use the vertical metrics of the
// first available font.
for family in props.families.iter() {
if let Some(face_id) = loader.query(family, props.variant) {
vertical(loader.face(face_id));
break;
}
}
} else {
for (face_id, group) in glyphs.group_by_key(|g| g.face_id) {
let face = loader.face(face_id);
vertical(face);
for glyph in group {
width += face.convert(props.size, glyph.x_advance);
}
}
}
if !shaped.glyphs.is_empty() {
results.push(shaped);
}
(Size::new(width, top + bottom), top)
}

View File

@ -44,6 +44,7 @@ pub mod parse;
pub mod pdf;
pub mod pretty;
pub mod syntax;
pub mod util;
use crate::diag::Pass;
use crate::env::Env;

View File

@ -50,7 +50,7 @@ impl<'a> PdfExporter<'a> {
for frame in frames {
for (_, element) in &frame.elements {
match element {
Element::Text(shaped) => fonts.insert(shaped.face),
Element::Text(shaped) => fonts.insert(shaped.face_id),
Element::Image(image) => {
let img = env.resources.loaded::<ImageResource>(image.res);
if img.buf.color().has_alpha() {
@ -187,11 +187,11 @@ impl<'a> PdfExporter<'a> {
// Then, also check if we need to issue a font switching
// action.
if shaped.face != face || shaped.size != size {
face = shaped.face;
if shaped.face_id != face || shaped.size != size {
face = shaped.face_id;
size = shaped.size;
let name = format!("F{}", self.fonts.map(shaped.face));
let name = format!("F{}", self.fonts.map(shaped.face_id));
text.font(Name(name.as_bytes()), size.to_pt() as f32);
}

80
src/util.rs Normal file
View File

@ -0,0 +1,80 @@
/// Utilities.
use std::cmp::Ordering;
use std::ops::Range;
/// Additional methods for slices.
pub trait SliceExt<T> {
/// Split a slice into consecutive groups with the same key.
///
/// Returns an iterator of pairs of a key and the group with that key.
fn group_by_key<K, F>(&self, f: F) -> GroupByKey<'_, T, F>
where
F: FnMut(&T) -> K,
K: PartialEq;
}
impl<T> SliceExt<T> for [T] {
fn group_by_key<K, F>(&self, f: F) -> GroupByKey<'_, T, F>
where
F: FnMut(&T) -> K,
K: PartialEq,
{
GroupByKey { slice: self, f }
}
}
/// This struct is produced by [`SliceExt::group_by_key`].
pub struct GroupByKey<'a, T, F> {
slice: &'a [T],
f: F,
}
impl<'a, T, K, F> Iterator for GroupByKey<'a, T, F>
where
F: FnMut(&T) -> K,
K: PartialEq,
{
type Item = (K, &'a [T]);
fn next(&mut self) -> Option<Self::Item> {
let first = self.slice.first()?;
let key = (self.f)(first);
let mut i = 1;
while self.slice.get(i).map_or(false, |t| (self.f)(t) == key) {
i += 1;
}
let (head, tail) = self.slice.split_at(i);
self.slice = tail;
Some((key, head))
}
}
/// Additional methods for [`Range<usize>`].
pub trait RangeExt {
/// Locate a position relative to a range.
///
/// This can be used for binary searching the range that contains the
/// position as follows:
/// ```
/// # use typst::util::RangeExt;
/// assert_eq!(
/// [1..2, 2..7, 7..10].binary_search_by(|r| r.locate(5)),
/// Ok(1),
/// );
/// ```
fn locate(&self, pos: usize) -> Ordering;
}
impl RangeExt for Range<usize> {
fn locate(&self, pos: usize) -> Ordering {
if pos < self.start {
Ordering::Greater
} else if pos < self.end {
Ordering::Equal
} else {
Ordering::Less
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -20,7 +20,7 @@ use typst::env::{Env, FsIndexExt, ImageResource, ResourceLoader};
use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value};
use typst::exec::State;
use typst::geom::{self, Length, Point, Sides, Size};
use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, ShapedText};
use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, Text};
use typst::library;
use typst::parse::{LineMap, Scanner};
use typst::pdf;
@ -413,19 +413,20 @@ fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap {
canvas
}
fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &ShapedText) {
let ttf = env.fonts.face(shaped.face).ttf();
fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &Text) {
let ttf = env.fonts.face(shaped.face_id).ttf();
let mut x = 0.0;
for (&glyph, &offset) in shaped.glyphs.iter().zip(&shaped.offsets) {
for glyph in &shaped.glyphs {
let units_per_em = ttf.units_per_em().unwrap_or(1000);
let x = offset.to_pt() as f32;
let s = (shaped.size / units_per_em as f64).to_pt() as f32;
let ts = ts.pre_translate(x, 0.0);
let dx = glyph.x_offset.to_pt() as f32;
let ts = ts.pre_translate(x + dx, 0.0);
// Try drawing SVG if present.
if let Some(tree) = ttf
.glyph_svg_image(glyph)
.glyph_svg_image(glyph.id)
.and_then(|data| std::str::from_utf8(data).ok())
.map(|svg| {
let viewbox = format!("viewBox=\"0 0 {0} {0}\" xmlns", units_per_em);
@ -445,19 +446,19 @@ fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &ShapedText)
}
}
}
continue;
} else {
// Otherwise, draw normal outline.
let mut builder = WrappedPathBuilder(tiny_skia::PathBuilder::new());
if ttf.outline_glyph(glyph.id, &mut builder).is_some() {
let path = builder.0.finish().unwrap();
let ts = ts.pre_scale(s, -s);
let mut paint = convert_typst_fill(shaped.color);
paint.anti_alias = true;
canvas.fill_path(&path, &paint, FillRule::default(), ts, None);
}
}
// Otherwise, draw normal outline.
let mut builder = WrappedPathBuilder(tiny_skia::PathBuilder::new());
if ttf.outline_glyph(glyph, &mut builder).is_some() {
let path = builder.0.finish().unwrap();
let ts = ts.pre_scale(s, -s);
let mut paint = convert_typst_fill(shaped.color);
paint.anti_alias = true;
canvas.fill_path(&path, &paint, FillRule::default(), ts, None);
}
x += glyph.x_advance.to_pt() as f32;
}
}