Skip to content

Commit

Permalink
[write-fonts] Improve builder API
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cmyr committed Aug 1, 2023
1 parent 025941f commit 6106f96
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 10 deletions.
2 changes: 1 addition & 1 deletion write-fonts/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
63 changes: 60 additions & 3 deletions write-fonts/src/font_builder.rs
Original file line number Diff line number Diff line change
@@ -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");
Expand All @@ -15,6 +16,16 @@ pub struct FontBuilder<'a> {
tables: BTreeMap<Tag, Cow<'a, [u8]>>,
}

/// 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<TableRecord>) -> TableDirectory {
assert!(table_records.len() <= u16::MAX as usize);
Expand All @@ -37,16 +48,54 @@ impl TableDirectory {
}

impl<'a> FontBuilder<'a> {
pub fn add_table(&mut self, tag: Tag, data: impl Into<Cow<'a, [u8]>>) -> &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<T>(&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<Cow<'a, [u8]>>) -> &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) {
if let Some(data) = font.data_for_tag(tag) {
self.add_raw(tag, data);
} else {
log::warn!("data for '{tag}' is malformed");
}
}
}
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<u8> {
let header_len = std::mem::size_of::<u32>() // sfnt
+ std::mem::size_of::<u16>() * 4 // num_tables to range_shift
Expand Down Expand Up @@ -112,6 +161,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;
Expand All @@ -125,7 +182,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();
Expand Down
20 changes: 14 additions & 6 deletions write-fonts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,27 @@
//! 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();
//! std::fs::write("mynewfont.ttf", &new_bytes).unwrap();
//! ```
//!
//! [`read-fonts`]: https://docs.rs/read-fonts/
Expand Down Expand Up @@ -115,7 +123,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};
Expand Down
1 change: 1 addition & 0 deletions write-fonts/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ struct ValidationError {
}

/// One or more validation errors.
#[derive(Clone)]
pub struct ValidationReport {
errors: Vec<ValidationError>,
}
Expand Down

0 comments on commit 6106f96

Please sign in to comment.