glyphs_reader/
propagate_anchors.rs

1//! Propagating anchors from components to their composites
2//!
3//! Glyphs.app has a nice feature where anchors defined in the components
4//! of composite glyphs are copied into the composites themselves. This feature
5//! is not very extensively documented, and the code here is based off the
6//! Objective-C implementation, which was shared with us privately.
7
8use std::collections::{BTreeMap, HashMap};
9
10use indexmap::IndexMap;
11use kurbo::{Affine, Vec2};
12use smol_str::{format_smolstr, SmolStr};
13
14use crate::{
15    font::Anchor,
16    glyphdata::{Category, Subcategory},
17    Component, Font, Glyph, Layer, Shape,
18};
19
20impl Font {
21    /// Copy anchors from component glyphs into their including composites
22    pub fn propagate_all_anchors(&mut self) {
23        propagate_all_anchors_impl(&mut self.glyphs);
24    }
25}
26
27// the actual implementation: it's easier to test a free fn
28fn propagate_all_anchors_impl(glyphs: &mut BTreeMap<SmolStr, Glyph>) {
29    // the reference implementation does this recursively, but we opt to
30    // implement it by pre-sorting the work to ensure we always process components
31    // first.
32    let todo = depth_sorted_composite_glyphs(glyphs);
33    let mut num_base_glyphs = HashMap::new();
34    // NOTE: there's an important detail here, which is that we need to call the
35    // 'anchors_traversing_components' function on each glyph, and save the returned
36    // anchors, but we only *set* those anchors on glyphs that have components.
37    // to make this work, we write the anchors to a separate data structure, and
38    // then only update the actual glyphs after we've done all the work.
39    let mut all_anchors = HashMap::new();
40    for name in todo {
41        let glyph = glyphs.get(&name).unwrap();
42        for layer in &glyph.layers {
43            let anchors = anchors_traversing_components(
44                glyph,
45                layer,
46                glyphs,
47                &all_anchors,
48                &mut num_base_glyphs,
49            );
50            maybe_log_new_anchors(&anchors, glyph, layer);
51            all_anchors.entry(name.clone()).or_default().push(anchors);
52        }
53    }
54
55    // finally update our glyphs with the new anchors, where appropriate
56    for (name, layers) in all_anchors {
57        let glyph = glyphs.get_mut(&name).unwrap();
58        if glyph.has_components() {
59            assert_eq!(layers.len(), glyph.layers.len());
60            for (i, layer_anchors) in layers.into_iter().enumerate() {
61                glyph.layers[i].anchors = layer_anchors;
62            }
63        }
64    }
65}
66
67fn maybe_log_new_anchors(anchors: &[Anchor], glyph: &Glyph, layer: &Layer) {
68    if !glyph.has_components() || !log::log_enabled!(log::Level::Trace) || anchors == layer.anchors
69    {
70        return;
71    }
72    let prev_names: Vec<_> = layer.anchors.iter().map(|a| &a.name).collect();
73    let new_names: Vec<_> = anchors.iter().map(|a| &a.name).collect();
74    log::trace!(
75        "propagated anchors for ('{}': {prev_names:?} -> {new_names:?}",
76        glyph.name,
77    );
78}
79
80/// Return the anchors for this glyph, including anchors from components
81///
82/// This function is a reimplmentation of a similarly named function in glyphs.app.
83///
84/// The logic for copying anchors from components into their containing composites
85/// is tricky. Anchors need to be adjusted in various ways:
86///
87/// - a speical "*origin" anchor may exist, which modifies the position of other anchors
88/// - if a component is flipped on the x or y axes, we rename "top" to "bottom"
89///   and/or "left" to "right"
90/// - we need to apply the transform from the component
91/// - we may need to rename an anchor when the component is part of a ligature glyph
92fn anchors_traversing_components<'a>(
93    glyph: &'a Glyph,
94    layer: &'a Layer,
95    glyphs: &BTreeMap<SmolStr, Glyph>,
96    // map of glyph -> anchors, per-layer, updated as we do each glyph;
97    // since we sort by component depth before doing work, we know that any components
98    // of the current glyph have been done first.
99    done_anchors: &HashMap<SmolStr, Vec<Vec<Anchor>>>,
100    // each (glyph, layer) writes its number of base glyphs into this map during traversal
101    base_glyph_counts: &mut HashMap<(SmolStr, &'a str), usize>,
102) -> Vec<Anchor> {
103    if layer.anchors.is_empty() && layer.components().count() == 0 {
104        return Vec::new();
105    }
106
107    // if this is a mark and it has anchors, just return them
108    // (as in, don't even look at the components)
109    if !layer.anchors.is_empty() && glyph.category == Some(Category::Mark) {
110        return origin_adjusted_anchors(&layer.anchors).collect();
111    }
112
113    let is_ligature = glyph.sub_category == Some(Subcategory::Ligature);
114    let mut has_underscore = layer
115        .anchors
116        .iter()
117        .any(|anchor| anchor.name.starts_with('_'));
118
119    let mut number_of_base_glyphs = 0usize;
120    // we use an index map so we get the same ordering behaviour as python
121    let mut all_anchors = IndexMap::new();
122    for (component_idx, component) in layer.components().enumerate() {
123        // because we process dependencies first we know that all components
124        // referenced have already been propagated
125        let Some(mut anchors) =
126            // equivalent to the recursive call in the reference impl
127            get_component_layer_anchors(component, layer, glyphs, done_anchors)
128        else {
129            log::warn!(
130                "could not get layer '{}' for component '{}' of glyph '{}'",
131                layer.layer_id,
132                component.name,
133                glyph.name
134            );
135            continue;
136        };
137
138        // if this component has an explicitly set attachment anchor, use it
139        if let Some(comp_anchor) = component.anchor.as_ref().filter(|_| component_idx > 0) {
140            maybe_rename_component_anchor(comp_anchor.to_owned(), &mut anchors);
141        }
142
143        let component_number_of_base_glyphs = base_glyph_counts
144            .get(&(component.name.clone(), layer.layer_id.as_str()))
145            .copied()
146            .unwrap_or(0);
147
148        let comb_has_underscore = anchors
149            .iter()
150            .any(|a| a.name.len() >= 2 && a.name.starts_with('_'));
151        let comb_has_exit = anchors.iter().any(|a| a.name.ends_with("exit"));
152        if !(comb_has_underscore | comb_has_exit) {
153            // delete exit anchors we may have taken from earlier components
154            // (since a glyph should only have one exit anchor, and logically its at the end)
155            all_anchors.retain(|name: &SmolStr, _| !name.ends_with("exit"));
156        }
157
158        let scale = get_xy_rotation(component.transform);
159        for mut anchor in anchors {
160            let new_has_underscore = anchor.name.starts_with('_');
161            if (component_idx > 0 || has_underscore) && new_has_underscore {
162                continue;
163            }
164            // skip entry anchors on non-first glyphs
165            if component_idx > 0 && anchor.name.ends_with("entry") {
166                continue;
167            }
168
169            let mut new_anchor_name = rename_anchor_for_scale(&anchor.name, scale);
170            if is_ligature
171                && component_number_of_base_glyphs > 0
172                && !new_has_underscore
173                && !(new_anchor_name.ends_with("exit") || new_anchor_name.ends_with("entry"))
174            {
175                // dealing with marks like top_1 on a ligature
176                new_anchor_name = make_liga_anchor_name(new_anchor_name, number_of_base_glyphs);
177            }
178
179            apply_transform_to_anchor(&mut anchor, component.transform);
180            anchor.name = new_anchor_name;
181            all_anchors.insert(anchor.name.clone(), anchor);
182            has_underscore |= new_has_underscore;
183        }
184        number_of_base_glyphs += base_glyph_counts
185            .get(&(component.name.clone(), layer.layer_id.as_str()))
186            .copied()
187            .unwrap_or(0);
188    }
189
190    // now we've handled all the anchors from components, so copy over anchors
191    // that were explicitly defined on this layer:
192    all_anchors.extend(origin_adjusted_anchors(&layer.anchors).map(|a| (a.name.clone(), a)));
193    let mut has_underscore_anchor = false;
194    let mut has_mark_anchor = false;
195    let mut component_count_from_anchors = 0;
196
197    // now we count how many components we have, based on our anchors
198    for name in all_anchors.keys() {
199        has_underscore_anchor |= name.starts_with('_');
200        has_mark_anchor |= name.chars().next().unwrap_or('\0').is_ascii_alphabetic();
201        if !is_ligature
202            && number_of_base_glyphs == 0
203            && !name.starts_with('_')
204            && !(name.ends_with("entry") | name.ends_with("exit"))
205            && name.contains('_')
206        {
207            let (_, suffix) = name.split_once('_').unwrap();
208            // carets count space between components, so the last caret
209            // is n_components - 1
210            let maybe_add_one = name.starts_with("caret").then_some(1).unwrap_or_default();
211            let anchor_index = suffix.parse::<usize>().unwrap_or(0) + maybe_add_one;
212            component_count_from_anchors = component_count_from_anchors.max(anchor_index);
213        }
214    }
215    if !has_underscore_anchor && number_of_base_glyphs == 0 && has_mark_anchor {
216        number_of_base_glyphs += 1;
217    }
218    number_of_base_glyphs = number_of_base_glyphs.max(component_count_from_anchors);
219
220    if layer.anchors.iter().any(|a| a.name == "_bottom") {
221        all_anchors.shift_remove("top");
222        all_anchors.shift_remove("_top");
223    }
224    if layer.anchors.iter().any(|a| a.name == "_top") {
225        all_anchors.shift_remove("bottom");
226        all_anchors.shift_remove("_bottom");
227    }
228    base_glyph_counts.insert(
229        (glyph.name.clone(), layer.layer_id.as_str()),
230        number_of_base_glyphs,
231    );
232    all_anchors.into_values().collect()
233}
234
235/// returns an iterator over anchors in the layer, accounting for a possible "*origin" anchor
236///
237/// If that anchor is present it will be used to adjust the positions of other
238/// anchors, and will not be included in the output.
239fn origin_adjusted_anchors(anchors: &[Anchor]) -> impl Iterator<Item = Anchor> + '_ {
240    let origin = anchors
241        .iter()
242        .find_map(Anchor::origin_delta)
243        .unwrap_or_default();
244    anchors
245        .iter()
246        .filter(|a| !a.is_origin())
247        .cloned()
248        .map(move |mut a| {
249            a.pos -= origin;
250            a
251        })
252}
253
254// returns a vec2 where for each axis a negative value indicates that axis is considerd flipped
255fn get_xy_rotation(xform: Affine) -> Vec2 {
256    // this is based on examining the behaviour of glyphs via the macro panel
257    // and careful testing.
258    let [xx, xy, ..] = xform.as_coeffs();
259    // first take the rotation
260    let angle = xy.atan2(xx);
261    // then remove the rotation, and take the scale
262    let rotated = xform.pre_rotate(-angle).as_coeffs();
263    let mut scale = Vec2::new(rotated[0], rotated[3]);
264    // then invert the scale if the rotation was >= 180°
265    if (angle.to_degrees() - 180.0).abs() < 0.001 {
266        scale *= -1.0;
267    }
268
269    scale
270}
271
272// apply the transform but also do some rounding, so we don't have anchors
273// with points like (512, 302.000000006)
274fn apply_transform_to_anchor(anchor: &mut Anchor, transform: Affine) {
275    // how many zeros do we care about? not this many
276    const ROUND_TO: f64 = 1e6;
277    let mut pos = (transform * anchor.pos).to_vec2();
278    pos *= ROUND_TO;
279    pos = pos.round();
280    pos /= ROUND_TO;
281    anchor.pos = pos.to_point();
282}
283
284fn maybe_rename_component_anchor(comp_name: SmolStr, anchors: &mut [Anchor]) {
285    // e.g, go from 'top' to 'top_1'
286    let Some((sub_name, _)) = comp_name.as_str().split_once('_') else {
287        return;
288    };
289    let mark_name = format_smolstr!("_{sub_name}");
290    if anchors.iter().any(|a| a.name == sub_name) && anchors.iter().any(|a| a.name == mark_name) {
291        anchors
292            .iter_mut()
293            .find(|a| a.name == sub_name)
294            .unwrap()
295            .name = comp_name.clone();
296    }
297}
298
299fn make_liga_anchor_name(name: SmolStr, base_number: usize) -> SmolStr {
300    match name.split_once('_') {
301        // if this anchor already has a number (like 'top_2') we want to consider that
302        Some((name, suffix)) => {
303            let suffix = base_number + suffix.parse::<usize>().ok().unwrap_or(1);
304            format_smolstr!("{name}_{suffix}")
305        }
306        // otherwise we're turning 'top' into 'top_N'
307        None => format_smolstr!("{name}_{}", base_number + 1),
308    }
309}
310
311// if a component is rotated, flip bottom/top, left/right, entry/exit
312fn rename_anchor_for_scale(name: &SmolStr, scale: Vec2) -> SmolStr {
313    // swap the two words in the target, if they're present
314    fn swap_pair(s: &mut String, one: &str, two: &str) {
315        fn replace(s: &mut String, target: &str, by: &str) -> bool {
316            if let Some(idx) = s.find(target) {
317                s.replace_range(idx..idx + target.len(), by);
318                return true;
319            }
320            false
321        }
322        // once we swap 'left' for 'right' we don't want to then check for 'right'!
323        if !replace(s, one, two) {
324            replace(s, two, one);
325        }
326    }
327
328    if scale.x >= 0. && scale.y >= 0. {
329        return name.to_owned();
330    }
331
332    let mut name = name.to_string();
333    if scale.y < 0. {
334        swap_pair(&mut name, "bottom", "top");
335    }
336    if scale.x < 0. {
337        swap_pair(&mut name, "left", "right");
338        swap_pair(&mut name, "exit", "entry");
339    }
340
341    SmolStr::from(name)
342}
343
344// in glyphs.app this function will synthesize a layer if it is missing.
345fn get_component_layer_anchors(
346    component: &Component,
347    layer: &Layer,
348    glyphs: &BTreeMap<SmolStr, Glyph>,
349    anchors: &HashMap<SmolStr, Vec<Vec<Anchor>>>,
350) -> Option<Vec<Anchor>> {
351    let glyph = glyphs.get(&component.name)?;
352    glyph
353        .layers
354        .iter()
355        .position(|comp_layer| comp_layer.layer_id == layer.layer_id)
356        .and_then(|layer_idx| {
357            anchors
358                .get(&component.name)
359                .and_then(|layers| layers.get(layer_idx))
360        })
361        .cloned()
362}
363
364/// returns a list of all glyphs, sorted by component depth.
365///
366/// That is: a glyph in the list will always occur before any other glyph that
367/// references it as a component.
368fn depth_sorted_composite_glyphs(glyphs: &BTreeMap<SmolStr, Glyph>) -> Vec<SmolStr> {
369    // map of the maximum component depth of a glyph.
370    // - a glyph with no components has depth 0,
371    // - a glyph with a component has depth 1,
372    // - a glyph with a component that itself has a component has depth 2, etc
373
374    // For context, in a typical font most glyphs are not composites and composites are not terribly deep
375    // indeterminate_depth is initially all components then empties as we find depths for them
376    let mut indeterminate_depth = Vec::new();
377    let mut depths: HashMap<_, _> = glyphs
378        .iter()
379        .filter_map(|(name, glyph)| {
380            if glyph.has_components() {
381                indeterminate_depth.push(glyph); // maybe some of our components are components
382                None
383            } else {
384                Some((name, 0))
385            }
386        })
387        .collect();
388
389    // Progress is the number of glyphs we processed in a cycle, initially the number of simple glyphs.
390    // If we fail to make progress all that's left is glyphs with cycles or bad references
391    let mut progress = glyphs.len() - indeterminate_depth.len();
392    while progress > 0 {
393        progress = indeterminate_depth.len();
394
395        // We know the depth once every component we rely on has a depth
396        indeterminate_depth.retain(|glyph| {
397            let max_component_depth = glyph
398                .layers
399                .iter()
400                .flat_map(|layer| layer.shapes.iter())
401                .filter_map(|shape| match shape {
402                    Shape::Path(..) => None,
403                    Shape::Component(c) => Some(&c.name),
404                })
405                .map(|name| depths.get(name).copied())
406                .try_fold(0, |acc, e| e.map(|e| acc.max(e)));
407            if let Some(max_component_depth) = max_component_depth {
408                depths.insert(&glyph.name, max_component_depth + 1);
409            }
410            max_component_depth.is_none() // retain if we don't yet have an answer
411        });
412
413        progress -= indeterminate_depth.len();
414    }
415
416    // We may have failed some of you
417    if !indeterminate_depth.is_empty() {
418        // Shouldn't we return an error instead of just dropping results?
419        for g in indeterminate_depth.iter() {
420            depths.remove(&g.name);
421        }
422
423        if log::log_enabled!(log::Level::Warn) {
424            let mut names = indeterminate_depth
425                .into_iter()
426                .map(|g| g.name.as_str())
427                .collect::<Vec<_>>();
428            names.sort();
429            log::warn!(
430                "Invalid component graph (cycles or bad refs) for {} glyphs: {:?}",
431                names.len(),
432                names
433            );
434        }
435    }
436
437    let mut by_depth = depths
438        .into_iter()
439        .map(|(glyph, depth)| (depth, glyph))
440        .collect::<Vec<_>>();
441
442    by_depth.sort();
443    by_depth.into_iter().map(|(_, name)| name.clone()).collect()
444}
445
446#[cfg(test)]
447mod tests {
448
449    use std::collections::BTreeSet;
450
451    use kurbo::Point;
452
453    use crate::{glyphdata::GlyphData, Layer, Shape};
454
455    use super::*;
456
457    #[derive(Debug, Default)]
458    struct GlyphSetBuilder(BTreeMap<SmolStr, Glyph>);
459
460    impl GlyphSetBuilder {
461        fn new() -> Self {
462            Default::default()
463        }
464
465        fn build(&self) -> BTreeMap<SmolStr, Glyph> {
466            self.0.clone()
467        }
468
469        fn add_glyph(&mut self, name: &str, build_fn: impl FnOnce(&mut GlyphBuilder)) -> &mut Self {
470            let mut glyph = GlyphBuilder::new(name);
471            build_fn(&mut glyph);
472            self.0.insert(glyph.0.name.clone(), glyph.build());
473            self
474        }
475    }
476    // a little helper to make it easier to generate data for these tests
477    #[derive(Debug, Default)]
478    struct GlyphBuilder(Glyph);
479
480    impl GlyphBuilder {
481        fn new(name: &str) -> Self {
482            let mut this = GlyphBuilder(Glyph {
483                name: name.into(),
484                export: true,
485                ..Default::default()
486            });
487            if let Some(result) = GlyphData::default().query(name, None) {
488                this.set_category(result.category);
489                if let Some(subcategory) = result.subcategory {
490                    this.set_subcategory(subcategory);
491                }
492                if let Some(unicode) = result.codepoint {
493                    this.set_unicode(unicode);
494                }
495            }
496            this.add_layer();
497            this
498        }
499
500        fn build(&self) -> Glyph {
501            self.0.clone()
502        }
503
504        /// Add a new layer to a glyph; all other operations work on the last added layer
505        fn add_layer(&mut self) -> &mut Self {
506            self.0.layers.push(Layer {
507                layer_id: format!("layer-{}", self.0.layers.len()),
508                ..Default::default()
509            });
510            self
511        }
512
513        fn last_layer_mut(&mut self) -> &mut Layer {
514            self.0.layers.last_mut().unwrap()
515        }
516
517        fn set_unicode(&mut self, unicode: u32) -> &mut Self {
518            self.0.unicode = BTreeSet::from([unicode]);
519            self
520        }
521
522        fn set_category(&mut self, category: Category) -> &mut Self {
523            self.0.category = Some(category);
524            self
525        }
526
527        fn set_subcategory(&mut self, sub_category: Subcategory) -> &mut Self {
528            self.0.sub_category = Some(sub_category);
529            self
530        }
531
532        // use an int for pos to simplify the call site ('0' instead of'0.0')
533        fn add_component(&mut self, name: &str, pos: (i32, i32)) -> &mut Self {
534            self.last_layer_mut()
535                .shapes
536                .push(Shape::Component(Component {
537                    name: name.into(),
538                    transform: Affine::translate((pos.0 as f64, pos.1 as f64)),
539                    ..Default::default()
540                }));
541            self
542        }
543
544        /// Set an explicit translate + rotation for the component
545        fn rotate_component(&mut self, degrees: f64) -> &mut Self {
546            if let Some(Shape::Component(comp)) = self.last_layer_mut().shapes.last_mut() {
547                comp.transform = comp.transform.pre_rotate(degrees.to_radians());
548            }
549            self
550        }
551
552        /// add an explicit anchor to the last added component
553        fn add_component_anchor(&mut self, name: &str) -> &mut Self {
554            if let Some(Shape::Component(comp)) = self.last_layer_mut().shapes.last_mut() {
555                comp.anchor = Some(name.into());
556            }
557            self
558        }
559        fn add_anchor(&mut self, name: &str, pos: (i32, i32)) -> &mut Self {
560            self.last_layer_mut().anchors.push(Anchor {
561                name: name.into(),
562                pos: Point::new(pos.0 as _, pos.1 as _),
563            });
564            self
565        }
566    }
567
568    impl PartialEq<(&str, (f64, f64))> for Anchor {
569        fn eq(&self, other: &(&str, (f64, f64))) -> bool {
570            self.name == other.0 && self.pos == other.1.into()
571        }
572    }
573
574    #[test]
575    fn components_by_depth() {
576        fn make_glyph(name: &str, components: &[&str]) -> Glyph {
577            let mut builder = GlyphBuilder::new(name);
578            for comp in components {
579                builder.add_component(comp, (0, 0)); // pos doesn't matter for this test
580            }
581            builder.build()
582        }
583
584        let glyphs: &[(&str, &[&str])] = &[
585            ("A", &[]),
586            ("E", &[]),
587            ("acutecomb", &[]),
588            ("brevecomb", &[]),
589            ("brevecomb_acutecomb", &["acutecomb", "brevecomb"]),
590            ("AE", &["A", "E"]),
591            ("Aacute", &["A", "acutecomb"]),
592            ("Aacutebreve", &["A", "brevecomb_acutecomb"]),
593            ("AEacutebreve", &["AE", "brevecomb_acutecomb"]),
594        ];
595        let glyphs = glyphs
596            .iter()
597            .map(|(name, components)| make_glyph(name, components))
598            .map(|glyph| (glyph.name.clone(), glyph))
599            .collect();
600
601        let result = depth_sorted_composite_glyphs(&glyphs);
602        let expected = [
603            "A",
604            "E",
605            "acutecomb",
606            "brevecomb",
607            "AE",
608            "Aacute",
609            "brevecomb_acutecomb",
610            "AEacutebreve",
611            "Aacutebreve",
612        ]
613        .into_iter()
614        .map(SmolStr::new)
615        .collect::<Vec<_>>();
616
617        assert_eq!(result, expected)
618    }
619
620    #[test]
621    fn no_components_anchors_are_unchanged() {
622        // derived from the observed behaviour of glyphs 3.2.2 (3259)
623        let glyphs = GlyphSetBuilder::new()
624            .add_glyph("A", |glyph| {
625                glyph
626                    .add_anchor("bottom", (234, 0))
627                    .add_anchor("ogonek", (411, 0))
628                    .add_anchor("top", (234, 810));
629            })
630            .add_glyph("acutecomb", |glyph| {
631                glyph
632                    .add_anchor("_top", (0, 578))
633                    .add_anchor("top", (0, 810));
634            })
635            .build();
636
637        let mut glyphs_2 = glyphs.clone();
638        propagate_all_anchors_impl(&mut glyphs_2);
639        assert_eq!(glyphs, glyphs_2, "nothing should change here");
640    }
641
642    #[test]
643    fn basic_composite_anchor() {
644        // derived from the observed behaviour of glyphs 3.2.2 (3259)
645        let mut glyphs = GlyphSetBuilder::new()
646            .add_glyph("A", |glyph| {
647                glyph
648                    .add_anchor("bottom", (234, 0))
649                    .add_anchor("ogonek", (411, 0))
650                    .add_anchor("top", (234, 810));
651            })
652            .add_glyph("acutecomb", |glyph| {
653                glyph
654                    .add_anchor("_top", (0, 578))
655                    .add_anchor("top", (0, 810));
656            })
657            .add_glyph("Aacute", |glyph| {
658                glyph
659                    .add_component("A", (0, 0))
660                    .add_component("acutecomb", (234, 232));
661            })
662            .build();
663        propagate_all_anchors_impl(&mut glyphs);
664
665        let new_glyph = glyphs.get("Aacute").unwrap();
666        assert_eq!(
667            new_glyph.layers[0].anchors,
668            [
669                ("bottom", (234., 0.)),
670                ("ogonek", (411., 0.)),
671                ("top", (234., 1042.))
672            ]
673        );
674    }
675
676    #[test]
677    fn propagate_ligature_anchors() {
678        // derived from the observed behaviour of glyphs 3.2.2 (3259)
679        // this is based on the IJ glyph in Oswald (ExtraLight)
680        let mut glyphs = GlyphSetBuilder::new()
681            .add_glyph("I", |glyph| {
682                glyph
683                    .add_anchor("bottom", (103, 0))
684                    .add_anchor("ogonek", (103, 0))
685                    .add_anchor("top", (103, 810))
686                    .add_anchor("topleft", (20, 810));
687            })
688            .add_glyph("J", |glyph| {
689                glyph
690                    .add_anchor("bottom", (133, 0))
691                    .add_anchor("top", (163, 810));
692            })
693            .add_glyph("IJ", |glyph| {
694                glyph
695                    // we need to manually override this, it isn't actually a
696                    // ligature by default
697                    .set_subcategory(Subcategory::Ligature)
698                    .add_component("I", (0, 0))
699                    .add_component("J", (206, 0));
700            })
701            .build();
702        propagate_all_anchors_impl(&mut glyphs);
703        let ij = glyphs.get("IJ").unwrap();
704        // these were derived by running the built in glyphs.app propagate anchors
705        // method from the macro panel
706        assert_eq!(
707            ij.layers[0].anchors,
708            [
709                ("bottom_1", (103., 0.)),
710                ("ogonek_1", (103., 0.)),
711                ("top_1", (103., 810.)),
712                ("topleft_1", (20., 810.)),
713                ("bottom_2", (339., 0.)),
714                ("top_2", (369., 810.))
715            ]
716        )
717    }
718
719    #[test]
720    fn digraphs_arent_ligatures() {
721        // derived from the observed behaviour of glyphs 3.2.2 (3259)
722        // this is based on the IJ glyph in Oswald (ExtraLight)
723        let mut glyphs = GlyphSetBuilder::new()
724            .add_glyph("I", |glyph| {
725                glyph
726                    .add_anchor("bottom", (103, 0))
727                    .add_anchor("ogonek", (103, 0))
728                    .add_anchor("top", (103, 810))
729                    .add_anchor("topleft", (20, 810));
730            })
731            .add_glyph("J", |glyph| {
732                glyph
733                    .add_anchor("bottom", (133, 0))
734                    .add_anchor("top", (163, 810));
735            })
736            .add_glyph("IJ", |glyph| {
737                glyph
738                    .add_component("I", (0, 0))
739                    .add_component("J", (206, 0));
740            })
741            .build();
742        propagate_all_anchors_impl(&mut glyphs);
743        let ij = glyphs.get("IJ").unwrap();
744        // these were derived by running the built in glyphs.app propagate anchors
745        // method from the macro panel
746        assert_eq!(
747            ij.layers[0].anchors,
748            [
749                ("bottom", (339., 0.)),
750                ("ogonek", (103., 0.)),
751                ("top", (369., 810.)),
752                ("topleft", (20., 810.)),
753            ]
754        )
755    }
756
757    #[test]
758    fn propagate_across_layers() {
759        // derived from the observed behaviour of glyphs 3.2.2 (3259)
760        let mut glyphs = GlyphSetBuilder::new()
761            .add_glyph("A", |glyph| {
762                glyph
763                    .add_anchor("bottom", (290, 10))
764                    .add_anchor("ogonek", (490, 3))
765                    .add_anchor("top", (290, 690))
766                    .add_layer()
767                    .add_anchor("bottom", (300, 0))
768                    .add_anchor("ogonek", (540, 10))
769                    .add_anchor("top", (300, 700));
770            })
771            .add_glyph("acutecomb", |glyph| {
772                glyph
773                    .add_anchor("_top", (335, 502))
774                    .add_anchor("top", (353, 721))
775                    .add_layer()
776                    .add_anchor("_top", (366, 500))
777                    .add_anchor("top", (366, 765));
778            })
779            .add_glyph("Aacute", |glyph| {
780                glyph
781                    .add_component("A", (0, 0))
782                    .add_component("acutecomb", (-45, 188))
783                    .add_layer()
784                    .add_component("A", (0, 0))
785                    .add_component("acutecomb", (-66, 200));
786            })
787            .build();
788        propagate_all_anchors_impl(&mut glyphs);
789
790        let new_glyph = glyphs.get("Aacute").unwrap();
791        assert_eq!(
792            new_glyph.layers[0].anchors,
793            [
794                ("bottom", (290., 10.)),
795                ("ogonek", (490., 3.)),
796                ("top", (308., 909.))
797            ]
798        );
799
800        assert_eq!(
801            new_glyph.layers[1].anchors,
802            [
803                ("bottom", (300., 0.)),
804                ("ogonek", (540., 10.)),
805                ("top", (300., 965.))
806            ]
807        );
808    }
809
810    #[test]
811    fn remove_exit_anchor_on_component() {
812        // derived from the observed behaviour of glyphs 3.2.2 (3259)
813        let mut glyphs = GlyphSetBuilder::new()
814            .add_glyph("comma", |_| {})
815            .add_glyph("ain-ar.init", |glyph| {
816                glyph
817                    .add_anchor("top", (294, 514))
818                    .add_anchor("exit", (0, 0));
819            })
820            .add_glyph("ain-ar.init.alt", |glyph| {
821                glyph
822                    .add_component("ain-ar.init", (0, 0))
823                    .add_component("comma", (0, 0));
824            })
825            .build();
826        propagate_all_anchors_impl(&mut glyphs);
827
828        let new_glyph = glyphs.get("ain-ar.init.alt").unwrap();
829        assert_eq!(new_glyph.layers[0].anchors, [("top", (294., 514.)),]);
830    }
831
832    #[test]
833    fn component_anchor() {
834        // derived from the observed behaviour of glyphs 3.2.2 (3259)
835        let mut glyphs = GlyphSetBuilder::new()
836            .add_glyph("acutecomb", |glyph| {
837                glyph
838                    .add_anchor("_top", (150, 580))
839                    .add_anchor("top", (170, 792));
840            })
841            .add_glyph("aa", |glyph| {
842                glyph
843                    .add_anchor("bottom_1", (218, 8))
844                    .add_anchor("bottom_2", (742, 7))
845                    .add_anchor("ogonek_1", (398, 9))
846                    .add_anchor("ogonek_2", (902, 9))
847                    .add_anchor("top_1", (227, 548))
848                    .add_anchor("top_2", (746, 548));
849            })
850            .add_glyph("a_a", |glyph| {
851                glyph.add_component("aa", (0, 0));
852            })
853            .add_glyph("a_aacute", |glyph| {
854                glyph
855                    .add_component("a_a", (0, 0))
856                    .add_component("acutecomb", (596, -32))
857                    .add_component_anchor("top_2");
858            })
859            .build();
860        propagate_all_anchors_impl(&mut glyphs);
861
862        let new_glyph = glyphs.get("a_aacute").unwrap();
863        assert_eq!(
864            new_glyph.layers[0].anchors,
865            [
866                ("bottom_1", (218., 8.)),
867                ("bottom_2", (742., 7.)),
868                ("ogonek_1", (398., 9.)),
869                ("ogonek_2", (902., 9.)),
870                ("top_1", (227., 548.)),
871                ("top_2", (766., 760.)),
872            ]
873        );
874    }
875
876    #[test]
877    fn origin_anchor() {
878        // derived from the observed behaviour of glyphs 3.2.2 (3259)
879        let mut glyphs = GlyphSetBuilder::new()
880            .add_glyph("a", |glyph| {
881                glyph
882                    .add_anchor("*origin", (-20, 0))
883                    .add_anchor("bottom", (242, 7))
884                    .add_anchor("ogonek", (402, 9))
885                    .add_anchor("top", (246, 548));
886            })
887            .add_glyph("acutecomb", |glyph| {
888                glyph
889                    .add_anchor("_top", (150, 580))
890                    .add_anchor("top", (170, 792));
891            })
892            .add_glyph("aacute", |glyph| {
893                glyph
894                    .add_component("a", (0, 0))
895                    .add_component("acutecomb", (116, -32));
896            })
897            .build();
898        propagate_all_anchors_impl(&mut glyphs);
899
900        let new_glyph = glyphs.get("aacute").unwrap();
901        assert_eq!(
902            new_glyph.layers[0].anchors,
903            [
904                ("bottom", (262.0, 7.0)),
905                ("ogonek", (422.0, 9.0)),
906                ("top", (286.0, 760.0)),
907            ]
908        );
909    }
910
911    #[test]
912    fn invert_names_on_rotation() {
913        // derived from the observed behaviour of glyphs 3.2.2 (3259)
914        let mut glyphs = GlyphSetBuilder::new()
915            .add_glyph("comma", |_| {})
916            .add_glyph("commaaccentcomb", |glyph| {
917                glyph
918                    .add_anchor("_bottom", (289, 0))
919                    .add_anchor("mybottom", (277, -308))
920                    .add_component("comma", (9, -164));
921            })
922            .add_glyph("commaturnedabovecomb", |glyph| {
923                glyph
924                    .add_component("commaaccentcomb", (589, 502))
925                    .rotate_component(180.);
926            })
927            .build();
928        propagate_all_anchors_impl(&mut glyphs);
929
930        let new_glyph = glyphs.get("commaturnedabovecomb").unwrap();
931        assert_eq!(
932            new_glyph.layers[0].anchors,
933            [("_top", (300., 502.)), ("mytop", (312., 810.)),]
934        );
935    }
936
937    #[test]
938    fn affine_scale() {
939        let affine = Affine::rotate((180.0f64).to_radians()).then_translate((589., 502.).into());
940        let delta = get_xy_rotation(affine);
941        assert!(delta.x.is_sign_negative() && delta.y.is_sign_negative());
942
943        let affine = Affine::translate((10., 10.));
944        let delta = get_xy_rotation(affine);
945        assert!(delta.x.is_sign_positive() && delta.y.is_sign_positive());
946        let flip_y = get_xy_rotation(Affine::FLIP_Y);
947        assert!(flip_y.y.is_sign_negative());
948        assert!(flip_y.x.is_sign_positive());
949        let flip_x = get_xy_rotation(Affine::FLIP_X);
950        assert!(flip_x.y.is_sign_positive());
951        assert!(flip_x.x.is_sign_negative());
952
953        let rotate_flip = Affine::rotate((180.0f64).to_radians())
954            .then_translate((589., 502.).into())
955            * Affine::FLIP_X;
956        let rotate_flip = get_xy_rotation(rotate_flip);
957        assert!(rotate_flip.x.is_sign_positive());
958        assert!(rotate_flip.y.is_sign_negative());
959    }
960
961    // the tricky parts of these files have been factored out into separate tests,
962    // but we'll keep them in case there are other regressions lurking
963    #[test]
964    fn real_files() {
965        let expected =
966            Font::load_raw("../resources/testdata/glyphs3/PropagateAnchorsTest-propagated.glyphs")
967                .unwrap();
968        let font = Font::load(std::path::Path::new(
969            "../resources/testdata/glyphs3/PropagateAnchorsTest.glyphs",
970        ))
971        .unwrap();
972
973        assert_eq!(expected.glyphs.len(), font.glyphs.len());
974        assert!(expected
975            .glyphs
976            .keys()
977            .zip(font.glyphs.keys())
978            .all(|(a, b)| a == b));
979
980        for (g1, g2) in expected.glyphs.values().zip(font.glyphs.values()) {
981            assert_eq!(g1.layers.len(), g2.layers.len());
982            for (l1, l2) in g1.layers.iter().zip(g2.layers.iter()) {
983                let a1 = l1.anchors.clone();
984                let a2 = l2.anchors.clone();
985                assert_eq!(a1, a2, "{}", g1.name);
986            }
987        }
988    }
989
990    #[test]
991    fn composite_cycle() {
992        let _ = env_logger::builder().is_test(true).try_init();
993        let glyphs = GlyphSetBuilder::new()
994            .add_glyph("A", |glyph| {
995                glyph.add_component("B", (0, 0));
996            })
997            .add_glyph("B", |glyph| {
998                glyph.add_component("A", (0, 0));
999            })
1000            .build();
1001        // all we actually care about is that this doesn't run forever
1002        let sorted = depth_sorted_composite_glyphs(&glyphs);
1003        // cycles should be dropped
1004        assert!(sorted.is_empty())
1005    }
1006
1007    #[test]
1008    fn composite_not_a_cycle() {
1009        let _ = env_logger::builder().is_test(true).try_init();
1010        let glyphs = GlyphSetBuilder::new()
1011            .add_glyph("A", |_glyph| {})
1012            .add_glyph("B", |glyph| {
1013                glyph.add_component("A", (0, 0)).add_component("D", (0, 0));
1014            })
1015            .add_glyph("C", |_glyph| {})
1016            .add_glyph("D", |glyph| {
1017                glyph.add_component("E", (0, 0));
1018            })
1019            .add_glyph("E", |glyph| {
1020                glyph.add_component("C", (0, -180));
1021            })
1022            .build();
1023        let sorted = depth_sorted_composite_glyphs(&glyphs);
1024        assert_eq!(sorted, ["A", "C", "E", "D", "B"]);
1025    }
1026
1027    #[test]
1028    fn dont_propagate_anchors() {
1029        let font = Font::load(std::path::Path::new(
1030            "../resources/testdata/glyphs2/DontPropagateAnchors.glyphs",
1031        ))
1032        .unwrap();
1033        assert_eq!(font.custom_parameters.propagate_anchors, Some(false));
1034        let glyph = font.glyphs.get("Aacute").unwrap();
1035        assert!(glyph.layers.first().unwrap().anchors.is_empty());
1036    }
1037}