Text shaping 🚀

- Shapes text with rustybuzz
- Font fallback with family list
- Tofus are shown in the first font

Co-Authored-By: Martin <mhaug@live.de>
This commit is contained in:
Laurenz 2021-03-24 17:12:34 +01:00
parent 6720520ec0
commit 73615f7e3c
44 changed files with 466 additions and 379 deletions

View File

@ -26,7 +26,8 @@ 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" }
ttf-parser = "0.8.2"
rustybuzz = "0.3"
ttf-parser = "0.9"
unicode-xid = "0.2"
anyhow = { version = "1", optional = true }
serde = { version = "1", features = ["derive"], optional = true }

Binary file not shown.

View File

@ -7,14 +7,15 @@ use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use fontdock::{FaceFromVec, FaceId, FontSource};
use fontdock::{FaceId, FontSource};
use image::io::Reader as ImageReader;
use image::{DynamicImage, GenericImageView, ImageFormat};
use ttf_parser::Face;
#[cfg(feature = "fs")]
use fontdock::{FsIndex, FsSource};
use crate::font::FaceBuf;
/// Encapsulates all environment dependencies (fonts, resources).
#[derive(Debug)]
pub struct Env {
@ -47,42 +48,6 @@ impl Env {
/// A font loader that is backed by a dynamic source.
pub type FontLoader = fontdock::FontLoader<Box<dyn FontSource<Face = FaceBuf>>>;
/// An owned font face.
pub struct FaceBuf {
data: Box<[u8]>,
face: Face<'static>,
}
impl FaceBuf {
/// Get a reference to the underlying face.
pub fn get(&self) -> &Face<'_> {
// We can't implement Deref because that would leak the internal 'static
// lifetime.
&self.face
}
/// The raw face data.
pub fn data(&self) -> &[u8] {
&self.data
}
}
impl FaceFromVec for FaceBuf {
fn from_vec(vec: Vec<u8>, i: u32) -> Option<Self> {
let data = vec.into_boxed_slice();
// SAFETY: The slices's location is stable in memory since we don't
// touch it and it can't be touched from outside this type.
let slice: &'static [u8] =
unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) };
Some(Self {
data,
face: Face::from_slice(slice, i).ok()?,
})
}
}
/// Simplify font loader construction from an [`FsIndex`].
#[cfg(feature = "fs")]
pub trait FsIndexExt {

View File

@ -1,7 +1,4 @@
use std::mem;
use std::rc::Rc;
use fontdock::FontStyle;
use super::{Exec, FontFamily, State};
use crate::diag::{Diag, DiagSet, Pass};
@ -87,7 +84,7 @@ impl<'a> ExecContext<'a> {
/// Push a word space into the active paragraph.
pub fn push_space(&mut self) {
let em = self.state.font.font_size();
let em = self.state.font.resolve_size();
self.push(SpacingNode {
amount: self.state.par.word_spacing.resolve(em),
softness: 1,
@ -103,19 +100,19 @@ impl<'a> ExecContext<'a> {
while let Some(c) = scanner.eat_merging_crlf() {
if is_newline(c) {
self.push(self.make_text_node(mem::take(&mut line)));
self.push(TextNode::new(mem::take(&mut line), &self.state));
self.push_linebreak();
} else {
line.push(c);
}
}
self.push(self.make_text_node(line));
self.push(TextNode::new(line, &self.state));
}
/// Apply a forced line break.
pub fn push_linebreak(&mut self) {
let em = self.state.font.font_size();
let em = self.state.font.resolve_size();
self.push_into_stack(SpacingNode {
amount: self.state.par.leading.resolve(em),
softness: 2,
@ -124,7 +121,7 @@ impl<'a> ExecContext<'a> {
/// Apply a forced paragraph break.
pub fn push_parbreak(&mut self) {
let em = self.state.font.font_size();
let em = self.state.font.resolve_size();
self.push_into_stack(SpacingNode {
amount: self.state.par.spacing.resolve(em),
softness: 1,
@ -154,36 +151,6 @@ impl<'a> ExecContext<'a> {
result
}
/// Construct a text node from the given string based on the active text
/// state.
pub fn make_text_node(&self, text: String) -> TextNode {
let mut variant = self.state.font.variant;
if self.state.font.strong {
variant.weight = variant.weight.thicken(300);
}
if self.state.font.emph {
variant.style = match variant.style {
FontStyle::Normal => FontStyle::Italic,
FontStyle::Italic => FontStyle::Normal,
FontStyle::Oblique => FontStyle::Normal,
}
}
TextNode {
text,
dir: self.state.dirs.cross,
aligns: self.state.aligns,
families: Rc::clone(&self.state.font.families),
variant,
font_size: self.state.font.font_size(),
top_edge: self.state.font.top_edge,
bottom_edge: self.state.font.bottom_edge,
color: self.state.font.color,
}
}
/// Finish the active paragraph.
fn finish_par(&mut self) {
let mut par = mem::replace(&mut self.par, ParNode::new(&self.state));
@ -292,7 +259,7 @@ impl StackNode {
impl ParNode {
fn new(state: &State) -> Self {
let em = state.font.font_size();
let em = state.font.resolve_size();
Self {
dirs: state.dirs,
aligns: state.aligns,
@ -301,3 +268,14 @@ impl ParNode {
}
}
}
impl TextNode {
fn new(text: String, state: &State) -> Self {
Self {
text,
dir: state.dirs.cross,
aligns: state.aligns,
props: state.font.resolve_props(),
}
}
}

View File

@ -4,8 +4,9 @@ use std::rc::Rc;
use fontdock::{FontStretch, FontStyle, FontVariant, FontWeight};
use crate::color::{Color, RgbaColor};
use crate::font::VerticalFontMetric;
use crate::geom::*;
use crate::layout::{Fill, VerticalFontMetric};
use crate::layout::Fill;
use crate::paper::{Paper, PaperClass, PAPER_A4};
/// The evaluation state.
@ -100,7 +101,7 @@ impl Default for ParState {
#[derive(Debug, Clone, PartialEq)]
pub struct FontState {
/// A list of font families with generic class definitions.
pub families: Rc<FamilyMap>,
pub families: Rc<FamilyList>,
/// The selected font variant.
pub variant: FontVariant,
/// The font size.
@ -111,32 +112,58 @@ pub struct FontState {
pub top_edge: VerticalFontMetric,
/// The bottom end of the text bounding box.
pub bottom_edge: VerticalFontMetric,
/// The glyph fill color / texture.
pub color: Fill,
/// Whether the strong toggle is active or inactive. This determines
/// whether the next `*` adds or removes font weight.
pub strong: bool,
/// Whether the emphasis toggle is active or inactive. This determines
/// whether the next `_` makes italic or non-italic.
pub emph: bool,
/// The glyph fill color / texture.
pub color: Fill,
}
impl FontState {
/// Access the `families` mutably.
pub fn families_mut(&mut self) -> &mut FamilyMap {
Rc::make_mut(&mut self.families)
/// The resolved font size.
pub fn resolve_size(&self) -> Length {
self.scale.resolve(self.size)
}
/// The absolute font size.
pub fn font_size(&self) -> Length {
self.scale.resolve(self.size)
/// Resolve font properties.
pub fn resolve_props(&self) -> FontProps {
let mut variant = self.variant;
if self.strong {
variant.weight = variant.weight.thicken(300);
}
if self.emph {
variant.style = match variant.style {
FontStyle::Normal => FontStyle::Italic,
FontStyle::Italic => FontStyle::Normal,
FontStyle::Oblique => FontStyle::Normal,
}
}
FontProps {
families: Rc::clone(&self.families),
variant,
size: self.resolve_size(),
top_edge: self.top_edge,
bottom_edge: self.bottom_edge,
color: self.color,
}
}
/// Access the `families` mutably.
pub fn families_mut(&mut self) -> &mut FamilyList {
Rc::make_mut(&mut self.families)
}
}
impl Default for FontState {
fn default() -> Self {
Self {
families: Rc::new(FamilyMap::default()),
families: Rc::new(FamilyList::default()),
variant: FontVariant {
style: FontStyle::Normal,
weight: FontWeight::REGULAR,
@ -146,16 +173,33 @@ impl Default for FontState {
top_edge: VerticalFontMetric::CapHeight,
bottom_edge: VerticalFontMetric::Baseline,
scale: Linear::ONE,
color: Fill::Color(Color::Rgba(RgbaColor::BLACK)),
strong: false,
emph: false,
color: Fill::Color(Color::Rgba(RgbaColor::BLACK)),
}
}
}
/// Properties used for font selection and layout.
#[derive(Debug, Clone, PartialEq)]
pub struct FontProps {
/// The list of font families to use for shaping.
pub families: Rc<FamilyList>,
/// Which variant of the font to use.
pub variant: FontVariant,
/// The font size.
pub size: Length,
/// What line to consider the top edge of text.
pub top_edge: VerticalFontMetric,
/// What line to consider the bottom edge of text.
pub bottom_edge: VerticalFontMetric,
/// The color of the text.
pub color: Fill,
}
/// Font family definitions.
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct FamilyMap {
pub struct FamilyList {
/// The user-defined list of font families.
pub list: Vec<FontFamily>,
/// Definition of serif font families.
@ -168,9 +212,9 @@ pub struct FamilyMap {
pub base: Vec<String>,
}
impl FamilyMap {
impl FamilyList {
/// Flat iterator over this map's family names.
pub fn iter(&self) -> impl Iterator<Item = &str> {
pub fn iter(&self) -> impl Iterator<Item = &str> + Clone {
self.list
.iter()
.flat_map(move |family: &FontFamily| {
@ -186,7 +230,7 @@ impl FamilyMap {
}
}
impl Default for FamilyMap {
impl Default for FamilyList {
fn default() -> Self {
Self {
list: vec![FontFamily::Serif],

View File

@ -187,14 +187,15 @@ impl<'a> PdfExporter<'a> {
// Then, also check if we need to issue a font switching
// action.
if shaped.face != face || shaped.font_size != size {
if shaped.face != face || shaped.size != size {
face = shaped.face;
size = shaped.font_size;
size = shaped.size;
let name = format!("F{}", self.fonts.map(shaped.face));
text.font(Name(name.as_bytes()), size.to_pt() as f32);
}
// TODO: Respect individual glyph offsets.
text.matrix(1.0, 0.0, 0.0, 1.0, x, y);
text.show(&shaped.encode_glyphs_be());
}
@ -206,10 +207,10 @@ impl<'a> PdfExporter<'a> {
fn write_fonts(&mut self) {
for (refs, face_id) in self.refs.fonts().zip(self.fonts.layout_indices()) {
let owned_face = self.env.fonts.face(face_id);
let face = owned_face.get();
let face = self.env.fonts.face(face_id);
let ttf = face.ttf();
let name = face
let name = ttf
.names()
.find(|entry| {
entry.name_id() == name_id::POST_SCRIPT_NAME && entry.is_unicode()
@ -228,18 +229,18 @@ impl<'a> PdfExporter<'a> {
let mut flags = FontFlags::empty();
flags.set(FontFlags::SERIF, name.contains("Serif"));
flags.set(FontFlags::FIXED_PITCH, face.is_monospaced());
flags.set(FontFlags::ITALIC, face.is_italic());
flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced());
flags.set(FontFlags::ITALIC, ttf.is_italic());
flags.insert(FontFlags::SYMBOLIC);
flags.insert(FontFlags::SMALL_CAP);
// Convert from OpenType font units to PDF glyph units.
let em_per_unit = 1.0 / face.units_per_em().unwrap_or(1000) as f32;
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 = face.global_bounding_box();
let global_bbox = ttf.global_bounding_box();
let bbox = Rect::new(
convert_i16(global_bbox.x_min),
convert_i16(global_bbox.y_min),
@ -247,11 +248,11 @@ impl<'a> PdfExporter<'a> {
convert_i16(global_bbox.y_max),
);
let italic_angle = face.italic_angle().unwrap_or(0.0);
let ascender = convert_i16(face.typographic_ascender().unwrap_or(0));
let descender = convert_i16(face.typographic_descender().unwrap_or(0));
let cap_height = face.capital_height().map(convert_i16);
let stem_v = 10.0 + 0.244 * (f32::from(face.weight().to_number()) - 50.0);
let italic_angle = ttf.italic_angle().unwrap_or(0.0);
let ascender = convert_i16(ttf.typographic_ascender().unwrap_or(0));
let descender = convert_i16(ttf.typographic_descender().unwrap_or(0));
let cap_height = ttf.capital_height().map(convert_i16);
let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0);
// Write the base font object referencing the CID font.
self.writer
@ -269,9 +270,9 @@ impl<'a> PdfExporter<'a> {
.font_descriptor(refs.font_descriptor)
.widths()
.individual(0, {
let num_glyphs = face.number_of_glyphs();
let num_glyphs = ttf.number_of_glyphs();
(0 .. num_glyphs).map(|g| {
let advance = face.glyph_hor_advance(GlyphId(g));
let advance = ttf.glyph_hor_advance(GlyphId(g));
convert_u16(advance.unwrap_or(0))
})
});
@ -294,10 +295,10 @@ impl<'a> PdfExporter<'a> {
self.writer
.cmap(refs.cmap, &{
let mut cmap = UnicodeCmap::new(cmap_name, system_info);
for subtable in face.character_mapping_subtables() {
for subtable in ttf.character_mapping_subtables() {
subtable.codepoints(|n| {
if let Some(c) = std::char::from_u32(n) {
if let Some(g) = face.glyph_index(c) {
if let Some(g) = ttf.glyph_index(c) {
cmap.pair(g.0, c);
}
}
@ -309,7 +310,7 @@ impl<'a> PdfExporter<'a> {
.system_info(system_info);
// Write the face's bytes.
self.writer.stream(refs.data, owned_face.data());
self.writer.stream(refs.data, face.data());
}
}

115
src/font.rs Normal file
View File

@ -0,0 +1,115 @@
//! Font handling.
use std::fmt::{self, Display, Formatter};
use fontdock::FaceFromVec;
/// An owned font face.
pub struct FaceBuf {
data: Box<[u8]>,
ttf: ttf_parser::Face<'static>,
buzz: rustybuzz::Face<'static>,
}
impl FaceBuf {
/// The raw face data.
pub fn data(&self) -> &[u8] {
&self.data
}
/// Get a reference to the underlying ttf-parser face.
pub fn ttf(&self) -> &ttf_parser::Face<'_> {
// We can't implement Deref because that would leak the internal 'static
// lifetime.
&self.ttf
}
/// Get a reference to the underlying rustybuzz face.
pub fn buzz(&self) -> &rustybuzz::Face<'_> {
// We can't implement Deref because that would leak the internal 'static
// lifetime.
&self.buzz
}
}
impl FaceFromVec for FaceBuf {
fn from_vec(vec: Vec<u8>, i: u32) -> Option<Self> {
let data = vec.into_boxed_slice();
// SAFETY: The slices's location is stable in memory since we don't
// touch it and it can't be touched from outside this type.
let slice: &'static [u8] =
unsafe { std::slice::from_raw_parts(data.as_ptr(), data.len()) };
Some(Self {
data,
ttf: ttf_parser::Face::from_slice(slice, i).ok()?,
buzz: rustybuzz::Face::from_slice(slice, i)?,
})
}
}
/// Identifies a vertical metric of a font.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum VerticalFontMetric {
/// The distance from the baseline to the typographic ascender.
///
/// Corresponds to the typographic ascender from the `OS/2` table if present
/// and falls back to the ascender from the `hhea` table otherwise.
Ascender,
/// The approximate height of uppercase letters.
CapHeight,
/// The approximate height of non-ascending lowercase letters.
XHeight,
/// The baseline on which the letters rest.
Baseline,
/// The distance from the baseline to the typographic descender.
///
/// Corresponds to the typographic descender from the `OS/2` table if
/// present and falls back to the descender from the `hhea` table otherwise.
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 {
Self::Ascender => "ascender",
Self::CapHeight => "cap-height",
Self::XHeight => "x-height",
Self::Baseline => "baseline",
Self::Descender => "descender",
})
}
}

View File

@ -1,7 +1,9 @@
use super::Shaped;
use fontdock::FaceId;
use ttf_parser::GlyphId;
use crate::color::Color;
use crate::env::ResourceId;
use crate::geom::{Path, Point, Size};
use crate::geom::{Length, Path, Point, Size};
/// A finished layout with elements at fixed positions.
#[derive(Debug, Clone, PartialEq)]
@ -36,13 +38,67 @@ impl Frame {
#[derive(Debug, Clone, PartialEq)]
pub enum Element {
/// Shaped text.
Text(Shaped),
Text(ShapedText),
/// A geometric shape.
Geometry(Geometry),
/// A raster image.
Image(Image),
}
/// A shaped run of text.
#[derive(Debug, Clone, PartialEq)]
pub struct ShapedText {
/// The font face the text was shaped with.
pub face: 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>,
}
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,
}
}
/// 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);
}
bytes
}
}
/// A shape with some kind of fill.
#[derive(Debug, Clone, PartialEq)]
pub struct Geometry {

View File

@ -1,205 +1,129 @@
//! Super-basic text shaping.
//!
//! This is really only suited for simple Latin text. It picks the most suitable
//! font for each individual character. When the direction is right-to-left, the
//! word is spelled backwards. Vertical shaping is not supported.
use std::fmt::{self, Debug, Display, Formatter};
use fontdock::{FaceId, FontVariant};
use ttf_parser::{Face, GlyphId};
use fontdock::FaceId;
use rustybuzz::UnicodeBuffer;
use ttf_parser::GlyphId;
use super::{Element, Frame, ShapedText};
use crate::env::FontLoader;
use crate::exec::FamilyMap;
use crate::geom::{Dir, Length, Point, Size};
use crate::layout::{Element, Fill, Frame};
use crate::exec::FontProps;
use crate::geom::{Length, Point, Size};
/// A shaped run of text.
#[derive(Clone, PartialEq)]
pub struct Shaped {
/// The shaped text.
pub text: String,
/// The font face the text was shaped with.
pub face: FaceId,
/// 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 font size.
pub font_size: Length,
/// The glyph fill color / texture.
pub color: Fill,
}
impl Shaped {
/// Create a new shape run with empty `text`, `glyphs` and `offsets`.
pub fn new(face: FaceId, font_size: Length, color: Fill) -> Self {
Self {
text: String::new(),
face,
glyphs: vec![],
offsets: vec![],
font_size,
color,
}
}
/// 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);
}
bytes
}
}
impl Debug for Shaped {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Debug::fmt(&self.text, f)
}
}
/// Identifies a vertical metric of a font.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum VerticalFontMetric {
/// The distance from the baseline to the typographic ascender.
///
/// Corresponds to the typographic ascender from the `OS/2` table if present
/// and falls back to the ascender from the `hhea` table otherwise.
Ascender,
/// The approximate height of uppercase letters.
CapHeight,
/// The approximate height of non-ascending lowercase letters.
XHeight,
/// The baseline on which the letters rest.
Baseline,
/// The distance from the baseline to the typographic descender.
///
/// Corresponds to the typographic descender from the `OS/2` table if
/// present and falls back to the descender from the `hhea` table otherwise.
Descender,
}
impl Display for VerticalFontMetric {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(match self {
Self::Ascender => "ascender",
Self::CapHeight => "cap-height",
Self::XHeight => "x-height",
Self::Baseline => "baseline",
Self::Descender => "descender",
})
}
}
/// Shape text into a frame containing [`Shaped`] runs.
pub fn shape(
text: &str,
dir: Dir,
families: &FamilyMap,
variant: FontVariant,
font_size: Length,
top_edge: VerticalFontMetric,
bottom_edge: VerticalFontMetric,
color: Fill,
loader: &mut FontLoader,
) -> Frame {
/// Shape text into a frame containing shaped [`ShapedText`] runs.
pub fn shape(text: &str, loader: &mut FontLoader, props: &FontProps) -> Frame {
let mut frame = Frame::new(Size::new(Length::ZERO, Length::ZERO));
let mut shaped = Shaped::new(FaceId::MAX, font_size, color);
let mut width = Length::ZERO;
let mut top = Length::ZERO;
let mut bottom = Length::ZERO;
// Create an iterator with conditional direction.
let mut forwards = text.chars();
let mut backwards = text.chars().rev();
let chars: &mut dyn Iterator<Item = char> = if dir.is_positive() {
&mut forwards
} else {
&mut backwards
};
for c in chars {
for family in families.iter() {
if let Some(id) = loader.query(family, variant) {
let face = loader.face(id).get();
let (glyph, glyph_width) = match lookup_glyph(face, c) {
Some(v) => v,
None => continue,
};
let units_per_em = f64::from(face.units_per_em().unwrap_or(1000));
let convert = |units| units / units_per_em * font_size;
// Flush the buffer and reset the metrics if we use a new font face.
if shaped.face != id {
place(&mut frame, shaped, width, top, bottom);
shaped = Shaped::new(id, font_size, color);
width = Length::ZERO;
top = convert(f64::from(lookup_metric(face, top_edge)));
bottom = convert(f64::from(lookup_metric(face, bottom_edge)));
}
shaped.text.push(c);
shaped.glyphs.push(glyph);
shaped.offsets.push(width);
width += convert(f64::from(glyph_width));
break;
}
}
}
place(&mut frame, shaped, width, top, bottom);
shape_segment(&mut frame, text, props.families.iter(), None, loader, props);
frame
}
/// Shape text into a frame with font fallback using the `families` iterator.
fn shape_segment<'a>(
frame: &mut Frame,
text: &str,
mut families: impl Iterator<Item = &'a str> + Clone,
mut first: Option<FaceId>,
loader: &mut FontLoader,
props: &FontProps,
) {
// Select the font family.
let (id, fallback) = loop {
// Try to load the next available font family.
match families.next() {
Some(family) => match loader.query(family, props.variant) {
Some(id) => break (id, true),
None => {}
},
// We're out of families, so we don't do any more fallback and just
// shape the tofus with the first face we originally used.
None => match first {
Some(id) => break (id, false),
None => return,
},
}
};
// Register that this is the first available font.
let face = loader.face(id);
if first.is_none() {
first = Some(id);
}
// Find out some metrics and prepare the shaped text container.
let ttf = face.ttf();
let units_per_em = f64::from(ttf.units_per_em().unwrap_or(1000));
let convert = |units| f64::from(units) / units_per_em * props.size;
let top = convert(i32::from(props.top_edge.lookup(ttf)));
let bottom = convert(i32::from(props.bottom_edge.lookup(ttf)));
let mut shaped = ShapedText::new(id, props.size, top, bottom, props.color);
// Fill the buffer with our text.
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
buffer.guess_segment_properties();
// Find out the text direction.
// TODO: Replace this once we do BiDi.
let rtl = matches!(buffer.direction(), rustybuzz::Direction::RightToLeft);
// Shape!
let glyphs = rustybuzz::shape(face.buzz(), &[], buffer);
let info = glyphs.glyph_infos();
let pos = glyphs.glyph_positions();
let mut iter = info.iter().zip(pos).peekable();
while let Some((info, pos)) = iter.next() {
// Do font fallback if the glyph is a tofu.
if info.codepoint == 0 && fallback {
// Flush what we have so far.
if !shaped.glyphs.is_empty() {
place(frame, shaped);
shaped = ShapedText::new(id, props.size, top, bottom, props.color);
}
// Determine the start and end cluster index of the tofu sequence.
let mut start = info.cluster as usize;
let mut end = info.cluster as usize;
while let Some((info, _)) = iter.peek() {
if info.codepoint != 0 {
break;
}
end = info.cluster as usize;
iter.next();
}
// Because Harfbuzz outputs glyphs in visual order, the start
// cluster actually corresponds to the last codepoint in
// right-to-left text.
if rtl {
assert!(end <= start);
std::mem::swap(&mut start, &mut end);
}
// The end cluster index points right before the last character that
// mapped to the tofu sequence. So we have to offset the end by one
// char.
let offset = text[end ..].chars().next().unwrap().len_utf8();
let range = start .. end + offset;
// Recursively shape the tofu sequence with the next family.
shape_segment(frame, &text[range], families.clone(), first, loader, props);
} else {
// Add the glyph to the shaped output.
// TODO: Don't ignore y_advance and y_offset.
let glyph = GlyphId(info.codepoint as u16);
shaped.glyphs.push(glyph);
shaped.offsets.push(shaped.width + convert(pos.x_offset));
shaped.width += convert(pos.x_advance);
}
}
if !shaped.glyphs.is_empty() {
place(frame, shaped)
}
}
/// Place shaped text into a frame.
fn place(frame: &mut Frame, shaped: Shaped, width: Length, top: Length, bottom: Length) {
if !shaped.text.is_empty() {
frame.push(Point::new(frame.size.width, top), Element::Text(shaped));
frame.size.width += width;
frame.size.height = frame.size.height.max(top - bottom);
}
}
/// Look up the glyph for `c` and returns its index alongside its advance width.
fn lookup_glyph(face: &Face, c: char) -> Option<(GlyphId, u16)> {
let glyph = face.glyph_index(c)?;
let width = face.glyph_hor_advance(glyph)?;
Some((glyph, width))
}
/// Look up a vertical metric.
fn lookup_metric(face: &Face, metric: VerticalFontMetric) -> i16 {
match metric {
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: &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: &Face) -> i16 {
// See `lookup_ascender` for reason.
face.typographic_descender().unwrap_or_else(|| face.descender())
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

@ -1,50 +1,25 @@
use std::fmt::{self, Debug, Formatter};
use std::rc::Rc;
use fontdock::FontVariant;
use super::*;
use crate::exec::FamilyMap;
use crate::exec::FontProps;
/// A consecutive, styled run of text.
#[derive(Clone, PartialEq)]
pub struct TextNode {
/// The text.
pub text: String,
/// The text direction.
pub dir: Dir,
/// How to align this text node in its parent.
pub aligns: LayoutAligns,
/// The list of font families for shaping.
pub families: Rc<FamilyMap>,
/// The font variant,
pub variant: FontVariant,
/// The font size.
pub font_size: Length,
/// The top end of the text bounding box.
pub top_edge: VerticalFontMetric,
/// The bottom end of the text bounding box.
pub bottom_edge: VerticalFontMetric,
/// The glyph fill.
pub color: Fill,
/// The text.
pub text: String,
/// Properties used for font selection and layout.
pub props: FontProps,
}
impl Layout for TextNode {
fn layout(&self, ctx: &mut LayoutContext, _: &Areas) -> Fragment {
Fragment::Frame(
shape(
&self.text,
self.dir,
&self.families,
self.variant,
self.font_size,
self.top_edge,
self.bottom_edge,
self.color,
&mut ctx.env.fonts,
),
self.aligns,
)
let frame = shape(&self.text, &mut ctx.env.fonts, &self.props);
Fragment::Frame(frame, self.aligns)
}
}

View File

@ -37,6 +37,7 @@ pub mod color;
pub mod env;
pub mod exec;
pub mod export;
pub mod font;
pub mod geom;
pub mod layout;
pub mod library;

View File

@ -32,8 +32,8 @@ use fontdock::{FontStyle, FontWeight};
use crate::eval::{AnyValue, FuncValue, Scope};
use crate::eval::{EvalContext, FuncArgs, TemplateValue, Value};
use crate::exec::{Exec, ExecContext, FontFamily};
use crate::font::VerticalFontMetric;
use crate::geom::*;
use crate::layout::VerticalFontMetric;
use crate::syntax::{Node, Spanned};
/// Construct a scope containing all standard library definitions.

View File

@ -27,7 +27,7 @@ fn spacing_impl(ctx: &mut EvalContext, args: &mut FuncArgs, axis: SpecAxis) -> V
let spacing: Option<Linear> = args.require(ctx, "spacing");
Value::template("spacing", move |ctx| {
if let Some(linear) = spacing {
let amount = linear.resolve(ctx.state.font.font_size());
let amount = linear.resolve(ctx.state.font.resolve_size());
let spacing = SpacingNode { amount, softness: 0 };
if axis == ctx.state.dirs.main.axis() {
ctx.push_into_stack(spacing);

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: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 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: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 776 B

After

Width:  |  Height:  |  Size: 770 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.5 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.2 KiB

After

Width:  |  Height:  |  Size: 1.2 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: 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.1 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.7 KiB

After

Width:  |  Height:  |  Size: 3.8 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.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 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: 36 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,38 @@
// Test complex text shaping.
---
// Test ligatures.
// This should create an "fi" ligature.
Le fira
// This should just shape nicely.
#font("Noto Sans Arabic")
منش إلا بسم الله
// This should form a three-member family.
#font("Twitter Color Emoji")
👩‍👩‍👦 🤚🏿
// These two shouldn't be affected by a zero-width joiner.
🏞‍🌋
---
// Test font fallback.
#font("EB Garamond", "Noto Sans Arabic", "Twitter Color Emoji")
// Font fallback for emoji.
A😀B
// Font fallback for entire text.
منش إلا بسم الله
// Font fallback in right-to-left text.
ب🐈😀سم
// Multi-layer font fallback.
Aب😀🏞سمB
// Tofus are rendered with the first font.
A🐈中文B

View File

@ -21,7 +21,7 @@ use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value};
use typst::exec::State;
use typst::export::pdf;
use typst::geom::{self, Length, Point, Sides, Size};
use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, Shaped};
use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, ShapedText};
use typst::library;
use typst::parse::{LineMap, Scanner};
use typst::pretty::pretty;
@ -391,15 +391,18 @@ fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap {
for &(pos, ref element) in &frame.elements {
let pos = origin + pos;
let x = pos.x.to_pt() as f32;
let y = pos.y.to_pt() as f32;
let ts = ts.pre_translate(x, y);
match element {
Element::Text(shaped) => {
draw_text(&mut canvas, env, ts, pos, shaped);
draw_text(&mut canvas, env, ts, shaped);
}
Element::Image(image) => {
draw_image(&mut canvas, env, ts, pos, image);
draw_image(&mut canvas, env, ts, image);
}
Element::Geometry(geom) => {
draw_geometry(&mut canvas, ts, pos, geom);
draw_geometry(&mut canvas, ts, geom);
}
}
}
@ -410,18 +413,18 @@ fn draw(env: &Env, frames: &[Frame], pixel_per_pt: f32) -> Pixmap {
canvas
}
fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, pos: Point, shaped: &Shaped) {
let face = env.fonts.face(shaped.face).get();
fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &ShapedText) {
let ttf = env.fonts.face(shaped.face).ttf();
for (&glyph, &offset) in shaped.glyphs.iter().zip(&shaped.offsets) {
let units_per_em = face.units_per_em().unwrap_or(1000);
let units_per_em = ttf.units_per_em().unwrap_or(1000);
let x = (pos.x + offset).to_pt() as f32;
let y = pos.y.to_pt() as f32;
let scale = (shaped.font_size / units_per_em as f64).to_pt() as f32;
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);
// Try drawing SVG if present.
if let Some(tree) = face
if let Some(tree) = ttf
.glyph_svg_image(glyph)
.and_then(|data| std::str::from_utf8(data).ok())
.map(|svg| {
@ -433,11 +436,9 @@ fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, pos: Point, shaped:
for child in tree.root().children() {
if let usvg::NodeKind::Path(node) = &*child.borrow() {
let path = convert_usvg_path(&node.data);
let transform = convert_usvg_transform(node.transform);
let ts = transform
.post_concat(Transform::from_row(scale, 0.0, 0.0, scale, x, y))
let ts = convert_usvg_transform(node.transform)
.post_scale(s, s)
.post_concat(ts);
if let Some(fill) = &node.fill {
let (paint, fill_rule) = convert_usvg_fill(fill);
canvas.fill_path(&path, &paint, fill_rule, ts, None);
@ -450,9 +451,9 @@ fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, pos: Point, shaped:
// Otherwise, draw normal outline.
let mut builder = WrappedPathBuilder(tiny_skia::PathBuilder::new());
if face.outline_glyph(glyph, &mut builder).is_some() {
if ttf.outline_glyph(glyph, &mut builder).is_some() {
let path = builder.0.finish().unwrap();
let ts = Transform::from_row(scale, 0.0, 0.0, -scale, x, y).post_concat(ts);
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);
@ -460,11 +461,7 @@ fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, pos: Point, shaped:
}
}
fn draw_geometry(canvas: &mut Pixmap, ts: Transform, pos: Point, element: &Geometry) {
let x = pos.x.to_pt() as f32;
let y = pos.y.to_pt() as f32;
let ts = Transform::from_translate(x, y).post_concat(ts);
fn draw_geometry(canvas: &mut Pixmap, ts: Transform, element: &Geometry) {
let paint = convert_typst_fill(element.fill);
let rule = FillRule::default();
@ -486,13 +483,7 @@ fn draw_geometry(canvas: &mut Pixmap, ts: Transform, pos: Point, element: &Geome
};
}
fn draw_image(
canvas: &mut Pixmap,
env: &Env,
ts: Transform,
pos: Point,
element: &Image,
) {
fn draw_image(canvas: &mut Pixmap, env: &Env, ts: Transform, element: &Image) {
let img = &env.resources.loaded::<ImageResource>(element.res);
let mut pixmap = Pixmap::new(img.buf.width(), img.buf.height()).unwrap();
@ -501,8 +492,6 @@ fn draw_image(
*dest = ColorU8::from_rgba(r, g, b, a).premultiply();
}
let x = pos.x.to_pt() as f32;
let y = pos.y.to_pt() as f32;
let view_width = element.size.width.to_pt() as f32;
let view_height = element.size.height.to_pt() as f32;
let scale_x = view_width as f32 / pixmap.width() as f32;
@ -514,10 +503,10 @@ fn draw_image(
SpreadMode::Pad,
FilterQuality::Bilinear,
1.0,
Transform::from_row(scale_x, 0.0, 0.0, scale_y, x, y),
Transform::from_row(scale_x, 0.0, 0.0, scale_y, 0.0, 0.0),
);
let rect = Rect::from_xywh(x, y, view_width, view_height).unwrap();
let rect = Rect::from_xywh(0.0, 0.0, view_width, view_height).unwrap();
canvas.fill_rect(rect, &paint, ts, None);
}