Skip to main content

hayro_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 hayro_syntax::object::Dict;
10use hayro_syntax::object::Name;
11use hayro_syntax::object::dict::keys::{BASE_FONT, FONT_DESC, FONT_WEIGHT, ITALIC_ANGLE};
12use kurbo::BezPath;
13use skrifa::GlyphId;
14use skrifa::raw::TableProvider;
15use std::cell::RefCell;
16use std::collections::HashMap;
17
18/// The 14 standard fonts of PDF.
19#[derive(Copy, Clone, Debug)]
20pub enum StandardFont {
21    /// Helvetica.
22    Helvetica,
23    /// Helvetica Bold.
24    HelveticaBold,
25    /// Helvetica Oblique.
26    HelveticaOblique,
27    /// Helvetica Bold Oblique.
28    HelveticaBoldOblique,
29    /// Courier.
30    Courier,
31    /// Courier Bold.
32    CourierBold,
33    /// Courier Oblique.
34    CourierOblique,
35    /// Courier Bold Oblique.
36    CourierBoldOblique,
37    /// Times Roman.
38    TimesRoman,
39    /// Times Bold.
40    TimesBold,
41    /// Times Italic.
42    TimesItalic,
43    /// Times Bold Italic.
44    TimesBoldItalic,
45    /// Zapf Dingbats - a decorative symbol font.
46    ZapfDingBats,
47    /// Symbol - a mathematical symbol font.
48    Symbol,
49}
50
51impl StandardFont {
52    pub(crate) fn code_to_name(&self, code: u8) -> Option<&'static str> {
53        match self {
54            Self::Symbol => symbol::get(code),
55            // Note that this font does not return postscript character names,
56            // but instead has a custom encoding.
57            Self::ZapfDingBats => zapf_dings::get(code),
58            _ => standard::get(code),
59        }
60    }
61
62    pub(crate) fn get_width(&self, mut name: &str) -> Option<f32> {
63        // <https://github.com/apache/pdfbox/blob/129aafe26548c1ff935af9c55cb40a996186c35f/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDSimpleFont.java#L340>
64        if name == ".notdef" {
65            return Some(250.0);
66        }
67
68        name = normalized_glyph_name(name);
69
70        match self {
71            Self::Helvetica => metrics::HELVETICA.get(name).copied(),
72            Self::HelveticaBold => metrics::HELVETICA_BOLD.get(name).copied(),
73            Self::HelveticaOblique => metrics::HELVETICA_OBLIQUE.get(name).copied(),
74            Self::HelveticaBoldOblique => metrics::HELVETICA_BOLD_OBLIQUE.get(name).copied(),
75            Self::Courier => metrics::COURIER.get(name).copied(),
76            Self::CourierBold => metrics::COURIER_BOLD.get(name).copied(),
77            Self::CourierOblique => metrics::COURIER_OBLIQUE.get(name).copied(),
78            Self::CourierBoldOblique => metrics::COURIER_BOLD_OBLIQUE.get(name).copied(),
79            Self::TimesRoman => metrics::TIMES_ROMAN.get(name).copied(),
80            Self::TimesBold => metrics::TIMES_BOLD.get(name).copied(),
81            Self::TimesItalic => metrics::TIMES_ITALIC.get(name).copied(),
82            Self::TimesBoldItalic => metrics::TIMES_BOLD_ITALIC.get(name).copied(),
83            Self::ZapfDingBats => metrics::ZAPF_DING_BATS.get(name).copied(),
84            Self::Symbol => metrics::SYMBOL.get(name).copied(),
85        }
86    }
87
88    pub(crate) fn as_str(&self) -> &'static str {
89        match self {
90            Self::Helvetica => "Helvetica",
91            Self::HelveticaBold => "Helvetica Bold",
92            Self::HelveticaOblique => "Helvetica Oblique",
93            Self::HelveticaBoldOblique => "Helvetica Bold Oblique",
94            Self::Courier => "Courier",
95            Self::CourierBold => "Courier Bold",
96            Self::CourierOblique => "Courier Oblique",
97            Self::CourierBoldOblique => "Courier Bold Oblique",
98            Self::TimesRoman => "Times Roman",
99            Self::TimesBold => "Times Bold",
100            Self::TimesItalic => "Times Italic",
101            Self::TimesBoldItalic => "Times Bold Italic",
102            Self::ZapfDingBats => "Zapf Dingbats",
103            Self::Symbol => "Symbol",
104        }
105    }
106
107    /// Return the postscrit name of the font.
108    pub fn postscript_name(&self) -> &'static str {
109        match self {
110            Self::Helvetica => "Helvetica",
111            Self::HelveticaBold => "Helvetica-Bold",
112            Self::HelveticaOblique => "Helvetica-Oblique",
113            Self::HelveticaBoldOblique => "Helvetica-BoldOblique",
114            Self::Courier => "Courier",
115            Self::CourierBold => "Courier-Bold",
116            Self::CourierOblique => "Courier-Oblique",
117            Self::CourierBoldOblique => "Courier-BoldOblique",
118            Self::TimesRoman => "Times-Roman",
119            Self::TimesBold => "Times-Bold",
120            Self::TimesItalic => "Times-Italic",
121            Self::TimesBoldItalic => "Times-BoldItalic",
122            Self::ZapfDingBats => "ZapfDingbats",
123            Self::Symbol => "Symbol",
124        }
125    }
126
127    pub(crate) fn is_bold(&self) -> bool {
128        matches!(
129            self,
130            Self::HelveticaBold
131                | Self::HelveticaBoldOblique
132                | Self::CourierBold
133                | Self::CourierBoldOblique
134                | Self::TimesBold
135                | Self::TimesBoldItalic
136        )
137    }
138
139    pub(crate) fn is_italic(&self) -> bool {
140        matches!(
141            self,
142            Self::HelveticaOblique
143                | Self::HelveticaBoldOblique
144                | Self::CourierOblique
145                | Self::CourierBoldOblique
146                | Self::TimesItalic
147                | Self::TimesBoldItalic
148        )
149    }
150
151    pub(crate) fn is_serif(&self) -> bool {
152        matches!(
153            self,
154            Self::TimesRoman | Self::TimesBold | Self::TimesItalic | Self::TimesBoldItalic
155        )
156    }
157
158    pub(crate) fn is_monospace(&self) -> bool {
159        matches!(
160            self,
161            Self::Courier | Self::CourierBold | Self::CourierOblique | Self::CourierBoldOblique
162        )
163    }
164
165    /// Return suitable font data for the given standard font.
166    ///
167    /// Currently, this will return the corresponding Foxit font, which is a set of permissibly
168    /// licensed fonts that is also very light-weight.
169    ///
170    /// You can use the result of this method in your implementation of [`FontResolverFn`].
171    ///
172    /// [`FontResolverFn`]: crate::FontResolverFn
173    #[cfg(feature = "embed-fonts")]
174    pub fn get_font_data(&self) -> (FontData, u32) {
175        use std::sync::Arc;
176
177        let data = match self {
178            Self::Helvetica => &include_bytes!("../../assets/FoxitSans.pfb")[..],
179            Self::HelveticaBold => &include_bytes!("../../assets/FoxitSansBold.pfb")[..],
180            Self::HelveticaOblique => &include_bytes!("../../assets/FoxitSansItalic.pfb")[..],
181            Self::HelveticaBoldOblique => {
182                &include_bytes!("../../assets/FoxitSansBoldItalic.pfb")[..]
183            }
184            Self::Courier => &include_bytes!("../../assets/FoxitFixed.pfb")[..],
185            Self::CourierBold => &include_bytes!("../../assets/FoxitFixedBold.pfb")[..],
186            Self::CourierOblique => &include_bytes!("../../assets/FoxitFixedItalic.pfb")[..],
187            Self::CourierBoldOblique => {
188                &include_bytes!("../../assets/FoxitFixedBoldItalic.pfb")[..]
189            }
190            Self::TimesRoman => &include_bytes!("../../assets/FoxitSerif.pfb")[..],
191            Self::TimesBold => &include_bytes!("../../assets/FoxitSerifBold.pfb")[..],
192            Self::TimesItalic => &include_bytes!("../../assets/FoxitSerifItalic.pfb")[..],
193            Self::TimesBoldItalic => &include_bytes!("../../assets/FoxitSerifBoldItalic.pfb")[..],
194            Self::ZapfDingBats => &include_bytes!("../../assets/FoxitDingbats.pfb")[..],
195            Self::Symbol => {
196                include_bytes!("../../assets/FoxitSymbol.pfb")
197            }
198        };
199
200        (Arc::new(data), 0)
201    }
202}
203
204enum StandardFontFamily {
205    Helvetica,
206    Courier,
207    Times,
208}
209
210pub(crate) fn select_standard_font(
211    dict: &Dict<'_>,
212    descriptor: &Dict<'_>,
213) -> Option<(StandardFont, bool)> {
214    let base_font = dict.get::<Name<'_>>(BASE_FONT)?;
215    let name = strip_subset_prefix(base_font.as_str());
216
217    // First try whether it matches literally.
218    match name {
219        "Helvetica" => return Some((StandardFont::Helvetica, true)),
220        "Helvetica-Bold" => return Some((StandardFont::HelveticaBold, true)),
221        "Helvetica-Oblique" => return Some((StandardFont::HelveticaOblique, true)),
222        "Helvetica-BoldOblique" => return Some((StandardFont::HelveticaBoldOblique, true)),
223        "Courier" => return Some((StandardFont::Courier, true)),
224        "Courier-Bold" => return Some((StandardFont::CourierBold, true)),
225        "Courier-Oblique" => return Some((StandardFont::CourierOblique, true)),
226        "Courier-BoldOblique" => return Some((StandardFont::CourierBoldOblique, true)),
227        "Times-Roman" => return Some((StandardFont::TimesRoman, true)),
228        "Times-Bold" => return Some((StandardFont::TimesBold, true)),
229        "Times-Italic" => return Some((StandardFont::TimesItalic, true)),
230        "Times-BoldItalic" => return Some((StandardFont::TimesBoldItalic, true)),
231        "Symbol" => return Some((StandardFont::Symbol, true)),
232        "ZapfDingbats" => return Some((StandardFont::ZapfDingBats, true)),
233        _ => {}
234    }
235
236    // Now, we bruteforce, trying to determine a suitable font based on the
237    // keywords that appear in the name.
238    let lower = name.to_ascii_lowercase();
239
240    let is_bold = descriptor.get::<u32>(FONT_WEIGHT).is_some_and(|w| w >= 700)
241        || lower.contains("bold")
242        || lower.contains("demi");
243    let is_italic = descriptor
244        .get::<f32>(ITALIC_ANGLE)
245        .is_some_and(|a| a != 0.0)
246        || lower.contains("italic")
247        || lower.contains("oblique");
248
249    let (family, exact) = if lower.contains("helvetica") {
250        (Some(StandardFontFamily::Helvetica), true)
251    } else if lower.contains("arial") || lower.contains("sans") {
252        (Some(StandardFontFamily::Helvetica), false)
253    } else if lower.contains("courier") {
254        (Some(StandardFontFamily::Courier), true)
255    } else if lower.contains("mono") {
256        (Some(StandardFontFamily::Courier), false)
257    } else if lower.contains("times") {
258        (Some(StandardFontFamily::Times), true)
259    } else if lower.contains("serif") {
260        (Some(StandardFontFamily::Times), false)
261    } else if lower.contains("zapfdingbats") || lower.contains("dingbats") {
262        return Some((StandardFont::ZapfDingBats, false));
263    } else {
264        (None, false)
265    };
266
267    let font = match (family?, is_bold, is_italic) {
268        (StandardFontFamily::Helvetica, false, false) => StandardFont::Helvetica,
269        (StandardFontFamily::Helvetica, true, false) => StandardFont::HelveticaBold,
270        (StandardFontFamily::Helvetica, false, true) => StandardFont::HelveticaOblique,
271        (StandardFontFamily::Helvetica, true, true) => StandardFont::HelveticaBoldOblique,
272        (StandardFontFamily::Courier, false, false) => StandardFont::Courier,
273        (StandardFontFamily::Courier, true, false) => StandardFont::CourierBold,
274        (StandardFontFamily::Courier, false, true) => StandardFont::CourierOblique,
275        (StandardFontFamily::Courier, true, true) => StandardFont::CourierBoldOblique,
276        (StandardFontFamily::Times, false, false) => StandardFont::TimesRoman,
277        (StandardFontFamily::Times, true, false) => StandardFont::TimesBold,
278        (StandardFontFamily::Times, false, true) => StandardFont::TimesItalic,
279        (StandardFontFamily::Times, true, true) => StandardFont::TimesBoldItalic,
280    };
281
282    Some((font, exact))
283}
284
285#[derive(Debug)]
286pub(crate) enum StandardFontBlob {
287    Cff(CffFontBlob),
288    Otf(OpenTypeFontBlob, HashMap<String, GlyphId>),
289}
290
291impl StandardFontBlob {
292    pub(crate) fn from_data(data: FontData, index: u32) -> Option<Self> {
293        if let Some(blob) = CffFontBlob::new(data.clone()) {
294            Some(Self::new_cff(blob))
295        } else {
296            OpenTypeFontBlob::new(data, index).map(Self::new_otf)
297        }
298    }
299
300    pub(crate) fn new_cff(blob: CffFontBlob) -> Self {
301        Self::Cff(blob)
302    }
303
304    pub(crate) fn new_otf(blob: OpenTypeFontBlob) -> Self {
305        let glyph_names = blob.glyph_names();
306        Self::Otf(blob, glyph_names)
307    }
308}
309
310impl StandardFontBlob {
311    pub(crate) fn name_to_glyph(&self, name: &str) -> Option<GlyphId> {
312        match self {
313            Self::Cff(blob) => blob.glyph_index_by_name(name),
314            Self::Otf(_, glyph_names) => glyph_names.get(name).copied(),
315        }
316    }
317
318    pub(crate) fn unicode_to_glyph(&self, code: u32) -> Option<GlyphId> {
319        match self {
320            Self::Cff(_) => None,
321            Self::Otf(blob, _) => blob
322                .font_ref()
323                .cmap()
324                .ok()
325                .and_then(|c| c.map_codepoint(code)),
326        }
327    }
328
329    pub(crate) fn advance_width(&self, glyph: GlyphId) -> Option<f32> {
330        match self {
331            Self::Cff(_) => None,
332            Self::Otf(blob, _) => blob.glyph_metrics().advance_width(glyph),
333        }
334    }
335
336    pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath {
337        // Standard fonts have empty outlines for these, but in Liberation Sans
338        // they are a .notdef rectangle.
339        if glyph == GlyphId::NOTDEF {
340            return BezPath::new();
341        }
342
343        match self {
344            Self::Cff(blob) => blob.outline_glyph(glyph),
345            Self::Otf(blob, _) => blob.outline_glyph(glyph),
346        }
347    }
348}
349
350#[derive(Debug)]
351pub(crate) struct StandardKind {
352    base_font: StandardFont,
353    base_font_blob: StandardFontBlob,
354    encoding: Encoding,
355    widths: Vec<Width>,
356    missing_width: f32,
357    fallback: bool,
358    glyph_to_code: RefCell<HashMap<GlyphId, u8>>,
359    encodings: HashMap<u8, String>,
360}
361
362impl StandardKind {
363    pub(crate) fn new(dict: &Dict<'_>, resolver: &FontResolverFn) -> Option<Self> {
364        let descriptor = dict.get::<Dict<'_>>(FONT_DESC).unwrap_or_default();
365        let (font, exact) = select_standard_font(dict, &descriptor)?;
366        Self::new_with_standard(dict, font, !exact, resolver)
367    }
368
369    pub(crate) fn new_with_standard(
370        dict: &Dict<'_>,
371        base_font: StandardFont,
372        fallback: bool,
373        resolver: &FontResolverFn,
374    ) -> Option<Self> {
375        let descriptor = dict.get::<Dict<'_>>(FONT_DESC).unwrap_or_default();
376        let (widths, missing_width) = read_widths(dict, &descriptor)?;
377
378        let (mut encoding, encoding_map) = read_encoding(dict);
379
380        // See PDFJS-16464: Ignore encodings for non-embedded Type1 symbol fonts.
381        if matches!(base_font, StandardFont::Symbol | StandardFont::ZapfDingBats) {
382            encoding = Encoding::BuiltIn;
383        }
384
385        let (blob, index) = resolver(&FontQuery::Standard(base_font))?;
386        let base_font_blob = StandardFontBlob::from_data(blob, index)?;
387
388        Some(Self {
389            base_font,
390            base_font_blob,
391            widths,
392            missing_width,
393            encodings: encoding_map,
394            glyph_to_code: RefCell::new(HashMap::new()),
395            fallback,
396            encoding,
397        })
398    }
399
400    fn code_to_ps_name(&self, code: u8) -> Option<&str> {
401        let bf = self.base_font;
402
403        self.encodings
404            .get(&code)
405            .map(String::as_str)
406            .or_else(|| match self.encoding {
407                Encoding::BuiltIn => bf.code_to_name(code),
408                _ => self.encoding.map_code(code),
409            })
410    }
411
412    pub(crate) fn map_code(&self, code: u8) -> GlyphId {
413        let result = self
414            .code_to_ps_name(code)
415            .and_then(|c| {
416                self.base_font_blob.name_to_glyph(c).or_else(|| {
417                    // If the font doesn't have a POST table, try to map via unicode instead.
418                    glyph_names::get(c).and_then(|c| {
419                        self.base_font_blob
420                            .unicode_to_glyph(c.chars().nth(0).unwrap() as u32)
421                    })
422                })
423            })
424            .unwrap_or(GlyphId::NOTDEF);
425        self.glyph_to_code.borrow_mut().insert(result, code);
426
427        result
428    }
429
430    pub(crate) fn outline_glyph(&self, glyph: GlyphId) -> BezPath {
431        let path = self.base_font_blob.outline_glyph(glyph);
432
433        // If the font is not embedded, we might need to stretch it so that
434        // it matches the metrics of the actual underlying font blob.
435
436        if let Some(code) = self.glyph_to_code.borrow().get(&glyph).copied()
437            && let Some(actual_width) = self.base_font_blob.advance_width(glyph).or_else(|| {
438                self.code_to_ps_name(code)
439                    .and_then(|name| self.base_font.get_width(name))
440            })
441        {
442            // From my experiments: Most PDF viewers, if they detect a font is a
443            // standard font, they completely ignore the widths array, even if
444            // different widths are indicated there. So only if it's an unknown
445            // font do we check the widths array. Otherwise, we always use the
446            // base font metrics.
447            let should_width = if self.fallback {
448                if let Some(Width::Value(w)) = self.widths.get(code as usize).copied() {
449                    w
450                } else {
451                    return path;
452                }
453            } else if let Some(w) = self
454                .code_to_ps_name(code)
455                .and_then(|name| self.base_font.get_width(name))
456            {
457                w
458            } else {
459                return path;
460            };
461
462            return stretch_glyph(path, should_width, actual_width);
463        }
464
465        path
466    }
467
468    pub(crate) fn glyph_width(&self, code: u8) -> Option<f32> {
469        match self.widths.get(code as usize).copied() {
470            Some(Width::Value(w)) => Some(w),
471            Some(Width::Missing) => Some(self.missing_width),
472            None => self
473                .code_to_ps_name(code)
474                .and_then(|c| self.base_font.get_width(c)),
475        }
476    }
477
478    pub(crate) fn char_code_to_unicode(&self, code: u8) -> Option<char> {
479        self.code_to_ps_name(code).and_then(glyph_name_to_unicode)
480    }
481
482    pub(crate) fn is_italic(&self) -> bool {
483        self.base_font.is_italic()
484    }
485
486    pub(crate) fn is_bold(&self) -> bool {
487        self.base_font.is_bold()
488    }
489
490    pub(crate) fn is_serif(&self) -> bool {
491        self.base_font.is_serif()
492    }
493
494    pub(crate) fn is_monospace(&self) -> bool {
495        self.base_font.is_monospace()
496    }
497}