1
//! Variable font instancing.
2

            
3
#![deny(missing_docs)]
4

            
5
use std::borrow::Cow;
6
use std::fmt;
7
use std::fmt::Write;
8
use std::str::FromStr;
9

            
10
use pathfinder_geometry::rect::RectI;
11
use pathfinder_geometry::vector::vec2i;
12
use rustc_hash::FxHashSet;
13

            
14
use crate::binary::read::{ReadArrayCow, ReadScope};
15
use crate::cff::cff2::CFF2;
16
use crate::cff::CFFError;
17
use crate::error::{ParseError, ReadWriteError, WriteError};
18
use crate::post::PostTable;
19
use crate::subset::{FontBuilder, TableFilter};
20
use crate::tables::glyf::{BoundingBox, GlyfRecord, GlyfTable, Glyph};
21
use crate::tables::loca::LocaTable;
22
use crate::tables::os2::{FsSelectionFlag, Os2};
23
use crate::tables::variable_fonts::avar::AvarTable;
24
use crate::tables::variable_fonts::cvar::CvarTable;
25
use crate::tables::variable_fonts::fvar::FvarTable;
26
use crate::tables::variable_fonts::gvar::GvarTable;
27
use crate::tables::variable_fonts::hvar::HvarTable;
28
use crate::tables::variable_fonts::mvar::MvarTable;
29
use crate::tables::variable_fonts::stat::{ElidableName, StatTable};
30
use crate::tables::variable_fonts::OwnedTuple;
31
use crate::tables::{
32
    owned, CvtTable, Fixed, FontTableProvider, HeadTable, HheaTable, HmtxTable, IndexToLocFormat,
33
    LongHorMetric, MacStyleFlag, MaxpTable, NameTable, CFF_MAGIC, TRUE_MAGIC,
34
};
35
use crate::tag;
36
use crate::tag::DisplayTag;
37

            
38
/// Error type returned from instancing a variable font.
39
#[derive(Debug)]
40
pub enum VariationError {
41
    /// An error occurred reading or parsing data.
42
    Parse(ParseError),
43
    /// An error occurred processing CFF data.
44
    CFF(CFFError),
45
    /// An error occurred serializing data.
46
    Write(WriteError),
47
    /// The font is not a variable font.
48
    NotVariableFont,
49
    /// The font is a variable font but support for its format is not
50
    /// implemented.
51
    ///
52
    /// Encountered for variable CFF fonts.
53
    NotImplemented,
54
    /// The font did not contain a `name` table entry for the family name in a
55
    /// usable encoding.
56
    NameError,
57
    /// The list of table tags was unable to be retrieved from the font.
58
    TagError,
59
}
60

            
61
enum GlyphData<'a> {
62
    Glyf(GlyfTable<'a>),
63
    Cff2(CFF2<'a>),
64
}
65

            
66
/// Name information for a variation axis.
67
#[derive(Debug, Eq, PartialEq)]
68
pub struct NamedAxis<'a> {
69
    /// The four-character code identifying the axis.
70
    pub tag: u32,
71
    /// The name of the axis.
72
    pub name: Cow<'a, str>,
73
    /// The suggested ordering of this axis in a user interface.
74
    pub ordering: u16,
75
}
76

            
77
/// Error type returned from [axis_names].
78
#[derive(Debug, Eq, PartialEq)]
79
pub enum AxisNamesError {
80
    /// An error occurred reading or parsing data.
81
    Parse(ParseError),
82
    /// Font is missing STAT table.
83
    NoStatTable,
84
    /// Font is missing name table.
85
    NoNameTable,
86
}
87

            
88
/// Retrieve the variation axis names.
89
///
90
/// Requires the font to have a `STAT` table. If any invalid name ids are encountered
91
/// the name will be replaced with "Unknown".
92
pub fn axis_names<'a>(
93
    provider: &impl FontTableProvider,
94
) -> Result<Vec<NamedAxis<'a>>, AxisNamesError> {
95
    let stat_data = provider
96
        .table_data(tag::STAT)?
97
        .ok_or(AxisNamesError::NoStatTable)?;
98
    let stat = ReadScope::new(&stat_data).read::<StatTable<'_>>()?;
99
    let name_data = provider
100
        .table_data(tag::NAME)?
101
        .ok_or(AxisNamesError::NoNameTable)?;
102
    let name = ReadScope::new(&name_data).read::<NameTable<'_>>()?;
103

            
104
    stat.design_axes()
105
        .map(|axis| {
106
            let axis = axis?;
107
            let name = name
108
                .string_for_id(axis.axis_name_id)
109
                .map(Cow::from)
110
                .unwrap_or_else(|| Cow::from(String::from("Unknown")));
111
            Ok(NamedAxis {
112
                tag: axis.axis_tag,
113
                name,
114
                ordering: axis.axis_ordering,
115
            })
116
        })
117
        .collect()
118
}
119

            
120
/// Create a static instance of a variable font according to the variation
121
/// instance `instance`.
122
///
123
/// TrueType fonts with a `gvar` table as well as CFF2 fonts are supported.
124
/// If the font is variable but does not contain a `gvar` or `CFF2` table
125
/// [VariationError::NotImplemented] is returned.
126
pub fn instance(
127
    provider: &impl FontTableProvider,
128
    user_instance: &[Fixed],
129
) -> Result<(Vec<u8>, OwnedTuple), VariationError> {
130
    is_supported_variable_font(provider)?;
131

            
132
    // We need to create a font with at least these tables:
133
    //
134
    // cmap 	Character to glyph mapping
135
    // head 	Font header
136
    // hhea 	Horizontal header
137
    // hmtx 	Horizontal metrics
138
    // maxp 	Maximum profile
139
    // name 	Naming table
140
    // OS/2 	OS/2 and Windows specific metrics
141
    // post 	PostScript information
142
    //
143
    // https://learn.microsoft.com/en-us/typography/opentype/spec/otff#required-tables
144
    let mut head = ReadScope::new(&provider.read_table_data(tag::HEAD)?).read::<HeadTable>()?;
145
    let maxp = ReadScope::new(&provider.read_table_data(tag::MAXP)?).read::<MaxpTable>()?;
146
    let loca_data = provider.table_data(tag::LOCA)?;
147
    let loca = loca_data
148
        .as_ref()
149
        .map(|loca_data| {
150
            ReadScope::new(loca_data)
151
                .read_dep::<LocaTable<'_>>((maxp.num_glyphs, head.index_to_loc_format))
152
        })
153
        .transpose()?;
154

            
155
    let glyf_data = provider.table_data(tag::GLYF)?;
156
    let cff2_data = provider.table_data(tag::CFF2)?;
157
    let glyph_data = match (&loca, &glyf_data, &cff2_data) {
158
        (Some(loca), Some(glyf_data), _) => {
159
            let glyf = ReadScope::new(glyf_data).read_dep::<GlyfTable<'_>>(loca)?;
160
            GlyphData::Glyf(glyf)
161
        }
162
        (_, _, Some(cff2_data)) => {
163
            let cff2 = ReadScope::new(cff2_data).read::<CFF2<'_>>()?;
164
            GlyphData::Cff2(cff2)
165
        }
166
        _ => return Err(ParseError::MissingValue.into()),
167
    };
168
    let mut hhea = ReadScope::new(&provider.read_table_data(tag::HHEA)?).read::<HheaTable>()?;
169
    let hmtx_data = provider.read_table_data(tag::HMTX)?;
170
    let hmtx = ReadScope::new(&hmtx_data).read_dep::<HmtxTable<'_>>((
171
        usize::from(maxp.num_glyphs),
172
        usize::from(hhea.num_h_metrics),
173
    ))?;
