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}