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