Skip to main content

pdfrum_font/glyphs/
mod.rs

1//! Where a glyph index becomes an outline, an advance, or a bounding box.
2//!
3//! Two backends sit behind one enum: `skrifa` for everything with an SFNT or
4//! CFF shape, and `pdfrum-type1` for Type 1 programs, which Fontations reads
5//! only at the weight vector the file ships with — not enough for the
6//! Multiple-Master fallback faces PDFium leans on.
7
8mod cache;
9mod face;
10
11pub use cache::{GlyphCache, GlyphKey};
12pub use face::{Charmap, CharmapId, Face};
13
14pub use crate::descriptor::em_adjust;
15pub(crate) use crate::descriptor::normalize_font_metric;
16
17use crate::Gid;
18use crate::ids::GlyphName;
19use pdfrum_common::kurbo::{Affine, BezPath, Rect};
20use pdfrum_common::{Diagnostics, Limits};
21use std::sync::Arc;
22
23/// Where glyphs come from.
24///
25/// `Fontations` covers TrueType, bare CFF, OpenType and everything else with a
26/// table directory; `Type1` covers PFA/PFB programs and the two Multiple-Master
27/// fallback faces; `None` is a Type3 font or a program nothing could read.
28#[derive(Debug, Clone, Default)]
29pub enum GlyphSource {
30    /// A face read by `skrifa`, over bytes this value owns.
31    Fontations(Face),
32    /// A Type 1 program, optionally instantiated at design coordinates.
33    Type1(Arc<pdfrum_type1::Type1Font>),
34    /// No glyphs at all.
35    #[default]
36    None,
37}
38
39/// The parameters that change what a glyph *looks like*, beyond its index.
40///
41/// For an ordinary face these are all inert. For a Multiple-Master face they
42/// are not: `dest_width` alone changes the outline, which is why the glyph
43/// cache keys on them.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
45pub(crate) struct GlyphParams {
46    /// The width the PDF declared for this character code, in 1000/em units.
47    /// Zero means "whatever the face does naturally".
48    pub dest_width: i32,
49    /// The substitution weight, or 0 for the face's own.
50    pub weight: i32,
51}
52
53impl GlyphSource {
54    /// Open a TrueType, OpenType, bare-CFF, or Type 1 program from its bytes.
55    ///
56    /// `None` when no backend recognises the blob.
57    #[must_use]
58    pub fn from_bytes(bytes: impl Into<Arc<[u8]>>) -> Option<Self> {
59        let bytes = bytes.into();
60        if let Some(face) = Face::new(Arc::clone(&bytes), 0) {
61            return Some(Self::Fontations(face));
62        }
63        let mut diags = Diagnostics::default();
64        pdfrum_type1::Type1Font::parse(&bytes, &Limits::default(), &mut diags)
65            .ok()
66            .map(|font| Self::Type1(Arc::new(font)))
67    }
68
69    /// Is there a face at all?
70    #[must_use]
71    pub(crate) fn is_some(&self) -> bool {
72        !matches!(self, Self::None)
73    }
74
75    /// Design units per em; 0 when there is no face.
76    #[must_use]
77    pub fn units_per_em(&self) -> u16 {
78        match self {
79            Self::Fontations(f) => f.units_per_em(),
80            Self::Type1(f) => f.units_per_em(),
81            Self::None => 0,
82        }
83    }
84
85    /// How many glyphs the face declares.
86    #[must_use]
87    pub fn num_glyphs(&self) -> u32 {
88        match self {
89            Self::Fontations(f) => f.num_glyphs(),
90            Self::Type1(f) => f.num_glyphs(),
91            Self::None => 0,
92        }
93    }
94
95    /// Is this a TrueType-shaped face? PDFium's per-glyph fallback and its
96    /// `ShouldUseFont` test both branch on it.
97    #[must_use]
98    pub fn is_truetype(&self) -> bool {
99        match self {
100            Self::Fontations(f) => f.is_truetype(),
101            Self::Type1(_) | Self::None => false,
102        }
103    }
104
105    /// The glyph a character code selects through the face's *currently
106    /// selected* charmap.
107    ///
108    /// Returns 0 rather than `None` on a miss, because every ladder in the former working note
109    /// and the former working note tests `!= 0` and 0 is `.notdef` either way.
110    #[must_use]
111    pub fn char_index(&self, charmap: Charmap, code: u32) -> u16 {
112        match self {
113            Self::Fontations(f) => f.char_index(charmap, code),
114            // A Type 1 face's "charmap" is its built-in encoding vector for a
115            // byte code, and the synthesized Unicode map otherwise.
116            Self::Type1(f) => {
117                let gid = match charmap {
118                    Charmap::Unicode => char::from_u32(code).and_then(|c| f.unicode_to_gid(c)),
119                    _ => u8::try_from(code).ok().and_then(|b| f.code_to_gid(b)),
120                };
121                gid.map_or(0, |g| g.0)
122            }
123            Self::None => 0,
124        }
125    }
126
127    /// The glyph a *name* selects. Zero on a miss, as `FT_Get_Name_Index`
128    /// leaves it.
129    #[must_use]
130    pub(crate) fn name_index(&self, name: &[u8]) -> u16 {
131        let Ok(name) = std::str::from_utf8(name) else {
132            return 0;
133        };
134        match self {
135            Self::Fontations(f) => f.name_index(name),
136            Self::Type1(f) => f.name_to_gid(name).map_or(0, |g| g.0),
137            Self::None => 0,
138        }
139    }
140
141    /// A glyph's own name, when the face has a name table.
142    #[must_use]
143    pub(crate) fn glyph_name(&self, gid: Gid) -> Option<GlyphName> {
144        match self {
145            Self::Fontations(f) => f.glyph_name(gid).map(|n| GlyphName::new(n.into_bytes())),
146            Self::Type1(f) => f
147                .glyph_name(gid.into())
148                .map(|n| GlyphName::new(n.as_bytes().to_vec())),
149            Self::None => None,
150        }
151    }
152
153    /// Whether the face can name its glyphs at all.
154    #[must_use]
155    pub(crate) fn has_glyph_names(&self) -> bool {
156        match self {
157            Self::Fontations(f) => f.has_glyph_names(),
158            Self::Type1(_) => true,
159            Self::None => false,
160        }
161    }
162
163    /// The charmaps the face declares, as `(platform, encoding)` pairs in
164    /// table order.
165    #[must_use]
166    pub fn charmaps(&self) -> Vec<CharmapId> {
167        match self {
168            Self::Fontations(f) => f.charmaps(),
169            // A Type 1 face exposes a synthesized Unicode charmap first and
170            // its own encoding second — the shape `UseType1Charmap` expects.
171            Self::Type1(_) => vec![CharmapId::UNICODE_SYNTHETIC, CharmapId::ADOBE_CUSTOM],
172            Self::None => Vec::new(),
173        }
174    }
175
176    /// A glyph's outline in **1000/em text space**.
177    ///
178    /// Three things happen here that a plain `draw` would not do:
179    ///
180    /// - The outline is requested **unscaled**, in font units, and then scaled
181    ///   by `1000 / upem` — matching the Fontations path PDFium itself is
182    ///   moving to, rather than its FreeType path's 64-pixel dance.
183    /// - **Degenerate trailing contours are trimmed** (`Outline_CheckEmptyContour`),
184    ///   because `kurbo` will happily hold a zero-area contour that changes
185    ///   what a rasterizer produces.
186    /// - An outline that trims to nothing yields `None`, not an empty path.
187    #[must_use]
188    pub(crate) fn outline(&self, gid: Gid, params: GlyphParams) -> Option<BezPath> {
189        let upem = self.units_per_em();
190        let raw = match self {
191            Self::Fontations(f) => {
192                // An instructed composite is not fully described by its
193                // component offsets; the bytecode is what places them. Run it
194                // for those glyphs, and take the unhinted outline back if the
195                // interpreter declines the face.
196                if f.composite_is_instructed(gid)
197                    && let Some(hinted) = self.hinted_outline(gid)
198                {
199                    return Some(hinted);
200                }
201                f.outline(gid)?
202            }
203            Self::Type1(f) => match Self::mm_instance(f, gid, params) {
204                Some(inst) => inst.outline(gid.into())?.0,
205                None => f.outline(gid.into())?.0,
206            },
207            Self::None => return None,
208        };
209        let trimmed = trim_empty_contours(raw)?;
210        if upem == 0 || upem == 1000 {
211            return Some(trimmed);
212        }
213        let scale = 1000.0 / f64::from(upem);
214        Some(Affine::scale(scale) * trimmed)
215    }
216
217    /// A glyph's outline in 1000/em text space, **grid-fitted at 64 ppem**.
218    ///
219    /// The same space [`Self::outline`] returns, so the two are interchangeable
220    /// at every call site and the renderer's glyph matrix does not change. The
221    /// difference is what happened before the scaling: this one ran the face's
222    /// own hinting programs against a 64-pixel grid, which is what an SFNT
223    /// face drawn as a *bitmap* gets.
224    ///
225    /// The conversion is a pure scale — `1000 / 64` — because a 64-ppem
226    /// instance draws in 64ths of an em. Grid-fitting at a pinned ppem and
227    /// then scaling is not the same thing as grid-fitting at the size the
228    /// glyph is drawn at, and the pinned ppem is the one that is correct here.
229    ///
230    /// `None` for every face that is not hinted, which is the caller's signal
231    /// to fall back to [`Self::outline`] rather than to draw nothing: a face
232    /// with no table directory (every bare CFF, so every base-14
233    /// substitution, and every `Type1` program), and a face whose own
234    /// programs the interpreter refuses.
235    // The two `None` arms are `cfx_face.cpp:841-843`'s `!IsTtOt()` gate and
236    // `cfx_face.cpp:849-857`'s pedantic-load failure, which reloads the glyph
237    // unhinted; the 64-ppem grid is `CFX_Face::RenderGlyph`'s.
238    #[must_use]
239    pub(crate) fn hinted_outline(&self, gid: Gid) -> Option<BezPath> {
240        let Self::Fontations(f) = self else {
241            return None;
242        };
243        let raw = f.hinted_outline(gid)?;
244        let trimmed = trim_empty_contours(raw)?;
245        Some(Affine::scale(1000.0 / f64::from(Face::HINT_PPEM)) * trimmed)
246    }
247
248    /// Advance in 1000/em units at the face's default location.
249    #[must_use]
250    pub fn default_advance(&self, gid: Gid) -> i32 {
251        self.advance(gid, GlyphParams::default())
252    }
253
254    /// A glyph's advance width in 1000/em units.
255    ///
256    /// Uses the **truncating** normalizer [`em_adjust`], not the rounding
257    /// `normalize_font_metric` — the two disagree for half the inputs, and an
258    /// advance takes the truncating one.
259    #[must_use]
260    pub(crate) fn advance(&self, gid: Gid, params: GlyphParams) -> i32 {
261        let upem = self.units_per_em();
262        let raw = match self {
263            Self::Fontations(f) => f.advance(gid),
264            Self::Type1(f) => match Self::mm_instance(f, gid, params) {
265                Some(inst) => inst.advance(gid.into()),
266                None => f.outline(gid.into()).map(|(_, a)| a),
267            },
268            Self::None => None,
269        };
270        let Some(raw) = raw else { return 0 };
271        // The C++'s range guard: an advance that would overflow the ×1000
272        // scaling reports zero rather than a wrapped value.
273        let raw = raw as i64;
274        if raw < i64::from(i32::MIN) / 1000 || raw > i64::from(i32::MAX) / 1000 {
275            return 0;
276        }
277        em_adjust(raw as i32, upem)
278    }
279
280    /// A glyph's advance through the **rounding** normalizer, which is what
281    /// `LoadCharMetrics` uses when filling in a width the PDF omitted.
282    #[must_use]
283    pub(crate) fn advance_tt(&self, gid: Gid) -> i32 {
284        let upem = self.units_per_em();
285        let raw = match self {
286            Self::Fontations(f) => f.advance(gid),
287            Self::Type1(f) => f.outline(gid.into()).map(|(_, a)| a),
288            Self::None => None,
289        };
290        raw.map_or(0, |a| normalize_font_metric(a as i64, upem))
291    }
292
293    /// A glyph's bounding box in 1000/em units, y-up.
294    ///
295    /// PDFium's own differential check maps skrifa's `(x_min, y_min, x_max,
296    /// y_max)` to `(left, top, right, bottom)` in its y-down convention and
297    /// asserts agreement within 2 units; we take that mapping and keep the
298    /// result y-up, which is what `kurbo::Rect` means.
299    #[must_use]
300    pub(crate) fn glyph_bbox(&self, gid: Gid) -> Option<Rect> {
301        let upem = self.units_per_em();
302        let raw = match self {
303            Self::Fontations(f) => f.glyph_bbox(gid)?,
304            Self::Type1(f) => f.glyph_bounds(gid.into())?,
305            Self::None => return None,
306        };
307        let n = |v: f64| f64::from(normalize_font_metric(v as i64, upem));
308        Some(Rect::new(n(raw.x0), n(raw.y0), n(raw.x1), n(raw.y1)))
309    }
310
311    /// The design-space instance to draw a Multiple-Master glyph at, solving
312    /// the width axis for `dest_width` (`AdjustVariationParams`, the former working note).
313    ///
314    /// Axis 0 is weight, taken **directly** as a design coordinate. Axis 1 is
315    /// width, found by probing the advance at both ends of the axis and
316    /// interpolating — **without clamping**, so an extreme `dest_width`
317    /// deliberately extrapolates past the axis.
318    fn mm_instance(
319        font: &pdfrum_type1::Type1Font,
320        gid: Gid,
321        params: GlyphParams,
322    ) -> Option<pdfrum_type1::Type1Instance<'_>> {
323        let axes = font.mm_axes()?;
324        let weight_axis = axes.first()?;
325        let width_axis = axes.get(1)?;
326
327        let weight = if params.weight == 0 {
328            weight_axis.default
329        } else {
330            params.weight as f32
331        };
332
333        if params.dest_width == 0 {
334            return font.instantiate(&[weight, width_axis.default]);
335        }
336
337        let upem = font.units_per_em();
338        let probe = |coord: f32| -> Option<i32> {
339            let inst = font.instantiate(&[weight, coord])?;
340            let adv = inst.advance(gid.into())?;
341            Some(em_adjust(adv as i32, upem))
342        };
343        let (lo, hi) = (width_axis.min, width_axis.max);
344        let min_w = probe(lo)?;
345        let max_w = probe(hi)?;
346        if max_w == min_w {
347            // Degenerate: the C++ leaves the coordinates at the max probe.
348            return font.instantiate(&[weight, hi]);
349        }
350        let t = (params.dest_width - min_w) as f32 / (max_w - min_w) as f32;
351        font.instantiate(&[weight, (hi - lo).mul_add(t, lo)])
352    }
353
354    /// PostScript name, or a family/style display name, when the face has one.
355    #[must_use]
356    pub fn postscript_name(&self) -> Option<String> {
357        match self {
358            Self::Fontations(f) => f.postscript_name(),
359            Self::Type1(f) => f
360                .postscript_name()
361                .map(ToOwned::to_owned)
362                .or_else(|| f.family_name().map(ToOwned::to_owned)),
363            Self::None => None,
364        }
365    }
366
367    /// Fixed pitch: `post.isFixedPitch`, or Type 1 `/isFixedPitch`.
368    #[must_use]
369    pub fn is_fixed_pitch(&self) -> bool {
370        match self {
371            Self::Fontations(f) => f.is_fixed_pitch(),
372            Self::Type1(f) => f.is_fixed_pitch(),
373            Self::None => false,
374        }
375    }
376
377    /// Italic: OS/2 / `macStyle` / `post.italicAngle`, or a Type 1 `/ItalicAngle`.
378    #[must_use]
379    pub fn is_italic(&self) -> bool {
380        match self {
381            Self::Fontations(f) => f.is_italic(),
382            Self::Type1(f) => f.italic_angle() != 0.0,
383            Self::None => false,
384        }
385    }
386
387    /// Bold: OS/2 / `macStyle`, or a Type 1 name containing `Bold` / `Black`.
388    #[must_use]
389    pub fn is_bold(&self) -> bool {
390        match self {
391            Self::Fontations(f) => f.is_bold(),
392            Self::Type1(f) => {
393                let name = f.postscript_name().or_else(|| f.full_name()).unwrap_or("");
394                name.contains("Bold") || name.contains("Black")
395            }
396            Self::None => false,
397        }
398    }
399
400    /// OS/2 `sCapHeight` in font units, when present.
401    #[must_use]
402    pub fn cap_height_unscaled(&self) -> Option<f32> {
403        match self {
404            Self::Fontations(f) => f.cap_height(),
405            Self::Type1(_) | Self::None => None,
406        }
407    }
408
409    /// Ascender in font units (`hhea`, or the Type 1 bbox top).
410    #[must_use]
411    pub fn unscaled_ascent(&self) -> Option<i32> {
412        match self {
413            Self::Fontations(f) => f.metrics().and_then(|m| i32::try_from(m.ascender).ok()),
414            Self::Type1(f) => Some(f.bbox().y1 as i32),
415            Self::None => None,
416        }
417    }
418
419    /// Descender in font units (`hhea`, or the Type 1 bbox bottom).
420    #[must_use]
421    pub fn unscaled_descent(&self) -> Option<i32> {
422        match self {
423            Self::Fontations(f) => f.metrics().and_then(|m| i32::try_from(m.descender).ok()),
424            Self::Type1(f) => Some(f.bbox().y0 as i32),
425            Self::None => None,
426        }
427    }
428
429    /// Font bounding box in font units, `(left, bottom, right, top)`.
430    #[must_use]
431    pub fn unscaled_bbox(&self) -> Option<(i32, i32, i32, i32)> {
432        match self {
433            Self::Fontations(f) => {
434                let m = f.metrics()?;
435                Some((
436                    i32::try_from(m.bbox_left).ok()?,
437                    i32::try_from(m.bbox_bottom).ok()?,
438                    i32::try_from(m.bbox_right).ok()?,
439                    i32::try_from(m.bbox_top).ok()?,
440                ))
441            }
442            Self::Type1(f) => {
443                let b = f.bbox();
444                Some((b.x0 as i32, b.y0 as i32, b.x1 as i32, b.y1 as i32))
445            }
446            Self::None => None,
447        }
448    }
449
450    /// Unicode → glyph mappings with `code <= max`, sorted by codepoint.
451    ///
452    /// A miss is omitted rather than recorded as glyph 0.
453    #[must_use]
454    pub fn unicode_mappings(&self, max: u32) -> Vec<(u32, u16)> {
455        match self {
456            Self::Fontations(f) => f.unicode_mappings(max),
457            Self::Type1(f) => {
458                let mut out: Vec<(u32, u16)> = f
459                    .unicode_pairs()
460                    .filter(|(ch, gid)| u32::from(*ch) <= max && gid.0 != 0)
461                    .map(|(ch, gid)| (u32::from(ch), gid.0))
462                    .collect();
463                out.sort_unstable_by_key(|(cp, _)| *cp);
464                out
465            }
466            Self::None => Vec::new(),
467        }
468    }
469}
470
471/// Drop degenerate trailing contours (`Outline_CheckEmptyContour`).
472///
473/// FreeType's decomposition leaves two shapes behind that draw nothing but do
474/// change a rasterizer's output: a `MoveTo` followed by a line back to the
475/// same point, and a `MoveTo` followed by three curves all landing on it. Both
476/// are trimmed, repeatedly, and an outline that trims away entirely yields
477/// `None` rather than an empty path.
478fn trim_empty_contours(path: BezPath) -> Option<BezPath> {
479    use pdfrum_common::kurbo::PathEl;
480
481    let mut els: Vec<PathEl> = path.into_iter().collect();
482    loop {
483        // A `ClosePath` is not itself degenerate; look past it.
484        let end = els
485            .iter()
486            .rposition(|e| !matches!(e, PathEl::ClosePath))
487            .map_or(0, |i| i + 1);
488
489        // `[MoveTo(p), LineTo(p)]`.
490        if end >= 2
491            && let (Some(PathEl::MoveTo(a)), Some(PathEl::LineTo(b))) =
492                (els.get(end - 2), els.get(end - 1))
493            && a == b
494        {
495            els.truncate(end - 2);
496            continue;
497        }
498        // `[MoveTo(p), CurveTo(_,_,p) × 3]`.
499        if end >= 4
500            && let (
501                Some(PathEl::MoveTo(a)),
502                Some(PathEl::CurveTo(_, _, b)),
503                Some(PathEl::CurveTo(_, _, c)),
504                Some(PathEl::CurveTo(_, _, d)),
505            ) = (
506                els.get(end - 4),
507                els.get(end - 3),
508                els.get(end - 2),
509                els.get(end - 1),
510            )
511            && a == b
512            && b == c
513            && c == d
514        {
515            els.truncate(end - 4);
516            continue;
517        }
518        break;
519    }
520    if els.iter().all(|e| matches!(e, PathEl::ClosePath)) {
521        return None;
522    }
523    let out = BezPath::from_vec(els);
524    if out.elements().is_empty() {
525        None
526    } else {
527        Some(out)
528    }
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534    use pdfrum_common::kurbo::{PathEl, Point};
535
536    #[test]
537    fn a_move_and_a_line_back_to_it_is_trimmed() {
538        let mut p = BezPath::new();
539        p.move_to((10.0, 10.0));
540        p.line_to((50.0, 10.0));
541        p.line_to((50.0, 50.0));
542        p.close_path();
543        p.move_to((7.0, 7.0));
544        p.line_to((7.0, 7.0));
545        let trimmed = trim_empty_contours(p).expect("the real contour survives");
546        assert_eq!(trimmed.elements().len(), 4);
547        assert!(matches!(
548            trimmed.elements().first(),
549            Some(PathEl::MoveTo(_))
550        ));
551    }
552
553    #[test]
554    fn a_move_and_three_curves_to_it_is_trimmed() {
555        let mut p = BezPath::new();
556        p.move_to((0.0, 0.0));
557        p.line_to((10.0, 0.0));
558        p.close_path();
559        let q = Point::new(3.0, 3.0);
560        p.move_to(q);
561        for _ in 0..3 {
562            p.curve_to(q, q, q);
563        }
564        let trimmed = trim_empty_contours(p).expect("the real contour survives");
565        assert_eq!(trimmed.elements().len(), 3);
566    }
567
568    #[test]
569    fn repeated_degenerate_contours_are_all_trimmed() {
570        let mut p = BezPath::new();
571        p.move_to((0.0, 0.0));
572        p.line_to((10.0, 0.0));
573        p.close_path();
574        for i in 0..3 {
575            let q = Point::new(f64::from(i), f64::from(i));
576            p.move_to(q);
577            p.line_to(q);
578        }
579        let trimmed = trim_empty_contours(p).expect("the real contour survives");
580        assert_eq!(trimmed.elements().len(), 3);
581    }
582
583    #[test]
584    fn an_entirely_degenerate_outline_is_none_not_an_empty_path() {
585        let mut p = BezPath::new();
586        p.move_to((5.0, 5.0));
587        p.line_to((5.0, 5.0));
588        assert!(trim_empty_contours(p).is_none());
589        assert!(trim_empty_contours(BezPath::new()).is_none());
590    }
591
592    #[test]
593    fn a_healthy_outline_is_untouched() {
594        let mut p = BezPath::new();
595        p.move_to((0.0, 0.0));
596        p.curve_to((10.0, 0.0), (10.0, 10.0), (0.0, 10.0));
597        p.close_path();
598        let n = p.elements().len();
599        assert_eq!(trim_empty_contours(p).map(|q| q.elements().len()), Some(n));
600    }
601
602    #[test]
603    fn only_a_composite_carrying_bytecode_is_reported_as_instructed() {
604        let bytes = crate::testfonts::load("tt_composite_instructions.ttf");
605        let face = Face::new(bytes.into(), 0).expect("the fixture loads");
606        // Glyph 1 is simple, 2 a composite with no instructions, 3 the
607        // instructed composite; only the last needs the interpreter.
608        assert!(!face.composite_is_instructed(Gid(1)));
609        assert!(!face.composite_is_instructed(Gid(2)));
610        assert!(face.composite_is_instructed(Gid(3)));
611    }
612
613    #[test]
614    fn a_gid_past_the_end_of_loca_is_not_instructed() {
615        let bytes = crate::testfonts::load("tt_composite_instructions.ttf");
616        let face = Face::new(bytes.into(), 0).expect("the fixture loads");
617        assert!(!face.composite_is_instructed(Gid(u16::MAX)));
618    }
619
620    #[test]
621    fn the_empty_source_answers_everything_with_nothing() {
622        let s = GlyphSource::None;
623        assert!(!s.is_some());
624        assert_eq!(s.units_per_em(), 0);
625        assert_eq!(s.num_glyphs(), 0);
626        assert!(!s.is_truetype());
627        assert_eq!(s.char_index(Charmap::Unicode, 0x41), 0);
628        assert_eq!(s.name_index(b"A"), 0);
629        assert!(s.glyph_name(Gid(0)).is_none());
630        assert!(!s.has_glyph_names());
631        assert!(s.charmaps().is_empty());
632        assert!(s.outline(Gid(0), GlyphParams::default()).is_none());
633        assert_eq!(s.advance(Gid(0), GlyphParams::default()), 0);
634        assert_eq!(s.advance_tt(Gid(0)), 0);
635        assert!(s.glyph_bbox(Gid(0)).is_none());
636    }
637}