1use gf_metadata::{AxisProto, FallbackProto};
2use std::{collections::HashSet, ops::Index};
3
4#[cfg(feature = "fontations")]
5use fontations::skrifa::string::StringId;
6#[cfg(feature = "fontations")]
7use fontations::{
8 read::FontRef,
9 read::{ReadError, TableProvider},
10 write::FontBuilder,
11};
12use indexmap::IndexMap;
13
14include!(concat!(env!("OUT_DIR"), "/data.rs"));
15
16const LINKED_VALUES: [(&str, (f32, f32)); 2] = [("wght", (400.0, 700.0)), ("ital", (0.0, 1.0))];
17
18fn linked_value(axis: &str, value: f32) -> Option<f32> {
19 LINKED_VALUES
20 .iter()
21 .find(|(linked_axis, (cur, _link))| *linked_axis == axis && value == *cur)
22 .map(|(_, (_, link))| *link)
23}
24
25const GF_STATIC_STYLES: [(&str, u16); 18] = [
26 ("Thin", 100),
27 ("ExtraLight", 200),
28 ("Light", 300),
29 ("Regular", 400),
30 ("Medium", 500),
31 ("SemiBold", 600),
32 ("Bold", 700),
33 ("ExtraBold", 800),
34 ("Black", 900),
35 ("Thin Italic", 100),
36 ("ExtraLight Italic", 200),
37 ("Light Italic", 300),
38 ("Italic", 400),
39 ("Medium Italic", 500),
40 ("SemiBold Italic", 600),
41 ("Bold Italic", 700),
42 ("ExtraBold Italic", 800),
43 ("Black Italic", 900),
44];
45
46#[cfg(feature = "fontations")]
47const PROTECTED_IDS: [StringId; 9] = [
48 StringId::FAMILY_NAME,
49 StringId::SUBFAMILY_NAME,
50 StringId::UNIQUE_ID,
51 StringId::FULL_NAME,
52 StringId::VERSION_STRING,
53 StringId::POSTSCRIPT_NAME,
54 StringId::TYPOGRAPHIC_FAMILY_NAME,
55 StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
56 StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
57];
58
59pub struct AxisRegistry {
60 axes: BTreeMap<String, Box<AxisProto>>,
61}
62
63#[derive(Debug, Clone)]
64pub struct FontAxis {
65 pub tag: String,
66 pub min: f32,
67 pub max: f32,
68 pub default: f32,
69}
70
71#[derive(Debug, Clone)]
72pub struct NameParticle {
73 pub name: Option<String>,
74 pub value: f32,
75 pub elided: bool,
76}
77
78impl AxisRegistry {
79 pub fn new() -> Self {
80 Self {
81 axes: (*AXES).clone(),
82 }
83 }
84
85 pub fn get(&self, tag: &str) -> Option<&AxisProto> {
86 self.axes.get(tag).map(|v| &**v)
87 }
88
89 pub fn contains_key(&self, tag: &str) -> bool {
90 self.axes.contains_key(tag)
91 }
92
93 pub fn iter(&self) -> impl Iterator<Item = (&String, &AxisProto)> {
94 self.axes.iter().map(|(k, v)| (k, &**v))
95 }
96
97 pub fn get_fallback<'a>(&'a self, name: &str) -> Option<(&'a str, &'a FallbackProto)> {
98 self.axes
99 .iter()
100 .flat_map(|(tag, axis)| {
101 let fallback = axis
102 .fallback
103 .iter()
104 .find(|f| f.name.as_deref() == Some(name));
105 fallback.map(|f| (tag.as_str(), f))
106 })
107 .next()
108 }
109
110 pub fn fallbacks<'a>(
112 &'a self,
113 font_axes: &'a [FontAxis],
114 ) -> impl Iterator<Item = (String, Vec<FallbackProto>)> + 'a {
115 font_axes.iter().filter_map(|axis| {
116 self.get(&axis.tag).map(|registry_axis| {
117 (
118 registry_axis.tag.clone().unwrap_or_default(),
119 registry_axis
120 .fallback
121 .iter()
122 .filter(|f| f.value() >= axis.min && f.value() <= axis.max)
123 .cloned()
124 .collect(),
125 )
126 })
127 })
128 }
129
130 pub fn name_table_fallbacks<'a>(
132 &'a self,
133 family_name: &'a str,
134 subfamily_name: &'a str,
135 font_axes: &'a [FontAxis],
136 ) -> impl Iterator<Item = (&'a str, &'a FallbackProto)> + 'a {
137 let axis_names: HashSet<&str> = font_axes.iter().map(|axis| axis.tag.as_ref()).collect();
138 let tokens = family_name
139 .split_whitespace()
140 .skip(1)
141 .chain(subfamily_name.split_whitespace());
142 tokens
143 .flat_map(|token| self.get_fallback(token))
144 .filter(move |(tag, _)| !axis_names.contains(tag))
145 }
146
147 pub fn fallback_for_value<'a>(
148 &'a self,
149 axis_tag: &str,
150 value: f32,
151 ) -> Option<&'a FallbackProto> {
152 self.get(axis_tag)
153 .and_then(|axis| axis.fallback.iter().find(|f| f.value == Some(value)))
154 }
155
156 pub fn axis_order(&self) -> Vec<&str> {
157 let mut axis_tags: Vec<&str> = self
158 .axes
159 .keys()
160 .filter(|k| k.chars().all(|c| c.is_ascii_uppercase()))
161 .map(|k| k.as_str())
162 .collect();
163 axis_tags.sort();
164 axis_tags.extend(vec!["opsz", "wdth", "wght", "ital", "slnt"]);
165 axis_tags
166 }
167
168 pub fn name_particles<'a>(&self, font_axes: &'a [FontAxis]) -> IndexMap<&'a str, NameParticle> {
170 let mut particles = IndexMap::new();
171 for axis in font_axes {
172 if axis.tag == "opsz" {
173 particles.insert(
174 "opsz",
175 NameParticle {
176 name: Some(format!("{}pt", axis.default)),
177 value: axis.default,
178 elided: true,
179 },
180 );
181 } else if let Some(fallback) = self.fallback_for_value(&axis.tag, axis.default) {
182 particles.insert(
183 &axis.tag,
184 NameParticle {
185 name: fallback.name.clone(),
186 value: axis.default,
187 elided: (fallback.value() == self.get(&axis.tag).unwrap().default_value())
188 && !(["Regular", "Italic", "14pt"].contains(&fallback.name())),
189 },
190 );
191 } else {
192 particles.insert(
193 &axis.tag,
194 NameParticle {
195 name: None,
196 value: axis.default,
197 elided: true,
198 },
199 );
200 };
201 }
202 particles
203 }
204}
205
206impl Default for AxisRegistry {
207 fn default() -> Self {
208 Self::new()
209 }
210}
211
212impl Index<&str> for AxisRegistry {
213 type Output = AxisProto;
214
215 fn index(&self, tag: &str) -> &Self::Output {
216 self.get(tag).expect("No such axis")
217 }
218}
219
220#[cfg(feature = "fontations")]
221mod monkeypatching;
222#[cfg(feature = "fontations")]
223mod nametable;
224#[cfg(feature = "fontations")]
225mod stat;
226#[cfg(feature = "fontations")]
227mod fontations_impl {
228 use super::*;
229 use fontations::{
230 skrifa::{string::StringId, MetadataProvider, Tag},
231 write::{
232 from_obj::ToOwnedTable,
233 tables::{
234 fvar::{Fvar, InstanceRecord},
235 name::{Name, NameRecord},
236 os2::Os2,
237 stat::Stat,
238 },
239 types::Fixed,
240 },
241 };
242 use monkeypatching::{AxisValueNameId, SetAxisValueNameId};
243 use nametable::{
244 add_name, best_familyname, best_subfamilyname, find_or_add_name, rewrite_or_insert,
245 };
246 use stat::{AxisLocation, AxisRecord, AxisValue, StatBuilder};
247 use std::{cmp::Reverse, collections::HashMap};
248
249 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
250 pub enum RenameAggressiveness {
251 #[default]
252 Aggressive,
253 Conservative,
254 }
255
256 pub fn build_name_table(
257 font: FontRef,
258 family_name: Option<&str>,
259 style_name: Option<&str>,
260 siblings: &[FontRef],
261 aggressive: Option<RenameAggressiveness>,
262 ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
263 let mut new_font = FontBuilder::new();
264 let family_name = family_name
265 .map(|x| x.to_string())
266 .unwrap_or_else(|| best_familyname(&font).unwrap_or("Unknown".to_string()));
267 let style_name = style_name
268 .map(|x| x.to_string())
269 .unwrap_or_else(|| best_subfamilyname(&font).unwrap_or("Regular".to_string()));
270
271 let mut new_name = if font.table_data(Tag::new(b"fvar")).is_some() {
272 build_vf_name_table(&mut new_font, &font, &family_name, siblings, aggressive)?
273 } else {
274 build_static_name_table_v1(&mut new_font, &font, &family_name, &style_name, aggressive)?
275 };
276
277 let mut styles: Vec<_> = GF_STATIC_STYLES.iter().collect();
278 styles.sort_by_key(|(name, _weight)| Reverse(name.len()));
279 for (name, weight) in styles.iter() {
280 if style_name.contains(name) {
281 let mut new_os2: Os2 = font.os2()?.to_owned_table();
282 new_os2.us_weight_class = *weight;
283 new_font.add_table(&new_os2)?;
284 break;
285 }
286 }
287 new_name.name_record.sort();
289 new_font.add_table(&new_name)?;
290 Ok(new_font.copy_missing_tables(font).build())
291 }
292
293 fn fvar_instance_collisions(font: &FontRef, siblings: &[FontRef]) -> bool {
294 let fonts = siblings.iter().chain(std::iter::once(font));
295 let is_italic = fonts
296 .map(|f| {
297 f.post()
298 .map(|post| post.italic_angle().abs() != Fixed::from_f64(0.0))
299 .unwrap_or(false)
300 })
301 .collect::<Vec<_>>();
302 is_italic.len() != is_italic.iter().collect::<HashSet<_>>().len()
303 }
304
305 fn build_vf_name_table(
306 newfont: &mut FontBuilder,
307 font: &FontRef,
308 family_name: &str,
309 siblings: &[FontRef],
310 aggressive: Option<RenameAggressiveness>,
311 ) -> Result<Name, Box<dyn std::error::Error>> {
312 let style_name = vf_style_name(font, family_name)?;
313 let mut new_name: Name = (if fvar_instance_collisions(font, siblings) {
314 build_static_name_table_v1(newfont, font, family_name, &style_name, aggressive)
315 } else {
316 build_static_name_table(newfont, font, family_name, style_name, aggressive)
317 })?;
318 build_variations_ps_name(&mut new_name, font, Some(family_name));
320
321 new_name.name_record.sort();
323 Ok(new_name)
324 }
325
326 pub fn build_variations_ps_name(newname: &mut Name, font: &FontRef, family_name: Option<&str>) {
327 let fallback = best_familyname(font);
328 let family_name = family_name.or(fallback.as_deref()).unwrap_or("New Font");
329 let subfamily_name = best_subfamilyname(font).unwrap_or("Regular".to_string());
330 let font_axes = font_axes(font).unwrap_or_default();
331 let registry = AxisRegistry::new();
332 let font_styles = registry.name_table_fallbacks(family_name, &subfamily_name, &font_axes);
333 let mut var_ps = family_name.replace(" ", "");
334 for (_, fallback) in font_styles {
335 let fallback_name = fallback.name();
336 if !var_ps.contains(fallback_name) {
337 var_ps.push_str(fallback_name);
338 }
339 }
340 rewrite_or_insert(
341 &mut newname.name_record,
342 StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
343 &var_ps,
344 );
345 }
346
347 fn build_static_name_table_v1(
348 newfont: &mut FontBuilder,
349 font: &FontRef,
350 family_name: &str,
351 style_name: &str,
352 aggressive: Option<RenameAggressiveness>,
353 ) -> Result<Name, Box<dyn std::error::Error>> {
354 let (v1_tokens, non_weight) = style_name
355 .split_whitespace()
356 .partition::<Vec<_>, _>(|token| GF_STATIC_STYLES.iter().any(|(name, _)| name == token));
357 let family_tokens = family_name.split_whitespace();
358 let mut new_family_name = vec![];
359 for token in family_tokens {
360 if non_weight.contains(&token) || new_family_name.contains(&token) {
361 continue;
362 }
363 new_family_name.push(token);
364 }
365 new_family_name.extend(non_weight);
366 let family_name = new_family_name.join(" ");
367 let mut style_name = v1_tokens
368 .join(" ")
369 .replace("Regular Italic", "Italic")
370 .trim()
371 .to_string();
372 if style_name.is_empty() {
373 style_name = "Regular".to_string();
374 }
375 build_static_name_table(newfont, font, &family_name, style_name, aggressive)
376 }
377
378 fn build_static_name_table(
379 newfont: &mut FontBuilder,
380 font: &FontRef,
381 family_name: &str,
382 style_name: String,
383 aggressive: Option<RenameAggressiveness>,
384 ) -> Result<Name, Box<dyn std::error::Error>> {
385 let mut name: Name = font.name()?.to_owned_table();
386 let mut records = name.name_record.into_iter().collect::<Vec<NameRecord>>();
387 records.retain(|record| record.platform_id != 1);
388 let existing_name = best_familyname(font).unwrap_or("New Font".to_string());
389 let mut removed_names: HashMap<StringId, String> = HashMap::new();
390 let full_name = family_name.to_string() + " " + &style_name;
391 let ps_name = (family_name.to_string() + "-" + &style_name).replace(" ", "");
392 let removeable_name_ids =
393 if ["Italic", "Bold Italic", "Bold", "Regular"].contains(&style_name.as_str()) {
394 rewrite_or_insert(&mut records, StringId::FAMILY_NAME, family_name);
395 rewrite_or_insert(&mut records, StringId::SUBFAMILY_NAME, &style_name);
396 rewrite_or_insert(&mut records, StringId::FULL_NAME, &full_name);
397 rewrite_or_insert(&mut records, StringId::POSTSCRIPT_NAME, &ps_name);
398 vec![
399 StringId::TYPOGRAPHIC_FAMILY_NAME,
400 StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
401 StringId::new(21),
402 StringId::new(22),
403 ]
404 } else {
405 let style_tokens = style_name.split_whitespace().collect::<Vec<_>>();
406 let mut new_family_name_tokens = family_name.split_whitespace().collect::<Vec<_>>();
407 let is_italic = style_tokens.contains(&"Italic");
408 let additional_tokens = (style_tokens.into_iter().filter(|token| {
409 !(*token == "Regular"
410 || *token == "Italic"
411 || new_family_name_tokens.contains(token))
412 }))
413 .collect::<Vec<_>>();
414 new_family_name_tokens.extend(additional_tokens);
415 let new_family_name = new_family_name_tokens.join(" ");
416 let new_style_name = if is_italic { "Italic" } else { "Regular" };
417
418 rewrite_or_insert(&mut records, StringId::FAMILY_NAME, &new_family_name);
419 rewrite_or_insert(&mut records, StringId::SUBFAMILY_NAME, new_style_name);
420 rewrite_or_insert(&mut records, StringId::FULL_NAME, &full_name);
421 rewrite_or_insert(&mut records, StringId::POSTSCRIPT_NAME, &ps_name);
422 rewrite_or_insert(&mut records, StringId::TYPOGRAPHIC_FAMILY_NAME, family_name);
423 rewrite_or_insert(
424 &mut records,
425 StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
426 &style_name,
427 );
428
429 vec![StringId::new(21), StringId::new(22)]
430 };
431
432 let mut to_delete = vec![];
433 for name_id in removeable_name_ids.into_iter() {
434 if let Some(existing) = records.iter_mut().position(|r| {
435 r.name_id == name_id
436 && r.platform_id == 3
437 && r.encoding_id == 1
438 && r.language_id == 0x409
439 }) {
440 removed_names.insert(name_id, records[existing].string.to_string());
441 to_delete.push(existing);
442 }
443 }
444 for i in to_delete.into_iter().rev() {
445 records.remove(i);
446 }
447
448 if !removed_names.is_empty() && font.table_data(Tag::from_be_bytes(*b"STAT")).is_some() {
450 let mut stat: Stat = font.stat()?.to_owned_table();
451 for axis in stat.design_axes.iter_mut() {
452 let id = axis.axis_name_id;
453 if let Some(old_name) = removed_names.get(&id) {
454 axis.axis_name_id = find_or_add_name(&mut records, old_name);
455 }
456 }
457 if let Some(axis_values) = stat.offset_to_axis_values.as_deref_mut() {
459 for axis_value in axis_values.iter_mut() {
460 if let Some(name_id) = axis_value.value_name_id() {
461 if let Some(old_name) = removed_names.get(&name_id) {
462 axis_value.set_value_name_id(find_or_add_name(&mut records, old_name));
463 }
464 }
465 }
466 }
467 newfont.add_table(&stat)?;
468 }
469
470 if let Some(existing) = records.iter_mut().find(|r| {
471 r.name_id == StringId::UNIQUE_ID
472 && r.platform_id == 3
473 && r.encoding_id == 1
474 && r.language_id == 0x409
475 }) {
476 if let Some(new_unique) = new_unique_id(font, &full_name, &ps_name, &existing.string) {
477 *existing.string = new_unique;
478 }
479 }
480 if aggressive.unwrap_or_default() == RenameAggressiveness::Aggressive {
481 for record in records.iter_mut() {
482 if PROTECTED_IDS.contains(&record.name_id)
483 || !record.string.contains(&existing_name)
484 {
485 continue;
486 }
487 if !record.string.contains(' ') {
488 *record.string = record
489 .string
490 .replace(&existing_name, family_name)
491 .replace(" ", "");
492 } else {
493 *record.string = record.string.replace(&existing_name, family_name);
494 }
495 }
496 }
497 name.name_record = records;
498 name.name_record.sort();
499 Ok(name)
500 }
501
502 fn new_unique_id(
503 font: &FontRef<'_>,
504 full_name: &str,
505 ps_name: &str,
506 existing: &str,
507 ) -> Option<String> {
508 let new = existing.to_string();
509 if let Some(existing_full_name) = font
510 .localized_strings(StringId::FULL_NAME)
511 .english_or_first()
512 {
513 let existing_full_name = existing_full_name.chars().collect::<String>();
514 if new.contains(&existing_full_name) {
515 return Some(new.replace(&existing_full_name, full_name));
516 }
517 }
518 if let Some(existing_ps_name) = font
519 .localized_strings(StringId::POSTSCRIPT_NAME)
520 .english_or_first()
521 {
522 let existing_ps_name = existing_ps_name.chars().collect::<String>();
523 if new.contains(&existing_ps_name) {
524 return Some(new.replace(&existing_ps_name, ps_name));
525 }
526 }
527 None
528 }
529
530 fn font_axes(font: &FontRef) -> Result<Vec<FontAxis>, ReadError> {
531 let fvar = font.fvar().unwrap();
532 let mut axes = vec![];
533 for axis in fvar.axes()? {
534 let tag = axis.axis_tag().to_string();
535 let min = axis.min_value().to_f32();
536 let max = axis.max_value().to_f32();
537 let default = axis.default_value().to_f32();
538 axes.push(FontAxis {
539 tag,
540 min,
541 max,
542 default,
543 });
544 }
545 Ok(axes)
546 }
547
548 fn vf_style_name(font: &FontRef, family_name: &str) -> Result<String, ReadError> {
549 let axisregistry = AxisRegistry::new();
550 let axes: Vec<_> = font_axes(font)?;
551 let fvar_dflts = axisregistry.name_particles(&axes);
552 let mut relevant_particles: Vec<String> = axisregistry
553 .axis_order()
554 .iter()
555 .flat_map(|tag| fvar_dflts.get(tag))
556 .filter(|particle| !particle.elided)
557 .flat_map(|particle| particle.name.clone())
558 .collect::<Vec<_>>();
559 let family_name_tokens = family_name.split_whitespace().collect::<HashSet<_>>();
560 let subfamily_name = best_subfamilyname(font);
561 let font_styles = axisregistry
562 .name_table_fallbacks(
563 family_name,
564 subfamily_name.as_deref().unwrap_or("Regular"),
565 &axes,
566 )
567 .map(|(_tag, proto)| proto.name())
568 .filter(|name| !family_name_tokens.contains(name))
569 .map(|name| name.to_string())
570 .filter(|name| !relevant_particles.contains(name))
571 .collect::<Vec<_>>();
572 relevant_particles.extend(font_styles);
573 let name = relevant_particles
574 .join(" ")
575 .replace("Regular Italic", "Italic");
576 Ok(name)
577 }
578
579 pub fn build_fvar_instances(
582 font: FontRef,
583 axis_dflts: Option<HashMap<String, f32>>,
584 ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
585 let axis_registry = AxisRegistry::new();
586 let mut new_font = FontBuilder::new();
587 let mut fvar: Fvar = font.fvar().unwrap().to_owned_table();
588 let mut name_table: Name = font.name().unwrap().to_owned_table();
589 let family_name = best_familyname(&font).unwrap_or("New Font".to_string());
590 let style_name = best_subfamilyname(&font).unwrap_or("Regular".to_string());
591 let mut stat_name_ids = HashSet::new();
593 if let Ok(stat) = font.stat() {
594 for axis in stat.design_axes()?.iter() {
595 stat_name_ids.insert(axis.axis_name_id());
596 }
597 if let Some(axis_values_offset) = stat.offset_to_axis_values().transpose()? {
598 for axis_value in axis_values_offset.axis_values().iter().flatten() {
599 stat_name_ids.insert(axis_value.value_name_id());
600 }
601 }
602 }
603
604 for instance in fvar.axis_instance_arrays.instances.iter() {
606 if instance.subfamily_name_id != StringId::SUBFAMILY_NAME
607 && instance.subfamily_name_id != StringId::TYPOGRAPHIC_SUBFAMILY_NAME
608 && !stat_name_ids.contains(&instance.subfamily_name_id)
609 {
610 name_table
611 .name_record
612 .retain(|record| record.name_id != instance.subfamily_name_id);
613 }
614 if let Some(psname) = instance.post_script_name_id {
615 if psname != StringId::POSTSCRIPT_NAME {
616 name_table
617 .name_record
618 .retain(|record| record.name_id != psname);
619 }
620 }
621 }
622
623 let axes = font_axes(&font)?;
624 let fvar_defaults = axis_registry.name_particles(&axes);
625
626 let axis_dflts = axis_dflts.unwrap_or_else(|| {
627 fvar_defaults
628 .iter()
629 .map(|(tag, particle)| (tag.to_string(), particle.value))
630 .collect()
631 });
632
633 let is_italic = style_name.contains("Italic");
634 let is_roman_and_italic =
635 fvar_defaults.contains_key("ital") || fvar_defaults.contains_key("slnt");
636
637 let mut fallbacks: HashMap<String, Vec<FallbackProto>> =
638 axis_registry.fallbacks(&axes).collect();
639 if !fvar_defaults.contains_key("wght") {
640 fallbacks.insert(
641 "wght".to_string(),
642 axis_registry
643 .get("wght")
644 .unwrap()
645 .fallback
646 .iter()
647 .filter(|f| f.value == Some(400.0))
648 .take(1)
649 .cloned()
650 .collect(),
651 );
652 }
653 let wght_fallbacks = fallbacks.get("wght").ok_or("No wght fallbacks")?;
654 let min_ital: Option<f32> = axes
655 .iter()
656 .filter(|axis| axis.tag == "ital")
657 .map(|axis| axis.min)
658 .next();
659 let min_slnt: Option<f32> = axes
660 .iter()
661 .filter(|axis| axis.tag == "ital")
662 .map(|axis| axis.min)
663 .next();
664
665 let mut instances = vec![];
666 let do_italic = if is_roman_and_italic {
667 vec![false, true]
668 } else if is_italic {
669 vec![true]
670 } else {
671 vec![false]
672 };
673 for italic in do_italic.into_iter() {
674 for fallback in wght_fallbacks.iter() {
675 let mut name = fallback.name.as_ref().unwrap().to_string();
676 if italic {
677 name += " Italic";
678 }
679 name = name.replace("Regular Italic", "Italic");
680 let mut coordinates = axis_dflts.clone();
681 if fvar_defaults.contains_key("wght") {
682 coordinates.insert("wght".to_string(), fallback.value.unwrap());
683 }
684 if italic {
685 if let Some(min) = min_ital {
686 coordinates.insert("ital".to_string(), min);
687 } else if let Some(min) = min_slnt {
688 coordinates.insert("slnt".to_string(), min);
689 }
690 }
691 let subfamily_name_id = add_name(&mut name_table.name_record, &name);
692 let post_script_name_id = add_name(
693 &mut name_table.name_record,
694 &format!("{}-{}", family_name, name).replace(" ", ""),
695 );
696 let coordinates = axes
697 .iter()
698 .map(|axis| coordinates.get(&axis.tag).cloned().unwrap_or(axis.default))
699 .map(|val| Fixed::from_f64(val as f64))
700 .collect();
701 instances.push(InstanceRecord {
702 subfamily_name_id,
703 flags: 0,
704 coordinates,
705 post_script_name_id: Some(post_script_name_id),
706 })
707 }
708 }
709
710 fvar.axis_instance_arrays.instances = instances;
711
712 new_font.add_table(&fvar)?;
713 name_table.name_record.sort();
714 new_font.add_table(&name_table)?;
715 Ok(new_font.copy_missing_tables(font).build())
716 }
717
718 pub fn build_stat(
720 font: FontRef,
721 siblings: &[FontRef],
722 ) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
723 let mut new_font = FontBuilder::new();
724 let axes = font_axes(&font)?;
725 let axis_registry = AxisRegistry::new();
726 let fallbacks_in_fvar: IndexMap<String, Vec<FallbackProto>> =
727 axis_registry.fallbacks(&axes).collect();
728
729 let mut fallbacks_in_siblings: Vec<(String, FallbackProto)> = vec![];
730 for fnt in siblings {
731 let family_name = best_familyname(fnt).unwrap_or("New Font".to_string());
732 let subfamily_name = best_subfamilyname(fnt).unwrap_or("Regular".to_string());
733 let font_axes = font_axes(fnt).unwrap_or_default();
734 fallbacks_in_siblings.extend(
735 axis_registry
736 .name_table_fallbacks(&family_name, &subfamily_name, &font_axes)
737 .map(|(tag, proto)| (tag.to_string(), proto.clone())),
738 )
739 }
740 let family_name = best_familyname(&font).unwrap_or("New Font".to_string());
742 let subfamily_name = best_subfamilyname(&font).unwrap_or("Regular".to_string());
743 let fallbacks_in_names =
744 axis_registry.name_table_fallbacks(&family_name, &subfamily_name, &axes);
745
746 let fvar: Fvar = font.fvar().unwrap().to_owned_table();
747 let mut name: Name = font.name().unwrap().to_owned_table();
748 let fvar_name_ids: HashSet<StringId> = fvar
749 .axis_instance_arrays
750 .instances
751 .iter()
752 .map(|x| x.subfamily_name_id)
753 .chain(
754 fvar.axis_instance_arrays
755 .axes
756 .iter()
757 .map(|x| x.axis_name_id),
758 )
759 .collect();
760 let keep = |name_id: StringId| -> bool {
761 name_id.to_u16() <= 25 || fvar_name_ids.contains(&name_id)
762 };
763 let mut delete_ids = vec![];
764 if let Ok(stat) = font.stat() {
765 for axis in stat.design_axes()?.iter() {
766 let id = axis.axis_name_id.get();
767 if !keep(id) {
768 delete_ids.push(id);
769 }
770 }
771 if let Some(axis_values) = stat.offset_to_axis_values().transpose()? {
772 for axis_value in axis_values.axis_values().iter().flatten() {
773 let id = axis_value.value_name_id();
774 if !keep(id) {
775 delete_ids.push(id);
776 }
777 }
778 }
779 }
780 name.name_record
781 .retain(|record| !delete_ids.contains(&record.name_id));
782 let mut axis_records: Vec<AxisRecord> = vec![];
783 let mut values: Vec<AxisValue> = vec![];
784 let mut seen_axes = HashSet::new();
785
786 fn make_location(axis: Tag, value: f32, linked_value: Option<f32>) -> AxisLocation {
787 if let Some(linked_value) = linked_value {
788 AxisLocation::Three {
789 tag: axis,
790 value: Fixed::from_f64(value as f64),
791 linked: Fixed::from_f64(linked_value as f64),
792 }
793 } else {
794 AxisLocation::One {
795 tag: axis,
796 value: Fixed::from_f64(value as f64),
797 }
798 }
799 }
800
801 for (axis, fallbacks) in fallbacks_in_fvar.iter() {
802 let tag = Tag::new_checked(&axis.as_bytes()[0..4])?;
803 let ar_axis = axis_registry.get(axis).unwrap();
804 seen_axes.insert(tag);
805 axis_records.push(AxisRecord {
806 tag,
807 name: ar_axis.display_name().to_string(),
808 ordering: 0,
809 });
810 let fallback_values = fallbacks.iter().map(|f| f.value()).collect::<Vec<f32>>();
811 for fallback in fallbacks.iter() {
812 values.push(AxisValue {
813 flags: if fallback.value() == ar_axis.default_value() {
814 0x2
815 } else {
816 0x0
817 },
818 name: fallback.name().to_string(),
819 location: make_location(
820 tag,
821 fallback.value(),
822 linked_value(axis, fallback.value())
823 .filter(|value| fallback_values.contains(value)),
824 ),
825 })
826 }
827 }
828
829 for (axis, fallback) in fallbacks_in_names {
830 let tag = Tag::new_checked(&axis.as_bytes()[0..4])?;
831 if seen_axes.contains(&tag) {
832 continue;
833 }
834 seen_axes.insert(tag);
836
837 let ar_axis = axis_registry.get(axis).unwrap();
838 axis_records.push(AxisRecord {
839 tag,
840 name: ar_axis.display_name().to_string(),
841 ordering: 0,
842 });
843 values.push(AxisValue {
844 flags: 0x0,
845 name: fallback.name().to_string(),
846 location: make_location(
847 tag,
848 fallback.value(),
849 linked_value(axis, fallback.value()),
850 ),
851 });
852 }
853
854 for (axis, _fallback) in fallbacks_in_siblings {
855 let tag = Tag::new_checked(&axis.as_bytes()[0..4])?;
856 if seen_axes.contains(&tag) {
857 continue;
858 }
859 seen_axes.insert(tag);
860
861 let ar_axis = axis_registry.get(&axis).unwrap();
863 let elided_value = ar_axis.default_value();
864 axis_records.push(AxisRecord {
865 tag,
866 name: ar_axis.display_name().to_string(),
867 ordering: 0,
868 });
869 if let Some(elided_fallback) = axis_registry.fallback_for_value(&axis, elided_value) {
870 values.push(AxisValue {
871 flags: 0x2,
872 name: elided_fallback.name().to_string(),
873 location: make_location(tag, elided_value, linked_value(&axis, elided_value)),
874 })
875 }
876 }
877 axis_records.iter_mut().enumerate().for_each(|(i, record)| {
878 record.ordering = i as u16;
879 });
880
881 let stat_builder = StatBuilder {
882 records: axis_records,
883 values,
884 };
885 let stat = stat_builder.build(&mut name.name_record);
886 name.name_record.sort();
887 new_font.add_table(&name)?;
888 new_font.add_table(&stat)?;
889 Ok(new_font.copy_missing_tables(font).build())
890 }
891
892 pub fn build_filename(font: FontRef, extension: &str) -> String {
893 let family_name = best_familyname(&font)
894 .unwrap_or("New Font".to_string())
895 .replace(" ", "");
896 let style_name = best_subfamilyname(&font).unwrap_or("Regular".to_string());
897 if font.table_data(Tag::new(b"fvar")).is_some() {
898 let is_italic = style_name.contains("Italic");
899 let axes = font_axes(&font).unwrap_or_default();
900 let mut axes = axes
902 .iter()
903 .map(|axis| axis.tag.to_string())
904 .collect::<Vec<_>>();
905 axes.sort();
906 let axes = axes.join(",");
907 return format!(
908 "{}{}[{}].{}",
909 family_name,
910 if is_italic { "-Italic" } else { "" },
911 axes,
912 extension
913 );
914 }
915 format!("{family_name}-{style_name}.{extension}").replace(" ", "")
916 }
917}
918
919#[cfg(feature = "fontations")]
920pub use fontations_impl::*;
921
922#[cfg(test)]
923mod tests {
924 use fontations::{
925 skrifa::{string::StringId, MetadataProvider, Tag},
926 write::{
927 from_obj::ToOwnedTable,
928 tables::{
929 name::Name,
930 stat::{AxisValue, Stat},
931 },
932 },
933 };
934 use pretty_assertions::assert_eq;
935
936 use super::*;
937
938 #[test]
939 fn opsz() {
940 let ar = AxisRegistry::new();
941 assert!(ar.contains_key("opsz"));
942 assert_eq!(ar["opsz"].display_name.as_deref(), Some("Optical Size"));
943 }
944 const MAVEN_PRO: &[u8; 83576] = include_bytes!("../tests/data/MavenPro-Regular.ttf");
945 const OPEN_SANS: &[u8; 532636] = include_bytes!("../tests/data/OpenSans[wdth,wght].ttf");
946 const OPEN_SANS_ITALIC: &[u8; 584112] =
947 include_bytes!("../tests/data/OpenSans-Italic[wdth,wght].ttf");
948 const OPEN_SANS_CONDENSED: &[u8; 973780] =
949 include_bytes!("../tests/data/OpenSansCondensed[wght].ttf");
950 const OPEN_SANS_CONDENSED_ITALIC: &[u8; 1054652] =
951 include_bytes!("../tests/data/OpenSansCondensed-Italic[wght].ttf");
952 const WONKY: &[u8; 532564] = include_bytes!("../tests/data/Wonky[wdth,wght].ttf");
953 const PLAYFAIR: &[u8; 1150824] = include_bytes!("../tests/data/Playfair[opsz,wdth,wght].ttf");
954
955 struct NameTableTestCase {
956 binary: &'static [u8],
957 family_name: &'static str,
958 subfamily_name: Option<&'static str>,
959 siblings: Vec<&'static [u8]>,
960 expectations: Vec<(StringId, Option<&'static str>)>,
961 }
962
963 #[test]
964 fn test_name_table() {
965 let cases: Vec<NameTableTestCase> = vec![
966 NameTableTestCase {
967 binary: MAVEN_PRO,
968 family_name: "Maven Pro",
969 subfamily_name: Some("Regular"),
970 siblings: vec![],
971 expectations: vec![
972 (StringId::FAMILY_NAME, Some("Maven Pro")),
973 (StringId::SUBFAMILY_NAME, Some("Regular")),
974 (StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-Regular")),
975 (StringId::FULL_NAME, Some("Maven Pro Regular")),
976 (StringId::POSTSCRIPT_NAME, Some("MavenPro-Regular")),
977 (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
978 (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
979 ],
980 },
981 NameTableTestCase {
982 binary: MAVEN_PRO,
983 family_name: "Maven Pro",
984 subfamily_name: Some("Italic"),
985 siblings: vec![],
986 expectations: vec![
987 (StringId::FAMILY_NAME, Some("Maven Pro")),
988 (StringId::SUBFAMILY_NAME, Some("Italic")),
989 (StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-Italic")),
990 (StringId::FULL_NAME, Some("Maven Pro Italic")),
991 (StringId::POSTSCRIPT_NAME, Some("MavenPro-Italic")),
992 (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
993 (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
994 ],
995 },
996 NameTableTestCase {
997 binary: MAVEN_PRO,
998 family_name: "Maven Pro",
999 subfamily_name: Some("Bold"),
1000 siblings: vec![],
1001 expectations: vec![
1002 (StringId::FAMILY_NAME, Some("Maven Pro")),
1003 (StringId::SUBFAMILY_NAME, Some("Bold")),
1004 (StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-Bold")),
1005 (StringId::FULL_NAME, Some("Maven Pro Bold")),
1006 (StringId::POSTSCRIPT_NAME, Some("MavenPro-Bold")),
1007 (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1008 (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1009 ],
1010 },
1011 NameTableTestCase {
1012 binary: MAVEN_PRO,
1013 family_name: "Maven Pro",
1014 subfamily_name: Some("Bold Italic"),
1015 siblings: vec![],
1016 expectations: vec![
1017 (StringId::FAMILY_NAME, Some("Maven Pro")),
1018 (StringId::SUBFAMILY_NAME, Some("Bold Italic")),
1019 (StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-BoldItalic")),
1020 (StringId::FULL_NAME, Some("Maven Pro Bold Italic")),
1021 (StringId::POSTSCRIPT_NAME, Some("MavenPro-BoldItalic")),
1022 (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1023 (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1024 ],
1025 },
1026 NameTableTestCase {
1027 binary: MAVEN_PRO,
1028 family_name: "Maven Pro",
1029 subfamily_name: Some("Black"),
1030 siblings: vec![],
1031 expectations: vec![
1032 (StringId::FAMILY_NAME, Some("Maven Pro Black")),
1033 (StringId::SUBFAMILY_NAME, Some("Regular")),
1034 (StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-Black")),
1035 (StringId::FULL_NAME, Some("Maven Pro Black")),
1036 (StringId::POSTSCRIPT_NAME, Some("MavenPro-Black")),
1037 (StringId::TYPOGRAPHIC_FAMILY_NAME, Some("Maven Pro")),
1038 (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, Some("Black")),
1039 ],
1040 },
1041 NameTableTestCase {
1042 binary: MAVEN_PRO,
1043 family_name: "Maven Pro",
1044 subfamily_name: Some("Black Italic"),
1045 siblings: vec![],
1046 expectations: vec![
1047 (StringId::FAMILY_NAME, Some("Maven Pro Black")),
1048 (StringId::SUBFAMILY_NAME, Some("Italic")),
1049 (StringId::UNIQUE_ID, Some("2.003;NONE;MavenPro-BlackItalic")),
1050 (StringId::FULL_NAME, Some("Maven Pro Black Italic")),
1051 (StringId::POSTSCRIPT_NAME, Some("MavenPro-BlackItalic")),
1052 (StringId::TYPOGRAPHIC_FAMILY_NAME, Some("Maven Pro")),
1053 (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, Some("Black Italic")),
1054 ],
1055 },
1056 NameTableTestCase {
1057 binary: MAVEN_PRO,
1058 family_name: "Maven Pro",
1059 subfamily_name: Some("ExtraLight Italic"),
1060 siblings: vec![],
1061 expectations: vec![
1062 (StringId::FAMILY_NAME, Some("Maven Pro ExtraLight")),
1063 (StringId::SUBFAMILY_NAME, Some("Italic")),
1064 (
1065 StringId::UNIQUE_ID,
1066 Some("2.003;NONE;MavenPro-ExtraLightItalic"),
1067 ),
1068 (StringId::FULL_NAME, Some("Maven Pro ExtraLight Italic")),
1069 (StringId::POSTSCRIPT_NAME, Some("MavenPro-ExtraLightItalic")),
1070 (StringId::TYPOGRAPHIC_FAMILY_NAME, Some("Maven Pro")),
1071 (
1072 StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
1073 Some("ExtraLight Italic"),
1074 ),
1075 ],
1076 },
1077 NameTableTestCase {
1078 binary: MAVEN_PRO,
1079 family_name: "Maven Pro",
1080 subfamily_name: Some("UltraExpanded Regular"),
1081 siblings: vec![],
1082 expectations: vec![
1083 (StringId::FAMILY_NAME, Some("Maven Pro UltraExpanded")),
1084 (StringId::SUBFAMILY_NAME, Some("Regular")),
1085 (
1086 StringId::UNIQUE_ID,
1087 Some("2.003;NONE;MavenProUltraExpanded-Regular"),
1088 ),
1089 (StringId::FULL_NAME, Some("Maven Pro UltraExpanded Regular")),
1090 (
1091 StringId::POSTSCRIPT_NAME,
1092 Some("MavenProUltraExpanded-Regular"),
1093 ),
1094 (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1095 (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1096 ],
1097 },
1098 NameTableTestCase {
1099 binary: MAVEN_PRO,
1100 family_name: "Maven Pro",
1101 subfamily_name: Some("Condensed ExtraLight Italic"),
1102 siblings: vec![],
1103 expectations: vec![
1104 (
1105 StringId::FAMILY_NAME,
1106 Some("Maven Pro Condensed ExtraLight"),
1107 ),
1108 (StringId::SUBFAMILY_NAME, Some("Italic")),
1109 (
1110 StringId::UNIQUE_ID,
1111 Some("2.003;NONE;MavenProCondensed-ExtraLightItalic"),
1112 ),
1113 (
1114 StringId::FULL_NAME,
1115 Some("Maven Pro Condensed ExtraLight Italic"),
1116 ),
1117 (
1118 StringId::POSTSCRIPT_NAME,
1119 Some("MavenProCondensed-ExtraLightItalic"),
1120 ),
1121 (
1122 StringId::TYPOGRAPHIC_FAMILY_NAME,
1123 Some("Maven Pro Condensed"),
1124 ),
1125 (
1126 StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
1127 Some("ExtraLight Italic"),
1128 ),
1129 ],
1130 },
1131 NameTableTestCase {
1132 binary: OPEN_SANS,
1133 family_name: "Open Sans",
1134 subfamily_name: None,
1135 siblings: vec![
1136 OPEN_SANS_ITALIC,
1137 OPEN_SANS_CONDENSED,
1138 OPEN_SANS_CONDENSED_ITALIC,
1139 ],
1140 expectations: vec![
1141 (StringId::FAMILY_NAME, Some("Open Sans")),
1142 (StringId::SUBFAMILY_NAME, Some("Regular")),
1143 (StringId::UNIQUE_ID, Some("3.000;GOOG;OpenSans-Regular")),
1144 (StringId::FULL_NAME, Some("Open Sans Regular")),
1145 (StringId::POSTSCRIPT_NAME, Some("OpenSans-Regular")),
1146 (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1147 (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1148 (
1149 StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
1150 Some("OpenSans"),
1151 ),
1152 ],
1153 },
1154 NameTableTestCase {
1155 binary: OPEN_SANS_ITALIC,
1156 family_name: "Open Sans",
1157 subfamily_name: None,
1158 siblings: vec![OPEN_SANS, OPEN_SANS_CONDENSED, OPEN_SANS_CONDENSED_ITALIC],
1159 expectations: vec![
1160 (StringId::FAMILY_NAME, Some("Open Sans")),
1161 (StringId::SUBFAMILY_NAME, Some("Italic")),
1162 (StringId::UNIQUE_ID, Some("3.000;GOOG;OpenSans-Italic")),
1163 (StringId::FULL_NAME, Some("Open Sans Italic")),
1164 (StringId::POSTSCRIPT_NAME, Some("OpenSans-Italic")),
1165 (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1166 (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1167 (
1168 StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
1169 Some("OpenSansItalic"),
1170 ),
1171 ],
1172 },
1173 NameTableTestCase {
1174 binary: OPEN_SANS_CONDENSED,
1175 family_name: "Open Sans Condensed",
1176 subfamily_name: None,
1177 siblings: vec![OPEN_SANS, OPEN_SANS_ITALIC, OPEN_SANS_CONDENSED_ITALIC],
1178 expectations: vec![
1179 (StringId::FAMILY_NAME, Some("Open Sans Condensed")),
1180 (StringId::SUBFAMILY_NAME, Some("Regular")),
1181 (
1182 StringId::UNIQUE_ID,
1183 Some("3.000;GOOG;OpenSansCondensed-Regular"),
1184 ),
1185 (StringId::FULL_NAME, Some("Open Sans Condensed Regular")),
1186 (StringId::POSTSCRIPT_NAME, Some("OpenSansCondensed-Regular")),
1187 (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1188 (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1189 (
1190 StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
1191 Some("OpenSansCondensed"),
1192 ),
1193 ],
1194 },
1195 NameTableTestCase {
1196 binary: OPEN_SANS_CONDENSED_ITALIC,
1197 family_name: "Open Sans Condensed",
1198 subfamily_name: None,
1199 siblings: vec![OPEN_SANS, OPEN_SANS_ITALIC, OPEN_SANS_CONDENSED],
1200 expectations: vec![
1201 (StringId::FAMILY_NAME, Some("Open Sans Condensed")),
1202 (StringId::SUBFAMILY_NAME, Some("Italic")),
1203 (
1204 StringId::UNIQUE_ID,
1205 Some("3.000;GOOG;OpenSansCondensed-Italic"),
1206 ),
1207 (StringId::FULL_NAME, Some("Open Sans Condensed Italic")),
1208 (StringId::POSTSCRIPT_NAME, Some("OpenSansCondensed-Italic")),
1209 (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1210 (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1211 (
1212 StringId::VARIATIONS_POSTSCRIPT_NAME_PREFIX,
1213 Some("OpenSansCondensedItalic"),
1214 ),
1215 ],
1216 },
1217 NameTableTestCase {
1219 binary: MAVEN_PRO,
1220 family_name: "Maven Pro",
1221 subfamily_name: Some("Fat"),
1222 siblings: vec![],
1223 expectations: vec![
1224 (StringId::FAMILY_NAME, Some("Maven Pro Fat")),
1225 (StringId::SUBFAMILY_NAME, Some("Regular")),
1226 (StringId::UNIQUE_ID, Some("2.003;NONE;MavenProFat-Regular")),
1227 (StringId::FULL_NAME, Some("Maven Pro Fat Regular")),
1228 (StringId::POSTSCRIPT_NAME, Some("MavenProFat-Regular")),
1229 (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1230 (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1231 ],
1232 },
1233 NameTableTestCase {
1234 binary: WONKY,
1235 family_name: "Wonky",
1236 subfamily_name: None,
1237 siblings: vec![],
1238 expectations: vec![
1239 (StringId::FAMILY_NAME, Some("Wonky")),
1240 (StringId::SUBFAMILY_NAME, Some("Regular")),
1241 (StringId::UNIQUE_ID, Some("3.000;GOOG;Wonky-Regular")),
1242 (StringId::FULL_NAME, Some("Wonky Regular")),
1243 (StringId::POSTSCRIPT_NAME, Some("Wonky-Regular")),
1244 (StringId::TYPOGRAPHIC_FAMILY_NAME, None),
1245 (StringId::TYPOGRAPHIC_SUBFAMILY_NAME, None),
1246 ],
1247 },
1248 NameTableTestCase {
1249 binary: PLAYFAIR,
1250 family_name: "Playfair",
1251 subfamily_name: None,
1252 siblings: vec![],
1253 expectations: vec![
1254 (StringId::FAMILY_NAME, Some("Playfair SemiExpanded Light")),
1255 (StringId::SUBFAMILY_NAME, Some("Regular")),
1256 (
1257 StringId::UNIQUE_ID,
1258 Some("2.000;FTH;Playfair-SemiExpandedLight"),
1259 ),
1260 (StringId::FULL_NAME, Some("Playfair SemiExpanded Light")),
1261 (
1262 StringId::POSTSCRIPT_NAME,
1263 Some("Playfair-SemiExpandedLight"),
1264 ),
1265 (StringId::TYPOGRAPHIC_FAMILY_NAME, Some("Playfair")),
1266 (
1267 StringId::TYPOGRAPHIC_SUBFAMILY_NAME,
1268 Some("SemiExpanded Light"),
1269 ),
1270 ],
1271 },
1272 ];
1273
1274 run_name_table_tests(&cases, None);
1275 }
1276
1277 fn run_name_table_tests(cases: &[NameTableTestCase], aggression: Option<RenameAggressiveness>) {
1278 for (ix, case) in cases.iter().enumerate() {
1279 let font = FontRef::new(case.binary).expect("Failed to read font");
1280 let siblings: Vec<FontRef> = case
1281 .siblings
1282 .iter()
1283 .map(|b| FontRef::new(b).unwrap())
1284 .collect();
1285 let result = build_name_table(
1286 font,
1287 Some(case.family_name),
1288 case.subfamily_name,
1289 &siblings,
1290 aggression,
1291 )
1292 .unwrap();
1293 let result_font = FontRef::new(&result).unwrap();
1294 for (id, expectation) in case.expectations.iter() {
1295 let record = result_font.localized_strings(*id).english_or_first();
1296 if let Some(expectation) = expectation {
1297 assert_eq!(
1298 record.unwrap().chars().collect::<String>(),
1299 *expectation,
1300 "Case {}, {}",
1301 ix + 1,
1302 id
1303 );
1304 } else {
1305 assert!(record.is_none());
1306 }
1307 }
1308 }
1309 }
1310
1311 #[test]
1312 fn test_name_table_aggression() {
1313 let cases = [
1314 NameTableTestCase {
1315 binary: MAVEN_PRO,
1316 family_name: "Raven Am",
1317 subfamily_name: Some("Regular"),
1318 siblings: vec![],
1319 expectations: vec![
1320 (StringId::COPYRIGHT_NOTICE, Some("Copyright 2011 The Raven Am Project Authors (http://www.vissol.co.uk/mavenpro/), with Reserved Font Name \"Raven Am\".")),
1321 (StringId::FULL_NAME, Some("Raven Am Regular")),
1322 ]
1323 }
1324 ];
1325 run_name_table_tests(&cases, Some(RenameAggressiveness::Aggressive));
1326 let cases = [
1327 NameTableTestCase {
1328 binary: MAVEN_PRO,
1329 family_name: "Raven Am",
1330 subfamily_name: Some("Regular"),
1331 siblings: vec![],
1332 expectations: vec![
1333 (StringId::COPYRIGHT_NOTICE, Some("Copyright 2011 The Maven Pro Project Authors (http://www.vissol.co.uk/mavenpro/), with Reserved Font Name \"Maven Pro\".")),
1334 (StringId::FULL_NAME, Some("Raven Am Regular")),
1335 ]
1336 }
1337 ];
1338 run_name_table_tests(&cases, Some(RenameAggressiveness::Conservative));
1339 }
1340
1341 #[derive(Debug, Clone, PartialEq)]
1342 struct DumpStatValue<'a> {
1343 axis: Tag,
1344 name: &'a str,
1345 value: f32,
1346 linked: Option<f32>,
1347 }
1348
1349 fn dump_stat_values<'a>(stat: &Stat, name: &'a Name) -> Vec<DumpStatValue<'a>> {
1350 let mut values = vec![];
1351 let tags = stat
1352 .design_axes
1353 .iter()
1354 .map(|a| a.axis_tag)
1355 .collect::<Vec<_>>();
1356 if let Some(axis_values) = stat.offset_to_axis_values.as_ref() {
1357 for axis_value in axis_values.iter() {
1358 match axis_value.as_ref() {
1359 AxisValue::Format1(v) => values.push(DumpStatValue {
1360 axis: tags[v.axis_index as usize],
1361 name: name
1362 .name_record
1363 .iter()
1364 .find(|record| record.name_id == v.value_name_id)
1365 .map(|record| record.string.as_str())
1366 .unwrap_or(""),
1367 value: v.value.to_f32(),
1368 linked: None,
1369 }),
1370 AxisValue::Format2(_) => {
1371 panic!("I didn't produce this")
1372 }
1373 AxisValue::Format3(v) => values.push(DumpStatValue {
1374 axis: tags[v.axis_index as usize],
1375 name: name
1376 .name_record
1377 .iter()
1378 .find(|record| record.name_id == v.value_name_id)
1379 .map(|record| record.string.as_str())
1380 .unwrap_or(""),
1381 value: v.value.to_f32(),
1382 linked: Some(v.linked_value.to_f32()),
1383 }),
1384 AxisValue::Format4(_) => {
1385 panic!("I didn't produce this")
1386 }
1387 }
1388 }
1389 }
1390 values
1391 }
1392
1393 fn value<'a>(axis: &str, name: &'a str, value: f32, linked: Option<f32>) -> DumpStatValue<'a> {
1394 DumpStatValue {
1395 axis: Tag::new_checked(axis.as_bytes()).unwrap(),
1396 name,
1397 value,
1398 linked,
1399 }
1400 }
1401
1402 #[test]
1403 fn test_build_stat() {
1404 let font_data = build_stat(
1405 FontRef::new(OPEN_SANS).unwrap(),
1406 &[
1407 FontRef::new(OPEN_SANS_ITALIC).unwrap(),
1408 FontRef::new(OPEN_SANS_CONDENSED).unwrap(),
1409 FontRef::new(OPEN_SANS_CONDENSED_ITALIC).unwrap(),
1410 ],
1411 )
1412 .unwrap();
1413 let new_font = FontRef::new(&font_data).unwrap();
1414 let new_stat: Stat = new_font.stat().unwrap().to_owned_table();
1415 let name: Name = new_font.name().unwrap().to_owned_table();
1416 assert_eq!(
1418 new_stat
1419 .design_axes
1420 .iter()
1421 .map(|a| a.axis_tag)
1422 .collect::<Vec<_>>(),
1423 vec![Tag::new(b"wght"), Tag::new(b"wdth"), Tag::new(b"ital")]
1424 );
1425 assert_eq!(
1426 dump_stat_values(&new_stat, &name),
1427 vec![
1428 value("wght", "Light", 300.0, None),
1429 value("wght", "Regular", 400.0, Some(700.0)),
1430 value("wght", "Medium", 500.0, None),
1431 value("wght", "SemiBold", 600.0, None),
1432 value("wght", "Bold", 700.0, None),
1433 value("wght", "ExtraBold", 800.0, None),
1434 value("wdth", "Condensed", 75.0, None),
1435 value("wdth", "SemiCondensed", 87.5, None),
1436 value("wdth", "Normal", 100.0, None),
1437 value("ital", "Roman", 0.0, Some(1.0)),
1438 ]
1439 )
1440 }
1441
1442 #[test]
1443 fn test_build_stat2() {
1444 let font_data = build_stat(
1445 FontRef::new(OPEN_SANS_ITALIC).unwrap(),
1446 &[
1447 FontRef::new(OPEN_SANS).unwrap(),
1448 FontRef::new(OPEN_SANS_CONDENSED).unwrap(),
1449 FontRef::new(OPEN_SANS_CONDENSED_ITALIC).unwrap(),
1450 ],
1451 )
1452 .unwrap();
1453 let new_font = FontRef::new(&font_data).unwrap();
1454 let new_stat: Stat = new_font.stat().unwrap().to_owned_table();
1455 let name: Name = new_font.name().unwrap().to_owned_table();
1456 assert_eq!(
1458 new_stat
1459 .design_axes
1460 .iter()
1461 .map(|a| a.axis_tag)
1462 .collect::<Vec<_>>(),
1463 vec![Tag::new(b"wght"), Tag::new(b"wdth"), Tag::new(b"ital")]
1464 );
1465 assert_eq!(
1466 dump_stat_values(&new_stat, &name),
1467 vec![
1468 value("wght", "Light", 300.0, None),
1469 value("wght", "Regular", 400.0, Some(700.0)),
1470 value("wght", "Medium", 500.0, None),
1471 value("wght", "SemiBold", 600.0, None),
1472 value("wght", "Bold", 700.0, None),
1473 value("wght", "ExtraBold", 800.0, None),
1474 value("wdth", "Condensed", 75.0, None),
1475 value("wdth", "SemiCondensed", 87.5, None),
1476 value("wdth", "Normal", 100.0, None),
1477 value("ital", "Italic", 1.0, None),
1478 ]
1479 )
1480 }
1481
1482 #[test]
1483 fn test_build_filename() {
1484 let maven_pro = FontRef::new(MAVEN_PRO).unwrap();
1485 assert_eq!(build_filename(maven_pro, "ttf"), "MavenPro-Regular.ttf");
1486 let open_sans = FontRef::new(OPEN_SANS).unwrap();
1487 assert_eq!(build_filename(open_sans, "ttf"), "OpenSans[wdth,wght].ttf");
1488 let open_sans_italic = FontRef::new(OPEN_SANS_ITALIC).unwrap();
1489 assert_eq!(
1490 build_filename(open_sans_italic, "ttf"),
1491 "OpenSans-Italic[wdth,wght].ttf"
1492 );
1493 let open_sans_condensed = FontRef::new(OPEN_SANS_CONDENSED).unwrap();
1494 assert_eq!(
1495 build_filename(open_sans_condensed, "ttf"),
1496 "OpenSansCondensed[wght].ttf"
1497 );
1498 let open_sans_condensed_italic = FontRef::new(OPEN_SANS_CONDENSED_ITALIC).unwrap();
1499 assert_eq!(
1500 build_filename(open_sans_condensed_italic, "ttf"),
1501 "OpenSansCondensed-Italic[wght].ttf"
1502 );
1503 let wonky = FontRef::new(WONKY).unwrap();
1504 assert_eq!(build_filename(wonky, "ttf"), "Wonky[wdth,wght].ttf");
1505 let playfair = FontRef::new(PLAYFAIR).unwrap();
1506 assert_eq!(
1507 build_filename(playfair, "ttf"),
1508 "Playfair[opsz,wdth,wght].ttf"
1509 );
1510 }
1511}