From 1ebee42f56856d207f949d57e53ea214e8e78a55 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Tue, 1 Aug 2023 15:04:00 -0400 Subject: [PATCH] [write-fonts] Improve builder API This makes a number of tweaks to the FontBuilder API, intended to improve ergonomics: - A 'copy_missing_tables' method was added to simplify the common step of bulk copying existing tables from a FontRef - The existing 'add_table' method was renamed 'add_raw', - and a new 'add_table' method was added that removes the need for the caller to manually compile the table and provide the appropriate tag to the builder. --- write-fonts/src/error.rs | 2 +- write-fonts/src/font_builder.rs | 66 +++++++++++++++++++++++++++++++-- write-fonts/src/lib.rs | 19 +++++++--- write-fonts/src/validate.rs | 1 + 4 files changed, 78 insertions(+), 10 deletions(-) diff --git a/write-fonts/src/error.rs b/write-fonts/src/error.rs index c72df55d8..f312fa94e 100644 --- a/write-fonts/src/error.rs +++ b/write-fonts/src/error.rs @@ -12,7 +12,7 @@ pub struct PackingError { } /// An error occured while writing this table -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Error { ValidationFailed(ValidationReport), PackingFailed(PackingError), diff --git a/write-fonts/src/font_builder.rs b/write-fonts/src/font_builder.rs index f91018afe..6f57ab542 100644 --- a/write-fonts/src/font_builder.rs +++ b/write-fonts/src/font_builder.rs @@ -1,8 +1,9 @@ //! A builder for top-level font objects -use std::borrow::Cow; use std::collections::BTreeMap; +use std::{borrow::Cow, fmt::Display}; +use read::{FontRef, TableProvider}; use types::{Tag, TT_SFNT_VERSION}; include!("../generated/generated_font.rs"); @@ -15,6 +16,16 @@ pub struct FontBuilder<'a> { tables: BTreeMap>, } +/// An error returned when attempting to add a table to the builder. +/// +/// This wraps a compilation error, adding the tag of the table where it was +/// encountered. +#[derive(Clone, Debug)] +pub struct BuilderError { + tag: Tag, + inner: crate::error::Error, +} + impl TableDirectory { pub fn from_table_records(table_records: Vec) -> TableDirectory { assert!(table_records.len() <= u16::MAX as usize); @@ -37,16 +48,57 @@ impl TableDirectory { } impl<'a> FontBuilder<'a> { - pub fn add_table(&mut self, tag: Tag, data: impl Into>) -> &mut Self { + /// Create a new builder to compile a binary font + pub fn new() -> Self { + Self::default() + } + + /// Add a table to the builder. + /// + /// The table can be any top-level table defined in this crate. This function + /// will attempt to compile the table and then add it to the builder if + /// successful, returning an error otherwise. + pub fn add_table(&mut self, table: &T) -> Result<&mut Self, BuilderError> + where + T: FontWrite + Validate + TopLevelTable, + { + let tag = T::TAG; + let bytes = crate::dump_table(table).map_err(|inner| BuilderError { inner, tag })?; + Ok(self.add_raw(tag, bytes)) + } + + /// A builder method to add raw data for the provided tag + pub fn add_raw(&mut self, tag: Tag, data: impl Into>) -> &mut Self { self.tables.insert(tag, data.into()); self } + /// Copy each table from the source font if it does not already exist + pub fn copy_missing_tables(&mut self, font: FontRef<'a>) -> &mut Self { + for record in font.table_directory.table_records() { + let tag = record.tag(); + if !self.tables.contains_key(&tag) { + let data = match font.data_for_tag(tag) { + Some(data) => data, + None => { + log::warn!("data for '{tag}' is malformed"); + Default::default() + } + }; + self.add_raw(tag, data.as_bytes()); + } + } + self + } + /// Returns `true` if the builder contains a table with this tag. pub fn contains(&self, tag: Tag) -> bool { self.tables.contains_key(&tag) } + /// Assemble all the tables into a binary font file with a [Table Directory]. + /// + /// [Table Directory]: https://learn.microsoft.com/en-us/typography/opentype/spec/otff#table-directory pub fn build(&mut self) -> Vec { let header_len = std::mem::size_of::() // sfnt + std::mem::size_of::() * 4 // num_tables to range_shift @@ -112,6 +164,14 @@ impl TTCHeader { } } +impl Display for BuilderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "failed to build '{}' table: '{}'", self.tag, self.inner) + } +} + +impl std::error::Error for BuilderError {} + #[cfg(test)] mod tests { use font_types::Tag; @@ -125,7 +185,7 @@ mod tests { let data = b"doesn't matter".to_vec(); let mut builder = FontBuilder::default(); (0..0x16u32).for_each(|i| { - builder.add_table(Tag::from_be_bytes(i.to_ne_bytes()), &data); + builder.add_raw(Tag::from_be_bytes(i.to_ne_bytes()), &data); }); let bytes = builder.build(); let font = FontRef::new(&bytes).unwrap(); diff --git a/write-fonts/src/lib.rs b/write-fonts/src/lib.rs index a8500925c..baf1a08d0 100644 --- a/write-fonts/src/lib.rs +++ b/write-fonts/src/lib.rs @@ -72,19 +72,26 @@ //! let _bytes = write_fonts::dump_table(&my_table).expect("failed to write bytes"); //! ``` //! -//! Read and modify an existing 'head' table +//! Read/modify/write an existing font //! ```no_run //! # let path_to_my_font_file = std::path::Path::new(""); //! # fn seconds_since_font_epoch() -> LongDateTime { todo!() } //! use read_fonts::{FontRef, TableProvider}; -//! use write_fonts::{from_obj::ToOwnedTable, tables::head::Head, types::LongDateTime}; -//! +//! use write_fonts::{ +//! from_obj::ToOwnedTable, +//! tables::head::Head, +//! types::LongDateTime, +//! FontBuilder, +//! }; //! let font_bytes = std::fs::read(path_to_my_font_file).unwrap(); //! let font = FontRef::new(&font_bytes).expect("failed to read font data"); //! let mut head: Head = font.head().expect("missing 'head' table").to_owned_table(); //! head.modified = seconds_since_font_epoch(); -//! // to save the font you need to use `FontBuilder`, adding all the unchanged -//! // tables from the original font, and then the new head table +//! let new_bytes = FontBuilder::new() +//! .add_table(&head) +//! .unwrap() // errors if we can't compile 'head', unlikely here +//! .copy_missing_tables(font) +//! .build(); //! ``` //! //! [`read-fonts`]: https://docs.rs/read-fonts/ @@ -115,7 +122,7 @@ mod codegen_test; #[cfg(test)] mod hex_diff; -pub use font_builder::FontBuilder; +pub use font_builder::{BuilderError, FontBuilder}; pub use offsets::{NullableOffsetMarker, OffsetMarker}; pub use round::OtRound; pub use write::{dump_table, FontWrite, TableWriter}; diff --git a/write-fonts/src/validate.rs b/write-fonts/src/validate.rs index d7b84dae5..60fb248a0 100644 --- a/write-fonts/src/validate.rs +++ b/write-fonts/src/validate.rs @@ -77,6 +77,7 @@ struct ValidationError { } /// One or more validation errors. +#[derive(Clone)] pub struct ValidationReport { errors: Vec, }