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>
@ -26,7 +26,8 @@ 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" }
|
||||||
ttf-parser = "0.8.2"
|
rustybuzz = "0.3"
|
||||||
|
ttf-parser = "0.9"
|
||||||
unicode-xid = "0.2"
|
unicode-xid = "0.2"
|
||||||
anyhow = { version = "1", optional = true }
|
anyhow = { version = "1", optional = true }
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
|
BIN
fonts/NotoSansArabic-Regular.ttf
Normal file
41
src/env.rs
@ -7,14 +7,15 @@ use std::fs;
|
|||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use fontdock::{FaceFromVec, FaceId, FontSource};
|
use fontdock::{FaceId, FontSource};
|
||||||
use image::io::Reader as ImageReader;
|
use image::io::Reader as ImageReader;
|
||||||
use image::{DynamicImage, GenericImageView, ImageFormat};
|
use image::{DynamicImage, GenericImageView, ImageFormat};
|
||||||
use ttf_parser::Face;
|
|
||||||
|
|
||||||
#[cfg(feature = "fs")]
|
#[cfg(feature = "fs")]
|
||||||
use fontdock::{FsIndex, FsSource};
|
use fontdock::{FsIndex, FsSource};
|
||||||
|
|
||||||
|
use crate::font::FaceBuf;
|
||||||
|
|
||||||
/// Encapsulates all environment dependencies (fonts, resources).
|
/// Encapsulates all environment dependencies (fonts, resources).
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Env {
|
pub struct Env {
|
||||||
@ -47,42 +48,6 @@ impl Env {
|
|||||||
/// A font loader that is backed by a dynamic source.
|
/// A font loader that is backed by a dynamic source.
|
||||||
pub type FontLoader = fontdock::FontLoader<Box<dyn FontSource<Face = FaceBuf>>>;
|
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`].
|
/// Simplify font loader construction from an [`FsIndex`].
|
||||||
#[cfg(feature = "fs")]
|
#[cfg(feature = "fs")]
|
||||||
pub trait FsIndexExt {
|
pub trait FsIndexExt {
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
use std::mem;
|
use std::mem;
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use fontdock::FontStyle;
|
|
||||||
|
|
||||||
use super::{Exec, FontFamily, State};
|
use super::{Exec, FontFamily, State};
|
||||||
use crate::diag::{Diag, DiagSet, Pass};
|
use crate::diag::{Diag, DiagSet, Pass};
|
||||||
@ -87,7 +84,7 @@ impl<'a> ExecContext<'a> {
|
|||||||
|
|
||||||
/// Push a word space into the active paragraph.
|
/// Push a word space into the active paragraph.
|
||||||
pub fn push_space(&mut self) {
|
pub fn push_space(&mut self) {
|
||||||
let em = self.state.font.font_size();
|
let em = self.state.font.resolve_size();
|
||||||
self.push(SpacingNode {
|
self.push(SpacingNode {
|
||||||
amount: self.state.par.word_spacing.resolve(em),
|
amount: self.state.par.word_spacing.resolve(em),
|
||||||
softness: 1,
|
softness: 1,
|
||||||
@ -103,19 +100,19 @@ impl<'a> ExecContext<'a> {
|
|||||||
|
|
||||||
while let Some(c) = scanner.eat_merging_crlf() {
|
while let Some(c) = scanner.eat_merging_crlf() {
|
||||||
if is_newline(c) {
|
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();
|
self.push_linebreak();
|
||||||
} else {
|
} else {
|
||||||
line.push(c);
|
line.push(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.push(self.make_text_node(line));
|
self.push(TextNode::new(line, &self.state));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a forced line break.
|
/// Apply a forced line break.
|
||||||
pub fn push_linebreak(&mut self) {
|
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 {
|
self.push_into_stack(SpacingNode {
|
||||||
amount: self.state.par.leading.resolve(em),
|
amount: self.state.par.leading.resolve(em),
|
||||||
softness: 2,
|
softness: 2,
|
||||||
@ -124,7 +121,7 @@ impl<'a> ExecContext<'a> {
|
|||||||
|
|
||||||
/// Apply a forced paragraph break.
|
/// Apply a forced paragraph break.
|
||||||
pub fn push_parbreak(&mut self) {
|
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 {
|
self.push_into_stack(SpacingNode {
|
||||||
amount: self.state.par.spacing.resolve(em),
|
amount: self.state.par.spacing.resolve(em),
|
||||||
softness: 1,
|
softness: 1,
|
||||||
@ -154,36 +151,6 @@ impl<'a> ExecContext<'a> {
|
|||||||
result
|
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.
|
/// Finish the active paragraph.
|
||||||
fn finish_par(&mut self) {
|
fn finish_par(&mut self) {
|
||||||
let mut par = mem::replace(&mut self.par, ParNode::new(&self.state));
|
let mut par = mem::replace(&mut self.par, ParNode::new(&self.state));
|
||||||
@ -292,7 +259,7 @@ impl StackNode {
|
|||||||
|
|
||||||
impl ParNode {
|
impl ParNode {
|
||||||
fn new(state: &State) -> Self {
|
fn new(state: &State) -> Self {
|
||||||
let em = state.font.font_size();
|
let em = state.font.resolve_size();
|
||||||
Self {
|
Self {
|
||||||
dirs: state.dirs,
|
dirs: state.dirs,
|
||||||
aligns: state.aligns,
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,8 +4,9 @@ use std::rc::Rc;
|
|||||||
use fontdock::{FontStretch, FontStyle, FontVariant, FontWeight};
|
use fontdock::{FontStretch, FontStyle, FontVariant, FontWeight};
|
||||||
|
|
||||||
use crate::color::{Color, RgbaColor};
|
use crate::color::{Color, RgbaColor};
|
||||||
|
use crate::font::VerticalFontMetric;
|
||||||
use crate::geom::*;
|
use crate::geom::*;
|
||||||
use crate::layout::{Fill, VerticalFontMetric};
|
use crate::layout::Fill;
|
||||||
use crate::paper::{Paper, PaperClass, PAPER_A4};
|
use crate::paper::{Paper, PaperClass, PAPER_A4};
|
||||||
|
|
||||||
/// The evaluation state.
|
/// The evaluation state.
|
||||||
@ -100,7 +101,7 @@ impl Default for ParState {
|
|||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct FontState {
|
pub struct FontState {
|
||||||
/// A list of font families with generic class definitions.
|
/// A list of font families with generic class definitions.
|
||||||
pub families: Rc<FamilyMap>,
|
pub families: Rc<FamilyList>,
|
||||||
/// The selected font variant.
|
/// The selected font variant.
|
||||||
pub variant: FontVariant,
|
pub variant: FontVariant,
|
||||||
/// The font size.
|
/// The font size.
|
||||||
@ -111,32 +112,58 @@ pub struct FontState {
|
|||||||
pub top_edge: VerticalFontMetric,
|
pub top_edge: VerticalFontMetric,
|
||||||
/// The bottom end of the text bounding box.
|
/// The bottom end of the text bounding box.
|
||||||
pub bottom_edge: VerticalFontMetric,
|
pub bottom_edge: VerticalFontMetric,
|
||||||
|
/// The glyph fill color / texture.
|
||||||
|
pub color: Fill,
|
||||||
/// Whether the strong toggle is active or inactive. This determines
|
/// Whether the strong toggle is active or inactive. This determines
|
||||||
/// whether the next `*` adds or removes font weight.
|
/// whether the next `*` adds or removes font weight.
|
||||||
pub strong: bool,
|
pub strong: bool,
|
||||||
/// Whether the emphasis toggle is active or inactive. This determines
|
/// Whether the emphasis toggle is active or inactive. This determines
|
||||||
/// whether the next `_` makes italic or non-italic.
|
/// whether the next `_` makes italic or non-italic.
|
||||||
pub emph: bool,
|
pub emph: bool,
|
||||||
/// The glyph fill color / texture.
|
|
||||||
pub color: Fill,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FontState {
|
impl FontState {
|
||||||
/// Access the `families` mutably.
|
/// The resolved font size.
|
||||||
pub fn families_mut(&mut self) -> &mut FamilyMap {
|
pub fn resolve_size(&self) -> Length {
|
||||||
Rc::make_mut(&mut self.families)
|
self.scale.resolve(self.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The absolute font size.
|
/// Resolve font properties.
|
||||||
pub fn font_size(&self) -> Length {
|
pub fn resolve_props(&self) -> FontProps {
|
||||||
self.scale.resolve(self.size)
|
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 {
|
impl Default for FontState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
families: Rc::new(FamilyMap::default()),
|
families: Rc::new(FamilyList::default()),
|
||||||
variant: FontVariant {
|
variant: FontVariant {
|
||||||
style: FontStyle::Normal,
|
style: FontStyle::Normal,
|
||||||
weight: FontWeight::REGULAR,
|
weight: FontWeight::REGULAR,
|
||||||
@ -146,16 +173,33 @@ impl Default for FontState {
|
|||||||
top_edge: VerticalFontMetric::CapHeight,
|
top_edge: VerticalFontMetric::CapHeight,
|
||||||
bottom_edge: VerticalFontMetric::Baseline,
|
bottom_edge: VerticalFontMetric::Baseline,
|
||||||
scale: Linear::ONE,
|
scale: Linear::ONE,
|
||||||
|
color: Fill::Color(Color::Rgba(RgbaColor::BLACK)),
|
||||||
strong: false,
|
strong: false,
|
||||||
emph: 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.
|
/// Font family definitions.
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
pub struct FamilyMap {
|
pub struct FamilyList {
|
||||||
/// The user-defined list of font families.
|
/// The user-defined list of font families.
|
||||||
pub list: Vec<FontFamily>,
|
pub list: Vec<FontFamily>,
|
||||||
/// Definition of serif font families.
|
/// Definition of serif font families.
|
||||||
@ -168,9 +212,9 @@ pub struct FamilyMap {
|
|||||||
pub base: Vec<String>,
|
pub base: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FamilyMap {
|
impl FamilyList {
|
||||||
/// Flat iterator over this map's family names.
|
/// 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
|
self.list
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(move |family: &FontFamily| {
|
.flat_map(move |family: &FontFamily| {
|
||||||
@ -186,7 +230,7 @@ impl FamilyMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FamilyMap {
|
impl Default for FamilyList {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
list: vec![FontFamily::Serif],
|
list: vec![FontFamily::Serif],
|
||||||
|
@ -187,14 +187,15 @@ 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.font_size != size {
|
if shaped.face != face || shaped.size != size {
|
||||||
face = shaped.face;
|
face = shaped.face;
|
||||||
size = shaped.font_size;
|
size = shaped.size;
|
||||||
|
|
||||||
let name = format!("F{}", self.fonts.map(shaped.face));
|
let name = format!("F{}", self.fonts.map(shaped.face));
|
||||||
text.font(Name(name.as_bytes()), size.to_pt() as f32);
|
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.matrix(1.0, 0.0, 0.0, 1.0, x, y);
|
||||||
text.show(&shaped.encode_glyphs_be());
|
text.show(&shaped.encode_glyphs_be());
|
||||||
}
|
}
|
||||||
@ -206,10 +207,10 @@ impl<'a> PdfExporter<'a> {
|
|||||||
|
|
||||||
fn write_fonts(&mut self) {
|
fn write_fonts(&mut self) {
|
||||||
for (refs, face_id) in self.refs.fonts().zip(self.fonts.layout_indices()) {
|
for (refs, face_id) in self.refs.fonts().zip(self.fonts.layout_indices()) {
|
||||||
let owned_face = self.env.fonts.face(face_id);
|
let face = self.env.fonts.face(face_id);
|
||||||
let face = owned_face.get();
|
let ttf = face.ttf();
|
||||||
|
|
||||||
let name = face
|
let name = ttf
|
||||||
.names()
|
.names()
|
||||||
.find(|entry| {
|
.find(|entry| {
|
||||||
entry.name_id() == name_id::POST_SCRIPT_NAME && entry.is_unicode()
|
entry.name_id() == name_id::POST_SCRIPT_NAME && entry.is_unicode()
|
||||||
@ -228,18 +229,18 @@ impl<'a> PdfExporter<'a> {
|
|||||||
|
|
||||||
let mut flags = FontFlags::empty();
|
let mut flags = FontFlags::empty();
|
||||||
flags.set(FontFlags::SERIF, name.contains("Serif"));
|
flags.set(FontFlags::SERIF, name.contains("Serif"));
|
||||||
flags.set(FontFlags::FIXED_PITCH, face.is_monospaced());
|
flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced());
|
||||||
flags.set(FontFlags::ITALIC, face.is_italic());
|
flags.set(FontFlags::ITALIC, ttf.is_italic());
|
||||||
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.
|
// 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 = |font_unit: f32| (1000.0 * em_per_unit * font_unit).round();
|
||||||
let convert_i16 = |font_unit: i16| convert(font_unit as f32);
|
let convert_i16 = |font_unit: i16| convert(font_unit as f32);
|
||||||
let convert_u16 = |font_unit: u16| 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(
|
let bbox = Rect::new(
|
||||||
convert_i16(global_bbox.x_min),
|
convert_i16(global_bbox.x_min),
|
||||||
convert_i16(global_bbox.y_min),
|
convert_i16(global_bbox.y_min),
|
||||||
@ -247,11 +248,11 @@ impl<'a> PdfExporter<'a> {
|
|||||||
convert_i16(global_bbox.y_max),
|
convert_i16(global_bbox.y_max),
|
||||||
);
|
);
|
||||||
|
|
||||||
let italic_angle = face.italic_angle().unwrap_or(0.0);
|
let italic_angle = ttf.italic_angle().unwrap_or(0.0);
|
||||||
let ascender = convert_i16(face.typographic_ascender().unwrap_or(0));
|
let ascender = convert_i16(ttf.typographic_ascender().unwrap_or(0));
|
||||||
let descender = convert_i16(face.typographic_descender().unwrap_or(0));
|
let descender = convert_i16(ttf.typographic_descender().unwrap_or(0));
|
||||||
let cap_height = face.capital_height().map(convert_i16);
|
let cap_height = ttf.capital_height().map(convert_i16);
|
||||||
let stem_v = 10.0 + 0.244 * (f32::from(face.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.
|
||||||
self.writer
|
self.writer
|
||||||
@ -269,9 +270,9 @@ impl<'a> PdfExporter<'a> {
|
|||||||
.font_descriptor(refs.font_descriptor)
|
.font_descriptor(refs.font_descriptor)
|
||||||
.widths()
|
.widths()
|
||||||
.individual(0, {
|
.individual(0, {
|
||||||
let num_glyphs = face.number_of_glyphs();
|
let num_glyphs = ttf.number_of_glyphs();
|
||||||
(0 .. num_glyphs).map(|g| {
|
(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))
|
convert_u16(advance.unwrap_or(0))
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@ -294,10 +295,10 @@ impl<'a> PdfExporter<'a> {
|
|||||||
self.writer
|
self.writer
|
||||||
.cmap(refs.cmap, &{
|
.cmap(refs.cmap, &{
|
||||||
let mut cmap = UnicodeCmap::new(cmap_name, system_info);
|
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| {
|
subtable.codepoints(|n| {
|
||||||
if let Some(c) = std::char::from_u32(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);
|
cmap.pair(g.0, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -309,7 +310,7 @@ impl<'a> PdfExporter<'a> {
|
|||||||
.system_info(system_info);
|
.system_info(system_info);
|
||||||
|
|
||||||
// Write the face's bytes.
|
// 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
@ -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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
use super::Shaped;
|
use fontdock::FaceId;
|
||||||
|
use ttf_parser::GlyphId;
|
||||||
|
|
||||||
use crate::color::Color;
|
use crate::color::Color;
|
||||||
use crate::env::ResourceId;
|
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.
|
/// A finished layout with elements at fixed positions.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@ -36,13 +38,67 @@ impl Frame {
|
|||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Element {
|
pub enum Element {
|
||||||
/// Shaped text.
|
/// Shaped text.
|
||||||
Text(Shaped),
|
Text(ShapedText),
|
||||||
/// 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.
|
||||||
|
#[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.
|
/// A shape with some kind of fill.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Geometry {
|
pub struct Geometry {
|
||||||
|
@ -1,205 +1,129 @@
|
|||||||
//! Super-basic text shaping.
|
use fontdock::FaceId;
|
||||||
//!
|
use rustybuzz::UnicodeBuffer;
|
||||||
//! This is really only suited for simple Latin text. It picks the most suitable
|
use ttf_parser::GlyphId;
|
||||||
//! 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 super::{Element, Frame, ShapedText};
|
||||||
use crate::env::FontLoader;
|
use crate::env::FontLoader;
|
||||||
use crate::exec::FamilyMap;
|
use crate::exec::FontProps;
|
||||||
use crate::geom::{Dir, Length, Point, Size};
|
use crate::geom::{Length, Point, Size};
|
||||||
use crate::layout::{Element, Fill, Frame};
|
|
||||||
|
|
||||||
/// A shaped run of text.
|
/// Shape text into a frame containing shaped [`ShapedText`] runs.
|
||||||
#[derive(Clone, PartialEq)]
|
pub fn shape(text: &str, loader: &mut FontLoader, props: &FontProps) -> Frame {
|
||||||
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 {
|
|
||||||
let mut frame = Frame::new(Size::new(Length::ZERO, Length::ZERO));
|
let mut frame = Frame::new(Size::new(Length::ZERO, Length::ZERO));
|
||||||
let mut shaped = Shaped::new(FaceId::MAX, font_size, color);
|
shape_segment(&mut frame, text, props.families.iter(), None, loader, props);
|
||||||
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);
|
|
||||||
|
|
||||||
frame
|
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.
|
/// Place shaped text into a frame.
|
||||||
fn place(frame: &mut Frame, shaped: Shaped, width: Length, top: Length, bottom: Length) {
|
fn place(frame: &mut Frame, shaped: ShapedText) {
|
||||||
if !shaped.text.is_empty() {
|
let offset = frame.size.width;
|
||||||
frame.push(Point::new(frame.size.width, top), Element::Text(shaped));
|
frame.size.width += shaped.width;
|
||||||
frame.size.width += width;
|
frame.size.height = frame.size.height.max(shaped.top - shaped.bottom);
|
||||||
frame.size.height = frame.size.height.max(top - bottom);
|
frame.push(Point::new(offset, shaped.top), Element::Text(shaped));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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())
|
|
||||||
}
|
}
|
||||||
|
@ -1,50 +1,25 @@
|
|||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use fontdock::FontVariant;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::exec::FamilyMap;
|
use crate::exec::FontProps;
|
||||||
|
|
||||||
/// A consecutive, styled run of text.
|
/// A consecutive, styled run of text.
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct TextNode {
|
pub struct TextNode {
|
||||||
/// The text.
|
|
||||||
pub text: String,
|
|
||||||
/// The text direction.
|
/// The text direction.
|
||||||
pub dir: Dir,
|
pub dir: Dir,
|
||||||
/// How to align this text node in its parent.
|
/// How to align this text node in its parent.
|
||||||
pub aligns: LayoutAligns,
|
pub aligns: LayoutAligns,
|
||||||
/// The list of font families for shaping.
|
/// The text.
|
||||||
pub families: Rc<FamilyMap>,
|
pub text: String,
|
||||||
/// The font variant,
|
/// Properties used for font selection and layout.
|
||||||
pub variant: FontVariant,
|
pub props: FontProps,
|
||||||
/// 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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Layout for TextNode {
|
impl Layout for TextNode {
|
||||||
fn layout(&self, ctx: &mut LayoutContext, _: &Areas) -> Fragment {
|
fn layout(&self, ctx: &mut LayoutContext, _: &Areas) -> Fragment {
|
||||||
Fragment::Frame(
|
let frame = shape(&self.text, &mut ctx.env.fonts, &self.props);
|
||||||
shape(
|
Fragment::Frame(frame, self.aligns)
|
||||||
&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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ pub mod color;
|
|||||||
pub mod env;
|
pub mod env;
|
||||||
pub mod exec;
|
pub mod exec;
|
||||||
pub mod export;
|
pub mod export;
|
||||||
|
pub mod font;
|
||||||
pub mod geom;
|
pub mod geom;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod library;
|
pub mod library;
|
||||||
|
@ -32,8 +32,8 @@ use fontdock::{FontStyle, FontWeight};
|
|||||||
use crate::eval::{AnyValue, FuncValue, Scope};
|
use crate::eval::{AnyValue, FuncValue, Scope};
|
||||||
use crate::eval::{EvalContext, FuncArgs, TemplateValue, Value};
|
use crate::eval::{EvalContext, FuncArgs, TemplateValue, Value};
|
||||||
use crate::exec::{Exec, ExecContext, FontFamily};
|
use crate::exec::{Exec, ExecContext, FontFamily};
|
||||||
|
use crate::font::VerticalFontMetric;
|
||||||
use crate::geom::*;
|
use crate::geom::*;
|
||||||
use crate::layout::VerticalFontMetric;
|
|
||||||
use crate::syntax::{Node, Spanned};
|
use crate::syntax::{Node, Spanned};
|
||||||
|
|
||||||
/// Construct a scope containing all standard library definitions.
|
/// Construct a scope containing all standard library definitions.
|
||||||
|
@ -27,7 +27,7 @@ fn spacing_impl(ctx: &mut EvalContext, args: &mut FuncArgs, axis: SpecAxis) -> V
|
|||||||
let spacing: Option<Linear> = args.require(ctx, "spacing");
|
let spacing: Option<Linear> = args.require(ctx, "spacing");
|
||||||
Value::template("spacing", move |ctx| {
|
Value::template("spacing", move |ctx| {
|
||||||
if let Some(linear) = spacing {
|
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 };
|
let spacing = SpacingNode { amount, softness: 0 };
|
||||||
if axis == ctx.state.dirs.main.axis() {
|
if axis == ctx.state.dirs.main.axis() {
|
||||||
ctx.push_into_stack(spacing);
|
ctx.push_into_stack(spacing);
|
||||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 776 B After Width: | Height: | Size: 770 B |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 36 KiB |
BIN
tests/ref/text/basic.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
tests/ref/text/complex.png
Normal file
After Width: | Height: | Size: 12 KiB |
38
tests/typ/text/complex.typ
Normal 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
|
@ -21,7 +21,7 @@ use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value};
|
|||||||
use typst::exec::State;
|
use typst::exec::State;
|
||||||
use typst::export::pdf;
|
use typst::export::pdf;
|
||||||
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, Shaped};
|
use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, ShapedText};
|
||||||
use typst::library;
|
use typst::library;
|
||||||
use typst::parse::{LineMap, Scanner};
|
use typst::parse::{LineMap, Scanner};
|
||||||
use typst::pretty::pretty;
|
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 {
|
for &(pos, ref element) in &frame.elements {
|
||||||
let pos = origin + pos;
|
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 {
|
match element {
|
||||||
Element::Text(shaped) => {
|
Element::Text(shaped) => {
|
||||||
draw_text(&mut canvas, env, ts, pos, shaped);
|
draw_text(&mut canvas, env, ts, shaped);
|
||||||
}
|
}
|
||||||
Element::Image(image) => {
|
Element::Image(image) => {
|
||||||
draw_image(&mut canvas, env, ts, pos, image);
|
draw_image(&mut canvas, env, ts, image);
|
||||||
}
|
}
|
||||||
Element::Geometry(geom) => {
|
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
|
canvas
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, pos: Point, shaped: &Shaped) {
|
fn draw_text(canvas: &mut Pixmap, env: &Env, ts: Transform, shaped: &ShapedText) {
|
||||||
let face = env.fonts.face(shaped.face).get();
|
let ttf = env.fonts.face(shaped.face).ttf();
|
||||||
|
|
||||||
for (&glyph, &offset) in shaped.glyphs.iter().zip(&shaped.offsets) {
|
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 x = offset.to_pt() as f32;
|
||||||
let y = pos.y.to_pt() as f32;
|
let s = (shaped.size / units_per_em as f64).to_pt() as f32;
|
||||||
let scale = (shaped.font_size / units_per_em as f64).to_pt() as f32;
|
let ts = ts.pre_translate(x, 0.0);
|
||||||
|
|
||||||
// Try drawing SVG if present.
|
// Try drawing SVG if present.
|
||||||
if let Some(tree) = face
|
if let Some(tree) = ttf
|
||||||
.glyph_svg_image(glyph)
|
.glyph_svg_image(glyph)
|
||||||
.and_then(|data| std::str::from_utf8(data).ok())
|
.and_then(|data| std::str::from_utf8(data).ok())
|
||||||
.map(|svg| {
|
.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() {
|
for child in tree.root().children() {
|
||||||
if let usvg::NodeKind::Path(node) = &*child.borrow() {
|
if let usvg::NodeKind::Path(node) = &*child.borrow() {
|
||||||
let path = convert_usvg_path(&node.data);
|
let path = convert_usvg_path(&node.data);
|
||||||
let transform = convert_usvg_transform(node.transform);
|
let ts = convert_usvg_transform(node.transform)
|
||||||
let ts = transform
|
.post_scale(s, s)
|
||||||
.post_concat(Transform::from_row(scale, 0.0, 0.0, scale, x, y))
|
|
||||||
.post_concat(ts);
|
.post_concat(ts);
|
||||||
|
|
||||||
if let Some(fill) = &node.fill {
|
if let Some(fill) = &node.fill {
|
||||||
let (paint, fill_rule) = convert_usvg_fill(fill);
|
let (paint, fill_rule) = convert_usvg_fill(fill);
|
||||||
canvas.fill_path(&path, &paint, fill_rule, ts, None);
|
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.
|
// Otherwise, draw normal outline.
|
||||||
let mut builder = WrappedPathBuilder(tiny_skia::PathBuilder::new());
|
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 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);
|
let mut paint = convert_typst_fill(shaped.color);
|
||||||
paint.anti_alias = true;
|
paint.anti_alias = true;
|
||||||
canvas.fill_path(&path, &paint, FillRule::default(), ts, None);
|
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) {
|
fn draw_geometry(canvas: &mut Pixmap, ts: Transform, 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);
|
|
||||||
|
|
||||||
let paint = convert_typst_fill(element.fill);
|
let paint = convert_typst_fill(element.fill);
|
||||||
let rule = FillRule::default();
|
let rule = FillRule::default();
|
||||||
|
|
||||||
@ -486,13 +483,7 @@ fn draw_geometry(canvas: &mut Pixmap, ts: Transform, pos: Point, element: &Geome
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_image(
|
fn draw_image(canvas: &mut Pixmap, env: &Env, ts: Transform, element: &Image) {
|
||||||
canvas: &mut Pixmap,
|
|
||||||
env: &Env,
|
|
||||||
ts: Transform,
|
|
||||||
pos: Point,
|
|
||||||
element: &Image,
|
|
||||||
) {
|
|
||||||
let img = &env.resources.loaded::<ImageResource>(element.res);
|
let img = &env.resources.loaded::<ImageResource>(element.res);
|
||||||
|
|
||||||
let mut pixmap = Pixmap::new(img.buf.width(), img.buf.height()).unwrap();
|
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();
|
*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_width = element.size.width.to_pt() as f32;
|
||||||
let view_height = element.size.height.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;
|
let scale_x = view_width as f32 / pixmap.width() as f32;
|
||||||
@ -514,10 +503,10 @@ fn draw_image(
|
|||||||
SpreadMode::Pad,
|
SpreadMode::Pad,
|
||||||
FilterQuality::Bilinear,
|
FilterQuality::Bilinear,
|
||||||
1.0,
|
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);
|
canvas.fill_rect(rect, &paint, ts, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|