use std::collections::HashSet;
use std::hash::Hasher;
use std::io::{Error, ErrorKind};
use write_fonts::read::tables::post::DEFAULT_GLYPH_NAMES;
use write_fonts::tables::cmap::Cmap;
use write_fonts::tables::head::Head;
use write_fonts::tables::hhea::Hhea;
use write_fonts::tables::hmtx::{Hmtx, LongMetric};
use write_fonts::tables::maxp::Maxp;
use write_fonts::tables::name::{Name, NameRecord};
use write_fonts::tables::os2::Os2;
use write_fonts::tables::post::Post;
use write_fonts::types::{FWord, Fixed, GlyphId, LongDateTime, NameId, Tag, UfWord};
use crate::pipeline::TtfGlyphCache;
use crate::sfnt::SerializedFontTables;
use super::cache::{
dump_cached_ttf_table, dump_ttf_table, hash_option_str, hash_str, table_cache_key,
};
use super::color::ColorFont;
use super::ligatures::LigaturePlaceholderGlyph;
use super::types::{CompiledGlyph, GlyphMetrics, TtfOptions};
use super::{clamp_to_i16, clamp_to_u16, current_unix_timestamp};
const DEFAULT_VENDOR_ID: Tag = Tag::new(b"ATLW");
const DEFAULT_TTF_DESCRIPTION: &str = "Generated by svg2ttf from Fontello project.";
const DEFAULT_TTF_MANUFACTURER_URL: &str = "http://fontello.com";
#[expect(clippy::too_many_arguments)]
pub(super) fn assemble_font(
options: &TtfOptions,
compiled_glyphs: &[CompiledGlyph],
cmap_aliases: &[(u32, usize)],
ligature_placeholders: &[LigaturePlaceholderGlyph],
glyf: write_fonts::tables::glyf::Glyf,
loca: write_fonts::tables::loca::Loca,
loca_format: write_fonts::tables::loca::LocaFormat,
metrics: &GlyphMetrics,
ascent: f64,
descent: f64,
font_height: f64,
color: ColorFont,
mut table_cache: Option<&mut TtfGlyphCache>,
) -> Result<SerializedFontTables, Error> {
let units_per_em = clamp_to_u16(font_height.round(), 16, 16_384);
let h_metrics = std::iter::once(LongMetric::new(0, 0))
.chain(
compiled_glyphs
.iter()
.map(|g| LongMetric::new(g.advance_width, g.left_side_bearing)),
)
.chain(ligature_placeholders.iter().map(|_| LongMetric::new(0, 0)))
.chain(
color
.layers
.iter()
.map(|g| LongMetric::new(0, g.left_side_bearing)),
)
.collect::<Vec<_>>();
let head_timestamp = derive_head_timestamp(options.ts);
let head = Head::new(
Fixed::from_i32(1),
0,
Default::default(),
units_per_em,
head_timestamp,
head_timestamp,
metrics.bbox.0,
metrics.bbox.1,
metrics.bbox.2,
metrics.bbox.3,
Default::default(),
8,
i16::from(loca_format == write_fonts::tables::loca::LocaFormat::Long),
);
let hhea = Hhea::new(
FWord::new(clamp_to_i16(ascent.round())),
FWord::new(clamp_to_i16(-descent.round())),
FWord::new(0),
UfWord::new(metrics.advance_width_max),
FWord::new(metrics.min_left_side_bearing),
FWord::new(clamp_to_i16(f64::from(metrics.min_right_side_bearing))),
FWord::new(clamp_to_i16(f64::from(metrics.x_max_extent))),
1,
0,
0,
h_metrics.len() as u16,
);
let hmtx = Hmtx::new(h_metrics, Vec::new());
let maxp = Maxp {
num_glyphs: (compiled_glyphs.len() + ligature_placeholders.len() + color.layers.len() + 1)
as u16,
max_points: Some(metrics.max_points),
max_contours: Some(metrics.max_contours),
max_composite_points: Some(0),
max_composite_contours: Some(0),
max_zones: Some(2),
max_twilight_points: Some(0),
max_storage: Some(0),
max_function_defs: Some(0),
max_instruction_defs: Some(0),
max_stack_elements: Some(0),
max_size_of_instructions: Some(0),
max_component_elements: Some(0),
max_component_depth: Some(0),
};
let os2 = build_os2(
options,
metrics,
ascent,
descent,
compiled_glyphs
.iter()
.map(|glyph| glyph.codepoint)
.chain(ligature_placeholders.iter().map(|glyph| glyph.codepoint)),
);
let cmap = Cmap::from_mappings(
compiled_glyphs
.iter()
.enumerate()
.filter_map(|(i, g)| {
char::from_u32(g.codepoint).map(|c| (c, GlyphId::new((i + 1) as u32)))
})
.chain(cmap_aliases.iter().filter_map(|(cp, idx)| {
char::from_u32(*cp).map(|c| (c, GlyphId::new((*idx + 1) as u32)))
}))
.chain(
ligature_placeholders
.iter()
.enumerate()
.filter_map(|(i, g)| {
char::from_u32(g.codepoint)
.map(|c| (c, GlyphId::new((compiled_glyphs.len() + i + 1) as u32)))
}),
),
)
.map_err(|error| {
Error::new(
ErrorKind::InvalidData,
format!("Failed to build cmap table: {error}"),
)
})?;
let font_subfamily = derive_subfamily_name(options);
let name = build_name_table(
options.font_name,
&font_subfamily,
None,
options.copyright,
options.description,
options.manufacturer_url,
derive_version_string(options.version).as_deref(),
);
let glyph_names = std::iter::once(".notdef")
.chain(compiled_glyphs.iter().map(|g| g.name.as_str()))
.chain(ligature_placeholders.iter().map(|g| g.name.as_str()))
.chain(color.layers.iter().map(|g| g.name.as_str()));
let max_custom_names = usize::from(u16::MAX) + 1 - DEFAULT_GLYPH_NAMES.len();
if glyph_names.clone().count() > max_custom_names {
let standard_names: HashSet<_> = DEFAULT_GLYPH_NAMES.iter().copied().collect();
let custom_names: HashSet<_> = glyph_names
.clone()
.filter(|name| !standard_names.contains(name))
.collect();
if custom_names.len() > max_custom_names {
return Err(Error::new(
ErrorKind::InvalidInput,
format!(
"Font exceeds the {max_custom_names} unique custom glyph-name limit in the post table."
),
));
}
}
let post = Post::new_v2(glyph_names);
let gsub = super::ligatures::build_ligature_gsub(compiled_glyphs, ligature_placeholders);
let mut used_table_keys = HashSet::new();
let mut tables = Vec::with_capacity(11);
tables.push(dump_ttf_table(&head, "head")?);
tables.push(dump_ttf_table(&hhea, "hhea")?);
tables.push(dump_ttf_table(&maxp, "maxp")?);
tables.push(dump_ttf_table(&os2, "OS/2")?);
tables.push(dump_cached_ttf_table(
&mut table_cache,
&mut used_table_keys,
|| hmtx_cache_key(compiled_glyphs, ligature_placeholders, &color.layers),
&hmtx,
"hmtx",
)?);
tables.push(dump_cached_ttf_table(
&mut table_cache,
&mut used_table_keys,
|| cmap_cache_key(compiled_glyphs, cmap_aliases, ligature_placeholders),
&cmap,
"cmap",
)?);
tables.push(dump_cached_ttf_table(
&mut table_cache,
&mut used_table_keys,
|| {
glyf_loca_cache_key(
b"loca",
compiled_glyphs,
ligature_placeholders,
&color.layers,
)
},
&loca,
"loca",
)?);
tables.push(dump_cached_ttf_table(
&mut table_cache,
&mut used_table_keys,
|| {
glyf_loca_cache_key(
b"glyf",
compiled_glyphs,
ligature_placeholders,
&color.layers,
)
},
&glyf,
"glyf",
)?);
tables.push(dump_cached_ttf_table(
&mut table_cache,
&mut used_table_keys,
|| name_cache_key(options, &font_subfamily),
&name,
"name",
)?);
tables.push(dump_cached_ttf_table(
&mut table_cache,
&mut used_table_keys,
|| post_cache_key(compiled_glyphs, ligature_placeholders, &color.layers),
&post,
"post",
)?);
if let Some(gsub) = &gsub {
tables.push(dump_cached_ttf_table(
&mut table_cache,
&mut used_table_keys,
|| gsub_cache_key(compiled_glyphs, ligature_placeholders),
gsub,
"GSUB",
)?);
}
if let Some(cache) = table_cache {
cache.tables.retain(|key, _| used_table_keys.contains(key));
}
tables.extend(color.tables);
SerializedFontTables::new(tables)
}
fn hmtx_cache_key(
compiled_glyphs: &[CompiledGlyph],
ligature_placeholders: &[LigaturePlaceholderGlyph],
layers: &[CompiledGlyph],
) -> u64 {
table_cache_key(b"hmtx", |hasher| {
for glyph in compiled_glyphs {
hasher.write_u16(glyph.advance_width);
hasher.write_i16(glyph.left_side_bearing);
}
hasher.write_usize(ligature_placeholders.len());
for layer in layers {
hasher.write_u16(0);
hasher.write_i16(layer.left_side_bearing);
}
})
}
fn cmap_cache_key(
compiled_glyphs: &[CompiledGlyph],
cmap_aliases: &[(u32, usize)],
ligature_placeholders: &[LigaturePlaceholderGlyph],
) -> u64 {
table_cache_key(b"cmap", |hasher| {
for glyph in compiled_glyphs {
hasher.write_u32(glyph.codepoint);
}
for (codepoint, index) in cmap_aliases {
hasher.write_u32(*codepoint);
hasher.write_usize(*index);
}
for glyph in ligature_placeholders {
hasher.write_u32(glyph.codepoint);
}
})
}
fn glyf_loca_cache_key(
tag: &[u8; 4],
compiled_glyphs: &[CompiledGlyph],
ligature_placeholders: &[LigaturePlaceholderGlyph],
layers: &[CompiledGlyph],
) -> u64 {
table_cache_key(tag, |hasher| {
for glyph in compiled_glyphs {
hasher.write_u64(
glyph
.outline_key
.expect("glyf/loca cache keys are only built for cached glyphs"),
);
}
hasher.write_usize(ligature_placeholders.len());
for layer in layers {
hasher.write_u64(layer.outline_key.expect("compiled layer outline key"));
}
})
}
fn name_cache_key(options: &TtfOptions, font_subfamily: &str) -> u64 {
table_cache_key(b"name", |hasher| {
hash_str(hasher, options.font_name);
hash_str(hasher, font_subfamily);
hash_option_str(hasher, options.copyright);
hash_option_str(hasher, options.description);
hash_option_str(hasher, options.manufacturer_url);
hash_option_str(hasher, derive_version_string(options.version).as_deref());
})
}
fn post_cache_key(
compiled_glyphs: &[CompiledGlyph],
ligature_placeholders: &[LigaturePlaceholderGlyph],
layers: &[CompiledGlyph],
) -> u64 {
table_cache_key(b"post", |hasher| {
for glyph in compiled_glyphs {
hash_str(hasher, &glyph.name);
}
for glyph in ligature_placeholders {
hash_str(hasher, &glyph.name);
}
for layer in layers {
hash_str(hasher, &layer.name);
}
})
}
fn gsub_cache_key(
compiled_glyphs: &[CompiledGlyph],
ligature_placeholders: &[LigaturePlaceholderGlyph],
) -> u64 {
table_cache_key(b"GSUB", |hasher| {
for glyph in compiled_glyphs {
hash_str(hasher, &glyph.name);
}
for glyph in ligature_placeholders {
hasher.write_u32(glyph.codepoint);
}
})
}
pub(super) fn build_name_table(
font_family: &str,
font_subfamily: &str,
postscript_name: Option<&str>,
copyright: Option<&str>,
description: Option<&str>,
url: Option<&str>,
version: Option<&str>,
) -> Name {
let full_name = if font_subfamily == "Regular" {
font_family.to_string()
} else {
format!("{font_family} {font_subfamily}")
};
let default_postscript_name = full_name.replace(' ', "-");
let mut name_record = vec![
make_windows_name_record(1, font_family),
make_windows_name_record(2, font_subfamily),
make_windows_name_record(4, &full_name),
make_windows_name_record(5, version.unwrap_or("Version 1.0")),
make_windows_name_record(6, postscript_name.unwrap_or(&default_postscript_name)),
];
if let Some(copyright) = copyright {
name_record.push(make_windows_name_record(0, copyright));
}
name_record.push(make_windows_name_record(
10,
description.unwrap_or(DEFAULT_TTF_DESCRIPTION),
));
name_record.push(make_windows_name_record(
11,
url.unwrap_or(DEFAULT_TTF_MANUFACTURER_URL),
));
name_record.sort();
Name {
name_record,
..Default::default()
}
}
pub(super) fn make_windows_name_record(name_id: u16, value: &str) -> NameRecord {
NameRecord::new(3, 1, 0x0409, NameId::new(name_id), value.to_string().into())
}
pub(super) fn build_os2(
options: &TtfOptions,
metrics: &GlyphMetrics,
ascent: f64,
descent: f64,
codepoints: impl Iterator<Item = u32>,
) -> Os2 {
let character_indices = codepoints
.map(|codepoint| codepoint.min(u32::from(u16::MAX)) as u16)
.collect::<Vec<_>>();
Os2 {
x_avg_char_width: metrics.x_avg_char_width,
us_weight_class: derive_weight_class(options.font_weight),
ach_vend_id: DEFAULT_VENDOR_ID,
us_first_char_index: character_indices.iter().copied().min().unwrap_or(0),
us_last_char_index: character_indices.iter().copied().max().unwrap_or(0),
s_typo_ascender: clamp_to_i16(ascent.round()),
s_typo_descender: clamp_to_i16(-descent.round()),
s_typo_line_gap: 0,
us_win_ascent: clamp_to_u16(ascent.round(), 0, u16::MAX),
us_win_descent: clamp_to_u16(descent.round(), 0, u16::MAX),
..Default::default()
}
}
fn derive_subfamily_name(options: &TtfOptions) -> String {
match options.font_style {
Some(style) if style.eq_ignore_ascii_case("italic") => "Italic".to_string(),
_ => "Regular".to_string(),
}
}
fn derive_weight_class(font_weight: Option<&str>) -> u16 {
match font_weight {
Some(value) if value.eq_ignore_ascii_case("bold") => 700,
Some(value) => value
.parse::<u16>()
.ok()
.filter(|weight| (1..=1000).contains(weight))
.unwrap_or(400),
None => 400,
}
}
pub(super) fn derive_version_string(version: Option<&str>) -> Option<String> {
let version = version?.trim();
if version.is_empty() {
return None;
}
if version.starts_with("Version ") {
return Some(version.to_string());
}
Some(format!("Version {}", version.trim_end_matches('.')))
}
fn derive_head_timestamp(ts: Option<i64>) -> LongDateTime {
const MAC_EPOCH_OFFSET_SECONDS: i64 = 2_082_844_800;
let unix_seconds = ts.unwrap_or_else(current_unix_timestamp);
LongDateTime::new(unix_seconds.saturating_add(MAC_EPOCH_OFFSET_SECONDS))
}