use std::{cmp::Ordering, collections::HashSet};
use fontdrasil::{
orchestration::{Access, AccessBuilder, Work},
types::WidthClass,
};
use fontir::{
ir::{GlobalMetricsInstance, Panose},
orchestration::WorkId as FeWorkId,
};
use write_fonts::{
OtRound,
read::{FontData, TopLevelTable, tables::hmtx::Hmtx},
tables::os2::Os2,
types::Tag,
};
use crate::{
error::Error,
orchestration::{AnyWorkId, BeWork, Context, WorkId},
};
mod max_context;
const UNICODE_RANGES: &[(u32, u32, u32)] = &[
(0x0000, 0x007F, 0), (0x0080, 0x00FF, 1), (0x0100, 0x017F, 2), (0x0180, 0x024F, 3), (0x0250, 0x02AF, 4), (0x02B0, 0x02FF, 5), (0x0300, 0x036F, 6), (0x0370, 0x03FF, 7), (0x0400, 0x04FF, 9), (0x0500, 0x052F, 9), (0x0530, 0x058F, 10), (0x0590, 0x05FF, 11), (0x0600, 0x06FF, 13), (0x0700, 0x074F, 71), (0x0750, 0x077F, 13), (0x0780, 0x07BF, 72), (0x07C0, 0x07FF, 14), (0x0900, 0x097F, 15), (0x0980, 0x09FF, 16), (0x0A00, 0x0A7F, 17), (0x0A80, 0x0AFF, 18), (0x0B00, 0x0B7F, 19), (0x0B80, 0x0BFF, 20), (0x0C00, 0x0C7F, 21), (0x0C80, 0x0CFF, 22), (0x0D00, 0x0D7F, 23), (0x0D80, 0x0DFF, 73), (0x0E00, 0x0E7F, 24), (0x0E80, 0x0EFF, 25), (0x0F00, 0x0FFF, 70), (0x1000, 0x109F, 74), (0x10A0, 0x10FF, 26), (0x1100, 0x11FF, 28), (0x1200, 0x137F, 75), (0x1380, 0x139F, 75), (0x13A0, 0x13FF, 76), (0x1400, 0x167F, 77), (0x1680, 0x169F, 78), (0x16A0, 0x16FF, 79), (0x1700, 0x171F, 84), (0x1720, 0x173F, 84), (0x1740, 0x175F, 84), (0x1760, 0x177F, 84), (0x1780, 0x17FF, 80), (0x1800, 0x18AF, 81), (0x1900, 0x194F, 93), (0x1950, 0x197F, 94), (0x1980, 0x19DF, 95), (0x19E0, 0x19FF, 80), (0x1A00, 0x1A1F, 96), (0x1B00, 0x1B7F, 27), (0x1B80, 0x1BBF, 112), (0x1C00, 0x1C4F, 113), (0x1C50, 0x1C7F, 114), (0x1D00, 0x1D7F, 4), (0x1D80, 0x1DBF, 4), (0x1DC0, 0x1DFF, 6), (0x1E00, 0x1EFF, 29), (0x1F00, 0x1FFF, 30), (0x2000, 0x206F, 31), (0x2070, 0x209F, 32), (0x20A0, 0x20CF, 33), (0x20D0, 0x20FF, 34), (0x2100, 0x214F, 35), (0x2150, 0x218F, 36), (0x2190, 0x21FF, 37), (0x2200, 0x22FF, 38), (0x2300, 0x23FF, 39), (0x2400, 0x243F, 40), (0x2440, 0x245F, 41), (0x2460, 0x24FF, 42), (0x2500, 0x257F, 43), (0x2580, 0x259F, 44), (0x25A0, 0x25FF, 45), (0x2600, 0x26FF, 46), (0x2700, 0x27BF, 47), (0x27C0, 0x27EF, 38), (0x27F0, 0x27FF, 37), (0x2800, 0x28FF, 82), (0x2900, 0x297F, 37), (0x2980, 0x29FF, 38), (0x2A00, 0x2AFF, 38), (0x2B00, 0x2BFF, 37), (0x2C00, 0x2C5F, 97), (0x2C60, 0x2C7F, 29), (0x2C80, 0x2CFF, 8), (0x2D00, 0x2D2F, 26), (0x2D30, 0x2D7F, 98), (0x2D80, 0x2DDF, 75), (0x2DE0, 0x2DFF, 9), (0x2E00, 0x2E7F, 31), (0x2E80, 0x2EFF, 59), (0x2F00, 0x2FDF, 59), (0x2FF0, 0x2FFF, 59), (0x3000, 0x303F, 48), (0x3040, 0x309F, 49), (0x30A0, 0x30FF, 50), (0x3100, 0x312F, 51), (0x3130, 0x318F, 52), (0x3190, 0x319F, 59), (0x31A0, 0x31BF, 51), (0x31C0, 0x31EF, 61), (0x31F0, 0x31FF, 50), (0x3200, 0x32FF, 54), (0x3300, 0x33FF, 55), (0x3400, 0x4DBF, 59), (0x4DC0, 0x4DFF, 99), (0x4E00, 0x9FFF, 59), (0xA000, 0xA48F, 83), (0xA490, 0xA4CF, 83), (0xA500, 0xA63F, 12), (0xA640, 0xA69F, 9), (0xA700, 0xA71F, 5), (0xA720, 0xA7FF, 29), (0xA800, 0xA82F, 100), (0xA840, 0xA87F, 53), (0xA880, 0xA8DF, 115), (0xA900, 0xA92F, 116), (0xA930, 0xA95F, 117), (0xAA00, 0xAA5F, 118), (0xAC00, 0xD7AF, 56), (0xD800, 0xDFFF, 57), (0xE000, 0xF8FF, 60), (0xF900, 0xFAFF, 61), (0xFB00, 0xFB4F, 62), (0xFB50, 0xFDFF, 63), (0xFE00, 0xFE0F, 91), (0xFE10, 0xFE1F, 65), (0xFE20, 0xFE2F, 64), (0xFE30, 0xFE4F, 65), (0xFE50, 0xFE6F, 66), (0xFE70, 0xFEFF, 67), (0xFF00, 0xFFEF, 68), (0xFFF0, 0xFFFF, 69), (0x10000, 0x1007F, 101), (0x10080, 0x100FF, 101), (0x10100, 0x1013F, 101), (0x10140, 0x1018F, 102), (0x10190, 0x101CF, 119), (0x101D0, 0x101FF, 120), (0x10280, 0x1029F, 121), (0x102A0, 0x102DF, 121), (0x10300, 0x1032F, 85), (0x10330, 0x1034F, 86), (0x10380, 0x1039F, 103), (0x103A0, 0x103DF, 104), (0x10400, 0x1044F, 87), (0x10450, 0x1047F, 105), (0x10480, 0x104AF, 106), (0x10800, 0x1083F, 107), (0x10900, 0x1091F, 58), (0x10920, 0x1093F, 121), (0x10A00, 0x10A5F, 108), (0x12000, 0x123FF, 110), (0x12400, 0x1247F, 110), (0x1D000, 0x1D0FF, 88), (0x1D100, 0x1D1FF, 88), (0x1D200, 0x1D24F, 88), (0x1D300, 0x1D35F, 109), (0x1D360, 0x1D37F, 111), (0x1D400, 0x1D7FF, 89), (0x1F000, 0x1F02F, 122), (0x1F030, 0x1F09F, 122), (0x20000, 0x2A6DF, 59), (0x2F800, 0x2FA1F, 61), (0xE0000, 0xE007F, 92), (0xE0100, 0xE01EF, 91), (0xF0000, 0xFFFFD, 90), (0x100000, 0x10FFFD, 90), ];
#[derive(Debug)]
struct Os2Work {}
pub fn create_os2_work() -> Box<BeWork> {
Box::new(Os2Work {})
}
fn x_avg_char_width(context: &Context) -> Result<i16, Error> {
let glyph_order = context.ir.glyph_order.get();
let hhea = context.hhea.get();
let raw_hmtx = context.hmtx.get();
let num_glyphs = glyph_order.len() as u64;
let hmtx = Hmtx::read(FontData::new(raw_hmtx.get()), hhea.number_of_h_metrics)
.map_err(|_| Error::InvalidTableBytes(Hmtx::TAG))?;
let (count, total) = hmtx
.h_metrics()
.iter()
.filter_map(|metric| match metric.advance() {
0 => None,
v => Some(v as u64),
})
.fold((0_u64, 0_u64), |(count, total), value| {
(count + 1, total + value)
});
let last_advance = hmtx
.h_metrics()
.last()
.map(|m| m.advance() as u64)
.unwrap_or_default();
let (count, total) = if last_advance > 0 {
let num_short = num_glyphs - hhea.number_of_h_metrics as u64;
(count + num_short, total + num_short * last_advance)
} else {
(count, total)
};
Ok((total as f32 / count as f32).ot_round())
}
fn apply_panose(os2: &mut Os2, panose: Option<&Panose>) {
let Some(panose) = panose else {
return;
};
os2.panose_10 = panose.to_bytes();
}
fn apply_metrics(os2: &mut Os2, metrics: &GlobalMetricsInstance) {
os2.s_cap_height = Some(metrics.cap_height.ot_round());
os2.sx_height = Some(metrics.x_height.ot_round());
os2.y_subscript_x_size = metrics.subscript_x_size.ot_round();
os2.y_subscript_y_size = metrics.subscript_y_size.ot_round();
os2.y_subscript_x_offset = metrics.subscript_x_offset.ot_round();
os2.y_subscript_y_offset = metrics.subscript_y_offset.ot_round();
os2.y_superscript_x_size = metrics.superscript_x_size.ot_round();
os2.y_superscript_y_size = metrics.superscript_y_size.ot_round();
os2.y_superscript_x_offset = metrics.superscript_x_offset.ot_round();
os2.y_superscript_y_offset = metrics.superscript_y_offset.ot_round();
os2.y_strikeout_size = metrics.strikeout_size.ot_round();
os2.y_strikeout_position = metrics.strikeout_position.ot_round();
os2.s_typo_ascender = metrics.os2_typo_ascender.ot_round();
os2.s_typo_descender = metrics.os2_typo_descender.ot_round();
os2.s_typo_line_gap = metrics.os2_typo_line_gap.ot_round();
os2.us_win_ascent = metrics.os2_win_ascent.ot_round();
os2.us_win_descent = metrics.os2_win_descent.ot_round();
}
fn add_unicode_range_bits(add_to: &mut HashSet<u32>, codepoint: u32) {
let maybe_idx = UNICODE_RANGES
.binary_search_by(|(from, to, _)| match codepoint {
_ if codepoint < *from => Ordering::Greater,
_ if codepoint > *to => Ordering::Less,
_ => Ordering::Equal,
})
.ok();
if let Some(idx) = maybe_idx {
add_to.insert(UNICODE_RANGES[idx].2);
}
if (0x10000..=0x10FFFF).contains(&codepoint) {
add_to.insert(57);
}
}
fn apply_unicode_range(
os2: &mut Os2,
assigned_bits: Option<HashSet<u32>>,
codepoints: &HashSet<u32>,
) {
let bits = assigned_bits.unwrap_or_else(|| {
let mut bits = HashSet::new();
for codepoint in codepoints {
add_unicode_range_bits(&mut bits, *codepoint);
}
bits
});
let mut unicode_range = [0u32; 4];
for bit in bits {
let idx = bit / 32;
let bit = bit - idx * 32;
assert!(bit <= 32, "{bit}");
unicode_range[idx as usize] |= 1 << bit;
}
os2.ul_unicode_range_1 = unicode_range[0];
os2.ul_unicode_range_2 = unicode_range[1];
os2.ul_unicode_range_3 = unicode_range[2];
os2.ul_unicode_range_4 = unicode_range[3];
}
fn codepage_range_bits(codepoints: &HashSet<u32>) -> HashSet<u32> {
let mut bits = HashSet::new();
let chars = codepoints
.iter()
.filter_map(|cp| char::from_u32(*cp))
.collect::<HashSet<_>>();
let has_ascii = (0x20_u32..0x7E).all(|cp| codepoints.contains(&cp));
let has_lineart = chars.contains(&'┤');
for char in chars.iter() {
match char {
'Þ' if has_ascii => {
bits.insert(0);
} 'Ľ' if has_ascii => {
bits.insert(1); if has_lineart {
bits.insert(58); }
}
'Б' => {
bits.insert(2); if chars.contains(&'Ѕ') && has_lineart {
bits.insert(57); }
if chars.contains(&'╜') && has_lineart {
bits.insert(49); }
}
'Ά' => {
bits.insert(3); if has_lineart && chars.contains(&'½') {
bits.insert(48); }
if has_lineart && chars.contains(&'√') {
bits.insert(60); }
}
'İ' if has_ascii => {
bits.insert(4); if has_lineart {
bits.insert(56); }
}
'א' => {
bits.insert(5); if has_lineart && chars.contains(&'√') {
bits.insert(53); }
}
'ر' => {
bits.insert(6); if chars.contains(&'√') {
bits.insert(51); }
if has_lineart {
bits.insert(61); }
}
'ŗ' if has_ascii => {
bits.insert(7); if has_lineart {
bits.insert(59); }
}
'₫' if has_ascii => {
bits.insert(8); }
'ๅ' => {
bits.insert(16); }
'エ' => {
bits.insert(17); }
'ㄅ' => {
bits.insert(18); }
'ㄱ' => {
bits.insert(19); }
'央' => {
bits.insert(20); }
'곴' => {
bits.insert(21); }
'♥' if has_ascii => {
bits.insert(30); }
'þ' if has_ascii && has_lineart => {
bits.insert(54); }
'╚' if has_ascii => {
bits.insert(62); bits.insert(63); }
'Å' if has_ascii && has_lineart && chars.contains(&'√') => {
bits.insert(50); }
'é' if has_ascii && has_lineart && chars.contains(&'√') => {
bits.insert(52); }
'õ' if has_ascii && has_lineart && chars.contains(&'√') => {
bits.insert(55); }
_ => (),
}
}
if has_ascii && chars.contains(&'‰') && chars.contains(&'∑') {
bits.insert(29); }
if bits.is_empty() {
bits.insert(0);
}
bits
}
fn apply_codepage_range(
os2: &mut Os2,
assigned_bits: Option<HashSet<u32>>,
codepoints: &HashSet<u32>,
) {
let bits = assigned_bits.unwrap_or_else(|| codepage_range_bits(codepoints));
let mut codepage_range = [0u32; 2];
for bit in bits {
let idx = bit / 32;
let bit = bit - idx * 32;
assert!(bit <= 32, "{bit}");
codepage_range[idx as usize] |= 1 << bit;
}
os2.ul_code_page_range_1 = Some(codepage_range[0]);
os2.ul_code_page_range_2 = Some(codepage_range[1]);
}
fn apply_min_max_char_index(os2: &mut Os2, codepoints: &HashSet<u32>) {
let (min, max) = codepoints
.iter()
.fold((0xFFFF, 0), |(min, max), cp| (*cp.min(&min), *cp.max(&max)));
os2.us_first_char_index = min.min(0xFFFF) as u16;
os2.us_last_char_index = max.min(0xFFFF) as u16;
}
fn apply_max_context(os2: &mut Os2, context: &Context) {
let gsub = context.gsub.try_get();
let gsub = gsub.as_deref();
let gpos = context.gpos.try_get();
let gpos = gpos.as_deref();
os2.us_max_context = Some(max_context::compute_max_context_value(gpos, gsub));
}
fn codepoints(context: &Context) -> HashSet<u32> {
let glyph_order = context.ir.glyph_order.get();
let mut codepoints = HashSet::new();
for glyph_name in glyph_order.names() {
codepoints.extend(context.ir.get_glyph(glyph_name.clone()).codepoints.iter());
}
codepoints
}
impl Work<Context, AnyWorkId, Error> for Os2Work {
fn id(&self) -> AnyWorkId {
WorkId::Os2.into()
}
fn read_access(&self) -> Access<AnyWorkId> {
AccessBuilder::new()
.variant(FeWorkId::StaticMetadata)
.variant(FeWorkId::GlyphOrder)
.variant(FeWorkId::GlobalMetrics)
.variant(WorkId::Hhea)
.variant(WorkId::Hmtx)
.variant(WorkId::Gpos)
.variant(WorkId::Gsub)
.variant(FeWorkId::ALL_GLYPHS)
.build()
}
fn exec(&self, context: &Context) -> Result<(), Error> {
let static_metadata = context.ir.static_metadata.get();
let us_x_class = |misc_value: Option<u16>, axis_tag, default, clamp: fn(f64) -> u16| {
static_metadata
.axis(&Tag::new(axis_tag))
.map(|axis| clamp(axis.default.into_inner().0))
.or(misc_value)
.unwrap_or(clamp(default))
};
let us_weight_class =
us_x_class(static_metadata.misc.us_weight_class, b"wght", 400.0, |v| {
v.clamp(1.0, 1000.0).ot_round()
});
let us_width_class = us_x_class(static_metadata.misc.us_width_class, b"wdth", 100.0, |v| {
WidthClass::nearest(v) as u16
});
let metrics = context
.ir
.global_metrics
.get()
.at(static_metadata.default_location());
let codepoints = codepoints(context);
let mut os2 = Os2 {
us_weight_class,
us_width_class,
fs_type: static_metadata.misc.fs_type.unwrap_or_default(),
ach_vend_id: static_metadata.misc.vendor_id,
fs_selection: static_metadata.misc.selection_flags,
x_avg_char_width: x_avg_char_width(context)?,
us_break_char: Some(32),
s_family_class: static_metadata.misc.family_class.unwrap_or_default(),
us_default_char: Some(0),
us_max_context: Some(0),
..Default::default()
};
apply_panose(&mut os2, static_metadata.misc.panose.as_ref());
apply_metrics(&mut os2, &metrics);
apply_unicode_range(
&mut os2,
static_metadata.misc.unicode_range_bits.clone(),
&codepoints,
);
apply_codepage_range(
&mut os2,
static_metadata.misc.codepage_range_bits.clone(),
&codepoints,
);
apply_min_max_char_index(&mut os2, &codepoints);
apply_max_context(&mut os2, context);
context.os2.set(os2);
Ok(())
}
}
#[cfg(test)]
mod tests {
use fontdrasil::{coords::NormalizedLocation, types::Axes};
use fontir::ir::{GlobalMetric, GlobalMetricsBuilder};
use std::collections::HashSet;
use write_fonts::tables::os2::Os2;
use crate::os2::codepage_range_bits;
use super::*;
#[test]
fn build_basic_os2() {
let default_location = NormalizedLocation::new();
let mut global_metrics = GlobalMetricsBuilder::new();
global_metrics.set(GlobalMetric::CapHeight, default_location.clone(), 37.5);
global_metrics.set(GlobalMetric::XHeight, default_location.clone(), 112.2);
global_metrics.populate_defaults(&default_location, 1000, None, None, None, Some(0.0));
let mut os2 = Os2 {
ach_vend_id: Tag::new(b"DUCK"),
x_avg_char_width: 42,
..Default::default()
};
apply_metrics(
&mut os2,
&global_metrics
.build(&Axes::default())
.unwrap()
.at(&default_location),
);
assert_eq!(Tag::new(b"DUCK"), os2.ach_vend_id);
assert_eq!(42, os2.x_avg_char_width);
assert_eq!(Some(38), os2.s_cap_height);
assert_eq!(Some(112), os2.sx_height);
}
fn unicode_range_bits(codepoint: u32) -> HashSet<u32> {
let mut bits = HashSet::new();
add_unicode_range_bits(&mut bits, codepoint);
bits
}
#[test]
fn unicode_range_bit_lut_latin() {
assert_eq!(HashSet::from([0]), unicode_range_bits(0x65));
}
#[test]
fn unicode_range_bit_lut_large() {
assert_eq!(HashSet::from([57]), unicode_range_bits(0x10FFFF));
}
#[test]
fn unicode_range_bit_lut_mahjong() {
assert_eq!(HashSet::from([57, 122]), unicode_range_bits(0x1F02F));
}
#[test]
fn codepage_range_report_latin_1() {
assert_eq!(
HashSet::from([0]),
codepage_range_bits(&(0x20..=0x7E).collect())
);
}
#[test]
fn codepage_range_fallback_to_latin_1() {
let latin_1_sentinel = 'Þ' as u32;
let latin_2_sentinel = 'Ľ' as u32;
let mut codepoints = (0x20..=0x7E).collect::<HashSet<_>>();
codepoints.remove(&latin_1_sentinel);
assert_eq!(HashSet::from([0]), codepage_range_bits(&codepoints));
codepoints.insert(latin_2_sentinel);
assert_eq!(HashSet::from([1]), codepage_range_bits(&codepoints));
codepoints.insert(latin_1_sentinel);
assert_eq!(HashSet::from([0, 1]), codepage_range_bits(&codepoints));
}
#[test]
fn min_max_char_index_simple() {
let codepoints = HashSet::from([5, 6, 99]);
let mut os2 = Os2::default();
apply_min_max_char_index(&mut os2, &codepoints);
assert_eq!((5, 99), (os2.us_first_char_index, os2.us_last_char_index));
}
#[test]
fn min_max_char_index_big_values() {
let codepoints = (0x1F000..=0x1F02B).collect();
let mut os2 = Os2::default();
apply_min_max_char_index(&mut os2, &codepoints);
assert_eq!(
(0xFFFF, 0xFFFF),
(os2.us_first_char_index, os2.us_last_char_index)
);
}
}