174
    let vhea_data = provider.table_data(tag::VHEA)?;
175
    let vhea = vhea_data
176
        .as_ref()
177
        .map(|vhea_data| ReadScope::new(vhea_data).read::<HheaTable>())
178
        .transpose()?;
179
    let vmtx_data = provider.table_data(tag::VMTX)?;
180
    let vmtx = vhea
181
        .and_then(|vhea| {
182
            vmtx_data.as_ref().map(|vmtx_data| {
183
                ReadScope::new(vmtx_data).read_dep::<HmtxTable<'_>>((
184
                    usize::from(maxp.num_glyphs),
185
                    usize::from(vhea.num_h_metrics),
186
                ))
187
            })
188
        })
189
        .transpose()?;
190

            
191
    let os2_data = provider.read_table_data(tag::OS_2)?;
192
    let mut os2 = ReadScope::new(&os2_data).read_dep::<Os2>(os2_data.len())?;
193
    let post_data = provider.read_table_data(tag::POST)?;
194
    let mut post = ReadScope::new(&post_data).read::<PostTable<'_>>()?;
195
    let fvar_data = provider.read_table_data(tag::FVAR)?;
196
    let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>()?;
197
    let avar_data = provider.table_data(tag::AVAR)?;
198
    let avar = avar_data
199
        .as_ref()
