Skip to main content

pdfrum_font/glyphs/
face.rs

1//! The `skrifa` / `read-fonts` adapter.
2//!
3//! PDFium drives FreeType, which carries a *selected charmap* as face state
4//! and mutates it as the glyph ladders walk. Selecting a charmap on a shared
5//! face is exactly the kind of hidden mutation this crate avoids, so the
6//! selection becomes a value — [`Charmap`] — that the ladders pass to every
7//! lookup. The ladders' sequence of "select this, try that" reads the same;
8//! nothing is hidden in the face.
9
10use crate::Gid;
11use pdfrum_common::kurbo::{BezPath, Rect};
12use read_fonts::TableProvider;
13use read_fonts::tables::cmap::PlatformId;
14use skrifa::MetadataProvider;
15use skrifa::instance::{LocationRef, Size};
16use skrifa::outline::{
17    DrawSettings, Engine as HintingEngine, HintingInstance, HintingOptions, OutlinePen,
18    Target as HintingTarget,
19};
20use std::collections::HashMap;
21use std::fmt;
22use std::sync::{Arc, OnceLock, RwLock};
23
24/// A charmap's `(platform, encoding)` identity, as the `cmap` table declares
25/// it.
26///
27/// PDFium compares these pairs literally — `(3,1)` for Windows Unicode,
28/// `(3,0)` for Windows Symbol, `(1,0)` for Mac Roman — and the *order* it
29/// prefers them in flips with the symbolic flag, so the pairs have to survive
30/// as data rather than being collapsed into a "best charmap".
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub struct CharmapId {
33    /// The `cmap` platform ID.
34    pub platform: u16,
35    /// The `cmap` encoding ID, whose meaning depends on the platform.
36    pub encoding: u16,
37}
38
39impl CharmapId {
40    /// Windows Unicode BMP — the charmap `UseTTCharmapUnicode` accepts outright.
41    pub const WINDOWS_UNICODE: Self = Self {
42        platform: 3,
43        encoding: 1,
44    };
45    /// Windows Symbol, the `0xF0xx` private-use charmap.
46    pub const WINDOWS_SYMBOL: Self = Self {
47        platform: 3,
48        encoding: 0,
49    };
50    /// Mac Roman.
51    pub const MAC_ROMAN: Self = Self {
52        platform: 1,
53        encoding: 0,
54    };
55    /// The synthesized Unicode charmap a Type 1 face exposes first.
56    pub const UNICODE_SYNTHETIC: Self = Self {
57        platform: 0,
58        encoding: 3,
59    };
60    /// A Type 1 face's own encoding vector, which FreeType reports as
61    /// `ADOBE_CUSTOM`.
62    pub const ADOBE_CUSTOM: Self = Self {
63        platform: 4,
64        encoding: 0,
65    };
66
67    /// Does this charmap map Unicode?
68    ///
69    /// Platform 0 is Unicode by definition and `(3,1)`/`(3,10)` are Windows'
70    /// Unicode encodings. This is FreeType's `FT_ENCODING_UNICODE` test, which
71    /// `UseTTCharmapUnicode` reads for any charmap that is not `(3,0)`.
72    #[must_use]
73    pub fn is_unicode(self) -> bool {
74        self.platform == 0 || (self.platform == 3 && (self.encoding == 1 || self.encoding == 10))
75    }
76
77    /// The `fxge`-level encoding this charmap reports, for the reverse lookups
78    /// of the former working note.
79    #[must_use]
80    pub(crate) fn face_encoding(self) -> crate::encoding::FaceEncoding {
81        use crate::encoding::FaceEncoding as E;
82        match (self.platform, self.encoding) {
83            (0, _) | (3, 1 | 10) => E::Unicode,
84            (3, 0) => E::Symbol,
85            (1, 0) => E::AppleRoman,
86            (4, _) => E::AdobeCustom,
87            _ => E::Other,
88        }
89    }
90}
91
92/// Which charmap a lookup reads.
93///
94/// A value rather than face state: PDFium's `FT_Set_Charmap` mutates the face,
95/// which would make every ladder order-dependent on a shared value.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
97pub enum Charmap {
98    /// The face's best Unicode charmap, chosen by `skrifa`.
99    #[default]
100    Unicode,
101    /// A specific subtable, by index into the `cmap` encoding records.
102    Index(usize),
103    /// No charmap: every lookup yields 0.
104    None,
105}
106
107/// Which reader answers for a face's bytes.
108///
109/// **A bare CFF has no table directory**, so `skrifa::FontRef` cannot open one
110/// — and all fourteen Foxit base-14 blobs are bare CFF, which makes this a
111/// requirement rather than a nicety. PDFium's own Rust bridge splits the same
112/// way (`Sfnt::new ?? CffFontRef::new ?? Type1Font::new`), so this is the
113/// shape upstream arrived at too.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115enum Backend {
116    /// A table-directory font: TrueType, OpenType/CFF, a collection member.
117    Sfnt,
118    /// A bare CFF font program, read through `read_fonts::ps::cff`.
119    BareCff,
120}
121
122/// A font face, owning its bytes.
123///
124/// The bytes are `Arc`'d and the reader is rebuilt per use rather than stored.
125/// Opening only validates a header, so this is a handful of bounds checks —
126/// cheap next to drawing a glyph, and it keeps the type free of the
127/// self-reference a borrowed `FontRef<'static>` would need.
128#[derive(Clone)]
129pub struct Face {
130    bytes: Arc<[u8]>,
131    index: u32,
132    backend: Backend,
133    upem: u16,
134    num_glyphs: u32,
135    is_truetype: bool,
136    charmaps: Vec<CharmapId>,
137    /// The 64-ppem hinting instance, built on first use.
138    ///
139    /// The one piece of state this type keeps, and it earns the exception by
140    /// measurement rather than by principle: building the instance runs the
141    /// face's `fpgm` and `prep`
142    /// programs and costs about **50 µs**, against 4 µs to rasterize a glyph
143    /// bitmap and 0.4 µs to blit one. Rebuilding it per glyph made a
144    /// text-heavy page 30% slower than filling outlines; keeping it makes the
145    /// same page faster.
146    ///
147    /// It cannot be a borrowed `HintingInstance<'_>` because there is no such
148    /// type — `skrifa`'s is owned, which is precisely what lets this sit beside
149    /// the bytes without the self-reference the doc above rules out.
150    ///
151    /// `None` inside the lock is a face that cannot be hinted at all, cached so
152    /// that a bare CFF does not re-attempt it once per glyph. The `Arc` shares
153    /// the lock across clones, so two fonts substituted onto one face pay for
154    /// the interpreter once between them.
155    hinting: Arc<OnceLock<Option<HintingInstance>>>,
156    /// Whether this face is one whose outlines are wrong without the
157    /// interpreter, memoized because answering digs through the `name` table
158    /// and may checksum tables — `skrifa` documents it as slow enough to
159    /// cache, and it is asked once per glyph drawn.
160    hint_reliant: Arc<OnceLock<bool>>,
161    /// Glyph name → the first glyph id carrying it, built on the first name
162    /// lookup. A simple font with `/Differences` looks up hundreds of names
163    /// against one face; scanning the `post` table per name was quadratic.
164    names: Arc<OnceLock<HashMap<Box<[u8]>, u16>>>,
165    /// Glyph id → the advance [`advance`](Self::advance) reported for it.
166    ///
167    /// The second measured exception, and for the same reason as
168    /// [`hinting`](Self::hinting): a **bare CFF** carries no `hmtx`, so the
169    /// only place an advance exists is inside the charstring, and reading it
170    /// means running the Type 2 interpreter over the glyph's whole outline
171    /// and discarding the path. Text extraction asks for a width once per
172    /// shown character — `Font::char_width` for every glyph the page draws,
173    /// then again through the extractor's own fallback ladder — so the same
174    /// handful of glyphs are drawn hundreds of times each. On
175    /// `text_tcpdf_063` that interpreter was **84% of the whole text run**.
176    ///
177    /// A map rather than a `num_glyphs`-long table because a CID font has
178    /// tens of thousands of glyphs and a page shows tens of them; a
179    /// `RwLock` rather than a `Mutex` because after the first few characters
180    /// every access is a read, and `TextPage` is `Send + Sync` precisely so
181    /// that pages extract in parallel. The `Arc` shares the cache across
182    /// clones, so two fonts substituted onto one face pay once between them.
183    advances: Arc<RwLock<HashMap<Gid, Option<f32>>>>,
184    /// Glyph id → the box [`glyph_bbox`](Self::glyph_bbox) reported for it,
185    /// cached for the same reason as [`advances`](Self::advances) and asked
186    /// for just as often -- once per shown character through
187    /// `TextRun::glyph_bbox`, and again by the width ladder's last rung.
188    ///
189    /// Each miss rebuilds a `skrifa::FontRef` and a whole `GlyphMetrics`
190    /// (`hmtx`, `loca`, `glyf`, the variation tables) to read one box, or,
191    /// on a CFF-flavoured face, draws the outline and measures it. On
192    /// `text_foxit_products` that was **28% of the whole text run**, over
193    /// half of it inside `GlyphMetrics::new`.
194    boxes: Arc<RwLock<HashMap<Gid, Option<Rect>>>>,
195}
196
197impl fmt::Debug for Face {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        f.debug_struct("Face")
200            .field("bytes", &format_args!("{} bytes", self.bytes.len()))
201            .field("index", &self.index)
202            .field("backend", &self.backend)
203            .field("upem", &self.upem)
204            .field("num_glyphs", &self.num_glyphs)
205            .field("is_truetype", &self.is_truetype)
206            .field("charmaps", &self.charmaps)
207            .field("hinting", &self.hinting.get().map(Option::is_some))
208            .field("hint_reliant", &self.hint_reliant.get())
209            .field("names", &self.names.get().map(HashMap::len))
210            .field(
211                "advances",
212                &self.advances.read().map(|cache| cache.len()).ok(),
213            )
214            .field("boxes", &self.boxes.read().map(|cache| cache.len()).ok())
215            .finish()
216    }
217}
218
219impl Face {
220    /// Read a face from a font program.
221    ///
222    /// Accepts anything with a table directory — TrueType, bare CFF,
223    /// OpenType/CFF, and a TrueType Collection member by `index`. Returns
224    /// `None` for a blob no backend recognises, which is the signal to fall
225    /// back to [`pdfrum_type1`] and then to substitution.
226    #[must_use]
227    pub fn new(bytes: Arc<[u8]>, index: u32) -> Option<Self> {
228        // A table directory first, then a bare CFF. The order matters only in
229        // that an SFNT is unambiguous while a bare CFF is identified by a very
230        // short header, so trying the specific format first avoids a false
231        // positive on a truncated SFNT.
232        Self::from_sfnt(&bytes, index).or_else(|| Self::from_bare_cff(bytes, index))
233    }
234
235    /// Read a table-directory font.
236    fn from_sfnt(bytes: &Arc<[u8]>, index: u32) -> Option<Self> {
237        let font = skrifa::FontRef::from_index(bytes.as_ref(), index).ok()?;
238        let upem = font.head().map_or(0, |h| h.units_per_em());
239        let num_glyphs = u32::from(font.maxp().ok()?.num_glyphs());
240        // `glyf` present means outlines are quadratic TrueType splines; a
241        // bare or wrapped CFF has none. PDFium asks FreeType the same question
242        // through `FT_IS_SFNT` plus the driver name.
243        let is_truetype = font.glyf().is_ok();
244        let charmaps: Vec<CharmapId> = font
245            .cmap()
246            .map(|cmap| {
247                cmap.encoding_records()
248                    .iter()
249                    .map(|rec| CharmapId {
250                        platform: platform_ordinal(rec.platform_id()),
251                        encoding: rec.encoding_id(),
252                    })
253                    .collect()
254            })
255            .unwrap_or_default();
256        Some(Self {
257            bytes: Arc::clone(bytes),
258            index,
259            backend: Backend::Sfnt,
260            upem,
261            num_glyphs,
262            is_truetype,
263            charmaps,
264            hinting: Arc::default(),
265            hint_reliant: Arc::default(),
266            names: Arc::default(),
267            advances: Arc::default(),
268            boxes: Arc::default(),
269        })
270    }
271
272    /// Read a bare CFF font program.
273    ///
274    /// It reports exactly one charmap — its built-in encoding, which FreeType
275    /// surfaces as `ADOBE_CUSTOM` — plus a synthesized Unicode one from its
276    /// glyph names, matching the shape a Type 1 face presents. That is what
277    /// makes the Type 1 ladder's `UseType1Charmap` step behave the same for a
278    /// bare CFF as for a PFB, which is what PDFium's FreeType backend does.
279    fn from_bare_cff(bytes: Arc<[u8]>, index: u32) -> Option<Self> {
280        let cff = read_fonts::ps::cff::CffFontRef::new(bytes.as_ref(), 0, None).ok()?;
281        let num_glyphs = cff.num_glyphs();
282        let upem = u16::try_from(cff.upem()).unwrap_or(1000);
283        Some(Self {
284            bytes,
285            index,
286            backend: Backend::BareCff,
287            upem,
288            num_glyphs,
289            // CFF outlines are cubic charstrings, never `glyf` splines.
290            is_truetype: false,
291            charmaps: vec![CharmapId::UNICODE_SYNTHETIC, CharmapId::ADOBE_CUSTOM],
292            hinting: Arc::default(),
293            hint_reliant: Arc::default(),
294            names: Arc::default(),
295            advances: Arc::default(),
296            boxes: Arc::default(),
297        })
298    }
299
300    /// Open the bare-CFF reader, when that is this face's backend.
301    fn cff(&self) -> Option<read_fonts::ps::cff::CffFontRef<'_>> {
302        if self.backend != Backend::BareCff {
303            return None;
304        }
305        read_fonts::ps::cff::CffFontRef::new(&self.bytes, 0, None).ok()
306    }
307
308    /// The `CFF ` table of an SFNT-wrapped **CID-keyed** CFF, if that is what
309    /// this face holds.
310    ///
311    /// Opened as a bare program over the table's own bytes, because the only
312    /// thing wanted from it is its charset. Answers `None` for every other
313    /// face, including a `CFF `-flavoured OpenType that is not CID-keyed.
314    fn sfnt_cid_keyed_cff(&self) -> Option<read_fonts::ps::cff::CffFontRef<'_>> {
315        if self.backend != Backend::Sfnt {
316            return None;
317        }
318        let font = read_fonts::FontRef::from_index(&self.bytes, self.index).ok()?;
319        let table = font.table_data(read_fonts::types::Tag::new(b"CFF "))?;
320        let cff = read_fonts::ps::cff::CffFontRef::new_cff(table.as_bytes(), 0, None).ok()?;
321        cff.is_cid().then_some(cff)
322    }
323
324    /// The glyph index to actually draw for `gid`, whatever the packaging.
325    // For a bare CFF the caller already goes through `cff_glyph_id`. An
326    // SFNT-wrapped one needs the same mapping: FreeType's CFF driver puts the
327    // incoming index through the charset on CID-keyedness alone —
328    // `cid_registry != 0xFFFF && charset.cids`, with no bare-versus-wrapped
329    // condition (`third_party/freetype/src/src/cff/cffgload.c:222-236`) — and
330    // PDFium hands it a raw CID either way
331    // (`core/fpdfapi/font/cpdf_cidfont.cpp:787-789`). A subsetted CIDFontType0
332    // embedded as `/FontFile3 /Subtype /OpenType`, which is standard Acrobat
333    // and InDesign CJK output, is exactly that case: its glyphs are numbered
334    // 0..N while its CIDs run to five figures, so skipping the mapping asks a
335    // forty-glyph font for glyph 12345 and nothing draws.
336    fn drawn_glyph_id(&self, gid: Gid) -> read_fonts::types::GlyphId {
337        match self.sfnt_cid_keyed_cff() {
338            Some(cff) => Self::cff_glyph_id(&cff, gid),
339            None => read_fonts::types::GlyphId::new(u32::from(gid.0)),
340        }
341    }
342
343    /// The index a bare CFF actually stores a glyph under.
344    ///
345    /// For an ordinary CFF this is the number it was handed. For a **CID-keyed**
346    /// one it is not: the composite-font layer above hands down a CID, because
347    /// that is what PDFium hands FreeType, and FreeType silently maps it
348    /// through the font's charset. A subsetted CID-keyed program holds a
349    /// handful of glyphs numbered from zero while its CIDs are wherever the
350    /// original collection put them, so skipping the mapping asks for a glyph
351    /// number that does not exist and the font draws nothing at all.
352    fn cff_glyph_id(
353        cff: &read_fonts::ps::cff::CffFontRef<'_>,
354        gid: Gid,
355    ) -> read_fonts::types::GlyphId {
356        let raw = read_fonts::types::GlyphId::new(u32::from(gid.0));
357        if !cff.is_cid() {
358            return raw;
359        }
360        // In a CID-keyed font the charset's string identifiers *are* CIDs.
361        cff.charset()
362            .and_then(|charset| {
363                charset
364                    .glyph_id(read_fonts::ps::string::Sid::new(gid.0))
365                    .ok()
366            })
367            .unwrap_or(raw)
368    }
369
370    /// Design units per em.
371    #[must_use]
372    pub fn units_per_em(&self) -> u16 {
373        self.upem
374    }
375
376    /// How many glyphs the face declares.
377    #[must_use]
378    pub fn num_glyphs(&self) -> u32 {
379        self.num_glyphs
380    }
381
382    /// Whether outlines come from a `glyf` table.
383    #[must_use]
384    pub fn is_truetype(&self) -> bool {
385        self.is_truetype
386    }
387
388    /// The `(platform, encoding)` pairs the `cmap` table declares, in table
389    /// order — which is the order every ladder scans them in.
390    #[must_use]
391    pub fn charmaps(&self) -> &[CharmapId] {
392        &self.charmaps
393    }
394
395    /// The glyph a code selects through `charmap`. Zero on any miss.
396    #[must_use]
397    pub fn char_index(&self, charmap: Charmap, code: u32) -> u16 {
398        if let Some(cff) = self.cff() {
399            // A bare CFF has two routes: its built-in encoding for a byte
400            // code, and its glyph names through the Adobe Glyph List for a
401            // Unicode. Which one applies is exactly the distinction
402            // `UseType1Charmap` draws.
403            let gid = match charmap {
404                Charmap::None => None,
405                Charmap::Unicode => self.cff_unicode_to_gid(code),
406                Charmap::Index(_) => u8::try_from(code).ok().and_then(|b| cff.encoding()?.map(b)),
407            };
408            return gid
409                .and_then(|g| u16::try_from(g.to_u32()).ok())
410                .unwrap_or(0);
411        }
412
413        let Ok(font) = skrifa::FontRef::from_index(&self.bytes, self.index) else {
414            return 0;
415        };
416        let gid = match charmap {
417            Charmap::None => None,
418            Charmap::Unicode => font.charmap().map(code),
419            Charmap::Index(i) => font
420                .cmap()
421                .ok()
422                .and_then(|cmap| {
423                    let rec = cmap.encoding_records().get(i)?;
424                    rec.subtable(cmap.offset_data()).ok()
425                })
426                .and_then(|sub| sub.map_codepoint(code)),
427        };
428        gid.and_then(|g| u16::try_from(g.to_u32()).ok())
429            .unwrap_or(0)
430    }
431
432    /// A bare CFF's synthesized Unicode charmap: glyph names through the AGL.
433    fn cff_unicode_to_gid(&self, code: u32) -> Option<read_fonts::types::GlyphId> {
434        let ch = char::from_u32(code)?;
435        let mut buf = [0u8; read_fonts::ps::agl::MAX_NAME_LEN];
436        let name = read_fonts::ps::agl::char_to_name(u32::from(ch), &mut buf)?;
437        let gid = self.name_index(name);
438        (gid != 0).then(|| read_fonts::types::GlyphId::new(u32::from(gid)))
439    }
440
441    /// Scan a bare CFF's charset for a glyph name.
442    /// One-pass twin of the per-name scan this replaced: every name the
443    /// face carries, mapped to the **first** glyph id that has it.
444    fn build_name_map(&self) -> HashMap<Box<[u8]>, u16> {
445        if let Some(cff) = self.cff() {
446            let Some(charset) = cff.charset() else {
447                return HashMap::new();
448            };
449            let mut map = HashMap::new();
450            for gid in 0..self.num_glyphs {
451                let Ok(g) = u16::try_from(gid) else { break };
452                let Ok(sid) = charset.string_id(read_fonts::types::GlyphId::new(gid)) else {
453                    continue;
454                };
455                if let Some(bytes) = cff.string(sid) {
456                    map.entry(bytes.into()).or_insert(g);
457                }
458            }
459            return map;
460        }
461        let Ok(font) = skrifa::FontRef::from_index(&self.bytes, self.index) else {
462            return HashMap::new();
463        };
464        let Ok(post) = font.post() else {
465            return HashMap::new();
466        };
467        let default_names = &read_fonts::tables::post::DEFAULT_GLYPH_NAMES;
468        let mut map = HashMap::new();
469        if post.version() == read_fonts::types::Version16Dot16::VERSION_1_0 {
470            for (gid, name) in default_names
471                .iter()
472                .enumerate()
473                .take(self.num_glyphs as usize)
474            {
475                let Ok(g) = u16::try_from(gid) else { break };
476                map.entry(name.as_bytes().into()).or_insert(g);
477            }
478            return map;
479        }
480        if post.version() != read_fonts::types::Version16Dot16::VERSION_2_0 {
481            return map;
482        }
483        let Some(index) = post.glyph_name_index() else {
484            return map;
485        };
486        // The custom strings are a Pascal-string array: read it once,
487        // sequentially, instead of walking it from the start per glyph.
488        let strings: Vec<&str> = post
489            .string_data()
490            .map(|d| d.iter().map_while(Result::ok).map(|s| s.as_str()).collect())
491            .unwrap_or_default();
492        for gid in 0..self.num_glyphs {
493            let Ok(g) = u16::try_from(gid) else { break };
494            let Some(idx) = index.get(gid as usize) else {
495                break;
496            };
497            let idx = usize::from(idx.get());
498            let name = if idx < default_names.len() {
499                default_names.get(idx).copied()
500            } else {
501                strings.get(idx - default_names.len()).copied()
502            };
503            if let Some(name) = name {
504                map.entry(name.as_bytes().into()).or_insert(g);
505            }
506        }
507        map
508    }
509
510    /// The glyph a name selects. Zero on a miss.
511    #[must_use]
512    pub fn name_index(&self, name: &str) -> u16 {
513        self.names
514            .get_or_init(|| self.build_name_map())
515            .get(name.as_bytes())
516            .copied()
517            .unwrap_or(0)
518    }
519
520    /// The scan [`Face::build_name_map`] replaced, kept as the test oracle
521    /// for it: the first glyph whose name matches, zero on a miss.
522    #[cfg(test)]
523    pub(crate) fn name_index_by_scan(&self, name: &str) -> u16 {
524        if let Some(cff) = self.cff() {
525            let Some(charset) = cff.charset() else {
526                return 0;
527            };
528            for gid in 0..self.num_glyphs {
529                let Ok(g) = u16::try_from(gid) else { break };
530                let id = read_fonts::types::GlyphId::new(gid);
531                let Ok(sid) = charset.string_id(id) else {
532                    continue;
533                };
534                if cff.string(sid) == Some(name.as_bytes()) {
535                    return g;
536                }
537            }
538            return 0;
539        }
540        let Ok(font) = skrifa::FontRef::from_index(&self.bytes, self.index) else {
541            return 0;
542        };
543        let Ok(post) = font.post() else { return 0 };
544        for gid in 0..self.num_glyphs {
545            let Ok(g) = u16::try_from(gid) else { break };
546            if post.glyph_name(read_fonts::types::GlyphId16::new(g)) == Some(name) {
547                return g;
548            }
549        }
550        0
551    }
552
553    /// A glyph's own name.
554    #[must_use]
555    pub fn glyph_name(&self, gid: Gid) -> Option<String> {
556        if let Some(cff) = self.cff() {
557            let sid = cff
558                .charset()?
559                .string_id(read_fonts::types::GlyphId::new(u32::from(gid.0)))
560                .ok()?;
561            return cff
562                .string(sid)
563                .and_then(|b| std::str::from_utf8(b).ok())
564                .map(ToOwned::to_owned);
565        }
566        let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
567        font.post()
568            .ok()?
569            .glyph_name(read_fonts::types::GlyphId16::new(gid.0))
570            .map(ToOwned::to_owned)
571    }
572
573    /// Whether the face carries glyph names at all.
574    #[must_use]
575    pub fn has_glyph_names(&self) -> bool {
576        if self.backend == Backend::BareCff {
577            // A CFF charset always names its glyphs.
578            return self.cff().and_then(|c| c.charset()).is_some();
579        }
580        skrifa::FontRef::from_index(&self.bytes, self.index)
581            .ok()
582            .and_then(|f| f.post().ok())
583            .is_some_and(|p| p.glyph_name(read_fonts::types::GlyphId16::new(0)).is_some())
584    }
585
586    /// The pixels-per-em every hinted glyph is grid-fitted at.
587    ///
588    /// A pinned constant rather than the glyph's real size: hinting always
589    /// fits to a 64-pixel grid, and the size the glyph is actually drawn at
590    /// is applied afterwards as a plain scale. Grid-fitting at a pinned ppem
591    /// is therefore *not* the same thing as grid-fitting at the drawn size.
592    ///
593    /// The measured consequence: this moves outline points by about 1/25 of a
594    /// device pixel at 9 pt, which is up to 10 counts per pixel on a 6 pt stem
595    /// once the glyph is rasterized.
596    // Where the 64 comes from: `CFX_Face::New` calls
597    // `FT_Set_Pixel_Sizes(rec, 64, 64)` once (`cfx_face.cpp:376`) and nothing
598    // ever changes it; the real size reaches FreeType through
599    // `FT_Set_Transform` with the matrix pre-divided by 64
600    // (`cfx_face.cpp:822-825`). FreeType applies a transform *after* hinting,
601    // so the interpreter fits to a 64-pixel grid whose alignment is then
602    // scaled away.
603    pub(crate) const HINT_PPEM: f32 = 64.0;
604
605    /// A glyph's outline grid-fitted at [`Self::HINT_PPEM`], in **64ths of an
606    /// em** — the units a 64-ppem instance draws in.
607    ///
608    /// `None` for every face that is not hinted, and that is exactly the
609    /// faces with **no table directory**: a bare CFF is never hinted, which
610    /// matters because all fourteen base-14 blobs are bare CFF.
611    ///
612    /// It is also `None` when the interpreter refuses the face's own
613    /// programs, in which case the caller falls back to the unhinted
614    /// [`Self::outline`] rather than drawing nothing.
615    ///
616    /// Building the instance costs about 50 µs — the face's `fpgm` and `prep`
617    /// programs run — so it is memoized per face rather than per glyph. See
618    /// [`Self::hinting`].
619    // The two `None` arms restate one upstream rule each.
620    // `CFX_Face::RenderGlyph` adds `FT_LOAD_NO_HINTING` exactly when
621    // `!IsTtOt()` — no `FT_FACE_FLAG_SFNT`, i.e. no table directory
622    // (`cfx_face.cpp:841-843`). And a glyph is loaded `FT_LOAD_PEDANTIC`; on
623    // an error `cfx_face.cpp:849-857` reloads it *unhinted* rather than
624    // failing, which is the same place our second `None` sends the caller.
625    #[must_use]
626    pub(crate) fn hinted_outline(&self, gid: Gid) -> Option<BezPath> {
627        let instance = self.hinting_instance()?;
628        let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
629        let glyph = font
630            .outline_glyphs()
631            .get(skrifa::GlyphId::new(self.drawn_glyph_id(gid).to_u32()))?;
632        let mut pen = PathPen::default();
633        glyph
634            .draw(DrawSettings::hinted(instance, false), &mut pen)
635            .ok()?;
636        Some(pen.path)
637    }
638
639    /// The memoized 64-ppem hinting instance, built on first use.
640    fn hinting_instance(&self) -> Option<&HintingInstance> {
641        self.hinting
642            .get_or_init(|| {
643                if self.backend != Backend::Sfnt {
644                    return None;
645                }
646                let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
647                let outlines = font.outline_glyphs();
648                // `Engine::Interpreter` rather than the default
649                // `AutoFallback`: the autofitter is compiled out of the
650                // oracle's FreeType (`ftmodule.h`), so a face with no
651                // `fpgm`/`prep` gets no hinting there at all, and falling back
652                // to an autohinter here would invent grid-fitting the oracle
653                // never applies. `Target::Smooth`'s default `Normal` mode is
654                // `FT_RENDER_MODE_NORMAL`, which is what `RenderGlyph` selects
655                // by passing no `FT_LOAD_TARGET_*` at all.
656                //
657                // Except for a **hint-reliant** face, which takes `Mono`.
658                // Smooth is subpixel-positioned, and its backward-compatibility
659                // rules exist to stop a legacy program from moving points
660                // across the x axis — which is precisely what these faces'
661                // programs are *for*. A stroke-assembled CJK face stores its
662                // strokes off-canvas and moves them into place from `fpgm`, so
663                // suppressing that movement leaves the strokes piled where they
664                // were stored and the glyph unreadable. `Mono` runs the program
665                // as written, which is what `skrifa` documents for the faces
666                // `require_interpreter` selects and what the oracle gets by
667                // passing no load target at all.
668                let target = if outlines.require_interpreter() {
669                    HintingTarget::Mono
670                } else {
671                    HintingTarget::default()
672                };
673                HintingInstance::new(
674                    &outlines,
675                    Size::new(Self::HINT_PPEM),
676                    LocationRef::default(),
677                    HintingOptions {
678                        engine: HintingEngine::Interpreter,
679                        target,
680                    },
681                )
682                .ok()
683            })
684            .as_ref()
685    }
686
687    /// Whether this face's outlines are wrong without the interpreter.
688    ///
689    /// FreeType's `FT_FACE_FLAG_TRICKY`, which it sets for a hardcoded list of
690    /// faces — a handful of stroke-assembled CJK families — whose glyphs are
691    /// *assembled* by their bytecode rather than merely fitted to a grid by
692    /// it. The strokes are stored off-canvas and the `fpgm` program moves them
693    /// into place, so an unhinted outline of one is not a coarser rendering of
694    /// the glyph but a pile of misplaced strokes.
695    ///
696    /// The oracle reads the same flag to decide the same thing: `LoadGlyphPath`
697    /// adds `FT_LOAD_NO_HINTING` unless `IsTtOt() && IsTricky()`
698    /// (`cfx_face.cpp:882`), which is the *only* case where its path side is
699    /// hinted at all.
700    #[must_use]
701    pub(crate) fn is_hint_reliant(&self) -> bool {
702        *self.hint_reliant.get_or_init(|| {
703            if self.backend != Backend::Sfnt {
704                return false;
705            }
706            skrifa::FontRef::from_index(&self.bytes, self.index)
707                .is_ok_and(|font| font.outline_glyphs().require_interpreter())
708        })
709    }
710
711    /// A glyph's outline in **font units**, unhinted.
712    ///
713    /// Unhinted at every size, for every face — this is the *path* side of
714    /// text, which is never grid-fitted. The glyph-*bitmap* side is a
715    /// different rule and a different function: see [`Self::hinted_outline`].
716    // Unconditionally unhinted is not a simplification. `CFX_Face::LoadGlyphPath`
717    // hints only a face that is both SFNT and on FreeType's ~20-font "tricky"
718    // list (`cfx_face.cpp:948-951`); `skrifa` does not model that list and no
719    // corpus font is on it, so the hinted arm is unreachable either way.
720    #[must_use]
721    pub(crate) fn outline(&self, gid: Gid) -> Option<BezPath> {
722        let mut pen = PathPen::default();
723        if let Some(cff) = self.cff() {
724            let id = Self::cff_glyph_id(&cff, gid);
725            let subfont_index = cff.subfont_index(id)?;
726            let subfont = cff.subfont(subfont_index, &[]).ok()?;
727            // `ppem: None` means unscaled font units, which is the same
728            // request the SFNT path makes through `Size::unscaled`.
729            cff.draw(&subfont, id, &[], None, &mut pen).ok()?;
730            return Some(pen.path);
731        }
732        let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
733        let glyph = font
734            .outline_glyphs()
735            .get(skrifa::GlyphId::new(self.drawn_glyph_id(gid).to_u32()))?;
736        glyph
737            .draw(
738                DrawSettings::unhinted(Size::unscaled(), LocationRef::default()),
739                &mut pen,
740            )
741            .ok()?;
742        Some(pen.path)
743    }
744
745    /// A glyph's advance in font units.
746    ///
747    /// A bare CFF carries no `hmtx`: the advance comes out of the charstring
748    /// itself, which is why drawing is how it is read.
749    #[must_use]
750    pub(crate) fn advance(&self, gid: Gid) -> Option<f32> {
751        if let Ok(cache) = self.advances.read()
752            && let Some(hit) = cache.get(&gid)
753        {
754            return *hit;
755        }
756        let computed = self.advance_uncached(gid);
757        if let Ok(mut cache) = self.advances.write() {
758            cache.insert(gid, computed);
759        }
760        computed
761    }
762
763    /// [`advance`](Self::advance) with the cache bypassed.
764    #[must_use]
765    fn advance_uncached(&self, gid: Gid) -> Option<f32> {
766        if let Some(cff) = self.cff() {
767            let id = Self::cff_glyph_id(&cff, gid);
768            let subfont_index = cff.subfont_index(id)?;
769            let subfont = cff.subfont(subfont_index, &[]).ok()?;
770            let mut pen = PathPen::default();
771            return cff.draw(&subfont, id, &[], None, &mut pen).ok().flatten();
772        }
773        let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
774        font.glyph_metrics(Size::unscaled(), LocationRef::default())
775            .advance_width(skrifa::GlyphId::new(self.drawn_glyph_id(gid).to_u32()))
776    }
777
778    /// A glyph's bounding box in font units, y-up.
779    ///
780    /// The fast path is the `glyf` table's per-glyph bounds, which only a
781    /// TrueType-outlined face has. A **CFF-flavoured** OpenType face has no
782    /// such table — its bounds live inside each charstring — so it falls
783    /// through to measuring the outline, exactly as the C++'s FreeType
784    /// backend does by loading the glyph and reading its control box. Getting
785    /// this wrong makes every glyph of a CFF font report a zero box, which
786    /// text extraction reads as a degenerate text object and drops whole.
787    #[must_use]
788    pub(crate) fn glyph_bbox(&self, gid: Gid) -> Option<Rect> {
789        if let Ok(cache) = self.boxes.read()
790            && let Some(hit) = cache.get(&gid)
791        {
792            return *hit;
793        }
794        let computed = self.glyph_bbox_uncached(gid);
795        if let Ok(mut cache) = self.boxes.write() {
796            cache.insert(gid, computed);
797        }
798        computed
799    }
800
801    /// [`glyph_bbox`](Self::glyph_bbox) with the cache bypassed.
802    #[must_use]
803    fn glyph_bbox_uncached(&self, gid: Gid) -> Option<Rect> {
804        if self.backend != Backend::BareCff {
805            let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
806            if let Some(b) = font
807                .glyph_metrics(Size::unscaled(), LocationRef::default())
808                .bounds(skrifa::GlyphId::new(self.drawn_glyph_id(gid).to_u32()))
809            {
810                return Some(Rect::new(
811                    f64::from(b.x_min),
812                    f64::from(b.y_min),
813                    f64::from(b.x_max),
814                    f64::from(b.y_max),
815                ));
816            }
817        }
818        let path = self.outline(gid)?;
819        let b = pdfrum_common::kurbo::Shape::bounding_box(&path);
820        (b.width() > 0.0 || b.height() > 0.0).then_some(b)
821    }
822
823    /// The raw metrics `CheckFontMetrics` derives a bounding box from.
824    #[must_use]
825    pub(crate) fn metrics(&self) -> Option<crate::descriptor::FaceMetrics> {
826        if self.backend == Backend::BareCff {
827            // A bare CFF declares no `head` or `hhea`; PDFium's FreeType
828            // backend synthesizes the same nothing, and the caller's
829            // per-code union then supplies the box.
830            return None;
831        }
832        let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
833        let head = font.head().ok()?;
834        let hhea = font.hhea().ok()?;
835        Some(crate::descriptor::FaceMetrics {
836            upem: head.units_per_em(),
837            bbox_left: i64::from(head.x_min()),
838            bbox_top: i64::from(head.y_max()),
839            bbox_right: i64::from(head.x_max()),
840            bbox_bottom: i64::from(head.y_min()),
841            ascender: i64::from(hhea.ascender().to_i16()),
842            descender: i64::from(hhea.descender().to_i16()),
843        })
844    }
845
846    /// The face's own bytes, for the `GSUB` reader.
847    #[must_use]
848    pub(crate) fn bytes(&self) -> &Arc<[u8]> {
849        &self.bytes
850    }
851
852    /// The face index within a collection.
853    #[must_use]
854    pub(crate) fn index(&self) -> u32 {
855        self.index
856    }
857
858    /// The family and style names, joined as PDFium's `GetFontNameFromFace`
859    /// joins them: family, then a space and the style unless the style is
860    /// empty or `Regular`.
861    #[must_use]
862    pub(crate) fn display_name(&self) -> Option<String> {
863        if let Some(cff) = self.cff() {
864            let meta = cff.metadata()?;
865            return meta
866                .family_name()
867                .or_else(|| meta.name())
868                .map(ToOwned::to_owned);
869        }
870        let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
871        let strings = font.localized_strings(skrifa::string::StringId::FAMILY_NAME);
872        let family: String = strings.english_or_first()?.chars().collect();
873        if family.is_empty() {
874            return None;
875        }
876        let style: String = font
877            .localized_strings(skrifa::string::StringId::SUBFAMILY_NAME)
878            .english_or_first()
879            .map(|s| s.chars().collect())
880            .unwrap_or_default();
881        if style.is_empty() || style == "Regular" {
882            Some(family)
883        } else {
884            Some(format!("{family} {style}"))
885        }
886    }
887
888    /// The PostScript name (name ID 6), falling back to the family name.
889    #[must_use]
890    pub fn postscript_name(&self) -> Option<String> {
891        if let Some(cff) = self.cff() {
892            let meta = cff.metadata()?;
893            return meta
894                .name()
895                .or_else(|| meta.family_name())
896                .map(ToOwned::to_owned);
897        }
898        let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
899        let ps: String = font
900            .localized_strings(skrifa::string::StringId::POSTSCRIPT_NAME)
901            .english_or_first()
902            .map(|s| s.chars().collect())
903            .unwrap_or_default();
904        if !ps.is_empty() {
905            return Some(ps);
906        }
907        self.display_name()
908    }
909
910    /// `post.isFixedPitch`, or false when the table is missing.
911    #[must_use]
912    pub fn is_fixed_pitch(&self) -> bool {
913        let Ok(font) = skrifa::FontRef::from_index(&self.bytes, self.index) else {
914            return false;
915        };
916        font.post().is_ok_and(|p| p.is_fixed_pitch() != 0)
917    }
918
919    /// Italic from OS/2 `fsSelection`, `head.macStyle`, or a non-zero `post.italicAngle`.
920    #[must_use]
921    pub fn is_italic(&self) -> bool {
922        let Ok(font) = skrifa::FontRef::from_index(&self.bytes, self.index) else {
923            return false;
924        };
925        if let Ok(os2) = font.os2() {
926            let sel = os2.fs_selection();
927            if sel.contains(read_fonts::tables::os2::SelectionFlags::ITALIC)
928                || sel.contains(read_fonts::tables::os2::SelectionFlags::OBLIQUE)
929            {
930                return true;
931            }
932        }
933        if font.head().is_ok_and(|h| {
934            h.mac_style()
935                .contains(read_fonts::tables::head::MacStyle::ITALIC)
936        }) {
937            return true;
938        }
939        font.post().is_ok_and(|p| p.italic_angle().to_f64() != 0.0)
940    }
941
942    /// Bold from OS/2 `fsSelection` / `usWeightClass >= 700`, or `head.macStyle`.
943    #[must_use]
944    pub fn is_bold(&self) -> bool {
945        let Ok(font) = skrifa::FontRef::from_index(&self.bytes, self.index) else {
946            return false;
947        };
948        if let Ok(os2) = font.os2() {
949            if os2
950                .fs_selection()
951                .contains(read_fonts::tables::os2::SelectionFlags::BOLD)
952            {
953                return true;
954            }
955            if os2.us_weight_class() >= 700 {
956                return true;
957            }
958        }
959        font.head().is_ok_and(|h| {
960            h.mac_style()
961                .contains(read_fonts::tables::head::MacStyle::BOLD)
962        })
963    }
964
965    /// OS/2 `sCapHeight` in font units, when the table is version 2 or later.
966    #[must_use]
967    pub fn cap_height(&self) -> Option<f32> {
968        let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
969        font.os2().ok()?.s_cap_height().map(f32::from)
970    }
971
972    /// Unicode codepoint → glyph mappings of the Unicode cmap, with `code <= max`.
973    ///
974    /// Sorted by codepoint. Glyph 0 (`.notdef`) is omitted, matching
975    /// `FT_Get_Next_Char`'s `glyph_index == 0` stop.
976    #[must_use]
977    pub fn unicode_mappings(&self, max: u32) -> Vec<(u32, u16)> {
978        if self.backend == Backend::BareCff {
979            return (0..=max)
980                .filter_map(|cp| {
981                    let gid = self.char_index(Charmap::Unicode, cp);
982                    (gid != 0).then_some((cp, gid))
983                })
984                .collect();
985        }
986        let Ok(font) = skrifa::FontRef::from_index(&self.bytes, self.index) else {
987            return Vec::new();
988        };
989        let mut out: Vec<(u32, u16)> = font
990            .charmap()
991            .mappings()
992            .filter_map(|(cp, gid)| {
993                if cp > max {
994                    return None;
995                }
996                let g = u16::try_from(gid.to_u32()).ok()?;
997                (g != 0).then_some((cp, g))
998            })
999            .collect();
1000        out.sort_unstable_by_key(|(cp, _)| *cp);
1001        out.dedup_by_key(|(cp, _)| *cp);
1002        out
1003    }
1004}
1005
1006fn platform_ordinal(p: PlatformId) -> u16 {
1007    match p {
1008        PlatformId::Unicode => 0,
1009        PlatformId::Macintosh => 1,
1010        PlatformId::ISO => 2,
1011        PlatformId::Windows => 3,
1012        PlatformId::Custom => 4,
1013        // A malformed platform id must not collide with a real one.
1014        PlatformId::Unknown => u16::MAX,
1015    }
1016}
1017
1018/// Collects `skrifa`'s outline verbs into a `kurbo` path.
1019///
1020/// Quadratics are elevated to cubics rather than kept, matching the
1021/// `ConvertOutline` step PDFium's own Fontations bridge performs so FreeType's
1022/// decomposition and this one agree.
1023#[derive(Default)]
1024struct PathPen {
1025    path: BezPath,
1026    current: (f32, f32),
1027    open: bool,
1028}
1029
1030impl OutlinePen for PathPen {
1031    fn move_to(&mut self, x: f32, y: f32) {
1032        if self.open {
1033            self.path.close_path();
1034        }
1035        self.path.move_to((f64::from(x), f64::from(y)));
1036        self.current = (x, y);
1037        self.open = true;
1038    }
1039
1040    fn line_to(&mut self, x: f32, y: f32) {
1041        if self.open {
1042            self.path.line_to((f64::from(x), f64::from(y)));
1043            self.current = (x, y);
1044        }
1045    }
1046
1047    fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
1048        if !self.open {
1049            return;
1050        }
1051        // The standard quadratic-to-cubic elevation: each cubic control point
1052        // sits two thirds of the way from an endpoint to the quadratic's.
1053        let (px, py) = self.current;
1054        let c1 = (
1055            f64::from(px) + 2.0 / 3.0 * f64::from(cx0 - px),
1056            f64::from(py) + 2.0 / 3.0 * f64::from(cy0 - py),
1057        );
1058        let c2 = (
1059            f64::from(cx0) + f64::from(x - cx0) / 3.0,
1060            f64::from(cy0) + f64::from(y - cy0) / 3.0,
1061        );
1062        self.path.curve_to(c1, c2, (f64::from(x), f64::from(y)));
1063        self.current = (x, y);
1064    }
1065
1066    fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
1067        if !self.open {
1068            return;
1069        }
1070        self.path.curve_to(
1071            (f64::from(cx0), f64::from(cy0)),
1072            (f64::from(cx1), f64::from(cy1)),
1073            (f64::from(x), f64::from(y)),
1074        );
1075        self.current = (x, y);
1076    }
1077
1078    fn close(&mut self) {
1079        if self.open {
1080            self.path.close_path();
1081            self.open = false;
1082        }
1083    }
1084}
1085
1086#[cfg(test)]
1087mod tests {
1088    /// The one-pass map answers exactly what the per-name scan answered, for
1089    /// every name every fixture face carries, and zero for a name none has.
1090    #[test]
1091    fn the_name_map_answers_what_the_scan_answered() {
1092        let fixtures = [
1093            "tt_custom_40.ttf",
1094            "tt_macroman_10.ttf",
1095            "tt_macroman_empty.ttf",
1096            "tt_named_no_cmap.ttf",
1097            "tt_sjis_and_unicode.ttf",
1098            "tt_symbol_30.ttf",
1099            "tt_symbol_and_macroman.ttf",
1100            "tt_symbol_empty.ttf",
1101            "tt_unicode_03_and_symbol.ttf",
1102            "tt_unicode_03.ttf",
1103            "tt_unicode_31_and_symbol.ttf",
1104            "tt_unicode_31.ttf",
1105        ];
1106        let mut named_faces = 0;
1107        for fixture in fixtures {
1108            let bytes: Arc<[u8]> = crate::testfonts::load(fixture).into();
1109            let face = Face::new(bytes, 0).unwrap();
1110            let map = face.build_name_map();
1111            named_faces += usize::from(!map.is_empty());
1112            for (name, gid) in &map {
1113                let name = std::str::from_utf8(name).unwrap();
1114                let scanned = face.name_index_by_scan(name);
1115                assert_eq!(*gid, scanned, "{fixture}: {name}");
1116                assert_eq!(face.name_index(name), scanned, "{fixture}: {name}");
1117            }
1118            assert_eq!(face.name_index("nonesuch"), 0, "{fixture}");
1119            assert_eq!(face.name_index_by_scan("nonesuch"), 0, "{fixture}");
1120        }
1121        assert!(
1122            named_faces > 0,
1123            "no fixture carries glyph names; the pin proves nothing"
1124        );
1125    }
1126
1127    use super::*;
1128
1129    #[test]
1130    fn charmap_ids_classify_unicode_correctly() {
1131        assert!(CharmapId::WINDOWS_UNICODE.is_unicode());
1132        assert!(CharmapId::UNICODE_SYNTHETIC.is_unicode());
1133        assert!(
1134            CharmapId {
1135                platform: 3,
1136                encoding: 10
1137            }
1138            .is_unicode()
1139        );
1140        // `(3,0)` is Windows *Symbol*, deliberately not Unicode — the whole
1141        // `UseTTCharmapUnicode` rule turns on that distinction.
1142        assert!(!CharmapId::WINDOWS_SYMBOL.is_unicode());
1143        assert!(!CharmapId::MAC_ROMAN.is_unicode());
1144    }
1145
1146    #[test]
1147    fn charmap_ids_map_to_face_encodings() {
1148        use crate::encoding::FaceEncoding as E;
1149        assert_eq!(CharmapId::WINDOWS_UNICODE.face_encoding(), E::Unicode);
1150        assert_eq!(CharmapId::WINDOWS_SYMBOL.face_encoding(), E::Symbol);
1151        assert_eq!(CharmapId::MAC_ROMAN.face_encoding(), E::AppleRoman);
1152        assert_eq!(CharmapId::ADOBE_CUSTOM.face_encoding(), E::AdobeCustom);
1153        assert_eq!(
1154            CharmapId {
1155                platform: 2,
1156                encoding: 7
1157            }
1158            .face_encoding(),
1159            E::Other
1160        );
1161    }
1162
1163    #[test]
1164    fn garbage_bytes_yield_no_face() {
1165        assert!(Face::new(Arc::from(&b""[..]), 0).is_none());
1166        assert!(Face::new(Arc::from(&b"not a font at all"[..]), 0).is_none());
1167        assert!(Face::new(Arc::from(vec![0u8; 4096].as_slice()), 0).is_none());
1168    }
1169
1170    #[test]
1171    fn a_foxit_base14_blob_reads_as_a_non_truetype_face() {
1172        let bytes: Arc<[u8]> = Arc::from(crate::subst::standard_font_data(
1173            crate::StandardFont::Helvetica,
1174        ));
1175        let face = Face::new(bytes, 0).expect("bare CFF is readable");
1176        assert!(!face.is_truetype(), "a bare CFF has no glyf table");
1177        assert!(face.num_glyphs() > 100);
1178        assert_eq!(face.units_per_em(), 1000);
1179    }
1180
1181    /// Both packagings of one CID-keyed CFF program reach the same glyph.
1182    ///
1183    /// FreeType maps a CID through the charset on CID-keyedness alone, with no
1184    /// condition on whether the program arrived bare or inside an SFNT
1185    /// (`third_party/freetype/src/src/cff/cffgload.c:222-236`), and PDFium
1186    /// hands it the raw CID either way
1187    /// (`core/fpdfapi/font/cpdf_cidfont.cpp:787-789`). The CIDs here are
1188    /// four-figure numbers in a three-glyph font, so nothing draws at all
1189    /// unless the mapping runs.
1190    ///
1191    /// Advances are not compared across the two: an SFNT reads `hmtx` and a
1192    /// bare CFF reads the charstring's own width operand, which is a real
1193    /// difference between the packagings and not this mapping's business.
1194    #[test]
1195    fn an_sfnt_wrapped_cid_keyed_cff_maps_its_cids_through_the_charset_like_a_bare_one() {
1196        let bare = Face::new(crate::testfonts::load("cid_keyed.cff").into(), 0)
1197            .expect("bare CFF is readable");
1198        let otto = Face::new(crate::testfonts::load("cid_keyed_otto.otf").into(), 0)
1199            .expect("OTTO is readable");
1200        assert_eq!(otto.num_glyphs(), 3, "three glyphs, CIDs in the thousands");
1201        for cid in [4000u16, 4001] {
1202            let gid = Gid(cid);
1203            let expected = bare.outline(gid).expect("bare draws the CID");
1204            let drawn = otto.outline(gid).expect("OTTO draws the CID too");
1205            assert_eq!(
1206                pdfrum_common::kurbo::Shape::bounding_box(&drawn),
1207                pdfrum_common::kurbo::Shape::bounding_box(&expected),
1208                "outline, CID {cid}"
1209            );
1210            assert_eq!(
1211                otto.glyph_bbox(gid),
1212                bare.glyph_bbox(gid),
1213                "bbox, CID {cid}"
1214            );
1215            assert_ne!(otto.advance(gid), None, "advance, CID {cid}");
1216        }
1217        // A CID this font does not carry stays unmapped rather than aliasing
1218        // onto some other glyph's outline.
1219        assert_eq!(
1220            otto.glyph_bbox(Gid(9999)),
1221            None,
1222            "an absent CID draws nothing"
1223        );
1224    }
1225
1226    /// A CFF that is *not* CID-keyed keeps taking its glyph index literally —
1227    /// FreeType's gate is `cid_registry != 0xFFFF`, and nothing else.
1228    #[test]
1229    fn a_non_cid_keyed_cff_is_not_put_through_the_charset() {
1230        let bytes: Arc<[u8]> = Arc::from(crate::subst::standard_font_data(
1231            crate::StandardFont::Helvetica,
1232        ));
1233        let face = Face::new(bytes, 0).expect("bare CFF is readable");
1234        assert!(
1235            face.sfnt_cid_keyed_cff().is_none(),
1236            "bare, and not CID-keyed"
1237        );
1238        let named = face.name_index("A");
1239        assert_ne!(named, 0, "Helvetica names its glyphs");
1240        assert!(face.outline(Gid(named)).is_some());
1241
1242        // The SFNT side of the same rule: a CFF-flavoured OpenType that is not
1243        // CID-keyed must not be put through a charset either.
1244        let tt: Arc<[u8]> = crate::testfonts::load("tt_unicode_31.ttf").into();
1245        let tt = Face::new(tt, 0).expect("TrueType is readable");
1246        assert!(tt.sfnt_cid_keyed_cff().is_none(), "no CFF table at all");
1247    }
1248
1249    #[test]
1250    fn the_pen_elevates_quadratics_to_cubics() {
1251        let mut pen = PathPen::default();
1252        pen.move_to(0.0, 0.0);
1253        pen.quad_to(30.0, 60.0, 60.0, 0.0);
1254        pen.close();
1255        let els: Vec<_> = pen.path.into_iter().collect();
1256        assert_eq!(els.len(), 3);
1257        assert!(matches!(
1258            els.get(1),
1259            Some(pdfrum_common::kurbo::PathEl::CurveTo(..))
1260        ));
1261    }
1262
1263    #[test]
1264    fn the_pen_ignores_segments_before_any_move() {
1265        let mut pen = PathPen::default();
1266        pen.line_to(10.0, 10.0);
1267        pen.curve_to(1.0, 1.0, 2.0, 2.0, 3.0, 3.0);
1268        pen.close();
1269        assert!(pen.path.elements().is_empty());
1270    }
1271}