From b61bd83f31175d41a49d14cf6b352c67b024bcdf Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Thu, 3 Aug 2023 11:33:10 -0400 Subject: [PATCH] [write-fonts] Add GlyfLocaBuilder This attempts to be a nice ergonomic way to build the glyf & loca tables. --- write-fonts/src/error.rs | 6 + write-fonts/src/tables/glyf.rs | 8 +- .../src/tables/glyf/glyf_loca_builder.rs | 200 ++++++++++++++++++ write-fonts/src/tables/loca.rs | 4 + write-fonts/src/write.rs | 10 +- 5 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 write-fonts/src/tables/glyf/glyf_loca_builder.rs diff --git a/write-fonts/src/error.rs b/write-fonts/src/error.rs index f312fa94e..8a8c77dd6 100644 --- a/write-fonts/src/error.rs +++ b/write-fonts/src/error.rs @@ -56,6 +56,12 @@ impl std::fmt::Debug for PackingError { impl std::error::Error for PackingError {} impl std::error::Error for Error {} +impl From for Error { + fn from(value: ValidationReport) -> Self { + Error::ValidationFailed(value) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/write-fonts/src/tables/glyf.rs b/write-fonts/src/tables/glyf.rs index b1e2bd13d..c84b37202 100644 --- a/write-fonts/src/tables/glyf.rs +++ b/write-fonts/src/tables/glyf.rs @@ -11,15 +11,19 @@ use kurbo::Rect; use read_fonts::{FontRead, TopLevelTable}; mod composite; +mod glyf_loca_builder; mod simple; pub use composite::{Anchor, Component, ComponentFlags, CompositeGlyph, Transform}; +pub use glyf_loca_builder::{GlyfLocaBuilder, SomeGlyph}; pub use simple::{simple_glyphs_from_kurbo, BadKurbo, Contour, SimpleGlyph}; /// The [glyf (Glyph Data)](https://docs.microsoft.com/en-us/typography/opentype/spec/glyf) table /// -/// Compiling the glyf table requires additional logic, since the positions -/// of glyfs are stored in the 'loca' type. +/// This table is the concatenated bytes of all the glyphs in the font, with +/// the positions of each individual glyph stored in the ['loca' table][super::loca]. +/// As such, these two tables must be constructed together. The [`GlyfLocaBuilder`] +/// type is provided to simplify this. pub struct Glyf(Vec); impl TopLevelTable for Glyf { diff --git a/write-fonts/src/tables/glyf/glyf_loca_builder.rs b/write-fonts/src/tables/glyf/glyf_loca_builder.rs new file mode 100644 index 000000000..cd0b1f230 --- /dev/null +++ b/write-fonts/src/tables/glyf/glyf_loca_builder.rs @@ -0,0 +1,200 @@ +//! A builder for the 'glyf' and 'loca' tables + +use crate::{ + error::Error, + tables::loca::{Loca, LocaFormat}, + validate::Validate, + FontWrite, TableWriter, +}; + +use super::{CompositeGlyph, Glyf, Glyph, SimpleGlyph}; + +/// A builder for constructing the 'glyf' & 'loca' tables. +/// +/// These two tables are tightly coupled, and are necessarily constructed +/// together. +/// +/// # Example +/// +/// ``` +/// use write_fonts::tables::glyf::{Glyph, GlyfLocaBuilder}; +/// # fn get_glyphs() -> Vec<(String, Glyph)> { Vec::new() } +/// +/// let names_and_glyphs: Vec<(String, Glyph)> = get_glyphs(); +/// let mut builder = GlyfLocaBuilder::new(); +/// +/// for (name, glyph) in names_and_glyphs { +/// // your error handling goes here +/// if let Err(e) = builder.add_glyph(&glyph) { +/// panic!("error compiling glyph '{name}': '{e}'"); +/// } +/// } +/// +/// let (_glyf, _loca, _loca_format) = builder.build(); +/// // store the results somewhere +/// ``` +pub struct GlyfLocaBuilder { + glyph_writer: TableWriter, + raw_loca: Vec, +} + +/// A trait encompassing [`Glyph`], [`SimpleGlyph`] and [`CompositeGlyph`] +/// +/// This is a marker trait to ensure that only glyphs are passed to [`GlyfLocaBuilder`]. +pub trait SomeGlyph: Validate + FontWrite { + /// Returns `true` if the glyph contains no contours or components. + /// + /// If a glyph is empty, we do not need to write any data at all for the glyph, + /// and we insert a duplicate value in the loca table. + fn is_empty(&self) -> bool; +} + +impl GlyfLocaBuilder { + /// Construct a new builder for the 'glyf' and 'loca' tables. + pub fn new() -> Self { + Self { + glyph_writer: TableWriter::default(), + raw_loca: vec![0], + } + } + + /// Add a glyph to the table. + /// + /// The argument can be any of [`Glyph`], [`SimpleGlyph`] or [`CompositeGlyph`]. + /// + /// The glyph is validated and compiled immediately, so that the caller can + /// associate any errors with a particular glyph. + pub fn add_glyph(&mut self, glyph: &impl SomeGlyph) -> Result<&mut Self, Error> { + if !glyph.is_empty() { + glyph.validate()?; + glyph.write_into(&mut self.glyph_writer); + } + let pos = self.glyph_writer.current_data().bytes.len(); + self.raw_loca.push(pos as u32); + Ok(self) + } + + /// Construct the final glyf and loca tables. + /// + /// This method also returns the loca format; the caller is responsible for + /// setting this field in the ['head'] table. + /// + /// [`head`]: crate::tables::head::Head::index_to_loc_format + #[must_use] + pub fn build(self) -> (Glyf, Loca, LocaFormat) { + let glyph_data = self.glyph_writer.into_data(); + let loca = Loca::new(self.raw_loca); + let format = loca.format(); + (Glyf(glyph_data), loca, format) + } +} + +impl SomeGlyph for SimpleGlyph { + fn is_empty(&self) -> bool { + self.contours().is_empty() + } +} + +impl SomeGlyph for CompositeGlyph { + fn is_empty(&self) -> bool { + self.components().is_empty() + } +} + +impl SomeGlyph for Glyph { + fn is_empty(&self) -> bool { + self.is_empty() + } +} + +impl Default for GlyfLocaBuilder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::super::{Anchor, Component, ComponentFlags, Transform}; + use crate::from_obj::FromTableRef; + use font_types::GlyphId; + use kurbo::{BezPath, Shape}; + use read_fonts::FontRead; + + use super::*; + + #[test] + fn build_some_glyphs() { + fn make_triangle() -> BezPath { + let mut path = BezPath::new(); + path.move_to((0., 0.)); + path.line_to((0., 40.)); + path.line_to((20., 40.)); + path.line_to((0., 0.)); + path + } + let notdef = BezPath::new(); + let square = kurbo::Rect::from_points((5., 5.), (100., 100.)).into_path(0.1); + let triangle = make_triangle(); + + let glyph0 = SimpleGlyph::from_kurbo(¬def).unwrap(); + let glyph1 = SimpleGlyph::from_kurbo(&square).unwrap(); + let glyph2 = SimpleGlyph::from_kurbo(&triangle).unwrap(); + let gid1 = GlyphId::new(1); + let gid2 = GlyphId::new(2); + + let mut glyph3 = CompositeGlyph::new( + Component::new( + gid1, + Anchor::Offset { x: 0, y: 0 }, + Transform::default(), + ComponentFlags::default(), + ), + square.bounding_box(), + ); + glyph3.add_component( + Component::new( + gid2, + Anchor::Offset { x: 0, y: 0 }, + Transform::default(), + ComponentFlags::default(), + ), + triangle.bounding_box(), + ); + + let len1 = crate::dump_table(&glyph1).unwrap().len() as u32; + let len2 = crate::dump_table(&glyph2).unwrap().len() as u32; + let len3 = crate::dump_table(&glyph3).unwrap().len() as u32; + + let mut builder = GlyfLocaBuilder::new(); + builder + .add_glyph(&glyph0) + .unwrap() + .add_glyph(&glyph1) + .unwrap() + .add_glyph(&glyph2) + .unwrap() + .add_glyph(&glyph3) + .unwrap(); + + let (glyf, loca, format) = builder.build(); + assert_eq!(loca.offsets.len(), 5); + assert_eq!(loca.offsets, &[0, 0, len1, len1 + len2, len1 + len2 + len3]); + + let rglyf = read_fonts::tables::glyf::Glyf::read(glyf.0.as_slice().into()).unwrap(); + let loca_bytes = crate::dump_table(&loca).unwrap(); + let rloca = read_fonts::tables::loca::Loca::read( + loca_bytes.as_slice().into(), + format == LocaFormat::Long, + ) + .unwrap(); + + let rglyph1 = Glyph::from_table_ref(&rloca.get_glyf(gid1, &rglyf).unwrap().unwrap()); + let rglyph2 = Glyph::from_table_ref(&rloca.get_glyf(gid2, &rglyf).unwrap().unwrap()); + let rglyph3 = + Glyph::from_table_ref(&rloca.get_glyf(GlyphId::new(3), &rglyf).unwrap().unwrap()); + assert_eq!(rglyph1, glyph1.into()); + assert_eq!(rglyph2, glyph2.into()); + assert_eq!(rglyph3, glyph3.into()); + } +} diff --git a/write-fonts/src/tables/loca.rs b/write-fonts/src/tables/loca.rs index d5f0f4009..aa8a404fa 100644 --- a/write-fonts/src/tables/loca.rs +++ b/write-fonts/src/tables/loca.rs @@ -41,6 +41,10 @@ impl Loca { /// Create a new loca table from 32-bit offsets. /// /// The loca format will be calculated based on the raw values. + /// + /// You generally do not construct this directly; it is constructed alongside + /// the corresponding 'glyf' table using the + /// [GlyfLocaBuilder](super::glyf::GlyfLocaBuilder). pub fn new(offsets: Vec) -> Self { let loca_format = LocaFormat::new(&offsets); diff --git a/write-fonts/src/write.rs b/write-fonts/src/write.rs index bb8a1000c..1552b9c41 100644 --- a/write-fonts/src/write.rs +++ b/write-fonts/src/write.rs @@ -50,7 +50,7 @@ pub struct TableWriter { /// otherwise it will return the bytes encoding the table. pub fn dump_table(table: &T) -> Result, Error> { log::trace!("writing table '{}'", table.table_type()); - table.validate().map_err(Error::ValidationFailed)?; + table.validate()?; let mut graph = TableWriter::make_graph(table); if !graph.pack_objects() { @@ -125,6 +125,14 @@ impl TableWriter { assert!(result.offsets.is_empty()); result.bytes } + + /// A reference to the current table data. + /// + /// This is currently only used to figure out the glyph positions when + /// compiling the glyf table. + pub(crate) fn current_data(&self) -> &TableData { + self.stack.last().unwrap() // there is always at least one + } } impl Default for TableWriter {