200
        .map(|avar_data| ReadScope::new(avar_data).read::<AvarTable<'_>>())
201
        .transpose()?;
202
    let cvt_data = provider.table_data(tag::CVT)?;
203
    let mut cvt = cvt_data
204
        .as_ref()
205
        .map(|cvt_data| ReadScope::new(cvt_data).read_dep::<CvtTable<'_>>(cvt_data.len() as u32))
206
        .transpose()?;
207
    let cvar_data = provider.table_data(tag::CVAR)?;
208
    let cvar = cvt
209
        .as_ref()
210
        .and_then(|cvt| {
211
            cvar_data.as_ref().map(|cvar_data| {
212
                ReadScope::new(cvar_data)
213
                    .read_dep::<CvarTable<'_>>((fvar.axis_count(), cvt.values.len() as u32))
214
            })
215
        })
216
        .transpose()?;
217
    let gvar_data = provider.table_data(tag::GVAR)?;
218
    let gvar = gvar_data
219
        .as_ref()
220
        .map(|gvar_data| ReadScope::new(gvar_data).read::<GvarTable<'_>>())
221
        .transpose()?;
222
    let hvar_data = provider.table_data(tag::HVAR)?;
223
    let hvar = hvar_data
224
        .as_ref()
225
        .map(|hvar_data| ReadScope::new(hvar_data).read::<HvarTable<'_>>())
226
        .transpose()?;
227
    let mvar_data = provider.table_data(tag::MVAR)?;
228
    let mvar = mvar_data
229
        .as_ref()
230
        .map(|mvar_data| ReadScope::new(mvar_data).read::<MvarTable<'_>>())
231
        .transpose()?;
232
    let stat_data = provider.table_data(tag::STAT)?;
233
    let stat = stat_data
234
        .as_ref()
235
        .map(|stat_data| ReadScope::new(stat_data).read::<StatTable<'_>>())
236
        .transpose()?;
237
    let name_data = provider.read_table_data(tag::NAME)?;
238
    let name = ReadScope::new(&name_data).read::<NameTable<'_>>()?;
239

            
240
    let instance = fvar.normalize(user_instance.iter().copied(), avar.as_ref())?;
241

            
242
    // Apply deltas to glyphs to build a new glyf/CFF2 table
243
    let (glyph_data, hmtx) = match (glyph_data, &gvar) {
244
        (GlyphData::Glyf(mut glyf), Some(gvar)) => {
245
            glyf = apply_gvar(
246
                glyf,
247
                gvar,
248
                &hmtx,
249
                vmtx.as_ref(),
250
                Some(&os2),
251
                &hhea,
252
                &instance,
253
            )?;
254

            
255
            // Update head
256
            let mut bbox = RectI::default();
257
            glyf.records().iter().for_each(|glyph| match glyph {
258
                GlyfRecord::Present { .. } => {}
259
                GlyfRecord::Parsed(glyph) => {
260
                    if let Some(bounding_box) = glyph.bounding_box() {
261
                        bbox = union_rect(bbox, bounding_box.into())
262
                    }
263
                }
264
            });
265
            head.x_min = bbox.min_x().try_into().ok().unwrap_or(i16::MIN);
266
            head.y_min = bbox.min_y().try_into().ok().unwrap_or(i16::MIN);
267
            head.x_max = bbox.max_x().try_into().ok().unwrap_or(i16::MAX);
268
            head.y_max = bbox.max_y().try_into().ok().unwrap_or(i16::MAX);
269

            
270
            // Build new hmtx table
271
            let hmtx = create_hmtx_table(&hmtx, hvar.as_ref(), &glyf, &instance, maxp.num_glyphs)?;
272
            (GlyphData::Glyf(glyf), hmtx)
273
        }
274
        (GlyphData::Cff2(mut cff2), _) => {
275
            cff2.instance_char_strings(&instance)?;
276
            cff2.vstore = None; // No need for the variation store now
277
            match &hvar {
278
                // If horizontal metrics need to vary then HVAR is required in CFF2 as there is no
279
                // phantom points concept.
280
                Some(hvar) => {
281
                    let hmtx = apply_hvar(&hmtx, hvar, None, &instance, maxp.num_glyphs)?;
282
                    (GlyphData::Cff2(cff2), hmtx)
283
                }
284
                // Pass through original hmtx unchanged
285
                None => (GlyphData::Cff2(cff2), hmtx),
286
            }
287
        }
288
        // It is possible for a TrueType variable font to exist without gvar or CFF2 tables.
289
        // The most likely place this would be encountered would be a COLRv1 font that varies the
290
        // colour information but not the glyph contours. We don't currently support COLRv1. There
291
        // are other ways such a font might exist, but it should be uncommon. For now these are
292
        // unsupported.
293
        _ => return Err(VariationError::NotImplemented),
294
    };
295

            
296
    // Update italic flags
297
    head.mac_style
298
        .set(MacStyleFlag::ITALIC, is_italic(user_instance, &fvar));
299
    os2.fs_selection
300
        .set(FsSelectionFlag::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(MacStyleFlag::BOLD, os2.us_weight_class >= 600);
332
                os2.fs_selection.set(FsSelectionFlag::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(MacStyleFlag::CONDENSED, os2.us_width_class < 4);
338
                head.mac_style
339
                    .set(MacStyleFlag::EXTENDED, os2.us_width_class > 6);
340
            }
341
            _ => {}
342
        }
343
    }
344
    os2.fs_selection.set(
345
        FsSelectionFlag::REGULAR,
346
        !(head.is_bold() || head.is_italic()),
347
    );
348

            
349
    if let (Some(cvt), Some(cvar)) = (cvt.as_mut(), cvar) {
350
        *cvt = cvar.apply(&instance, cvt)?;
351
    }
352

            
353
    // Update name
354
    let subfamily_name = stat
355
        .as_ref()
356
        .map(|stat| typographic_subfamily_name(user_instance, &fvar, stat, &name, "Regular"))
357
        .unwrap_or_else(|| {
358
            name.string_for_id(NameTable::TYPOGRAPHIC_SUBFAMILY_NAME)
359
                .or_else(|| name.string_for_id(NameTable::FONT_SUBFAMILY_NAME))
360
                .ok_or(VariationError::NameError)
361
        })?;
362
    let font_family_name = name
363
        .string_for_id(NameTable::FONT_FAMILY_NAME)
364
        .or_else(|| name.string_for_id(NameTable::TYPOGRAPHIC_FAMILY_NAME))
365
        .ok_or(VariationError::NameError)?;
366
    let typographic_family = name
367
        .string_for_id(NameTable::TYPOGRAPHIC_FAMILY_NAME)
368
        .or_else(|| name.string_for_id(NameTable::FONT_FAMILY_NAME))
369
        .ok_or(VariationError::NameError)?;
370
    let postscript_prefix = name.string_for_id(NameTable::VARIATIONS_POSTSCRIPT_NAME_PREFIX);
371
    let mut name = owned::NameTable::try_from(&name)?;
372

            
373
    // Replace name_id entries 1 & 2 and then populate 16 & 17, replacing any existing
374
    // entries
375
    let full_name = format!("{} {}", typographic_family, subfamily_name);
376
    let postscript_name = generate_postscript_name(
377
        &postscript_prefix,
378
        &typographic_family,
379
        user_instance,
380
        &fvar,
381
    );
382
    let unique_id = generate_unique_id(&head, &os2, &postscript_name);
383
    name.replace_entries(
384
        NameTable::FONT_FAMILY_NAME,
385
        &format!("{font_family_name} {subfamily_name}"),
386
    );
387
    name.replace_entries(NameTable::FONT_SUBFAMILY_NAME, "Regular");
388
    name.replace_entries(NameTable::UNIQUE_FONT_IDENTIFIER, &unique_id);
389
    name.replace_entries(NameTable::FULL_FONT_NAME, &full_name);
390
    name.replace_entries(NameTable::POSTSCRIPT_NAME, &postscript_name);
391
    name.replace_entries(NameTable::TYPOGRAPHIC_FAMILY_NAME, &typographic_family);
392
    name.replace_entries(NameTable::TYPOGRAPHIC_SUBFAMILY_NAME, &subfamily_name);
393

            
394
    // Build the new font
395
    let mut builder = match glyph_data {
396
        GlyphData::Cff2(_) => FontBuilder::new(CFF_MAGIC, TableFilter::All),
397
        GlyphData::Glyf(_) => FontBuilder::new(TRUE_MAGIC, TableFilter::All),
398
    };
399
    if let Some(cvt) = cvt {
400
        builder.add_table::<_, CvtTable<'_>>(tag::CVT, &cvt, ())?;
401
    }
402
    builder.add_table::<_, HheaTable>(tag::HHEA, &hhea, ())?;
403
    builder.add_table::<_, HmtxTable<'_>>(tag::HMTX, &hmtx, ())?;
404
    builder.add_table::<_, MaxpTable>(tag::MAXP, &maxp, ())?;
405
    builder.add_table::<_, owned::NameTable<'_>>(tag::NAME, &name, ())?;
406
    builder.add_table::<_, Os2>(tag::OS_2, &os2, ())?;
407
    builder.add_table::<_, PostTable<'_>>(tag::POST, &post, ())?;
408

            
409
    let glyf = match glyph_data {
410
        GlyphData::Cff2(cff2) => {
411
            builder.add_table::<_, CFF2<'_>>(tag::CFF2, cff2, ())?;
412
            None
413
        }
414
        GlyphData::Glyf(glyf) => Some(glyf),
415
    };
416

            
417
    // Add remaining non-variable tables from the source font that have not already been added.
418
    // This is important for ensuring GPOS/GSUB etc are included.
419
    let builder_tables = builder.table_tags().collect::<FxHashSet<_>>();
420
    let tags = provider.table_tags().ok_or(VariationError::TagError)?;
421

            
422
    for tag in tags.into_iter().filter(|tag| {
423
        // head, glyf, loca will be added later so don't add them now
424
        ![tag::HEAD, tag::GLYF, tag::LOCA].contains(tag)
425
            && !is_var_table(*tag)
426
            && !builder_tables.contains(tag)
427
    }) {
428
        let data = provider.read_table_data(tag)?;
429
        builder.add_table::<_, ReadScope<'_>>(tag, ReadScope::new(&data), ())?;
430
    }
431

            
432
    // TODO: Work out how to detect when short offsets would be ok
433
    head.index_to_loc_format = IndexToLocFormat::Long;
434
    let mut builder = builder.add_head_table(&head)?;
435
    if let Some(glyf) = glyf {
436
        builder.add_glyf_table(glyf)?;
437
    }
438
    builder
439
        .data()
440
        .map(|data| (data, instance))
441
        .map_err(VariationError::from)
442
}
443

            
444
fn typographic_subfamily_name<'a>(
445
    user_instance: &[Fixed],
446
    fvar: &FvarTable<'a>,
447
    stat: &'a StatTable<'a>,
448
    name: &NameTable<'a>,
449
    default: &str,
450
) -> Result<String, VariationError> {
451
    let mut names = Vec::new();
452
    for (axis, value) in fvar.axes().zip(user_instance.iter().copied()) {
453
        for (i, rec) in stat.design_axes().enumerate() {
454
            let rec = rec?;
455
            if rec.axis_tag == axis.axis_tag {
456
                if let Some(name_id) =
457
                    stat.name_for_axis_value(i as u16, value, ElidableName::Exclude)
458
                {
459
                    names.push((name_id, rec.axis_ordering));
460
                }
461
            }
462
        }
463
    }
464
    // Sort by axis_ordering
465
    names.sort_by_key(|res| res.1);
466
    let names = if names.is_empty() {
467
        // names might be empty if all the axis values names were elidable, fall back on
468
        // elidedFallbackNameID if present
469
        let name = stat
470
            .elided_fallback_name_id
471
            .and_then(|name_id| name.string_for_id(name_id))
472
            .unwrap_or_else(|| default.to_string());
473
        vec![name]
474
    } else {
475
        names
476
            .into_iter()
477
            .filter_map(|(name_id, _)| name.string_for_id(name_id))
478
            .collect::<Vec<_>>()
479
    };
480
    Ok(names.join(" "))
481
}
482

            
483
// https://web.archive.org/web/20190705180831/https://wwwimages2.adobe.com/content/dam/acom/en/devnet/font/pdfs/5902.AdobePSNameGeneration.pdf
484
fn generate_postscript_name(
485
    prefix: &Option<String>,
486
    typographic_family: &str,
487
    user_tuple: &[Fixed],
488
    fvar: &FvarTable<'_>,
489
) -> String {
490
    // Remove any characters other than ASCII-range uppercase Latin
491
    // letters, lowercase Latin letters, and digits.
492
    let mut prefix: String = prefix
493
        .as_deref()
494
        .unwrap_or(typographic_family)
495
        .chars()
496
        .filter(|c| c.is_ascii_alphanumeric())
497
        .collect();
498
    let mut postscript_name = prefix.clone();
499
    fvar.axes()
500
        .zip(user_tuple.iter().copied())
501
        .for_each(|(axis, value)| {
502
            if value != axis.default_value {
503
                // NOTE(unwrap): Should always succeed when writing to a String (I/O error not
504
                // possible)
505
                let tag = DisplayTag(axis.axis_tag).to_string();
506
                write!(
507
                    postscript_name,
508
                    "_{}{}",
509
                    fixed_to_min_float(value),
510
                    tag.trim()
511
                )
512
                .unwrap();
513
            }
514
        });
515

            
516
    if postscript_name.len() > 63 {
517
        // Too long, construct "last resort" name
518
        let crc = crate::crc32::hash(postscript_name.as_bytes());
519
        let hash = format!("-{:X}...", crc);
520
        // Ensure prefix is short enough when prepended to hash. Truncate is safe as
521
        // prefix is ASCII only.
522
        prefix.truncate(63 - hash.len());
523
        postscript_name = prefix + &hash;
524
    }
525

            
526
    postscript_name
527
}
528

            
529
fn generate_unique_id(head: &HeadTable, os2: &Os2, postscript_name: &str) -> String {
530
    let version = head.font_revision;
531
    let vendor = DisplayTag(os2.ach_vend_id).to_string();
532
    format!(
533
        "{:.3};{};{}",
534
        f32::from(version),
535
        vendor.trim(),
536
        postscript_name
537
    )
538
}
539

            
540
const VAR_UPPER: u32 = tag!(b"\0VAR");
541
const VAR_LOWER: u32 = tag!(b"\0var");
542

            
543
// `true` if the tag ends in VAR or var
544
fn is_var_table(tag: u32) -> bool {
545
    ((tag & VAR_LOWER) == VAR_LOWER) || ((tag & VAR_UPPER) == VAR_UPPER)
546
}
547

            
548
/// Format [Fixed] using minimal decimals (as specified for generating
549
/// postscript names)
550
fn fixed_to_min_float(fixed: Fixed) -> f64 {
551
    // Implementation ported from:
552
    // https://web.archive.org/web/20190705180831/https://wwwimages2.adobe.com/content/dam/acom/en/devnet/font/pdfs/5902.AdobePSNameGeneration.pdf
553
    if fixed.raw_value() == 0 {
554
        return 0.0;
555
    }
556
    let scale = (1 << 16) as f64;
557
    let value = fixed.raw_value() as f64 / scale;
558
    let eps = 0.5 / scale;
559
    let lo = value - eps;
560
    let hi = value + eps;
561
    // If the range of valid choices spans an integer, return the integer.
562
    if lo as i32 != hi as i32 {
563
        return value.round();
564
    }
565

            
566
    let lo = format!("{:.8}", lo);
567
    let hi = format!("{:.8}", hi);
568
    debug_assert!(
569
        lo.len() == hi.len() && lo != hi,
570
        "lo = {}, hi = {}, eps = {}",
571
        lo,
572
        hi,
573
        eps
574
    );
575
    let mut i = lo.len() - 1;
576
    for (index, (l, h)) in lo.bytes().zip(hi.bytes()).enumerate() {
577
        if l != h {
578
            i = index;
579
            break;
580
        }
581
    }
582
    let period = lo.bytes().position(|b| b == b'.').unwrap();
583
    debug_assert!(period < i);
584
    f64::from_str(&format!("{:.digits$}", value, digits = i - period)).unwrap()
585
}
586

            
587
fn process_mvar(
588
    mvar: &MvarTable<'_>,
589
    instance: &OwnedTuple,
590
    os2: &mut Os2,
591
    hhea: &mut HheaTable,
592
    vhea: &mut Option<HheaTable>,
593
    post: &mut PostTable<'_>,
594
) {
595
    for value_record in mvar.value_records() {
596
        let Some(delta) = mvar.lookup(value_record.value_tag, instance) else {
597
            continue;
598
        };
599

            
600
        match value_record.value_tag {
601
            // horizontal ascender 	OS/2.sTypoAscender
602
            tag::HASC => {
603
                if let Some(v0) = &mut os2.version0 {
604
                    v0.s_typo_ascender = add_delta_i16(v0.s_typo_ascender, delta);
605
                }
606
            }
607
            // horizontal descender 	OS/2.sTypoDescender
608
            tag::HDSC => {
609
                if let Some(v0) = &mut os2.version0 {
610
                    v0.s_typo_descender = add_delta_i16(v0.s_typo_descender, delta);
611
                }
612
            }
613
            // horizontal line gap 	OS/2.sTypoLineGap
614
            tag::HLGP => {
615
                if let Some(v0) = &mut os2.version0 {
616
                    v0.s_typo_line_gap = add_delta_i16(v0.s_typo_line_gap, delta);
617
                }
618
            }
619
            // horizontal clipping ascent 	OS/2.usWinAscent
620
            tag::HCLA => {
621
                if let Some(v0) = &mut os2.version0 {
622
                    v0.us_win_ascent = add_delta_u16(v0.us_win_ascent, delta);
623
                }
624
            }
625
            // horizontal clipping descent 	OS/2.usWinDescent
626
            tag::HCLD => {
627
                if let Some(v0) = &mut os2.version0 {
628
                    v0.us_win_descent = add_delta_u16(v0.us_win_descent, delta);
629
                }
630
            }
631
            // vertical ascender 	vhea.ascent
632
            tag::VASC => {
633
                if let Some(vhea) = vhea {
634
                    vhea.ascender = add_delta_i16(vhea.ascender, delta);
635
                }
636
            }
637
            // vertical descender 	vhea.descent
638
            tag::VDSC => {
639
                if let Some(vhea) = vhea {
640
                    vhea.descender = add_delta_i16(vhea.descender, delta);
641
                }
642
            }
643
            // vertical line gap 	vhea.lineGap
644
            tag::VLGP => {
645
                if let Some(vhea) = vhea {
646
                    vhea.line_gap = add_delta_i16(vhea.line_gap, delta);
647
                }
648
            }
649
            // horizontal caret rise 	hhea.caretSlopeRise
650
            tag::HCRS => {
651
                hhea.caret_slope_rise = add_delta_i16(hhea.caret_slope_rise, delta);
652
            }
653
            // horizontal caret run 	hhea.caretSlopeRun
654
            tag::HCRN => {
655
                hhea.caret_slope_run = add_delta_i16(hhea.caret_slope_run, delta);
656
            }
657
            // horizontal caret offset 	hhea.caretOffset
658
            tag::HCOF => {
659
                hhea.caret_offset = add_delta_i16(hhea.caret_offset, delta);
660
            }
661
            // vertical caret rise 	vhea.caretSlopeRise
662
            tag::VCRS => {
663
                if let Some(vhea) = vhea {
664
                    vhea.caret_slope_rise = add_delta_i16(vhea.caret_slope_rise, delta);
665
                }
666
            }
667
            // vertical caret run 	vhea.caretSlopeRun
668
            tag::VCRN => {
669
                if let Some(vhea) = vhea {
670
                    vhea.caret_slope_run = add_delta_i16(vhea.caret_slope_run, delta);
671
                }
672
            }
673
            // vertical caret offset 	vhea.caretOffset
674
            tag::VCOF => {
675
                if let Some(vhea) = vhea {
676
                    vhea.caret_offset = add_delta_i16(vhea.caret_offset, delta);
677
                }
678
            }
679
            // x height 	OS/2.sxHeight
680
            tag::XHGT => {
681
                if let Some(version) = &mut os2.version2to4 {
682
                    version.s_x_height = add_delta_i16(version.s_x_height, delta);
683
                }
684
            }
685
            // cap height 	OS/2.sCapHeight
686
            tag::CPHT => {
687
                if let Some(version) = &mut os2.version2to4 {
688
                    version.s_cap_height = add_delta_i16(version.s_cap_height, delta);
689
                }
690
            }
691
            // subscript em x size 	OS/2.ySubscriptXSize
692
            tag::SBXS => {
693
                os2.y_subscript_x_size = add_delta_i16(os2.y_subscript_x_size, delta);
694
            }
695
            // subscript em y size 	OS/2.ySubscriptYSize
696
            tag::SBYS => {
697
                os2.y_subscript_y_size = add_delta_i16(os2.y_subscript_y_size, delta);
698
            }
699
            // subscript em x offset 	OS/2.ySubscriptXOffset
700
            tag::SBXO => {
701
                os2.y_subscript_x_offset = add_delta_i16(os2.y_subscript_x_offset, delta);
702
            }
703
            // subscript em y offset 	OS/2.ySubscriptYOffset
704
            tag::SBYO => {
705
                os2.y_subscript_y_offset = add_delta_i16(os2.y_subscript_y_offset, delta);
706
            }
707
            // superscript em x size 	OS/2.ySuperscriptXSize
708
            tag::SPXS => {
709
                os2.y_superscript_x_size = add_delta_i16(os2.y_superscript_x_size, delta);
710
            }
711
            // superscript em y size 	OS/2.ySuperscriptYSize
712
            tag::SPYS => {
713
                os2.y_superscript_y_size = add_delta_i16(os2.y_superscript_y_size, delta);
714
            }
715
            // superscript em x offset 	OS/2.ySuperscriptXOffset
716
            tag::SPXO => {
717
                os2.y_superscript_x_offset = add_delta_i16(os2.y_superscript_x_offset, delta);
718
            }
719
            // superscript em y offset 	OS/2.ySuperscriptYOffset
720
            tag::SPYO => {
721
                os2.y_superscript_y_offset = add_delta_i16(os2.y_superscript_y_offset, delta);
722
            }
723
            // strikeout size 	OS/2.yStrikeoutSize
724
            tag::STRS => {
725
                os2.y_strikeout_size = add_delta_i16(os2.y_strikeout_size, delta);
726
            }
727
            // strikeout offset 	OS/2.yStrikeoutPosition
728
            tag::STRO => {
729
                os2.y_strikeout_position = add_delta_i16(os2.y_strikeout_position, delta);
730
            }
731
            // underline size 	post.underlineThickness
732
            tag::UNDS => {
733
                post.header.underline_thickness =
734
                    add_delta_i16(post.header.underline_thickness, delta);
735
            }
736
            // underline offset 	post.underlinePosition
737
            tag::UNDO => {
738
                post.header.underline_position =
739
                    add_delta_i16(post.header.underline_position, delta);
740
            }
741
            // gaspRange[0] 	gasp.gaspRange[0..9].rangeMaxPPEM
742
            // We know about these but ignore them since the gasp table doesn't make it into subset
743
            // fonts.
744
            tag::GSP0
745
            | tag::GSP1
746
            | tag::GSP2
747
            | tag::GSP3
748
            | tag::GSP4
749
            | tag::GSP5
750
            | tag::GSP6
751
            | tag::GSP7
752
            | tag::GSP8
753
            | tag::GSP9 => (),
754
            // Skip/ignore unknown value tags
755
            _ => (),
756
        }
757
    }
758
}
759

            
760
fn add_delta_i16(value: i16, delta: f32) -> i16 {
761
    (value as f32 + delta)
762
        .round()
763
        .clamp(i16::MIN as f32, i16::MAX as f32) as i16
764
}
765

            
766
fn add_delta_u16(value: u16, delta: f32) -> u16 {
767
    (value as f32 + delta).round().clamp(0., u16::MAX as f32) as u16
768
}
769

            
770
fn is_supported_variable_font(provider: &impl FontTableProvider) -> Result<(), VariationError> {
771
    // The OpenType specification says two tables are required in all variable fonts:
772
    //
773
    // * A font variations ('fvar') table is required to describe the variations
774
    //   supported by the font.
775
    // * A style attributes (STAT) table is required and is used to establish
776
    //   relationships between different fonts belonging to a family and to provide
777
    //   some degree of compatibility with legacy applications by allowing platforms
778
    //   to project variation instances involving many axes into older font-family
779
    //   models that assume a limited set of axes.
780
    //
781
    // https://learn.microsoft.com/en-us/typography/opentype/spec/otvaroverview#vartables
782
    //
783
    // However it seems there are fonts in the wild that lack a `STAT` table.
784
    // These were first encountered in the Unicode text-rendering-tests and it was
785
    // suggested that the spec was overly strict. So to support these fonts we
786
    // don't require `STAT`.
787
    //
788
    // https://github.com/unicode-org/text-rendering-tests/issues/91
789
    if provider.has_table(tag::FVAR) {
790
        Ok(())
791
    } else {
792
        Err(VariationError::NotVariableFont)
793
    }
794
}
795

            
796
fn create_hmtx_table<'b>(
797
    hmtx: &HmtxTable<'_>,
798
    hvar: Option<&HvarTable<'_>>,
799
    glyf: &GlyfTable<'_>,
800
    instance: &OwnedTuple,
801
    num_glyphs: u16,
802
) -> Result<HmtxTable<'b>, ReadWriteError> {
803
    match hvar {
804
        // Apply deltas to hmtx
805
        Some(hvar) => apply_hvar(hmtx, hvar, Some(glyf), instance, num_glyphs),
806
        // Calculate from glyph deltas/phantom points
807
        None => htmx_from_phantom_points(glyf, num_glyphs),
808
    }
809
}
810

            
811
fn apply_hvar<'a>(
812
    hmtx: &HmtxTable<'_>,
813
    hvar: &HvarTable<'_>,
814
    glyf: Option<&GlyfTable<'_>>,
815
    instance: &OwnedTuple,
816
    num_glyphs: u16,
817
) -> Result<HmtxTable<'a>, ReadWriteError> {
818
    let mut h_metrics = Vec::with_capacity(usize::from(num_glyphs));
