use std::collections::BTreeMap;
use std::{borrow::Cow, fmt::Display};
use fontcull_read_fonts::{FontRef, TableProvider};
use types::{Tag, TT_SFNT_VERSION};
use crate::util::SearchRange;
include!("../generated/generated_font.rs");
const TABLE_RECORD_LEN: usize = 16;
#[derive(Debug, Clone, Default)]
pub struct FontBuilder<'a> {
tables: BTreeMap<Tag, Cow<'a, [u8]>>,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct BuilderError {
pub tag: Tag,
pub inner: crate::error::Error,
}
impl TableDirectory {
pub fn from_table_records(table_records: Vec<TableRecord>) -> TableDirectory {
assert!(table_records.len() <= u16::MAX as usize);
let computed = SearchRange::compute(table_records.len(), TABLE_RECORD_LEN);
TableDirectory::new(
TT_SFNT_VERSION,
computed.search_range,
computed.entry_selector,
computed.range_shift,
table_records,
)
}
}
const RECOMMENDED_TABLE_ORDER_TTF: [Tag; 19] = [
Tag::new(b"head"),
Tag::new(b"hhea"),
Tag::new(b"maxp"),
Tag::new(b"OS/2"),
Tag::new(b"hmtx"),
Tag::new(b"LTSH"),
Tag::new(b"VDMX"),
Tag::new(b"hdmx"),
Tag::new(b"cmap"),
Tag::new(b"fpgm"),
Tag::new(b"prep"),
Tag::new(b"cvt "),
Tag::new(b"loca"),
Tag::new(b"glyf"),
Tag::new(b"kern"),
Tag::new(b"name"),
Tag::new(b"post"),
Tag::new(b"gasp"),
Tag::new(b"PCLT"),
];
const RECOMMENDED_TABLE_ORDER_CFF: [Tag; 8] = [
Tag::new(b"head"),
Tag::new(b"hhea"),
Tag::new(b"maxp"),
Tag::new(b"OS/2"),
Tag::new(b"name"),
Tag::new(b"cmap"),
Tag::new(b"post"),
Tag::new(b"CFF "),
];
impl<'a> FontBuilder<'a> {
pub fn new() -> Self {
Self::default()
}
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))
}
pub fn add_raw(&mut self, tag: Tag, data: impl Into<Cow<'a, [u8]>>) -> &mut Self {
self.tables.insert(tag, data.into());
self
}
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
}
pub fn contains(&self, tag: Tag) -> bool {
self.tables.contains_key(&tag)
}
pub fn ordered_tags(&self) -> Vec<Tag> {
let recommended_order: &[Tag] = if self.contains(Tag::new(b"CFF ")) {
&RECOMMENDED_TABLE_ORDER_CFF
} else {
&RECOMMENDED_TABLE_ORDER_TTF
};
let mut ordered_tags: Vec<Tag> = self.tables.keys().copied().collect();
let dsig = Tag::new(b"DSIG");
ordered_tags.sort_unstable_by_key(|rtag| {
let tag = *rtag;
if tag == dsig {
(2, 0, tag)
} else if let Some(idx) = recommended_order.iter().position(|t| t == rtag) {
(0, idx, tag)
} else {
(1, 0, tag)
}
});
ordered_tags
}
pub fn build(&mut self) -> Vec<u8> {
const HEAD_CHECKSUM_START: usize = 8;
const HEAD_CHECKSUM_END: usize = 12;
let header_len = std::mem::size_of::<u32>() + std::mem::size_of::<u16>() * 4 + self.tables.len() * TABLE_RECORD_LEN;
let table_order = self.ordered_tags();
let mut position = header_len as u32;
let mut checksums = Vec::new();
let head_tag = Tag::new(b"head");
let mut table_records = Vec::new();
for tag in table_order.iter() {
let data = self.tables.get_mut(tag).unwrap();
let offset = position;
let length = data.len() as u32;
position += length;
if *tag == head_tag && data.len() >= HEAD_CHECKSUM_END {
let head = data.to_mut();
head[HEAD_CHECKSUM_START..HEAD_CHECKSUM_END].copy_from_slice(&[0, 0, 0, 0]);
}
let (checksum, padding) = checksum_and_padding(data);
checksums.push(checksum);
position += padding;
table_records.push(TableRecord::new(*tag, checksum, offset, length));
}
table_records.sort_unstable_by_key(|record| record.tag);
let directory = TableDirectory::from_table_records(table_records);
let mut writer = TableWriter::default();
directory.write_into(&mut writer);
let mut data = writer.into_data().bytes;
checksums.push(fontcull_read_fonts::tables::compute_checksum(&data));
let checksum = checksums.into_iter().fold(0u32, u32::wrapping_add);
let checksum_adjustment = 0xB1B0_AFBAu32.wrapping_sub(checksum);
for tag in table_order {
let table = self.tables.remove(&tag).unwrap();
if tag == head_tag && table.len() >= HEAD_CHECKSUM_END {
data.extend_from_slice(&table[..HEAD_CHECKSUM_START]);
data.extend_from_slice(&checksum_adjustment.to_be_bytes());
data.extend_from_slice(&table[HEAD_CHECKSUM_END..]);
} else {
data.extend_from_slice(&table);
}
let rem = round4(table.len()) - table.len();
let padding = [0u8; 4];
data.extend_from_slice(&padding[..rem]);
}
data
}
}
fn round4(sz: usize) -> usize {
(sz + 3) & !3
}
fn checksum_and_padding(table: &[u8]) -> (u32, u32) {
let checksum = fontcull_read_fonts::tables::compute_checksum(table);
let padding = round4(table.len()) - table.len();
(checksum, padding as u32)
}
impl TTCHeader {
fn compute_version(&self) -> MajorMinor {
panic!("TTCHeader writing not supported (yet)")
}
}
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 {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.inner)
}
}
#[cfg(test)]
mod tests {
use super::{RECOMMENDED_TABLE_ORDER_CFF, RECOMMENDED_TABLE_ORDER_TTF};
use fontcull_font_types::Tag;
use fontcull_read_fonts::FontRef;
use crate::{font_builder::checksum_and_padding, FontBuilder};
use rand::seq::SliceRandom;
use rand::Rng;
use rstest::rstest;
#[test]
fn sets_binary_search_assists() {
let data = b"doesn't matter".to_vec();
let mut builder = FontBuilder::default();
(0..0x16u32).for_each(|i| {
builder.add_raw(Tag::from_be_bytes(i.to_ne_bytes()), &data);
});
let bytes = builder.build();
let font = FontRef::new(&bytes).unwrap();
let td = font.table_directory();
assert_eq!(
(256, 4, 96),
(td.search_range(), td.entry_selector(), td.range_shift())
);
}
#[test]
fn survives_no_tables() {
FontBuilder::default().build();
}
#[test]
fn pad4() {
for i in 0..10 {
let pad = checksum_and_padding(&vec![0; i]).1;
assert!(pad < 4);
assert!((i + pad as usize) % 4 == 0, "pad {i} +{pad} bytes");
}
}
#[test]
fn validate_font_checksum() {
let head_size = 54;
let mut rng = rand::thread_rng();
let mut builder = FontBuilder::default();
for tag in [Tag::new(b"head"), Tag::new(b"FOO "), Tag::new(b"BAR ")] {
let data: Vec<u8> = (0..=head_size).map(|_| rng.gen()).collect();
builder.add_raw(tag, data);
}
let font_data = builder.build();
assert_eq!(
fontcull_read_fonts::tables::compute_checksum(&font_data),
0xB1B0AFBA
);
}
#[test]
fn minimum_head_size_for_checksum_rewrite() {
let mut builder = FontBuilder::default();
builder.add_raw(
Tag::new(b"head"),
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
);
let font_data = builder.build();
let font = FontRef::new(&font_data).unwrap();
let head = font.table_data(Tag::new(b"head")).unwrap();
assert_eq!(
head.as_bytes(),
&vec![0, 1, 2, 3, 4, 5, 6, 7, 65, 61, 62, 10]
);
}
#[test]
fn doesnt_overflow_head() {
let mut builder = FontBuilder::default();
builder.add_raw(Tag::new(b"head"), vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
let font_data = builder.build();
let font = FontRef::new(&font_data).unwrap();
let head = font.table_data(Tag::new(b"head")).unwrap();
assert_eq!(head.as_bytes(), &vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
}
#[rstest]
#[case::ttf(&RECOMMENDED_TABLE_ORDER_TTF)]
#[case::cff(&RECOMMENDED_TABLE_ORDER_CFF)]
fn recommended_table_order(#[case] recommended_order: &[Tag]) {
let dsig = Tag::new(b"DSIG");
let mut builder = FontBuilder::default();
builder.add_raw(dsig, vec![0]);
let mut tags = recommended_order.to_vec();
tags.shuffle(&mut rand::thread_rng());
for tag in tags {
builder.add_raw(tag, vec![0]);
}
builder.add_raw(Tag::new(b"ZZZZ"), vec![0]);
builder.add_raw(Tag::new(b"AAAA"), vec![0]);
let mut expected = recommended_order.to_vec();
expected.push(Tag::new(b"AAAA"));
expected.push(Tag::new(b"ZZZZ"));
expected.push(dsig);
assert_eq!(builder.ordered_tags(), expected);
}
}