summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml3
-rw-r--r--fonts/NotoSansArabic-Regular.ttfbin0 -> 140400 bytes
-rw-r--r--src/env.rs41
-rw-r--r--src/exec/context.rs56
-rw-r--r--src/exec/state.rs76
-rw-r--r--src/export/pdf.rs39
-rw-r--r--src/font.rs115
-rw-r--r--src/layout/frame.rs62
-rw-r--r--src/layout/shaping.rs292
-rw-r--r--src/layout/text.rs39
-rw-r--r--src/lib.rs1
-rw-r--r--src/library/mod.rs2
-rw-r--r--src/library/spacing.rs2
-rw-r--r--tests/ref/control/for.pngbin2723 -> 2715 bytes
-rw-r--r--tests/ref/control/if.pngbin1637 -> 1644 bytes
-rw-r--r--tests/ref/control/let.pngbin1961 -> 1974 bytes
-rw-r--r--tests/ref/expr/call.pngbin5991 -> 6000 bytes
-rw-r--r--tests/ref/expr/ops.pngbin776 -> 770 bytes
-rw-r--r--tests/ref/full/coma.pngbin59286 -> 59402 bytes
-rw-r--r--tests/ref/library/circle.pngbin13617 -> 13642 bytes
-rw-r--r--tests/ref/library/ellipse.pngbin7638 -> 7588 bytes
-rw-r--r--tests/ref/library/font.pngbin21246 -> 21305 bytes
-rw-r--r--tests/ref/library/pad.pngbin1226 -> 1224 bytes
-rw-r--r--tests/ref/library/page.pngbin8290 -> 8180 bytes
-rw-r--r--tests/ref/library/pagebreak.pngbin1357 -> 1360 bytes
-rw-r--r--tests/ref/library/paragraph.pngbin4170 -> 4162 bytes
-rw-r--r--tests/ref/library/rect.pngbin2769 -> 2760 bytes
-rw-r--r--tests/ref/library/spacing.pngbin3833 -> 3845 bytes
-rw-r--r--tests/ref/library/square.pngbin7166 -> 7184 bytes
-rw-r--r--tests/ref/markup/emph.pngbin3387 -> 3372 bytes
-rw-r--r--tests/ref/markup/escape.pngbin6530 -> 6517 bytes
-rw-r--r--tests/ref/markup/heading.pngbin5205 -> 5177 bytes
-rw-r--r--tests/ref/markup/linebreak.pngbin4308 -> 4292 bytes
-rw-r--r--tests/ref/markup/nbsp.pngbin1763 -> 1769 bytes
-rw-r--r--tests/ref/markup/parbreak.pngbin1654 -> 1682 bytes
-rw-r--r--tests/ref/markup/raw.pngbin8299 -> 8261 bytes
-rw-r--r--tests/ref/markup/strong.pngbin3374 -> 3385 bytes
-rw-r--r--tests/ref/repr.pngbin10828 -> 10833 bytes
-rw-r--r--tests/ref/text.pngbin36910 -> 0 bytes
-rw-r--r--tests/ref/text/basic.pngbin0 -> 37041 bytes
-rw-r--r--tests/ref/text/complex.pngbin0 -> 12273 bytes
-rw-r--r--tests/typ/text/basic.typ (renamed from tests/typ/text.typ)0
-rw-r--r--tests/typ/text/complex.typ38
-rw-r--r--tests/typeset.rs55
44 files changed, 454 insertions, 367 deletions
diff --git a/Cargo.toml b/Cargo.toml
index ecb895c0..883d3443 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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 }
diff --git a/fonts/NotoSansArabic-Regular.ttf b/fonts/NotoSansArabic-Regular.ttf
new file mode 100644
index 00000000..94eead4c
--- /dev/null
+++ b/fonts/NotoSansArabic-Regular.ttf
Binary files differ
diff --git a/src/env.rs b/src/env.rs
index 75e2853a..10230bbf 100644
--- a/src/env.rs
+++ b/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 {
diff --git a/src/exec/context.rs b/src/exec/context.rs
index 6ba5b25f..b6a67a2e 100644
--- a/src/exec/context.rs
+++ b/src/exec/context.rs
@@ -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(),
+ }
+ }
+}
diff --git a/src/exec/state.rs b/src/exec/state.rs
index 0322c437..7957f312 100644
--- a/src/exec/state.rs
+++ b/src/exec/state.rs
@@ -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],
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index 6881188d..1abe104d 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -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());
}
}
diff --git a/src/font.rs b/src/font.rs
new file mode 100644
index 00000000..78fc0d44
--- /dev/null
+++ b/src/font.rs
@@ -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",
+ })
+ }
+}
diff --git a/src/layout/frame.rs b/src/layout/frame.rs
index 6e876151..d3276e99 100644
--- a/src/layout/frame.rs
+++ b/src/layout/frame.rs
@@ -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 {
diff --git a/src/layout/shaping.rs b/src/layout/shaping.rs
index fd10b41e..f7eece92 100644
--- a/src/layout/shaping.rs
+++ b/src/layout/shaping.rs
@@ -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};
-
-/// 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,
-}
+use crate::exec::FontProps;
+use crate::geom::{Length, Point, Size};
-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 [`ShapedText`] runs.
+pub fn shape(text: &str, loader: &mut FontLoader, props: &FontProps) -> Frame {
+ let mut frame = Frame::new(Size::new(Length::ZERO, Length::ZERO));
+ shape_segment(&mut frame, text, props.families.iter(), None, loader, props);
+ frame
}
-/// Shape text into a frame containing [`Shaped`] runs.
-pub fn shape(
+/// Shape text into a frame with font fallback using the `families` iterator.
+fn shape_segment<'a>(
+ frame: &mut Frame,
text: &str,
- dir: Dir,
- families: &FamilyMap,
- variant: FontVariant,
- font_size: Length,
- top_edge: VerticalFontMetric,
- bottom_edge: VerticalFontMetric,
- color: Fill,
+ mut families: impl Iterator<Item = &'a str> + Clone,
+ mut first: Option<FaceId>,
loader: &mut FontLoader,
-) -> 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
+ 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,
+ },
+ }
};
- 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;
+ // Register that this is the first available font.
+ let face = loader.face(id);
+ if first.is_none() {
+ first = Some(id);
+ }
- // 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);
+ // 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);
+ }
- 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)));
+ // 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;
}
-
- shaped.text.push(c);
- shaped.glyphs.push(glyph);
- shaped.offsets.push(width);
- width += convert(f64::from(glyph_width));
- break;
+ end = info.cluster as usize;
+ iter.next();
}
- }
- }
- place(&mut frame, shaped, width, top, bottom);
-
- frame
-}
+ // 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);
+ }
-/// 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);
+ // 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);
+ }
}
-}
-/// 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),
+ if !shaped.glyphs.is_empty() {
+ place(frame, shaped)
}
}
-/// 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())
+/// Place shaped text into a frame.
+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));
}
diff --git a/src/layout/text.rs b/src/layout/text.rs
index 2239afac..39866907 100644
--- a/src/layout/text.rs
+++ b/src/layout/text.rs
@@ -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)
}
}
diff --git a/src/lib.rs b/src/lib.rs
index cc370a0a..20c69fe9 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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;
diff --git a/src/library/mod.rs b/src/library/mod.rs
index 58e62d56..1f412cd0 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -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.
diff --git a/src/library/spacing.rs b/src/library/spacing.rs
index 506f6585..d4648566 100644
--- a/src/library/spacing.rs
+++ b/src/library/spacing.rs
@@ -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);
diff --git a/tests/ref/control/for.png b/tests/ref/control/for.png
index b3757692..1c7dfd42 100644
--- a/tests/ref/control/for.png
+++ b/tests/ref/control/for.png
Binary files differ
diff --git a/tests/ref/control/if.png b/tests/ref/control/if.png
index e38cf7c4..b30e63f8 100644
--- a/tests/ref/control/if.png
+++ b/tests/ref/control/if.png
Binary files differ
diff --git a/tests/ref/control/let.png b/tests/ref/control/let.png
index 20aad953..8a5c5fc6 100644
--- a/tests/ref/control/let.png
+++ b/tests/ref/control/let.png
Binary files differ
diff --git a/tests/ref/expr/call.png b/tests/ref/expr/call.png
index 26f1cb03..f05fb835 100644
--- a/tests/ref/expr/call.png
+++ b/tests/ref/expr/call.png
Binary files differ
diff --git a/tests/ref/expr/ops.png b/tests/ref/expr/ops.png
index 2f3a28ea..e6f2edab 100644
--- a/tests/ref/expr/ops.png
+++ b/tests/ref/expr/ops.png
Binary files differ
diff --git a/tests/ref/full/coma.png b/tests/ref/full/coma.png
index 5fd6d801..d869d57e 100644
--- a/tests/ref/full/coma.png
+++ b/tests/ref/full/coma.png
Binary files differ
diff --git a/tests/ref/library/circle.png b/tests/ref/library/circle.png
index 9e89d98b..f1128642 100644
--- a/tests/ref/library/circle.png
+++ b/tests/ref/library/circle.png
Binary files differ
diff --git a/tests/ref/library/ellipse.png b/tests/ref/library/ellipse.png
index de178d60..c78c9272 100644
--- a/tests/ref/library/ellipse.png
+++ b/tests/ref/library/ellipse.png
Binary files differ
diff --git a/tests/ref/library/font.png b/tests/ref/library/font.png
index 57a45006..07c65726 100644
--- a/tests/ref/library/font.png
+++ b/tests/ref/library/font.png
Binary files differ
diff --git a/tests/ref/library/pad.png b/tests/ref/library/pad.png
index 3536db5d..3bc0ddd0 100644
--- a/tests/ref/library/pad.png
+++ b/tests/ref/library/pad.png
Binary files differ
diff --git a/tests/ref/library/page.png b/tests/ref/library/page.png
index 7d1ff96f..8e5a83ff 100644
--- a/tests/ref/library/page.png
+++ b/tests/ref/library/page.png
Binary files differ
diff --git a/tests/ref/library/pagebreak.png b/tests/ref/library/pagebreak.png
index d513c963..ab990c69 100644
--- a/tests/ref/library/pagebreak.png
+++ b/tests/ref/library/pagebreak.png
Binary files differ
diff --git a/tests/ref/library/paragraph.png b/tests/ref/library/paragraph.png
index bf38bdf8..37898017 100644
--- a/tests/ref/library/paragraph.png
+++ b/tests/ref/library/paragraph.png
Binary files differ
diff --git a/tests/ref/library/rect.png b/tests/ref/library/rect.png
index 81ee91d7..56f1003f 100644
--- a/tests/ref/library/rect.png
+++ b/tests/ref/library/rect.png
Binary files differ
diff --git a/tests/ref/library/spacing.png b/tests/ref/library/spacing.png
index 6a234a8a..c266b9fa 100644
--- a/tests/ref/library/spacing.png
+++ b/tests/ref/library/spacing.png
Binary files differ
diff --git a/tests/ref/library/square.png b/tests/ref/library/square.png
index 401b1ab2..26469d20 100644
--- a/tests/ref/library/square.png
+++ b/tests/ref/library/square.png
Binary files differ
diff --git a/tests/ref/markup/emph.png b/tests/ref/markup/emph.png
index f43eeecb..9b6bda5c 100644
--- a/tests/ref/markup/emph.png
+++ b/tests/ref/markup/emph.png
Binary files differ
diff --git a/tests/ref/markup/escape.png b/tests/ref/markup/escape.png
index 561b5f1e..9c6f1f59 100644
--- a/tests/ref/markup/escape.png
+++ b/tests/ref/markup/escape.png
Binary files differ
diff --git a/tests/ref/markup/heading.png b/tests/ref/markup/heading.png
index f95b8c2c..a32229e7 100644
--- a/tests/ref/markup/heading.png
+++ b/tests/ref/markup/heading.png
Binary files differ
diff --git a/tests/ref/markup/linebreak.png b/tests/ref/markup/linebreak.png
index 4dfca22f..512fa0f5 100644
--- a/tests/ref/markup/linebreak.png
+++ b/tests/ref/markup/linebreak.png
Binary files differ
diff --git a/tests/ref/markup/nbsp.png b/tests/ref/markup/nbsp.png
index 89f75f14..cc920776 100644
--- a/tests/ref/markup/nbsp.png
+++ b/tests/ref/markup/nbsp.png
Binary files differ
diff --git a/tests/ref/markup/parbreak.png b/tests/ref/markup/parbreak.png
index 008afca2..f100b9d7 100644
--- a/tests/ref/markup/parbreak.png
+++ b/tests/ref/markup/parbreak.png
Binary files differ
diff --git a/tests/ref/markup/raw.png b/tests/ref/markup/raw.png
index 1aebc02d..a20ca999 100644
--- a/tests/ref/markup/raw.png
+++ b/tests/ref/markup/raw.png
Binary files differ
diff --git a/tests/ref/markup/strong.png b/tests/ref/markup/strong.png
index 20e29b1f..4bbf6ac0 100644
--- a/tests/ref/markup/strong.png
+++ b/tests/ref/markup/strong.png
Binary files differ
diff --git a/tests/ref/repr.png b/tests/ref/repr.png
index 390c2cab..71f415ef 100644
--- a/tests/ref/repr.png
+++ b/tests/ref/repr.png
Binary files differ
diff --git a/tests/ref/text.png b/tests/ref/text.png
deleted file mode 100644
index 1dd70c1c..00000000
--- a/tests/ref/text.png
+++ /dev/null
Binary files differ
diff --git a/tests/ref/text/basic.png b/tests/ref/text/basic.png
new file mode 100644
index 00000000..a06c8763
--- /dev/null
+++ b/tests/ref/text/basic.png
Binary files differ
diff --git a/tests/ref/text/complex.png b/tests/ref/text/complex.png
new file mode 100644
index 00000000..9af49f16
--- /dev/null
+++ b/tests/ref/text/complex.png
Binary files differ
diff --git a/tests/typ/text.typ b/tests/typ/text/basic.typ
index b424cee8..b424cee8 100644
--- a/tests/typ/text.typ
+++ b/tests/typ/text/basic.typ
diff --git a/tests/typ/text/complex.typ b/tests/typ/text/complex.typ
new file mode 100644
index 00000000..567a208d
--- /dev/null
+++ b/tests/typ/text/complex.typ
@@ -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
diff --git a/tests/typeset.rs b/tests/typeset.rs
index c5ec01d2..b38311aa 100644
--- a/tests/typeset.rs
+++ b/tests/typeset.rs
@@ -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);
}