Skip to main content

pdf_interpret/font/
mod.rs

1//! Interacting with the different kinds of PDF fonts.
2
3use crate::cache::Cache;
4use crate::context::Context;
5use crate::device::Device;
6use crate::font::cid::Type0Font;
7use crate::font::generated::{
8    glyph_names, mac_expert, mac_os_roman, mac_roman, standard, tex_glyph_names, 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::util::decode_or_warn;
15use crate::{CMapResolverFn, CacheKey, FontResolverFn, InterpreterSettings, Paint, WarningSinkFn};
16use bitflags::bitflags;
17use kurbo::{Affine, BezPath, Vec2};
18use log::warn;
19use outline::OutlineFont;
20use pdf_syntax::object::Name;
21use pdf_syntax::object::dict::keys::SUBTYPE;
22use pdf_syntax::object::dict::keys::*;
23use pdf_syntax::object::{Dict, Stream};
24use pdf_syntax::page::Resources;
25use pdf_syntax::xref::XRef;
26use skrifa::GlyphId;
27use std::fmt::Debug;
28use std::ops::Deref;
29use std::rc::Rc;
30use std::sync::Arc;
31
32mod blob;
33mod cid;
34mod generated;
35mod glyph_simulator;
36pub(crate) mod outline;
37mod standard_font;
38mod true_type;
39mod type1;
40pub(crate) mod type3;
41
42pub(crate) const UNITS_PER_EM: f32 = 1000.0;
43
44pub(crate) fn stretch_glyph(path: BezPath, expected_width: f32, actual_width: f32) -> BezPath {
45    if actual_width != 0.0 && actual_width != expected_width {
46        let stretch_factor = expected_width / actual_width;
47        Affine::scale_non_uniform(stretch_factor as f64, 1.0) * path
48    } else {
49        path
50    }
51}
52
53/// A container for the bytes of a PDF file.
54pub type FontData = Arc<dyn AsRef<[u8]> + Send + Sync>;
55
56/// Strip the 6-character subset prefix from a PostScript font name.
57///
58/// PDF subset fonts use names like "ABCDEF+TimesNewRoman". This function
59/// returns `TimesNewRoman` from such a name, or the original name if no
60/// valid prefix is found.
61pub(crate) fn strip_subset_prefix(name: &str) -> &str {
62    match name.split_once('+') {
63        Some((prefix, rest)) if prefix.len() == 6 => rest,
64        _ => name,
65    }
66}
67
68use crate::util::hash128;
69pub use outline::OutlineFontData;
70use pdf_font::cmap::{BfString, CMap, CMapName, CharacterCollection};
71pub use standard_font::StandardFont;
72
73/// A glyph that can be drawn.
74pub enum Glyph<'a> {
75    /// A glyph defined by an outline.
76    Outline(OutlineGlyph),
77    /// A type3 glyph, defined by PDF drawing instructions.
78    Type3(Box<Type3Glyph<'a>>),
79}
80
81impl Glyph<'_> {
82    /// Returns the Unicode code point for this glyph, if available.
83    ///
84    /// This method attempts to determine the Unicode character that this glyph
85    /// represents. The exact fallback chain depends on the font type:
86    ///
87    /// **For Outline Fonts (Type1, TrueType, CFF):**
88    /// 1. `ToUnicode` cmap
89    /// 2. Glyph name → Unicode (via Adobe Glyph List)
90    /// 3. Unicode naming conventions (e.g., "uni0041", "u0041")
91    ///
92    /// **For CID Fonts (Type0):**
93    /// 1. `ToUnicode` cmap
94    ///
95    ///
96    /// **For Type3 Fonts:**
97    /// 1. `ToUnicode` cmap
98    ///
99    /// Returns `None` if the Unicode value could not be determined.
100    ///
101    /// Please note that this method is still somewhat experimental and might
102    /// not work reliably in all cases.
103    pub fn as_unicode(&self) -> Option<BfString> {
104        match self {
105            Glyph::Outline(g) => g.as_unicode(),
106            Glyph::Type3(g) => g.as_unicode(),
107        }
108    }
109}
110
111/// An identifier that uniquely identifies a glyph, for caching purposes.
112#[derive(Clone, Debug)]
113pub struct GlyphIdentifier {
114    id: GlyphId,
115    font: OutlineFont,
116}
117
118impl CacheKey for GlyphIdentifier {
119    fn cache_key(&self) -> u128 {
120        hash128(&(self.id, self.font.cache_key()))
121    }
122}
123
124/// A glyph defined by an outline.
125#[derive(Clone, Debug)]
126pub struct OutlineGlyph {
127    pub(crate) id: GlyphId,
128    pub(crate) font: OutlineFont,
129    pub(crate) char_code: u32,
130}
131
132impl OutlineGlyph {
133    /// Return the outline of the glyph, assuming an upem value of 1000.
134    pub fn outline(&self) -> BezPath {
135        self.font.outline_glyph(self.id, self.char_code)
136    }
137
138    /// Return the identifier of the glyph. You can use this to calculate the cache key
139    /// for the glyph.
140    ///
141    /// Note that the `glyph_transform` attribute is not considered in the cache key of
142    /// the identifier, only the glyph ID and the font.
143    pub fn identifier(&self) -> GlyphIdentifier {
144        GlyphIdentifier {
145            id: self.id,
146            font: self.font.clone(),
147        }
148    }
149
150    /// Returns the Unicode code point for this glyph, if available.
151    ///
152    /// See [`Glyph::as_unicode`] for details on the fallback chain used.
153    ///
154    /// The chain refuses a step that answers with a code point carrying no
155    /// information -- see [`is_opaque`] -- and so can run out, in which case
156    /// the glyph has no character and none is reported. That is deliberate:
157    /// the alternative, which this returned until #550, was the character
158    /// code itself, and a control character in extracted text reads as text
159    /// to an indexer while being none.
160    pub fn as_unicode(&self) -> Option<BfString> {
161        self.font
162            .char_code_to_unicode(self.char_code)
163            .and_then(if_meaningful)
164    }
165
166    /// Get raw font bytes and metadata for downstream use.
167    ///
168    /// Returns `None` for Type1 fonts.
169    pub fn font_data(&self) -> Option<OutlineFontData> {
170        self.font.font_data()
171    }
172
173    /// PostScript name of the underlying font, if determinable.
174    ///
175    /// Works even when [`font_data`](Self::font_data) returns
176    /// `None` — e.g. non-embedded standard-14 Type1 fonts still
177    /// expose their canonical PostScript name through this
178    /// accessor. Returns `None` when no name source is available.
179    ///
180    /// Surfaced to support the WASM `getTextPositions()` `fontName`
181    /// field for the editor contract.
182    pub fn postscript_name(&self) -> Option<String> {
183        self.font.postscript_name()
184    }
185
186    /// Get the glyph ID within the font.
187    pub fn glyph_id(&self) -> GlyphId {
188        self.id
189    }
190
191    /// Get the advance width for this glyph.
192    ///
193    /// The advance width is how far to move horizontally after drawing
194    /// this glyph before drawing the next one.
195    pub fn advance_width(&self) -> Option<f32> {
196        self.font.glyph_advance_width(self.char_code)
197    }
198
199    /// Vertical font metrics in /1000 em.
200    ///
201    /// Returns `(ascent, descent, cap_height, x_height)` from the font's
202    /// embedded OS/2 tables (via skrifa) when available, or from the
203    /// Standard-14 AFM fallback table for non-embedded standard fonts.
204    /// Returns `None` only when no metrics source is reachable (e.g.
205    /// CFF fonts without a standard-font fallback, Type3).
206    pub fn font_metrics(&self) -> Option<(f64, f64, Option<f64>, Option<f64>)> {
207        self.font.font_metrics()
208    }
209
210    /// Get the cache key for this glyph's font.
211    ///
212    /// This identifies the font uniquely, even when `font_data()` returns `None`
213    /// (e.g., for Type1 fonts). Useful for grouping glyphs by font.
214    pub fn font_cache_key(&self) -> u128 {
215        self.font.cache_key()
216    }
217}
218
219/// A type3 glyph.
220#[derive(Clone)]
221pub struct Type3Glyph<'a> {
222    pub(crate) font: Rc<Type3<'a>>,
223    pub(crate) glyph_id: GlyphId,
224    pub(crate) state: State<'a>,
225    pub(crate) parent_resources: Resources<'a>,
226    pub(crate) cache: Cache,
227    pub(crate) xref: &'a XRef,
228    pub(crate) settings: InterpreterSettings,
229    pub(crate) char_code: u32,
230    /// The depth the interpretation sat at when this glyph was reached (#318).
231    /// A Type 3 glyph is a content stream, so it can name a pattern whose cell
232    /// draws text in this same font — a fresh Context starting at zero would
233    /// never reach the bound.
234    pub(crate) nesting_depth: u32,
235}
236
237/// A glyph defined by PDF drawing instructions.
238impl<'a> Type3Glyph<'a> {
239    /// Draw the type3 glyph to the given device.
240    pub fn interpret(
241        &self,
242        device: &mut impl Device<'a>,
243        transform: Affine,
244        glyph_transform: Affine,
245        paint: &Paint<'a>,
246    ) {
247        self.font
248            .render_glyph(self, transform, glyph_transform, paint, device);
249    }
250
251    /// Returns the Unicode code point for this glyph, if available.
252    ///
253    /// Note: Type3 fonts can only provide Unicode via `ToUnicode` cmap.
254    ///
255    /// As for an outline glyph, a code point that says nothing about the text
256    /// is not reported at all (#550).
257    pub fn as_unicode(&self) -> Option<BfString> {
258        self.font
259            .char_code_to_unicode(self.char_code)
260            .and_then(if_meaningful)
261    }
262}
263
264impl CacheKey for Type3Glyph<'_> {
265    fn cache_key(&self) -> u128 {
266        hash128(&(self.font.cache_key(), self.glyph_id))
267    }
268}
269
270#[derive(Clone, Debug)]
271pub(crate) struct Font<'a>(u128, FontType<'a>);
272
273impl<'a> Font<'a> {
274    pub(crate) fn new(
275        dict: &Dict<'a>,
276        font_resolver: &FontResolverFn,
277        cmap_resolver: &CMapResolverFn,
278        warning_sink: &WarningSinkFn,
279    ) -> Option<Self> {
280        let f_type = match dict.get::<Name>(SUBTYPE)?.deref() {
281            TYPE1 | MM_TYPE1 => FontType::Type1(Rc::new(Type1Font::new(
282                dict,
283                font_resolver,
284                cmap_resolver,
285                warning_sink,
286            )?)),
287            // PDFBOX-5463: PDF viewers seem to accept OpenType as well.
288            TRUE_TYPE | OPEN_TYPE => FontType::TrueType(Rc::new(TrueTypeFont::new(
289                dict,
290                font_resolver,
291                cmap_resolver,
292                warning_sink,
293            )?)),
294            TYPE0 => FontType::Type0(Rc::new(Type0Font::new(
295                dict,
296                font_resolver,
297                cmap_resolver,
298                warning_sink,
299            )?)),
300            TYPE3 => FontType::Type3(Rc::new(Type3::new(dict, cmap_resolver, warning_sink)?)),
301            f => {
302                warn!(
303                    "unimplemented font type {:?}",
304                    std::str::from_utf8(f).unwrap_or("unknown type")
305                );
306
307                return None;
308            }
309        };
310
311        let cache_key = dict.cache_key();
312
313        Some(Self(cache_key, f_type))
314    }
315
316    pub(crate) fn new_standard(
317        standard_font: StandardFont,
318        font_resolver: &FontResolverFn,
319    ) -> Option<Self> {
320        let font = Type1Font::new_standard(standard_font, font_resolver)?;
321
322        Some(Self(0, FontType::Type1(Rc::new(font))))
323    }
324
325    pub(crate) fn map_code(&self, code: u32) -> GlyphId {
326        match &self.1 {
327            FontType::Type1(f) => {
328                debug_assert!(code <= u8::MAX as u32);
329
330                f.map_code(code as u8)
331            }
332            FontType::TrueType(t) => {
333                debug_assert!(code <= u8::MAX as u32);
334
335                t.map_code(code as u8)
336            }
337            FontType::Type0(t) => t.map_code(code),
338            FontType::Type3(t) => {
339                debug_assert!(code <= u8::MAX as u32);
340
341                t.map_code(code as u8)
342            }
343        }
344    }
345
346    pub(crate) fn get_glyph(
347        &self,
348        glyph: GlyphId,
349        char_code: u32,
350        ctx: &mut Context<'a>,
351        resources: &Resources<'a>,
352        origin_displacement: Vec2,
353    ) -> (Glyph<'a>, Affine) {
354        let glyph_transform = ctx.get().text_state.full_transform()
355            * Affine::scale(1.0 / UNITS_PER_EM as f64)
356            * Affine::translate(origin_displacement);
357
358        let glyph = match &self.1 {
359            FontType::Type1(t) => {
360                let font = OutlineFont::Type1(t.clone());
361                Glyph::Outline(OutlineGlyph {
362                    id: glyph,
363                    font,
364                    char_code,
365                })
366            }
367            FontType::TrueType(t) => {
368                let font = OutlineFont::TrueType(t.clone());
369                Glyph::Outline(OutlineGlyph {
370                    id: glyph,
371                    font,
372                    char_code,
373                })
374            }
375            FontType::Type0(t) => {
376                let font = OutlineFont::Type0(t.clone());
377                Glyph::Outline(OutlineGlyph {
378                    id: glyph,
379                    font,
380                    char_code,
381                })
382            }
383            FontType::Type3(t) => {
384                let shape_glyph = Type3Glyph {
385                    nesting_depth: ctx.nesting_depth(),
386                    font: t.clone(),
387                    glyph_id: glyph,
388                    state: ctx.get().clone(),
389                    parent_resources: resources.clone(),
390                    cache: ctx.object_cache.clone(),
391                    xref: ctx.xref,
392                    settings: ctx.settings.clone(),
393                    char_code,
394                };
395
396                Glyph::Type3(Box::new(shape_glyph))
397            }
398        };
399
400        (glyph, glyph_transform)
401    }
402
403    pub(crate) fn code_advance(&self, code: u32) -> Vec2 {
404        match &self.1 {
405            FontType::Type1(t) => {
406                debug_assert!(code <= u8::MAX as u32);
407
408                Vec2::new(t.glyph_width(code as u8).unwrap_or(0.0) as f64, 0.0)
409            }
410            FontType::TrueType(t) => {
411                debug_assert!(code <= u8::MAX as u32);
412
413                Vec2::new(t.glyph_width(code as u8) as f64, 0.0)
414            }
415            FontType::Type0(t) => t.code_advance(code),
416            FontType::Type3(t) => {
417                debug_assert!(code <= u8::MAX as u32);
418
419                Vec2::new(t.glyph_width(code as u8) as f64, 0.0)
420            }
421        }
422    }
423
424    pub(crate) fn origin_displacement(&self, code: u32) -> Vec2 {
425        match &self.1 {
426            FontType::Type1(_) => Vec2::default(),
427            FontType::TrueType(_) => Vec2::default(),
428            FontType::Type0(t) => t.origin_displacement(code),
429            FontType::Type3(_) => Vec2::default(),
430        }
431    }
432
433    pub(crate) fn read_code(&self, bytes: &[u8], offset: usize) -> (u32, usize) {
434        match &self.1 {
435            FontType::Type1(_) => (bytes[offset] as u32, 1),
436            FontType::TrueType(_) => (bytes[offset] as u32, 1),
437            FontType::Type0(t) => t.read_code(bytes, offset),
438            FontType::Type3(_) => (bytes[offset] as u32, 1),
439        }
440    }
441
442    pub(crate) fn is_horizontal(&self) -> bool {
443        match &self.1 {
444            FontType::Type1(_) => true,
445            FontType::TrueType(_) => true,
446            FontType::Type0(t) => t.is_horizontal(),
447            FontType::Type3(_) => true,
448        }
449    }
450}
451
452impl CacheKey for Font<'_> {
453    fn cache_key(&self) -> u128 {
454        self.0
455    }
456}
457
458#[derive(Clone, Debug)]
459enum FontType<'a> {
460    Type1(Rc<Type1Font>),
461    TrueType(Rc<TrueTypeFont>),
462    Type0(Rc<Type0Font>),
463    Type3(Rc<Type3<'a>>),
464}
465
466#[derive(Debug)]
467enum Encoding {
468    Standard,
469    MacRoman,
470    WinAnsi,
471    MacExpert,
472    BuiltIn,
473}
474
475impl Encoding {
476    fn map_code(&self, code: u8) -> Option<&'static str> {
477        if code == 0 {
478            return Some(".notdef");
479        }
480        match self {
481            Self::Standard => standard::get(code),
482            Self::MacRoman => mac_roman::get(code).or_else(|| mac_os_roman::get(code)),
483            Self::WinAnsi => win_ansi::get(code),
484            Self::MacExpert => mac_expert::get(code),
485            Self::BuiltIn => None,
486        }
487    }
488}
489
490/// The font stretch.
491#[derive(Debug, Copy, Clone)]
492pub enum FontStretch {
493    /// Normal.
494    Normal,
495    /// Ultra condensed.
496    UltraCondensed,
497    /// Extra condensed.
498    ExtraCondensed,
499    /// Condensed.
500    Condensed,
501    /// Semi condensed.
502    SemiCondensed,
503    /// Semi expanded.
504    SemiExpanded,
505    /// Expanded.
506    Expanded,
507    /// Extra expanded.
508    ExtraExpanded,
509    /// Ultra expanded.
510    UltraExpanded,
511}
512
513impl FontStretch {
514    fn from_string(s: &str) -> Self {
515        match s {
516            "UltraCondensed" => Self::UltraCondensed,
517            "ExtraCondensed" => Self::ExtraCondensed,
518            "Condensed" => Self::Condensed,
519            "SemiCondensed" => Self::SemiCondensed,
520            "SemiExpanded" => Self::SemiExpanded,
521            "Expanded" => Self::Expanded,
522            "ExtraExpanded" => Self::ExtraExpanded,
523            "UltraExpanded" => Self::UltraExpanded,
524            _ => Self::Normal,
525        }
526    }
527}
528
529bitflags! {
530    /// Bitflags describing various characteristics of fonts.
531    #[derive(Debug)]
532    pub(crate) struct FontFlags: u32 {
533        const FIXED_PITCH = 1 << 0;
534        const SERIF = 1 << 1;
535        const SYMBOLIC = 1 << 2;
536        const SCRIPT = 1 << 3;
537        const NON_SYMBOLIC = 1 << 5;
538        const ITALIC = 1 << 6;
539        const ALL_CAP = 1 << 16;
540        const SMALL_CAP = 1 << 17;
541        const FORCE_BOLD = 1 << 18;
542    }
543}
544
545/// A query for a font.
546pub enum FontQuery {
547    /// A query for one of the 14 PDF standard fonts.
548    Standard(StandardFont),
549    /// A query for a font that is not embedded in the PDF file.
550    ///
551    /// Note that this type of query is currently not supported,
552    /// but will be implemented in the future.
553    Fallback(FallbackFontQuery),
554}
555
556/// A query for a font with specific properties.
557#[derive(Debug, Clone)]
558pub struct FallbackFontQuery {
559    /// The postscript name of the font.
560    pub post_script_name: Option<String>,
561    /// The name of the font.
562    pub font_name: Option<String>,
563    /// The family of the font.
564    pub font_family: Option<String>,
565    /// The stretch of the font.
566    pub font_stretch: FontStretch,
567    /// The weight of the font.
568    pub font_weight: u32,
569    /// Whether the font is monospaced.
570    pub is_fixed_pitch: bool,
571    /// Whether the font is serif.
572    pub is_serif: bool,
573    /// Whether the font is italic.
574    pub is_italic: bool,
575    /// Whether the font is bold.
576    pub is_bold: bool,
577    /// Whether the font is small cap.
578    pub is_small_cap: bool,
579    /// The character collection (registry/ordering) if this is a CID font.
580    pub character_collection: Option<CharacterCollection>,
581}
582
583impl FallbackFontQuery {
584    pub(crate) fn new(dict: &Dict<'_>) -> Self {
585        let post_script_name = dict
586            .get::<Name>(BASE_FONT)
587            .map(|n| strip_subset_prefix(n.as_str()).to_string());
588
589        let mut data = Self {
590            post_script_name,
591            ..Default::default()
592        };
593
594        if let Some(descriptor) = dict.get::<Dict<'_>>(FONT_DESC) {
595            data.font_name = dict
596                .get::<Name>(FONT_NAME)
597                .map(|n| strip_subset_prefix(n.as_str()).to_string());
598            data.font_family = descriptor
599                .get::<Name>(FONT_FAMILY)
600                .map(|n| n.as_str().to_string());
601            data.font_stretch = descriptor
602                .get::<Name>(FONT_STRETCH)
603                .map(|n| FontStretch::from_string(n.as_str()))
604                .unwrap_or(FontStretch::Normal);
605            data.font_weight = descriptor.get::<u32>(FONT_WEIGHT).unwrap_or(400);
606
607            if let Some(flags) = descriptor
608                .get::<u32>(FLAGS)
609                .map(FontFlags::from_bits_truncate)
610            {
611                data.is_fixed_pitch = flags.contains(FontFlags::FIXED_PITCH);
612                data.is_serif = flags.contains(FontFlags::SERIF);
613                data.is_italic = flags.contains(FontFlags::ITALIC);
614                data.is_small_cap = flags.contains(FontFlags::SMALL_CAP);
615            }
616        }
617
618        data.is_bold |= data.font_weight >= 700;
619
620        if let Some(name) = &data.post_script_name {
621            let lower = name.to_ascii_lowercase();
622            data.is_italic |=
623                lower.contains("italic") || lower.contains("oblique") || lower.contains("slant");
624            data.is_bold |= lower.contains("bold")
625                || lower.contains("demi")
626                || lower.contains("semibold")
627                || lower.contains("heavy")
628                || lower.contains("black");
629        }
630
631        data
632    }
633
634    /// Do a best-effort fallback to the 14 standard fonts based on the query.
635    pub fn pick_standard_font(&self) -> StandardFont {
636        if self.is_fixed_pitch {
637            match (self.is_bold, self.is_italic) {
638                (true, true) => StandardFont::CourierBoldOblique,
639                (true, false) => StandardFont::CourierBold,
640                (false, true) => StandardFont::CourierOblique,
641                (false, false) => StandardFont::Courier,
642            }
643        } else if !self.is_serif {
644            match (self.is_bold, self.is_italic) {
645                (true, true) => StandardFont::HelveticaBoldOblique,
646                (true, false) => StandardFont::HelveticaBold,
647                (false, true) => StandardFont::HelveticaOblique,
648                (false, false) => StandardFont::Helvetica,
649            }
650        } else {
651            match (self.is_bold, self.is_italic) {
652                (true, true) => StandardFont::TimesBoldItalic,
653                (true, false) => StandardFont::TimesBold,
654                (false, true) => StandardFont::TimesItalic,
655                (false, false) => StandardFont::TimesRoman,
656            }
657        }
658    }
659}
660
661impl Default for FallbackFontQuery {
662    fn default() -> Self {
663        Self {
664            post_script_name: None,
665            font_name: None,
666            font_family: None,
667            font_stretch: FontStretch::Normal,
668            font_weight: 400,
669            is_fixed_pitch: false,
670            is_serif: false,
671            is_italic: false,
672            is_bold: false,
673            is_small_cap: false,
674            character_collection: None,
675        }
676    }
677}
678
679/// A code point that carries no information about the text (#550).
680///
681/// Two documents used to reach the extracted text as their own character
682/// codes. `pdfjs bug1068432.pdf` ships a `/ToUnicode` that maps every code to
683/// itself, so `\u{3}\u{14}\u{15}` came out where the document draws
684/// `square`, `dblarrowup` and `dblarrowdwn`; `6-2-11-7-3-t01-pass-e.pdf` maps
685/// its one code into the private-use area and U+E020 came out.
686///
687/// Both are worse than nothing. A control character reads as text to an
688/// indexer and is not one, and a private-use code point means something only
689/// inside the font that defined it -- neither can be searched, compared or
690/// displayed. A step of the fallback chain that produces one of these has not
691/// answered the question, so the chain carries on to the next step, and a
692/// chain that runs out emits nothing for the glyph.
693///
694/// Tab, line feed, form feed and carriage return are text: they say where the
695/// text breaks. They are the only characters below U+0020 that do.
696pub(crate) fn is_opaque(c: char) -> bool {
697    let v = c as u32;
698    (v < 0x20 && !matches!(v, 0x09 | 0x0A | 0x0C | 0x0D))
699        || (0x7F..=0x9F).contains(&v)
700        || (0xE000..=0xF8FF).contains(&v)
701        || (0xFDD0..=0xFDEF).contains(&v)
702        || (v & 0xFFFE) == 0xFFFE
703        || (0xF0000..=0xFFFFD).contains(&v)
704        || (0x10_0000..=0x10FFFD).contains(&v)
705}
706
707/// Whether a reading says anything about the text: it is not empty and no
708/// code point in it is [opaque](is_opaque).
709///
710/// A reading is judged whole. A `bfrange` that produced two letters and a
711/// private-use code point is one answer about one glyph, and half of it is not
712/// a better answer than none: it would put a code in the text that the reader
713/// cannot tell from the letters beside it.
714pub(crate) fn says_something(text: &str) -> bool {
715    !text.is_empty() && !text.chars().any(is_opaque)
716}
717
718/// Keep a mapping only when it [says something](says_something); `None` sends
719/// the caller on to the next step of the fallback chain.
720pub(crate) fn if_meaningful(s: BfString) -> Option<BfString> {
721    let keep = match &s {
722        BfString::Char(c) => !is_opaque(*c),
723        BfString::String(s) => says_something(s),
724    };
725    keep.then_some(s)
726}
727
728/// As [`if_meaningful`], for the steps of the chain that produce a string.
729///
730/// Every step is filtered where it is produced rather than where the chain
731/// ends, because a step that answers with an opaque code point must let the
732/// next one try: an `/Encoding` naming `uniE020` used to satisfy the `or_else`
733/// that would have reached the font program, and only then be discarded, so
734/// the glyph came out as nothing where the font's own `cmap` knew the letter.
735pub(crate) fn text_if_meaningful(text: String) -> Option<String> {
736    says_something(&text).then_some(text)
737}
738
739/// A [`BfString`] holding `text`: `Char` when it is one code point and
740/// `String` otherwise. The two carry the same text; which one is used is a
741/// question of allocation and never of meaning.
742pub(crate) fn bf_string_of(text: &str) -> BfString {
743    let mut chars = text.chars();
744    match (chars.next(), chars.next()) {
745        (Some(c), None) => BfString::Char(c),
746        _ => BfString::String(text.to_string()),
747    }
748}
749
750/// As [`if_meaningful`], for the steps of the chain that produce a single
751/// character.
752pub(crate) fn char_if_meaningful(c: char) -> Option<char> {
753    (!is_opaque(c)).then_some(c)
754}
755
756/// Convert a glyph name to a Unicode character, if possible.
757/// Implements the Adobe Glyph List Specification with extended fallbacks
758/// for non-standard names found in real-world PDFs.
759/// <https://github.com/adobe-type-tools/agl-specification>
760pub(crate) fn glyph_name_to_unicode(name: &str) -> Option<char> {
761    // 1. Direct AGL lookup
762    if let Some(unicode_str) = glyph_names::get(name) {
763        return unicode_str.chars().next();
764    }
765
766    // 2. The TeX and AMS names the AGL does not carry (#550). Every TeX
767    //    distribution writes them into `/Encoding /Differences`, and without
768    //    them a glyph out of MSAM10 or CMSY10 has no character at all.
769    if let Some(unicode_str) = tex_glyph_names::get(name) {
770        return unicode_str.chars().next();
771    }
772
773    // 3. uni/u prefix convention
774    if let Some(c) = unicode_from_name(name) {
775        return Some(c);
776    }
777
778    // 4. AGL §2.4: strip variant suffix after period (e.g., "A.swash" → "A",
779    //    "comma.alt" → "comma"). Only the base name before the first period
780    //    maps to Unicode.
781    if let Some(dot_pos) = name.find('.') {
782        let base = &name[..dot_pos];
783        if !base.is_empty() {
784            if let Some(c) = glyph_names::get(base).and_then(|s| s.chars().next()) {
785                return Some(c);
786            }
787            if let Some(c) = tex_glyph_names::get(base).and_then(|s| s.chars().next()) {
788                return Some(c);
789            }
790            if let Some(c) = unicode_from_name(base) {
791                return Some(c);
792            }
793        }
794    }
795
796    // 5. Handle "aXX" decimal glyph names (e.g., "a65" → 'A') used by some
797    //    TeX/LaTeX generated PDFs and custom encoding vectors.
798    if name.starts_with('a')
799        && name.len() >= 2
800        && let Ok(code) = name[1..].parse::<u32>()
801        && let Some(c) = char::from_u32(code)
802        && (!c.is_control() || c == ' ')
803    {
804        return Some(c);
805    }
806
807    warn!("failed to map glyph name {} to unicode", name);
808    None
809}
810
811/// The complete reading the Adobe and TeX lists give a name, when either names
812/// it -- including the names that stand for more than one code point.
813///
814/// The Adobe list has 81 such entries and the TeX list 22. Everything that
815/// reduces a lookup to a `char` loses all but the first of them, which is why
816/// this is separate from [`glyph_name_to_unicode`]: a `char` cannot hold the
817/// answer, so the function that returns one must not be the only way to ask.
818///
819/// The variant suffix of AGL §2.4 is stripped on a second attempt, so
820/// `FFIsmall.alt` reads like `FFIsmall`.
821pub(crate) fn full_from_the_lists(name: &str) -> Option<String> {
822    let look = |n: &str| {
823        glyph_names::get(n)
824            .or_else(|| tex_glyph_names::get(n))
825            .map(str::to_string)
826    };
827
828    look(name).or_else(|| {
829        let (base, _) = name.split_once('.')?;
830        (!base.is_empty()).then(|| look(base))?
831    })
832}
833
834/// AGL §2.2: resolve a glyph name to one or more Unicode code points. Handles
835/// everything `glyph_name_to_unicode` does, plus underscore-joined ligature
836/// names (e.g. `f_i` → "fi", `f_f_i` → "ffi") which glyph-list-based fonts
837/// use for Latin ligatures. Returns the full string, so ligatures surface as
838/// their component code points rather than being dropped when the composite
839/// name isn't in the AGL.
840///
841/// Strips the variant suffix (`.ss01`, `.swash`, …) from the base name before
842/// splitting, matching AGL §2.4. Bails out if any component fails to resolve
843/// so we never produce a partial string (which would be worse than None).
844pub(crate) fn glyph_name_to_string(name: &str) -> Option<String> {
845    // The lists first, and whole. Both of them name glyphs that stand for more
846    // than one code point -- `FFIsmall` is `ffi`, `Germandbls` is `SS`, a
847    // Hebrew letter with its point is two -- and the single-character path
848    // below would keep the first and drop the rest without saying so (#550).
849    if let Some(s) = full_from_the_lists(name) {
850        return Some(s);
851    }
852
853    // Then the single-code-point path, which also handles `.suffix` stripping,
854    // `uniXXXX`, and the `aNN` decimal convention.
855    if let Some(c) = glyph_name_to_unicode(name) {
856        return Some(c.to_string());
857    }
858
859    let base = name.split_once('.').map(|(b, _)| b).unwrap_or(name);
860    if !base.contains('_') {
861        return None;
862    }
863
864    let mut out = String::new();
865    for part in base.split('_') {
866        if part.is_empty() {
867            return None;
868        }
869        let c = glyph_name_to_unicode(part)?;
870        out.push(c);
871    }
872    (!out.is_empty()).then_some(out)
873}
874
875pub(crate) fn unicode_from_name(name: &str) -> Option<char> {
876    let convert = |input: &str| u32::from_str_radix(input, 16).ok().and_then(char::from_u32);
877
878    name.starts_with("uni")
879        .then(|| name.get(3..).and_then(convert))
880        .or_else(|| {
881            name.starts_with("u")
882                .then(|| name.get(1..).and_then(convert))
883        })
884        .flatten()
885}
886
887pub(crate) fn read_to_unicode(
888    dict: &Dict<'_>,
889    cmap_resolver: &CMapResolverFn,
890    warning_sink: &WarningSinkFn,
891) -> Option<CMap> {
892    dict.get::<Stream<'_>>(TO_UNICODE)
893        .and_then(|s| decode_or_warn(&s, warning_sink))
894        // See PDFJS-11915, where `Identity-H` is used for `ToUnicode`. I don't
895        // believe it's valid, but at least mupdf seems to be able to deal with it.
896        .or_else(|| {
897            dict.get::<Name>(TO_UNICODE)
898                .and_then(|name| (cmap_resolver)(CMapName::from_bytes(name.as_ref())))
899                .map(|d| d.to_vec())
900        })
901        .and_then(|data| {
902            let cmap_resolver = cmap_resolver.clone();
903            CMap::parse(&data, move |name| (cmap_resolver)(name))
904        })
905}
906
907/// Build a 256-entry Unicode map from the font's `/Encoding` entry.
908/// Used as a fallback when no `/ToUnicode` CMap is present.
909pub(crate) fn synthesize_unicode_map_from_encoding(dict: &Dict<'_>) -> Option<Vec<Option<String>>> {
910    let (base_encoding, differences) = true_type::read_encoding(dict);
911    // Only synthesize if there is something to map (encoding name or differences).
912    // For BuiltIn with no differences we cannot reliably guess the mapping.
913    if matches!(base_encoding, Encoding::BuiltIn) && differences.is_empty() {
914        return None;
915    }
916    // A string and not a `char`, because a glyph name can stand for more than
917    // one code point -- `FFIsmall` is `ffi` -- and a table of `char` answers
918    // such a name with its first letter and no warning (#550).
919    let mut table: Vec<Option<String>> = vec![None; 256];
920    for code in 0u8..=255 {
921        let glyph_name = differences
922            .get(&code)
923            .map(String::as_str)
924            .or_else(|| base_encoding.map_code(code));
925        if let Some(name) = glyph_name {
926            table[code as usize] = glyph_name_to_string(name);
927        }
928    }
929    Some(table)
930}
931
932// When mapping to glyphs, some fonts might only have a glyph for the "normalized"
933// name. These aliases come from the Adobe Glyph List (AGL) and are safe
934// synonyms: they are consulted only when the literal lookup already failed, so
935// adding new entries broadens recovery without overriding correct mappings.
936pub(crate) fn normalized_glyph_name(mut name: &str) -> &str {
937    if name == "nbspace" || name == "nonbreakingspace" {
938        name = "space";
939    }
940
941    if name == "sfthyphen" || name == "softhyphen" {
942        name = "hyphen";
943    }
944
945    name
946}
947
948#[cfg(test)]
949mod normalized_glyph_name_tests {
950    use super::normalized_glyph_name;
951
952    #[test]
953    fn maps_space_aliases() {
954        assert_eq!(normalized_glyph_name("nbspace"), "space");
955        assert_eq!(normalized_glyph_name("nonbreakingspace"), "space");
956    }
957
958    #[test]
959    fn maps_hyphen_aliases() {
960        assert_eq!(normalized_glyph_name("sfthyphen"), "hyphen");
961        assert_eq!(normalized_glyph_name("softhyphen"), "hyphen");
962    }
963
964    #[test]
965    fn preserves_unrelated_names() {
966        assert_eq!(normalized_glyph_name("A"), "A");
967        assert_eq!(normalized_glyph_name(".notdef"), ".notdef");
968        assert_eq!(normalized_glyph_name("hyphen"), "hyphen");
969        assert_eq!(normalized_glyph_name("space"), "space");
970    }
971}
972
973#[cfg(test)]
974mod glyph_name_to_unicode_tests {
975    use super::{glyph_name_to_string, glyph_name_to_unicode};
976
977    #[test]
978    fn standard_agl_name() {
979        assert_eq!(glyph_name_to_unicode("A"), Some('A'));
980        assert_eq!(glyph_name_to_unicode("space"), Some(' '));
981        assert_eq!(glyph_name_to_unicode("hyphen"), Some('-'));
982    }
983
984    #[test]
985    fn ligature_name_underscore_joined() {
986        // AGL maps `fi` directly (U+FB01); the underscore form is what
987        // CFF fonts without a ToUnicode map tend to emit for the same
988        // ligature. We want the decomposed string so readers see "fi".
989        assert_eq!(glyph_name_to_string("f_i"), Some("fi".to_string()));
990        assert_eq!(glyph_name_to_string("f_f_i"), Some("ffi".to_string()));
991        assert_eq!(glyph_name_to_string("A_B_C"), Some("ABC".to_string()));
992    }
993
994    #[test]
995    fn ligature_name_with_suffix() {
996        // Variant suffix must be stripped before splitting on `_`.
997        assert_eq!(glyph_name_to_string("f_i.alt"), Some("fi".to_string()));
998    }
999
1000    #[test]
1001    fn ligature_name_falls_back_to_single_char_path() {
1002        // Direct AGL hit should still win and yield the precomposed char.
1003        assert_eq!(glyph_name_to_string("fi"), Some("\u{FB01}".to_string()));
1004        assert_eq!(glyph_name_to_string("A"), Some("A".to_string()));
1005    }
1006
1007    #[test]
1008    fn ligature_name_rejects_unresolvable_component() {
1009        // If any segment is unknown we return None rather than a partial
1010        // string — a partial would be more confusing than a drop.
1011        assert!(glyph_name_to_string("A_totallyUnknownGlyph").is_none());
1012        assert!(glyph_name_to_string("_").is_none());
1013        assert!(glyph_name_to_string("A__B").is_none());
1014    }
1015
1016    #[test]
1017    fn uni_prefix() {
1018        assert_eq!(glyph_name_to_unicode("uni0041"), Some('A'));
1019        assert_eq!(glyph_name_to_unicode("uni00E9"), Some('é'));
1020    }
1021
1022    #[test]
1023    fn u_prefix() {
1024        assert_eq!(glyph_name_to_unicode("u0041"), Some('A'));
1025        assert_eq!(glyph_name_to_unicode("u2022"), Some('•'));
1026    }
1027
1028    #[test]
1029    fn variant_suffix_stripped() {
1030        assert_eq!(glyph_name_to_unicode("A.swash"), Some('A'));
1031        assert_eq!(glyph_name_to_unicode("comma.alt"), Some(','));
1032        assert_eq!(glyph_name_to_unicode("space.narrow"), Some(' '));
1033    }
1034
1035    #[test]
1036    fn variant_suffix_with_uni_prefix() {
1037        assert_eq!(glyph_name_to_unicode("uni0041.ss01"), Some('A'));
1038    }
1039
1040    #[test]
1041    fn a_decimal_glyph_name() {
1042        assert_eq!(glyph_name_to_unicode("a65"), Some('A'));
1043        assert_eq!(glyph_name_to_unicode("a32"), Some(' '));
1044        assert_eq!(glyph_name_to_unicode("a97"), Some('a'));
1045    }
1046
1047    #[test]
1048    fn a_decimal_rejects_control_chars() {
1049        assert_eq!(glyph_name_to_unicode("a0"), None);
1050        assert_eq!(glyph_name_to_unicode("a7"), None);
1051    }
1052
1053    #[test]
1054    fn unknown_name_returns_none() {
1055        assert_eq!(glyph_name_to_unicode("xyzzynonexistent"), None);
1056    }
1057}
1058
1059#[cfg(test)]
1060mod fallback_font_query_tests {
1061    use super::*;
1062
1063    fn query_with(name: &str, flags: u32, weight: u32) -> FallbackFontQuery {
1064        let mut q = FallbackFontQuery {
1065            post_script_name: Some(name.to_string()),
1066            font_weight: weight,
1067            ..Default::default()
1068        };
1069
1070        let font_flags = FontFlags::from_bits_truncate(flags);
1071        q.is_fixed_pitch = font_flags.contains(FontFlags::FIXED_PITCH);
1072        q.is_serif = font_flags.contains(FontFlags::SERIF);
1073        q.is_italic = font_flags.contains(FontFlags::ITALIC);
1074        q.is_small_cap = font_flags.contains(FontFlags::SMALL_CAP);
1075
1076        q.is_bold |= q.font_weight >= 700;
1077
1078        if let Some(name) = &q.post_script_name {
1079            let lower = name.to_ascii_lowercase();
1080            q.is_italic |=
1081                lower.contains("italic") || lower.contains("oblique") || lower.contains("slant");
1082            q.is_bold |= lower.contains("bold")
1083                || lower.contains("demi")
1084                || lower.contains("semibold")
1085                || lower.contains("heavy")
1086                || lower.contains("black");
1087        }
1088        q
1089    }
1090
1091    #[test]
1092    fn fixed_pitch_flag_selects_courier() {
1093        let q = query_with("LetterGothic", FontFlags::FIXED_PITCH.bits(), 400);
1094        assert!(matches!(q.pick_standard_font(), StandardFont::Courier));
1095    }
1096
1097    #[test]
1098    fn demi_in_name_selects_bold() {
1099        let q = query_with("FranklinGothic-Demi", FontFlags::SERIF.bits(), 400);
1100        assert!(q.is_bold);
1101        assert!(matches!(q.pick_standard_font(), StandardFont::TimesBold));
1102    }
1103
1104    #[test]
1105    fn oblique_detected_as_italic() {
1106        let q = query_with("HelveticaNeue-LightOblique", 0, 400);
1107        assert!(q.is_italic);
1108        assert!(matches!(
1109            q.pick_standard_font(),
1110            StandardFont::HelveticaOblique
1111        ));
1112    }
1113
1114    #[test]
1115    fn font_weight_700_detected_as_bold() {
1116        let q = query_with("CustomFont", 0, 700);
1117        assert!(q.is_bold);
1118        assert!(matches!(
1119            q.pick_standard_font(),
1120            StandardFont::HelveticaBold
1121        ));
1122    }
1123
1124    #[test]
1125    fn semibold_detected_as_bold() {
1126        let q = query_with("AGaramond-Semibold", FontFlags::SERIF.bits(), 400);
1127        assert!(q.is_bold);
1128        assert!(matches!(q.pick_standard_font(), StandardFont::TimesBold));
1129    }
1130}