Merge pull request #19 from typst/shape-runs 🔀

Text work
This commit is contained in:
Martin 2021-04-07 13:50:21 +02:00 committed by GitHub
commit df58a4d89b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 1246 additions and 462 deletions

View File

@ -26,9 +26,11 @@ fontdock = { path = "../fontdock", default-features = false }
image = { version = "0.23", default-features = false, features = ["jpeg", "png"] } image = { version = "0.23", default-features = false, features = ["jpeg", "png"] }
miniz_oxide = "0.3" miniz_oxide = "0.3"
pdf-writer = { path = "../pdf-writer" } pdf-writer = { path = "../pdf-writer" }
rustybuzz = "0.3" rustybuzz = { git = "https://github.com/laurmaedje/rustybuzz" }
ttf-parser = "0.9" ttf-parser = "0.12"
unicode-bidi = "0.3.5"
unicode-xid = "0.2" unicode-xid = "0.2"
xi-unicode = "0.3"
anyhow = { version = "1", optional = true } anyhow = { version = "1", optional = true }
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", features = ["derive"], optional = true }

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -6,9 +6,8 @@ use crate::env::Env;
use crate::eval::TemplateValue; use crate::eval::TemplateValue;
use crate::geom::{Align, Dir, Gen, GenAxis, Length, Linear, Sides, Size}; use crate::geom::{Align, Dir, Gen, GenAxis, Length, Linear, Sides, Size};
use crate::layout::{ use crate::layout::{
AnyNode, PadNode, PageRun, ParChild, ParNode, StackChild, StackNode, TextNode, Tree, AnyNode, PadNode, PageRun, ParChild, ParNode, StackChild, StackNode, Tree,
}; };
use crate::parse::{is_newline, Scanner};
use crate::syntax::Span; use crate::syntax::Span;
/// The context for execution. /// The context for execution.
@ -73,28 +72,14 @@ impl<'a> ExecContext<'a> {
/// Push a word space into the active paragraph. /// Push a word space into the active paragraph.
pub fn push_word_space(&mut self) { pub fn push_word_space(&mut self) {
let em = self.state.font.resolve_size(); self.stack.par.push_soft(self.make_text_node(" "));
let amount = self.state.par.word_spacing.resolve(em);
self.stack.par.push_soft(ParChild::Spacing(amount));
} }
/// Push text into the active paragraph. /// Push text into the active paragraph.
/// ///
/// The text is split into lines at newlines. /// The text is split into lines at newlines.
pub fn push_text(&mut self, text: &str) { pub fn push_text(&mut self, text: impl Into<String>) {
let mut scanner = Scanner::new(text); self.stack.par.push(self.make_text_node(text));
let mut text = String::new();
while let Some(c) = scanner.eat_merging_crlf() {
if is_newline(c) {
self.stack.par.push_text(mem::take(&mut text), &self.state);
self.linebreak();
} else {
text.push(c);
}
}
self.stack.par.push_text(text, &self.state);
} }
/// Push spacing into paragraph or stack depending on `axis`. /// Push spacing into paragraph or stack depending on `axis`.
@ -112,7 +97,7 @@ impl<'a> ExecContext<'a> {
/// Apply a forced line break. /// Apply a forced line break.
pub fn linebreak(&mut self) { pub fn linebreak(&mut self) {
self.stack.par.push_hard(ParChild::Linebreak); self.stack.par.push_hard(self.make_text_node("\n"));
} }
/// Apply a forced paragraph break. /// Apply a forced paragraph break.
@ -140,6 +125,12 @@ impl<'a> ExecContext<'a> {
self.pagebreak(true, false, Span::default()); self.pagebreak(true, false, Span::default());
Pass::new(self.tree, self.diags) Pass::new(self.tree, self.diags)
} }
fn make_text_node(&self, text: impl Into<String>) -> ParChild {
let align = self.state.aligns.cross;
let props = self.state.font.resolve_props();
ParChild::Text(text.into(), props, align)
}
} }
struct PageBuilder { struct PageBuilder {
@ -231,24 +222,10 @@ impl ParBuilder {
} }
fn push(&mut self, child: ParChild) { fn push(&mut self, child: ParChild) {
self.children.extend(self.last.any()); if let Some(soft) = self.last.any() {
self.children.push(child); self.push_inner(soft);
} }
self.push_inner(child);
fn push_text(&mut self, text: String, state: &State) {
self.children.extend(self.last.any());
let align = state.aligns.cross;
let props = state.font.resolve_props();
if let Some(ParChild::Text(prev, prev_align)) = self.children.last_mut() {
if *prev_align == align && prev.props == props {
prev.text.push_str(&text);
return;
}
}
self.children.push(ParChild::Text(TextNode { text, props }, align));
} }
fn push_soft(&mut self, child: ParChild) { fn push_soft(&mut self, child: ParChild) {
@ -257,6 +234,21 @@ impl ParBuilder {
fn push_hard(&mut self, child: ParChild) { fn push_hard(&mut self, child: ParChild) {
self.last.hard(); self.last.hard();
self.push_inner(child);
}
fn push_inner(&mut self, child: ParChild) {
if let ParChild::Text(curr_text, curr_props, curr_align) = &child {
if let Some(ParChild::Text(prev_text, prev_props, prev_align)) =
self.children.last_mut()
{
if prev_align == curr_align && prev_props == curr_props {
prev_text.push_str(&curr_text);
return;
}
}
}
self.children.push(child); self.children.push(child);
} }

View File

@ -64,7 +64,7 @@ impl ExecWithMap for Tree {
impl ExecWithMap for Node { impl ExecWithMap for Node {
fn exec_with_map(&self, ctx: &mut ExecContext, map: &NodeMap) { fn exec_with_map(&self, ctx: &mut ExecContext, map: &NodeMap) {
match self { match self {
Node::Text(text) => ctx.push_text(text), Node::Text(text) => ctx.push_text(text.clone()),
Node::Space => ctx.push_word_space(), Node::Space => ctx.push_word_space(),
_ => map[&(self as *const _)].exec(ctx), _ => map[&(self as *const _)].exec(ctx),
} }
@ -75,9 +75,9 @@ impl Exec for Value {
fn exec(&self, ctx: &mut ExecContext) { fn exec(&self, ctx: &mut ExecContext) {
match self { match self {
Value::None => {} Value::None => {}
Value::Int(v) => ctx.push_text(&pretty(v)), Value::Int(v) => ctx.push_text(pretty(v)),
Value::Float(v) => ctx.push_text(&pretty(v)), Value::Float(v) => ctx.push_text(pretty(v)),
Value::Str(v) => ctx.push_text(v), Value::Str(v) => ctx.push_text(v.clone()),
Value::Template(v) => v.exec(ctx), Value::Template(v) => v.exec(ctx),
Value::Error => {} Value::Error => {}
other => { other => {
@ -85,7 +85,7 @@ impl Exec for Value {
// the representation in monospace. // the representation in monospace.
let prev = Rc::clone(&ctx.state.font.families); let prev = Rc::clone(&ctx.state.font.families);
ctx.set_monospace(); ctx.set_monospace();
ctx.push_text(&pretty(other)); ctx.push_text(pretty(other));
ctx.state.font.families = prev; ctx.state.font.families = prev;
} }
} }
@ -104,7 +104,7 @@ impl Exec for TemplateNode {
fn exec(&self, ctx: &mut ExecContext) { fn exec(&self, ctx: &mut ExecContext) {
match self { match self {
Self::Tree { tree, map } => tree.exec_with_map(ctx, &map), Self::Tree { tree, map } => tree.exec_with_map(ctx, &map),
Self::Str(v) => ctx.push_text(v), Self::Str(v) => ctx.push_text(v.clone()),
Self::Func(v) => v.exec(ctx), Self::Func(v) => v.exec(ctx),
} }
} }

View File

@ -97,6 +97,7 @@ pub struct ParState {
/// The spacing between lines (dependent on scaled font size). /// The spacing between lines (dependent on scaled font size).
pub leading: Linear, pub leading: Linear,
/// The spacing between words (dependent on scaled font size). /// The spacing between words (dependent on scaled font size).
// TODO: Don't ignore this.
pub word_spacing: Linear, pub word_spacing: Linear,
} }

View File

@ -4,12 +4,18 @@ use std::fmt::{self, Display, Formatter};
use fontdock::FaceFromVec; use fontdock::FaceFromVec;
use crate::geom::Length;
/// An owned font face. /// An owned font face.
pub struct FaceBuf { pub struct FaceBuf {
data: Box<[u8]>, data: Box<[u8]>,
index: u32, index: u32,
ttf: ttf_parser::Face<'static>, inner: rustybuzz::Face<'static>,
buzz: rustybuzz::Face<'static>, units_per_em: f64,
ascender: f64,
cap_height: f64,
x_height: f64,
descender: f64,
} }
impl FaceBuf { impl FaceBuf {
@ -23,18 +29,27 @@ impl FaceBuf {
self.index self.index
} }
/// Get a reference to the underlying ttf-parser face. /// Get a reference to the underlying ttf-parser/rustybuzz face.
pub fn ttf(&self) -> &ttf_parser::Face<'_> { pub fn ttf(&self) -> &rustybuzz::Face<'_> {
// We can't implement Deref because that would leak the internal 'static // We can't implement Deref because that would leak the internal 'static
// lifetime. // lifetime.
&self.ttf &self.inner
} }
/// Get a reference to the underlying rustybuzz face. /// Look up a vertical metric.
pub fn buzz(&self) -> &rustybuzz::Face<'_> { pub fn vertical_metric(&self, metric: VerticalFontMetric) -> EmLength {
// We can't implement Deref because that would leak the internal 'static self.convert(match metric {
// lifetime. VerticalFontMetric::Ascender => self.ascender,
&self.buzz VerticalFontMetric::CapHeight => self.cap_height,
VerticalFontMetric::XHeight => self.x_height,
VerticalFontMetric::Baseline => 0.0,
VerticalFontMetric::Descender => self.descender,
})
}
/// Convert from font units to an em length length.
pub fn convert(&self, units: impl Into<f64>) -> EmLength {
EmLength(units.into() / self.units_per_em)
} }
} }
@ -47,15 +62,44 @@ impl FaceFromVec for FaceBuf {
let slice: &'static [u8] = let slice: &'static [u8] =
unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) }; unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) };
let inner = rustybuzz::Face::from_slice(slice, index)?;
// Look up some metrics we may need often.
let units_per_em = inner.units_per_em();
let ascender = inner.typographic_ascender().unwrap_or(inner.ascender());
let cap_height = inner.capital_height().filter(|&h| h > 0).unwrap_or(ascender);
let x_height = inner.x_height().filter(|&h| h > 0).unwrap_or(ascender);
let descender = inner.typographic_descender().unwrap_or(inner.descender());
Some(Self { Some(Self {
data, data,
index, index,
ttf: ttf_parser::Face::from_slice(slice, index).ok()?, inner,
buzz: rustybuzz::Face::from_slice(slice, index)?, 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),
}) })
} }
} }
/// A length in resolved em units.
#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
pub struct EmLength(f64);
impl EmLength {
/// Convert to a length at the given font size.
pub fn scale(self, size: Length) -> Length {
self.0 * size
}
/// Get the number of em units.
pub fn get(self) -> f64 {
self.0
}
}
/// Identifies a vertical metric of a font. /// Identifies a vertical metric of a font.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum VerticalFontMetric { pub enum VerticalFontMetric {
@ -77,38 +121,6 @@ pub enum VerticalFontMetric {
Descender, 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 { impl Display for VerticalFontMetric {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(match self { f.pad(match self {

View File

@ -13,8 +13,8 @@ pub enum Align {
impl Align { impl Align {
/// Returns the position of this alignment in the given range. /// Returns the position of this alignment in the given range.
pub fn resolve(self, range: Range<Length>) -> Length { pub fn resolve(self, dir: Dir, range: Range<Length>) -> Length {
match self { match if dir.is_positive() { self } else { self.inv() } {
Self::Start => range.start, Self::Start => range.start,
Self::Center => (range.start + range.end) / 2.0, Self::Center => (range.start + range.end) / 2.0,
Self::End => range.end, Self::End => range.end,

View File

@ -81,6 +81,11 @@ impl Length {
Self { raw: self.raw.max(other.raw) } Self { raw: self.raw.max(other.raw) }
} }
/// Whether the other length fits into this one (i.e. is smaller).
pub fn fits(self, other: Self) -> bool {
self.raw + 1e-6 >= other.raw
}
/// Whether the length is zero. /// Whether the length is zero.
pub fn is_zero(self) -> bool { pub fn is_zero(self) -> bool {
self.raw == 0.0 self.raw == 0.0

View File

@ -28,8 +28,7 @@ impl Size {
/// Whether the other size fits into this one (smaller width and height). /// Whether the other size fits into this one (smaller width and height).
pub fn fits(self, other: Self) -> bool { pub fn fits(self, other: Self) -> bool {
const EPS: Length = Length::raw(1e-6); self.width.fits(other.width) && self.height.fits(other.height)
self.width + EPS >= other.width && self.height + EPS >= other.height
} }
/// Whether both components are finite. /// Whether both components are finite.

View File

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

View File

@ -38,6 +38,8 @@ fn pad(frame: &mut Frame, padding: Sides<Linear>) {
let origin = Point::new(padding.left, padding.top); let origin = Point::new(padding.left, padding.top);
frame.size = padded; frame.size = padded;
frame.baseline += origin.y;
for (point, _) in &mut frame.elements { for (point, _) in &mut frame.elements {
*point += origin; *point += origin;
} }

View File

@ -1,7 +1,14 @@
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::mem;
use unicode_bidi::{BidiInfo, Level};
use xi_unicode::LineBreakIterator;
use super::*; use super::*;
use crate::exec::FontProps; use crate::exec::FontProps;
use crate::util::{RangeExt, SliceExt};
type Range = std::ops::Range<usize>;
/// A node that arranges its children into a paragraph. /// A node that arranges its children into a paragraph.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -15,52 +22,63 @@ pub struct ParNode {
} }
/// A child of a paragraph node. /// A child of a paragraph node.
#[derive(Debug, Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub enum ParChild { pub enum ParChild {
/// Spacing between other nodes. /// Spacing between other nodes.
Spacing(Length), Spacing(Length),
/// A run of text and how to align it in its line. /// A run of text and how to align it in its line.
Text(TextNode, Align), Text(String, FontProps, Align),
/// Any child node and how to align it in its line. /// Any child node and how to align it in its line.
Any(AnyNode, Align), Any(AnyNode, Align),
/// A forced linebreak.
Linebreak,
}
/// A consecutive, styled run of text.
#[derive(Clone, PartialEq)]
pub struct TextNode {
/// The text.
pub text: String,
/// Properties used for font selection and layout.
pub props: FontProps,
}
impl Debug for TextNode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Text({})", self.text)
}
} }
impl Layout for ParNode { impl Layout for ParNode {
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Frame> { fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Frame> {
let mut layouter = ParLayouter::new(self.dir, self.line_spacing, areas.clone()); // Collect all text into one string used for BiDi analysis.
for child in &self.children { let text = self.collect_text();
match *child {
ParChild::Spacing(amount) => layouter.push_spacing(amount), // Find out the BiDi embedding levels.
ParChild::Text(ref node, align) => { let bidi = BidiInfo::new(&text, Level::from_dir(self.dir));
let frame = shape(&node.text, &mut ctx.env.fonts, &node.props);
layouter.push_frame(frame, align); // Build a representation of the paragraph on which we can do
} // linebreaking without layouting each and every line from scratch.
ParChild::Any(ref node, align) => { let layout = ParLayout::new(ctx, areas, self, bidi);
for frame in node.layout(ctx, &layouter.areas) {
layouter.push_frame(frame, align); // Find suitable linebreaks.
layout.build(ctx, areas.clone(), self)
} }
} }
ParChild::Linebreak => layouter.finish_line(),
impl ParNode {
/// Concatenate all text in the paragraph into one string, replacing spacing
/// with a space character and other non-text nodes with the object
/// replacement character. Returns the full text alongside the range each
/// child spans in the text.
fn collect_text(&self) -> String {
let mut text = String::new();
for string in self.strings() {
text.push_str(string);
} }
text
} }
layouter.finish()
/// The range of each item in the collected text.
fn ranges(&self) -> impl Iterator<Item = Range> + '_ {
let mut cursor = 0;
self.strings().map(move |string| {
let start = cursor;
cursor += string.len();
start .. cursor
})
}
/// The string representation of each child.
fn strings(&self) -> impl Iterator<Item = &str> {
self.children.iter().map(|child| match child {
ParChild::Spacing(_) => " ",
ParChild::Text(ref piece, _, _) => piece,
ParChild::Any(_, _) => "\u{FFFC}",
})
} }
} }
@ -70,174 +88,468 @@ impl From<ParNode> for AnyNode {
} }
} }
struct ParLayouter { impl Debug for ParChild {
dirs: Gen<Dir>, fn fmt(&self, f: &mut Formatter) -> fmt::Result {
main: SpecAxis, match self {
cross: SpecAxis, Self::Spacing(amount) => write!(f, "Spacing({:?})", amount),
Self::Text(text, _, align) => write!(f, "Text({:?}, {:?})", text, align),
Self::Any(any, align) => {
f.debug_tuple("Any").field(any).field(align).finish()
}
}
}
}
/// A paragraph representation in which children are already layouted and text
/// is separated into shapable runs.
struct ParLayout<'a> {
/// The top-level direction.
dir: Dir,
/// Bidirectional text embedding levels for the paragraph.
bidi: BidiInfo<'a>,
/// Layouted children and separated text runs.
items: Vec<ParItem<'a>>,
/// The ranges of the items in `bidi.text`.
ranges: Vec<Range>,
}
impl<'a> ParLayout<'a> {
/// Build a paragraph layout for the given node.
fn new(
ctx: &mut LayoutContext,
areas: &Areas,
par: &'a ParNode,
bidi: BidiInfo<'a>,
) -> Self {
// Prepare an iterator over each child an the range it spans.
let mut items = vec![];
let mut ranges = vec![];
// Layout the children and collect them into items.
for (range, child) in par.ranges().zip(&par.children) {
match *child {
ParChild::Spacing(amount) => {
items.push(ParItem::Spacing(amount));
ranges.push(range);
}
ParChild::Text(_, ref props, align) => {
// TODO: Also split by language and script.
for (subrange, dir) in split_runs(&bidi, range) {
let text = &bidi.text[subrange.clone()];
let shaped = shape(ctx, text, dir, props);
items.push(ParItem::Text(shaped, align));
ranges.push(subrange);
}
}
ParChild::Any(ref node, align) => {
let frames = node.layout(ctx, areas);
assert_eq!(frames.len(), 1);
let frame = frames.into_iter().next().unwrap();
items.push(ParItem::Frame(frame, align));
ranges.push(range);
}
}
}
Self { dir: par.dir, bidi, items, ranges }
}
/// Find first-fit line breaks and build the paragraph.
fn build(self, ctx: &mut LayoutContext, areas: Areas, par: &ParNode) -> Vec<Frame> {
let mut stack = LineStack::new(par.line_spacing, areas);
// The current line attempt.
// Invariant: Always fits into `stack.areas.current`.
let mut last = None;
// The start of the line in `last`.
let mut start = 0;
// Find suitable line breaks.
// TODO: Provide line break opportunities on alignment changes.
for (end, mandatory) in LineBreakIterator::new(self.bidi.text) {
// Compute the line and its size.
let mut line = LineLayout::new(ctx, &self, start .. end);
// If the line doesn't fit anymore, we push the last fitting attempt
// into the stack and rebuild the line from its end. The resulting
// line cannot be broken up further.
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(ctx, &self, start .. end);
}
}
// If the line does not fit vertically, we start a new area.
if !stack.areas.current.height.fits(line.size.height)
&& !stack.areas.in_full_last()
{
stack.finish_area(ctx);
}
if mandatory || !stack.areas.current.width.fits(line.size.width) {
// If the line does not fit horizontally or we have a mandatory
// line break (i.e. due to "\n"), we push the line into the
// stack.
stack.push(line);
start = end;
last = None;
// If there is a trailing line break at the end of the
// paragraph, we want to force an empty line.
if mandatory && end == self.bidi.text.len() {
stack.push(LineLayout::new(ctx, &self, end .. end));
}
} else {
// Otherwise, the line fits both horizontally and vertically
// and we remember it.
last = Some((line, end));
}
}
if let Some((line, _)) = last {
stack.push(line);
}
stack.finish(ctx)
}
/// Find the index of the item whose range contains the `text_offset`.
fn find(&self, text_offset: usize) -> Option<usize> {
self.ranges.binary_search_by(|r| r.locate(text_offset)).ok()
}
}
/// Split a range of text into runs of consistent direction.
fn split_runs<'a>(
bidi: &'a BidiInfo,
range: Range,
) -> impl Iterator<Item = (Range, Dir)> + 'a {
let mut cursor = range.start;
bidi.levels[range.clone()]
.group_by_key(|&level| level)
.map(move |(level, group)| {
let start = cursor;
cursor += group.len();
(start .. cursor, level.dir())
})
}
/// A prepared item in a paragraph layout.
enum ParItem<'a> {
/// Spacing between other items.
Spacing(Length),
/// A shaped text run with consistent direction.
Text(ShapedText<'a>, Align),
/// A layouted child node.
Frame(Frame, Align),
}
impl ParItem<'_> {
/// The size of the item.
pub fn size(&self) -> Size {
match self {
Self::Spacing(amount) => Size::new(*amount, Length::ZERO),
Self::Text(shaped, _) => shaped.size,
Self::Frame(frame, _) => frame.size,
}
}
/// The baseline of the item.
pub fn baseline(&self) -> Length {
match self {
Self::Spacing(_) => Length::ZERO,
Self::Text(shaped, _) => shaped.baseline,
Self::Frame(frame, _) => frame.baseline,
}
}
}
/// A simple layouter that stacks lines into areas.
struct LineStack<'a> {
line_spacing: Length, line_spacing: Length,
areas: Areas, areas: Areas,
finished: Vec<Frame>, finished: Vec<Frame>,
stack: Vec<(Length, Frame, Align)>, lines: Vec<LineLayout<'a>>,
stack_size: Gen<Length>, size: Size,
line: Vec<(Length, Frame, Align)>,
line_size: Gen<Length>,
line_ruler: Align,
} }
impl ParLayouter { impl<'a> LineStack<'a> {
fn new(dir: Dir, line_spacing: Length, areas: Areas) -> Self { fn new(line_spacing: Length, areas: Areas) -> Self {
Self { Self {
dirs: Gen::new(Dir::TTB, dir),
main: SpecAxis::Vertical,
cross: SpecAxis::Horizontal,
line_spacing, line_spacing,
areas, areas,
finished: vec![], finished: vec![],
stack: vec![], lines: vec![],
stack_size: Gen::ZERO, size: Size::ZERO,
line: vec![],
line_size: Gen::ZERO,
line_ruler: Align::Start,
} }
} }
fn push_spacing(&mut self, amount: Length) { fn push(&mut self, line: LineLayout<'a>) {
let cross_max = self.areas.current.get(self.cross); self.areas.current.height -= line.size.height + self.line_spacing;
self.line_size.cross = (self.line_size.cross + amount).min(cross_max);
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;
} }
fn push_frame(&mut self, frame: Frame, align: Align) { self.lines.push(line);
// When the alignment of the last pushed frame (stored in the "ruler")
// is further to the end than the new `frame`, we need a line break.
//
// For example
// ```
// #align(right)[First] #align(center)[Second]
// ```
// would be laid out as:
// +----------------------------+
// | First |
// | Second |
// +----------------------------+
if self.line_ruler > align {
self.finish_line();
} }
// Find out whether the area still has enough space for this frame. fn finish_area(&mut self, ctx: &mut LayoutContext) {
// Space occupied by previous lines is already removed from let expand = self.areas.expand.horizontal;
// `areas.current`, but the cross-extent of the current line needs to be self.size.width = expand.resolve(self.size.width, self.areas.full.width);
// subtracted to make sure the frame fits.
let fits = {
let mut usable = self.areas.current;
*usable.get_mut(self.cross) -= self.line_size.cross;
usable.fits(frame.size)
};
if !fits { let mut output = Frame::new(self.size, self.size.height);
self.finish_line(); let mut first = true;
let mut offset = Length::ZERO;
// Here, we can directly check whether the frame fits into for line in mem::take(&mut self.lines) {
// `areas.current` since we just called `finish_line`. let frame = line.build(ctx, self.size.width);
while !self.areas.current.fits(frame.size) { let Frame { size, baseline, .. } = frame;
if self.areas.in_full_last() {
// The frame fits nowhere.
// TODO: Should this be placed into the first area or the last?
// TODO: Produce diagnostic once the necessary spans exist.
break;
} else {
self.finish_area();
}
}
}
// A line can contain frames with different alignments. They exact let pos = Point::new(Length::ZERO, offset);
// positions are calculated later depending on the alignments.
let size = frame.size.switch(self.main);
self.line.push((self.line_size.cross, frame, align));
self.line_size.cross += size.cross;
self.line_size.main = self.line_size.main.max(size.main);
self.line_ruler = align;
}
fn finish_line(&mut self) {
let full_size = {
let expand = self.areas.expand.get(self.cross);
let full = self.areas.full.get(self.cross);
Gen::new(
self.line_size.main,
expand.resolve(self.line_size.cross, full),
)
};
let mut output = Frame::new(full_size.switch(self.main).to_size());
for (before, frame, align) in std::mem::take(&mut self.line) {
let child_cross_size = frame.size.get(self.cross);
// Position along the cross axis.
let cross = align.resolve(if self.dirs.cross.is_positive() {
let after_with_self = self.line_size.cross - before;
before .. full_size.cross - after_with_self
} else {
let before_with_self = before + child_cross_size;
let after = self.line_size.cross - (before + child_cross_size);
full_size.cross - before_with_self .. after
});
let pos = Gen::new(Length::ZERO, cross).switch(self.main).to_point();
output.push_frame(pos, frame); output.push_frame(pos, frame);
if first {
output.baseline = offset + baseline;
first = false;
} }
// Add line spacing, but only between lines. offset += size.height + self.line_spacing;
if !self.stack.is_empty() {
self.stack_size.main += self.line_spacing;
*self.areas.current.get_mut(self.main) -= self.line_spacing;
}
// Update metrics of paragraph and reset for line.
self.stack.push((self.stack_size.main, output, self.line_ruler));
self.stack_size.main += full_size.main;
self.stack_size.cross = self.stack_size.cross.max(full_size.cross);
*self.areas.current.get_mut(self.main) -= full_size.main;
self.line_size = Gen::ZERO;
self.line_ruler = Align::Start;
}
fn finish_area(&mut self) {
let full_size = self.stack_size;
let mut output = Frame::new(full_size.switch(self.main).to_size());
for (before, line, cross_align) in std::mem::take(&mut self.stack) {
let child_size = line.size.switch(self.main);
// Position along the main axis.
let main = if self.dirs.main.is_positive() {
before
} else {
full_size.main - (before + child_size.main)
};
// Align along the cross axis.
let cross = cross_align.resolve(if self.dirs.cross.is_positive() {
Length::ZERO .. full_size.cross - child_size.cross
} else {
full_size.cross - child_size.cross .. Length::ZERO
});
let pos = Gen::new(main, cross).switch(self.main).to_point();
output.push_frame(pos, line);
} }
self.finished.push(output); self.finished.push(output);
self.areas.next(); self.areas.next();
self.size = Size::ZERO;
// Reset metrics for the whole paragraph.
self.stack_size = Gen::ZERO;
} }
fn finish(mut self) -> Vec<Frame> { fn finish(mut self, ctx: &mut LayoutContext) -> Vec<Frame> {
self.finish_line(); self.finish_area(ctx);
self.finish_area();
self.finished self.finished
} }
} }
/// A lightweight representation of a line that spans a specific range in a
/// paragraph's text. This type enables you to cheaply measure the size of a
/// line in a range before comitting to building the line's frame.
struct LineLayout<'a> {
/// The paragraph the line was created in.
par: &'a ParLayout<'a>,
/// The range the line spans in the paragraph.
line: Range,
/// A reshaped text item if the line sliced up a text item at the start.
first: Option<ParItem<'a>>,
/// Middle items which don't need to be reprocessed.
items: &'a [ParItem<'a>],
/// A reshaped text item if the line sliced up a text item at the end. If
/// there is only one text item, this takes precedence over `first`.
last: Option<ParItem<'a>>,
/// The ranges, indexed as `[first, ..items, last]`. The ranges for `first`
/// and `last` aren't trimmed to the line, but it doesn't matter because
/// we're just checking which range an index falls into.
ranges: &'a [Range],
/// The size of the line.
size: Size,
/// The baseline of the line.
baseline: Length,
}
impl<'a> LineLayout<'a> {
/// Create a line which spans the given range.
fn new(ctx: &mut LayoutContext, par: &'a ParLayout<'a>, mut line: Range) -> Self {
// Find the items which bound the text range.
let last_idx = par.find(line.end.saturating_sub(1)).unwrap();
let first_idx = if line.is_empty() {
last_idx
} else {
par.find(line.start).unwrap()
};
// Slice out the relevant items and ranges.
let mut items = &par.items[first_idx ..= last_idx];
let ranges = &par.ranges[first_idx ..= last_idx];
// Reshape the last item if it's split in half.
let mut last = None;
if let Some((ParItem::Text(shaped, align), rest)) = items.split_last() {
// Compute the range we want to shape, trimming whitespace at the
// end of the line.
let base = par.ranges[last_idx].start;
let start = line.start.max(base);
let end = start + par.bidi.text[start .. line.end].trim_end().len();
let range = start - base .. end - base;
// Reshape if necessary.
if range.len() < shaped.text.len() {
// If start == end and the rest is empty, then we have an empty
// line. To make that line have the appropriate height, we shape the
// empty string.
if !range.is_empty() || rest.is_empty() {
// Reshape that part.
let reshaped = shaped.reshape(ctx, range);
last = Some(ParItem::Text(reshaped, *align));
}
items = rest;
line.end = end;
}
}
// Reshape the start item if it's split in half.
let mut first = None;
if let Some((ParItem::Text(shaped, align), rest)) = items.split_first() {
// Compute the range we want to shape.
let Range { start: base, end: first_end } = par.ranges[first_idx];
let start = line.start;
let end = line.end.min(first_end);
let range = start - base .. end - base;
// Reshape if necessary.
if range.len() < shaped.text.len() {
if !range.is_empty() {
let reshaped = shaped.reshape(ctx, range);
first = Some(ParItem::Text(reshaped, *align));
}
items = rest;
}
}
let mut width = Length::ZERO;
let mut top = Length::ZERO;
let mut bottom = Length::ZERO;
// Measure the size of the line.
for item in first.iter().chain(items).chain(&last) {
let size = item.size();
let baseline = item.baseline();
width += size.width;
top = top.max(baseline);
bottom = bottom.max(size.height - baseline);
}
Self {
par,
line,
first,
items,
last,
ranges,
size: Size::new(width, top + bottom),
baseline: top,
}
}
/// Build the line's frame.
fn build(&self, ctx: &mut LayoutContext, width: Length) -> Frame {
let full_width = self.size.width.max(width);
let full_size = Size::new(full_width, self.size.height);
let free_width = full_width - self.size.width;
let mut output = Frame::new(full_size, self.baseline);
let mut ruler = Align::Start;
let mut offset = Length::ZERO;
self.reordered(|item| {
let frame = match *item {
ParItem::Spacing(amount) => {
offset += amount;
return;
}
ParItem::Text(ref shaped, align) => {
ruler = ruler.max(align);
shaped.build(ctx)
}
ParItem::Frame(ref frame, align) => {
ruler = ruler.max(align);
frame.clone()
}
};
let Frame { size, baseline, .. } = frame;
let pos = Point::new(
ruler.resolve(self.par.dir, offset .. free_width + offset),
self.baseline - baseline,
);
output.push_frame(pos, frame);
offset += size.width;
});
output
}
/// Iterate through the line's items in visual order.
fn reordered(&self, mut f: impl FnMut(&ParItem<'a>)) {
// The bidi crate doesn't like empty lines.
if self.line.is_empty() {
return;
}
// Find the paragraph that contains the line.
let para = self
.par
.bidi
.paragraphs
.iter()
.find(|para| para.range.contains(&self.line.start))
.unwrap();
// Compute the reordered ranges in visual order (left to right).
let (levels, runs) = self.par.bidi.visual_runs(para, self.line.clone());
// Find the items for each run.
for run in runs {
let first_idx = self.find(run.start).unwrap();
let last_idx = self.find(run.end - 1).unwrap();
let range = first_idx ..= last_idx;
// Provide the items forwards or backwards depending on the run's
// direction.
if levels[run.start].is_ltr() {
for item in range {
f(self.get(item).unwrap());
}
} else {
for item in range.rev() {
f(self.get(item).unwrap());
}
}
}
}
/// Find the index of the item whose range contains the `text_offset`.
fn find(&self, text_offset: usize) -> Option<usize> {
self.ranges.binary_search_by(|r| r.locate(text_offset)).ok()
}
/// Get the item at the index.
fn get(&self, index: usize) -> Option<&ParItem<'a>> {
self.first.iter().chain(self.items).chain(&self.last).nth(index)
}
}
/// Helper methods for BiDi levels.
trait LevelExt: Sized {
fn from_dir(dir: Dir) -> Option<Self>;
fn dir(self) -> Dir;
}
impl LevelExt for Level {
fn from_dir(dir: Dir) -> Option<Self> {
match dir {
Dir::LTR => Some(Level::ltr()),
Dir::RTL => Some(Level::rtl()),
_ => None,
}
}
fn dir(self) -> Dir {
if self.is_ltr() { Dir::LTR } else { Dir::RTL }
}
}

