Skip to content

Commit

Permalink
[write-fonts] Add GlyfLocaBuilder
Browse files Browse the repository at this point in the history
This attempts to be a nice ergonomic way to build the glyf & loca
tables.
  • Loading branch information
cmyr committed Aug 3, 2023
1 parent 0af87b9 commit 1f3d213
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 3 deletions.
6 changes: 6 additions & 0 deletions write-fonts/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ impl std::fmt::Debug for PackingError {
impl std::error::Error for PackingError {}
impl std::error::Error for Error {}

impl From<ValidationReport> for Error {
fn from(value: ValidationReport) -> Self {
Error::ValidationFailed(value)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
8 changes: 6 additions & 2 deletions write-fonts/src/tables/glyf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>);

impl TopLevelTable for Glyf {
Expand Down
203 changes: 203 additions & 0 deletions write-fonts/src/tables/glyf/glyf_loca_builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
//! 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<u32>,
}

/// 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 glyph is validated and compiled immediately, so that the caller can
/// associate any errors with a particular glyph.
pub fn add_glyph<T>(&mut self, glyph: &T) -> Result<&mut Self, Error>
where
// the Into<Glyph> bound is a way of ensuring this is only Glyph/SimpleGlyph/CompositeGlyph
T: SomeGlyph,
{
// empty glyph doesn't get written, we just add a duplicate loca entry
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(&notdef).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());
}
}
4 changes: 4 additions & 0 deletions write-fonts/src/tables/loca.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32>) -> Self {
let loca_format = LocaFormat::new(&offsets);

Expand Down
10 changes: 9 additions & 1 deletion write-fonts/src/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub struct TableWriter {
/// otherwise it will return the bytes encoding the table.
pub fn dump_table<T: FontWrite + Validate>(table: &T) -> Result<Vec<u8>, 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() {
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 1f3d213

Please sign in to comment.