819
    for glyph_id in 0..num_glyphs {
820
        let mut metric = hmtx.metric(glyph_id)?;
821
        let delta = hvar.advance_delta(instance, glyph_id)?;
822
        let new = (metric.advance_width as f32 + delta).round();
823
        metric.advance_width = new.clamp(0., u16::MAX as f32) as u16;
824

            
825
        if let Some(delta) = hvar.left_side_bearing_delta(instance, glyph_id)? {
826
            metric.lsb = (metric.lsb as f32 + delta)
827
                .round()
828
                .clamp(i16::MIN as f32, i16::MAX as f32) as i16;
829
        } else if let Some(glyf) = glyf {
830
            // lsb can be calculated from phantom points
831
            let glyph = glyf
832
                .records()
833
                .get(usize::from(glyph_id))
834
                .and_then(|glyph_record| match glyph_record {
835
                    GlyfRecord::Parsed(glyph) => Some(glyph),
836
                    _ => None,
837
                })
838
                .ok_or(ParseError::BadIndex)?;
839
            let bounding_box = glyph.bounding_box().unwrap_or_else(BoundingBox::empty);
840
            // NOTE(unwrap): Phantom points are populated by apply_gvar
841
            let phantom_points = glyph.phantom_points().unwrap();
842
            let pp1 = phantom_points[0].0;
843
            metric.lsb = bounding_box.x_min - pp1;
844
        }
845
        h_metrics.push(metric)
846
    }
