Skip to main content

pdf_interpret/font/
standard_font.rs

1use crate::FontResolverFn;
2use crate::cache::Cache;
3use crate::font::blob::{CffFontBlob, OpenTypeFontBlob};
4use crate::font::generated::{glyph_names, metrics, standard, symbol, zapf_dings};
5use crate::font::true_type::{Width, read_encoding, read_widths};
6use crate::font::{
7    Encoding, FontData, FontQuery, glyph_name_to_bfstring, normalized_glyph_name, stretch_glyph,
8    strip_subset_prefix,
9};
10use kurbo::BezPath;
11use pdf_font::cmap::BfString;
12use pdf_syntax::object::Dict;
13use pdf_syntax::object::Name;
14use pdf_syntax::object::dict::keys::{
15    BASE_FONT, FONT_DESC, FONT_FAMILY, FONT_WEIGHT, ITALIC_ANGLE, MISSING_WIDTH,
16};
17use skrifa::raw::TableProvider;
18use skrifa::{GlyphId, GlyphId16};
19use std::cell::RefCell;
20use std::collections::HashMap;
21
22/// The 14 standard fonts of PDF.
23#[derive(Copy, Clone, Debug)]
24pub enum StandardFont {
25    /// Helvetica.
26    Helvetica,
27    /// Helvetica Bold.
28    HelveticaBold,
29    /// Helvetica Oblique.
30    HelveticaOblique,
31    /// Helvetica Bold Oblique.
32    HelveticaBoldOblique,
33    /// Courier.
34    Courier,
35    /// Courier Bold.
36    CourierBold,
37    /// Courier Oblique.
38    CourierOblique,
39    /// Courier Bold Oblique.
40    CourierBoldOblique,
41    /// Times Roman.
42    TimesRoman,
43    /// Times Bold.
44    TimesBold,
45    /// Times Italic.
46    TimesItalic,
47    /// Times Bold Italic.
48    TimesBoldItalic,
49    /// Zapf Dingbats - a decorative symbol font.
50    ZapfDingBats,
51    /// Symbol - a mathematical symbol font.
52    Symbol,
53}
54
55impl StandardFont {
56    /// Advance width of `ch` in thousandths of an em, if this face covers it.
57    ///
58    /// The character rides the face's built-in encoding to a glyph name, and
59    /// the name reads the standard width table; characters outside Latin-1
60    /// have no code there and read as uncovered.
61    pub fn char_width(&self, ch: char) -> Option<f32> {
62        let code = u8::try_from(ch as u32).ok()?;
63        let name = self.code_to_name(code)?;
64        self.get_width(name)
65    }
66
67    pub(crate) fn code_to_name(&self, code: u8) -> Option<&'static str> {
68        match self {
69            Self::Symbol => symbol::get(code),
70            // Note that this font does not return postscript character names,
71            // but instead has a custom encoding.
72            Self::ZapfDingBats => zapf_dings::get(code),
73            _ => standard::get(code),
74        }
75    }
76
77    pub(crate) fn get_width(&self, mut name: &str) -> Option<f32> {
78        // <https://github.com/apache/pdfbox/blob/129aafe26548c1ff935af9c55cb40a996186c35f/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDSimpleFont.java#L340>
79        if name == ".notdef" {
80            return Some(250.0);
81        }
82
83        name = normalized_glyph_name(name);
84
85        match self {
86            Self::Helvetica => metrics::HELVETICA.get(name).copied(),
87            Self::HelveticaBold => metrics::HELVETICA_BOLD.get(name).copied(),
88            Self::HelveticaOblique => metrics::HELVETICA_OBLIQUE.get(name).copied(),
89            Self::HelveticaBoldOblique => metrics::HELVETICA_BOLD_OBLIQUE.get(name).copied(),
90            Self::Courier => metrics::COURIER.get(name).copied(),
91            Self::CourierBold => metrics::COURIER_BOLD.get(name).copied(),
92            Self::CourierOblique => metrics::COURIER_OBLIQUE.get(name).copied(),
93            Self::CourierBoldOblique => metrics::COURIER_BOLD_OBLIQUE.get(name).copied(),
94            Self::TimesRoman => metrics::TIMES_ROMAN.get(name).copied(),
95            Self::TimesBold => metrics::TIMES_BOLD.get(name).copied(),
96            Self::TimesItalic => metrics::TIMES_ITALIC.get(name).copied(),
97            Self::TimesBoldItalic => metrics::TIMES_BOLD_ITALIC.get(name).copied(),
98            Self::ZapfDingBats => metrics::ZAPF_DING_BATS.get(name).copied(),
99            Self::Symbol => metrics::SYMBOL.get(name).copied(),
100        }
101    }
102
103    pub(crate) fn as_str(&self) -> &'static str {
104        match self {
105            Self::Helvetica => "Helvetica",
106            Self::HelveticaBold => "Helvetica Bold",
107            Self::HelveticaOblique => "Helvetica Oblique",
108            Self::HelveticaBoldOblique => "Helvetica Bold Oblique",
109            Self::Courier => "Courier",
110            Self::CourierBold => "Courier Bold",
111            Self::CourierOblique => "Courier Oblique",
112            Self::CourierBoldOblique => "Courier Bold Oblique",
113            Self::TimesRoman => "Times Roman",
114            Self::TimesBold => "Times Bold",
115            Self::TimesItalic => "Times Italic",
116            Self::TimesBoldItalic => "Times Bold Italic",
117            Self::ZapfDingBats => "Zapf Dingbats",
118            Self::Symbol => "Symbol",
119        }
120    }
121
122    /// Return the postscrit name of the font.
123    pub fn postscript_name(&self) -> &'static str {
124        match self {
125            Self::Helvetica => "Helvetica",
126            Self::HelveticaBold => "Helvetica-Bold",
127            Self::HelveticaOblique => "Helvetica-Oblique",
128            Self::HelveticaBoldOblique => "Helvetica-BoldOblique",
129            Self::Courier => "Courier",
130            Self::CourierBold => "Courier-Bold",
131            Self::CourierOblique => "Courier-Oblique",
132            Self::CourierBoldOblique => "Courier-BoldOblique",
133            Self::TimesRoman => "Times-Roman",
134            Self::TimesBold => "Times-Bold",
135            Self::TimesItalic => "Times-Italic",
136            Self::TimesBoldItalic => "Times-BoldItalic",
137            Self::ZapfDingBats => "ZapfDingbats",
138            Self::Symbol => "Symbol",
139        }
140    }
141
142    pub(crate) fn is_bold(&self) -> bool {
143        matches!(
144            self,
145            Self::HelveticaBold
146                | Self::HelveticaBoldOblique
147                | Self::CourierBold
148                | Self::CourierBoldOblique
149                | Self::TimesBold
150                | Self::TimesBoldItalic
151        )
152    }
153
154    pub(crate) fn is_italic(&self) -> bool {
155        matches!(
156            self,
157            Self::HelveticaOblique
158                | Self::HelveticaBoldOblique
159                | Self::CourierOblique
160                | Self::CourierBoldOblique
161                | Self::TimesItalic
162                | Self::TimesBoldItalic
163        )
164    }
165
166    pub(crate) fn is_serif(&self) -> bool {
167        matches!(
168            self,
169            Self::TimesRoman | Self::TimesBold | Self::TimesItalic | Self::TimesBoldItalic
170        )
171    }
172
173    pub(crate) fn is_monospace(&self) -> bool {
174        matches!(
175            self,
176            Self::Courier | Self::CourierBold | Self::CourierOblique | Self::CourierBoldOblique
177        )
178    }
179
180    /// Canonical vertical font metrics for the PDF Standard-14 faces, in /1000
181    /// em. Returns `(ascent, descent, cap_height, x_height)` where `ascent` is
182    /// positive (above the baseline) and `descent` is negative (below it).
183    ///
184    /// Source: the Adobe Core-14 AFM files (`Ascender` / `Descender` /
185    /// `CapHeight` / `XHeight` global keys; obliques share their upright face's
186    /// vertical metrics). Symbol and ZapfDingbats declare no letterform metrics
187    /// in their AFM, so `cap_height` / `x_height` are `None` and ascent/descent
188    /// are taken from the AFM `FontBBox` vertical extents.
189    ///
190    /// Used only as a fallback when no embedded font binary provides skrifa
191    /// metrics — embedded font metrics always take precedence.
192    pub(crate) fn canonical_metrics(&self) -> Option<(f64, f64, Option<f64>, Option<f64>)> {
193        match self {
194            Self::Helvetica => Some((718.0, -207.0, Some(718.0), Some(523.0))),
195            Self::HelveticaBold => Some((718.0, -207.0, Some(718.0), Some(532.0))),
196            Self::HelveticaOblique => Some((718.0, -207.0, Some(718.0), Some(523.0))),
197            Self::HelveticaBoldOblique => Some((718.0, -207.0, Some(718.0), Some(532.0))),
198            Self::Courier => Some((629.0, -157.0, Some(562.0), Some(426.0))),
199            Self::CourierBold => Some((629.0, -157.0, Some(562.0), Some(439.0))),
200            Self::CourierOblique => Some((629.0, -157.0, Some(562.0), Some(426.0))),
201            Self::CourierBoldOblique => Some((629.0, -157.0, Some(562.0), Some(439.0))),
202            Self::TimesRoman => Some((683.0, -217.0, Some(662.0), Some(450.0))),
203            Self::TimesBold => Some((683.0, -217.0, Some(676.0), Some(461.0))),
204            Self::TimesItalic => Some((683.0, -217.0, Some(653.0), Some(441.0))),
205            Self::TimesBoldItalic => Some((683.0, -217.0, Some(669.0), Some(462.0))),
206            Self::ZapfDingBats => Some((820.0, -143.0, None, None)),
207            Self::Symbol => Some((1010.0, -293.0, None, None)),
208        }
209    }
210
211    /// Return suitable font data for the given standard font.
212    ///
213    /// Currently, this will return the corresponding Foxit font, which is a set of permissibly
214    /// licensed fonts that is also very light-weight.
215    ///
216    /// You can use the result of this method in your implementation of [`FontResolverFn`].
217    ///
218    /// [`FontResolverFn`]: crate::FontResolverFn
219    #[cfg(feature = "embed-fonts")]
220    pub fn get_font_data(&self) -> (FontData, u32) {
221        use std::sync::Arc;
222
223        let data = match self {
224            Self::Helvetica => &include_bytes!("../../assets/FoxitSans.pfb")[..],
225            Self::HelveticaBold => &include_bytes!("../../assets/FoxitSansBold.pfb")[..],
226            Self::HelveticaOblique => &include_bytes!("../../assets/FoxitSansItalic.pfb")[..],
227            Self::HelveticaBoldOblique => {
228                &include_bytes!("../../assets/FoxitSansBoldItalic.pfb")[..]
229            }
230            Self::Courier => &include_bytes!("../../assets/FoxitFixed.pfb")[..],
231            Self::CourierBold => &include_bytes!("../../assets/FoxitFixedBold.pfb")[..],
232            Self::CourierOblique => &include_bytes!("../../assets/FoxitFixedItalic.pfb")[..],
233            Self::CourierBoldOblique => {
234                &include_bytes!("../../assets/FoxitFixedBoldItalic.pfb")[..]
235            }
236            Self::TimesRoman => &include_bytes!("../../assets/FoxitSerif.pfb")[..],
237            Self::TimesBold => &include_bytes!("../../assets/FoxitSerifBold.pfb")[..],
238            Self::TimesItalic => &include_bytes!("../../assets/FoxitSerifItalic.pfb")[..],
239            Self::TimesBoldItalic => &include_bytes!("../../assets/FoxitSerifBoldItalic.pfb")[..],
240            Self::ZapfDingBats => &include_bytes!("../../assets/FoxitDingbats.pfb")[..],
241            Self::Symbol => {
242                include_bytes!("../../assets/FoxitSymbol.pfb")
243            }
244        };
245
246        (Arc::new(data), 0)
247    }
248}
249
250enum StandardFontFamily {
251    Helvetica,
252    Courier,
253    Times,
254}
255
256/// PostScript-name aliases commonly produced by Office/iText/etc. that refer
257/// to fonts the reader is expected to substitute with the corresponding
258/// Standard-14 font. Matched after subset-prefix stripping and after the
259/// literal Standard-14 names, but before the keyword-based heuristic.
260///
261/// Aliases are intentionally exact (case-sensitive) matches — the keyword
262/// heuristic below already catches free-form variants like "ArialNarrow-Bold".
263fn standard_font_alias(name: &str) -> Option<StandardFont> {
264    match name {
265        // Arial family → Helvetica
266        "ArialMT" | "Arial" => Some(StandardFont::Helvetica),
267        "Arial-BoldMT" | "Arial,Bold" | "Arial-Bold" => Some(StandardFont::HelveticaBold),
268        "Arial-ItalicMT" | "Arial,Italic" | "Arial-Italic" => Some(StandardFont::HelveticaOblique),
269        "Arial-BoldItalicMT" | "Arial,BoldItalic" | "Arial-BoldItalic" => {
270            Some(StandardFont::HelveticaBoldOblique)
271        }
272        // Times New Roman family → Times
273        "TimesNewRomanPSMT" | "TimesNewRoman" | "TimesNewRomanPS" => Some(StandardFont::TimesRoman),
274        "TimesNewRomanPS-BoldMT"
275        | "TimesNewRoman-Bold"
276        | "TimesNewRomanPS-Bold"
277        | "TimesNewRoman,Bold" => Some(StandardFont::TimesBold),
278        "TimesNewRomanPS-ItalicMT"
279        | "TimesNewRoman-Italic"
280        | "TimesNewRomanPS-Italic"
281        | "TimesNewRoman,Italic" => Some(StandardFont::TimesItalic),
282        "TimesNewRomanPS-BoldItalicMT"
283        | "TimesNewRoman-BoldItalic"
284        | "TimesNewRomanPS-BoldItalic"
285        | "TimesNewRoman,BoldItalic" => Some(StandardFont::TimesBoldItalic),
286        // Courier New family → Courier
287        "CourierNewPSMT" | "CourierNew" => Some(StandardFont::Courier),
288        "CourierNewPS-BoldMT" | "CourierNew-Bold" | "CourierNewPS-Bold" => {
289            Some(StandardFont::CourierBold)
290        }
291        "CourierNewPS-ItalicMT" | "CourierNew-Italic" | "CourierNewPS-Italic" => {
292            Some(StandardFont::CourierOblique)
293        }
294        "CourierNewPS-BoldItalicMT" | "CourierNew-BoldItalic" | "CourierNewPS-BoldItalic" => {
295            Some(StandardFont::CourierBoldOblique)
296        }
297        _ => None,
298    }
299}
300
301pub(crate) fn select_standard_font(
302    dict: &Dict<'_>,
303    descriptor: &Dict<'_>,
304) -> Option<(StandardFont, bool)> {
305    let base_font = dict.get::<Name>(BASE_FONT)?;
306    let name = strip_subset_prefix(base_font.as_str());
307
308    // First try whether it matches literally.
309    match name {
310        "Helvetica" => return Some((StandardFont::Helvetica, true)),
311        "Helvetica-Bold" => return Some((StandardFont::HelveticaBold, true)),
312        "Helvetica-Oblique" => return Some((StandardFont::HelveticaOblique, true)),
313        "Helvetica-BoldOblique" => return Some((StandardFont::HelveticaBoldOblique, true)),
314        "Courier" => return Some((StandardFont::Courier, true)),
315        "Courier-Bold" => return Some((StandardFont::CourierBold, true)),
316        "Courier-Oblique" => return Some((StandardFont::CourierOblique, true)),
317        "Courier-BoldOblique" => return Some((StandardFont::CourierBoldOblique, true)),
318        "Times-Roman" => return Some((StandardFont::TimesRoman, true)),
319        "Times-Bold" => return Some((StandardFont::TimesBold, true)),
320        "Times-Italic" => return Some((StandardFont::TimesItalic, true)),
321        "Times-BoldItalic" => return Some((StandardFont::TimesBoldItalic, true)),
322        "Symbol" => return Some((StandardFont::Symbol, true)),
323        "ZapfDingbats" => return Some((StandardFont::ZapfDingBats, true)),
324        _ => {}
325    }
326
327    // PostScript-name aliases commonly emitted by Office/iText/etc. for
328    // unembedded Standard-14-equivalent fonts (e.g. ArialMT → Helvetica).
329    // Treated as non-exact so glyph-width fallback in StandardKind still
330    // consults the supplied Widths array when present.
331    if let Some(alias) = standard_font_alias(name) {
332        return Some((alias, false));
333    }
334
335    // Now, we bruteforce, trying to determine a suitable font based on the
336    // keywords that appear in the name and the descriptor.
337    let lower = name.to_ascii_lowercase();
338
339    // FontFamily (descriptor) captures the human-readable family, which is
340    // often present even when BaseFont is an opaque subset name. PDF 1.7 §9.8.1
341    // specifies it as a text string, but producers in the wild use name
342    // objects too; fetch as Name (covers both via implicit conversion).
343    let family_field = descriptor
344        .get::<Name>(FONT_FAMILY)
345        .map(|n| n.as_str().to_ascii_lowercase())
346        .unwrap_or_default();
347
348    // PDF spec §9.8.2 Table 120: FontWeight is a number in {100, 200, … 900};
349    // 400 is normal and 700 is bold. Adobe considers weights ≥ 600 (SemiBold,
350    // DemiBold) as "bold" for substitution purposes — matching that lowers
351    // the threshold from 700 to 600 so fonts like "HelveticaNeue-Medium"
352    // (weight 500) stay regular but "*-SemiBold" (600) map to the bold face.
353    let is_bold = descriptor.get::<u32>(FONT_WEIGHT).is_some_and(|w| w >= 600)
354        || lower.contains("bold")
355        || lower.contains("demi")
356        || family_field.contains("bold")
357        || family_field.contains("demi");
358    // PDF spec §9.8.2 Table 120: ItalicAngle is the angle, in counter-clockwise
359    // degrees, of the dominant vertical strokes. Italic/oblique faces are
360    // negative (typically -10° to -20°). Previously we accepted any non-zero
361    // value, which mis-classified upright fonts that shipped with tiny
362    // rounding noise (e.g. -0.1). The stricter −5° threshold follows what
363    // PDF.js and PDFBox use and avoids that false-positive.
364    let is_italic = descriptor
365        .get::<f32>(ITALIC_ANGLE)
366        .is_some_and(|a| !(-5.0..=5.0).contains(&a))
367        || lower.contains("italic")
368        || lower.contains("oblique")
369        || family_field.contains("italic")
370        || family_field.contains("oblique");
371
372    // Keyword/family heuristic. Prefer BaseFont; fall back to FontFamily.
373    let haystack = if family_field.is_empty() {
374        lower.clone()
375    } else {
376        format!("{lower} {family_field}")
377    };
378
379    // Keyword/family heuristic (last resort — only reached when the font name
380    // did not match any Standard-14 alias above).
381    //
382    // `exact` controls whether the caller should trust PDF /Widths entries or
383    // fall back to Standard-14 AFM metrics:
384    //   exact=true  → AFM metrics used; PDF /Widths ignored (safe for genuine
385    //                 Standard-14 faces whose names survived case folding here)
386    //   exact=false → PDF /Widths respected when present; AFM is only fallback
387    //                 (correct for non-Standard-14 lookalikes like "ArialMT")
388    //
389    // GL-QA38 regression note: setting exact=false for ALL heuristic matches
390    // caused 116 SSIM regressions in gate-5k-04 because many PDFs that contained
391    // "helvetica" or "times" in the font name ARE genuine Standard-14 and their
392    // /Widths arrays (when present) are less accurate than Standard-14 AFM.
393    // The corrected approach: treat Standard-14 keyword matches (helvetica,
394    // courier, times) as exact=true; treat clear non-Standard-14 keywords
395    // (arial, sans, mono, serif without "times") as exact=false so we respect
396    // their embedded /Widths.
397    let (family, exact) = if haystack.contains("helvetica") {
398        (Some(StandardFontFamily::Helvetica), true) // likely genuine Helvetica — use AFM
399    } else if haystack.contains("arial") || haystack.contains("sans") {
400        (Some(StandardFontFamily::Helvetica), false) // Arial/generic sans — respect /Widths
401    } else if haystack.contains("courier") {
402        (Some(StandardFontFamily::Courier), true) // likely genuine Courier — use AFM
403    } else if haystack.contains("mono") {
404        (Some(StandardFontFamily::Courier), false) // generic monospace — respect /Widths
405    } else if haystack.contains("times") {
406        (Some(StandardFontFamily::Times), true) // likely genuine Times — use AFM
407    } else if haystack.contains("serif") {
408        (Some(StandardFontFamily::Times), false) // generic serif — respect /Widths
409    } else if haystack.contains("zapfdingbats") || haystack.contains("dingbats") {
410        return Some((StandardFont::ZapfDingBats, false));
411    } else {
412        (None, false)
413    };
414
415    let font = match (family?, is_bold, is_italic) {
416        (StandardFontFamily::Helvetica, false, false) => StandardFont::Helvetica,
417        (StandardFontFamily::Helvetica, true, false) => StandardFont::HelveticaBold,
418        (StandardFontFamily::Helvetica, false, true) => StandardFont::HelveticaOblique,
419        (StandardFontFamily::Helvetica, true, true) => StandardFont::HelveticaBoldOblique,
420        (StandardFontFamily::Courier, false, false) => StandardFont::Courier,
421        (StandardFontFamily::Courier, true, false) => StandardFont::CourierBold,
422        (StandardFontFamily::Courier, false, true) => StandardFont::CourierOblique,
423        (StandardFontFamily::Courier, true, true) => StandardFont::CourierBoldOblique,
424        (StandardFontFamily::Times, false, false) => StandardFont::TimesRoman,
425        (StandardFontFamily::Times, true, false) => StandardFont::TimesBold,
426        (StandardFontFamily::Times, false, true) => StandardFont::TimesItalic,
427        (StandardFontFamily::Times, true, true) => StandardFont::TimesBoldItalic,
428    };
429
430    Some((font, exact))
431}
432
433#[derive(Debug, Clone)]
434pub(crate) enum StandardFontBlob {
435    Cff(CffFontBlob),
436    Otf(OpenTypeFontBlob, HashMap<String, GlyphId>),
437}
438
439impl StandardFontBlob {
440    pub(crate) fn from_data(data: FontData, index: u32) -> Option<Self> {
441        if let Some(blob) = CffFontBlob::new(data.clone()) {
442            Some(Self::new_cff(blob))
443        } else {
444            OpenTypeFontBlob::new(data, index).map(Self::new_otf)
445        }
446    }
447
448    pub(crate) fn new_cff(blob: CffFontBlob) -> Self {
449        Self::Cff(blob)
450    }
451
452    pub(crate) fn new_otf(blob: OpenTypeFontBlob) -> Self {
453        let mut glyph_names = HashMap::new();
454
455        if let Ok(post) = blob.font_ref().post() {
456            for i in 0..blob.num_glyphs() {
457                if let Some(str) = post.glyph_name(GlyphId16::new(i)) {
458                    glyph_names.insert(str.to_string(), GlyphId::new(i as u32));
459                }
460            }
461        }
462
463        Self::Otf(blob, glyph_names)
464    }
465}
466
467impl StandardFontBlob {
468    pub(crate) fn name_to_glyph(&self, name: &str) -> Option<GlyphId> {
469        match self {
470            Self::Cff(blob) => blob
471                .table()
472                .glyph_index_by_name(name)
473                .map(|g| GlyphId::new(g.0 as u32)),
474            Self::Otf(_, glyph_names) => glyph_names.get(name).copied(),
475        }
476    }
477
478    pub(crate) fn unicode_to_glyph(&self, code: u32) -> Option<GlyphId> {
479        match self {
480            Self::Cff(_) => None,
481            Self::Otf(blob, _) => blob
482                .font_ref()
483                .cmap()
484                .ok()
485                .and_then(|c| c.map_codepoint(code)),
486        }
487    }
488
489    pub(crate) fn advance_width(&self, glyph: GlyphId) -> Option<f32> {
490        match self {
491            Self::Cff(_) => None,
492            Self::Otf(blob, _) => blob.glyph_metrics().advance_width(glyph),
493        }
494    }
495
496    pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath {
497        // Standard fonts have empty outlines for these, but in Liberation Sans
498        // they are a .notdef rectangle.
499        if glyph == GlyphId::NOTDEF {
500            return BezPath::new();
501        }
502
503        match self {
504            Self::Cff(blob) => blob.outline_glyph(glyph),
505            Self::Otf(blob, _) => blob.outline_glyph(glyph),
506        }
507    }
508}
509
510#[derive(Debug)]
511pub(crate) struct StandardKind {
512    base_font: StandardFont,
513    base_font_blob: StandardFontBlob,
514    encoding: Encoding,
515    widths: Vec<Width>,
516    missing_width: Option<f32>,
517    fallback: bool,
518    glyph_to_code: RefCell<HashMap<GlyphId, u8>>,
519    encodings: HashMap<u8, String>,
520    /// The font dictionary's /BaseFont, when it names one.
521    named: Option<String>,
522}
523
524impl StandardKind {
525    pub(crate) fn new(dict: &Dict<'_>, resolver: &FontResolverFn, cache: &Cache) -> Option<Self> {
526        let descriptor = dict.get::<Dict<'_>>(FONT_DESC).unwrap_or_default();
527        let (font, exact) = select_standard_font(dict, &descriptor)?;
528        Self::new_with_standard(dict, font, !exact, resolver, cache)
529    }
530
531    pub(crate) fn new_with_standard(
532        dict: &Dict<'_>,
533        base_font: StandardFont,
534        fallback: bool,
535        resolver: &FontResolverFn,
536        cache: &Cache,
537    ) -> Option<Self> {
538        let descriptor = dict.get::<Dict<'_>>(FONT_DESC).unwrap_or_default();
539        let (widths, missing_width) = read_widths(dict, &descriptor)?;
540        let missing_width = descriptor
541            .contains_key(MISSING_WIDTH)
542            .then_some(missing_width);
543
544        let (mut encoding, encoding_map) = read_encoding(dict);
545
546        // See PDFJS-16464: Ignore encodings for non-embedded Type1 symbol fonts.
547        if matches!(base_font, StandardFont::Symbol | StandardFont::ZapfDingBats) {
548            encoding = Encoding::BuiltIn;
549        }
550
551        let (blob, index) = resolver(&FontQuery::Standard(base_font))?;
552        // A standard face is parsed once per document and shared by every
553        // font that stands on it, keyed by the bytes the resolver handed over.
554        let key = Cache::font_data_key(b"standard", index, blob.as_ref().as_ref());
555        let base_font_blob =
556            cache.get_or_insert_some(key, || StandardFontBlob::from_data(blob.clone(), index))?;
557
558        let named = dict
559            .get::<Name>(BASE_FONT)
560            .map(|name| name.as_str().to_string());
561
562        Some(Self {
563            base_font,
564            base_font_blob,
565            widths,
566            missing_width,
567            encodings: encoding_map,
568            glyph_to_code: RefCell::new(HashMap::new()),
569            fallback,
570            encoding,
571            named,
572        })
573    }
574
575    fn code_to_ps_name(&self, code: u8) -> Option<&str> {
576        let bf = self.base_font;
577
578        self.encodings
579            .get(&code)
580            .map(String::as_str)
581            .or_else(|| match self.encoding {
582                Encoding::BuiltIn => bf.code_to_name(code),
583                _ => self.encoding.map_code(code),
584            })
585    }
586
587    pub(crate) fn map_code(&self, code: u8) -> GlyphId {
588        let result = self
589            .code_to_ps_name(code)
590            .and_then(|c| {
591                self.base_font_blob.name_to_glyph(c).or_else(|| {
592                    // If the font doesn't have a POST table, try to map via unicode instead.
593                    glyph_names::get(c).and_then(|c| {
594                        self.base_font_blob
595                            .unicode_to_glyph(c.chars().nth(0).unwrap() as u32)
596                    })
597                })
598            })
599            .unwrap_or(GlyphId::NOTDEF);
600        self.glyph_to_code.borrow_mut().insert(result, code);
601
602        result
603    }
604
605    pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath {
606        let path = self.base_font_blob.outline_glyph(glyph);
607
608        // If the font is not embedded, we might need to stretch it so that
609        // it matches the metrics of the actual underlying font blob.
610
611        if let Some(code) = self.glyph_to_code.borrow().get(&glyph).copied()
612            && let Some(actual_width) = self.base_font_blob.advance_width(glyph).or_else(|| {
613                self.code_to_ps_name(code)
614                    .and_then(|name| self.base_font.get_width(name))
615            })
616        {
617            // From my experiments: Most PDF viewers, if they detect a font is a
618            // standard font, they completely ignore the widths array, even if
619            // different widths are indicated there. So only if it's an unknown
620            // font do we check the widths array. Otherwise, we always use the
621            // base font metrics.
622            let should_width = if self.fallback {
623                if let Some(Width::Value(w)) = self.widths.get(code as usize).copied() {
624                    w
625                } else {
626                    return path;
627                }
628            } else if let Some(w) = self
629                .code_to_ps_name(code)
630                .and_then(|name| self.base_font.get_width(name))
631            {
632                w
633            } else {
634                return path;
635            };
636
637            return stretch_glyph(path, should_width, actual_width);
638        }
639
640        path
641    }
642
643    pub(crate) fn glyph_width(&self, code: u8) -> Option<f32> {
644        match self.widths.get(code as usize).copied() {
645            Some(Width::Value(w)) => Some(w),
646            Some(Width::Missing) => self.missing_width.or_else(|| {
647                self.code_to_ps_name(code)
648                    .and_then(|c| self.base_font.get_width(c))
649            }),
650            _ => self
651                .code_to_ps_name(code)
652                .and_then(|c| self.base_font.get_width(c)),
653        }
654    }
655
656    pub(crate) fn char_code_to_unicode(&self, code: u8) -> Option<BfString> {
657        self.code_to_ps_name(code).and_then(glyph_name_to_bfstring)
658    }
659
660    pub(crate) fn is_italic(&self) -> bool {
661        self.base_font.is_italic()
662    }
663
664    pub(crate) fn is_bold(&self) -> bool {
665        self.base_font.is_bold()
666    }
667
668    pub(crate) fn is_serif(&self) -> bool {
669        self.base_font.is_serif()
670    }
671
672    pub(crate) fn is_monospace(&self) -> bool {
673        self.base_font.is_monospace()
674    }
675
676    /// PostScript name of the underlying standard-14 font.
677    ///
678    /// Always available because every `StandardKind` was constructed
679    /// against a known [`StandardFont`].  Surfaced for the WASM
680    /// `getTextPositions()` `fontName` field on non-embedded Type1
681    /// runs (see `pdf-engine`).
682    pub(crate) fn postscript_name(&self) -> &'static str {
683        self.base_font.postscript_name()
684    }
685
686    /// The font dictionary's /BaseFont, when it names one (#623): the
687    /// face the page asked for, which a standard font stands in for.
688    pub(crate) fn declared_name(&self) -> Option<&str> {
689        self.named.as_deref()
690    }
691
692    /// Vertical font metrics from the Standard-14 AFM fallback table.
693    pub(crate) fn font_metrics(&self) -> Option<(f64, f64, Option<f64>, Option<f64>)> {
694        self.base_font.canonical_metrics()
695    }
696}
697
698#[cfg(test)]
699mod tests {
700    use super::*;
701
702    fn build_widths(entries: &[(u8, f32)]) -> Vec<Width> {
703        let mut widths = vec![Width::Missing; 256];
704        for (code, width) in entries {
705            widths[*code as usize] = Width::Value(*width);
706        }
707        widths
708    }
709
710    fn build_standard_kind(widths: Vec<Width>, missing_width: Option<f32>) -> StandardKind {
711        let (data, index) = StandardFont::Helvetica.get_font_data();
712        let base_font_blob =
713            StandardFontBlob::from_data(data, index).expect("standard font data should parse");
714
715        StandardKind {
716            named: None,
717            base_font: StandardFont::Helvetica,
718            base_font_blob,
719            encoding: Encoding::WinAnsi,
720            widths,
721            missing_width,
722            fallback: true,
723            glyph_to_code: RefCell::new(HashMap::new()),
724            encodings: HashMap::new(),
725        }
726    }
727
728    #[test]
729    fn glyph_width_falls_back_to_base_metrics_when_missing_width_is_absent() {
730        let font = build_standard_kind(build_widths(&[(b'A', 600.0)]), None);
731
732        assert_eq!(font.glyph_width(b'A'), Some(600.0));
733        assert_eq!(
734            font.glyph_width(b'B'),
735            StandardFont::Helvetica.get_width("B")
736        );
737    }
738
739    #[test]
740    fn glyph_width_respects_explicit_zero_missing_width() {
741        let font = build_standard_kind(build_widths(&[(b'A', 600.0)]), Some(0.0));
742
743        assert_eq!(font.glyph_width(b'A'), Some(600.0));
744        assert_eq!(font.glyph_width(b'B'), Some(0.0));
745    }
746
747    #[test]
748    fn arial_aliases_resolve_to_helvetica_family() {
749        assert!(matches!(
750            standard_font_alias("ArialMT"),
751            Some(StandardFont::Helvetica)
752        ));
753        assert!(matches!(
754            standard_font_alias("Arial-BoldMT"),
755            Some(StandardFont::HelveticaBold)
756        ));
757        assert!(matches!(
758            standard_font_alias("Arial-ItalicMT"),
759            Some(StandardFont::HelveticaOblique)
760        ));
761        assert!(matches!(
762            standard_font_alias("Arial-BoldItalicMT"),
763            Some(StandardFont::HelveticaBoldOblique)
764        ));
765    }
766
767    #[test]
768    fn times_new_roman_aliases_resolve_to_times_family() {
769        assert!(matches!(
770            standard_font_alias("TimesNewRomanPSMT"),
771            Some(StandardFont::TimesRoman)
772        ));
773        assert!(matches!(
774            standard_font_alias("TimesNewRomanPS-BoldMT"),
775            Some(StandardFont::TimesBold)
776        ));
777        assert!(matches!(
778            standard_font_alias("TimesNewRomanPS-ItalicMT"),
779            Some(StandardFont::TimesItalic)
780        ));
781        assert!(matches!(
782            standard_font_alias("TimesNewRomanPS-BoldItalicMT"),
783            Some(StandardFont::TimesBoldItalic)
784        ));
785    }
786
787    #[test]
788    fn courier_new_aliases_resolve_to_courier_family() {
789        assert!(matches!(
790            standard_font_alias("CourierNewPSMT"),
791            Some(StandardFont::Courier)
792        ));
793        assert!(matches!(
794            standard_font_alias("CourierNewPS-BoldMT"),
795            Some(StandardFont::CourierBold)
796        ));
797        assert!(matches!(
798            standard_font_alias("CourierNewPS-ItalicMT"),
799            Some(StandardFont::CourierOblique)
800        ));
801        assert!(matches!(
802            standard_font_alias("CourierNewPS-BoldItalicMT"),
803            Some(StandardFont::CourierBoldOblique)
804        ));
805    }
806
807    #[test]
808    fn unknown_names_do_not_alias() {
809        assert!(standard_font_alias("LiberationSans").is_none());
810        assert!(standard_font_alias("CenturySchoolbook").is_none());
811        assert!(standard_font_alias("").is_none());
812    }
813
814    // ── canonical_metrics tests ──
815
816    #[test]
817    fn helvetica_metrics() {
818        let m = StandardFont::Helvetica.canonical_metrics().unwrap();
819        assert_eq!(m.0, 718.0);
820        assert_eq!(m.1, -207.0);
821        assert_eq!(m.2, Some(718.0));
822        assert_eq!(m.3, Some(523.0));
823    }
824
825    #[test]
826    fn helvetica_bold_metrics() {
827        let m = StandardFont::HelveticaBold.canonical_metrics().unwrap();
828        assert_eq!(m.0, 718.0);
829        assert_eq!(m.1, -207.0);
830        assert_eq!(m.2, Some(718.0));
831        assert_eq!(m.3, Some(532.0));
832    }
833
834    #[test]
835    fn helvetica_oblique_metrics() {
836        let m = StandardFont::HelveticaOblique.canonical_metrics().unwrap();
837        assert_eq!(m.0, 718.0);
838        assert_eq!(m.1, -207.0);
839        assert_eq!(m.2, Some(718.0));
840        assert_eq!(m.3, Some(523.0));
841    }
842
843    #[test]
844    fn helvetica_bold_oblique_metrics() {
845        let m = StandardFont::HelveticaBoldOblique
846            .canonical_metrics()
847            .unwrap();
848        assert_eq!(m.0, 718.0);
849        assert_eq!(m.1, -207.0);
850        assert_eq!(m.2, Some(718.0));
851        assert_eq!(m.3, Some(532.0));
852    }
853
854    #[test]
855    fn courier_metrics() {
856        let m = StandardFont::Courier.canonical_metrics().unwrap();
857        assert_eq!(m.0, 629.0);
858        assert_eq!(m.1, -157.0);
859        assert_eq!(m.2, Some(562.0));
860        assert_eq!(m.3, Some(426.0));
861    }
862
863    #[test]
864    fn courier_bold_metrics() {
865        let m = StandardFont::CourierBold.canonical_metrics().unwrap();
866        assert_eq!(m.0, 629.0);
867        assert_eq!(m.1, -157.0);
868        assert_eq!(m.2, Some(562.0));
869        assert_eq!(m.3, Some(439.0));
870    }
871
872    #[test]
873    fn courier_oblique_metrics() {
874        let m = StandardFont::CourierOblique.canonical_metrics().unwrap();
875        assert_eq!(m.0, 629.0);
876        assert_eq!(m.1, -157.0);
877        assert_eq!(m.2, Some(562.0));
878        assert_eq!(m.3, Some(426.0));
879    }
880
881    #[test]
882    fn courier_bold_oblique_metrics() {
883        let m = StandardFont::CourierBoldOblique
884            .canonical_metrics()
885            .unwrap();
886        assert_eq!(m.0, 629.0);
887        assert_eq!(m.1, -157.0);
888        assert_eq!(m.2, Some(562.0));
889        assert_eq!(m.3, Some(439.0));
890    }
891
892    #[test]
893    fn times_roman_metrics() {
894        let m = StandardFont::TimesRoman.canonical_metrics().unwrap();
895        assert_eq!(m.0, 683.0);
896        assert_eq!(m.1, -217.0);
897        assert_eq!(m.2, Some(662.0));
898        assert_eq!(m.3, Some(450.0));
899    }
900
901    #[test]
902    fn times_bold_metrics() {
903        let m = StandardFont::TimesBold.canonical_metrics().unwrap();
904        assert_eq!(m.0, 683.0);
905        assert_eq!(m.1, -217.0);
906        assert_eq!(m.2, Some(676.0));
907        assert_eq!(m.3, Some(461.0));
908    }
909
910    #[test]
911    fn times_italic_metrics() {
912        let m = StandardFont::TimesItalic.canonical_metrics().unwrap();
913        assert_eq!(m.0, 683.0);
914        assert_eq!(m.1, -217.0);
915        assert_eq!(m.2, Some(653.0));
916        assert_eq!(m.3, Some(441.0));
917    }
918
919    #[test]
920    fn times_bold_italic_metrics() {
921        let m = StandardFont::TimesBoldItalic.canonical_metrics().unwrap();
922        assert_eq!(m.0, 683.0);
923        assert_eq!(m.1, -217.0);
924        assert_eq!(m.2, Some(669.0));
925        assert_eq!(m.3, Some(462.0));
926    }
927
928    #[test]
929    fn symbol_metrics_no_letterform_values() {
930        let m = StandardFont::Symbol.canonical_metrics().unwrap();
931        assert_eq!(m.0, 1010.0);
932        assert_eq!(m.1, -293.0);
933        assert!(m.2.is_none(), "Symbol has no cap_height");
934        assert!(m.3.is_none(), "Symbol has no x_height");
935    }
936
937    #[test]
938    fn zapf_dingbats_metrics_no_letterform_values() {
939        let m = StandardFont::ZapfDingBats.canonical_metrics().unwrap();
940        assert_eq!(m.0, 820.0);
941        assert_eq!(m.1, -143.0);
942        assert!(m.2.is_none(), "ZapfDingbats has no cap_height");
943        assert!(m.3.is_none(), "ZapfDingbats has no x_height");
944    }
945
946    #[test]
947    fn all_14_fonts_return_some() {
948        let all = &[
949            StandardFont::Helvetica,
950            StandardFont::HelveticaBold,
951            StandardFont::HelveticaOblique,
952            StandardFont::HelveticaBoldOblique,
953            StandardFont::Courier,
954            StandardFont::CourierBold,
955            StandardFont::CourierOblique,
956            StandardFont::CourierBoldOblique,
957            StandardFont::TimesRoman,
958            StandardFont::TimesBold,
959            StandardFont::TimesItalic,
960            StandardFont::TimesBoldItalic,
961            StandardFont::Symbol,
962            StandardFont::ZapfDingBats,
963        ];
964        for font in all {
965            assert!(
966                font.canonical_metrics().is_some(),
967                "font {:?} returned None",
968                font
969            );
970        }
971    }
972
973    #[test]
974    fn ascent_always_positive_descent_always_negative() {
975        let all = &[
976            StandardFont::Helvetica,
977            StandardFont::HelveticaBold,
978            StandardFont::HelveticaOblique,
979            StandardFont::HelveticaBoldOblique,
980            StandardFont::Courier,
981            StandardFont::CourierBold,
982            StandardFont::CourierOblique,
983            StandardFont::CourierBoldOblique,
984            StandardFont::TimesRoman,
985            StandardFont::TimesBold,
986            StandardFont::TimesItalic,
987            StandardFont::TimesBoldItalic,
988            StandardFont::Symbol,
989            StandardFont::ZapfDingBats,
990        ];
991        for font in all {
992            let (ascent, descent, _, _) = font.canonical_metrics().unwrap();
993            assert!(
994                ascent > 0.0,
995                "font {:?} ascent {} not positive",
996                font,
997                ascent
998            );
999            assert!(
1000                descent < 0.0,
1001                "font {:?} descent {} not negative",
1002                font,
1003                descent
1004            );
1005        }
1006    }
1007}