diff --git a/otl-normalizer/src/args.rs b/otl-normalizer/src/args.rs index 70ca6dfb8..9cdaf4de6 100644 --- a/otl-normalizer/src/args.rs +++ b/otl-normalizer/src/args.rs @@ -7,8 +7,8 @@ pub struct Args { /// Optional destination path for writing output. Default is stdout. pub out: Option, /// Target table to print, one of gpos/gsub/all (case insensitive) - #[arg(short, long)] - pub table: Option, + #[arg(short, long, default_value_t)] + pub table: Table, /// Index of font to examine, if target is a font collection #[arg(short, long)] pub index: Option, @@ -21,16 +21,29 @@ pub enum Table { All, Gpos, Gsub, + Gdef, +} + +impl std::fmt::Display for Table { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Table::All => f.write_str("all"), + Table::Gpos => f.write_str("gpos"), + Table::Gsub => f.write_str("gsub"), + Table::Gdef => f.write_str("gdef"), + } + } } impl FromStr for Table { type Err = &'static str; fn from_str(s: &str) -> Result { - static ERR_MSG: &str = "expected one of 'gsub', 'gpos', 'all'"; + static ERR_MSG: &str = "expected one of 'gsub', 'gpos', 'gdef', 'all'"; match s.to_ascii_lowercase().trim() { "gpos" => Ok(Self::Gpos), "gsub" => Ok(Self::Gsub), + "gdef" => Ok(Self::Gdef), "all" => Ok(Self::All), _ => Err(ERR_MSG), } diff --git a/otl-normalizer/src/common.rs b/otl-normalizer/src/common.rs index 9b4ec4234..3c2ce7a4c 100644 --- a/otl-normalizer/src/common.rs +++ b/otl-normalizer/src/common.rs @@ -8,12 +8,18 @@ use std::{ }; use write_fonts::{ - read::tables::layout::{FeatureList, ScriptList}, + read::{ + tables::{ + gpos::DeviceOrVariationIndex, + layout::{FeatureList, ScriptList}, + }, + ReadError, + }, tables::layout::LookupFlag, types::{GlyphId16, Tag}, }; -use crate::glyph_names::NameMap; +use crate::{glyph_names::NameMap, variations::DeltaComputer}; pub(crate) struct LanguageSystem { script: Tag, @@ -71,6 +77,37 @@ where filter_set: Option, } +/// Resolved device or delta values +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum DeviceOrDeltas { + Device { + start: u16, + end: u16, + values: Vec, + }, + Deltas(Vec), +} + +impl DeviceOrDeltas { + pub fn new( + default_value: i16, + device: DeviceOrVariationIndex, + ivs: Option<&DeltaComputer>, + ) -> Result { + match device { + DeviceOrVariationIndex::Device(device) => Ok(Self::Device { + start: device.start_size(), + end: device.end_size(), + values: device.iter().collect(), + }), + DeviceOrVariationIndex::VariationIndex(idx) => ivs + .unwrap() + .master_values(default_value as _, idx) + .map(Self::Deltas), + } + } +} + impl LanguageSystem { fn sort_key(&self) -> impl Ord { (tag_to_int(self.script), tag_to_int(self.lang)) @@ -377,3 +414,31 @@ impl FromIterator for GlyphSet { GlyphSet::Multiple(iter.into_iter().collect()) } } + +impl Display for DeviceOrDeltas { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DeviceOrDeltas::Device { start, end, values } => { + write!(f, " [({start})")?; + for (i, adj) in values.iter().enumerate() { + if i > 0 { + write!(f, ",")?; + } + write!(f, "{adj}")?; + } + write!(f, "({end})]")?; + } + DeviceOrDeltas::Deltas(deltas) => { + write!(f, " {{")?; + for (i, var) in deltas.iter().enumerate() { + if i > 0 { + write!(f, ",")?; + } + write!(f, "{var}")?; + } + write!(f, "}}")?; + } + } + Ok(()) + } +} diff --git a/otl-normalizer/src/error.rs b/otl-normalizer/src/error.rs index 030f2c0e9..c206451e5 100644 --- a/otl-normalizer/src/error.rs +++ b/otl-normalizer/src/error.rs @@ -17,7 +17,7 @@ pub enum Error { #[error("write error: '{0}'")] Write(#[from] std::io::Error), #[error("could not read font data: '{0}")] - FontRead(ReadError), + FontRead(#[from] ReadError), #[error("missing table '{0}'")] MissingTable(Tag), } diff --git a/otl-normalizer/src/gdef.rs b/otl-normalizer/src/gdef.rs new file mode 100644 index 000000000..ede9a8f4c --- /dev/null +++ b/otl-normalizer/src/gdef.rs @@ -0,0 +1,104 @@ +//! Normalizing the ligature caret table + +use std::{fmt::Display, io}; + +use fontdrasil::types::GlyphName; +use write_fonts::read::{ + tables::gdef::{CaretValue, Gdef, LigGlyph}, + ReadError, +}; + +use crate::{common::DeviceOrDeltas, variations::DeltaComputer, Error, NameMap}; + +/// Print normalized GDEF ligature carets +pub fn print(f: &mut dyn io::Write, table: &Gdef, names: &NameMap) -> Result<(), Error> { + let var_store = table + .item_var_store() + .map(|ivs| ivs.and_then(DeltaComputer::new)) + .transpose() + .unwrap(); + + // so this is relatively simple; we're just looking at the ligature caret list. + // - realistically, we only care if this has variations? but I think it's simpler + // if we just always normalize, variations or no. + + let Some(lig_carets) = table.lig_caret_list().transpose().unwrap() else { + return Ok(()); + }; + + let coverage = lig_carets.coverage()?; + for (gid, lig_glyph) in coverage.iter().zip(lig_carets.lig_glyphs().iter()) { + let lig_glyph = lig_glyph?; + let name = names.get(gid); + print_lig_carets(f, name, lig_glyph, var_store.as_ref())?; + } + + Ok(()) +} + +enum ResolvedCaret { + Coordinate { + pos: i16, + device_or_deltas: Option, + }, + // basically never used? + ContourPoint { + idx: u16, + }, +} + +impl ResolvedCaret { + fn new(raw: CaretValue, computer: Option<&DeltaComputer>) -> Result { + match raw { + CaretValue::Format1(table_ref) => Ok(Self::Coordinate { + pos: table_ref.coordinate(), + device_or_deltas: None, + }), + CaretValue::Format2(table_ref) => Ok(Self::ContourPoint { + idx: table_ref.caret_value_point_index(), + }), + CaretValue::Format3(table_ref) => { + let pos = table_ref.coordinate(); + let device = table_ref.device()?; + let device_or_deltas = DeviceOrDeltas::new(pos, device, computer)?; + Ok(Self::Coordinate { + pos, + device_or_deltas: Some(device_or_deltas), + }) + } + } + } +} + +fn print_lig_carets( + f: &mut dyn io::Write, + gname: &GlyphName, + lig_glyph: LigGlyph, + computer: Option<&DeltaComputer>, +) -> Result<(), Error> { + writeln!(f, "{gname}")?; + for (i, caret) in lig_glyph.caret_values().iter().enumerate() { + let resolved = caret.and_then(|caret| ResolvedCaret::new(caret, computer))?; + writeln!(f, " {i}: {resolved}")?; + } + + Ok(()) +} + +impl Display for ResolvedCaret { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ResolvedCaret::Coordinate { + pos, + device_or_deltas, + } => { + write!(f, "coord {pos}")?; + if let Some(device_or_deltas) = device_or_deltas { + write!(f, "{device_or_deltas}")?; + } + } + ResolvedCaret::ContourPoint { idx } => write!(f, "idx {idx}")?, + } + Ok(()) + } +} diff --git a/otl-normalizer/src/gpos.rs b/otl-normalizer/src/gpos.rs index 8b8da0b33..efb60c3e2 100644 --- a/otl-normalizer/src/gpos.rs +++ b/otl-normalizer/src/gpos.rs @@ -18,7 +18,7 @@ use write_fonts::{ }; use crate::{ - common::{self, GlyphSet, Lookup, PrintNames, SingleRule}, + common::{self, DeviceOrDeltas, GlyphSet, Lookup, PrintNames, SingleRule}, error::Error, glyph_names::NameMap, variations::DeltaComputer, @@ -116,16 +116,6 @@ fn print_rules( Ok(()) } -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -enum DeviceOrDeltas { - Device { - start: u16, - end: u16, - values: Vec, - }, - Deltas(Vec), -} - /// A value plus an optional device table or set of deltas #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] struct ResolvedValue { @@ -223,17 +213,9 @@ impl ResolvedValue { ivs: Option<&DeltaComputer>, ) -> Result { let default = default.unwrap_or_default(); - let device_or_deltas = device.transpose()?.map(|device| match device { - DeviceOrVariationIndex::Device(device) => Ok(DeviceOrDeltas::Device { - start: device.start_size(), - end: device.end_size(), - values: device.iter().collect(), - }), - DeviceOrVariationIndex::VariationIndex(idx) => ivs - .unwrap() - .master_values(default as _, idx) - .map(DeviceOrDeltas::Deltas), - }); + let device_or_deltas = device + .transpose()? + .map(|device| DeviceOrDeltas::new(default, device, ivs)); device_or_deltas .transpose() .map(|device_or_deltas| ResolvedValue { @@ -490,28 +472,8 @@ impl Display for ResolvedValueRecord { impl Display for ResolvedValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.default)?; - match &self.device_or_deltas { - Some(DeviceOrDeltas::Device { start, end, values }) => { - write!(f, " [({start})")?; - for (i, adj) in values.iter().enumerate() { - if i > 0 { - write!(f, ",")?; - } - write!(f, "{adj}")?; - } - write!(f, "({end})]")?; - } - Some(DeviceOrDeltas::Deltas(deltas)) => { - write!(f, " {{")?; - for (i, var) in deltas.iter().enumerate() { - if i > 0 { - write!(f, ",")?; - } - write!(f, "{var}")?; - } - write!(f, "}}")?; - } - None => (), + if let Some(device_or_deltas) = self.device_or_deltas.as_ref() { + write!(f, "{device_or_deltas}")?; } Ok(()) } diff --git a/otl-normalizer/src/lib.rs b/otl-normalizer/src/lib.rs index 07ab3c495..cd8b81346 100644 --- a/otl-normalizer/src/lib.rs +++ b/otl-normalizer/src/lib.rs @@ -5,6 +5,7 @@ pub mod args; mod common; mod error; +mod gdef; mod glyph_names; mod gpos; mod gsub; @@ -12,5 +13,7 @@ mod variations; pub use error::Error; pub use glyph_names::NameMap; + +pub use gdef::print as print_gdef; pub use gpos::print as print_gpos; pub use gsub::print as print_gsub; diff --git a/otl-normalizer/src/main.rs b/otl-normalizer/src/main.rs index 63767ad2e..5c86c31c1 100644 --- a/otl-normalizer/src/main.rs +++ b/otl-normalizer/src/main.rs @@ -16,6 +16,12 @@ fn main() -> Result<(), Error> { inner, })?; + let font = get_font(&data, args.index)?; + // exit early if there's no work, so we don't bother creating an empty file + if !is_there_something_to_do(&font, &args) { + return Ok(()); + } + let mut write_target: Box = match args.out.as_ref() { Some(path) => File::create(path) .map_err(|inner| Error::FileWrite { @@ -26,10 +32,17 @@ fn main() -> Result<(), Error> { None => Box::new(std::io::stdout()), }; - let font = get_font(&data, args.index)?; let name_map = NameMap::from_font(&font)?; - let to_print = args.table.unwrap_or_default(); + let to_print = args.table; let gdef = font.gdef().ok(); + + if matches!(to_print, args::Table::All | args::Table::Gdef) { + if let Some(gdef) = gdef.as_ref().filter(|gdef| gdef.lig_caret_list().is_some()) { + writeln!(&mut write_target, "# GDEF #")?; + otl_normalizer::print_gdef(&mut write_target, gdef, &name_map)?; + } + } + if matches!(to_print, args::Table::All | args::Table::Gpos) { if let Ok(gpos) = font.gpos() { writeln!(&mut write_target, "# GPOS #")?; @@ -57,3 +70,16 @@ fn get_font(bytes: &[u8], idx: Option) -> Result { (FileRef::Collection(collection), idx) => collection.get(idx).map_err(Error::FontRead), } } + +fn is_there_something_to_do(font: &FontRef, args: &args::Args) -> bool { + match args.table { + // gdef is meaningless without one of these two + args::Table::All => font.gpos().is_ok() || font.gsub().is_ok(), + args::Table::Gpos => font.gpos().is_ok(), + args::Table::Gsub => font.gsub().is_ok(), + args::Table::Gdef => font + .gdef() + .map(|gdef| gdef.lig_caret_list().is_some()) + .unwrap_or(false), + } +} diff --git a/resources/scripts/ttx_diff.py b/resources/scripts/ttx_diff.py index 6497b86bd..245d3e8ad 100755 --- a/resources/scripts/ttx_diff.py +++ b/resources/scripts/ttx_diff.py @@ -61,6 +61,7 @@ FLAGS = flags.FLAGS # used instead of a tag for the normalized mark/kern output MARK_KERN_NAME = "(mark/kern)" +LIG_CARET_NAME = "ligcaret" # maximum chars of stderr to include when reporting errors; prevents # too much bloat when run in CI MAX_ERR_LEN = 1000 @@ -154,14 +155,20 @@ def run_ttx(font_file: Path): # generate a simple text repr for gpos for this font, with retry -def run_normalizer_gpos(normalizer_bin: Path, font_file: Path): - out_path = font_file.with_suffix(".markkern.txt") +def run_normalizer(normalizer_bin: Path, font_file: Path, table: str): + if table == "gpos": + out_path = font_file.with_suffix(".markkern.txt") + elif table == "gdef": + out_path = font_file.with_suffix(f".{LIG_CARET_NAME}.txt") + else: + raise ValueError(f"unknown table for normalizer: '{table}'") + if out_path.exists(): eprint(f"reusing {out_path}") NUM_RETRIES = 5 for i in range(NUM_RETRIES + 1): try: - return try_normalizer_gpos(normalizer_bin, font_file, out_path) + return try_normalizer(normalizer_bin, font_file, out_path, table) except subprocess.CalledProcessError as e: time.sleep(0.1) if i >= NUM_RETRIES: @@ -171,7 +178,7 @@ def run_normalizer_gpos(normalizer_bin: Path, font_file: Path): # we had a bug where this would sometimes hang in mysterious ways, so we may # call it multiple times if it fails -def try_normalizer_gpos(normalizer_bin: Path, font_file: Path, out_path: Path): +def try_normalizer(normalizer_bin: Path, font_file: Path, out_path: Path, table: str): NORMALIZER_TIMEOUT = 60 * 10 # ten minutes if not out_path.is_file(): cmd = [ @@ -180,9 +187,12 @@ def try_normalizer_gpos(normalizer_bin: Path, font_file: Path, out_path: Path): "-o", out_path.name, "--table", - "gpos", + table, ] log_and_run(cmd, font_file.parent, check=True, timeout=NORMALIZER_TIMEOUT) + # if we finished running and there's no file then there's no output: + if not out_path.is_file(): + return "" with open(out_path) as f: return f.read() @@ -498,6 +508,18 @@ def remove_mark_and_kern_lookups(ttx): lookup_type_el.attrib["value"] = str(lookup_type) +# this all gets handled by otl-normalizer +def remove_gdef_lig_caret_and_var_store(ttx: etree.ElementTree): + gdef = ttx.find("GDEF") + if gdef is None: + return + + for ident in ["LigCaretList", "VarStore"]: + subtable = gdef.find(ident) + if subtable: + gdef.remove(subtable) + + def reduce_diff_noise(fontc: etree.ElementTree, fontmake: etree.ElementTree): sort_fontmake_feature_lookups(fontmake) for ttx in (fontc, fontmake): @@ -518,6 +540,7 @@ def reduce_diff_noise(fontc: etree.ElementTree, fontmake: etree.ElementTree): normalize_glyf_contours(ttx) erase_type_from_stranded_points(ttx) + remove_gdef_lig_caret_and_var_store(ttx) allow_some_off_by_ones(fontc, fontmake, "glyf/TTGlyph", "name", "/contour/pt") allow_some_off_by_ones( @@ -567,8 +590,10 @@ def generate_output( ): fontc_ttx = run_ttx(fontc_ttf) fontmake_ttx = run_ttx(fontmake_ttf) - fontc_gpos = run_normalizer_gpos(otl_norm_bin, fontc_ttf) - fontmake_gpos = run_normalizer_gpos(otl_norm_bin, fontmake_ttf) + fontc_gpos = run_normalizer(otl_norm_bin, fontc_ttf, "gpos") + fontmake_gpos = run_normalizer(otl_norm_bin, fontmake_ttf, "gpos") + fontc_gdef = run_normalizer(otl_norm_bin, fontc_ttf, "gdef") + fontmake_gdef = run_normalizer(otl_norm_bin, fontmake_ttf, "gdef") fontc = etree.parse(fontc_ttx) fontmake = etree.parse(fontmake_ttx) @@ -579,6 +604,10 @@ def generate_output( size_diffs = check_sizes(fontmake_ttf, fontc_ttf) fontc[MARK_KERN_NAME] = fontc_gpos fontmake[MARK_KERN_NAME] = fontmake_gpos + if len(fontc_gdef): + fontc[LIG_CARET_NAME] = fontc_gdef + if len(fontmake_gdef): + fontmake[LIG_CARET_NAME] = fontmake_gdef result = {"fontc": fontc, "fontmake": fontmake} if len(size_diffs) > 0: result["sizes"] = size_diffs