847

            
848
    // TODO: Can we apply the optimisation if they're all the same at the end
849
    Ok(HmtxTable {
850
        h_metrics: ReadArrayCow::Owned(h_metrics),
851
        left_side_bearings: ReadArrayCow::Owned(vec![]),
852
    })
853
}
854

            
855
fn htmx_from_phantom_points<'a>(
856
    glyf: &GlyfTable<'_>,
857
    num_glyphs: u16,
858
) -> Result<HmtxTable<'a>, ReadWriteError> {
859
    // Take note that, in a variable font with TrueType outlines, the left side
860
    // bearing for each glyph must equal xMin, and bit 1 in the flags
861
    // field of the 'head' table must be set.
862
    //
863
    // If a glyph has no contours, xMax/xMin are not defined. The left side bearing
864
    // indicated in the 'hmtx' table for such glyphs should be zero.
865
    let mut h_metrics = Vec::with_capacity(usize::from(num_glyphs));
866

            
867
    for glyph_record in glyf.records().iter() {
868
        let metric = match glyph_record {
869
            GlyfRecord::Parsed(glyph) => {
870
                let bounding_box = glyph.bounding_box().unwrap_or_else(BoundingBox::empty);
871
                // NOTE(unwrap): Phantom points are populated by apply_gvar
872
                let phantom_points = glyph.phantom_points().unwrap();
873
                let pp1 = phantom_points[0].0;
874
                let pp2 = phantom_points[1].0;
875
                // pp1 = xMin - lsb
876
                // pp2 = pp1 + aw
877
                let lsb = bounding_box.x_min - pp1;
878
                let advance_width = u16::try_from(pp2 - pp1).unwrap_or(0);
879
                LongHorMetric { advance_width, lsb }
880
            }
881
            _ => unreachable!("glyph should be parsed with phantom points present"),
882
        };
883
        h_metrics.push(metric);
884
    }
