allsorts_subset_browser/
variations.rs

1//! Variable font instancing.
2
3#![deny(missing_docs)]
4
5use std::borrow::Cow;
6use std::convert::{TryFrom, TryInto};
7use std::fmt;
8use std::fmt::Write;
9use std::str::FromStr;
10
11use pathfinder_geometry::rect::RectI;
12use pathfinder_geometry::vector::vec2i;
13use rustc_hash::FxHashSet;
14
15use crate::binary::read::{ReadArrayCow, ReadScope};
16use crate::cff::cff2::CFF2;
17use crate::cff::CFFError;
18use crate::error::{ParseError, ReadWriteError, WriteError};
19use crate::post::PostTable;
20use crate::subset::FontBuilder;
21use crate::tables::glyf::{BoundingBox, GlyfRecord, GlyfTable, Glyph};
22use crate::tables::loca::LocaTable;
23use crate::tables::os2::{FsSelection, Os2};
24use crate::tables::variable_fonts::avar::AvarTable;
25use crate::tables::variable_fonts::cvar::CvarTable;
26use crate::tables::variable_fonts::fvar::FvarTable;
27use crate::tables::variable_fonts::gvar::GvarTable;
28use crate::tables::variable_fonts::hvar::HvarTable;
29use crate::tables::variable_fonts::mvar::MvarTable;
30use crate::tables::variable_fonts::stat::{ElidableName, StatTable};
31use crate::tables::variable_fonts::OwnedTuple;
32use crate::tables::{
33    owned, CvtTable, Fixed, FontTableProvider, HeadTable, HheaTable, HmtxTable, IndexToLocFormat,
34    LongHorMetric, MacStyle, MaxpTable, NameTable, CFF_MAGIC, TRUE_MAGIC,
35};
36use crate::tag;
37use crate::tag::DisplayTag;
38
39/// Error type returned from instancing a variable font.
40#[derive(Debug)]
41pub enum VariationError {
42    /// An error occurred reading or parsing data.
43    Parse(ParseError),
44    /// An error occurred processing CFF data.
45    CFF(CFFError),
46    /// An error occurred serializing data.
47    Write(WriteError),
48    /// The font is not a variable font.
49    NotVariableFont,
50    /// The font is a variable font but support for its format is not
51    /// implemented.
52    ///
53    /// Encountered for variable CFF fonts.
54    NotImplemented,
55    /// The font did not contain a `name` table entry for the family name in a
56    /// usable encoding.
57    NameError,
58    /// The list of table tags was unable to be retrieved from the font.
59    TagError,
60}
61
62enum GlyphData<'a> {
63    Glyf(GlyfTable<'a>),
64    Cff2(CFF2<'a>),
65}
66
67/// Name information for a variation axis.
68#[derive(Debug, Eq, PartialEq)]
69pub struct NamedAxis<'a> {
70    /// The four-character code identifying the axis.
71    pub tag: u32,
72    /// The name of the axis.
73    pub name: Cow<'a, str>,
74    /// The suggested ordering of this axis in a user interface.
75    pub ordering: u16,
76}
77
78/// Error type returned from [axis_names].
79#[derive(Debug, Eq, PartialEq)]
80pub enum AxisNamesError {
81    /// An error occurred reading or parsing data.
82    Parse(ParseError),
83    /// Font is missing STAT table.
84    NoStatTable,
85    /// Font is missing name table.
86    NoNameTable,
87}
88
89/// Retrieve the variation axis names.
90///
91/// Requires the font to have a `STAT` table. If any invalid name ids are encountered
92/// the name will be replaced with "Unknown".
93pub fn axis_names<'a>(
94    provider: &impl FontTableProvider,
95) -> Result<Vec<NamedAxis<'a>>, AxisNamesError> {
96    let stat_data = provider
97        .table_data(tag::STAT)?
98        .ok_or(AxisNamesError::NoStatTable)?;
99    let stat = ReadScope::new(&stat_data).read::<StatTable<'_>>()?;
100    let name_data = provider
101        .table_data(tag::NAME)?
102        .ok_or(AxisNamesError::NoNameTable)?;
103    let name = ReadScope::new(&name_data).read::<NameTable<'_>>()?;
104
105    stat.design_axes()
106        .map(|axis| {
107            let axis = axis?;
108            let name = name
109                .string_for_id(axis.axis_name_id)
110                .map(Cow::from)
111                .unwrap_or_else(|| Cow::from(String::from("Unknown")));
112            Ok(NamedAxis {
113                tag: axis.axis_tag,
114                name,
115                ordering: axis.axis_ordering,
116            })
117        })
118        .collect()
119}
120
121/// Create a static instance of a variable font according to the variation
122/// instance `instance`.
123///
124/// TrueType fonts with a `gvar` table as well as CFF2 fonts are supported.
125/// If the font is variable but does not contain a `gvar` or `CFF2` table
126/// [VariationError::NotImplemented] is returned.
127pub fn instance(
128    provider: &impl FontTableProvider,
129    user_instance: &[Fixed],
130) -> Result<(Vec<u8>, OwnedTuple), VariationError> {
131    is_supported_variable_font(provider)?;
132
133    // We need to create a font with at least these tables:
134    //
135    // cmap 	Character to glyph mapping
136    // head 	Font header
137    // hhea 	Horizontal header
138    // hmtx 	Horizontal metrics
139    // maxp 	Maximum profile
140    // name 	Naming table
141    // OS/2 	OS/2 and Windows specific metrics
142    // post 	PostScript information
143    //
144    // https://learn.microsoft.com/en-us/typography/opentype/spec/otff#required-tables
145    let mut head = ReadScope::new(&provider.read_table_data(tag::HEAD)?).read::<HeadTable>()?;
146    let maxp = ReadScope::new(&provider.read_table_data(tag::MAXP)?).read::<MaxpTable>()?;
147    let loca_data = provider.table_data(tag::LOCA)?;
148    let loca = loca_data
149        .as_ref()
150        .map(|loca_data| {
151            ReadScope::new(loca_data)
152                .read_dep::<LocaTable<'_>>((usize::from(maxp.num_glyphs), head.index_to_loc_format))
153        })
154        .transpose()?;
155
156    let glyf_data = provider.table_data(tag::GLYF)?;
157    let cff2_data = provider.table_data(tag::CFF2)?;
158    let glyph_data = match (&loca, &glyf_data, &cff2_data) {
159        (Some(loca), Some(glyf_data), _) => {
160            let glyf = ReadScope::new(glyf_data).read_dep::<GlyfTable<'_>>(&loca)?;
161            GlyphData::Glyf(glyf)
162        }
163        (_, _, Some(cff2_data)) => {
164            let cff2 = ReadScope::new(cff2_data).read::<CFF2<'_>>()?;
165            GlyphData::Cff2(cff2)
166        }
167        _ => return Err(ParseError::MissingValue.into()),
168    };
169    let mut hhea = ReadScope::new(&provider.read_table_data(tag::HHEA)?).read::<HheaTable>()?;
170    let hmtx_data = provider.read_table_data(tag::HMTX)?;
171    let hmtx = ReadScope::new(&hmtx_data).read_dep::<HmtxTable<'_>>((
172        usize::from(maxp.num_glyphs),
173        usize::from(hhea.num_h_metrics),
174    ))?;
175    let vhea_data = provider.table_data(tag::VHEA)?;
176    let vhea = vhea_data
177        .as_ref()
178        .map(|vhea_data| ReadScope::new(vhea_data).read::<HheaTable>())
179        .transpose()?;
180    let vmtx_data = provider.table_data(tag::VMTX)?;
181    let vmtx = vhea
182        .and_then(|vhea| {
183            vmtx_data.as_ref().map(|vmtx_data| {
184                ReadScope::new(vmtx_data).read_dep::<HmtxTable<'_>>((
185                    usize::from(maxp.num_glyphs),
186                    usize::from(vhea.num_h_metrics),
187                ))
188            })
189        })
190        .transpose()?;
191
192    let os2_data = provider.read_table_data(tag::OS_2)?;
193    let mut os2 = ReadScope::new(&os2_data).read_dep::<Os2>(os2_data.len())?;
194    let post_data = provider.read_table_data(tag::POST)?;
195    let mut post = ReadScope::new(&post_data).read::<PostTable<'_>>()?;
196    let fvar_data = provider.read_table_data(tag::FVAR)?;
197    let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>()?;
198    let avar_data = provider.table_data(tag::AVAR)?;
199    let avar = avar_data
200        .as_ref()
201        .map(|avar_data| ReadScope::new(avar_data).read::<AvarTable<'_>>())
202        .transpose()?;
203    let cvt_data = provider.table_data(tag::CVT)?;
204    let mut cvt = cvt_data
205        .as_ref()
206        .map(|cvt_data| ReadScope::new(cvt_data).read_dep::<CvtTable<'_>>(cvt_data.len() as u32))
207        .transpose()?;
208    let cvar_data = provider.table_data(tag::CVAR)?;
209    let cvar = cvt
210        .as_ref()
211        .and_then(|cvt| {
212            cvar_data.as_ref().map(|cvar_data| {
213                ReadScope::new(cvar_data)
214                    .read_dep::<CvarTable<'_>>((fvar.axis_count(), cvt.values.len() as u32))
215            })
216        })
217        .transpose()?;
218    let gvar_data = provider.table_data(tag::GVAR)?;
219    let gvar = gvar_data
220        .as_ref()
221        .map(|gvar_data| ReadScope::new(gvar_data).read::<GvarTable<'_>>())
222        .transpose()?;
223    let hvar_data = provider.table_data(tag::HVAR)?;
224    let hvar = hvar_data
225        .as_ref()
226        .map(|hvar_data| ReadScope::new(hvar_data).read::<HvarTable<'_>>())
227        .transpose()?;
228    let mvar_data = provider.table_data(tag::MVAR)?;
229    let mvar = mvar_data
230        .as_ref()
231        .map(|mvar_data| ReadScope::new(mvar_data).read::<MvarTable<'_>>())
232        .transpose()?;
233    let stat_data = provider.table_data(tag::STAT)?;
234    let stat = stat_data
235        .as_ref()
236        .map(|stat_data| ReadScope::new(stat_data).read::<StatTable<'_>>())
237        .transpose()?;
238    let name_data = provider.read_table_data(tag::NAME)?;
239    let name = ReadScope::new(&name_data).read::<NameTable<'_>>()?;
240
241    let instance = fvar.normalize(user_instance.iter().copied(), avar.as_ref())?;
242
243    // Apply deltas to glyphs to build a new glyf/CFF2 table
244    let (glyph_data, hmtx) = match (glyph_data, &gvar) {
245        (GlyphData::Glyf(mut glyf), Some(gvar)) => {
246            glyf = apply_gvar(
247                glyf,
248                gvar,
249                &hmtx,
250                vmtx.as_ref(),
251                Some(&os2),
252                &hhea,
253                &instance,
254            )?;
255
256            // Update head
257            let mut bbox = RectI::default();
258            glyf.records().iter().for_each(|glyph| match glyph {
259                GlyfRecord::Present { .. } => {}
260                GlyfRecord::Parsed(glyph) => {
261                    if let Some(bounding_box) = glyph.bounding_box() {
262                        bbox = union_rect(bbox, bounding_box.into())
263                    }
264                }
265            });
266            head.x_min = bbox.min_x().try_into().ok().unwrap_or(i16::MIN);
267            head.y_min = bbox.min_y().try_into().ok().unwrap_or(i16::MIN);
268            head.x_max = bbox.max_x().try_into().ok().unwrap_or(i16::MAX);
269            head.y_max = bbox.max_y().try_into().ok().unwrap_or(i16::MAX);
270
271            // Build new hmtx table
272            let hmtx = create_hmtx_table(&hmtx, hvar.as_ref(), &glyf, &instance, maxp.num_glyphs)?;
273            (GlyphData::Glyf(glyf), hmtx)
274        }
275        (GlyphData::Cff2(mut cff2), _) => {
276            cff2.instance_char_strings(&instance)?;
277            cff2.vstore = None; // No need for the variation store now
278            match &hvar {
279                // If horizontal metrics need to vary then HVAR is required in CFF2 as there is no
280                // phantom points concept.
281                Some(hvar) => {
282                    let hmtx = apply_hvar(&hmtx, hvar, None, &instance, maxp.num_glyphs)?;
283                    (GlyphData::Cff2(cff2), hmtx)
284                }
285                // Pass through original hmtx unchanged
286                None => (GlyphData::Cff2(cff2), hmtx),
287            }
288        }
289        // It is possible for a TrueType variable font to exist without gvar or CFF2 tables.
290        // The most likely place this would be encountered would be a COLRv1 font that varies the
291        // colour information but not the glyph contours. We don't currently support COLRv1. There
292        // are other ways such a font might exist, but it should be uncommon. For now these are
293        // unsupported.
294        _ => return Err(VariationError::NotImplemented),
295    };
296
297    // Update italic flags
298    head.mac_style
299        .set(MacStyle::ITALIC, is_italic(user_instance, &fvar));
300    os2.fs_selection.set(FsSelection::ITALIC, head.is_italic());
301
302    // Update hhea
303    hhea.num_h_metrics = maxp.num_glyphs; // there's now metrics for each glyph
304    hhea.advance_width_max = hmtx
305        .h_metrics
306        .iter()
307        .map(|m| m.advance_width)
308        .max()
309        .unwrap_or(0);
310
311    // Apply deltas to OS/2, hhea, vhea, post
312    if let Some(mvar) = &mvar {
313        process_mvar(mvar, &instance, &mut os2, &mut hhea, &mut None, &mut post);
314    }
315
316    // If one of the axes is wght or wdth then when need to update the corresponding
317    // fields in OS/2
318    for (axis, value) in fvar.axes().zip(user_instance.iter().copied()) {
319        if value == axis.default_value {
320            continue;
321        }
322
323        match axis.axis_tag {
324            tag::WGHT => {
325                // Map the value to one of the weight classes. Weight can be 1 to 1000 but
326                // weight classes are only defined for 100, 200, 300... 900.
327                os2.us_weight_class = ((f32::from(value).clamp(1., 1000.) / 100.0).round() as u16
328                    * 100)
329                    .clamp(100, 900);
330                head.mac_style
331                    .set(MacStyle::BOLD, os2.us_weight_class >= 600);
332                os2.fs_selection.set(FsSelection::BOLD, head.is_bold());
333            }
334            tag::WDTH => {
335                os2.us_width_class = Os2::value_to_width_class(value);
336                head.mac_style
337                    .set(MacStyle::CONDENSED, os2.us_width_class < 4);
338                head.mac_style
339                    .set(MacStyle::EXTENDED, os2.us_width_class > 6);
340            }
341            _ => {}
342        }
343    }
344    os2.fs_selection
345        .set(FsSelection::REGULAR, !(head.is_bold() || head.is_italic()));
346
347    if let (Some(cvt), Some(cvar)) = (cvt.as_mut(), cvar) {
348        *cvt = cvar.apply(&instance, cvt)?;
349    }
350
351    // Update name
352    let subfamily_name = stat
353        .as_ref()
354        .map(|stat| typographic_subfamily_name(user_instance, &fvar, stat, &name, "Regular"))
355        .unwrap_or_else(|| {
356            name.string_for_id(NameTable::TYPOGRAPHIC_SUBFAMILY_NAME)
357                .or_else(|| name.string_for_id(NameTable::FONT_SUBFAMILY_NAME))
358                .ok_or(VariationError::NameError)
359        })?;
360    let font_family_name = name
361        .string_for_id(NameTable::FONT_FAMILY_NAME)
362        .or_else(|| name.string_for_id(NameTable::TYPOGRAPHIC_FAMILY_NAME))
363        .ok_or(VariationError::NameError)?;
364    let typographic_family = name
365        .string_for_id(NameTable::TYPOGRAPHIC_FAMILY_NAME)
366        .or_else(|| name.string_for_id(NameTable::FONT_FAMILY_NAME))
367        .ok_or(VariationError::NameError)?;
368    let postscript_prefix = name.string_for_id(NameTable::VARIATIONS_POSTSCRIPT_NAME_PREFIX);
369    let mut name = owned::NameTable::try_from(&name)?;
370
371    // Replace name_id entries 1 & 2 and then populate 16 & 17, replacing any existing
372    // entries
373    let full_name = format!("{} {}", typographic_family, subfamily_name);
374    let postscript_name = generate_postscript_name(
375        &postscript_prefix,
376        &typographic_family,
377        user_instance,
378        &fvar,
379    );
380    let unique_id = generate_unique_id(&head, &os2, &postscript_name);
381    name.replace_entries(
382        NameTable::FONT_FAMILY_NAME,
383        &format!("{font_family_name} {subfamily_name}"),
384    );
385    name.replace_entries(NameTable::FONT_SUBFAMILY_NAME, "Regular");
386    name.replace_entries(NameTable::UNIQUE_FONT_IDENTIFIER, &unique_id);
387    name.replace_entries(NameTable::FULL_FONT_NAME, &full_name);
388    name.replace_entries(NameTable::POSTSCRIPT_NAME, &postscript_name);
389    name.replace_entries(NameTable::TYPOGRAPHIC_FAMILY_NAME, &typographic_family);
390    name.replace_entries(NameTable::TYPOGRAPHIC_SUBFAMILY_NAME, &subfamily_name);
391
392    // Build the new font
393    let mut builder = match glyph_data {
394        GlyphData::Cff2(_) => FontBuilder::new(CFF_MAGIC),
395        GlyphData::Glyf(_) => FontBuilder::new(TRUE_MAGIC),
396    };
397    if let Some(cvt) = cvt {
398        builder.add_table::<_, CvtTable<'_>>(tag::CVT, &cvt, ())?;
399    }
400    builder.add_table::<_, HheaTable>(tag::HHEA, &hhea, ())?;
401    builder.add_table::<_, HmtxTable<'_>>(tag::HMTX, &hmtx, ())?;
402    builder.add_table::<_, MaxpTable>(tag::MAXP, &maxp, ())?;
403    builder.add_table::<_, owned::NameTable<'_>>(tag::NAME, &name, ())?;
404    builder.add_table::<_, Os2>(tag::OS_2, &os2, ())?;
405    builder.add_table::<_, PostTable<'_>>(tag::POST, &post, ())?;
406
407    let glyf = match glyph_data {
408        GlyphData::Cff2(cff2) => {
409            builder.add_table::<_, CFF2<'_>>(tag::CFF2, cff2, ())?;
410            None
411        }
412        GlyphData::Glyf(glyf) => Some(glyf),
413    };
414
415    // Add remaining non-variable tables from the source font that have not already been added.
416    // This is important for ensuring GPOS/GSUB etc are included.
417    let builder_tables = builder.table_tags().collect::<FxHashSet<_>>();
418    let tags = provider.table_tags().ok_or(VariationError::TagError)?;
419
420    for tag in tags.into_iter().filter(|tag| {
421        // head, glyf, loca will be added later so don't add them now
422        ![tag::HEAD, tag::GLYF, tag::LOCA].contains(tag)
423            && !is_var_table(*tag)
424            && !builder_tables.contains(tag)
425    }) {
426        let data = provider.read_table_data(tag)?;
427        builder.add_table::<_, ReadScope<'_>>(tag, ReadScope::new(&data), ())?;
428    }
429
430    // TODO: Work out how to detect when short offsets would be ok
431    head.index_to_loc_format = IndexToLocFormat::Long;
432    let mut builder = builder.add_head_table(&head)?;
433    if let Some(glyf) = glyf {
434        builder.add_glyf_table(glyf)?;
435    }
436    builder
437        .data()
438        .map(|data| (data, instance))
439        .map_err(VariationError::from)
440}
441
442fn typographic_subfamily_name<'a>(
443    user_instance: &[Fixed],
444    fvar: &FvarTable<'a>,
445    stat: &'a StatTable<'a>,
446    name: &NameTable<'a>,
447    default: &str,
448) -> Result<String, VariationError> {
449    let mut names = Vec::new();
450    for (axis, value) in fvar.axes().zip(user_instance.iter().copied()) {
451        for (i, rec) in stat.design_axes().enumerate() {
452            let rec = rec?;
453            if rec.axis_tag == axis.axis_tag {
454                if let Some(name_id) =
455                    stat.name_for_axis_value(i as u16, value, ElidableName::Exclude)
456                {
457                    names.push((name_id, rec.axis_ordering));
458                }
459            }
460        }
461    }
462    // Sort by axis_ordering
463    names.sort_by_key(|res| res.1);
464    let names = if names.is_empty() {
465        // names might be empty if all the axis values names were elidable, fall back on
466        // elidedFallbackNameID if present
467        let name = stat
468            .elided_fallback_name_id
469            .and_then(|name_id| name.string_for_id(name_id))
470            .unwrap_or_else(|| default.to_string());
471        vec![name]
472    } else {
473        names
474            .into_iter()
475            .filter_map(|(name_id, _)| name.string_for_id(name_id))
476            .collect::<Vec<_>>()
477    };
478    Ok(names.join(" "))
479}
480
481// https://web.archive.org/web/20190705180831/https://wwwimages2.adobe.com/content/dam/acom/en/devnet/font/pdfs/5902.AdobePSNameGeneration.pdf
482fn generate_postscript_name(
483    prefix: &Option<String>,
484    typographic_family: &str,
485    user_tuple: &[Fixed],
486    fvar: &FvarTable<'_>,
487) -> String {
488    // Remove any characters other than ASCII-range uppercase Latin
489    // letters, lowercase Latin letters, and digits.
490    let mut prefix: String = prefix
491        .as_deref()
492        .unwrap_or(typographic_family)
493        .chars()
494        .filter(|c| c.is_ascii_alphanumeric())
495        .collect();
496    let mut postscript_name = prefix.clone();
497    fvar.axes()
498        .zip(user_tuple.iter().copied())
499        .for_each(|(axis, value)| {
500            if value != axis.default_value {
501                // NOTE(unwrap): Should always succeed when writing to a String (I/O error not
502                // possible)
503                let tag = DisplayTag(axis.axis_tag).to_string();
504                write!(
505                    postscript_name,
506                    "_{}{}",
507                    fixed_to_min_float(value),
508                    tag.trim()
509                )
510                .unwrap();
511            }
512        });
513
514    if postscript_name.len() > 63 {
515        // Too long, construct "last resort" name
516        let crc = crc32fast::hash(postscript_name.as_bytes());
517        let hash = format!("-{:X}...", crc);
518        // Ensure prefix is short enough when prepended to hash. Truncate is safe as
519        // prefix is ASCII only.
520        prefix.truncate(63 - hash.len());
521        postscript_name = prefix + &hash;
522    }
523
524    postscript_name
525}
526
527fn generate_unique_id(head: &HeadTable, os2: &Os2, postscript_name: &str) -> String {
528    let version = head.font_revision;
529    let vendor = DisplayTag(os2.ach_vend_id).to_string();
530    format!(
531        "{:.3};{};{}",
532        f32::from(version),
533        vendor.trim(),
534        postscript_name
535    )
536}
537
538const VAR_UPPER: u32 = tag!(b"\0VAR");
539const VAR_LOWER: u32 = tag!(b"\0var");
540
541// `true` if the tag ends in VAR or var
542fn is_var_table(tag: u32) -> bool {
543    ((tag & VAR_LOWER) == VAR_LOWER) || ((tag & VAR_UPPER) == VAR_UPPER)
544}
545
546/// Format [Fixed] using minimal decimals (as specified for generating
547/// postscript names)
548fn fixed_to_min_float(fixed: Fixed) -> f64 {
549    // Implementation ported from:
550    // https://web.archive.org/web/20190705180831/https://wwwimages2.adobe.com/content/dam/acom/en/devnet/font/pdfs/5902.AdobePSNameGeneration.pdf
551    if fixed.raw_value() == 0 {
552        return 0.0;
553    }
554    let scale = (1 << 16) as f64;
555    let value = fixed.raw_value() as f64 / scale;
556    let eps = 0.5 / scale;
557    let lo = value - eps;
558    let hi = value + eps;
559    // If the range of valid choices spans an integer, return the integer.
560    if lo as i32 != hi as i32 {
561        return value.round();
562    }
563
564    let lo = format!("{:.8}", lo);
565    let hi = format!("{:.8}", hi);
566    debug_assert!(
567        lo.len() == hi.len() && lo != hi,
568        "lo = {}, hi = {}, eps = {}",
569        lo,
570        hi,
571        eps
572    );
573    let mut i = lo.len() - 1;
574    for (index, (l, h)) in lo.bytes().zip(hi.bytes()).enumerate() {
575        if l != h {
576            i = index;
577            break;
578        }
579    }
580    let period = lo.bytes().position(|b| b == b'.').unwrap();
581    debug_assert!(period < i);
582    f64::from_str(&format!("{:.digits$}", value, digits = i - period)).unwrap()
583}
584
585fn process_mvar(
586    mvar: &MvarTable<'_>,
587    instance: &OwnedTuple,
588    os2: &mut Os2,
589    hhea: &mut HheaTable,
590    vhea: &mut Option<HheaTable>,
591    post: &mut PostTable<'_>,
592) {
593    for value_record in mvar.value_records() {
594        let Some(delta) = mvar.lookup(value_record.value_tag, instance) else {
595            continue;
596        };
597
598        match value_record.value_tag {
599            // horizontal ascender 	OS/2.sTypoAscender
600            tag::HASC => {
601                if let Some(v0) = &mut os2.version0 {
602                    v0.s_typo_ascender = add_delta_i16(v0.s_typo_ascender, delta);
603                }
604            }
605            // horizontal descender 	OS/2.sTypoDescender
606            tag::HDSC => {
607                if let Some(v0) = &mut os2.version0 {
608                    v0.s_typo_descender = add_delta_i16(v0.s_typo_descender, delta);
609                }
610            }
611            // horizontal line gap 	OS/2.sTypoLineGap
612            tag::HLGP => {
613                if let Some(v0) = &mut os2.version0 {
614                    v0.s_typo_line_gap = add_delta_i16(v0.s_typo_line_gap, delta);
615                }
616            }
617            // horizontal clipping ascent 	OS/2.usWinAscent
618            tag::HCLA => {
619                if let Some(v0) = &mut os2.version0 {
620                    v0.us_win_ascent = add_delta_u16(v0.us_win_ascent, delta);
621                }
622            }
623            // horizontal clipping descent 	OS/2.usWinDescent
624            tag::HCLD => {
625                if let Some(v0) = &mut os2.version0 {
626                    v0.us_win_descent = add_delta_u16(v0.us_win_descent, delta);
627                }
628            }
629            // vertical ascender 	vhea.ascent
630            tag::VASC => {
631                if let Some(vhea) = vhea {
632                    vhea.ascender = add_delta_i16(vhea.ascender, delta);
633                }
634            }
635            // vertical descender 	vhea.descent
636            tag::VDSC => {
637                if let Some(vhea) = vhea {
638                    vhea.descender = add_delta_i16(vhea.descender, delta);
639                }
640            }
641            // vertical line gap 	vhea.lineGap
642            tag::VLGP => {
643                if let Some(vhea) = vhea {
644                    vhea.line_gap = add_delta_i16(vhea.line_gap, delta);
645                }
646            }
647            // horizontal caret rise 	hhea.caretSlopeRise
648            tag::HCRS => {
649                hhea.caret_slope_rise = add_delta_i16(hhea.caret_slope_rise, delta);
650            }
651            // horizontal caret run 	hhea.caretSlopeRun
652            tag::HCRN => {
653                hhea.caret_slope_run = add_delta_i16(hhea.caret_slope_run, delta);
654            }
655            // horizontal caret offset 	hhea.caretOffset
656            tag::HCOF => {
657                hhea.caret_offset = add_delta_i16(hhea.caret_offset, delta);
658            }
659            // vertical caret rise 	vhea.caretSlopeRise
660            tag::VCRS => {
661                if let Some(vhea) = vhea {
662                    vhea.caret_slope_rise = add_delta_i16(vhea.caret_slope_rise, delta);
663                }
664            }
665            // vertical caret run 	vhea.caretSlopeRun
666            tag::VCRN => {
667                if let Some(vhea) = vhea {
668                    vhea.caret_slope_run = add_delta_i16(vhea.caret_slope_run, delta);
669                }
670            }
671            // vertical caret offset 	vhea.caretOffset
672            tag::VCOF => {
673                if let Some(vhea) = vhea {
674                    vhea.caret_offset = add_delta_i16(vhea.caret_offset, delta);
675                }
676            }
677            // x height 	OS/2.sxHeight
678            tag::XHGT => {
679                if let Some(version) = &mut os2.version2to4 {
680                    version.sx_height = add_delta_i16(version.sx_height, delta);
681                }
682            }
683            // cap height 	OS/2.sCapHeight
684            tag::CPHT => {
685                if let Some(version) = &mut os2.version2to4 {
686                    version.s_cap_height = add_delta_i16(version.s_cap_height, delta);
687                }
688            }
689            // subscript em x size 	OS/2.ySubscriptXSize
690            tag::SBXS => {
691                os2.y_subscript_x_size = add_delta_i16(os2.y_subscript_x_size, delta);
692            }
693            // subscript em y size 	OS/2.ySubscriptYSize
694            tag::SBYS => {
695                os2.y_subscript_y_size = add_delta_i16(os2.y_subscript_y_size, delta);
696            }
697            // subscript em x offset 	OS/2.ySubscriptXOffset
698            tag::SBXO => {
699                os2.y_subscript_x_offset = add_delta_i16(os2.y_subscript_x_offset, delta);
700            }
701            // subscript em y offset 	OS/2.ySubscriptYOffset
702            tag::SBYO => {
703                os2.y_subscript_y_offset = add_delta_i16(os2.y_subscript_y_offset, delta);
704            }
705            // superscript em x size 	OS/2.ySuperscriptXSize
706            tag::SPXS => {
707                os2.y_superscript_x_size = add_delta_i16(os2.y_superscript_x_size, delta);
708            }
709            // superscript em y size 	OS/2.ySuperscriptYSize
710            tag::SPYS => {
711                os2.y_superscript_y_size = add_delta_i16(os2.y_superscript_y_size, delta);
712            }
713            // superscript em x offset 	OS/2.ySuperscriptXOffset
714            tag::SPXO => {
715                os2.y_superscript_x_offset = add_delta_i16(os2.y_superscript_x_offset, delta);
716            }
717            // superscript em y offset 	OS/2.ySuperscriptYOffset
718            tag::SPYO => {
719                os2.y_superscript_y_offset = add_delta_i16(os2.y_superscript_y_offset, delta);
720            }
721            // strikeout size 	OS/2.yStrikeoutSize
722            tag::STRS => {
723                os2.y_strikeout_size = add_delta_i16(os2.y_strikeout_size, delta);
724            }
725            // strikeout offset 	OS/2.yStrikeoutPosition
726            tag::STRO => {
727                os2.y_strikeout_position = add_delta_i16(os2.y_strikeout_position, delta);
728            }
729            // underline size 	post.underlineThickness
730            tag::UNDS => {
731                post.header.underline_thickness =
732                    add_delta_i16(post.header.underline_thickness, delta);
733            }
734            // underline offset 	post.underlinePosition
735            tag::UNDO => {
736                post.header.underline_position =
737                    add_delta_i16(post.header.underline_position, delta);
738            }
739            // gaspRange[0] 	gasp.gaspRange[0..9].rangeMaxPPEM
740            // We know about these but ignore them since the gasp table doesn't make it into subset
741            // fonts.
742            tag::GSP0
743            | tag::GSP1
744            | tag::GSP2
745            | tag::GSP3
746            | tag::GSP4
747            | tag::GSP5
748            | tag::GSP6
749            | tag::GSP7
750            | tag::GSP8
751            | tag::GSP9 => (),
752            // Skip/ignore unknown value tags
753            _ => (),
754        }
755    }
756}
757
758fn add_delta_i16(value: i16, delta: f32) -> i16 {
759    (value as f32 + delta)
760        .round()
761        .clamp(i16::MIN as f32, i16::MAX as f32) as i16
762}
763
764fn add_delta_u16(value: u16, delta: f32) -> u16 {
765    (value as f32 + delta).round().clamp(0., u16::MAX as f32) as u16
766}
767
768fn is_supported_variable_font(provider: &impl FontTableProvider) -> Result<(), VariationError> {
769    // The OpenType specification says two tables are required in all variable fonts:
770    //
771    // * A font variations ('fvar') table is required to describe the variations
772    //   supported by the font.
773    // * A style attributes (STAT) table is required and is used to establish
774    //   relationships between different fonts belonging to a family and to provide
775    //   some degree of compatibility with legacy applications by allowing platforms
776    //   to project variation instances involving many axes into older font-family
777    //   models that assume a limited set of axes.
778    //
779    // https://learn.microsoft.com/en-us/typography/opentype/spec/otvaroverview#vartables
780    //
781    // However it seems there are fonts in the wild that lack a `STAT` table.
782    // These were first encountered in the Unicode text-rendering-tests and it was
783    // suggested that the spec was overly strict. So to support these fonts we
784    // don't require `STAT`.
785    //
786    // https://github.com/unicode-org/text-rendering-tests/issues/91
787    if provider.has_table(tag::FVAR) {
788        Ok(())
789    } else {
790        Err(VariationError::NotVariableFont)
791    }
792}
793
794fn create_hmtx_table<'b>(
795    hmtx: &HmtxTable<'_>,
796    hvar: Option<&HvarTable<'_>>,
797    glyf: &GlyfTable<'_>,
798    instance: &OwnedTuple,
799    num_glyphs: u16,
800) -> Result<HmtxTable<'b>, ReadWriteError> {
801    match hvar {
802        // Apply deltas to hmtx
803        Some(hvar) => apply_hvar(hmtx, hvar, Some(glyf), instance, num_glyphs),
804        // Calculate from glyph deltas/phantom points
805        None => htmx_from_phantom_points(glyf, num_glyphs),
806    }
807}
808
809fn apply_hvar<'a>(
810    hmtx: &HmtxTable<'_>,
811    hvar: &HvarTable<'_>,
812    glyf: Option<&GlyfTable<'_>>,
813    instance: &OwnedTuple,
814    num_glyphs: u16,
815) -> Result<HmtxTable<'a>, ReadWriteError> {
816    let mut h_metrics = Vec::with_capacity(usize::from(num_glyphs));
817    for glyph_id in 0..num_glyphs {
818        let mut metric = hmtx.metric(glyph_id)?;
819        let delta = hvar.advance_delta(instance, glyph_id)?;
820        let new = (metric.advance_width as f32 + delta).round();
821        metric.advance_width = new.clamp(0., u16::MAX as f32) as u16;
822
823        if let Some(delta) = hvar.left_side_bearing_delta(instance, glyph_id)? {
824            metric.lsb = (metric.lsb as f32 + delta)
825                .round()
826                .clamp(i16::MIN as f32, i16::MAX as f32) as i16;
827        } else if let Some(glyf) = glyf {
828            // lsb can be calculated from phantom points
829            let glyph = glyf
830                .records()
831                .get(usize::from(glyph_id))
832                .and_then(|glyph_record| match glyph_record {
833                    GlyfRecord::Parsed(glyph) => Some(glyph),
834                    _ => None,
835                })
836                .ok_or(ParseError::BadIndex)?;
837            let bounding_box = glyph.bounding_box().unwrap_or_else(BoundingBox::empty);
838            // NOTE(unwrap): Phantom points are populated by apply_gvar
839            let phantom_points = glyph.phantom_points().unwrap();
840            let pp1 = phantom_points[0].0;
841            metric.lsb = bounding_box.x_min - pp1;
842        }
843        h_metrics.push(metric)
844    }
845
846    // TODO: Can we apply the optimisation if they're all the same at the end
847    Ok(HmtxTable {
848        h_metrics: ReadArrayCow::Owned(h_metrics),
849        left_side_bearings: ReadArrayCow::Owned(vec![]),
850    })
851}
852
853fn htmx_from_phantom_points<'a>(
854    glyf: &GlyfTable<'_>,
855    num_glyphs: u16,
856) -> Result<HmtxTable<'a>, ReadWriteError> {
857    // Take note that, in a variable font with TrueType outlines, the left side
858    // bearing for each glyph must equal xMin, and bit 1 in the flags
859    // field of the 'head' table must be set.
860    //
861    // If a glyph has no contours, xMax/xMin are not defined. The left side bearing
862    // indicated in the 'hmtx' table for such glyphs should be zero.
863    let mut h_metrics = Vec::with_capacity(usize::from(num_glyphs));
864
865    for glyph_record in glyf.records().iter() {
866        let metric = match glyph_record {
867            GlyfRecord::Parsed(glyph) => {
868                let bounding_box = glyph.bounding_box().unwrap_or_else(BoundingBox::empty);
869                // NOTE(unwrap): Phantom points are populated by apply_gvar
870                let phantom_points = glyph.phantom_points().unwrap();
871                let pp1 = phantom_points[0].0;
872                let pp2 = phantom_points[1].0;
873                // pp1 = xMin - lsb
874                // pp2 = pp1 + aw
875                let lsb = bounding_box.x_min - pp1;
876                let advance_width = u16::try_from(pp2 - pp1).unwrap_or(0);
877                LongHorMetric { advance_width, lsb }
878            }
879            _ => unreachable!("glyph should be parsed with phantom points present"),
880        };
881        h_metrics.push(metric);
882    }
883
884    // TODO: Can we apply the optimisation if they're all the same at the end
885    Ok(HmtxTable {
886        h_metrics: ReadArrayCow::Owned(h_metrics),
887        left_side_bearings: ReadArrayCow::Owned(vec![]),
888    })
889}
890
891/// Applies glyph deltas from the `gvar` table to glyphs in the `glyf` table.
892///
893/// Takes ownership of the `glyf` table as placeholder values are swapped in
894/// during processing (see note in body of function) and returning early would
895/// leave the `glyf` table in an incorrect state. So we consume it and return
896/// the modified, valid result only on success.
897fn apply_gvar<'a>(
898    mut glyf: GlyfTable<'a>,
899    gvar: &GvarTable<'a>,
900    hmtx: &HmtxTable<'a>,
901    vmtx: Option<&HmtxTable<'a>>,
902    os2: Option<&Os2>,
903    hhea: &HheaTable,
904    instance: &OwnedTuple,
905) -> Result<GlyfTable<'a>, ReadWriteError> {
906    for (glyph_id, glyph_record) in glyf.records_mut().iter_mut().enumerate() {
907        // NOTE(cast): Safe as num_glyphs is u16
908        let glyph_id = glyph_id as u16;
909        glyph_record.parse()?;
910        match glyph_record {
911            GlyfRecord::Parsed(glyph) => {
912                glyph.apply_variations(glyph_id, instance, gvar, hmtx, vmtx, os2, hhea)?;
913            }
914            GlyfRecord::Present { .. } => unreachable!("glyph should be parsed"),
915        }
916    }
917
918    // Do a pass to update the bounding boxes of composite glyphs
919    for glyph_id in 0..glyf.num_glyphs() {
920        // We do a little take/replace dance here to work within Rust's unique (mut)
921        // access constraints: we need to mutate the glyph but also pass an
922        // immutable reference to the glyf table that holds it. To work around
923        // this we swap the glyph we're processing with an empty glyph in the
924        // glyf table and then put it back afterwards. This works because
925        // the glyf table is required for `apply_variations` to resolve child components
926        // in composite glyphs to calculate the bounding box, and a composite
927        // glyph can't refer to itself so should never encounter the empty
928        // replacement.
929        if glyf.records()[usize::from(glyph_id)].is_composite() {
930            // NOTE(unwrap): should not panic as glyph_id < num_glyphs
931            let mut glyph_record = glyf.take(glyph_id).unwrap();
932            let GlyfRecord::Parsed(Glyph::Composite(ref mut composite)) = glyph_record else {
933                unreachable!("expected parsed composite glyph")
934            };
935            // Calculate the new bounding box for this composite glyph
936            let bbox = composite
937                .calculate_bounding_box(&glyf)?
938                .round_out()
939                .to_i32();
940            composite.bounding_box = BoundingBox {
941                x_min: bbox
942                    .min_x()
943                    .try_into()
944                    .map_err(|_| ParseError::LimitExceeded)?,
945                x_max: bbox
946                    .max_x()
947                    .try_into()
948                    .map_err(|_| ParseError::LimitExceeded)?,
949                y_min: bbox
950                    .min_y()
951                    .try_into()
952                    .map_err(|_| ParseError::LimitExceeded)?,
953                y_max: bbox
954                    .max_y()
955                    .try_into()
956                    .map_err(|_| ParseError::LimitExceeded)?,
957            };
958            glyf.replace(glyph_id, glyph_record)?;
959        }
960    }
961
962    Ok(glyf)
963}
964
965fn union_rect(rect: RectI, other: RectI) -> RectI {
966    RectI::from_points(
967        rect.origin().min(other.origin()),
968        rect.lower_right().max(other.lower_right()),
969    )
970}
971
972fn is_italic(tuple: &[Fixed], fvar: &FvarTable<'_>) -> bool {
973    // If the font has a `slnt` axis and the instance has a non-zero angle for the slant then
974    // consider it italic.
975    let Some(slnt_index) = fvar.axes().position(|axis| axis.axis_tag == tag::SLNT) else {
976        return false;
977    };
978
979    tuple
980        .get(slnt_index)
981        .filter(|&&value| value != Fixed::from(0i32))
982        .is_some()
983}
984
985impl From<BoundingBox> for RectI {
986    fn from(bbox: BoundingBox) -> Self {
987        RectI::from_points(
988            vec2i(bbox.x_min.into(), bbox.y_min.into()),
989            vec2i(bbox.x_max.into(), bbox.y_max.into()),
990        )
991    }
992}
993
994impl From<ParseError> for VariationError {
995    fn from(err: ParseError) -> VariationError {
996        VariationError::Parse(err)
997    }
998}
999
1000impl From<CFFError> for VariationError {
1001    fn from(err: CFFError) -> VariationError {
1002        VariationError::CFF(err)
1003    }
1004}
1005
1006impl From<WriteError> for VariationError {
1007    fn from(err: WriteError) -> VariationError {
1008        VariationError::Write(err)
1009    }
1010}
1011
1012impl From<ReadWriteError> for VariationError {
1013    fn from(err: ReadWriteError) -> VariationError {
1014        match err {
1015            ReadWriteError::Read(err) => VariationError::Parse(err),
1016            ReadWriteError::Write(err) => VariationError::Write(err),
1017        }
1018    }
1019}
1020
1021impl fmt::Display for VariationError {
1022    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1023        match self {
1024            VariationError::Parse(err) => write!(f, "variation: parse error: {}", err),
1025            VariationError::CFF(err) => write!(f, "variation: CFF error: {}", err),
1026            VariationError::Write(err) => write!(f, "variation: write error: {}", err),
1027            VariationError::NotVariableFont => write!(f, "variation: not a variable font"),
1028            VariationError::NotImplemented => {
1029                write!(f, "variation: unsupported variable font format")
1030            }
1031            VariationError::NameError => write!(f, "font did not contain a `name` table entry for the family name in a usable encoding"),
1032            VariationError::TagError => write!(f, "the list of table tags was unable to be retrieved from the font"),
1033        }
1034    }
1035}
1036
1037impl std::error::Error for VariationError {}
1038
1039impl From<ParseError> for AxisNamesError {
1040    fn from(err: ParseError) -> AxisNamesError {
1041        AxisNamesError::Parse(err)
1042    }
1043}
1044
1045impl fmt::Display for AxisNamesError {
1046    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1047        match self {
1048            AxisNamesError::Parse(err) => write!(f, "axis names: parse error: {}", err),
1049            AxisNamesError::NoStatTable => f.write_str("axis names: no STAT table"),
1050            AxisNamesError::NoNameTable => f.write_str("axis names: no name table"),
1051        }
1052    }
1053}
1054
1055impl std::error::Error for AxisNamesError {}
1056
1057#[cfg(test)]
1058mod tests {
1059    use super::*;
1060    use crate::assert_close;
1061    use crate::cff::charstring::{ArgumentsStack, CharStringVisitorContext};
1062    use crate::cff::{cff2, CFFFont};
1063    use crate::font_data::FontData;
1064    use crate::tables::{OpenTypeData, OpenTypeFont};
1065    use crate::tests::read_fixture;
1066
1067    #[test]
1068    fn test_generate_postscript_name_with_postscript_prefix() {
1069        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
1070        let scope = ReadScope::new(&buffer);
1071        let font_file = scope
1072            .read::<FontData<'_>>()
1073            .expect("unable to parse font file");
1074        let table_provider = font_file
1075            .table_provider(0)
1076            .expect("unable to create font provider");
1077        let fvar_data = table_provider
1078            .read_table_data(tag::FVAR)
1079            .expect("unable to read fvar table data");
1080        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>().unwrap();
1081
1082        // Display SemiCondensed Thin: [100.0, 87.5, 100.0]
1083        let user_tuple = [Fixed::from(100.0), Fixed::from(87.5), Fixed::from(100.0)];
1084        let typographic_family = "Family";
1085        let postscript_prefix = Some(String::from("PSPrefix"));
1086        let postscript_name =
1087            generate_postscript_name(&postscript_prefix, typographic_family, &user_tuple, &fvar);
1088        assert_eq!(postscript_name, "PSPrefix_100wght_87.5wdth_100CTGR");
1089    }
1090
1091    #[test]
1092    fn test_generate_postscript_name_without_postscript_prefix() {
1093        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
1094        let scope = ReadScope::new(&buffer);
1095        let font_file = scope
1096            .read::<FontData<'_>>()
1097            .expect("unable to parse font file");
1098        let table_provider = font_file
1099            .table_provider(0)
1100            .expect("unable to create font provider");
1101        let fvar_data = table_provider
1102            .read_table_data(tag::FVAR)
1103            .expect("unable to read fvar table data");
1104        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>().unwrap();
1105
1106        // Display SemiCondensed Thin: [100.0, 87.5, 100.0]
1107        let user_tuple = [Fixed::from(100.0), Fixed::from(87.5), Fixed::from(100.0)];
1108        let typographic_family = "Family";
1109        let postscript_prefix = None;
1110        let postscript_name =
1111            generate_postscript_name(&postscript_prefix, typographic_family, &user_tuple, &fvar);
1112        assert_eq!(postscript_name, "Family_100wght_87.5wdth_100CTGR");
1113    }
1114
1115    #[test]
1116    fn test_generate_postscript_name_omit_defaults() {
1117        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
1118        let scope = ReadScope::new(&buffer);
1119        let font_file = scope
1120            .read::<FontData<'_>>()
1121            .expect("unable to parse font file");
1122        let table_provider = font_file
1123            .table_provider(0)
1124            .expect("unable to create font provider");
1125        let fvar_data = table_provider
1126            .read_table_data(tag::FVAR)
1127            .expect("unable to read fvar table data");
1128        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>().unwrap();
1129
1130        let user_tuple = [Fixed::from(400.0), Fixed::from(87.5), Fixed::from(0.0)];
1131        let typographic_family = "Family";
1132        let postscript_prefix = Some(String::from("PSPrefix"));
1133        let postscript_name =
1134            generate_postscript_name(&postscript_prefix, typographic_family, &user_tuple, &fvar);
1135        assert_eq!(postscript_name, "PSPrefix_87.5wdth");
1136    }
1137
1138    #[test]
1139    fn test_generate_postscript_name_strip_forbidden_chars() {
1140        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
1141        let scope = ReadScope::new(&buffer);
1142        let font_file = scope
1143            .read::<FontData<'_>>()
1144            .expect("unable to parse font file");
1145        let table_provider = font_file
1146            .table_provider(0)
1147            .expect("unable to create font provider");
1148        let fvar_data = table_provider
1149            .read_table_data(tag::FVAR)
1150            .expect("unable to read fvar table data");
1151        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>().unwrap();
1152
1153        let user_tuple = [Fixed::from(100.0), Fixed::from(87.5), Fixed::from(100.0)];
1154        let typographic_family = "These aren't allowed []<>!";
1155        let postscript_name =
1156            generate_postscript_name(&None, typographic_family, &user_tuple, &fvar);
1157        assert_eq!(
1158            postscript_name,
1159            "Thesearentallowed_100wght_87.5wdth_100CTGR"
1160        );
1161    }
1162
1163    #[test]
1164    fn test_generate_postscript_name_truncate() {
1165        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
1166        let scope = ReadScope::new(&buffer);
1167        let font_file = scope
1168            .read::<FontData<'_>>()
1169            .expect("unable to parse font file");
1170        let table_provider = font_file
1171            .table_provider(0)
1172            .expect("unable to create font provider");
1173        let fvar_data = table_provider
1174            .read_table_data(tag::FVAR)
1175            .expect("unable to read fvar table data");
1176        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>().unwrap();
1177
1178        let user_tuple = [Fixed::from(100.0), Fixed::from(87.5), Fixed::from(100.0)];
1179        let typographic_family = "IfAfterConstructingThePostScriptNameInThisWayTheLengthIsGreaterThan127CharactersThenConstructTheLastResortPostScriptName";
1180        let postscript_name =
1181            generate_postscript_name(&None, typographic_family, &user_tuple, &fvar);
1182        assert!(postscript_name.len() <= 63);
1183        assert_eq!(
1184            postscript_name,
1185            "IfAfterConstructingThePostScriptNameInThisWayTheLen-189E39CF..."
1186        );
1187    }
1188
1189    #[test]
1190    fn typographic_subfamily_name_non_elidable() -> Result<(), ReadWriteError> {
1191        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
1192        let scope = ReadScope::new(&buffer);
1193        let font_file = scope.read::<FontData<'_>>()?;
1194        let table_provider = font_file.table_provider(0)?;
1195        let fvar_data = table_provider.read_table_data(tag::FVAR)?;
1196        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>()?;
1197        let stat_data = table_provider.read_table_data(tag::STAT)?;
1198        let stat = ReadScope::new(&stat_data).read::<StatTable<'_>>()?;
1199        let name_data = table_provider.read_table_data(tag::NAME)?;
1200        let name = ReadScope::new(&name_data).read::<NameTable<'_>>()?;
1201
1202        let user_tuple = [Fixed::from(100.0), Fixed::from(87.5), Fixed::from(100.0)];
1203        let name = typographic_subfamily_name(&user_tuple, &fvar, &stat, &name, "Default").unwrap();
1204        assert_eq!(name, "Thin SemiCondensed Display");
1205        Ok(())
1206    }
1207
1208    #[test]
1209    fn typographic_subfamily_name_elidable() -> Result<(), ReadWriteError> {
1210        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
1211        let scope = ReadScope::new(&buffer);
1212        let font_file = scope.read::<FontData<'_>>()?;
1213        let table_provider = font_file.table_provider(0)?;
1214        let fvar_data = table_provider.read_table_data(tag::FVAR)?;
1215        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>()?;
1216        let stat_data = table_provider.read_table_data(tag::STAT)?;
1217        let stat = ReadScope::new(&stat_data).read::<StatTable<'_>>()?;
1218        let name_data = table_provider.read_table_data(tag::NAME)?;
1219        let name = ReadScope::new(&name_data).read::<NameTable<'_>>()?;
1220
1221        // - wght = min: 100, max: 900, default: 400
1222        // - wdth = min: 62.5, max: 100, default: 100
1223        // - CTGR = min: 0, max: 100, default: 0
1224
1225        // Use default values to trigger elidable fallback
1226        let user_tuple = [Fixed::from(400.0), Fixed::from(100.0), Fixed::from(0.0)];
1227        let name = typographic_subfamily_name(&user_tuple, &fvar, &stat, &name, "Default").unwrap();
1228        assert_eq!(name, "Regular");
1229        Ok(())
1230    }
1231
1232    #[test]
1233    fn subfamily_name_axis_value_format3() -> Result<(), ReadWriteError> {
1234        let buffer = read_fixture("tests/fonts/variable/Inter[slnt,wght].abc.ttf");
1235        let scope = ReadScope::new(&buffer);
1236        let font_file = scope.read::<FontData<'_>>()?;
1237        let table_provider = font_file.table_provider(0)?;
1238        let fvar_data = table_provider.read_table_data(tag::FVAR)?;
1239        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>()?;
1240        let stat_data = table_provider.read_table_data(tag::STAT)?;
1241        let stat = ReadScope::new(&stat_data).read::<StatTable<'_>>()?;
1242        let name_data = table_provider.read_table_data(tag::NAME)?;
1243        let name = ReadScope::new(&name_data).read::<NameTable<'_>>()?;
1244
1245        // - wght = min: 100, max: 900, default: 400
1246        // - slnt = min: -10, max: 0, default: 0
1247
1248        // slnt value is the elidable value. In a previous version of the code it was not elided
1249        // in the output because STAT axis value table format 3 was not processed.
1250        let user_tuple = [Fixed::from(700.0), Fixed::from(0.0)];
1251        let name = typographic_subfamily_name(&user_tuple, &fvar, &stat, &name, "Default").unwrap();
1252        assert_eq!(name, "Bold");
1253        Ok(())
1254    }
1255
1256    #[test]
1257    fn test_fixed_to_float() {
1258        assert_close!(fixed_to_min_float(Fixed::from(0)), 0., f64::EPSILON);
1259        assert_close!(fixed_to_min_float(Fixed::from(900)), 900., f64::EPSILON);
1260        assert_close!(fixed_to_min_float(Fixed::from(5.5)), 5.5, f64::EPSILON);
1261        assert_close!(fixed_to_min_float(Fixed::from(2.9)), 2.9, f64::EPSILON);
1262        assert_close!(fixed_to_min_float(Fixed::from(-1.4)), -1.4, f64::EPSILON);
1263        assert_close!(
1264            fixed_to_min_float(Fixed::from(-1. + (1. / 65536.))),
1265            -0.99998,
1266            f64::EPSILON
1267        );
1268    }
1269
1270    // This font triggers the hvar path through create_hmtx_table and exposed bug in it.
1271    #[test]
1272    fn instance_underline_test() -> Result<(), ReadWriteError> {
1273        let buffer = read_fixture("tests/fonts/variable/UnderlineTest-VF.ttf");
1274        let scope = ReadScope::new(&buffer);
1275        let font_file = scope.read::<FontData<'_>>()?;
1276        let table_provider = font_file.table_provider(0)?;
1277        let user_tuple = [Fixed::from(500), Fixed::from(500)];
1278        let (inst, _tuple) = instance(&table_provider, &user_tuple).unwrap();
1279
1280        let scope = ReadScope::new(&inst);
1281        let font_file = scope.read::<FontData<'_>>()?;
1282        let table_provider = font_file.table_provider(0)?;
1283        let maxp =
1284            ReadScope::new(&table_provider.read_table_data(tag::MAXP)?).read::<MaxpTable>()?;
1285        let hhea =
1286            ReadScope::new(&table_provider.read_table_data(tag::HHEA)?).read::<HheaTable>()?;
1287        let hmtx_data = table_provider.read_table_data(tag::HMTX)?;
1288        assert!(ReadScope::new(&hmtx_data)
1289            .read_dep::<HmtxTable<'_>>((
1290                usize::from(maxp.num_glyphs),
1291                usize::from(hhea.num_h_metrics),
1292            ))
1293            .is_ok());
1294        Ok(())
1295    }
1296
1297    #[test]
1298    #[cfg(feature = "prince")]
1299    fn instance_minipax() -> Result<(), ReadWriteError> {
1300        let buffer =
1301            read_fixture("../../../tests/data/fonts/minipax/variable/Minipax Variable.ttf");
1302        let scope = ReadScope::new(&buffer);
1303        let font_file = scope.read::<FontData<'_>>()?;
1304        let table_provider = font_file.table_provider(0)?;
1305        let user_tuple = [Fixed::from(600)];
1306        assert!(instance(&table_provider, &user_tuple).is_ok());
1307
1308        Ok(())
1309    }
1310
1311    #[test]
1312    fn test_axis_names() {
1313        let buffer = read_fixture("tests/fonts/variable/UnderlineTest-VF.ttf");
1314        let scope = ReadScope::new(&buffer);
1315        let font_file = scope.read::<FontData<'_>>().unwrap();
1316        let table_provider = font_file.table_provider(0).unwrap();
1317        let names = axis_names(&table_provider).unwrap();
1318        assert_eq!(
1319            names,
1320            vec![
1321                NamedAxis {
1322                    tag: tag!(b"UNDO"),
1323                    name: Cow::from("Underline Offset"),
1324                    ordering: 0
1325                },
1326                NamedAxis {
1327                    tag: tag!(b"UNDS"),
1328                    name: Cow::from("Underline Size"),
1329                    ordering: 1
1330                }
1331            ]
1332        );
1333    }
1334
1335    #[test]
1336    fn test_axis_names_not_variable() {
1337        let buffer = read_fixture("tests/fonts/opentype/SourceCodePro-Regular.otf");
1338        let scope = ReadScope::new(&buffer);
1339        let font_file = scope.read::<FontData<'_>>().unwrap();
1340        let table_provider = font_file.table_provider(0).unwrap();
1341        let names = axis_names(&table_provider);
1342        assert_eq!(names, Err(AxisNamesError::NoStatTable));
1343    }
1344
1345    #[test]
1346    fn instance_cff2() -> Result<(), VariationError> {
1347        let buffer = read_fixture("tests/fonts/opentype/cff2/SourceSansVariable-Roman.abc.otf");
1348        let scope = ReadScope::new(&buffer);
1349        let font_file = scope.read::<FontData<'_>>()?;
1350        let table_provider = font_file.table_provider(0)?;
1351
1352        let user_tuple = [Fixed::from(650.0)];
1353        let (res, _tuple) = instance(&table_provider, &user_tuple)?;
1354
1355        // Read the font back in
1356        let otf = ReadScope::new(&res).read::<OpenTypeFont<'_>>().unwrap();
1357
1358        let offset_table = match otf.data {
1359            OpenTypeData::Single(ttf) => ttf,
1360            OpenTypeData::Collection(_) => unreachable!(),
1361        };
1362
1363        let cff2_table_data = offset_table
1364            .read_table(&otf.scope, tag::CFF2)
1365            .unwrap()
1366            .unwrap();
1367        let cff2 = cff2_table_data
1368            .read::<CFF2<'_>>()
1369            .expect("unable to parse CFF2 instance");
1370
1371        for glyph_id in 0..cff2.char_strings_index.len() as u16 {
1372            let font_dict_index = cff2
1373                .fd_select
1374                .as_ref()
1375                .and_then(|fd_select| fd_select.font_dict_index(glyph_id))
1376                .unwrap_or(0);
1377            let font_dict = &cff2.fonts[usize::from(font_dict_index)];
1378            println!("-- glyph {glyph_id} --");
1379            let mut visitor = crate::cff::charstring::DebugVisitor;
1380            let variable = None;
1381            let mut ctx = CharStringVisitorContext::new(
1382                glyph_id,
1383                &cff2.char_strings_index,
1384                font_dict.local_subr_index.as_ref(),
1385                &cff2.global_subr_index,
1386                variable,
1387            );
1388            let mut stack = ArgumentsStack {
1389                data: &mut [0.0; cff2::MAX_OPERANDS],
1390                len: 0,
1391                max_len: cff2::MAX_OPERANDS,
1392            };
1393            ctx.visit(CFFFont::CFF2(&font_dict), &mut stack, &mut visitor)?;
1394        }
1395
1396        Ok(())
1397    }
1398}