View File

@ -1,30 +1,219 @@
use std::borrow::Cow;
use std::fmt::{self, Debug, Formatter};
use std::ops::Range;
use fontdock::FaceId; use fontdock::FaceId;
use rustybuzz::UnicodeBuffer; use rustybuzz::UnicodeBuffer;
use ttf_parser::GlyphId; use ttf_parser::GlyphId;
use super::{Element, Frame, ShapedText}; use super::{Element, Frame, Glyph, LayoutContext, Text};
use crate::env::FontLoader; use crate::env::FontLoader;
use crate::exec::FontProps; use crate::exec::FontProps;
use crate::geom::{Point, Size}; use crate::font::FaceBuf;
use crate::geom::{Dir, Length, Point, Size};
use crate::util::SliceExt;
/// 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]>,
}
/// 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 glyph_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,
}
/// A visual side.
enum Side {
Left,
Right,
}
impl<'a> ShapedText<'a> {
/// Build the shaped text's frame.
pub fn build(&self, ctx: &mut LayoutContext) -> Frame {
let mut frame = Frame::new(self.size, self.baseline);
let mut offset = Length::ZERO;
for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
let pos = Point::new(offset, self.baseline);
let mut text = Text {
face_id,
size: self.props.size,
color: self.props.color,
glyphs: vec![],
};
let face = ctx.env.fonts.face(face_id);
for glyph in group {
let x_advance = face.convert(glyph.x_advance).scale(self.props.size);
let x_offset = face.convert(glyph.x_offset).scale(self.props.size);
text.glyphs.push(Glyph { id: glyph.glyph_id, x_advance, x_offset });
offset += x_advance;
}
frame.push(pos, Element::Text(text));
}
/// 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 frame
} }
/// Shape text into a frame with font fallback using the `families` iterator. /// Reshape a range of the shaped text, reusing information from this
/// shaping process if possible.
pub fn reshape(
&'a self,
ctx: &mut LayoutContext,
text_range: Range<usize>,
) -> ShapedText<'a> {
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
let (size, baseline) = measure(&mut ctx.env.fonts, glyphs, self.props);
Self {
text: &self.text[text_range],
dir: self.dir,
props: self.props,
size,
baseline,
glyphs: Cow::Borrowed(glyphs),
}
} else {
shape(ctx, &self.text[text_range], self.dir, self.props)
}
}
/// 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 Range { mut start, mut end } = text_range;
if !self.dir.is_positive() {
std::mem::swap(&mut start, &mut end);
}
let left = self.find_safe_to_break(start, Side::Left)?;
let right = self.find_safe_to_break(end, Side::Right)?;
Some(&self.glyphs[left .. right])
}
/// Find the glyph offset matching the text index that is most towards the
/// given side and safe-to-break.
fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> {
let ltr = self.dir.is_positive();
// 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 });
}
// Find any glyph with the text index.
let mut idx = self
.glyphs
.binary_search_by(|g| {
let ordering = g.text_index.cmp(&text_index);
if ltr { ordering } else { ordering.reverse() }
})
.ok()?;
let next = match towards {
Side::Left => usize::checked_sub,
Side::Right => usize::checked_add,
};
// Search for the outermost glyph with the text index.
while let Some(next) = next(idx, 1) {
if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) {
break;
}
idx = next;
}
// RTL needs offset one because the left side of the range should be
// exclusive and the right side inclusive, contrary to the normal
// behaviour of ranges.
if !ltr {
idx += 1;
}
self.glyphs[idx].safe_to_break.then(|| idx)
}
}
impl Debug for ShapedText<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Shaped({:?})", self.text)
}
}
/// Shape text into [`ShapedText`].
pub fn shape<'a>(
ctx: &mut LayoutContext,
text: &'a str,
dir: Dir,
props: &'a FontProps,
) -> ShapedText<'a> {
let loader = &mut ctx.env.fonts;
let mut glyphs = vec![];
let families = props.families.iter();
if !text.is_empty() {
shape_segment(loader, &mut glyphs, 0, text, dir, props, families, None);
}
let (size, baseline) = measure(loader, &glyphs, props);
ShapedText {
text,
dir,
props,
size,
baseline,
glyphs: Cow::Owned(glyphs),
}
}
/// Shape text with font fallback using the `families` iterator.
fn shape_segment<'a>( fn shape_segment<'a>(
frame: &mut Frame,
text: &str,
loader: &mut FontLoader, loader: &mut FontLoader,
glyphs: &mut Vec<ShapedGlyph>,
base: usize,
text: &str,
dir: Dir,
props: &FontProps, props: &FontProps,
mut families: impl Iterator<Item = &'a str> + Clone, mut families: impl Iterator<Item = &'a str> + Clone,
mut first: Option<FaceId>, mut first_face: Option<FaceId>,
) { ) {
// Select the font family. // Select the font family.
let (id, fallback) = loop { let (face_id, fallback) = loop {
// Try to load the next available font family. // Try to load the next available font family.
match families.next() { match families.next() {
Some(family) => match loader.query(family, props.variant) { Some(family) => match loader.query(family, props.variant) {
@ -33,97 +222,140 @@ fn shape_segment<'a>(
}, },
// We're out of families, so we don't do any more fallback and just // 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. // shape the tofus with the first face we originally used.
None => match first { None => match first_face {
Some(id) => break (id, false), Some(id) => break (id, false),
None => return, None => return,
}, },
} }
}; };
// Register that this is the first available font. // Remember the id if this the first available face since we use that one to
if first.is_none() { // shape tofus.
first = Some(id); first_face.get_or_insert(face_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. // Fill the buffer with our text.
let mut buffer = UnicodeBuffer::new(); let mut buffer = UnicodeBuffer::new();
buffer.push_str(text); buffer.push_str(text);
buffer.guess_segment_properties(); buffer.set_direction(match dir {
Dir::LTR => rustybuzz::Direction::LeftToRight,
// Find out the text direction. Dir::RTL => rustybuzz::Direction::RightToLeft,
// TODO: Replace this once we do BiDi. _ => unimplemented!(),
let rtl = matches!(buffer.direction(), rustybuzz::Direction::RightToLeft); });
// Shape! // Shape!
let glyphs = rustybuzz::shape(face.buzz(), &[], buffer); let buffer = rustybuzz::shape(loader.face(face_id).ttf(), &[], buffer);
let info = glyphs.glyph_infos(); let infos = buffer.glyph_infos();
let pos = glyphs.glyph_positions(); let pos = buffer.glyph_positions();
let mut iter = info.iter().zip(pos).peekable();
while let Some((info, pos)) = iter.next() { // Collect the shaped glyphs, doing fallback and shaping parts again with
// Do font fallback if the glyph is a tofu. // the next font if necessary.
if info.codepoint == 0 && fallback { let mut i = 0;
// Flush what we have so far. while i < infos.len() {
if !shaped.glyphs.is_empty() { let info = &infos[i];
place(frame, shaped); let cluster = info.cluster as usize;
shaped = ShapedText::new(id, props.size, top, bottom, props.color);
}
// Determine the start and end cluster index of the tofu sequence. if info.codepoint != 0 || !fallback {
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. // Add the glyph to the shaped output.
// TODO: Don't ignore y_advance and y_offset. // TODO: Don't ignore y_advance and y_offset.
let glyph = GlyphId(info.codepoint as u16); glyphs.push(ShapedGlyph {
shaped.glyphs.push(glyph); face_id,
shaped.offsets.push(shaped.width + convert(pos.x_offset)); glyph_id: GlyphId(info.codepoint as u16),
shaped.width += convert(pos.x_advance); x_advance: pos[i].x_advance,
x_offset: pos[i].x_offset,
text_index: base + cluster,
safe_to_break: !info.unsafe_to_break(),
});
} else {
// Determine the source text range for the tofu sequence.
let range = {
// 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;
}
// Then, determine the start and end text index.
//
// Examples:
// Everything is shown in visual order. Tofus are written as "_".
// We want to find out that the tofus span the text `2..6`.
// Note that the clusters are longer than 1 char.
//
// Left-to-right:
// Text: h a l i h a l l o
// Glyphs: A _ _ C E
// Clusters: 0 2 4 6 8
// k=1 i=2
//
// Right-to-left:
// Text: O L L A H I L A H
// Glyphs: E C _ _ A
// Clusters: 8 6 4 2 0
// k=2 i=3
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_or(text.len(), |info| info.cluster as usize);
start .. end
};
// Recursively shape the tofu sequence with the next family.
shape_segment(
loader,
glyphs,
base + range.start,
&text[range],
dir,
props,
families.clone(),
first_face,
);
}
i += 1;
} }
} }
if !shaped.glyphs.is_empty() { /// Measure the size and baseline of a run of shaped glyphs with the given
place(frame, shaped) /// properties.
fn measure(
loader: &mut FontLoader,
glyphs: &[ShapedGlyph],
props: &FontProps,
) -> (Size, Length) {
let mut width = Length::ZERO;
let mut top = Length::ZERO;
let mut bottom = Length::ZERO;
let mut expand_vertical = |face: &FaceBuf| {
top = top.max(face.vertical_metric(props.top_edge).scale(props.size));
bottom = bottom.max(-face.vertical_metric(props.bottom_edge).scale(props.size));
};
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) {
expand_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);
expand_vertical(face);
for glyph in group {
width += face.convert(glyph.x_advance).scale(props.size);
}
} }
} }
/// Place shaped text into a frame. (Size::new(width, top + bottom), top)
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));
} }