885

            
886
    // TODO: Can we apply the optimisation if they're all the same at the end
887
    Ok(HmtxTable {
888
        h_metrics: ReadArrayCow::Owned(h_metrics),
889
        left_side_bearings: ReadArrayCow::Owned(vec![]),
890
    })
891
}
892

            
893
/// Applies glyph deltas from the `gvar` table to glyphs in the `glyf` table.
894
///
895
/// Takes ownership of the `glyf` table as placeholder values are swapped in
896
/// during processing (see note in body of function) and returning early would
897
/// leave the `glyf` table in an incorrect state. So we consume it and return
898
/// the modified, valid result only on success.
899
fn apply_gvar<'a>(
900
    mut glyf: GlyfTable<'a>,
901
    gvar: &GvarTable<'a>,
902
    hmtx: &HmtxTable<'a>,
903
    vmtx: Option<&HmtxTable<'a>>,
904
    os2: Option<&Os2>,
905
    hhea: &HheaTable,
906
    instance: &OwnedTuple,
907
) -> Result<GlyfTable<'a>, ReadWriteError> {
908
    for (glyph_id, glyph_record) in glyf.records_mut().iter_mut().enumerate() {
909
        // NOTE(cast): Safe as num_glyphs is u16
910
        let glyph_id = glyph_id as u16;
911
        glyph_record.parse()?;
912
        match glyph_record {
913
            GlyfRecord::Parsed(glyph) => {
914
                glyph.apply_variations(glyph_id, instance, gvar, hmtx, vmtx, os2, hhea)?;
915
            }
916
            GlyfRecord::Present { .. } => unreachable!("glyph should be parsed"),
917
        }
918
    }
919

            
920
    // Do a pass to update the bounding boxes of composite glyphs
921
    for glyph_id in 0..glyf.num_glyphs() {
922
        // We do a little take/replace dance here to work within Rust's unique (mut)
923
        // access constraints: we need to mutate the glyph but also pass an
924
        // immutable reference to the glyf table that holds it. To work around
925
        // this we swap the glyph we're processing with an empty glyph in the
926
        // glyf table and then put it back afterwards. This works because
927
        // the glyf table is required for `apply_variations` to resolve child components
928
        // in composite glyphs to calculate the bounding box, and a composite
929
        // glyph can't refer to itself so should never encounter the empty
930
        // replacement.
931
        if glyf.records()[usize::from(glyph_id)].is_composite() {
932
            // NOTE(unwrap): should not panic as glyph_id < num_glyphs
933
            let mut glyph_record = glyf.take(glyph_id).unwrap();
934
            let GlyfRecord::Parsed(Glyph::Composite(ref mut composite)) = glyph_record else {
935
                unreachable!("expected parsed composite glyph")
936
            };
937
            // Calculate the new bounding box for this composite glyph
938
            let bbox = composite
939
                .calculate_bounding_box(&glyf)?
940
                .round_out()
941
                .to_i32();
942
            composite.bounding_box = BoundingBox {
943
                x_min: bbox
944
                    .min_x()
945
                    .try_into()
946
                    .map_err(|_| ParseError::LimitExceeded)?,
947
                x_max: bbox
948
                    .max_x()
949
                    .try_into()
950
                    .map_err(|_| ParseError::LimitExceeded)?,
951
                y_min: bbox
952
                    .min_y()
953
                    .try_into()
954
                    .map_err(|_| ParseError::LimitExceeded)?,
955
                y_max: bbox
956
                    .max_y()
957
                    .try_into()
958
                    .map_err(|_| ParseError::LimitExceeded)?,
959
            };
960
            glyf.replace(glyph_id, glyph_record)?;
961
        }
962
    }
963

            
964
    Ok(glyf)
