1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
4
5use fea_rs::{
6 compile::{FeatureProvider, LookupId, PendingLookup},
7 GlyphSet,
8};
9use fontdrasil::{
10 orchestration::{Access, AccessBuilder, Work},
11 types::GlyphName,
12};
13
14use ordered_float::OrderedFloat;
15use smol_str::SmolStr;
16use write_fonts::{
17 read::collections::IntSet,
18 tables::{
19 gdef::GlyphClassDef,
20 gpos::builders::{
21 AnchorBuilder, CursivePosBuilder, MarkToBaseBuilder, MarkToLigBuilder,
22 MarkToMarkBuilder,
23 },
24 layout::{builders::CaretValueBuilder, LookupFlag},
25 },
26 types::{GlyphId16, Tag},
27};
28
29use crate::{
30 error::Error,
31 features::properties::ScriptDirection,
32 orchestration::{
33 AnyWorkId, BeWork, Context, FeaFirstPassOutput, FeaRsMarks, MarkLookups, WorkId,
34 },
35};
36use fontir::{
37 ir::{self, Anchor, AnchorKind, GlyphAnchors, GlyphOrder, StaticMetadata},
38 orchestration::WorkId as FeWorkId,
39 variations::DeltaError,
40};
41
42use super::{
43 ot_tags::{INDIC_SCRIPTS, USE_SCRIPTS},
44 properties::UnicodeShortName,
45};
46
47#[derive(Debug)]
48struct MarkWork {}
49
50pub fn create_mark_work() -> Box<BeWork> {
51 Box::new(MarkWork {})
52}
53
54const MARK: Tag = Tag::new(b"mark");
55const MKMK: Tag = Tag::new(b"mkmk");
56const CURS: Tag = Tag::new(b"curs");
57const ABVM: Tag = Tag::new(b"abvm");
58const BLWM: Tag = Tag::new(b"blwm");
59type GroupName = SmolStr;
61
62struct MarkLookupBuilder<'a> {
63 gdef_classes: HashMap<GlyphId16, GlyphClassDef>,
65 anchor_lists: BTreeMap<GlyphId16, Vec<&'a ir::Anchor>>,
67 glyph_order: &'a GlyphOrder,
68 static_metadata: &'a StaticMetadata,
69 fea_first_pass: &'a FeaFirstPassOutput,
70 mark_glyphs: BTreeSet<GlyphId16>,
72 lig_carets: BTreeMap<GlyphId16, Vec<CaretValueBuilder>>,
73 char_map: HashMap<u32, GlyphId16>,
74}
75
76#[derive(Debug, Clone, PartialEq)]
78enum BaseOrLigAnchors<T> {
79 Base(T),
80 Ligature(Vec<Option<T>>),
81}
82
83#[derive(Default, Debug, Clone, PartialEq)]
85struct MarkGroup<'a> {
86 bases: Vec<(GlyphId16, BaseOrLigAnchors<&'a ir::Anchor>)>,
87 marks: Vec<(GlyphId16, &'a ir::Anchor)>,
88 filter_glyphs: bool,
91}
92
93impl MarkGroup<'_> {
94 fn make_filter_glyph_set(&self, filter_glyphs: &IntSet<GlyphId16>) -> Option<GlyphSet> {
96 let all_marks = self
97 .marks
98 .iter()
99 .map(|(gid, _)| *gid)
100 .collect::<HashSet<_>>();
101 self.filter_glyphs.then(|| {
102 self.marks
103 .iter()
104 .filter_map(|(gid, _)| filter_glyphs.contains(*gid).then_some(*gid))
105 .chain(
106 self.bases
107 .iter()
108 .filter_map(|(gid, _)| (!all_marks.contains(gid)).then_some(*gid)),
109 )
110 .collect()
111 })
112 }
113
114 fn only_using_glyphs(&self, include: &IntSet<GlyphId16>) -> Option<MarkGroup<'_>> {
115 let bases = self
116 .bases
117 .iter()
118 .filter(|(gid, _)| include.contains(*gid))
119 .map(|(gid, anchors)| (*gid, anchors.clone()))
120 .collect::<Vec<_>>();
121 if bases.is_empty() || self.marks.is_empty() {
122 None
123 } else {
124 Some(MarkGroup {
125 bases,
126 marks: self.marks.clone(),
127 filter_glyphs: self.filter_glyphs,
128 })
129 }
130 }
131}
132
133trait MarkAttachmentBuilder: Default {
135 fn add_mark(&mut self, gid: GlyphId16, group: &GroupName, anchor: AnchorBuilder);
136 fn add_base(
137 &mut self,
138 gid: GlyphId16,
139 group: &GroupName,
140 anchor: BaseOrLigAnchors<AnchorBuilder>,
141 );
142}
143
144impl MarkAttachmentBuilder for MarkToBaseBuilder {
145 fn add_mark(&mut self, gid: GlyphId16, group: &GroupName, anchor: AnchorBuilder) {
146 let _ = self.insert_mark(gid, group, anchor);
147 }
148
149 fn add_base(
150 &mut self,
151 gid: GlyphId16,
152 group: &GroupName,
153 anchor: BaseOrLigAnchors<AnchorBuilder>,
154 ) {
155 match anchor {
156 BaseOrLigAnchors::Base(anchor) => self.insert_base(gid, group, anchor),
157 BaseOrLigAnchors::Ligature(_) => panic!("lig anchors in mark2base builder"),
158 }
159 }
160}
161
162impl MarkAttachmentBuilder for MarkToMarkBuilder {
163 fn add_mark(&mut self, gid: GlyphId16, group: &GroupName, anchor: AnchorBuilder) {
164 let _ = self.insert_mark1(gid, group, anchor);
165 }
166
167 fn add_base(
168 &mut self,
169 gid: GlyphId16,
170 group: &GroupName,
171 anchor: BaseOrLigAnchors<AnchorBuilder>,
172 ) {
173 match anchor {
174 BaseOrLigAnchors::Base(anchor) => self.insert_mark2(gid, group, anchor),
175 BaseOrLigAnchors::Ligature(_) => panic!("lig anchors in mark2mark to builder"),
176 }
177 }
178}
179
180impl MarkAttachmentBuilder for MarkToLigBuilder {
181 fn add_mark(&mut self, gid: GlyphId16, group: &GroupName, anchor: AnchorBuilder) {
182 let _ = self.insert_mark(gid, group, anchor);
183 }
184
185 fn add_base(
186 &mut self,
187 gid: GlyphId16,
188 group: &GroupName,
189 anchors: BaseOrLigAnchors<AnchorBuilder>,
190 ) {
191 match anchors {
192 BaseOrLigAnchors::Ligature(anchors) => self.insert_ligature(gid, group, anchors),
193 BaseOrLigAnchors::Base(_) => panic!("base anchors passed to mark2lig builder"),
194 }
195 }
196}
197
198impl<'a> MarkLookupBuilder<'a> {
199 fn new(
200 anchors: Vec<&'a GlyphAnchors>,
201 glyph_order: &'a GlyphOrder,
202 static_metadata: &'a StaticMetadata,
203 fea_first_pass: &'a FeaFirstPassOutput,
204 char_map: HashMap<u32, GlyphId16>,
205 ) -> Result<Self, Error> {
206 let gdef_classes = super::get_gdef_classes(static_metadata, fea_first_pass, glyph_order);
207 let lig_carets = get_ligature_carets(glyph_order, static_metadata, &anchors)?;
208 let mut pruned = BTreeMap::new();
211 let mut base_groups = HashSet::new();
212 let mut mark_groups = HashSet::new();
213 let include = gdef_classes
214 .iter()
215 .filter_map(|(name, cls)| {
216 matches!(
217 *cls,
218 GlyphClassDef::Base | GlyphClassDef::Mark | GlyphClassDef::Ligature
219 )
220 .then_some(name)
221 })
222 .collect::<HashSet<_>>();
223 for anchors in anchors {
224 if !glyph_order.contains(&anchors.glyph_name) {
226 continue;
227 }
228 let gid = glyph_order.glyph_id(&anchors.glyph_name).unwrap();
229 if !include.is_empty() && !include.contains(&gid) {
231 continue;
232 }
233 for anchor in &anchors.anchors {
234 match &anchor.kind {
235 AnchorKind::Base(group)
236 | AnchorKind::Ligature {
237 group_name: group, ..
238 } => {
239 base_groups.insert(group);
240 }
241 AnchorKind::Mark(group) => {
242 mark_groups.insert(group);
243 }
244 AnchorKind::ComponentMarker(_)
245 | AnchorKind::CursiveEntry
246 | AnchorKind::CursiveExit
247 | AnchorKind::Caret(_)
248 | AnchorKind::VCaret(_) => (),
249 }
250 pruned.entry(gid).or_insert(Vec::new()).push(anchor);
251 }
252 }
253
254 let used_groups = base_groups
255 .intersection(&mark_groups)
256 .collect::<HashSet<_>>();
257 pruned.retain(|_, anchors| {
260 anchors.retain(|anchor| {
261 anchor.is_cursive()
262 || anchor
263 .mark_group_name()
264 .map(|group| used_groups.contains(&group))
265 .unwrap_or_else(|| anchor.is_component_marker())
266 });
267 !anchors.is_empty()
268 });
269
270 let mark_glyphs = find_mark_glyphs(&pruned, &gdef_classes);
271 Ok(Self {
272 anchor_lists: pruned,
273 glyph_order,
274 static_metadata,
275 fea_first_pass,
276 gdef_classes,
277 mark_glyphs,
278 lig_carets,
279 char_map,
280 })
281 }
282
283 fn build(&self) -> Result<FeaRsMarks, Error> {
285 let mark_base_groups = self.make_mark_to_base_groups();
286 let mark_mark_groups = self.make_mark_to_mark_groups();
287 let mark_lig_groups = self.make_mark_to_liga_groups();
288
289 let (abvm_glyphs, non_abvm_glyphs) = self.split_mark_and_abvm_blwm_glyphs()?;
290
291 let todo = super::feature_writer_todo_list(
292 &[MARK, MKMK, ABVM, BLWM, CURS],
293 &self.fea_first_pass.ast,
294 );
295
296 let mut mark_mkmk = self.make_lookups(
297 &mark_base_groups,
298 &mark_mark_groups,
299 &mark_lig_groups,
300 &non_abvm_glyphs,
301 |_| true,
302 )?;
303 if !todo.contains(&MARK) {
304 mark_mkmk.mark_base.clear();
305 mark_mkmk.mark_lig.clear();
306 }
307 if !todo.contains(&MKMK) {
308 mark_mkmk.mark_mark.clear();
309 }
310
311 let curs = todo
312 .contains(&CURS)
313 .then(|| self.make_cursive_lookups())
314 .transpose()?
315 .unwrap_or_default();
316 let (abvm, blwm) = if !abvm_glyphs.is_empty() {
317 let abvm = todo
318 .contains(&ABVM)
319 .then(|| {
320 self.make_lookups(
321 &mark_base_groups,
322 &mark_mark_groups,
323 &mark_lig_groups,
324 &abvm_glyphs,
325 is_above_mark,
326 )
327 })
328 .transpose()?;
329 let blwm = todo
330 .contains(&BLWM)
331 .then(|| {
332 self.make_lookups(
333 &mark_base_groups,
334 &mark_mark_groups,
335 &mark_lig_groups,
336 &abvm_glyphs,
337 is_below_mark,
338 )
339 })
340 .transpose()?;
341 (abvm.unwrap_or_default(), blwm.unwrap_or_default())
342 } else {
343 Default::default()
344 };
345
346 Ok(FeaRsMarks {
347 glyphmap: self.glyph_order.names().cloned().collect(),
348 mark_mkmk,
349 abvm,
350 blwm,
351 curs,
352 lig_carets: self.lig_carets.clone(),
353 })
354 }
355
356 fn make_lookups(
357 &self,
358 mark_base_groups: &BTreeMap<GroupName, MarkGroup>,
359 mark_mark_groups: &BTreeMap<GroupName, MarkGroup>,
360 mark_lig_groups: &BTreeMap<GroupName, MarkGroup>,
361 include_glyphs: &IntSet<GlyphId16>,
362 marks_filter: impl Fn(&GroupName) -> bool,
363 ) -> Result<MarkLookups, Error> {
364 let mark_base = self.make_lookups_type::<MarkToBaseBuilder>(
365 mark_base_groups,
366 include_glyphs,
367 &marks_filter,
368 )?;
369 let mark_mark = self.make_lookups_type::<MarkToMarkBuilder>(
370 mark_mark_groups,
371 include_glyphs,
372 &marks_filter,
373 )?;
374 let mark_lig = self.make_lookups_type::<MarkToLigBuilder>(
375 mark_lig_groups,
376 include_glyphs,
377 &marks_filter,
378 )?;
379 Ok(MarkLookups {
380 mark_base,
381 mark_mark,
382 mark_lig,
383 })
384 }
385
386 fn make_lookups_type<T: MarkAttachmentBuilder>(
388 &self,
389 groups: &BTreeMap<GroupName, MarkGroup>,
390 include_glyphs: &IntSet<GlyphId16>,
391 marks_filter: &impl Fn(&GroupName) -> bool,
393 ) -> Result<Vec<PendingLookup<T>>, Error> {
394 let mut result = Vec::with_capacity(groups.len());
395 for (name, group) in groups {
396 if !marks_filter(name) {
397 continue;
398 }
399
400 let Some(group) = group.only_using_glyphs(include_glyphs) else {
402 continue;
403 };
404
405 let mut builder = T::default();
406 let filter_set = group.make_filter_glyph_set(include_glyphs);
407 let mut flags = LookupFlag::empty();
408 if filter_set.is_some() {
409 flags |= LookupFlag::USE_MARK_FILTERING_SET;
410 }
411 for (mark_gid, anchor) in &group.marks {
412 let anchor = resolve_anchor_once(anchor, self.static_metadata)
413 .map_err(|e| self.convert_delta_error(e, *mark_gid))?;
414 builder.add_mark(*mark_gid, name, anchor);
415 }
416
417 for (base_gid, anchor) in &group.bases {
418 if !include_glyphs.contains(*base_gid) {
419 continue;
420 }
421 let anchor = resolve_anchor(anchor, self.static_metadata)
422 .map_err(|e| self.convert_delta_error(e, *base_gid))?;
423 builder.add_base(*base_gid, name, anchor);
424 }
425 result.push(PendingLookup::new(vec![builder], flags, filter_set));
426 }
427 Ok(result)
428 }
429
430 fn make_mark_to_base_groups(&self) -> BTreeMap<GroupName, MarkGroup<'a>> {
431 let mut groups = BTreeMap::<_, MarkGroup>::new();
432 for (gid, anchors) in &self.anchor_lists {
433 let is_mark = self.mark_glyphs.contains(gid);
434 let is_not_base = !self.gdef_classes.is_empty()
436 && (self.gdef_classes.get(gid)) != Some(&GlyphClassDef::Base);
437
438 let treat_as_base = !(is_mark | is_not_base);
439 for anchor in anchors {
440 match &anchor.kind {
441 ir::AnchorKind::Base(group) if treat_as_base => groups
442 .entry(group.clone())
443 .or_default()
444 .bases
445 .push((*gid, BaseOrLigAnchors::Base(anchor))),
446 ir::AnchorKind::Mark(group) if is_mark => groups
447 .entry(group.clone())
448 .or_default()
449 .marks
450 .push((*gid, anchor)),
451 _ => continue,
452 }
453 }
454 }
455 groups
456 }
457
458 fn make_mark_to_mark_groups(&self) -> BTreeMap<GroupName, MarkGroup<'a>> {
459 let (mark_glyphs, mark_anchors): (BTreeSet<_>, BTreeSet<_>) = self
461 .anchor_lists
462 .iter()
463 .filter(|(name, _)| self.mark_glyphs.contains(*name))
464 .flat_map(|(name, anchors)| {
465 anchors.iter().filter_map(move |anchor| {
466 if let AnchorKind::Mark(group) = &anchor.kind {
467 Some((name, group))
468 } else {
469 None
470 }
471 })
472 })
473 .unzip();
474
475 let mut result = BTreeMap::<GroupName, MarkGroup>::new();
478 for (gid, glyph_anchors) in &self.anchor_lists {
479 if !mark_glyphs.contains(gid) {
480 continue;
481 }
482
483 for anchor in glyph_anchors {
484 if let AnchorKind::Base(group_name) = &anchor.kind {
485 if mark_anchors.contains(group_name) {
487 let group = result.entry(group_name.clone()).or_default();
488 group.filter_glyphs = true;
489 group.bases.push((*gid, BaseOrLigAnchors::Base(anchor)))
490 }
491 }
492 }
493 }
494
495 for mark in mark_glyphs {
497 let anchors = self.anchor_lists.get(mark).unwrap();
498 for anchor in anchors {
499 if let AnchorKind::Mark(group) = &anchor.kind {
500 if let Some(group) = result.get_mut(group) {
502 group.marks.push((*mark, anchor))
503 }
504 }
505 }
506 }
507 result
508 }
509
510 fn make_mark_to_liga_groups(&self) -> BTreeMap<GroupName, MarkGroup<'_>> {
511 let mut groups = BTreeMap::<_, MarkGroup>::new();
512 let mut liga_anchor_groups = HashSet::new();
513
514 let mut component_groups = HashMap::new();
516
517 for (gid, anchors) in &self.anchor_lists {
520 let might_be_liga = self.gdef_classes.is_empty()
522 || (self.gdef_classes.get(gid) == Some(&GlyphClassDef::Ligature));
523
524 if !might_be_liga {
525 continue;
526 }
527
528 let Some(max_index) = anchors.iter().filter_map(|a| a.ligature_index()).max() else {
530 continue;
531 };
532
533 for anchor in anchors {
534 let AnchorKind::Ligature { group_name, index } = &anchor.kind else {
535 continue;
536 };
537 liga_anchor_groups.insert(group_name);
538 let component_anchors = component_groups
539 .entry(group_name.clone())
540 .or_insert_with(|| vec![None; max_index]);
541 component_anchors[*index - 1] = Some(*anchor);
542 }
543
544 for (group_name, anchors) in component_groups.drain() {
545 groups
546 .entry(group_name)
547 .or_default()
548 .bases
549 .push((*gid, BaseOrLigAnchors::Ligature(anchors)));
550 }
551 }
552
553 for (gid, anchors) in &self.anchor_lists {
555 if !self.mark_glyphs.contains(gid) {
556 continue;
557 }
558 for anchor in anchors {
559 if let AnchorKind::Mark(group) = &anchor.kind {
560 if liga_anchor_groups.contains(group) {
561 groups
562 .entry(group.to_owned())
563 .or_default()
564 .marks
565 .push((*gid, anchor));
566 }
567 }
568 }
569 }
570 groups
571 }
572
573 fn make_cursive_lookups(&self) -> Result<Vec<PendingLookup<CursivePosBuilder>>, Error> {
575 let dir_glyphs = super::properties::glyphs_by_script_direction(
576 &self.char_map,
577 self.fea_first_pass.gsub().as_ref(),
578 )?;
579
580 let mut ltr_builder = CursivePosBuilder::default();
581 let mut rtl_builder = CursivePosBuilder::default();
582
583 let ltr_glyphs = dir_glyphs.get(&ScriptDirection::LeftToRight);
584 for (gid, anchors) in &self.anchor_lists {
585 let (entry, exit) = self.get_entry_and_exit(*gid, anchors)?;
588 if entry.is_none() && exit.is_none() {
589 continue;
590 }
591
592 if ltr_glyphs
595 .map(|glyphs| glyphs.contains(*gid))
596 .unwrap_or(false)
597 {
598 ltr_builder.insert(*gid, entry, exit);
599 } else {
600 rtl_builder.insert(*gid, entry, exit);
601 }
602 }
603
604 Ok([
605 (ltr_builder, LookupFlag::empty()),
606 (rtl_builder, LookupFlag::RIGHT_TO_LEFT),
607 ]
608 .into_iter()
609 .filter_map(|(builder, flags)| {
610 (!builder.is_empty())
611 .then(|| PendingLookup::new(vec![builder], LookupFlag::IGNORE_MARKS | flags, None))
612 })
613 .collect())
614 }
615
616 fn get_entry_and_exit(
618 &self,
619 gid: GlyphId16,
620 anchors: &[&Anchor],
621 ) -> Result<(Option<AnchorBuilder>, Option<AnchorBuilder>), Error> {
622 let mut entry = None;
623 let mut exit = None;
624
625 for anchor in anchors {
626 match anchor.kind {
627 AnchorKind::CursiveEntry => {
628 entry = Some(
629 resolve_anchor_once(anchor, self.static_metadata)
630 .map_err(|e| self.convert_delta_error(e, gid))?,
631 );
632 }
633 AnchorKind::CursiveExit => {
634 exit = Some(
635 resolve_anchor_once(anchor, self.static_metadata)
636 .map_err(|e| self.convert_delta_error(e, gid))?,
637 );
638 }
639 _ => (),
640 }
641 }
642 Ok((entry, exit))
643 }
644
645 fn convert_delta_error(&self, err: DeltaError, gid: GlyphId16) -> Error {
646 let name = self.glyph_order.glyph_name(gid.into()).cloned().unwrap();
647 Error::AnchorDeltaError(name, err)
648 }
649
650 fn split_mark_and_abvm_blwm_glyphs(
654 &self,
655 ) -> Result<(IntSet<GlyphId16>, IntSet<GlyphId16>), Error> {
656 let scripts_using_abvm = scripts_using_abvm();
657 let fea_scripts = super::get_script_language_systems(&self.fea_first_pass.ast)
658 .into_keys()
659 .collect::<HashSet<_>>();
660 let filtered_scripts_using_abvm = (!fea_scripts.is_empty()).then(|| {
661 scripts_using_abvm
662 .intersection(&fea_scripts)
663 .copied()
664 .collect::<HashSet<_>>()
665 });
666 let maybe_filtered = filtered_scripts_using_abvm
669 .as_ref()
670 .unwrap_or(&scripts_using_abvm);
671
672 let unicode_is_abvm = |uv: u32| -> Option<bool> {
675 let mut saw_abvm = false;
676 for script in super::properties::scripts_for_codepoint(uv) {
677 if script == super::properties::COMMON_SCRIPT {
678 return None;
679 }
680 saw_abvm |= maybe_filtered.contains(&script);
682 }
683 Some(saw_abvm)
684 };
685
686 let unicode_is_non_abvm = |uv: u32| -> Option<bool> {
688 Some(
689 super::properties::scripts_for_codepoint(uv)
690 .any(|script| !scripts_using_abvm.contains(&script)),
692 )
693 };
694
695 if scripts_using_abvm.is_empty()
696 || !self
697 .char_map
698 .keys()
699 .copied()
700 .any(|uv| unicode_is_abvm(uv).unwrap_or(false))
701 {
702 return Ok((
704 Default::default(),
705 self.glyph_order.iter().map(|(gid, _)| gid).collect(),
706 ));
707 }
708
709 let gsub = self.fea_first_pass.gsub();
710 let abvm_glyphs = super::properties::glyphs_matching_predicate(
711 &self.char_map,
712 unicode_is_abvm,
713 gsub.as_ref(),
714 )?;
715
716 let mut non_abvm_glyphs = super::properties::glyphs_matching_predicate(
717 &self.char_map,
718 unicode_is_non_abvm,
719 gsub.as_ref(),
720 )?;
721 non_abvm_glyphs.extend(
725 self.glyph_order
726 .iter()
727 .map(|(gid, _)| gid)
728 .filter(|gid| !abvm_glyphs.contains(*gid)),
729 );
730 Ok((abvm_glyphs, non_abvm_glyphs))
731 }
732}
733
734fn is_above_mark(anchor_name: &GroupName) -> bool {
737 !is_below_mark(anchor_name)
738}
739
740fn is_below_mark(anchor_name: &GroupName) -> bool {
741 anchor_name.starts_with("bottom") || anchor_name == "nukta"
742}
743
744impl Work<Context, AnyWorkId, Error> for MarkWork {
745 fn id(&self) -> AnyWorkId {
746 WorkId::Marks.into()
747 }
748
749 fn read_access(&self) -> Access<AnyWorkId> {
750 AccessBuilder::new()
751 .variant(FeWorkId::StaticMetadata)
752 .variant(FeWorkId::GlyphOrder)
753 .variant(WorkId::FeaturesAst)
754 .variant(FeWorkId::ALL_ANCHORS)
755 .build()
756 }
757
758 fn exec(&self, context: &Context) -> Result<(), Error> {
760 let static_metadata = context.ir.static_metadata.get();
761 let glyph_order = context.ir.glyph_order.get();
762 let raw_anchors = context.ir.anchors.all();
763 let fea_first_pass = context.fea_ast.get();
764
765 let anchors = raw_anchors
766 .iter()
767 .map(|(_, anchors)| anchors.as_ref())
768 .collect::<Vec<_>>();
769
770 let glyphs = glyph_order
771 .names()
772 .map(|glyphname| context.ir.get_glyph(glyphname.clone()))
773 .collect::<Vec<_>>();
774
775 let char_map = glyphs
776 .iter()
777 .flat_map(|g| {
778 let id = glyph_order.glyph_id(&g.name).unwrap();
779 g.codepoints.iter().map(move |cp| (*cp, id))
780 })
781 .collect::<HashMap<_, _>>();
782
783 let ctx = MarkLookupBuilder::new(
786 anchors,
787 &glyph_order,
788 &static_metadata,
789 &fea_first_pass,
790 char_map,
791 )?;
792 let all_marks = ctx.build()?;
793
794 context.fea_rs_marks.set(all_marks);
795
796 Ok(())
797 }
798}
799
800fn find_mark_glyphs(
803 anchors: &BTreeMap<GlyphId16, Vec<&Anchor>>,
804 gdef_classes: &HashMap<GlyphId16, GlyphClassDef>,
805) -> BTreeSet<GlyphId16> {
806 anchors
807 .iter()
808 .filter(|(name, anchors)| {
809 if !gdef_classes.is_empty() && gdef_classes.get(*name) != Some(&GlyphClassDef::Mark) {
812 return false;
813 }
814 anchors.iter().any(|a| a.is_mark())
817 })
818 .map(|(name, _)| name.to_owned())
819 .collect()
820}
821
822fn resolve_anchor(
823 anchor: &BaseOrLigAnchors<&ir::Anchor>,
824 static_metadata: &StaticMetadata,
825) -> Result<BaseOrLigAnchors<AnchorBuilder>, DeltaError> {
826 match anchor {
827 BaseOrLigAnchors::Base(anchor) => {
828 resolve_anchor_once(anchor, static_metadata).map(BaseOrLigAnchors::Base)
829 }
830 BaseOrLigAnchors::Ligature(anchors) => anchors
831 .iter()
832 .map(|a| {
833 a.map(|a| resolve_anchor_once(a, static_metadata))
834 .transpose()
835 })
836 .collect::<Result<_, _>>()
837 .map(BaseOrLigAnchors::Ligature),
838 }
839}
840
841fn resolve_anchor_once(
842 anchor: &ir::Anchor,
843 static_metadata: &StaticMetadata,
844) -> Result<AnchorBuilder, DeltaError> {
845 let (x_values, y_values): (Vec<_>, Vec<_>) = anchor
846 .positions
847 .iter()
848 .map(|(loc, pt)| {
849 (
850 (loc.clone(), OrderedFloat::from(pt.x)),
851 (loc.clone(), OrderedFloat::from(pt.y)),
852 )
853 })
854 .unzip();
855
856 let (x_default, x_deltas) = crate::features::resolve_variable_metric(
857 static_metadata,
858 x_values.iter().map(|item| (&item.0, &item.1)),
863 )?;
864 let (y_default, y_deltas) = crate::features::resolve_variable_metric(
865 static_metadata,
866 y_values.iter().map(|item| (&item.0, &item.1)),
867 )?;
868
869 let mut anchor = AnchorBuilder::new(x_default, y_default);
870 if x_deltas.iter().any(|v| v.1 != 0) {
871 anchor = anchor.with_x_device(x_deltas);
872 }
873 if y_deltas.iter().any(|v| v.1 != 0) {
874 anchor = anchor.with_y_device(y_deltas);
875 }
876
877 Ok(anchor)
878}
879
880fn get_ligature_carets(
885 glyph_order: &GlyphOrder,
886 static_metadata: &StaticMetadata,
887 anchors: &[&GlyphAnchors],
888) -> Result<BTreeMap<GlyphId16, Vec<CaretValueBuilder>>, Error> {
889 let mut out = BTreeMap::new();
890
891 for glyph_anchor in anchors {
892 let Some(gid) = glyph_order.glyph_id(&glyph_anchor.glyph_name) else {
893 continue;
894 };
895 let carets = glyph_anchor
896 .anchors
897 .iter()
898 .filter_map(|anchor| {
899 make_caret_value(anchor, static_metadata, &glyph_anchor.glyph_name).transpose()
900 })
901 .collect::<Result<Vec<_>, _>>()?;
902 if !carets.is_empty() {
903 out.insert(gid, carets);
904 }
905 }
906 Ok(out)
907}
908
909fn make_caret_value(
910 anchor: &ir::Anchor,
911 static_metadata: &StaticMetadata,
912 glyph_name: &GlyphName,
913) -> Result<Option<CaretValueBuilder>, Error> {
914 if !matches!(anchor.kind, AnchorKind::Caret(_) | AnchorKind::VCaret(_)) {
915 return Ok(None);
916 }
917 let is_vertical = matches!(anchor.kind, AnchorKind::VCaret(_));
918
919 let values = anchor
920 .positions
921 .iter()
922 .map(|(loc, pt)| {
923 let pos = if is_vertical { pt.y } else { pt.x };
924 (loc.clone(), OrderedFloat::from(pos))
925 })
926 .collect::<Vec<_>>();
927
928 let (default, mut deltas) = crate::features::resolve_variable_metric(
929 static_metadata,
930 values.iter().map(|item| (&item.0, &item.1)),
931 )
932 .map_err(|err| Error::AnchorDeltaError(glyph_name.to_owned(), err))?;
933
934 if deltas.iter().all(|d| d.1 == 0) {
936 deltas.clear();
937 }
938
939 Ok(Some(CaretValueBuilder::Coordinate {
940 default,
941 deltas: deltas.into(),
942 }))
943}
944
945impl FeatureProvider for FeaRsMarks {
946 fn add_features(&self, builder: &mut fea_rs::compile::FeatureBuilder) {
947 fn add_all_lookups(
949 builder: &mut fea_rs::compile::FeatureBuilder,
950 lookups: &MarkLookups,
951 ) -> Vec<LookupId> {
952 let mut out = Vec::new();
953 out.extend(
954 lookups
955 .mark_base
956 .iter()
957 .map(|lk| builder.add_lookup(lk.clone())),
958 );
959 out.extend(
960 lookups
961 .mark_lig
962 .iter()
963 .map(|lk| builder.add_lookup(lk.clone())),
964 );
965 out.extend(
966 lookups
967 .mark_mark
968 .iter()
969 .map(|lk| builder.add_lookup(lk.clone())),
970 );
971 out
972 }
973
974 let abvm_lookups = add_all_lookups(builder, &self.abvm);
976 let blwm_lookups = add_all_lookups(builder, &self.blwm);
977
978 let mut mark_lookups = Vec::new();
979 let mut mkmk_lookups = Vec::new();
980 let mut curs_lookups = Vec::new();
981
982 for mark_base in self.mark_mkmk.mark_base.iter() {
983 mark_lookups.push(builder.add_lookup(mark_base.clone()));
985 }
986 for mark_lig in self.mark_mkmk.mark_lig.iter() {
987 mark_lookups.push(builder.add_lookup(mark_lig.clone()));
988 }
989
990 for mark_mark in self.mark_mkmk.mark_mark.iter() {
992 mkmk_lookups.push(builder.add_lookup(mark_mark.clone()));
993 }
994
995 for curs in self.curs.iter() {
996 curs_lookups.push(builder.add_lookup(curs.clone()));
997 }
998
999 for (lookups, tag) in [
1000 (mark_lookups, MARK),
1001 (mkmk_lookups, MKMK),
1002 (curs_lookups, CURS),
1003 (abvm_lookups, ABVM),
1004 (blwm_lookups, BLWM),
1005 ] {
1006 if !lookups.is_empty() {
1007 builder.add_to_default_language_systems(tag, &lookups);
1008 }
1009 }
1010
1011 if !self.lig_carets.is_empty()
1012 && builder
1013 .gdef()
1014 .map(|gdef| gdef.ligature_pos.is_empty())
1015 .unwrap_or(true)
1016 {
1017 builder.add_lig_carets(self.lig_carets.clone());
1018 }
1019 }
1020}
1021
1022fn scripts_using_abvm() -> HashSet<UnicodeShortName> {
1024 INDIC_SCRIPTS
1025 .iter()
1026 .chain(USE_SCRIPTS)
1027 .chain(std::iter::once(&"Khmr"))
1028 .filter_map(|s| UnicodeShortName::try_from_str(s).ok())
1029 .collect()
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034 use fea_rs::compile::Compilation;
1035 use fontdrasil::{
1036 agl,
1037 coords::{Coord, CoordConverter, NormalizedLocation},
1038 types::Axis,
1039 };
1040 use fontir::ir::{GdefCategories, NamedInstance};
1041 use kurbo::Point;
1042
1043 use write_fonts::{
1044 dump_table,
1045 read::{
1046 tables::{gdef::Gdef as RGdef, gpos::Gpos as RGpos},
1047 FontRead,
1048 },
1049 tables::gdef::CaretValue as RawCaretValue,
1050 };
1051
1052 use crate::features::test_helpers::{LayoutOutput, LayoutOutputBuilder};
1053
1054 use super::*;
1055
1056 fn char_for_glyph(name: &GlyphName) -> Option<char> {
1057 static MANUAL: &[(&str, char)] = &[
1058 ("brevecomb", '\u{0306}'),
1059 ("breveinvertedcomb", '\u{0311}'),
1060 ("macroncomb", '\u{0304}'),
1061 ("f_f", '\u{FB00}'),
1062 ("f_i", '\u{FB01}'),
1063 ("f_l", '\u{FB02}'),
1064 ("f_i_i", '\u{FB03}'),
1065 ("hamza", '\u{0621}'),
1066 ("dottedCircle", '\u{25CC}'),
1067 ("nukta-kannada", '\u{0CBC}'),
1068 ("candrabindu-kannada", '\u{0C81}'),
1069 ("halant-kannada", '\u{0CCD}'),
1070 ("ka-kannada", '\u{0C95}'),
1071 ("taonethousand", '\u{0BF2}'),
1072 ("uni25CC", '\u{25CC}'),
1073 ];
1074
1075 static UNMAPPED: &[&str] = &["ka-kannada.base", "a.alt"];
1076
1077 let c = agl::char_for_agl_name(name.as_str()).or_else(|| {
1078 MANUAL
1079 .iter()
1080 .find_map(|(n, uv)| (name.as_str() == *n).then_some(*uv))
1081 });
1082 if c.is_none() && !UNMAPPED.iter().any(|except| *except == name.as_str()) {
1083 panic!("please add a manual charmap entry for '{name}'");
1084 }
1085 c
1086 }
1087
1088 #[derive(Clone, Debug)]
1090 struct MarksInput<const N: usize> {
1091 prefer_gdef_categories_in_fea: bool,
1092 locations: [NormalizedLocation; N],
1093 anchors: BTreeMap<GlyphName, Vec<Anchor>>,
1094 categories: BTreeMap<GlyphName, GlyphClassDef>,
1095 char_map: HashMap<u32, GlyphName>,
1096 user_fea: &'static str,
1097 }
1098
1099 struct AnchorBuilder<const N: usize> {
1100 locations: [NormalizedLocation; N],
1101 anchors: Vec<Anchor>,
1102 }
1103
1104 impl Default for MarksInput<1> {
1105 fn default() -> Self {
1106 Self::new([&[0.0]])
1107 }
1108 }
1109
1110 trait F32OrI16 {
1115 fn to_f64(self) -> f64;
1116 }
1117
1118 impl F32OrI16 for f32 {
1119 fn to_f64(self) -> f64 {
1120 self as _
1121 }
1122 }
1123
1124 impl F32OrI16 for i16 {
1125 fn to_f64(self) -> f64 {
1126 self as _
1127 }
1128 }
1129
1130 impl<const N: usize> AnchorBuilder<N> {
1131 fn add<T: F32OrI16>(&mut self, anchor_name: &str, pos: [(T, T); N]) -> &mut Self {
1136 let positions = pos
1137 .into_iter()
1138 .enumerate()
1139 .map(|(i, pt)| {
1140 (
1141 self.locations[i].clone(),
1142 Point::new(pt.0.to_f64(), pt.1.to_f64()),
1143 )
1144 })
1145 .collect();
1146 let kind = AnchorKind::new(anchor_name).unwrap();
1147 self.anchors.push(Anchor { kind, positions });
1148 self
1149 }
1150 }
1151
1152 impl<const N: usize> MarksInput<N> {
1153 fn new(locations: [&[f64]; N]) -> Self {
1155 const TAG_NAMES: [Tag; 3] = [Tag::new(b"axs1"), Tag::new(b"axs2"), Tag::new(b"axs3")];
1156 let locations = locations
1157 .iter()
1158 .map(|loc| {
1159 loc.iter()
1160 .enumerate()
1161 .map(|(i, pos)| {
1162 (
1163 *TAG_NAMES.get(i).expect("too many axes in test"),
1164 Coord::new(*pos),
1165 )
1166 })
1167 .collect()
1168 })
1169 .collect::<Vec<_>>()
1170 .try_into()
1171 .unwrap();
1172 Self {
1173 user_fea: "languagesystem DFLT dflt;",
1174 locations,
1175 anchors: Default::default(),
1176 categories: Default::default(),
1177 char_map: Default::default(),
1178 prefer_gdef_categories_in_fea: false,
1179 }
1180 }
1181
1182 fn set_user_fea(&mut self, fea: &'static str) -> &mut Self {
1186 self.user_fea = fea;
1187 self
1188 }
1189
1190 fn set_prefer_fea_gdef_categories(&mut self, flag: bool) -> &mut Self {
1195 self.prefer_gdef_categories_in_fea = flag;
1196 self
1197 }
1198
1199 fn add_glyph(
1204 &mut self,
1205 name: &str,
1206 category: impl Into<Option<GlyphClassDef>>,
1207 mut anchors_fn: impl FnMut(&mut AnchorBuilder<N>),
1208 ) -> &mut Self {
1209 let name: GlyphName = name.into();
1210 if let Some(unic) = char_for_glyph(&name) {
1211 self.char_map.insert(unic as u32, name.clone());
1212 }
1213
1214 if let Some(category) = category.into() {
1215 self.categories.insert(name.clone(), category);
1216 }
1217 let mut anchors = AnchorBuilder {
1218 locations: self.locations.clone(),
1219 anchors: Default::default(),
1220 };
1221
1222 anchors_fn(&mut anchors);
1223 self.anchors.insert(name, anchors.anchors);
1224 self
1225 }
1226
1227 fn make_layout_output(&self) -> LayoutOutput {
1228 let (min, default, max) = (Coord::new(-1.), Coord::new(0.0), Coord::new(1.0));
1229 let axes = self.locations[0]
1230 .axis_tags()
1231 .map(|tag| Axis {
1232 name: tag.to_string(),
1233 tag: *tag,
1234 min,
1235 default,
1236 max,
1237 hidden: false,
1238 converter: CoordConverter::unmapped(min, default, max),
1239 localized_names: Default::default(),
1240 })
1241 .collect();
1242 let named_instances = self
1243 .locations
1244 .iter()
1245 .enumerate()
1246 .map(|(i, loc)| NamedInstance {
1247 name: format!("instance{i}"),
1248 postscript_name: None,
1249 location: loc.to_user(&axes),
1250 })
1251 .collect();
1252 let glyph_locations = self.locations.iter().cloned().collect();
1253 let categories = GdefCategories {
1254 categories: self.categories.clone(),
1255 prefer_gdef_categories_in_fea: self.prefer_gdef_categories_in_fea,
1256 };
1257 LayoutOutputBuilder::new()
1258 .with_axes(axes)
1259 .with_instances(named_instances)
1260 .with_locations(glyph_locations)
1261 .with_categories(categories)
1262 .with_user_fea(self.user_fea)
1263 .with_glyph_order(self.anchors.keys().cloned().collect())
1264 .build()
1265 }
1266
1267 fn compile_and_inspect(&self, f: impl FnOnce(&MarkLookupBuilder)) -> Compilation {
1270 let layout_output = self.make_layout_output();
1271 let anchors = self
1272 .anchors
1273 .iter()
1274 .map(|(name, anchors)| GlyphAnchors::new(name.clone(), anchors.clone()))
1275 .collect::<Vec<_>>();
1276 let anchorsref = anchors.iter().collect();
1277 let char_map = self
1278 .char_map
1279 .iter()
1280 .map(|(uv, name)| (*uv, layout_output.glyph_order.glyph_id(name).unwrap()))
1281 .collect();
1282
1283 let ctx = MarkLookupBuilder::new(
1284 anchorsref,
1285 &layout_output.glyph_order,
1286 &layout_output.static_metadata,
1287 &layout_output.first_pass_fea,
1288 char_map,
1289 )
1290 .unwrap();
1291
1292 f(&ctx);
1293
1294 let marks = ctx.build().unwrap();
1295 layout_output.compile(&marks)
1296 }
1297
1298 fn compile(&self) -> Compilation {
1300 self.compile_and_inspect(|_| {})
1301 }
1302
1303 fn get_normalized_output(&mut self) -> String {
1305 let result = self.compile();
1306 let Some(gpos) = result.gpos else {
1308 return String::new();
1309 };
1310 let gpos_bytes = write_fonts::dump_table(&gpos).unwrap();
1311 let gdef_bytes = result.gdef.map(|gdef| dump_table(&gdef).unwrap());
1312 let gpos = RGpos::read(gpos_bytes.as_slice().into()).unwrap();
1313 let gdef = gdef_bytes
1314 .as_ref()
1315 .map(|b| RGdef::read(b.as_slice().into()).unwrap());
1316 let mut buf = Vec::new();
1317 let names = self.anchors.keys().cloned().collect();
1318
1319 otl_normalizer::print_gpos(&mut buf, &gpos, gdef.as_ref(), &names).unwrap();
1321 String::from_utf8(buf).unwrap()
1322 }
1323 }
1324
1325 fn normalize_layout_repr(s: &str) -> String {
1327 s.trim().chars().filter(|c| *c != ' ').collect()
1328 }
1329 macro_rules! assert_eq_ignoring_ws {
1330 ($left:expr, $right:expr) => {
1331 let left = normalize_layout_repr(&$left);
1332 let right = normalize_layout_repr(&$right);
1333 pretty_assertions::assert_str_eq!(left, right)
1334 };
1335 }
1336
1337 fn pytest_ufo() -> MarksInput<1> {
1340 let mut builder = MarksInput::default();
1341 builder
1342 .add_glyph("a", None, |anchors| {
1343 anchors.add("top", [(100, 200)]);
1344 })
1345 .add_glyph("f_i", None, |anchors| {
1346 anchors.add("top_1", [(100, 500)]);
1347 anchors.add("top_2", [(600, 500)]);
1348 })
1349 .add_glyph("acutecomb", None, |anchors| {
1350 anchors.add("_top", [(100, 200)]);
1351 })
1352 .add_glyph("tildecomb", None, |anchors| {
1353 anchors.add("_top", [(100, 200)]);
1354 anchors.add("top", [(100, 300)]);
1355 });
1356 builder
1357 }
1358
1359 fn simple_test_input() -> MarksInput<1> {
1360 let mut out = MarksInput::default();
1361 out.add_glyph("A", GlyphClassDef::Base, |anchors| {
1362 anchors.add("top", [(100, 400)]);
1363 })
1364 .add_glyph("acutecomb", GlyphClassDef::Mark, |anchors| {
1365 anchors.add("_top", [(50, 50)]);
1366 });
1367 out
1368 }
1369
1370 #[test]
1372 fn no_anchors_no_feature() {
1373 let out = MarksInput::default()
1374 .add_glyph("a", None, |_| {})
1375 .add_glyph("acutecomb", None, |_| {})
1376 .get_normalized_output();
1377
1378 assert!(out.is_empty());
1379 }
1380
1381 #[test]
1382 fn attach_a_mark_to_a_base() {
1383 let out = simple_test_input().get_normalized_output();
1384 assert_eq_ignoring_ws!(
1385 out,
1386 r#"
1387 # mark: DFLT/dflt
1388 # 1 MarkToBase rules
1389 # lookupflag LookupFlag(0)
1390 A @(x: 100, y: 400)
1391 @(x: 50, y: 50) acutecomb
1392 "#
1393 );
1394 }
1395
1396 #[test]
1398 fn mark_mkmk_features() {
1399 let out = pytest_ufo().get_normalized_output();
1400 assert_eq_ignoring_ws!(
1401 out,
1402 r#"
1403 # mark: DFLT/dflt
1404 # 1 MarkToBase rules
1405 # lookupflag LookupFlag(0)
1406 a @(x: 100, y: 200)
1407 @(x: 100, y: 200) [acutecomb, tildecomb]
1408 # 1 MarkToLig rules
1409 # lookupflag LookupFlag(0)
1410 f_i (lig) [@(x: 100, y: 500), @(x: 600, y: 500)]
1411 @(x: 100, y: 200) [acutecomb, tildecomb]
1412
1413 # mkmk: DFLT/dflt
1414 # 1 MarkToMark rules
1415 # lookupflag LookupFlag(16)
1416 # filter glyphs: [acutecomb, tildecomb]
1417 tildecomb @(x: 100, y: 300)
1418 @(x: 100, y: 200) [acutecomb, tildecomb]
1419 "#
1420 );
1421 }
1422
1423 #[test]
1427 fn oswald_ae_test_case() {
1428 let mut input = MarksInput::default();
1429 let out = input
1430 .add_glyph("A", None, |anchors| {
1431 anchors
1432 .add("bottom", [(234, 0)])
1433 .add("ogonek", [(411, 0)])
1434 .add("top", [(234, 810)]);
1435 })
1436 .add_glyph("E", None, |anchors| {
1437 anchors
1438 .add("topleft", [(20, 810)])
1439 .add("bottom", [(216, 0)])
1440 .add("ogonek", [(314, 0)])
1441 .add("top", [(217, 810)]);
1442 })
1443 .add_glyph("acutecomb", None, |anchors| {
1444 anchors.add("top", [(0, 810)]).add("_top", [(0, 578)]);
1445 })
1446 .add_glyph("brevecomb", None, |anchors| {
1447 anchors.add("top", [(0, 810)]).add("_top", [(0, 578)]);
1448 })
1449 .add_glyph("tildecomb", None, |anchors| {
1450 anchors.add("top", [(0, 742)]).add("_top", [(0, 578)]);
1451 })
1452 .add_glyph("macroncomb", None, |anchors| {
1453 anchors.add("top", [(0, 810)]).add("_top", [(0, 578)]);
1454 })
1455 .add_glyph("breveinvertedcomb", None, |anchors| {
1456 anchors
1457 .add("top", [(0, 810)])
1458 .add("top.a", [(0, 810)])
1459 .add("_top.a", [(0, 578)]);
1460 })
1461 .get_normalized_output();
1462
1463 assert_eq_ignoring_ws!(
1464 out,
1465 r#"
1467 # abvm: DFLT/dflt
1468 # 2 MarkToMark rules
1469 # lookupflag LookupFlag(16)
1470 # filter glyphs: [acutecomb,macroncomb]
1471 acutecomb @(x: 0, y: 810)
1472 @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1473 macroncomb @(x: 0, y: 810)
1474 @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1475
1476 # mark: DFLT/dflt
1477 # 2 MarkToBase rules
1478 # lookupflag LookupFlag(0)
1479 A @(x: 234, y: 810)
1480 @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1481 E @(x: 217, y: 810)
1482 @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1483
1484 # mkmk: DFLT/dflt
1485 # 6 MarkToMark rules
1486 # lookupflag LookupFlag(16)
1487 # filter glyphs: [acutecomb,brevecomb,breveinvertedcomb,macroncomb,tildecomb]
1488 acutecomb @(x: 0, y: 810)
1489 @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1490 brevecomb @(x: 0, y: 810)
1491 @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1492 breveinvertedcomb @(x: 0, y: 810)
1493 @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1494 # filter glyphs: [breveinvertedcomb]
1495 breveinvertedcomb @(x: 0, y: 810)
1496 @(x: 0, y: 578) breveinvertedcomb
1497 # filter glyphs: [acutecomb,brevecomb,breveinvertedcomb,macroncomb,tildecomb]
1498 macroncomb @(x: 0, y: 810)
1499 @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1500 tildecomb @(x: 0, y: 742)
1501 @(x: 0, y: 578) [acutecomb,brevecomb,macroncomb,tildecomb]
1502
1503 "#
1504 );
1505 }
1506
1507 #[test]
1508 fn abvm_blwm_features() {
1509 let mut input = MarksInput::default();
1510 let out = input
1511 .add_glyph("dottedCircle", None, |anchors| {
1512 anchors
1513 .add("top", [(297, 552)])
1514 .add("topright", [(491, 458)])
1515 .add("bottom", [(297, 0)]);
1516 })
1517 .add_glyph("nukta-kannada", None, |anchors| {
1518 anchors.add("_bottom", [(0, 0)]);
1519 })
1520 .add_glyph("candrabindu-kannada", None, |anchors| {
1521 anchors.add("_top", [(0, 547)]);
1522 })
1523 .add_glyph("halant-kannada", None, |anchors| {
1524 anchors.add("_topright", [(-456, 460)]);
1525 })
1526 .add_glyph("ka-kannada", None, |anchors| {
1527 anchors.add("bottom", [(290, 0)]);
1528 })
1529 .add_glyph("ka-kannada.base", None, |anchors| {
1530 anchors
1531 .add("top", [(291, 547)])
1532 .add("topright", [(391, 460)])
1533 .add("bottom", [(290, 0)]);
1534 })
1535 .set_user_fea(
1536 "
1537 languagesystem DFLT dflt;
1538 languagesystem knda dflt;
1539 languagesystem knd2 dflt;
1540
1541 feature psts {
1542 sub ka-kannada' halant-kannada by ka-kannada.base;
1543 } psts;",
1544 )
1545 .get_normalized_output();
1546
1547 assert_eq_ignoring_ws!(
1548 out,
1549 r#"
1550 # abvm: DFLT/dflt, knd2/dflt, knda/dflt
1551 # 2 MarkToBase rules
1552 # lookupflag LookupFlag(0)
1553 ka-kannada.base @(x: 291, y: 547)
1554 @(x: 0, y: 547) candrabindu-kannada
1555 ka-kannada.base @(x: 391, y: 460)
1556 @(x: -456, y: 460) halant-kannada
1557
1558 # blwm: DFLT/dflt, knd2/dflt, knda/dflt
1559 # 2 MarkToBase rules
1560 # lookupflag LookupFlag(0)
1561 ka-kannada @(x: 290, y: 0)
1562 @(x: 0, y: 0) nukta-kannada
1563 ka-kannada.base @(x: 290, y: 0)
1564 @(x: 0, y: 0) nukta-kannada
1565
1566 # mark: DFLT/dflt, knd2/dflt,knda/dflt
1567 # 3 MarkToBase rules
1568 # lookupflag LookupFlag(0)
1569 dottedCircle @(x: 297, y: 0)
1570 @(x: 0, y: 0) nukta-kannada
1571 dottedCircle @(x: 297, y: 552)
1572 @(x: 0, y: 547) candrabindu-kannada
1573 dottedCircle @(x: 491, y: 458)
1574 @(x: -456, y: 460) halant-kannada
1575
1576 "#
1577 );
1578 }
1579
1580 #[test]
1581 fn include_unmapped_glyph_with_no_abvm() {
1582 let mut out = MarksInput::default();
1585 let out = out
1586 .set_user_fea(
1588 "languagesystem latn dflt;
1589 feature test {
1590 sub a by a.alt;
1591 } test;",
1592 )
1593 .add_glyph("a", None, |anchors| {
1594 anchors.add("top", [(100, 0)]);
1595 })
1596 .add_glyph("a.alt", None, |anchors| {
1597 anchors.add("top", [(110, 0)]);
1598 })
1599 .add_glyph("acutecomb", None, |anchors| {
1600 anchors.add("_top", [(202, 0)]);
1601 })
1602 .get_normalized_output();
1603
1604 assert_eq_ignoring_ws!(
1605 out,
1606 r#"
1607 # mark: latn/dflt
1608 # 2 MarkToBase rules
1609 # lookupflag LookupFlag(0)
1610 a @(x: 100, y: 0)
1611 @(x: 202, y: 0) acutecomb
1612 a.alt @(x: 110, y: 0)
1613 @(x: 202, y: 0) acutecomb
1614 "#
1615 );
1616 }
1617
1618 #[test]
1619 fn include_unmapped_non_abvm_glyph_with_abvm() {
1620 let mut out = MarksInput::default();
1624 out
1625 .set_user_fea(
1627 "languagesystem latn dflt;
1628 languagesystem knda dflt;
1629 ",
1630 )
1631 .add_glyph("ka-kannada.base", None, |_| {})
1633 .add_glyph("nukta-kannada", None, |_| {})
1634 .compile_and_inspect(|ctx| {
1635 let (abvm, non_abvm) = ctx.split_mark_and_abvm_blwm_glyphs().unwrap();
1636 let nukta = ctx.glyph_order.glyph_id("nukta-kannada").unwrap();
1637 let ka = ctx.glyph_order.glyph_id("ka-kannada.base").unwrap();
1638 assert!(abvm.contains(nukta));
1639 assert!(non_abvm.contains(ka));
1643 });
1644 }
1645 #[test]
1646 fn custom_fea() {
1647 let out = simple_test_input()
1648 .set_user_fea("languagesystem latn dflt;")
1649 .get_normalized_output();
1650 assert_eq_ignoring_ws!(
1651 out,
1652 r#"
1653 # mark: latn/dflt
1654 # 1 MarkToBase rules
1655 # lookupflag LookupFlag(0)
1656 A @(x: 100, y: 400)
1657 @(x: 50, y: 50) acutecomb
1658 "#
1659 );
1660 }
1661
1662 fn gdef_test_input() -> MarksInput<1> {
1664 let mut out = simple_test_input();
1665 out.set_user_fea(
1666 r#"
1667 @Bases = [A];
1668 @Marks = [acutecomb];
1669 table GDEF {
1670 GlyphClassDef @Bases, [], @Marks,;
1671 } GDEF;
1672 "#,
1673 )
1674 .add_glyph("gravecomb", GlyphClassDef::Mark, |anchors| {
1676 anchors.add("_top", [(5, 15)]);
1677 });
1678 out
1679 }
1680
1681 #[test]
1682 fn prefer_fea_gdef_categories_true() {
1683 let out = gdef_test_input()
1684 .set_prefer_fea_gdef_categories(true)
1685 .get_normalized_output();
1686 assert_eq_ignoring_ws!(
1687 out,
1688 r#"
1689 # mark: DFLT/dflt
1690 # 1 MarkToBase rules
1691 # lookupflag LookupFlag(0)
1692 A @(x: 100, y: 400)
1693 @(x: 50, y: 50) acutecomb
1694 "#
1695 );
1696 }
1697
1698 #[test]
1699 fn prefer_fea_gdef_categories_false() {
1700 let out = gdef_test_input()
1701 .set_prefer_fea_gdef_categories(false)
1702 .get_normalized_output();
1703 assert_eq_ignoring_ws!(
1704 out,
1705 r#"
1706 # mark: DFLT/dflt
1707 # 1 MarkToBase rules
1708 # lookupflag LookupFlag(0)
1709 A @(x: 100, y: 400)
1710 @(x: 5, y: 15) gravecomb
1711 @(x: 50, y: 50) acutecomb
1712 "#
1713 );
1714 }
1715
1716 #[test]
1719 fn non_abvm_glyphs_use_unfiltered_scripts() {
1720 let _ = MarksInput::default()
1721 .set_user_fea(
1722 r#"
1723 languagesystem DFLT dflt;
1724 languagesystem sinh dflt;
1725 languagesystem taml dflt;
1726 "#,
1727 )
1728 .add_glyph("taonethousand", None, |_| {})
1729 .compile_and_inspect(|builder| {
1730 let taonethousand = builder.glyph_order.glyph_id("taonethousand").unwrap();
1731 let (abvm, non_abvm) = builder.split_mark_and_abvm_blwm_glyphs().unwrap();
1732 assert!(abvm.contains(taonethousand));
1733 assert!(!non_abvm.contains(taonethousand));
1734 });
1735 }
1736
1737 #[test]
1738 fn glyph_in_abvm_but_not_if_no_abvm_lang() {
1739 let dotbelowcomb_char = char_for_glyph(&GlyphName::new("dotbelowcomb")).unwrap();
1740 let dotbelow_scripts =
1741 super::super::properties::scripts_for_codepoint(dotbelowcomb_char as _)
1742 .collect::<Vec<_>>();
1743 let abvm_scripts = super::scripts_using_abvm();
1744
1745 assert!(dotbelow_scripts.iter().any(|sc| abvm_scripts.contains(sc)));
1747 assert!(dotbelow_scripts.iter().any(|sc| !abvm_scripts.contains(sc)));
1748
1749 let _ = MarksInput::default()
1750 .set_user_fea(
1752 r#"
1753 languagesystem DFLT dflt;
1754 languagesystem latn dflt;
1755 "#,
1756 )
1757 .add_glyph("dotbelowcomb", None, |_| {})
1758 .compile_and_inspect(|builder| {
1759 let dotbelowcomb = builder.glyph_order.glyph_id("dotbelowcomb").unwrap();
1760 let (abvm, non_abvm) = builder.split_mark_and_abvm_blwm_glyphs().unwrap();
1761 assert!(!abvm.contains(dotbelowcomb));
1763 assert!(non_abvm.contains(dotbelowcomb));
1764 });
1765 }
1766
1767 #[test]
1768 fn abvm_closure_excludes_glyphs_with_common_script() {
1769 let uni25cc = char_for_glyph(&GlyphName::new("uni25CC")).unwrap();
1770 assert_eq!(
1771 super::super::properties::scripts_for_codepoint(uni25cc as _).collect::<Vec<_>>(),
1772 [super::super::properties::COMMON_SCRIPT]
1773 );
1774 let _ = MarksInput::default()
1775 .set_user_fea(
1776 "languagesystem latn dflt;
1777 languagesystem knda dflt;
1778
1779 feature derp {
1780 sub ka-kannada by uni25CC;
1781 } derp;
1782 ",
1783 )
1784 .add_glyph("ka-kannada", None, |_| {})
1785 .add_glyph("uni25CC", None, |_| {})
1786 .compile_and_inspect(|ctx| {
1787 let (abvm, non_abvm) = ctx.split_mark_and_abvm_blwm_glyphs().unwrap();
1788 let uni25cc = ctx.glyph_order.glyph_id("uni25CC").unwrap();
1791 let ka = ctx.glyph_order.glyph_id("ka-kannada").unwrap();
1792 assert!(!abvm.contains(uni25cc));
1793 assert!(abvm.contains(ka));
1794 assert!(non_abvm.contains(uni25cc));
1795 });
1796 }
1797
1798 #[test]
1800 fn multiple_anchor_classes_base() {
1801 let out = MarksInput::default()
1802 .add_glyph("a", None, |anchors| {
1803 anchors.add("topA", [(515, 581)]);
1804 })
1805 .add_glyph("e", None, |anchors| {
1806 anchors.add("topE", [(-21, 396)]);
1807 })
1808 .add_glyph("acutecomb", None, |anchors| {
1809 anchors
1810 .add("_topA", [(-175, 589)])
1811 .add("_topE", [(-175, 572)]);
1812 })
1813 .get_normalized_output();
1814
1815 assert_eq_ignoring_ws!(
1816 out,
1817 r#"
1818 # mark: DFLT/dflt
1819 # 2 MarkToBase rules
1820 # lookupflag LookupFlag(0)
1821 a @(x: 515, y: 581)
1822 @(x: -175, y: 589) acutecomb
1823 e @(x: -21, y: 396)
1824 @(x: -175, y: 572) acutecomb
1825 "#
1826 );
1827 }
1828
1829 #[test]
1831 fn ligature_null_anchor() {
1832 let out = MarksInput::default()
1833 .add_glyph("f_i_i", None, |anchors| {
1834 anchors
1835 .add("top_1", [(250, 600)])
1836 .add("top_2", [(500, 600)])
1837 .add("_3", [(0, 0)]);
1838 })
1839 .add_glyph("acutecomb", None, |anchors| {
1840 anchors.add("_top", [(100, 200)]);
1841 })
1842 .get_normalized_output();
1843
1844 assert_eq_ignoring_ws!(
1845 out,
1846 r#"
1847 # mark: DFLT/dflt
1848 # 1 MarkToLig rules
1849 # lookupflag LookupFlag(0)
1850 f_i_i (lig) [@(x: 250, y: 600), @(x: 500, y: 600), <NULL>]
1851 @(x: 100, y: 200) acutecomb
1852 "#
1853 );
1854 }
1855
1856 #[test]
1858 fn multiple_anchor_classes_liga() {
1859 let out = MarksInput::default()
1860 .add_glyph("f_i", None, |anchors| {
1861 anchors
1862 .add("top_1", [(100, 500)])
1863 .add("top_2", [(600, 500)]);
1864 })
1865 .add_glyph("f_f", None, |anchors| {
1866 anchors
1867 .add("topOther_1", [(101, 501)])
1868 .add("topOther_2", [(601, 501)]);
1869 })
1870 .add_glyph("f_l", None, |anchors| {
1871 anchors
1872 .add("top_1", [(102, 502)])
1873 .add("topOther_2", [(602, 502)]);
1874 })
1875 .add_glyph("acutecomb", None, |anchors| {
1876 anchors.add("_top", [(100, 200)]);
1877 anchors.add("_topOther", [(150, 250)]);
1878 })
1879 .get_normalized_output();
1880
1881 assert_eq_ignoring_ws!(
1884 out,
1885 r#"
1886 # mark: DFLT/dflt
1887 # 3 MarkToLig rules
1888 # lookupflag LookupFlag(0)
1889 f_f (lig) [@(x: 101, y: 501), @(x: 601, y: 501)]
1890 @(x: 150, y: 250) acutecomb
1891 f_i (lig) [@(x: 100, y: 500), @(x: 600, y: 500)]
1892 @(x: 100, y: 200) acutecomb
1893 f_l (lig) [<NULL>, @(x: 602, y: 502)]
1894 @(x: 150, y: 250) acutecomb
1895 "#
1896 );
1897 }
1898
1899 #[test]
1900 fn basic_lig_carets() {
1901 let out = MarksInput::default()
1902 .add_glyph("f_i", None, |anchors| {
1903 anchors.add("caret_1", [(100, 0)]);
1904 })
1905 .add_glyph("f_l", None, |anchors| {
1906 anchors.add("vcaret_1", [(0, -222)]);
1907 })
1908 .compile();
1909
1910 let gdef = out.gdef.as_ref().unwrap();
1911 let lig_carets = gdef.lig_caret_list.as_ref().unwrap();
1912 let ff = &lig_carets.lig_glyphs[0].caret_values;
1913 assert_eq!(ff.len(), 1);
1914 let caret = ff[0].as_ref();
1915 assert!(
1916 matches!(caret, RawCaretValue::Format1(t) if t.coordinate == 100),
1917 "{caret:?}"
1918 );
1919
1920 let f_l = &lig_carets.lig_glyphs[1].caret_values;
1921 assert_eq!(f_l.len(), 1);
1922 let caret = f_l[0].as_ref();
1923 assert!(
1924 matches!(caret, RawCaretValue::Format1(t) if t.coordinate == -222),
1925 "{caret:?}"
1926 );
1927 }
1928
1929 #[test]
1930 fn lig_caret_nop_deltas() {
1931 use write_fonts::read::tables::gdef as rgdef;
1932 let out = MarksInput::new([&[-1.0], &[0.0], &[1.0]])
1934 .add_glyph("f_f", None, |anchors| {
1935 anchors.add("caret_1", [(10.0, 0.0), (10., 0.0), (10., 0.)]);
1936 })
1937 .compile();
1938
1939 let gdef_bytes = write_fonts::dump_table(&out.gdef.unwrap()).unwrap();
1940 let gdef = rgdef::Gdef::read(gdef_bytes.as_slice().into()).unwrap();
1941 let lig_carets = gdef.lig_caret_list().unwrap().unwrap();
1942 let ff = lig_carets.lig_glyphs().get(0).unwrap();
1943 let rgdef::CaretValue::Format1(caret) = ff.caret_values().get(0).unwrap() else {
1944 panic!("wrong caret format");
1945 };
1946 assert_eq!(caret.coordinate(), 10)
1947 }
1948
1949 #[test]
1950 fn lig_caret_rounding() {
1951 use write_fonts::read::tables::gdef as rgdef;
1952 let out = MarksInput::new([&[-1.0], &[0.0], &[1.0]])
1954 .add_glyph("f_f", None, |anchors| {
1955 anchors.add("caret_1", [(239.0, 0.0), (270.5, 0.0), (304.5, 0.)]);
1956 })
1957 .compile();
1958
1959 let gdef_bytes = write_fonts::dump_table(&out.gdef.unwrap()).unwrap();
1962 let gdef = rgdef::Gdef::read(gdef_bytes.as_slice().into()).unwrap();
1963 let lig_carets = gdef.lig_caret_list().unwrap().unwrap();
1964 let varstore = gdef.item_var_store().unwrap().unwrap();
1965 let ivs_data = varstore.item_variation_data().get(0).unwrap().unwrap();
1966 let ff = lig_carets.lig_glyphs().get(0).unwrap();
1967 let caret = ff.caret_values().get(0).unwrap();
1968 let rgdef::CaretValue::Format3(caret) = caret else {
1969 panic!("expected variable caret!");
1970 };
1971 let default = caret.coordinate();
1972 let mut values = ivs_data
1973 .delta_set(0)
1974 .map(|delta| default + (delta as i16))
1975 .collect::<Vec<_>>();
1976 values.insert(1, default);
1977
1978 assert_eq!(values, [239, 271, 305]);
1979 }
1980
1981 #[test]
1982 fn mark_anchors_rounding() {
1983 let out = MarksInput::new([&[0.0], &[0.75], &[1.0]])
1989 .add_glyph("a", None, |anchors| {
1990 anchors.add("top", [(377.0, 451.0), (386.0, 450.0), (389.0, 450.0)]);
1991 })
1992 .add_glyph("ordfeminine", None, |anchors| {
1993 anchors.add("top", [(282.75, 691.25), (289.5, 689.5), (291.75, 689.5)]);
1994 })
1995 .add_glyph("acutecomb", None, |anchors| {
1996 anchors.add("_top", [(218.0, 704.0), (265.0, 707.0), (282.0, 708.0)]);
1997 })
1998 .get_normalized_output();
1999
2000 assert_eq_ignoring_ws!(
2001 out,
2002 r#"
2003 # mark: DFLT/dflt
2004 # 2 MarkToBase rules
2005 # lookupflag LookupFlag(0)
2006 a @(x: 377 {386,389}, y: 451 {450,450})
2007 @(x: 218 {265,282}, y: 704 {707,708}) acutecomb
2008 ordfeminine @(x: 283 {290,292}, y: 691 {690,690})
2009 @(x: 218 {265,282}, y: 704 {707,708}) acutecomb
2010 "#
2011 );
2012 }
2013
2014 #[test]
2015 fn cursive_rtl_and_ltr() {
2016 let out = MarksInput::default()
2017 .add_glyph("a", None, |anchors| {
2018 anchors.add("entry", [(10., 10.)]).add("exit", [(10., 60.)]);
2019 })
2020 .add_glyph("hamza", None, |anchors| {
2021 anchors
2022 .add("entry", [(-11., 33.)])
2023 .add("exit", [(-11., 44.)]);
2024 })
2025 .get_normalized_output();
2026
2027 assert_eq_ignoring_ws!(
2028 out,
2029 r#"
2030 # curs: DFLT/dflt
2031 # 2 CursivePos rules
2032 # lookupflag LookupFlag(8)
2033 a
2034 entry: @(x: 10, y: 10)
2035 exit: @(x: 10, y: 60)
2036 # lookupflag LookupFlag(9)
2037 hamza
2038 entry: @(x: -11, y: 33)
2039 exit: @(x: -11, y: 44)
2040 "#
2041 );
2042 }
2043}