Skip to main content

hayro_interpret/font/
mod.rs

1//! Interacting with the different kinds of PDF fonts.
2
3use crate::context::Context;
4use crate::context::InterpreterCache;
5use crate::device::Device;
6use crate::font::cid::Type0Font;
7use crate::font::generated::{
8    glyph_names, mac_expert, mac_os_roman, mac_roman, standard, win_ansi,
9};
10use crate::font::true_type::TrueTypeFont;
11use crate::font::type1::Type1Font;
12use crate::font::type3::Type3;
13use crate::interpret::state::State;
14use crate::{CMapResolverFn, CacheKey, FontResolverFn, InterpreterSettings, Paint};
15use bitflags::bitflags;
16use hayro_syntax::object::Name;
17use hayro_syntax::object::dict::keys::SUBTYPE;
18use hayro_syntax::object::dict::keys::*;
19use hayro_syntax::object::{Dict, Stream};
20use hayro_syntax::page::Resources;
21use hayro_syntax::xref::XRef;
22use kurbo::{Affine, BezPath, Vec2};
23use outline::OutlineFont;
24use skrifa::GlyphId;
25use std::borrow::Cow;
26use std::fmt::Debug;
27use std::ops::Deref;
28use std::rc::Rc;
29use std::sync::Arc;
30
31mod blob;
32mod cid;
33mod generated;
34mod glyph_simulator;
35pub(crate) mod outline;
36mod standard_font;
37mod true_type;
38mod type1;
39pub(crate) mod type3;
40
41pub(crate) const UNITS_PER_EM: f32 = 1000.0;
42
43pub(crate) fn stretch_glyph(path: BezPath, expected_width: f32, actual_width: f32) -> BezPath {
44    if actual_width != 0.0 && actual_width != expected_width {
45        let stretch_factor = expected_width / actual_width;
46        Affine::scale_non_uniform(stretch_factor as f64, 1.0) * path
47    } else {
48        path
49    }
50}
51
52/// A container for the bytes of a PDF file.
53pub type FontData = Arc<dyn AsRef<[u8]> + Send + Sync>;
54
55/// Strip the 6-character subset prefix from a PostScript font name.
56///
57/// PDF subset fonts use names like "ABCDEF+TimesNewRoman". This function
58/// returns `TimesNewRoman` from such a name, or the original name if no
59/// valid prefix is found.
60pub(crate) fn strip_subset_prefix(name: &str) -> &str {
61    match name.split_once('+') {
62        Some((prefix, rest)) if prefix.len() == 6 => rest,
63        _ => name,
64    }
65}
66
67use crate::util::hash128;
68use hayro_cmap::{BfString, CMap, CMapName, CharacterCollection};
69pub use outline::OutlineFontData;
70pub use standard_font::StandardFont;
71
72/// A glyph that can be drawn.
73pub enum Glyph<'a> {
74    /// A glyph defined by an outline.
75    Outline(OutlineGlyph),
76    /// A type3 glyph, defined by PDF drawing instructions.
77    Type3(Box<Type3Glyph<'a>>),
78}
79
80impl Glyph<'_> {
81    /// Returns the Unicode code point for this glyph, if available.
82    ///
83    /// This method attempts to determine the Unicode character that this glyph
84    /// represents. The exact fallback chain depends on the font type:
85    ///
86    /// **For Outline Fonts (Type1, TrueType, CFF):**
87    /// 1. `ToUnicode` cmap
88    /// 2. Glyph name → Unicode (via Adobe Glyph List)
89    /// 3. Unicode naming conventions (e.g., "uni0041", "u0041")
90    ///
91    /// **For CID Fonts (Type0):**
92    /// 1. `ToUnicode` cmap
93    ///
94    ///
95    /// **For Type3 Fonts:**
96    /// 1. `ToUnicode` cmap
97    ///
98    /// Returns `None` if the Unicode value could not be determined.
99    ///
100    /// Please note that this method is still somewhat experimental and might
101    /// not work reliably in all cases.
102    pub fn as_unicode(&self) -> Option<BfString> {
103        match self {
104            Glyph::Outline(g) => g.as_unicode(),
105            Glyph::Type3(g) => g.as_unicode(),
106        }
107    }
108}
109
110/// An identifier that uniquely identifies a glyph, for caching purposes.
111#[derive(Clone, Debug)]
112pub struct GlyphIdentifier {
113    id: GlyphId,
114    font: OutlineFont,
115}
116
117impl CacheKey for GlyphIdentifier {
118    fn cache_key(&self) -> u128 {
119        hash128(&(self.id, self.font.cache_key()))
120    }
121}
122
123/// A glyph defined by an outline.
124#[derive(Clone, Debug)]
125pub struct OutlineGlyph {
126    pub(crate) id: GlyphId,
127    pub(crate) font: OutlineFont,
128    pub(crate) char_code: u32,
129}
130
131impl OutlineGlyph {
132    /// Return the outline of the glyph, assuming an upem value of 1000.
133    pub fn outline(&self) -> BezPath {
134        self.font.outline_glyph(self.id, self.char_code)
135    }
136
137    /// Return the identifier of the glyph. You can use this to calculate the cache key
138    /// for the glyph.
139    ///
140    /// Note that the `glyph_transform` attribute is not considered in the cache key of
141    /// the identifier, only the glyph ID and the font.
142    pub fn identifier(&self) -> GlyphIdentifier {
143        GlyphIdentifier {
144            id: self.id,
145            font: self.font.clone(),
146        }
147    }
148
149    /// Returns the Unicode code point for this glyph, if available.
150    ///
151    /// See [`Glyph::as_unicode`] for details on the fallback chain used.
152    pub fn as_unicode(&self) -> Option<BfString> {
153        self.font.char_code_to_unicode(self.char_code)
154    }
155
156    /// Get raw font bytes and metadata for downstream use.
157    ///
158    /// Returns `None` for Type1 fonts.
159    pub fn font_data(&self) -> Option<OutlineFontData> {
160        self.font.font_data()
161    }
162
163    /// Get the glyph ID within the font.
164    pub fn glyph_id(&self) -> GlyphId {
165        self.id
166    }
167
168    /// Get the advance width for this glyph.
169    ///
170    /// The advance width is how far to move horizontally after drawing
171    /// this glyph before drawing the next one.
172    pub fn advance_width(&self) -> Option<f32> {
173        self.font.glyph_advance_width(self.char_code)
174    }
175
176    /// Get the cache key for this glyph's font.
177    ///
178    /// This identifies the font uniquely, even when `font_data()` returns `None`
179    /// (e.g., for Type1 fonts). Useful for grouping glyphs by font.
180    pub fn font_cache_key(&self) -> u128 {
181        self.font.cache_key()
182    }
183}
184
185/// A type3 glyph.
186#[derive(Clone)]
187pub struct Type3Glyph<'a> {
188    pub(crate) font: Rc<Type3<'a>>,
189    pub(crate) glyph_id: GlyphId,
190    pub(crate) state: State<'a>,
191    pub(crate) parent_resources: Resources<'a>,
192    pub(crate) cache: InterpreterCache<'a>,
193    pub(crate) xref: &'a XRef,
194    pub(crate) settings: InterpreterSettings,
195    pub(crate) nesting_depth: u32,
196    pub(crate) char_code: u32,
197}
198
199/// A glyph defined by PDF drawing instructions.
200impl<'a> Type3Glyph<'a> {
201    /// Draw the type3 glyph to the given device.
202    pub fn interpret(
203        &self,
204        device: &mut impl Device<'a>,
205        transform: Affine,
206        glyph_transform: Affine,
207        paint: &Paint<'a>,
208    ) {
209        self.font
210            .render_glyph(self, transform, glyph_transform, paint, device);
211    }
212
213    /// Returns the Unicode code point for this glyph, if available.
214    ///
215    /// Note: Type3 fonts can only provide Unicode via `ToUnicode` cmap.
216    pub fn as_unicode(&self) -> Option<BfString> {
217        self.font.char_code_to_unicode(self.char_code)
218    }
219}
220
221impl CacheKey for Type3Glyph<'_> {
222    fn cache_key(&self) -> u128 {
223        hash128(&(self.font.cache_key(), self.glyph_id))
224    }
225}
226
227#[derive(Clone, Debug)]
228pub(crate) struct Font<'a>(u128, FontType<'a>);
229
230impl<'a> Font<'a> {
231    pub(crate) fn new(
232        dict: &Dict<'a>,
233        font_resolver: &FontResolverFn,
234        cmap_resolver: &CMapResolverFn,
235    ) -> Option<Self> {
236        let f_type = match dict.get::<Name<'_>>(SUBTYPE)?.deref() {
237            TYPE1 | MM_TYPE1 => {
238                FontType::Type1(Rc::new(Type1Font::new(dict, font_resolver, cmap_resolver)?))
239            }
240            // PDFBOX-5463: PDF viewers seem to accept OpenType as well.
241            TRUE_TYPE | OPEN_TYPE => FontType::TrueType(Rc::new(TrueTypeFont::new(
242                dict,
243                font_resolver,
244                cmap_resolver,
245            )?)),
246            TYPE0 => FontType::Type0(Rc::new(Type0Font::new(dict, font_resolver, cmap_resolver)?)),
247            TYPE3 => FontType::Type3(Rc::new(Type3::new(dict, cmap_resolver)?)),
248            f => {
249                warn!(
250                    "unimplemented font type {:?}",
251                    std::str::from_utf8(f).unwrap_or("unknown type")
252                );
253
254                return None;
255            }
256        };
257
258        let cache_key = dict.cache_key();
259
260        Some(Self(cache_key, f_type))
261    }
262
263    pub(crate) fn new_standard(
264        standard_font: StandardFont,
265        font_resolver: &FontResolverFn,
266    ) -> Option<Self> {
267        let font = Type1Font::new_standard(standard_font, font_resolver)?;
268
269        Some(Self(0, FontType::Type1(Rc::new(font))))
270    }
271
272    pub(crate) fn map_code(&self, code: u32) -> GlyphId {
273        match &self.1 {
274            FontType::Type1(f) => {
275                debug_assert!(code <= u8::MAX as u32);
276
277                f.map_code(code as u8)
278            }
279            FontType::TrueType(t) => {
280                debug_assert!(code <= u8::MAX as u32);
281
282                t.map_code(code as u8)
283            }
284            FontType::Type0(t) => t.map_code(code),
285            FontType::Type3(t) => {
286                debug_assert!(code <= u8::MAX as u32);
287
288                t.map_code(code as u8)
289            }
290        }
291    }
292
293    pub(crate) fn get_glyph(
294        &self,
295        glyph: GlyphId,
296        char_code: u32,
297        ctx: &mut Context<'a>,
298        resources: &Resources<'a>,
299        origin_displacement: Vec2,
300    ) -> (Glyph<'a>, Affine) {
301        let glyph_transform = ctx.get().text_state.full_transform()
302            * Affine::scale(1.0 / UNITS_PER_EM as f64)
303            * Affine::translate(origin_displacement);
304
305        let glyph = match &self.1 {
306            FontType::Type1(t) => {
307                let font = OutlineFont::Type1(t.clone());
308                Glyph::Outline(OutlineGlyph {
309                    id: glyph,
310                    font,
311                    char_code,
312                })
313            }
314            FontType::TrueType(t) => {
315                let font = OutlineFont::TrueType(t.clone());
316                Glyph::Outline(OutlineGlyph {
317                    id: glyph,
318                    font,
319                    char_code,
320                })
321            }
322            FontType::Type0(t) => {
323                let font = OutlineFont::Type0(t.clone());
324                Glyph::Outline(OutlineGlyph {
325                    id: glyph,
326                    font,
327                    char_code,
328                })
329            }
330            FontType::Type3(t) => {
331                let nesting_depth = ctx.nesting_depth() + 1;
332                let shape_glyph = Type3Glyph {
333                    font: t.clone(),
334                    glyph_id: glyph,
335                    state: ctx.get().clone(),
336                    parent_resources: resources.clone(),
337                    cache: ctx.interpreter_cache.clone(),
338                    xref: ctx.xref,
339                    settings: ctx.settings.clone(),
340                    nesting_depth,
341                    char_code,
342                };
343
344                Glyph::Type3(Box::new(shape_glyph))
345            }
346        };
347
348        (glyph, glyph_transform)
349    }
350
351    pub(crate) fn code_advance(&self, code: u32) -> Vec2 {
352        match &self.1 {
353            FontType::Type1(t) => {
354                debug_assert!(code <= u8::MAX as u32);
355
356                Vec2::new(t.glyph_width(code as u8).unwrap_or(0.0) as f64, 0.0)
357            }
358            FontType::TrueType(t) => {
359                debug_assert!(code <= u8::MAX as u32);
360
361                Vec2::new(t.glyph_width(code as u8) as f64, 0.0)
362            }
363            FontType::Type0(t) => t.code_advance(code),
364            FontType::Type3(t) => {
365                debug_assert!(code <= u8::MAX as u32);
366
367                Vec2::new(t.glyph_width(code as u8) as f64, 0.0)
368            }
369        }
370    }
371
372    pub(crate) fn origin_displacement(&self, code: u32) -> Vec2 {
373        match &self.1 {
374            FontType::Type1(_) => Vec2::default(),
375            FontType::TrueType(_) => Vec2::default(),
376            FontType::Type0(t) => t.origin_displacement(code),
377            FontType::Type3(_) => Vec2::default(),
378        }
379    }
380
381    pub(crate) fn read_code(&self, bytes: &[u8], offset: usize) -> (u32, usize) {
382        match &self.1 {
383            FontType::Type1(_) => (bytes[offset] as u32, 1),
384            FontType::TrueType(_) => (bytes[offset] as u32, 1),
385            FontType::Type0(t) => t.read_code(bytes, offset),
386            FontType::Type3(_) => (bytes[offset] as u32, 1),
387        }
388    }
389
390    pub(crate) fn is_horizontal(&self) -> bool {
391        match &self.1 {
392            FontType::Type1(_) => true,
393            FontType::TrueType(_) => true,
394            FontType::Type0(t) => t.is_horizontal(),
395            FontType::Type3(_) => true,
396        }
397    }
398}
399
400impl CacheKey for Font<'_> {
401    fn cache_key(&self) -> u128 {
402        self.0
403    }
404}
405
406#[derive(Clone, Debug)]
407enum FontType<'a> {
408    Type1(Rc<Type1Font>),
409    TrueType(Rc<TrueTypeFont>),
410    Type0(Rc<Type0Font>),
411    Type3(Rc<Type3<'a>>),
412}
413
414#[derive(Debug)]
415enum Encoding {
416    Standard,
417    MacRoman,
418    WinAnsi,
419    MacExpert,
420    BuiltIn,
421}
422
423impl Encoding {
424    fn map_code(&self, code: u8) -> Option<&'static str> {
425        if code == 0 {
426            return Some(".notdef");
427        }
428        match self {
429            Self::Standard => standard::get(code),
430            Self::MacRoman => mac_roman::get(code).or_else(|| mac_os_roman::get(code)),
431            Self::WinAnsi => win_ansi::get(code),
432            Self::MacExpert => mac_expert::get(code),
433            Self::BuiltIn => None,
434        }
435    }
436}
437
438/// The font stretch.
439#[derive(Debug, Copy, Clone)]
440pub enum FontStretch {
441    /// Normal.
442    Normal,
443    /// Ultra condensed.
444    UltraCondensed,
445    /// Extra condensed.
446    ExtraCondensed,
447    /// Condensed.
448    Condensed,
449    /// Semi condensed.
450    SemiCondensed,
451    /// Semi expanded.
452    SemiExpanded,
453    /// Expanded.
454    Expanded,
455    /// Extra expanded.
456    ExtraExpanded,
457    /// Ultra expanded.
458    UltraExpanded,
459}
460
461impl FontStretch {
462    fn from_string(s: &str) -> Self {
463        match s {
464            "UltraCondensed" => Self::UltraCondensed,
465            "ExtraCondensed" => Self::ExtraCondensed,
466            "Condensed" => Self::Condensed,
467            "SemiCondensed" => Self::SemiCondensed,
468            "SemiExpanded" => Self::SemiExpanded,
469            "Expanded" => Self::Expanded,
470            "ExtraExpanded" => Self::ExtraExpanded,
471            "UltraExpanded" => Self::UltraExpanded,
472            _ => Self::Normal,
473        }
474    }
475}
476
477bitflags! {
478    /// Bitflags describing various characteristics of fonts.
479    #[derive(Debug)]
480    pub(crate) struct FontFlags: u32 {
481        const FIXED_PITCH = 1 << 0;
482        const SERIF = 1 << 1;
483        const SYMBOLIC = 1 << 2;
484        const SCRIPT = 1 << 3;
485        const NON_SYMBOLIC = 1 << 5;
486        const ITALIC = 1 << 6;
487        const ALL_CAP = 1 << 16;
488        const SMALL_CAP = 1 << 17;
489        const FORCE_BOLD = 1 << 18;
490    }
491}
492
493/// A query for a font.
494pub enum FontQuery {
495    /// A query for one of the 14 PDF standard fonts.
496    Standard(StandardFont),
497    /// A query for a font that is not embedded in the PDF file.
498    ///
499    /// Note that this type of query is currently not supported,
500    /// but will be implemented in the future.
501    Fallback(FallbackFontQuery),
502}
503
504/// A query for a font with specific properties.
505#[derive(Debug, Clone)]
506pub struct FallbackFontQuery {
507    /// The postscript name of the font.
508    pub post_script_name: Option<String>,
509    /// The name of the font.
510    pub font_name: Option<String>,
511    /// The family of the font.
512    pub font_family: Option<String>,
513    /// The stretch of the font.
514    pub font_stretch: FontStretch,
515    /// The weight of the font.
516    pub font_weight: u32,
517    /// Whether the font is monospaced.
518    pub is_fixed_pitch: bool,
519    /// Whether the font is serif.
520    pub is_serif: bool,
521    /// Whether the font is italic.
522    pub is_italic: bool,
523    /// Whether the font is bold.
524    pub is_bold: bool,
525    /// Whether the font is small cap.
526    pub is_small_cap: bool,
527    /// The character collection (registry/ordering) if this is a CID font.
528    pub character_collection: Option<CharacterCollection>,
529}
530
531impl FallbackFontQuery {
532    pub(crate) fn new(dict: &Dict<'_>) -> Self {
533        let post_script_name = dict
534            .get::<Name<'_>>(BASE_FONT)
535            .map(|n| strip_subset_prefix(n.as_str()).to_string());
536
537        let mut data = Self {
538            post_script_name,
539            ..Default::default()
540        };
541
542        if let Some(descriptor) = dict.get::<Dict<'_>>(FONT_DESC) {
543            data.font_name = dict
544                .get::<Name<'_>>(FONT_NAME)
545                .map(|n| strip_subset_prefix(n.as_str()).to_string());
546            data.font_family = descriptor
547                .get::<Name<'_>>(FONT_FAMILY)
548                .map(|n| n.as_str().to_string());
549            data.font_stretch = descriptor
550                .get::<Name<'_>>(FONT_STRETCH)
551                .map(|n| FontStretch::from_string(n.as_str()))
552                .unwrap_or(FontStretch::Normal);
553            data.font_weight = descriptor.get::<u32>(FONT_WEIGHT).unwrap_or(400);
554
555            if let Some(flags) = descriptor
556                .get::<u32>(FLAGS)
557                .map(FontFlags::from_bits_truncate)
558            {
559                data.is_serif = flags.contains(FontFlags::SERIF);
560                data.is_italic = flags.contains(FontFlags::ITALIC);
561                data.is_small_cap = flags.contains(FontFlags::SMALL_CAP);
562            }
563        }
564
565        data.is_italic |= data
566            .post_script_name
567            .as_ref()
568            .is_some_and(|s| s.contains("Italic"));
569
570        data.is_bold |= data
571            .post_script_name
572            .as_ref()
573            .is_some_and(|s| s.contains("Bold"));
574
575        data
576    }
577
578    /// Do a best-effort fallback to the 14 standard fonts based on the query.
579    pub fn pick_standard_font(&self) -> StandardFont {
580        if self.is_fixed_pitch {
581            match (self.is_bold, self.is_italic) {
582                (true, true) => StandardFont::CourierBoldOblique,
583                (true, false) => StandardFont::CourierBold,
584                (false, true) => StandardFont::CourierOblique,
585                (false, false) => StandardFont::Courier,
586            }
587        } else if !self.is_serif {
588            match (self.is_bold, self.is_italic) {
589                (true, true) => StandardFont::HelveticaBoldOblique,
590                (true, false) => StandardFont::HelveticaBold,
591                (false, true) => StandardFont::HelveticaOblique,
592                (false, false) => StandardFont::Helvetica,
593            }
594        } else {
595            match (self.is_bold, self.is_italic) {
596                (true, true) => StandardFont::TimesBoldItalic,
597                (true, false) => StandardFont::TimesBold,
598                (false, true) => StandardFont::TimesItalic,
599                (false, false) => StandardFont::TimesRoman,
600            }
601        }
602    }
603}
604
605impl Default for FallbackFontQuery {
606    fn default() -> Self {
607        Self {
608            post_script_name: None,
609            font_name: None,
610            font_family: None,
611            font_stretch: FontStretch::Normal,
612            font_weight: 400,
613            is_fixed_pitch: false,
614            is_serif: false,
615            is_italic: false,
616            is_bold: false,
617            is_small_cap: false,
618            character_collection: None,
619        }
620    }
621}
622
623/// Convert a glyph name to a Unicode character, if possible.
624/// An incomplete implementation of the Adobe Glyph List Specification
625/// <https://github.com/adobe-type-tools/agl-specification>
626pub(crate) fn glyph_name_to_unicode(name: &str) -> Option<char> {
627    if let Some(unicode_str) = glyph_names::get(name) {
628        return unicode_str.chars().next();
629    }
630
631    unicode_from_name(name).or_else(|| {
632        warn!("failed to map glyph name {} to unicode", name);
633
634        None
635    })
636}
637
638pub(crate) fn unicode_from_name(name: &str) -> Option<char> {
639    let convert = |input: &str| u32::from_str_radix(input, 16).ok().and_then(char::from_u32);
640
641    name.starts_with("uni")
642        .then(|| name.get(3..).and_then(convert))
643        .or_else(|| {
644            name.starts_with("u")
645                .then(|| name.get(1..).and_then(convert))
646        })
647        .flatten()
648}
649
650pub(crate) fn read_to_unicode(dict: &Dict<'_>, cmap_resolver: &CMapResolverFn) -> Option<CMap> {
651    dict.get::<Stream<'_>>(TO_UNICODE)
652        .and_then(|s| s.decoded().ok())
653        // See PDFJS-11915, where `Identity-H` is used for `ToUnicode`. I don't
654        // believe it's valid, but at least mupdf seems to be able to deal with it.
655        .or_else(|| {
656            dict.get::<Name<'_>>(TO_UNICODE)
657                .and_then(|name| (cmap_resolver)(CMapName::from_bytes(name.as_ref())))
658                .map(|d| Cow::Owned(d.to_vec()))
659        })
660        .and_then(|data| {
661            let cmap_resolver = cmap_resolver.clone();
662            CMap::parse(&data, move |name| (cmap_resolver)(name))
663        })
664}
665
666// When mapping to glyphs, some fonts might only have a glyph for the "normalized"
667// name.
668pub(crate) fn normalized_glyph_name(mut name: &str) -> &str {
669    if name == "nbspace" {
670        name = "space";
671    }
672
673    if name == "sfthyphen" {
674        name = "hyphen";
675    }
676
677    name
678}