965
}
966

            
967
fn union_rect(rect: RectI, other: RectI) -> RectI {
968
    RectI::from_points(
969
        rect.origin().min(other.origin()),
970
        rect.lower_right().max(other.lower_right()),
971
    )
972
}
973

            
974
fn is_italic(tuple: &[Fixed], fvar: &FvarTable<'_>) -> bool {
975
    // If the font has a `slnt` axis and the instance has a non-zero angle for the slant then
976
    // consider it italic.
977
    let Some(slnt_index) = fvar.axes().position(|axis| axis.axis_tag == tag::SLNT) else {
978
        return false;
979
    };
980

            
981
    tuple
982
        .get(slnt_index)
983
        .filter(|&&value| value != Fixed::from(0i32))
984
        .is_some()
985
}
986

            
987
impl From<BoundingBox> for RectI {
988
    fn from(bbox: BoundingBox) -> Self {
989
        RectI::from_points(
990
            vec2i(bbox.x_min.into(), bbox.y_min.into()),
991
            vec2i(bbox.x_max.into(), bbox.y_max.into()),
992
        )
993
    }
994
}
995

            
996
impl From<ParseError> for VariationError {
997
    fn from(err: ParseError) -> VariationError {
998
        VariationError::Parse(err)
999
    }
}
impl From<CFFError> for VariationError {
    fn from(err: CFFError) -> VariationError {
        VariationError::CFF(err)
    }
}
impl From<WriteError> for VariationError {
    fn from(err: WriteError) -> VariationError {
        VariationError::Write(err)
    }
}
impl From<ReadWriteError> for VariationError {
    fn from(err: ReadWriteError) -> VariationError {
        match err {
            ReadWriteError::Read(err) => VariationError::Parse(err),
            ReadWriteError::Write(err) => VariationError::Write(err),
        }
    }
}
impl fmt::Display for VariationError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            VariationError::Parse(err) => write!(f, "variation: parse error: {}", err),
            VariationError::CFF(err) => write!(f, "variation: CFF error: {}", err),
            VariationError::Write(err) => write!(f, "variation: write error: {}", err),
            VariationError::NotVariableFont => write!(f, "variation: not a variable font"),
            VariationError::NotImplemented => {
                write!(f, "variation: unsupported variable font format")
            }
            VariationError::NameError => write!(f, "font did not contain a `name` table entry for the family name in a usable encoding"),
            VariationError::TagError => write!(f, "the list of table tags was unable to be retrieved from the font"),
        }
    }
}
impl std::error::Error for VariationError {}
impl From<ParseError> for AxisNamesError {
    fn from(err: ParseError) -> AxisNamesError {
        AxisNamesError::Parse(err)
    }
}
impl fmt::Display for AxisNamesError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AxisNamesError::Parse(err) => write!(f, "axis names: parse error: {}", err),
            AxisNamesError::NoStatTable => f.write_str("axis names: no STAT table"),
            AxisNamesError::NoNameTable => f.write_str("axis names: no name table"),
        }
    }
}
impl std::error::Error for AxisNamesError {}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::assert_close;
    use crate::cff::charstring::{ArgumentsStack, CharStringVisitorContext};
    use crate::cff::{cff2, CFFFont};
    use crate::font_data::FontData;
    use crate::tables::{OpenTypeData, OpenTypeFont};
    use crate::tests::read_fixture;
    #[test]
    fn test_generate_postscript_name_with_postscript_prefix() {
        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
        let scope = ReadScope::new(&buffer);
        let font_file = scope
            .read::<FontData<'_>>()
            .expect("unable to parse font file");
        let table_provider = font_file
            .table_provider(0)
            .expect("unable to create font provider");
        let fvar_data = table_provider
            .read_table_data(tag::FVAR)
            .expect("unable to read fvar table data");
        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>().unwrap();
        // Display SemiCondensed Thin: [100.0, 87.5, 100.0]
        let user_tuple = [Fixed::from(100.0), Fixed::from(87.5), Fixed::from(100.0)];
        let typographic_family = "Family";
        let postscript_prefix = Some(String::from("PSPrefix"));
        let postscript_name =
            generate_postscript_name(&postscript_prefix, typographic_family, &user_tuple, &fvar);
        assert_eq!(postscript_name, "PSPrefix_100wght_87.5wdth_100CTGR");
    }
    #[test]
    fn test_generate_postscript_name_without_postscript_prefix() {
        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
        let scope = ReadScope::new(&buffer);
        let font_file = scope
            .read::<FontData<'_>>()
            .expect("unable to parse font file");
        let table_provider = font_file
            .table_provider(0)
            .expect("unable to create font provider");
        let fvar_data = table_provider
            .read_table_data(tag::FVAR)
            .expect("unable to read fvar table data");
        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>().unwrap();
        // Display SemiCondensed Thin: [100.0, 87.5, 100.0]
        let user_tuple = [Fixed::from(100.0), Fixed::from(87.5), Fixed::from(100.0)];
        let typographic_family = "Family";
        let postscript_prefix = None;
        let postscript_name =
            generate_postscript_name(&postscript_prefix, typographic_family, &user_tuple, &fvar);
        assert_eq!(postscript_name, "Family_100wght_87.5wdth_100CTGR");
    }
    #[test]
    fn test_generate_postscript_name_omit_defaults() {
        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
        let scope = ReadScope::new(&buffer);
        let font_file = scope
            .read::<FontData<'_>>()
            .expect("unable to parse font file");
        let table_provider = font_file
            .table_provider(0)
            .expect("unable to create font provider");
        let fvar_data = table_provider
            .read_table_data(tag::FVAR)
            .expect("unable to read fvar table data");
        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>().unwrap();
        let user_tuple = [Fixed::from(400.0), Fixed::from(87.5), Fixed::from(0.0)];
        let typographic_family = "Family";
        let postscript_prefix = Some(String::from("PSPrefix"));
        let postscript_name =
            generate_postscript_name(&postscript_prefix, typographic_family, &user_tuple, &fvar);
        assert_eq!(postscript_name, "PSPrefix_87.5wdth");
    }
    #[test]
    fn test_generate_postscript_name_strip_forbidden_chars() {
        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
        let scope = ReadScope::new(&buffer);
        let font_file = scope
            .read::<FontData<'_>>()
            .expect("unable to parse font file");
        let table_provider = font_file
            .table_provider(0)
            .expect("unable to create font provider");
        let fvar_data = table_provider
            .read_table_data(tag::FVAR)
            .expect("unable to read fvar table data");
        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>().unwrap();
        let user_tuple = [Fixed::from(100.0), Fixed::from(87.5), Fixed::from(100.0)];
        let typographic_family = "These aren't allowed []<>!";
        let postscript_name =
            generate_postscript_name(&None, typographic_family, &user_tuple, &fvar);
        assert_eq!(
            postscript_name,
            "Thesearentallowed_100wght_87.5wdth_100CTGR"
        );
    }
    #[test]
    fn test_generate_postscript_name_truncate() {
        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
        let scope = ReadScope::new(&buffer);
        let font_file = scope
            .read::<FontData<'_>>()
            .expect("unable to parse font file");
        let table_provider = font_file
            .table_provider(0)
            .expect("unable to create font provider");
        let fvar_data = table_provider
            .read_table_data(tag::FVAR)
            .expect("unable to read fvar table data");
        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>().unwrap();
        let user_tuple = [Fixed::from(100.0), Fixed::from(87.5), Fixed::from(100.0)];
        let typographic_family = "IfAfterConstructingThePostScriptNameInThisWayTheLengthIsGreaterThan127CharactersThenConstructTheLastResortPostScriptName";
        let postscript_name =
            generate_postscript_name(&None, typographic_family, &user_tuple, &fvar);
        assert!(postscript_name.len() <= 63);
        assert_eq!(
            postscript_name,
            "IfAfterConstructingThePostScriptNameInThisWayTheLen-189E39CF..."
        );
    }
    #[test]
    fn typographic_subfamily_name_non_elidable() -> Result<(), ReadWriteError> {
        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
        let scope = ReadScope::new(&buffer);
        let font_file = scope.read::<FontData<'_>>()?;
        let table_provider = font_file.table_provider(0)?;
        let fvar_data = table_provider.read_table_data(tag::FVAR)?;
        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>()?;
        let stat_data = table_provider.read_table_data(tag::STAT)?;
        let stat = ReadScope::new(&stat_data).read::<StatTable<'_>>()?;
        let name_data = table_provider.read_table_data(tag::NAME)?;
        let name = ReadScope::new(&name_data).read::<NameTable<'_>>()?;
        let user_tuple = [Fixed::from(100.0), Fixed::from(87.5), Fixed::from(100.0)];
        let name = typographic_subfamily_name(&user_tuple, &fvar, &stat, &name, "Default").unwrap();
        assert_eq!(name, "Thin SemiCondensed Display");
        Ok(())
    }
    #[test]
    fn typographic_subfamily_name_elidable() -> Result<(), ReadWriteError> {
        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
        let scope = ReadScope::new(&buffer);
        let font_file = scope.read::<FontData<'_>>()?;
        let table_provider = font_file.table_provider(0)?;
        let fvar_data = table_provider.read_table_data(tag::FVAR)?;
        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>()?;
        let stat_data = table_provider.read_table_data(tag::STAT)?;
        let stat = ReadScope::new(&stat_data).read::<StatTable<'_>>()?;
        let name_data = table_provider.read_table_data(tag::NAME)?;
        let name = ReadScope::new(&name_data).read::<NameTable<'_>>()?;
        // - wght = min: 100, max: 900, default: 400
        // - wdth = min: 62.5, max: 100, default: 100
        // - CTGR = min: 0, max: 100, default: 0
        // Use default values to trigger elidable fallback
        let user_tuple = [Fixed::from(400.0), Fixed::from(100.0), Fixed::from(0.0)];
        let name = typographic_subfamily_name(&user_tuple, &fvar, &stat, &name, "Default").unwrap();
        assert_eq!(name, "Regular");
        Ok(())
    }
    #[test]
    fn subfamily_name_axis_value_format3() -> Result<(), ReadWriteError> {
        let buffer = read_fixture("tests/fonts/variable/Inter[slnt,wght].abc.ttf");
        let scope = ReadScope::new(&buffer);
        let font_file = scope.read::<FontData<'_>>()?;
        let table_provider = font_file.table_provider(0)?;
        let fvar_data = table_provider.read_table_data(tag::FVAR)?;
        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>()?;
        let stat_data = table_provider.read_table_data(tag::STAT)?;
        let stat = ReadScope::new(&stat_data).read::<StatTable<'_>>()?;
        let name_data = table_provider.read_table_data(tag::NAME)?;
        let name = ReadScope::new(&name_data).read::<NameTable<'_>>()?;
        // - wght = min: 100, max: 900, default: 400
        // - slnt = min: -10, max: 0, default: 0
        // slnt value is the elidable value. In a previous version of the code it was not elided
        // in the output because STAT axis value table format 3 was not processed.
        let user_tuple = [Fixed::from(700.0), Fixed::from(0.0)];
        let name = typographic_subfamily_name(&user_tuple, &fvar, &stat, &name, "Default").unwrap();
        assert_eq!(name, "Bold");
        Ok(())
    }
    #[test]
    fn test_fixed_to_float() {
        assert_close!(fixed_to_min_float(Fixed::from(0)), 0., f64::EPSILON);
        assert_close!(fixed_to_min_float(Fixed::from(900)), 900., f64::EPSILON);
        assert_close!(fixed_to_min_float(Fixed::from(5.5)), 5.5, f64::EPSILON);
        assert_close!(fixed_to_min_float(Fixed::from(2.9)), 2.9, f64::EPSILON);
        assert_close!(fixed_to_min_float(Fixed::from(-1.4)), -1.4, f64::EPSILON);
        assert_close!(
            fixed_to_min_float(Fixed::from(-1. + (1. / 65536.))),
            -0.99998,
            f64::EPSILON
        );
    }
    // This font triggers the hvar path through create_hmtx_table and exposed bug in it.
    #[test]
    fn instance_underline_test() -> Result<(), ReadWriteError> {
        let buffer = read_fixture("tests/fonts/variable/UnderlineTest-VF.ttf");
        let scope = ReadScope::new(&buffer);
        let font_file = scope.read::<FontData<'_>>()?;
        let table_provider = font_file.table_provider(0)?;
        let user_tuple = [Fixed::from(500), Fixed::from(500)];
        let (inst, _tuple) = instance(&table_provider, &user_tuple).unwrap();
        let scope = ReadScope::new(&inst);
        let font_file = scope.read::<FontData<'_>>()?;
        let table_provider = font_file.table_provider(0)?;
        let maxp =
            ReadScope::new(&table_provider.read_table_data(tag::MAXP)?).read::<MaxpTable>()?;
        let hhea =
            ReadScope::new(&table_provider.read_table_data(tag::HHEA)?).read::<HheaTable>()?;
        let hmtx_data = table_provider.read_table_data(tag::HMTX)?;
        assert!(ReadScope::new(&hmtx_data)
            .read_dep::<HmtxTable<'_>>((
                usize::from(maxp.num_glyphs),
                usize::from(hhea.num_h_metrics),
            ))
            .is_ok());
        Ok(())
    }
    #[test]
    #[cfg(feature = "prince")]
    fn instance_minipax() -> Result<(), ReadWriteError> {
        let buffer =
            read_fixture("../../../tests/data/fonts/minipax/variable/Minipax Variable.ttf");
        let scope = ReadScope::new(&buffer);
        let font_file = scope.read::<FontData<'_>>()?;
        let table_provider = font_file.table_provider(0)?;
        let user_tuple = [Fixed::from(600)];
        assert!(instance(&table_provider, &user_tuple).is_ok());
        Ok(())
    }
    #[test]
    fn test_axis_names() {
        let buffer = read_fixture("tests/fonts/variable/UnderlineTest-VF.ttf");
        let scope = ReadScope::new(&buffer);
        let font_file = scope.read::<FontData<'_>>().unwrap();
        let table_provider = font_file.table_provider(0).unwrap();
        let names = axis_names(&table_provider).unwrap();
        assert_eq!(
            names,
            vec![
                NamedAxis {
                    tag: tag!(b"UNDO"),
                    name: Cow::from("Underline Offset"),
                    ordering: 0
                },
                NamedAxis {
                    tag: tag!(b"UNDS"),
                    name: Cow::from("Underline Size"),
                    ordering: 1
                }
            ]
        );
    }
    #[test]
    fn test_axis_names_not_variable() {
        let buffer = read_fixture("tests/fonts/opentype/SourceCodePro-Regular.otf");
        let scope = ReadScope::new(&buffer);
        let font_file = scope.read::<FontData<'_>>().unwrap();
        let table_provider = font_file.table_provider(0).unwrap();
        let names = axis_names(&table_provider);
        assert_eq!(names, Err(AxisNamesError::NoStatTable));
    }
    #[test]
    fn instance_cff2() -> Result<(), VariationError> {
        let buffer = read_fixture("tests/fonts/opentype/cff2/SourceSansVariable-Roman.abc.otf");
        let scope = ReadScope::new(&buffer);
        let font_file = scope.read::<FontData<'_>>()?;
        let table_provider = font_file.table_provider(0)?;
        let user_tuple = [Fixed::from(650.0)];
        let (res, _tuple) = instance(&table_provider, &user_tuple)?;
        // Read the font back in
        let otf = ReadScope::new(&res).read::<OpenTypeFont<'_>>().unwrap();
        let offset_table = match otf.data {
            OpenTypeData::Single(ttf) => ttf,
            OpenTypeData::Collection(_) => unreachable!(),
        };
        let cff2_table_data = offset_table
            .read_table(&otf.scope, tag::CFF2)
            .unwrap()
            .unwrap();
        let cff2 = cff2_table_data
            .read::<CFF2<'_>>()
            .expect("unable to parse CFF2 instance");
        for glyph_id in 0..cff2.char_strings_index.len() as u16 {
            let font_dict_index = cff2
                .fd_select
                .as_ref()
                .and_then(|fd_select| fd_select.font_dict_index(glyph_id))
                .unwrap_or(0);
            let font_dict = &cff2.fonts[usize::from(font_dict_index)];
            println!("-- glyph {glyph_id} --");
            let mut visitor = crate::cff::charstring::DebugVisitor;
            let variable = None;
            let mut ctx = CharStringVisitorContext::new(
                glyph_id,
                &cff2.char_strings_index,
                font_dict.local_subr_index.as_ref(),
                &cff2.global_subr_index,
                variable,
            );
            let mut stack = ArgumentsStack {
                data: &mut [0.0; cff2::MAX_OPERANDS],
                len: 0,
                max_len: cff2::MAX_OPERANDS,
            };
            ctx.visit(CFFFont::CFF2(&font_dict), &mut stack, &mut visitor)?;
        }
        Ok(())
    }
}