Skip to main content

ttj/
lib.rs

1/// Convert a font to a serialized JSON representation
2pub mod context;
3mod gdef;
4pub mod jsondiff;
5mod layout;
6pub mod monkeypatching;
7pub mod namemap;
8mod serializefont;
9
10use crate::{jsondiff::diff, serializefont::ToValue};
11use context::SerializationContext;
12use namemap::NameMap;
13use read_fonts::{traversal::SomeTable, FontRef, TableProvider};
14use serde_json::{Map, Value};
15use skrifa::{charmap::Charmap, string::StringId, MetadataProvider};
16
17pub use layout::gpos::just_kerns;
18
19fn serialize_name_table<'a>(font: &(impl MetadataProvider<'a> + TableProvider<'a>)) -> Value {
20    let mut map = Map::new();
21    if let Ok(name) = font.name() {
22        let mut ids: Vec<StringId> = name.name_record().iter().map(|x| x.name_id()).collect();
23        ids.sort_by_key(|id| id.to_u16());
24        for id in ids {
25            let strings = font.localized_strings(id);
26            if strings.clone().next().is_some() {
27                let mut localized = Map::new();
28                for string in font.localized_strings(id) {
29                    localized.insert(
30                        string.language().unwrap_or("default").to_string(),
31                        Value::String(string.to_string()),
32                    );
33                }
34                map.insert(id.to_string(), Value::Object(localized));
35            }
36        }
37    }
38    Value::Object(map)
39}
40
41fn serialize_cmap_table<'a>(font: &FontRef<'a>, names: &NameMap) -> Value {
42    let charmap = Charmap::new(font);
43    let mut map: Map<String, Value> = Map::new();
44    for (codepoint, gid) in charmap.mappings() {
45        let name = names.get(gid);
46        map.insert(format!("U+{:04X}", codepoint), Value::String(name));
47    }
48    Value::Object(map)
49}
50
51fn serialize_hmtx_table<'a>(font: &impl TableProvider<'a>, names: &NameMap) -> Value {
52    let mut map = Map::new();
53    if let Ok(hmtx) = font.hmtx() {
54        let widths = hmtx.h_metrics();
55        let long_metrics = widths.len();
56        for gid in 0..font.maxp().unwrap().num_glyphs() {
57            let name = names.get(gid);
58            if gid < (long_metrics as u16) {
59                if let Some((width, lsb)) = widths
60                    .get(gid as usize)
61                    .map(|lm| (lm.advance(), lm.side_bearing()))
62                {
63                    map.insert(
64                        name,
65                        Value::Object(
66                            vec![
67                                ("width".to_string(), Value::Number(width.into())),
68                                ("lsb".to_string(), Value::Number(lsb.into())),
69                            ]
70                            .into_iter()
71                            .collect(),
72                        ),
73                    );
74                }
75            } else {
76                // XXX
77            }
78        }
79    }
80    Value::Object(map)
81}
82
83/// Convert a font to a serialized JSON representation
84///
85/// This function is used to serialize a font to a JSON representation which can be compared with
86/// another font. The JSON representation is a map of tables, where each table is represented as a
87/// map of fields and values. The user of this function can also provide a glyph map, which is a
88/// mapping from glyph IDs to glyph names. If the glyph map is not provided, the function will
89/// attempt to create one from the font itself. (You may want to specify a glyph map from another
90/// font to remove false positive differences if you are comparing two fonts which have the same glyph
91/// order but the glyph names have changed, e.g. when development names have changed to production names.)
92pub fn font_to_json(font: &FontRef, glyphmap: Option<&NameMap>) -> Value {
93    let glyphmap = if let Some(glyphmap) = glyphmap {
94        glyphmap
95    } else {
96        &NameMap::new(font)
97    };
98    let mut map = Map::new();
99    // A serialization context bundles up all the information we need to serialize a font
100    let context = SerializationContext::new(font, glyphmap.clone()).unwrap_or_else(|_| {
101        panic!("Could not create serialization context for font");
102    });
103
104    // Some tables are serialized by using read_font's traversal feature; typically those which
105    // are just a set of fields and values (or are so complicated we haven't yet been bothered
106    // to write our own serializers for them...)
107    for table in font.table_directory.table_records().iter() {
108        let key = table.tag().to_string();
109        let value = match table.tag().into_bytes().as_ref() {
110            b"head" => font.head().map(|t| <dyn SomeTable>::serialize(&t)),
111            b"hhea" => font.hhea().map(|t| <dyn SomeTable>::serialize(&t)),
112            b"vhea" => font.vhea().map(|t| <dyn SomeTable>::serialize(&t)),
113            b"vmtx" => font.vmtx().map(|t| <dyn SomeTable>::serialize(&t)),
114            b"fvar" => font.fvar().map(|t| <dyn SomeTable>::serialize(&t)),
115            b"avar" => font.avar().map(|t| <dyn SomeTable>::serialize(&t)),
116            b"HVAR" => font.hvar().map(|t| <dyn SomeTable>::serialize(&t)),
117            b"VVAR" => font.vvar().map(|t| <dyn SomeTable>::serialize(&t)),
118            b"MVAR" => font.mvar().map(|t| <dyn SomeTable>::serialize(&t)),
119            b"maxp" => font.maxp().map(|t| <dyn SomeTable>::serialize(&t)),
120            b"OS/2" => font.os2().map(|t| <dyn SomeTable>::serialize(&t)),
121            b"post" => font.post().map(|t| <dyn SomeTable>::serialize(&t)),
122            b"loca" => font.loca(None).map(|t| <dyn SomeTable>::serialize(&t)),
123            b"glyf" => font.glyf().map(|t| <dyn SomeTable>::serialize(&t)),
124            b"gvar" => font.gvar().map(|t| <dyn SomeTable>::serialize(&t)),
125            b"COLR" => font.colr().map(|t| <dyn SomeTable>::serialize(&t)),
126            b"CPAL" => font.cpal().map(|t| <dyn SomeTable>::serialize(&t)),
127            b"STAT" => font.stat().map(|t| <dyn SomeTable>::serialize(&t)),
128            _ => font.expect_data_for_tag(table.tag()).map(|tabledata| {
129                Value::Array(
130                    tabledata
131                        .as_ref()
132                        .iter()
133                        .map(|&x| Value::Number(x.into()))
134                        .collect(),
135                )
136            }),
137        };
138        map.insert(
139            key,
140            value.unwrap_or_else(|_| Value::String("Could not parse".to_string())),
141        );
142    }
143
144    // Other tables require a bit of massaging to produce information which makes sense to diff.
145    map.insert("name".to_string(), serialize_name_table(font));
146    map.insert("cmap".to_string(), serialize_cmap_table(font, glyphmap));
147    map.insert("hmtx".to_string(), serialize_hmtx_table(font, glyphmap));
148    map.insert("GDEF".to_string(), gdef::serialize_gdef_table(&context));
149    map.insert("GPOS".to_string(), layout::serialize_gpos_table(&context));
150    map.insert("GSUB".to_string(), layout::serialize_gsub_table(&context));
151    Value::Object(map)
152}
153
154/// Compare two fonts and return a JSON representation of the differences
155///
156/// This function compares two fonts and returns a JSON representation of the differences between
157/// them.
158///
159/// Arguments:
160///
161/// * `font_a` - The first font to compare
162/// * `font_b` - The second font to compare
163/// * `max_changes` - The maximum number of changes to report before giving up
164/// * `no_match` - Don't try to match glyph names between fonts
165pub fn table_diff(font_a: &FontRef, font_b: &FontRef, max_changes: usize, no_match: bool) -> Value {
166    let glyphmap_a = NameMap::new(font_a);
167    let glyphmap_b = NameMap::new(font_b);
168    let big_difference = !no_match && !glyphmap_a.compatible(&glyphmap_b);
169
170    #[cfg(not(target_family = "wasm"))]
171    if big_difference {
172        log::info!("Glyph names differ dramatically between fonts, using font names from font A");
173    }
174
175    let mut font_a_json = font_to_json(font_a, Some(&glyphmap_a));
176    let mut font_b_json = font_to_json(
177        font_b,
178        Some(if big_difference {
179            &glyphmap_a
180        } else {
181            &glyphmap_b
182        }),
183    );
184
185    // Remove some tables which aren't useful
186    font_a_json.as_object_mut().unwrap().remove("glyf");
187    font_b_json.as_object_mut().unwrap().remove("glyf");
188    font_a_json.as_object_mut().unwrap().remove("loca");
189    font_b_json.as_object_mut().unwrap().remove("loca");
190
191    diff(&font_a_json, &font_b_json, max_changes)
192}
193
194/// Compare two fonts and return a JSON representation of the differences in kerning
195///
196/// Arguments:
197///
198/// * `font_a` - The first font to compare
199/// * `font_b` - The second font to compare
200/// * `max_changes` - The maximum number of changes to report before giving up
201/// * `no_match` - Don't try to match glyph names between fonts
202pub fn kern_diff(font_a: &FontRef, font_b: &FontRef, max_changes: usize, no_match: bool) -> Value {
203    let glyphmap_a = NameMap::new(font_a);
204    let glyphmap_b = NameMap::new(font_b);
205    let big_difference = !no_match && !glyphmap_a.compatible(&glyphmap_b);
206
207    #[cfg(not(target_family = "wasm"))]
208    if big_difference {
209        log::info!("Glyph names differ dramatically between fonts, using font names from font A");
210    }
211
212    let kerns_a = just_kerns(font_to_json(font_a, None));
213    // println!("Font A flat kerning: {:#?}", kerns_a);
214    let kerns_b = just_kerns(font_to_json(
215        font_b,
216        Some(if big_difference {
217            &glyphmap_a
218        } else {
219            &glyphmap_b
220        }),
221    ));
222    // println!("Font B flat kerning: {:#?}", kerns_b);
223
224    diff(&kerns_a, &kerns_b, max_changes)
225}