View File

@ -28,7 +28,13 @@ impl Layout for StackNode {
match *child { match *child {
StackChild::Spacing(amount) => layouter.push_spacing(amount), StackChild::Spacing(amount) => layouter.push_spacing(amount),
StackChild::Any(ref node, aligns) => { StackChild::Any(ref node, aligns) => {
for frame in node.layout(ctx, &layouter.areas) { let mut frames = node.layout(ctx, &layouter.areas).into_iter();
if let Some(frame) = frames.next() {
layouter.push_frame(frame, aligns);
}
for frame in frames {
layouter.finish_area();
layouter.push_frame(frame, aligns); layouter.push_frame(frame, aligns);
} }
} }
@ -116,32 +122,39 @@ impl StackLayouter {
size = Size::new(width, width / aspect); size = Size::new(width, width / aspect);
} }
size.switch(self.main) size
}; };
let mut output = Frame::new(full_size.switch(self.main).to_size()); let mut output = Frame::new(full_size, full_size.height);
let mut first = true;
let full_size = full_size.switch(self.main);
for (before, frame, aligns) in std::mem::take(&mut self.frames) { for (before, frame, aligns) in std::mem::take(&mut self.frames) {
let child_size = frame.size.switch(self.main); let child_size = frame.size.switch(self.main);
// Align along the main axis. // Align along the main axis.
let main = aligns.main.resolve(if self.dirs.main.is_positive() { let main = aligns.main.resolve(
let after_with_self = self.size.main - before; self.dirs.main,
before .. full_size.main - after_with_self if self.dirs.main.is_positive() {
before .. before + full_size.main - self.size.main
} else { } else {
let before_with_self = before + child_size.main; self.size.main - (before + child_size.main)
let after = self.size.main - (before + child_size.main); .. full_size.main - (before + child_size.main)
full_size.main - before_with_self .. after },
}); );
// Align along the cross axis. // Align along the cross axis.
let cross = aligns.cross.resolve(if self.dirs.cross.is_positive() { let cross = aligns.cross.resolve(
Length::ZERO .. full_size.cross - child_size.cross self.dirs.cross,
} else { Length::ZERO .. full_size.cross - child_size.cross,
full_size.cross - child_size.cross .. Length::ZERO );
});
let pos = Gen::new(main, cross).switch(self.main).to_point(); let pos = Gen::new(main, cross).switch(self.main).to_point();
if first {
output.baseline = pos.y + frame.baseline;
first = false;
}
output.push_frame(pos, frame); output.push_frame(pos, frame);
} }

