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_unicode, normalized_glyph_name, stretch_glyph,
7    strip_subset_prefix,
8};
9use kurbo::BezPath;
10use pdf_syntax::object::Dict;
11use pdf_syntax::object::Name;
12use pdf_syntax::object::dict::keys::{
13    BASE_FONT, FONT_DESC, FONT_FAMILY, FONT_WEIGHT, ITALIC_ANGLE, MISSING_WIDTH,
14};
15use skrifa::raw::TableProvider;
16use skrifa::{GlyphId, GlyphId16};
17use std::cell::RefCell;
18use std::collections::HashMap;
19
20/// The 14 standard fonts of PDF.
21#[derive(Copy, Clone, Debug)]
22pub enum StandardFont {
23    /// Helvetica.
24    Helvetica,
25    /// Helvetica Bold.
26    HelveticaBold,
27    /// Helvetica Oblique.
28    HelveticaOblique,
29    /// Helvetica Bold Oblique.
30    HelveticaBoldOblique,
31    /// Courier.
32    Courier,
33    /// Courier Bold.
34    CourierBold,
35    /// Courier Oblique.
36    CourierOblique,
37    /// Courier Bold Oblique.
38    CourierBoldOblique,
39    /// Times Roman.
40    TimesRoman,
41    /// Times Bold.
42    TimesBold,
43    /// Times Italic.
44    TimesItalic,
45    /// Times Bold Italic.
46    TimesBoldItalic,
47    /// Zapf Dingbats - a decorative symbol font.
48    ZapfDingBats,
49    /// Symbol - a mathematical symbol font.
50    Symbol,
51}
52
53impl StandardFont {
54    pub(crate) fn code_to_name(&self, code: u8) -> Option<&'static str> {
55        match self {
56            Self::Symbol => symbol::get(code),
57            // Note that this font does not return postscript character names,
58            // but instead has a custom encoding.
59            Self::ZapfDingBats => zapf_dings::get(code),
60            _ => standard::get(code),
61        }
62    }
63
64    pub(crate) fn get_width(&self, mut name: &str) -> Option<f32> {
65        // <https://github.com/apache/pdfbox/blob/129aafe26548c1ff935af9c55cb40a996186c35f/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDSimpleFont.java#L340>
66        if name == ".notdef" {
67            return Some(250.0);
68        }
69
70        name = normalized_glyph_name(name);
71
72        match self {
73            Self::Helvetica => metrics::HELVETICA.get(name).copied(),
74            Self::HelveticaBold => metrics::HELVETICA_BOLD.get(name).copied(),
75            Self::HelveticaOblique => metrics::HELVETICA_OBLIQUE.get(name).copied(),
76            Self::HelveticaBoldOblique => metrics::HELVETICA_BOLD_OBLIQUE.get(name).copied(),
77            Self::Courier => metrics::COURIER.get(name).copied(),
78            Self::CourierBold => metrics::COURIER_BOLD.get(name).copied(),
79            Self::CourierOblique => metrics::COURIER_OBLIQUE.get(name).copied(),
80            Self::CourierBoldOblique => metrics::COURIER_BOLD_OBLIQUE.get(name).copied(),
81            Self::TimesRoman => metrics::TIMES_ROMAN.get(name).copied(),
82            Self::TimesBold => metrics::TIMES_BOLD.get(name).copied(),
83            Self::TimesItalic => metrics::TIMES_ITALIC.get(name).copied(),
84            Self::TimesBoldItalic => metrics::TIMES_BOLD_ITALIC.get(name).copied(),
85            Self::ZapfDingBats => metrics::ZAPF_DING_BATS.get(name).copied(),
86            Self::Symbol => metrics::SYMBOL.get(name).copied(),
87        }
88    }
89
90    pub(crate) fn as_str(&self) -> &'static str {
91        match self {
92            Self::Helvetica => "Helvetica",
93            Self::HelveticaBold => "Helvetica Bold",
94            Self::HelveticaOblique => "Helvetica Oblique",
95            Self::HelveticaBoldOblique => "Helvetica Bold Oblique",
96            Self::Courier => "Courier",
97            Self::CourierBold => "Courier Bold",
98            Self::CourierOblique => "Courier Oblique",
99            Self::CourierBoldOblique => "Courier Bold Oblique",
100            Self::TimesRoman => "Times Roman",
101            Self::TimesBold => "Times Bold",
102            Self::TimesItalic => "Times Italic",
103            Self::TimesBoldItalic => "Times Bold Italic",
104            Self::ZapfDingBats => "Zapf Dingbats",
105            Self::Symbol => "Symbol",
106        }
107    }
108
109    /// Return the postscrit name of the font.
110    pub fn postscript_name(&self) -> &'static str {
111        match self {
112            Self::Helvetica => "Helvetica",
113            Self::HelveticaBold => "Helvetica-Bold",
114            Self::HelveticaOblique => "Helvetica-Oblique",
115            Self::HelveticaBoldOblique => "Helvetica-BoldOblique",
116            Self::Courier => "Courier",
117            Self::CourierBold => "Courier-Bold",
118            Self::CourierOblique => "Courier-Oblique",
119            Self::CourierBoldOblique => "Courier-BoldOblique",
120            Self::TimesRoman => "Times-Roman",
121            Self::TimesBold => "Times-Bold",
122            Self::TimesItalic => "Times-Italic",
123            Self::TimesBoldItalic => "Times-BoldItalic",
124            Self::ZapfDingBats => "ZapfDingbats",
125            Self::Symbol => "Symbol",
126        }
127    }
128
129    pub(crate) fn is_bold(&self) -> bool {
130        matches!(
131            self,
132            Self::HelveticaBold
133                | Self::HelveticaBoldOblique
134                | Self::CourierBold
135                | Self::CourierBoldOblique
136                | Self::TimesBold
137                | Self::TimesBoldItalic
138        )
139    }
140
141    pub(crate) fn is_italic(&self) -> bool {
142        matches!(
143            self,
144            Self::HelveticaOblique
145                | Self::HelveticaBoldOblique
146                | Self::CourierOblique
147                | Self::CourierBoldOblique
148                | Self::TimesItalic
149                | Self::TimesBoldItalic
150        )
151    }
152
153    pub(crate) fn is_serif(&self) -> bool {
154        matches!(
155            self,
156            Self::TimesRoman | Self::TimesBold | Self::TimesItalic | Self::TimesBoldItalic
157        )
158    }
159
160    pub(crate) fn is_monospace(&self) -> bool {
161        matches!(
162            self,
163            Self::Courier | Self::CourierBold | Self::CourierOblique | Self::CourierBoldOblique
164        )
165    }
166
167    /// Return suitable font data for the given standard font.
168    ///
169    /// Currently, this will return the corresponding Foxit font, which is a set of permissibly
170    /// licensed fonts that is also very light-weight.
171    ///
172    /// You can use the result of this method in your implementation of [`FontResolverFn`].
173    ///
174    /// [`FontResolverFn`]: crate::FontResolverFn
175    #[cfg(feature = "embed-fonts")]
176    pub fn get_font_data(&self) -> (FontData, u32) {
177        use std::sync::Arc;
178
179        let data = match self {
180            Self::Helvetica => &include_bytes!("../../assets/FoxitSans.pfb")[..],
181            Self::HelveticaBold => &include_bytes!("../../assets/FoxitSansBold.pfb")[..],
182            Self::HelveticaOblique => &include_bytes!("../../assets/FoxitSansItalic.pfb")[..],
183            Self::HelveticaBoldOblique => {
184                &include_bytes!("../../assets/FoxitSansBoldItalic.pfb")[..]
185            }
186            Self::Courier => &include_bytes!("../../assets/FoxitFixed.pfb")[..],
187            Self::CourierBold => &include_bytes!("../../assets/FoxitFixedBold.pfb")[..],
188            Self::CourierOblique => &include_bytes!("../../assets/FoxitFixedItalic.pfb")[..],
189            Self::CourierBoldOblique => {
190                &include_bytes!("../../assets/FoxitFixedBoldItalic.pfb")[..]
191            }
192            Self::TimesRoman => &include_bytes!("../../assets/FoxitSerif.pfb")[..],
193            Self::TimesBold => &include_bytes!("../../assets/FoxitSerifBold.pfb")[..],
194            Self::TimesItalic => &include_bytes!("../../assets/FoxitSerifItalic.pfb")[..],
195            Self::TimesBoldItalic => &include_bytes!("../../assets/FoxitSerifBoldItalic.pfb")[..],
196            Self::ZapfDingBats => &include_bytes!("../../assets/FoxitDingbats.pfb")[..],
197            Self::Symbol => {
198                include_bytes!("../../assets/FoxitSymbol.pfb")
199            }
200        };
201
202        (Arc::new(data), 0)
203    }
204}
205
206enum StandardFontFamily {
207    Helvetica,
208    Courier,
209    Times,
210}
211
212/// PostScript-name aliases commonly produced by Office/iText/etc. that refer
213/// to fonts the reader is expected to substitute with the corresponding
214/// Standard-14 font. Matched after subset-prefix stripping and after the
215/// literal Standard-14 names, but before the keyword-based heuristic.
216///
217/// Aliases are intentionally exact (case-sensitive) matches — the keyword
218/// heuristic below already catches free-form variants like "ArialNarrow-Bold".
219fn standard_font_alias(name: &str) -> Option<StandardFont> {
220    match name {
221        // Arial family → Helvetica
222        "ArialMT" | "Arial" => Some(StandardFont::Helvetica),
223        "Arial-BoldMT" | "Arial,Bold" | "Arial-Bold" => Some(StandardFont::HelveticaBold),
224        "Arial-ItalicMT" | "Arial,Italic" | "Arial-Italic" => Some(StandardFont::HelveticaOblique),
225        "Arial-BoldItalicMT" | "Arial,BoldItalic" | "Arial-BoldItalic" => {
226            Some(StandardFont::HelveticaBoldOblique)
227        }
228        // Times New Roman family → Times
229        "TimesNewRomanPSMT" | "TimesNewRoman" | "TimesNewRomanPS" => Some(StandardFont::TimesRoman),
230        "TimesNewRomanPS-BoldMT"
231        | "TimesNewRoman-Bold"
232        | "TimesNewRomanPS-Bold"
233        | "TimesNewRoman,Bold" => Some(StandardFont::TimesBold),
234        "TimesNewRomanPS-ItalicMT"
235        | "TimesNewRoman-Italic"
236        | "TimesNewRomanPS-Italic"
237        | "TimesNewRoman,Italic" => Some(StandardFont::TimesItalic),
238        "TimesNewRomanPS-BoldItalicMT"
239        | "TimesNewRoman-BoldItalic"
240        | "TimesNewRomanPS-BoldItalic"
241        | "TimesNewRoman,BoldItalic" => Some(StandardFont::TimesBoldItalic),
242        // Courier New family → Courier
243        "CourierNewPSMT" | "CourierNew" => Some(StandardFont::Courier),
244        "CourierNewPS-BoldMT" | "CourierNew-Bold" | "CourierNewPS-Bold" => {
245            Some(StandardFont::CourierBold)
246        }
247        "CourierNewPS-ItalicMT" | "CourierNew-Italic" | "CourierNewPS-Italic" => {
248            Some(StandardFont::CourierOblique)
249        }
250        "CourierNewPS-BoldItalicMT" | "CourierNew-BoldItalic" | "CourierNewPS-BoldItalic" => {
251            Some(StandardFont::CourierBoldOblique)
252        }
253        _ => None,
254    }
255}
256
257pub(crate) fn select_standard_font(
258    dict: &Dict<'_>,
259    descriptor: &Dict<'_>,
260) -> Option<(StandardFont, bool)> {
261    let base_font = dict.get::<Name>(BASE_FONT)?;
262    let name = strip_subset_prefix(base_font.as_str());
263
264    // First try whether it matches literally.
265    match name {
266        "Helvetica" => return Some((StandardFont::Helvetica, true)),
267        "Helvetica-Bold" => return Some((StandardFont::HelveticaBold, true)),
268        "Helvetica-Oblique" => return Some((StandardFont::HelveticaOblique, true)),
269        "Helvetica-BoldOblique" => return Some((StandardFont::HelveticaBoldOblique, true)),
270        "Courier" => return Some((StandardFont::Courier, true)),
271        "Courier-Bold" => return Some((StandardFont::CourierBold, true)),
272        "Courier-Oblique" => return Some((StandardFont::CourierOblique, true)),
273        "Courier-BoldOblique" => return Some((StandardFont::CourierBoldOblique, true)),
274        "Times-Roman" => return Some((StandardFont::TimesRoman, true)),
275        "Times-Bold" => return Some((StandardFont::TimesBold, true)),
276        "Times-Italic" => return Some((StandardFont::TimesItalic, true)),
277        "Times-BoldItalic" => return Some((StandardFont::TimesBoldItalic, true)),
278        "Symbol" => return Some((StandardFont::Symbol, true)),
279        "ZapfDingbats" => return Some((StandardFont::ZapfDingBats, true)),
280        _ => {}
281    }
282
283    // PostScript-name aliases commonly emitted by Office/iText/etc. for
284    // unembedded Standard-14-equivalent fonts (e.g. ArialMT → Helvetica).
285    // Treated as non-exact so glyph-width fallback in StandardKind still
286    // consults the supplied Widths array when present.
287    if let Some(alias) = standard_font_alias(name) {
288        return Some((alias, false));
289    }
290
291    // Now, we bruteforce, trying to determine a suitable font based on the
292    // keywords that appear in the name and the descriptor.
293    let lower = name.to_ascii_lowercase();
294
295    // FontFamily (descriptor) captures the human-readable family, which is
296    // often present even when BaseFont is an opaque subset name. PDF 1.7 §9.8.1
297    // specifies it as a text string, but producers in the wild use name
298    // objects too; fetch as Name (covers both via implicit conversion).
299    let family_field = descriptor
300        .get::<Name>(FONT_FAMILY)
301        .map(|n| n.as_str().to_ascii_lowercase())
302        .unwrap_or_default();
303
304    // PDF spec §9.8.2 Table 120: FontWeight is a number in {100, 200, … 900};
305    // 400 is normal and 700 is bold. Adobe considers weights ≥ 600 (SemiBold,
306    // DemiBold) as "bold" for substitution purposes — matching that lowers
307    // the threshold from 700 to 600 so fonts like "HelveticaNeue-Medium"
308    // (weight 500) stay regular but "*-SemiBold" (600) map to the bold face.
309    let is_bold = descriptor.get::<u32>(FONT_WEIGHT).is_some_and(|w| w >= 600)
310        || lower.contains("bold")
311        || lower.contains("demi")
312        || family_field.contains("bold")
313        || family_field.contains("demi");
314    // PDF spec §9.8.2 Table 120: ItalicAngle is the angle, in counter-clockwise
315    // degrees, of the dominant vertical strokes. Italic/oblique faces are
316    // negative (typically -10° to -20°). Previously we accepted any non-zero
317    // value, which mis-classified upright fonts that shipped with tiny
318    // rounding noise (e.g. -0.1). The stricter −5° threshold follows what
319    // PDF.js and PDFBox use and avoids that false-positive.
320    let is_italic = descriptor
321        .get::<f32>(ITALIC_ANGLE)
322        .is_some_and(|a| a < -5.0 || a > 5.0)
323        || lower.contains("italic")
324        || lower.contains("oblique")
325        || family_field.contains("italic")
326        || family_field.contains("oblique");
327
328    // Keyword/family heuristic. Prefer BaseFont; fall back to FontFamily.
329    let haystack = if family_field.is_empty() {
330        lower.clone()
331    } else {
332        format!("{lower} {family_field}")
333    };
334
335    // Keyword/family heuristic (last resort — only reached when the font name
336    // did not match any Standard-14 alias above).
337    //
338    // `exact` controls whether the caller should trust PDF /Widths entries or
339    // fall back to Standard-14 AFM metrics:
340    //   exact=true  → AFM metrics used; PDF /Widths ignored (safe for genuine
341    //                 Standard-14 faces whose names survived case folding here)
342    //   exact=false → PDF /Widths respected when present; AFM is only fallback
343    //                 (correct for non-Standard-14 lookalikes like "ArialMT")
344    //
345    // GL-QA38 regression note: setting exact=false for ALL heuristic matches
346    // caused 116 SSIM regressions in gate-5k-04 because many PDFs that contained
347    // "helvetica" or "times" in the font name ARE genuine Standard-14 and their
348    // /Widths arrays (when present) are less accurate than Standard-14 AFM.
349    // The corrected approach: treat Standard-14 keyword matches (helvetica,
350    // courier, times) as exact=true; treat clear non-Standard-14 keywords
351    // (arial, sans, mono, serif without "times") as exact=false so we respect
352    // their embedded /Widths.
353    let (family, exact) = if haystack.contains("helvetica") {
354        (Some(StandardFontFamily::Helvetica), true) // likely genuine Helvetica — use AFM
355    } else if haystack.contains("arial") || haystack.contains("sans") {
356        (Some(StandardFontFamily::Helvetica), false) // Arial/generic sans — respect /Widths
357    } else if haystack.contains("courier") {
358        (Some(StandardFontFamily::Courier), true) // likely genuine Courier — use AFM
359    } else if haystack.contains("mono") {
360        (Some(StandardFontFamily::Courier), false) // generic monospace — respect /Widths
361    } else if haystack.contains("times") {
362        (Some(StandardFontFamily::Times), true) // likely genuine Times — use AFM
363    } else if haystack.contains("serif") {
364        (Some(StandardFontFamily::Times), false) // generic serif — respect /Widths
365    } else if haystack.contains("zapfdingbats") || haystack.contains("dingbats") {
366        return Some((StandardFont::ZapfDingBats, false));
367    } else {
368        (None, false)
369    };
370
371    let font = match (family?, is_bold, is_italic) {
372        (StandardFontFamily::Helvetica, false, false) => StandardFont::Helvetica,
373        (StandardFontFamily::Helvetica, true, false) => StandardFont::HelveticaBold,
374        (StandardFontFamily::Helvetica, false, true) => StandardFont::HelveticaOblique,
375        (StandardFontFamily::Helvetica, true, true) => StandardFont::HelveticaBoldOblique,
376        (StandardFontFamily::Courier, false, false) => StandardFont::Courier,
377        (StandardFontFamily::Courier, true, false) => StandardFont::CourierBold,
378        (StandardFontFamily::Courier, false, true) => StandardFont::CourierOblique,
379        (StandardFontFamily::Courier, true, true) => StandardFont::CourierBoldOblique,
380        (StandardFontFamily::Times, false, false) => StandardFont::TimesRoman,
381        (StandardFontFamily::Times, true, false) => StandardFont::TimesBold,
382        (StandardFontFamily::Times, false, true) => StandardFont::TimesItalic,
383        (StandardFontFamily::Times, true, true) => StandardFont::TimesBoldItalic,
384    };
385
386    Some((font, exact))
387}
388
389#[derive(Debug)]
390pub(crate) enum StandardFontBlob {
391    Cff(CffFontBlob),
392    Otf(OpenTypeFontBlob, HashMap<String, GlyphId>),
393}
394
395impl StandardFontBlob {
396    pub(crate) fn from_data(data: FontData, index: u32) -> Option<Self> {
397        if let Some(blob) = CffFontBlob::new(data.clone()) {
398            Some(Self::new_cff(blob))
399        } else {
400            OpenTypeFontBlob::new(data, index).map(Self::new_otf)
401        }
402    }
403
404    pub(crate) fn new_cff(blob: CffFontBlob) -> Self {
405        Self::Cff(blob)
406    }
407
408    pub(crate) fn new_otf(blob: OpenTypeFontBlob) -> Self {
409        let mut glyph_names = HashMap::new();
410
411        if let Ok(post) = blob.font_ref().post() {
412            for i in 0..blob.num_glyphs() {
413                if let Some(str) = post.glyph_name(GlyphId16::new(i)) {
414                    glyph_names.insert(str.to_string(), GlyphId::new(i as u32));
415                }
416            }
417        }
418
419        Self::Otf(blob, glyph_names)
420    }
421}
422
423impl StandardFontBlob {
424    pub(crate) fn name_to_glyph(&self, name: &str) -> Option<GlyphId> {
425        match self {
426            Self::Cff(blob) => blob
427                .table()
428                .glyph_index_by_name(name)
429                .map(|g| GlyphId::new(g.0 as u32)),
430            Self::Otf(_, glyph_names) => glyph_names.get(name).copied(),
431        }
432    }
433
434    pub(crate) fn unicode_to_glyph(&self, code: u32) -> Option<GlyphId> {
435        match self {
436            Self::Cff(_) => None,
437            Self::Otf(blob, _) => blob
438                .font_ref()
439                .cmap()
440                .ok()
441                .and_then(|c| c.map_codepoint(code)),
442        }
443    }
444
445    pub(crate) fn advance_width(&self, glyph: GlyphId) -> Option<f32> {
446        match self {
447            Self::Cff(_) => None,
448            Self::Otf(blob, _) => blob.glyph_metrics().advance_width(glyph),
449        }
450    }
451
452    pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath {
453        // Standard fonts have empty outlines for these, but in Liberation Sans
454        // they are a .notdef rectangle.
455        if glyph == GlyphId::NOTDEF {
456            return BezPath::new();
457        }
458
459        match self {
460            Self::Cff(blob) => blob.outline_glyph(glyph),
461            Self::Otf(blob, _) => blob.outline_glyph(glyph),
462        }
463    }
464}
465
466#[derive(Debug)]
467pub(crate) struct StandardKind {
468    base_font: StandardFont,
469    base_font_blob: StandardFontBlob,
470    encoding: Encoding,
471    widths: Vec<Width>,
472    missing_width: Option<f32>,
473    fallback: bool,
474    glyph_to_code: RefCell<HashMap<GlyphId, u8>>,
475    encodings: HashMap<u8, String>,
476}
477
478impl StandardKind {
479    pub(crate) fn new(dict: &Dict<'_>, resolver: &FontResolverFn) -> Option<Self> {
480        let descriptor = dict.get::<Dict<'_>>(FONT_DESC).unwrap_or_default();
481        let (font, exact) = select_standard_font(dict, &descriptor)?;
482        Self::new_with_standard(dict, font, !exact, resolver)
483    }
484
485    pub(crate) fn new_with_standard(
486        dict: &Dict<'_>,
487        base_font: StandardFont,
488        fallback: bool,
489        resolver: &FontResolverFn,
490    ) -> Option<Self> {
491        let descriptor = dict.get::<Dict<'_>>(FONT_DESC).unwrap_or_default();
492        let (widths, missing_width) = read_widths(dict, &descriptor)?;
493        let missing_width = descriptor
494            .contains_key(MISSING_WIDTH)
495            .then_some(missing_width);
496
497        let (mut encoding, encoding_map) = read_encoding(dict);
498
499        // See PDFJS-16464: Ignore encodings for non-embedded Type1 symbol fonts.
500        if matches!(base_font, StandardFont::Symbol | StandardFont::ZapfDingBats) {
501            encoding = Encoding::BuiltIn;
502        }
503
504        let (blob, index) = resolver(&FontQuery::Standard(base_font))?;
505        let base_font_blob = StandardFontBlob::from_data(blob, index)?;
506
507        Some(Self {
508            base_font,
509            base_font_blob,
510            widths,
511            missing_width,
512            encodings: encoding_map,
513            glyph_to_code: RefCell::new(HashMap::new()),
514            fallback,
515            encoding,
516        })
517    }
518
519    fn code_to_ps_name(&self, code: u8) -> Option<&str> {
520        let bf = self.base_font;
521
522        self.encodings
523            .get(&code)
524            .map(String::as_str)
525            .or_else(|| match self.encoding {
526                Encoding::BuiltIn => bf.code_to_name(code),
527                _ => self.encoding.map_code(code),
528            })
529    }
530
531    pub(crate) fn map_code(&self, code: u8) -> GlyphId {
532        let result = self
533            .code_to_ps_name(code)
534            .and_then(|c| {
535                self.base_font_blob.name_to_glyph(c).or_else(|| {
536                    // If the font doesn't have a POST table, try to map via unicode instead.
537                    glyph_names::get(c).and_then(|c| {
538                        self.base_font_blob
539                            .unicode_to_glyph(c.chars().nth(0).unwrap() as u32)
540                    })
541                })
542            })
543            .unwrap_or(GlyphId::NOTDEF);
544        self.glyph_to_code.borrow_mut().insert(result, code);
545
546        result
547    }
548
549    pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath {
550        let path = self.base_font_blob.outline_glyph(glyph);
551
552        // If the font is not embedded, we might need to stretch it so that
553        // it matches the metrics of the actual underlying font blob.
554
555        if let Some(code) = self.glyph_to_code.borrow().get(&glyph).copied()
556            && let Some(actual_width) = self.base_font_blob.advance_width(glyph).or_else(|| {
557                self.code_to_ps_name(code)
558                    .and_then(|name| self.base_font.get_width(name))
559            })
560        {
561            // From my experiments: Most PDF viewers, if they detect a font is a
562            // standard font, they completely ignore the widths array, even if
563            // different widths are indicated there. So only if it's an unknown
564            // font do we check the widths array. Otherwise, we always use the
565            // base font metrics.
566            let should_width = if self.fallback {
567                if let Some(Width::Value(w)) = self.widths.get(code as usize).copied() {
568                    w
569                } else {
570                    return path;
571                }
572            } else if let Some(w) = self
573                .code_to_ps_name(code)
574                .and_then(|name| self.base_font.get_width(name))
575            {
576                w
577            } else {
578                return path;
579            };
580
581            return stretch_glyph(path, should_width, actual_width);
582        }
583
584        path
585    }
586
587    pub(crate) fn glyph_width(&self, code: u8) -> Option<f32> {
588        match self.widths.get(code as usize).copied() {
589            Some(Width::Value(w)) => Some(w),
590            Some(Width::Missing) => self.missing_width.or_else(|| {
591                self.code_to_ps_name(code)
592                    .and_then(|c| self.base_font.get_width(c))
593            }),
594            _ => self
595                .code_to_ps_name(code)
596                .and_then(|c| self.base_font.get_width(c)),
597        }
598    }
599
600    pub(crate) fn char_code_to_unicode(&self, code: u8) -> Option<char> {
601        self.code_to_ps_name(code).and_then(glyph_name_to_unicode)
602    }
603
604    pub(crate) fn is_italic(&self) -> bool {
605        self.base_font.is_italic()
606    }
607
608    pub(crate) fn is_bold(&self) -> bool {
609        self.base_font.is_bold()
610    }
611
612    pub(crate) fn is_serif(&self) -> bool {
613        self.base_font.is_serif()
614    }
615
616    pub(crate) fn is_monospace(&self) -> bool {
617        self.base_font.is_monospace()
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624
625    fn build_widths(entries: &[(u8, f32)]) -> Vec<Width> {
626        let mut widths = vec![Width::Missing; 256];
627        for (code, width) in entries {
628            widths[*code as usize] = Width::Value(*width);
629        }
630        widths
631    }
632
633    fn build_standard_kind(widths: Vec<Width>, missing_width: Option<f32>) -> StandardKind {
634        let (data, index) = StandardFont::Helvetica.get_font_data();
635        let base_font_blob =
636            StandardFontBlob::from_data(data, index).expect("standard font data should parse");
637
638        StandardKind {
639            base_font: StandardFont::Helvetica,
640            base_font_blob,
641            encoding: Encoding::WinAnsi,
642            widths,
643            missing_width,
644            fallback: true,
645            glyph_to_code: RefCell::new(HashMap::new()),
646            encodings: HashMap::new(),
647        }
648    }
649
650    #[test]
651    fn glyph_width_falls_back_to_base_metrics_when_missing_width_is_absent() {
652        let font = build_standard_kind(build_widths(&[(b'A', 600.0)]), None);
653
654        assert_eq!(font.glyph_width(b'A'), Some(600.0));
655        assert_eq!(
656            font.glyph_width(b'B'),
657            StandardFont::Helvetica.get_width("B")
658        );
659    }
660
661    #[test]
662    fn glyph_width_respects_explicit_zero_missing_width() {
663        let font = build_standard_kind(build_widths(&[(b'A', 600.0)]), Some(0.0));
664
665        assert_eq!(font.glyph_width(b'A'), Some(600.0));
666        assert_eq!(font.glyph_width(b'B'), Some(0.0));
667    }
668
669    #[test]
670    fn arial_aliases_resolve_to_helvetica_family() {
671        assert!(matches!(
672            standard_font_alias("ArialMT"),
673            Some(StandardFont::Helvetica)
674        ));
675        assert!(matches!(
676            standard_font_alias("Arial-BoldMT"),
677            Some(StandardFont::HelveticaBold)
678        ));
679        assert!(matches!(
680            standard_font_alias("Arial-ItalicMT"),
681            Some(StandardFont::HelveticaOblique)
682        ));
683        assert!(matches!(
684            standard_font_alias("Arial-BoldItalicMT"),
685            Some(StandardFont::HelveticaBoldOblique)
686        ));
687    }
688
689    #[test]
690    fn times_new_roman_aliases_resolve_to_times_family() {
691        assert!(matches!(
692            standard_font_alias("TimesNewRomanPSMT"),
693            Some(StandardFont::TimesRoman)
694        ));
695        assert!(matches!(
696            standard_font_alias("TimesNewRomanPS-BoldMT"),
697            Some(StandardFont::TimesBold)
698        ));
699        assert!(matches!(
700            standard_font_alias("TimesNewRomanPS-ItalicMT"),
701            Some(StandardFont::TimesItalic)
702        ));
703        assert!(matches!(
704            standard_font_alias("TimesNewRomanPS-BoldItalicMT"),
705            Some(StandardFont::TimesBoldItalic)
706        ));
707    }
708
709    #[test]
710    fn courier_new_aliases_resolve_to_courier_family() {
711        assert!(matches!(
712            standard_font_alias("CourierNewPSMT"),
713            Some(StandardFont::Courier)
714        ));
715        assert!(matches!(
716            standard_font_alias("CourierNewPS-BoldMT"),
717            Some(StandardFont::CourierBold)
718        ));
719        assert!(matches!(
720            standard_font_alias("CourierNewPS-ItalicMT"),
721            Some(StandardFont::CourierOblique)
722        ));
723        assert!(matches!(
724            standard_font_alias("CourierNewPS-BoldItalicMT"),
725            Some(StandardFont::CourierBoldOblique)
726        ));
727    }
728
729    #[test]
730    fn unknown_names_do_not_alias() {
731        assert!(standard_font_alias("LiberationSans").is_none());
732        assert!(standard_font_alias("CenturySchoolbook").is_none());
733        assert!(standard_font_alias("").is_none());
734    }
735}