Reshaping with unsafe-to-break ⚡
Co-Authored-By: Martin <mhaug@live.de>
@ -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"
|
||||
|
74
src/font.rs
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 KiB |
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|