View File

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

View File

@ -73,7 +73,7 @@ impl Layout for ImageNode {
} }
}; };
let mut frame = Frame::new(size); let mut frame = Frame::new(size, size.height);
frame.push(Point::ZERO, Element::Image(Image { res: self.res, size })); frame.push(Point::ZERO, Element::Image(Image { res: self.res, size }));
vec![frame] vec![frame]

View File

@ -160,7 +160,7 @@ pub fn raw(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value {
let snapshot = ctx.state.clone(); let snapshot = ctx.state.clone();
ctx.set_monospace(); ctx.set_monospace();
ctx.push_text(&text); ctx.push_text(text.clone());
ctx.state = snapshot; ctx.state = snapshot;
if block { if block {

View File

@ -15,6 +15,7 @@ use ttf_parser::{name_id, GlyphId};
use crate::color::Color; use crate::color::Color;
use crate::env::{Env, ImageResource, ResourceId}; use crate::env::{Env, ImageResource, ResourceId};
use crate::font::{EmLength, VerticalFontMetric};
use crate::geom::{self, Length, Size}; use crate::geom::{self, Length, Size};
use crate::layout::{Element, Fill, Frame, Image, Shape}; use crate::layout::{Element, Fill, Frame, Image, Shape};
@ -50,7 +51,7 @@ impl<'a> PdfExporter<'a> {
for frame in frames { for frame in frames {
for (_, element) in &frame.elements { for (_, element) in &frame.elements {
match element { match element {
Element::Text(shaped) => fonts.insert(shaped.face), Element::Text(shaped) => fonts.insert(shaped.face_id),
Element::Image(image) => { Element::Image(image) => {
let img = env.resources.loaded::<ImageResource>(image.res); let img = env.resources.loaded::<ImageResource>(image.res);
if img.buf.color().has_alpha() { if img.buf.color().has_alpha() {
@ -187,11 +188,11 @@ impl<'a> PdfExporter<'a> {
// Then, also check if we need to issue a font switching // Then, also check if we need to issue a font switching
// action. // action.
if shaped.face != face || shaped.size != size { if shaped.face_id != face || shaped.size != size {
face = shaped.face; face = shaped.face_id;
size = shaped.size; 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); text.font(Name(name.as_bytes()), size.to_pt() as f32);
} }
@ -234,24 +235,18 @@ impl<'a> PdfExporter<'a> {
flags.insert(FontFlags::SYMBOLIC); flags.insert(FontFlags::SYMBOLIC);
flags.insert(FontFlags::SMALL_CAP); flags.insert(FontFlags::SMALL_CAP);
// Convert from OpenType font units to PDF glyph units.
let em_per_unit = 1.0 / ttf.units_per_em().unwrap_or(1000) as f32;
let convert = |font_unit: f32| (1000.0 * em_per_unit * font_unit).round();
let convert_i16 = |font_unit: i16| convert(font_unit as f32);
let convert_u16 = |font_unit: u16| convert(font_unit as f32);
let global_bbox = ttf.global_bounding_box(); let global_bbox = ttf.global_bounding_box();
let bbox = Rect::new( let bbox = Rect::new(
convert_i16(global_bbox.x_min), face.convert(global_bbox.x_min).to_pdf(),
convert_i16(global_bbox.y_min), face.convert(global_bbox.y_min).to_pdf(),
convert_i16(global_bbox.x_max), face.convert(global_bbox.x_max).to_pdf(),
convert_i16(global_bbox.y_max), face.convert(global_bbox.y_max).to_pdf(),
); );
let italic_angle = ttf.italic_angle().unwrap_or(0.0); let italic_angle = ttf.italic_angle().unwrap_or(0.0);
let ascender = convert_i16(ttf.typographic_ascender().unwrap_or(0)); let ascender = face.vertical_metric(VerticalFontMetric::Ascender).to_pdf();
let descender = convert_i16(ttf.typographic_descender().unwrap_or(0)); let descender = face.vertical_metric(VerticalFontMetric::Descender).to_pdf();
let cap_height = ttf.capital_height().map(convert_i16); let cap_height = face.vertical_metric(VerticalFontMetric::CapHeight).to_pdf();
let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0); let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0);
// Write the base font object referencing the CID font. // Write the base font object referencing the CID font.
@ -272,8 +267,8 @@ impl<'a> PdfExporter<'a> {
.individual(0, { .individual(0, {
let num_glyphs = ttf.number_of_glyphs(); let num_glyphs = ttf.number_of_glyphs();
(0 .. num_glyphs).map(|g| { (0 .. num_glyphs).map(|g| {
let advance = ttf.glyph_hor_advance(GlyphId(g)); let x = ttf.glyph_hor_advance(GlyphId(g)).unwrap_or(0);
convert_u16(advance.unwrap_or(0)) face.convert(x).to_pdf()
}) })
}); });
@ -286,7 +281,7 @@ impl<'a> PdfExporter<'a> {
.italic_angle(italic_angle) .italic_angle(italic_angle)
.ascent(ascender) .ascent(ascender)
.descent(descender) .descent(descender)
.cap_height(cap_height.unwrap_or(ascender)) .cap_height(cap_height)
.stem_v(stem_v) .stem_v(stem_v)
.font_file2(refs.data); .font_file2(refs.data);
@ -571,3 +566,15 @@ where
self.to_layout.iter().copied() self.to_layout.iter().copied()
} }
} }
/// Additional methods for [`EmLength`].
trait EmLengthExt {
/// Convert an em length to a number of PDF font units.
fn to_pdf(self) -> f32;
}
impl EmLengthExt for EmLength {
fn to_pdf(self) -> f32 {
1000.0 * self.get() as f32
}
}

81
src/util.rs Normal file
View File

@ -0,0 +1,81 @@
//! 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: 706 B

After

Width:  |  Height:  |  Size: 708 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 880 B

After

Width:  |  Height:  |  Size: 879 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 770 B

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
tests/ref/text/align.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

BIN
tests/ref/text/bidi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
tests/ref/text/chinese.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -3,6 +3,7 @@
--- ---
// Test configuring paragraph properties. // Test configuring paragraph properties.
// FIXME: Word spacing doesn't work due to new shaping process.
#par(spacing: 10pt, leading: 25%, word-spacing: 1pt) #par(spacing: 10pt, leading: 25%, word-spacing: 1pt)
But, soft! what light through yonder window breaks? It is the east, and Juliet But, soft! what light through yonder window breaks? It is the east, and Juliet

View File

@ -19,7 +19,7 @@
// Not visible, but creates a gap between the boxes above and below // Not visible, but creates a gap between the boxes above and below
// due to line spacing. // due to line spacing.
#rect(width: 2in, fill: #ff0000) #rect(width: 1in, fill: #ff0000)
// These are in a row! // These are in a row!
#rect(width: 0.5in, height: 10pt, fill: #D6CD67) #rect(width: 0.5in, height: 10pt, fill: #D6CD67)

View File

@ -3,7 +3,7 @@
--- ---
#let linebreak() = [ #let linebreak() = [
// Inside the old line break definition is still active. // Inside the old line break definition is still active.
#circle(radius: 2pt, fill: #000) \ #square(length: 3pt, fill: #000) \
] ]
A \ B \ C A \ B \ C

28
tests/typ/text/align.typ Normal file
View File

@ -0,0 +1,28 @@
// Test text alignment.
---
// Test that alignment depends on the paragraph's full width.
#rect[
Hello World \
#align(right)[World]
]
---
// Test that a line with multiple alignments respects the paragraph's full
// width.
#rect[
Hello #align(center)[World] \
Hello from the World
]
---
// Test that `start` alignment after `end` alignment doesn't do anything until
// the next line break ...
L #align(right)[R] R
// ... but make sure it resets to left after the line break.
L #align(right)[R] \ L
---
// FIXME: There should be a line break opportunity on alignment change.
LLLLLLLLLLLLL#align(center)[CCCC]

View File

@ -1,6 +1,7 @@
// Test simple text. // Test simple text.
#page(width: 250pt) ---
#page(width: 250pt, height: 110pt)
But, soft! what light through yonder window breaks? It is the east, and Juliet But, soft! what light through yonder window breaks? It is the east, and Juliet
is the sun. Arise, fair sun, and kill the envious moon, Who is already sick and is the sun. Arise, fair sun, and kill the envious moon, Who is already sick and

49
tests/typ/text/bidi.typ Normal file
View File

@ -0,0 +1,49 @@
// Test bidirectional text.
---
// Test reordering with different top-level paragraph directions.
#let text = [Text טֶקסט]
#font("EB Garamond", "Noto Serif Hebrew")
#lang("de") {text}
#lang("he") {text}
---
// Test that consecutiv, embedded LTR runs stay LTR.
// Here, we have two runs: "A" and italic "B".
#let text = [أنت A_B_مطرC]
#font("EB Garamond", "Noto Sans Arabic")
#lang("de") {text}
#lang("ar") {text}
---
// Test that consecutive, embedded RTL runs stay RTL.
// Here, we have three runs: "גֶ", bold "שֶׁ", and "ם".
#let text = [Aגֶ*שֶׁ*םB]
#font("EB Garamond", "Noto Serif Hebrew")
#lang("de") {text}
#lang("he") {text}
---
// Test embedding up to level 4 with isolates.
#font("EB Garamond", "Noto Serif Hebrew", "Twitter Color Emoji")
#lang(dir: rtl)
א\u{2066}A\u{2067}Bב\u{2069}?
---
// Test hard line break (leads to two paragraphs in unicode-bidi).
#font("Noto Sans Arabic", "EB Garamond")
#lang("ar")
Life المطر هو الحياة \
الحياة تمطر is rain.
---
// Test spacing.
#font("EB Garamond", "Noto Serif Hebrew")
L #h(1cm) ריווחR \
יווח #h(1cm) R
---
// Test inline object.
#font("Noto Serif Hebrew", "EB Garamond")
#lang("he")
קרנפיםRh#image("res/rhino.png", height: 11pt)inoחיים

View File

@ -0,0 +1,9 @@
// Test chinese text from Wikipedia.
---
#font("Noto Serif CJK SC")
是美国广播公司电视剧《迷失》第3季的第22和23集也是全剧的第71集和72集
由执行制作人戴蒙·林道夫和卡尔顿·库斯编剧,导演则是另一名执行制作人杰克·本德
节目于2007年5月23日在美国和加拿大首播共计吸引了1400万美国观众收看
本集加上插播广告一共也持续有两个小时

View File

@ -0,0 +1,28 @@
// Test line breaking special cases.
---
// Test overlong word that is not directly after a hard break.
This is a spaceexceedinglylongishy.
---
// Test two overlong words in a row.
Supercalifragilisticexpialidocious Expialigoricmetrioxidation.
---
// Test that there are no unwanted line break opportunities on run change.
This is partly emph_as_ized.
---
Hard \ break.
---
// Test hard break directly after normal break.
Hard break directly after \ normal break.
---
// Test consecutive breaks.
Two consecutive \ \ breaks and three \ \ \ more.
---
// Test trailing newline.
Trailing break \

View File

@ -8,7 +8,7 @@ Le fira
// This should just shape nicely. // This should just shape nicely.
#font("Noto Sans Arabic") #font("Noto Sans Arabic")
منش إلا بسم الله دع النص يمطر عليك
// This should form a three-member family. // This should form a three-member family.
#font("Twitter Color Emoji") #font("Twitter Color Emoji")
@ -26,7 +26,7 @@ Le fira
A😀B A😀B
// Font fallback for entire text. // Font fallback for entire text.
منش إلا بسم الله دع النص يمطر عليك
// Font fallback in right-to-left text. // Font fallback in right-to-left text.
ب🐈😀سم ب🐈😀سم
@ -36,3 +36,10 @@ Aب😀🏞سمB
// Tofus are rendered with the first font. // Tofus are rendered with the first font.
A🐈中文B A🐈中文B
---
// Test reshaping.
#font("Noto Serif Hebrew")
#lang("he")
ס \ טֶ

View File

@ -0,0 +1,17 @@
// Test whitespace handling.
---
// Test that a run consisting only of whitespace isn't trimmed.
A#font("PT Sans")[ ]B
---
// Test font change after space.
Left #font("PT Sans")[Right].
---
// Test that space at start of line is not trimmed.
A{"\n"} B
---
// Test that trailing space does not force a line break.
LLLLLLLLLLLLLL R _L_

View File

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