Skip to main content

diffenator3_lib/
dfont.rs

1use crate::setting::parse_location;
2use read_fonts::{types::NameId, FontRef, ReadError, TableProvider};
3use skrifa::{instance::Location, setting::VariationSetting, MetadataProvider};
4use std::{
5    borrow::Cow,
6    collections::{HashMap, HashSet},
7};
8use ttj::monkeypatching::DenormalizeLocation;
9use ucd::Codepoint;
10
11/// A representation of everything we need to know about a font for diffenator purposes
12#[derive(Debug, Clone)]
13pub struct DFont {
14    /// The font binary data
15    pub backing: Vec<u8>,
16    /// The location of the font we are interested in diffing
17    pub location: Vec<VariationSetting>,
18    /// The normalized location of the font
19    pub normalized_location: Location,
20    /// The set of encoded codepoints in the font
21    pub codepoints: HashSet<u32>,
22}
23
24impl DFont {
25    /// Create a new DFont from a byte slice
26    pub fn new(string: &[u8]) -> Self {
27        let backing: Vec<u8> = string.to_vec();
28
29        let mut fnt = DFont {
30            backing,
31            codepoints: HashSet::new(),
32            normalized_location: Location::default(),
33            location: vec![],
34        };
35        let cmap = fnt.fontref().charmap();
36        fnt.codepoints = cmap.mappings().map(|(cp, _)| cp).collect();
37        fnt
38    }
39
40    /// Normalize the location
41    ///
42    /// This method must be called after the location is changed.
43    /// (It's that or getters and setters, and nobody wants that.)
44    pub fn normalize_location(&mut self) {
45        self.normalized_location = self.fontref().axes().location(&self.location);
46    }
47
48    /// Set the location of the font given a user-specified location string
49    pub fn set_location(&mut self, variations: &str) -> Result<(), String> {
50        self.location = parse_location(variations)?;
51        self.normalize_location();
52        Ok(())
53    }
54
55    /// The names of the font's named instances
56    pub fn instances(&self) -> Vec<String> {
57        self.fontref()
58            .named_instances()
59            .iter()
60            .flat_map(|ni| {
61                self.fontref()
62                    .localized_strings(ni.subfamily_name_id())
63                    .english_or_first()
64            })
65            .map(|s| s.to_string())
66            .collect()
67    }
68
69    /// Set the location of the font to a given named instance
70    pub fn set_instance(&mut self, instance: &str) -> Result<(), String> {
71        let instance = self
72            .fontref()
73            .named_instances()
74            .iter()
75            .find(|ni| {
76                self.fontref()
77                    .localized_strings(ni.subfamily_name_id())
78                    .any(|s| instance == s.chars().collect::<Cow<str>>())
79            })
80            .ok_or_else(|| format!("No instance named {}", instance))?;
81        let user_coords = instance.user_coords();
82        let location = instance.location();
83        self.location = self
84            .fontref()
85            .axes()
86            .iter()
87            .zip(user_coords)
88            .map(|(a, v)| (a.tag(), v).into())
89            .collect();
90        self.normalized_location = location;
91        Ok(())
92    }
93
94    pub fn fontref(&self) -> FontRef<'_> {
95        FontRef::new(&self.backing).expect("Couldn't parse font")
96    }
97    pub fn family_name(&self) -> String {
98        self.fontref()
99            .localized_strings(NameId::FAMILY_NAME)
100            .english_or_first()
101            .map_or_else(|| "Unknown".to_string(), |s| s.chars().collect())
102    }
103
104    pub fn style_name(&self) -> String {
105        self.fontref()
106            .localized_strings(NameId::SUBFAMILY_NAME)
107            .english_or_first()
108            .map_or_else(|| "Regular".to_string(), |s| s.chars().collect())
109    }
110
111    /// The axes of the font
112    ///
113    /// Returns a map from axis tag to (min, default, max) values
114    pub fn axis_info(&self) -> HashMap<String, (f32, f32, f32)> {
115        self.fontref()
116            .axes()
117            .iter()
118            .map(|axis| {
119                (
120                    axis.tag().to_string(),
121                    (axis.min_value(), axis.default_value(), axis.max_value()),
122                )
123            })
124            .collect()
125    }
126
127    /// Returns a list of scripts where the font has at least one encoded
128    /// character from that script.
129    pub fn supported_scripts(&self) -> HashSet<String> {
130        let cmap = self.fontref().charmap();
131        let mut strings = HashSet::new();
132        for (codepoint, _glyphid) in cmap.mappings() {
133            if let Some(script) = char::from_u32(codepoint).and_then(|c| c.script()) {
134                // Would you believe, no Display, no .to_string(), we just have to grub around with Debug.
135                strings.insert(format!("{:?}", script));
136            }
137        }
138        strings
139    }
140
141    /// Returns a list of the master locations in the font
142    ///
143    /// This is derived heuristically from locations of shared tuples in the `gvar` table.
144    /// This should work well enough for most "normal" fonts.
145    pub fn masters(&self) -> Result<Vec<Vec<VariationSetting>>, ReadError> {
146        let gvar = self.fontref().gvar()?;
147        let tuples = gvar.shared_tuples()?.tuples();
148        let peaks: Vec<Vec<VariationSetting>> = tuples
149            .iter()
150            .flatten()
151            .flat_map(|tuple| {
152                let location = tuple
153                    .values()
154                    .iter()
155                    .map(|x| x.get().to_f32())
156                    .collect::<Vec<f32>>();
157                self.fontref().denormalize_location(&location)
158            })
159            .collect();
160        Ok(peaks)
161    }
162}
163
164type InstancePositions = Vec<(String, HashMap<String, f32>)>;
165type AxisDescription = HashMap<String, (f32, f32, f32)>;
166
167/// Compare two fonts and return the axes and instances they have in common
168pub fn shared_axes(f_a: &DFont, f_b: &DFont) -> (AxisDescription, InstancePositions) {
169    let mut axes = f_a.axis_info();
170    let b_axes = f_b.axis_info();
171    let a_axes_names: Vec<String> = axes.keys().cloned().collect();
172    for axis_tag in a_axes_names.iter() {
173        if !b_axes.contains_key(axis_tag) {
174            axes.remove(axis_tag);
175        }
176    }
177    for (axis_tag, values) in b_axes.iter() {
178        let (our_min, _our_default, our_max) = values;
179        axes.entry(axis_tag.clone())
180            .and_modify(|(their_min, _their_default, their_max)| {
181                // This looks upside-down but remember we are
182                // narrowing the axis ranges to the union of the
183                // two fonts.
184                *their_min = their_min.max(*our_min);
185                *their_max = their_max.min(*our_max);
186            });
187    }
188    let axis_names: Vec<String> = f_a
189        .fontref()
190        .axes()
191        .iter()
192        .map(|axis| axis.tag().to_string())
193        .collect();
194    let instances = f_a
195        .fontref()
196        .named_instances()
197        .iter()
198        .map(|ni| {
199            let name = f_a
200                .fontref()
201                .localized_strings(ni.subfamily_name_id())
202                .english_or_first()
203                .map_or_else(|| "Unknown".to_string(), |s| s.chars().collect());
204            let location_map = axis_names.iter().cloned().zip(ni.user_coords()).collect();
205            (name, location_map)
206        })
207        .collect::<Vec<(String, HashMap<String, f32>)>>();
208    (axes, instances)
209}