Skip to main content

pdf_interpret/font/
standard_font.rs

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