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"] }
|
||||
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 }
|
||||
|
BIN
fonts/NotoSansArabic-Regular.ttf
Normal file
41
src/env.rs
@ -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 {
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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],
|
||||
|
@ -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
@ -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::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 {
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
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::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);
|
||||
}
|
||||
|
||||
|