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    ams_glyph_names, 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::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;
36mod marks;
37pub(crate) mod outline;
38mod standard_font;
39mod true_type;
40mod type1;
41pub(crate) mod type3;
42
43pub(crate) const UNITS_PER_EM: f32 = 1000.0;
44
45pub(crate) fn stretch_glyph(path: BezPath, expected_width: f32, actual_width: f32) -> BezPath {
46    if actual_width != 0.0 && actual_width != expected_width {
47        let stretch_factor = expected_width / actual_width;
48        Affine::scale_non_uniform(stretch_factor as f64, 1.0) * path
49    } else {
50        path
51    }
52}
53
54/// A container for the bytes of a PDF file.
55pub type FontData = Arc<dyn AsRef<[u8]> + Send + Sync>;
56
57/// Strip the 6-character subset prefix from a PostScript font name.
58///
59/// PDF subset fonts use names like "ABCDEF+TimesNewRoman". This function
60/// returns `TimesNewRoman` from such a name, or the original name if no
61/// valid prefix is found.
62pub(crate) fn strip_subset_prefix(name: &str) -> &str {
63    match name.split_once('+') {
64        Some((prefix, rest)) if prefix.len() == 6 => rest,
65        _ => name,
66    }
67}
68
69use crate::util::hash128;
70pub(crate) use marks::{MarkPlacement, NamedEntry, table_entry};
71use marks::{
72    MarkSite, is_combining_mark, mark_rows_set_aside, may_be_row_mark, placements_held,
73    row_mark_of, without_mark_rows,
74};
75pub use outline::OutlineFontData;
76use pdf_font::cmap::{BfString, CMap, CMapName, CharacterCollection};
77pub use standard_font::StandardFont;
78
79/// A glyph that can be drawn.
80pub enum Glyph<'a> {
81    /// A glyph defined by an outline.
82    Outline(OutlineGlyph),
83    /// A type3 glyph, defined by PDF drawing instructions.
84    Type3(Box<Type3Glyph<'a>>),
85}
86
87impl Glyph<'_> {
88    /// Returns the Unicode code point for this glyph, if available.
89    ///
90    /// This method attempts to determine the Unicode character that this glyph
91    /// represents. The exact fallback chain depends on the font type:
92    ///
93    /// **For Outline Fonts (Type1, TrueType, CFF):**
94    /// 1. `ToUnicode` cmap
95    /// 2. Glyph name → Unicode (via Adobe Glyph List)
96    /// 3. Unicode naming conventions (e.g., "uni0041", "u0041")
97    ///
98    /// **For CID Fonts (Type0):**
99    /// 1. `ToUnicode` cmap
100    /// 2. Font program fallback (`cmap`, `post`/charset/CharStrings glyph
101    ///    names via the Adobe Glyph List, Identity CID-as-Unicode)
102    ///
103    ///
104    /// **For Type3 Fonts:**
105    /// 1. `ToUnicode` cmap
106    ///
107    /// Returns `None` if the Unicode value could not be determined.
108    ///
109    /// Please note that this method is still somewhat experimental and might
110    /// not work reliably in all cases.
111    pub fn as_unicode(&self) -> Option<BfString> {
112        match self {
113            Glyph::Outline(g) => g.as_unicode(),
114            Glyph::Type3(g) => g.as_unicode(),
115        }
116    }
117
118    /// The resource name the glyph's font was set from, if the `Tf` handler
119    /// stamped one (see [`OutlineGlyph::font_resource_name`]).
120    pub fn font_resource_name(&self) -> Option<&str> {
121        match self {
122            Glyph::Outline(g) => g.font_resource_name(),
123            Glyph::Type3(_) => None,
124        }
125    }
126}
127
128/// An identifier that uniquely identifies a glyph, for caching purposes.
129#[derive(Clone, Debug)]
130pub struct GlyphIdentifier {
131    id: GlyphId,
132    font: OutlineFont,
133}
134
135impl CacheKey for GlyphIdentifier {
136    fn cache_key(&self) -> u128 {
137        hash128(&(self.id, self.font.cache_key()))
138    }
139}
140
141/// A glyph defined by an outline.
142#[derive(Debug)]
143pub struct OutlineGlyph {
144    pub(crate) id: GlyphId,
145    pub(crate) font: OutlineFont,
146    pub(crate) char_code: u32,
147    pub(crate) resource_name: Option<Rc<str>>,
148    /// The glyph's place in the page's record of combining marks (spec
149    /// `specs/text/circlecopyrt.md`, C4 and C5a): `None` for a glyph built
150    /// outside a page interpretation.
151    pub(crate) marks: Option<MarkSite>,
152    /// The code's text as the font gives it with the mark rows standing,
153    /// read once when the glyph is made: the interpreter and the device each
154    /// read a glyph several times, and the font's answer for one code does
155    /// not change. A plain value, no shared state.
156    pub(crate) text: Option<BfString>,
157}
158
159impl Clone for OutlineGlyph {
160    fn clone(&self) -> Self {
161        Self {
162            id: self.id,
163            font: self.font.clone(),
164            char_code: self.char_code,
165            resource_name: self.resource_name.clone(),
166            marks: self.marks,
167            text: self.text.clone(),
168        }
169    }
170}
171
172/// Where a glyph stands among the glyphs of the page interpretation that
173/// drew it: a plain value a device keeps in place of the glyph, to hand to
174/// [`OutlineGlyph::reread_previous`] when the next glyph arrives.
175#[derive(Clone, Copy, Debug, PartialEq, Eq)]
176pub struct GlyphPlace(MarkSite);
177
178impl OutlineGlyph {
179    /// Where this glyph stands among the glyphs of its page interpretation;
180    /// `None` for a glyph made outside one.
181    pub fn place(&self) -> Option<GlyphPlace> {
182        self.marks.map(GlyphPlace)
183    }
184
185    /// What the glyph drawn directly before this one, at `previous`, reads
186    /// as now, when drawing this glyph changed it; `None` when it did not.
187    ///
188    /// Drawing a combining mark from an AMS name row next to a glyph offers
189    /// the mark to that glyph (see [`OutlineGlyph::as_unicode`]), so the
190    /// glyph drawn before can read differently once this one is drawn: a
191    /// `c` drawn before its circle reads `c⃝`, a slash drawn before the `=`
192    /// it strikes reads as nothing. This read is what takes the offer, as
193    /// reading that glyph with `as_unicode` would; a caller that does not
194    /// make it leaves the offer to be refused. On a page where nothing was
195    /// offered it answers `None` at the cost of one load.
196    ///
197    /// Call it before reading this glyph itself (`as_unicode` and the
198    /// like): reading this glyph first decides the offer from this side, and
199    /// the glyph before then reads as it did. A `previous` from another page
200    /// interpretation, or not directly before this glyph, answers `None`.
201    pub fn reread_previous(&self, previous: GlyphPlace) -> Option<Option<BfString>> {
202        self.marks?.reread_previous(previous.0)
203    }
204
205    /// The resource name the glyph's font was set from (`Tf`'s operand), if
206    /// the `Tf` handler stamped one: the name extraction reads back to tell
207    /// the engine's own OCR layer apart from author text set in the same face.
208    pub fn font_resource_name(&self) -> Option<&str> {
209        self.resource_name.as_deref()
210    }
211
212    /// Return the outline of the glyph, assuming an upem value of 1000.
213    pub fn outline(&self) -> BezPath {
214        self.font.outline_glyph(self.id, self.char_code)
215    }
216
217    /// Return the identifier of the glyph. You can use this to calculate the cache key
218    /// for the glyph.
219    ///
220    /// Note that the `glyph_transform` attribute is not considered in the cache key of
221    /// the identifier, only the glyph ID and the font.
222    pub fn identifier(&self) -> GlyphIdentifier {
223        GlyphIdentifier {
224            id: self.id,
225            font: self.font.clone(),
226        }
227    }
228
229    /// Returns the Unicode code point for this glyph, if available.
230    ///
231    /// See [`Glyph::as_unicode`] for details on the fallback chain used.
232    ///
233    /// A combining mark that an AMS name row gives (`circlecopyrt`,
234    /// `negationslash`) is never handed out on its own glyph. Where it is
235    /// placed, the glyph it marks reads as itself followed by the mark and the
236    /// mark's glyph reads as nothing; otherwise the mark's glyph reads as its
237    /// code did before the row existed (see `marks.rs`). A placement is
238    /// decided by which of the two glyphs is read first once it is offered,
239    /// so the glyph drawn before a mark can read differently when read again
240    /// after the mark is drawn.
241    pub fn as_unicode(&self) -> Option<BfString> {
242        // A glyph whose text cannot be a row's mark, read while no placement
243        // exists anywhere, reads as the font gives it: the common case costs
244        // two tests (spec, "Performance").
245        if !may_be_row_mark(&self.text) && !placements_held() {
246            return self.text.clone();
247        }
248        self.placed_text()
249    }
250
251    /// [`Self::as_unicode`] where a row's mark or a placement may be
252    /// involved.
253    #[inline(never)]
254    fn placed_text(&self) -> Option<BfString> {
255        let text = self.cached_text();
256        if row_mark_of(text, || self.font_text()).is_some() {
257            if self.marks.as_ref().is_some_and(MarkSite::mark_taken) {
258                return None;
259            }
260            return without_mark_rows(|| self.font_text());
261        }
262        let text = text.clone();
263        let marks = self.marks.as_ref().map(MarkSite::base_marks);
264        match marks.filter(|marks| !marks.is_empty()) {
265            Some(marks) => {
266                let mut placed = match text {
267                    Some(BfString::Char(c)) => c.to_string(),
268                    Some(BfString::String(s)) => s,
269                    None => String::new(),
270                };
271                placed.push_str(&marks);
272                Some(BfString::String(placed))
273            }
274            None => text,
275        }
276    }
277
278    /// The text the font gives this glyph's code, before any placement: a
279    /// row-derived combining mark is its bare character here, and whether a
280    /// row gave it is read beside the text (`marks::row_mark_of`).
281    ///
282    /// Borrowed from the glyph's one read of its code, made with the glyph.
283    /// A read that sets the mark rows aside asks [`Self::font_text`].
284    pub(crate) fn cached_text(&self) -> &Option<BfString> {
285        &self.text
286    }
287
288    /// What the font gives this glyph's code now, as the mark rows stand.
289    pub(crate) fn font_text(&self) -> Option<BfString> {
290        self.font.char_code_to_unicode(self.char_code)
291    }
292
293    /// Get raw font bytes and metadata for downstream use.
294    ///
295    /// Returns `None` for Type1 fonts.
296    pub fn font_data(&self) -> Option<OutlineFontData> {
297        self.font.font_data()
298    }
299
300    /// PostScript name of the underlying font, if determinable.
301    ///
302    /// Works even when [`font_data`](Self::font_data) returns
303    /// `None` — e.g. non-embedded standard-14 Type1 fonts still
304    /// expose their canonical PostScript name through this
305    /// accessor. Returns `None` when no name source is available.
306    ///
307    /// Surfaced to support the WASM `getTextPositions()` `fontName`
308    /// field for the editor contract.
309    pub fn postscript_name(&self) -> Option<String> {
310        self.font.postscript_name()
311    }
312
313    /// The font dictionary's /BaseFont as the page named it, subset prefix
314    /// and all, where the font carries one (#623). [`Self::postscript_name`]
315    /// names the face that draws the glyphs, which for a font standing on a
316    /// standard face is that face, not the page's.
317    pub fn declared_name(&self) -> Option<String> {
318        self.font.declared_name()
319    }
320
321    /// The name text extraction reports for a span: the document's own name
322    /// for an embedded font, the substitute face otherwise. Subset prefix
323    /// and all, like [`Self::declared_name`]; callers strip.
324    pub fn document_name(&self) -> Option<String> {
325        self.font.document_name()
326    }
327
328    /// Get the glyph ID within the font.
329    pub fn glyph_id(&self) -> GlyphId {
330        self.id
331    }
332
333    /// Get the advance width for this glyph.
334    ///
335    /// The advance width is how far to move horizontally after drawing
336    /// this glyph before drawing the next one.
337    pub fn advance_width(&self) -> Option<f32> {
338        self.font.glyph_advance_width(self.char_code)
339    }
340
341    /// Vertical font metrics in /1000 em.
342    ///
343    /// Returns `(ascent, descent, cap_height, x_height)` from the font's
344    /// embedded OS/2 tables (via skrifa) when available, or from the
345    /// Standard-14 AFM fallback table for non-embedded standard fonts.
346    /// Returns `None` only when no metrics source is reachable (e.g.
347    /// CFF fonts without a standard-font fallback, Type3).
348    pub fn font_metrics(&self) -> Option<(f64, f64, Option<f64>, Option<f64>)> {
349        self.font.font_metrics()
350    }
351
352    /// The substitute face's AFM pair for Type1/CFF, ignoring the
353    /// descriptor: the pre-E4 answer, kept for the default extraction path
354    /// (#643 follow-up).
355    pub fn substitute_metrics(&self) -> Option<(f64, f64, Option<f64>, Option<f64>)> {
356        self.font.substitute_metrics()
357    }
358
359    /// Get the cache key for this glyph's font.
360    ///
361    /// This identifies the font uniquely, even when `font_data()` returns `None`
362    /// (e.g., for Type1 fonts). Useful for grouping glyphs by font.
363    pub fn font_cache_key(&self) -> u128 {
364        self.font.cache_key()
365    }
366}
367
368/// A type3 glyph.
369#[derive(Clone)]
370pub struct Type3Glyph<'a> {
371    pub(crate) font: Rc<Type3<'a>>,
372    pub(crate) glyph_id: GlyphId,
373    pub(crate) state: State<'a>,
374    pub(crate) parent_resources: Resources<'a>,
375    pub(crate) cache: Cache,
376    pub(crate) xref: &'a XRef,
377    pub(crate) settings: InterpreterSettings,
378    pub(crate) char_code: u32,
379    /// The depth the interpretation sat at when this glyph was reached (#318).
380    /// A Type 3 glyph is a content stream, so it can name a pattern whose cell
381    /// draws text in this same font — a fresh Context starting at zero would
382    /// never reach the bound.
383    pub(crate) nesting_depth: u32,
384}
385
386/// A glyph defined by PDF drawing instructions.
387impl<'a> Type3Glyph<'a> {
388    /// Draw the type3 glyph to the given device.
389    pub fn interpret(
390        &self,
391        device: &mut impl Device<'a>,
392        transform: Affine,
393        glyph_transform: Affine,
394        paint: &Paint<'a>,
395    ) {
396        self.font
397            .render_glyph(self, transform, glyph_transform, paint, device);
398    }
399
400    /// Returns the Unicode code point for this glyph, if available.
401    ///
402    /// Note: Type3 fonts can only provide Unicode via `ToUnicode` cmap.
403    ///
404    /// A combining mark that an AMS name row gives is not placed on a Type 3
405    /// glyph's neighbour: it reads as its code did before the row existed.
406    pub fn as_unicode(&self) -> Option<BfString> {
407        let text = self.font.char_code_to_unicode(self.char_code);
408        if row_mark_of(&text, || self.font.char_code_to_unicode(self.char_code)).is_some() {
409            return without_mark_rows(|| self.font.char_code_to_unicode(self.char_code));
410        }
411        text
412    }
413}
414
415impl CacheKey for Type3Glyph<'_> {
416    fn cache_key(&self) -> u128 {
417        hash128(&(self.font.cache_key(), self.glyph_id))
418    }
419}
420
421#[derive(Clone, Debug)]
422pub(crate) struct Font<'a>(u128, FontType<'a>, Option<Rc<str>>);
423
424impl<'a> Font<'a> {
425    pub(crate) fn new(
426        dict: &Dict<'a>,
427        font_resolver: &FontResolverFn,
428        cmap_resolver: &CMapResolverFn,
429        warning_sink: &WarningSinkFn,
430        cache: &Cache,
431    ) -> Option<Self> {
432        let f_type = match dict.get::<Name>(SUBTYPE)?.deref() {
433            TYPE1 | MM_TYPE1 => FontType::Type1(Rc::new(Type1Font::new(
434                dict,
435                font_resolver,
436                cmap_resolver,
437                warning_sink,
438                cache,
439            )?)),
440            // PDFBOX-5463: PDF viewers seem to accept OpenType as well.
441            TRUE_TYPE | OPEN_TYPE => FontType::TrueType(Rc::new(TrueTypeFont::new(
442                dict,
443                font_resolver,
444                cmap_resolver,
445                warning_sink,
446                cache,
447            )?)),
448            TYPE0 => FontType::Type0(Rc::new(Type0Font::new(
449                dict,
450                font_resolver,
451                cmap_resolver,
452                warning_sink,
453                cache,
454            )?)),
455            TYPE3 => FontType::Type3(Rc::new(Type3::new(
456                dict,
457                cmap_resolver,
458                warning_sink,
459                cache,
460            )?)),
461            f => {
462                warn!(
463                    "unimplemented font type {:?}",
464                    std::str::from_utf8(f).unwrap_or("unknown type")
465                );
466
467                return None;
468            }
469        };
470
471        let cache_key = dict.cache_key();
472
473        Some(Self(cache_key, f_type, None))
474    }
475
476    pub(crate) fn new_standard(
477        standard_font: StandardFont,
478        font_resolver: &FontResolverFn,
479        cache: &Cache,
480    ) -> Option<Self> {
481        let font = Type1Font::new_standard(standard_font, font_resolver, cache)?;
482
483        Some(Self(0, FontType::Type1(Rc::new(font)), None))
484    }
485
486    /// Name the resource this font was set from (`Tf`'s operand): the name
487    /// extraction reads back to tell the engine's own OCR layer apart from
488    /// author text set in the same face. Stamped on the clone the `Tf`
489    /// handler takes out of the cache, so the cached font stays nameless and
490    /// one resource's name never leaks onto another's.
491    pub(crate) fn set_resource_name(&mut self, name: &str) {
492        self.2 = Some(Rc::from(name));
493    }
494
495    pub(crate) fn map_code(&self, code: u32) -> GlyphId {
496        match &self.1 {
497            FontType::Type1(f) => {
498                debug_assert!(code <= u8::MAX as u32);
499
500                f.map_code(code as u8)
501            }
502            FontType::TrueType(t) => {
503                debug_assert!(code <= u8::MAX as u32);
504
505                t.map_code(code as u8)
506            }
507            FontType::Type0(t) => t.map_code(code),
508            FontType::Type3(t) => {
509                debug_assert!(code <= u8::MAX as u32);
510
511                t.map_code(code as u8)
512            }
513        }
514    }
515
516    pub(crate) fn get_glyph(
517        &self,
518        glyph: GlyphId,
519        char_code: u32,
520        ctx: &mut Context<'a>,
521        resources: &Resources<'a>,
522        origin_displacement: Vec2,
523    ) -> (Glyph<'a>, Affine) {
524        let glyph_transform = ctx.get().text_state.full_transform()
525            * Affine::scale(1.0 / UNITS_PER_EM as f64)
526            * Affine::translate(origin_displacement);
527
528        let glyph = match &self.1 {
529            FontType::Type1(t) => {
530                let font = OutlineFont::Type1(t.clone());
531                let text = font.char_code_to_unicode(char_code);
532                Glyph::Outline(OutlineGlyph {
533                    id: glyph,
534                    font,
535                    char_code,
536                    resource_name: self.2.clone(),
537                    marks: Some(ctx.marks.site()),
538                    text,
539                })
540            }
541            FontType::TrueType(t) => {
542                let font = OutlineFont::TrueType(t.clone());
543                let text = font.char_code_to_unicode(char_code);
544                Glyph::Outline(OutlineGlyph {
545                    id: glyph,
546                    font,
547                    char_code,
548                    resource_name: self.2.clone(),
549                    marks: Some(ctx.marks.site()),
550                    text,
551                })
552            }
553            FontType::Type0(t) => {
554                let font = OutlineFont::Type0(t.clone());
555                let text = font.char_code_to_unicode(char_code);
556                Glyph::Outline(OutlineGlyph {
557                    id: glyph,
558                    font,
559                    char_code,
560                    resource_name: self.2.clone(),
561                    marks: Some(ctx.marks.site()),
562                    text,
563                })
564            }
565            FontType::Type3(t) => {
566                let shape_glyph = Type3Glyph {
567                    nesting_depth: ctx.nesting_depth(),
568                    font: t.clone(),
569                    glyph_id: glyph,
570                    state: ctx.get().clone(),
571                    parent_resources: resources.clone(),
572                    cache: ctx.object_cache.clone(),
573                    xref: ctx.xref,
574                    settings: ctx.settings.clone(),
575                    char_code,
576                };
577
578                Glyph::Type3(Box::new(shape_glyph))
579            }
580        };
581
582        (glyph, glyph_transform)
583    }
584
585    pub(crate) fn code_advance(&self, code: u32) -> Vec2 {
586        match &self.1 {
587            FontType::Type1(t) => {
588                debug_assert!(code <= u8::MAX as u32);
589
590                Vec2::new(t.glyph_width(code as u8).unwrap_or(0.0) as f64, 0.0)
591            }
592            FontType::TrueType(t) => {
593                debug_assert!(code <= u8::MAX as u32);
594
595                Vec2::new(t.glyph_width(code as u8) as f64, 0.0)
596            }
597            FontType::Type0(t) => t.code_advance(code),
598            FontType::Type3(t) => {
599                debug_assert!(code <= u8::MAX as u32);
600
601                Vec2::new(t.glyph_width(code as u8) as f64, 0.0)
602            }
603        }
604    }
605
606    pub(crate) fn origin_displacement(&self, code: u32) -> Vec2 {
607        match &self.1 {
608            FontType::Type1(_) => Vec2::default(),
609            FontType::TrueType(_) => Vec2::default(),
610            FontType::Type0(t) => t.origin_displacement(code),
611            FontType::Type3(_) => Vec2::default(),
612        }
613    }
614
615    pub(crate) fn read_code(&self, bytes: &[u8], offset: usize) -> (u32, usize) {
616        match &self.1 {
617            FontType::Type1(_) => (bytes[offset] as u32, 1),
618            FontType::TrueType(_) => (bytes[offset] as u32, 1),
619            FontType::Type0(t) => t.read_code(bytes, offset),
620            FontType::Type3(_) => (bytes[offset] as u32, 1),
621        }
622    }
623
624    pub(crate) fn is_horizontal(&self) -> bool {
625        match &self.1 {
626            FontType::Type1(_) => true,
627            FontType::TrueType(_) => true,
628            FontType::Type0(t) => t.is_horizontal(),
629            FontType::Type3(_) => true,
630        }
631    }
632}
633
634impl CacheKey for Font<'_> {
635    fn cache_key(&self) -> u128 {
636        self.0
637    }
638}
639
640#[derive(Clone, Debug)]
641enum FontType<'a> {
642    Type1(Rc<Type1Font>),
643    TrueType(Rc<TrueTypeFont>),
644    Type0(Rc<Type0Font>),
645    Type3(Rc<Type3<'a>>),
646}
647
648#[derive(Debug)]
649enum Encoding {
650    Standard,
651    MacRoman,
652    WinAnsi,
653    MacExpert,
654    BuiltIn,
655}
656
657impl Encoding {
658    fn map_code(&self, code: u8) -> Option<&'static str> {
659        if code == 0 {
660            return Some(".notdef");
661        }
662        match self {
663            Self::Standard => standard::get(code),
664            Self::MacRoman => mac_roman::get(code).or_else(|| mac_os_roman::get(code)),
665            Self::WinAnsi => win_ansi::get(code),
666            Self::MacExpert => mac_expert::get(code),
667            Self::BuiltIn => None,
668        }
669    }
670}
671
672/// The font stretch.
673#[derive(Debug, Copy, Clone)]
674pub enum FontStretch {
675    /// Normal.
676    Normal,
677    /// Ultra condensed.
678    UltraCondensed,
679    /// Extra condensed.
680    ExtraCondensed,
681    /// Condensed.
682    Condensed,
683    /// Semi condensed.
684    SemiCondensed,
685    /// Semi expanded.
686    SemiExpanded,
687    /// Expanded.
688    Expanded,
689    /// Extra expanded.
690    ExtraExpanded,
691    /// Ultra expanded.
692    UltraExpanded,
693}
694
695impl FontStretch {
696    fn from_string(s: &str) -> Self {
697        match s {
698            "UltraCondensed" => Self::UltraCondensed,
699            "ExtraCondensed" => Self::ExtraCondensed,
700            "Condensed" => Self::Condensed,
701            "SemiCondensed" => Self::SemiCondensed,
702            "SemiExpanded" => Self::SemiExpanded,
703            "Expanded" => Self::Expanded,
704            "ExtraExpanded" => Self::ExtraExpanded,
705            "UltraExpanded" => Self::UltraExpanded,
706            _ => Self::Normal,
707        }
708    }
709}
710
711bitflags! {
712    /// Bitflags describing various characteristics of fonts.
713    #[derive(Debug)]
714    pub(crate) struct FontFlags: u32 {
715        const FIXED_PITCH = 1 << 0;
716        const SERIF = 1 << 1;
717        const SYMBOLIC = 1 << 2;
718        const SCRIPT = 1 << 3;
719        const NON_SYMBOLIC = 1 << 5;
720        const ITALIC = 1 << 6;
721        const ALL_CAP = 1 << 16;
722        const SMALL_CAP = 1 << 17;
723        const FORCE_BOLD = 1 << 18;
724    }
725}
726
727/// A query for a font.
728pub enum FontQuery {
729    /// A query for one of the 14 PDF standard fonts.
730    Standard(StandardFont),
731    /// A query for a font that is not embedded in the PDF file.
732    ///
733    /// Note that this type of query is currently not supported,
734    /// but will be implemented in the future.
735    Fallback(FallbackFontQuery),
736}
737
738/// A query for a font with specific properties.
739#[derive(Debug, Clone)]
740pub struct FallbackFontQuery {
741    /// The postscript name of the font.
742    pub post_script_name: Option<String>,
743    /// The name of the font.
744    pub font_name: Option<String>,
745    /// The family of the font.
746    pub font_family: Option<String>,
747    /// The stretch of the font.
748    pub font_stretch: FontStretch,
749    /// The weight of the font.
750    pub font_weight: u32,
751    /// Whether the font is monospaced.
752    pub is_fixed_pitch: bool,
753    /// Whether the font is serif.
754    pub is_serif: bool,
755    /// Whether the font is italic.
756    pub is_italic: bool,
757    /// Whether the font is bold.
758    pub is_bold: bool,
759    /// Whether the font is small cap.
760    pub is_small_cap: bool,
761    /// The character collection (registry/ordering) if this is a CID font.
762    pub character_collection: Option<CharacterCollection>,
763}
764
765impl FallbackFontQuery {
766    pub(crate) fn new(dict: &Dict<'_>) -> Self {
767        let post_script_name = dict
768            .get::<Name>(BASE_FONT)
769            .map(|n| strip_subset_prefix(n.as_str()).to_string());
770
771        let mut data = Self {
772            post_script_name,
773            ..Default::default()
774        };
775
776        if let Some(descriptor) = dict.get::<Dict<'_>>(FONT_DESC) {
777            data.font_name = dict
778                .get::<Name>(FONT_NAME)
779                .map(|n| strip_subset_prefix(n.as_str()).to_string());
780            data.font_family = descriptor
781                .get::<Name>(FONT_FAMILY)
782                .map(|n| n.as_str().to_string());
783            data.font_stretch = descriptor
784                .get::<Name>(FONT_STRETCH)
785                .map(|n| FontStretch::from_string(n.as_str()))
786                .unwrap_or(FontStretch::Normal);
787            data.font_weight = descriptor.get::<u32>(FONT_WEIGHT).unwrap_or(400);
788
789            if let Some(flags) = descriptor
790                .get::<u32>(FLAGS)
791                .map(FontFlags::from_bits_truncate)
792            {
793                data.is_fixed_pitch = flags.contains(FontFlags::FIXED_PITCH);
794                data.is_serif = flags.contains(FontFlags::SERIF);
795                data.is_italic = flags.contains(FontFlags::ITALIC);
796                data.is_small_cap = flags.contains(FontFlags::SMALL_CAP);
797            }
798        }
799
800        data.is_bold |= data.font_weight >= 700;
801
802        if let Some(name) = &data.post_script_name {
803            let lower = name.to_ascii_lowercase();
804            data.is_italic |=
805                lower.contains("italic") || lower.contains("oblique") || lower.contains("slant");
806            data.is_bold |= lower.contains("bold")
807                || lower.contains("demi")
808                || lower.contains("semibold")
809                || lower.contains("heavy")
810                || lower.contains("black");
811        }
812
813        data
814    }
815
816    /// Do a best-effort fallback to the 14 standard fonts based on the query.
817    pub fn pick_standard_font(&self) -> StandardFont {
818        if self.is_fixed_pitch {
819            match (self.is_bold, self.is_italic) {
820                (true, true) => StandardFont::CourierBoldOblique,
821                (true, false) => StandardFont::CourierBold,
822                (false, true) => StandardFont::CourierOblique,
823                (false, false) => StandardFont::Courier,
824            }
825        } else if !self.is_serif {
826            match (self.is_bold, self.is_italic) {
827                (true, true) => StandardFont::HelveticaBoldOblique,
828                (true, false) => StandardFont::HelveticaBold,
829                (false, true) => StandardFont::HelveticaOblique,
830                (false, false) => StandardFont::Helvetica,
831            }
832        } else {
833            match (self.is_bold, self.is_italic) {
834                (true, true) => StandardFont::TimesBoldItalic,
835                (true, false) => StandardFont::TimesBold,
836                (false, true) => StandardFont::TimesItalic,
837                (false, false) => StandardFont::TimesRoman,
838            }
839        }
840    }
841}
842
843impl Default for FallbackFontQuery {
844    fn default() -> Self {
845        Self {
846            post_script_name: None,
847            font_name: None,
848            font_family: None,
849            font_stretch: FontStretch::Normal,
850            font_weight: 400,
851            is_fixed_pitch: false,
852            is_serif: false,
853            is_italic: false,
854            is_bold: false,
855            is_small_cap: false,
856            character_collection: None,
857        }
858    }
859}
860
861/// Convert a glyph name to a Unicode character, if possible.
862/// Implements the Adobe Glyph List Specification with extended fallbacks
863/// for non-standard names found in real-world PDFs.
864/// <https://github.com/adobe-type-tools/agl-specification>
865pub(crate) fn glyph_name_to_unicode(name: &str) -> Option<char> {
866    // 1. Direct AGL lookup
867    if let Some(unicode_str) = glyph_names::get(name) {
868        return unicode_str.chars().next();
869    }
870
871    // 2. uni/u prefix convention
872    if let Some(c) = unicode_from_name(name) {
873        return Some(c);
874    }
875
876    // 3. AGL §2.4: strip variant suffix after period (e.g., "A.swash" → "A",
877    //    "comma.alt" → "comma"). Only the base name before the first period
878    //    maps to Unicode.
879    if let Some(dot_pos) = name.find('.') {
880        let base = &name[..dot_pos];
881        if !base.is_empty() {
882            if let Some(c) = glyph_names::get(base).and_then(|s| s.chars().next()) {
883                return Some(c);
884            }
885            if let Some(c) = unicode_from_name(base) {
886                return Some(c);
887            }
888        }
889    }
890
891    // 4. Handle "aXX" decimal glyph names (e.g., "a65" → 'A') used by some
892    //    TeX/LaTeX generated PDFs and custom encoding vectors.
893    if name.starts_with('a')
894        && name.len() >= 2
895        && let Ok(code) = name[1..].parse::<u32>()
896        && let Some(c) = char::from_u32(code)
897        && (!c.is_control() || c == ' ')
898    {
899        return Some(c);
900    }
901
902    warn!("failed to map glyph name {} to unicode", name);
903    None
904}
905
906/// AGL §2.2: resolve a glyph name to one or more Unicode code points. Handles
907/// everything `glyph_name_to_unicode` does, plus the `uniXXXX…` sequences of
908/// AGLFN §3 and underscore-joined ligature names (e.g. `f_i` → "fi",
909/// `f_f_i` → "ffi") which glyph-list-based fonts use for Latin ligatures.
910/// Returns the full string, so ligatures surface as their component code
911/// points rather than being dropped when the composite name isn't in the AGL.
912///
913/// Strips the variant suffix (`.ss01`, `.swash`, …) from the base name before
914/// splitting, matching AGL §2.4. Bails out if any component fails to resolve
915/// so we never produce a partial string (which would be worse than None).
916///
917/// Only when no AGL rule reads the name is the small AMS math-font table
918/// asked, by exact name: it adds names, it never overrides an AGL answer.
919///
920/// A row that reads as a combining mark reads as no table knows it while the
921/// rows are set aside (see `marks.rs`), which is how a reader tells a mark
922/// the row gave from the same character given by `ToUnicode` or any other
923/// source.
924pub(crate) fn glyph_name_to_string(name: &str) -> Option<String> {
925    glyph_name_reading(name).map(|(text, _)| text)
926}
927
928/// [`glyph_name_to_string`], with whether a combining-mark row of the AMS
929/// table gave the text.
930fn glyph_name_reading(name: &str) -> Option<(String, bool)> {
931    if let Some(text) = agl_name_to_string(name) {
932        return Some((text, false));
933    }
934    let c = ams_glyph_names::get(name)?;
935    if is_combining_mark(c) {
936        if mark_rows_set_aside() {
937            return None;
938        }
939        return Some((String::from(c), true));
940    }
941    Some((String::from(c), false))
942}
943
944/// The AGL half of [`glyph_name_to_string`].
945fn agl_name_to_string(name: &str) -> Option<String> {
946    // Then the `uni`/`u` sequences, which spell their own code points --
947    // including the multi-group `uni00410042` form, which the
948    // single-character path below cannot hold (#636).
949    if let Some(s) = unicode_string_from_name(name) {
950        return Some(s);
951    }
952
953    // Then the single-code-point path, which also handles `.suffix`
954    // stripping and the `aNN` decimal convention.
955    if let Some(c) = glyph_name_to_unicode(name) {
956        return Some(c.to_string());
957    }
958
959    let base = name.split_once('.').map(|(b, _)| b).unwrap_or(name);
960    if !base.contains('_') {
961        return None;
962    }
963
964    let mut out = String::new();
965    for part in base.split('_') {
966        if part.is_empty() {
967            return None;
968        }
969        let text = unicode_string_from_name(part)
970            .or_else(|| glyph_name_to_unicode(part).map(|c| c.to_string()))?;
971        out.push_str(&text);
972    }
973    (!out.is_empty()).then_some(out)
974}
975
976/// AGLFN §3: read a `uni`/`u` name as the code points it spells, however many
977/// there are.
978///
979/// `uni` takes one or more groups of four hex digits (`uni20AC` is €,
980/// `uni00410042` is `AB`); `u` takes a single group of four to six
981/// (`u1F600` is 😀, longer runs join with underscores: `u0041_u0042`). The
982/// variant suffix of AGL §2.4 is stripped first, so `uni20AC.tnum` reads like
983/// `uni20AC`.
984///
985/// A name that is not well-formed under those rules -- a short group, a
986/// non-hex digit, a surrogate -- is refused rather than guessed at, and the
987/// caller falls through to the next step of the chain.
988pub(crate) fn unicode_string_from_name(name: &str) -> Option<String> {
989    let base = name.split_once('.').map(|(b, _)| b).unwrap_or(name);
990    if let Some(rest) = base.strip_prefix("uni") {
991        if rest.len() < 4 || !rest.len().is_multiple_of(4) {
992            return None;
993        }
994        let mut out = String::new();
995        for chunk in rest.as_bytes().chunks(4) {
996            let digits = core::str::from_utf8(chunk).ok()?;
997            let value = u32::from_str_radix(digits, 16).ok()?;
998            out.push(char::from_u32(value)?);
999        }
1000        return Some(out);
1001    }
1002    if let Some(rest) = base.strip_prefix('u') {
1003        if !(4..=6).contains(&rest.len()) {
1004            return None;
1005        }
1006        let value = u32::from_str_radix(rest, 16).ok()?;
1007        return Some(char::from_u32(value)?.to_string());
1008    }
1009    None
1010}
1011
1012pub(crate) fn unicode_from_name(name: &str) -> Option<char> {
1013    let convert = |input: &str| u32::from_str_radix(input, 16).ok().and_then(char::from_u32);
1014
1015    name.starts_with("uni")
1016        .then(|| name.get(3..).and_then(convert))
1017        .or_else(|| {
1018            name.starts_with("u")
1019                .then(|| name.get(1..).and_then(convert))
1020        })
1021        .flatten()
1022}
1023
1024pub(crate) fn read_to_unicode(
1025    dict: &Dict<'_>,
1026    cmap_resolver: &CMapResolverFn,
1027    warning_sink: &WarningSinkFn,
1028    cache: &Cache,
1029) -> Option<CMap> {
1030    // Parsed once per document: the same ToUnicode stream is read for its
1031    // font on every page, and a `None` is not kept, so a stream that fails
1032    // to decode warns on every page as before.
1033    if let Some(s) = dict.get::<Stream<'_>>(TO_UNICODE) {
1034        let key = Cache::font_stream_key(b"tounicode", &s);
1035        let parsed = cache.get_or_insert_some(key, || {
1036            let data = decode_or_warn(&s, warning_sink)?;
1037            let cmap_resolver = cmap_resolver.clone();
1038            CMap::parse(&data, move |name| (cmap_resolver)(name))
1039        });
1040        if parsed.is_some() {
1041            return parsed;
1042        }
1043    }
1044    // See PDFJS-11915, where `Identity-H` is used for `ToUnicode`. I don't
1045    // believe it's valid, but at least mupdf seems to be able to deal with it.
1046    let name = dict.get::<Name>(TO_UNICODE)?;
1047    let data = (cmap_resolver)(CMapName::from_bytes(name.as_ref()))?;
1048    let key = Cache::font_data_key(b"tounicode-name", 0, data);
1049    cache.get_or_insert_some(key, || {
1050        let cmap_resolver = cmap_resolver.clone();
1051        CMap::parse(data, move |name| (cmap_resolver)(name))
1052    })
1053}
1054
1055/// A glyph name read as the text it spells, in cmap-string form.
1056///
1057/// Single-code-point names answer as [`BfString::Char`] so the common case
1058/// keeps its shape; names that spell several code points (an underscore
1059/// ligature like `f_i`, a multi-group `uni00410042`, an AGL sequence entry)
1060/// answer as [`BfString::String`]. `None` when no rule reads the name.
1061pub(crate) fn glyph_name_to_bfstring(name: &str) -> Option<BfString> {
1062    glyph_name_to_string(name).map(bfstring_of)
1063}
1064
1065/// `text` in cmap-string form: one code point as [`BfString::Char`].
1066fn bfstring_of(text: String) -> BfString {
1067    let mut chars = text.chars();
1068    match (chars.next(), chars.next()) {
1069        (Some(c), None) => BfString::Char(c),
1070        _ => BfString::String(text),
1071    }
1072}
1073
1074/// True when a ToUnicode value holds nothing but control characters
1075/// (U+0000-U+001F, U+007F-U+009F). Such a value carries no text: a simple
1076/// font asks its glyph name instead, and keeps the value only when the name
1077/// reads as nothing. An empty value is not control-only.
1078pub(crate) fn is_control_only(value: &BfString) -> bool {
1079    let is_control = |c: char| matches!(c, '\u{0}'..='\u{1F}' | '\u{7F}'..='\u{9F}');
1080    match value {
1081        BfString::Char(c) => is_control(*c),
1082        BfString::String(s) => !s.is_empty() && s.chars().all(is_control),
1083    }
1084}
1085
1086/// Build a 256-entry Unicode map from the font's `/Encoding` entry.
1087/// Used as a fallback when no `/ToUnicode` CMap is present. Each entry
1088/// carries whether a combining-mark row gave it, so that it can be set aside
1089/// with the rows (see [`table_entry`]).
1090pub(crate) fn synthesize_unicode_map_from_encoding(
1091    dict: &Dict<'_>,
1092) -> Option<[Option<NamedEntry>; 256]> {
1093    let (base_encoding, differences) = true_type::read_encoding(dict);
1094    // Only synthesize if there is something to map (encoding name or differences).
1095    // For BuiltIn with no differences we cannot reliably guess the mapping.
1096    if matches!(base_encoding, Encoding::BuiltIn) && differences.is_empty() {
1097        return None;
1098    }
1099    let mut table: [Option<NamedEntry>; 256] = std::array::from_fn(|_| None);
1100    for code in 0u8..=255 {
1101        let glyph_name = differences
1102            .get(&code)
1103            .map(String::as_str)
1104            .or_else(|| base_encoding.map_code(code));
1105        if let Some(name) = glyph_name {
1106            table[code as usize] =
1107                glyph_name_reading(name).map(|(text, from_mark_row)| NamedEntry {
1108                    text: bfstring_of(text),
1109                    from_mark_row,
1110                });
1111        }
1112    }
1113    Some(table)
1114}
1115
1116// When mapping to glyphs, some fonts might only have a glyph for the "normalized"
1117// name. These aliases come from the Adobe Glyph List (AGL) and are safe
1118// synonyms: they are consulted only when the literal lookup already failed, so
1119// adding new entries broadens recovery without overriding correct mappings.
1120pub(crate) fn normalized_glyph_name(mut name: &str) -> &str {
1121    if name == "nbspace" || name == "nonbreakingspace" {
1122        name = "space";
1123    }
1124
1125    if name == "sfthyphen" || name == "softhyphen" {
1126        name = "hyphen";
1127    }
1128
1129    name
1130}
1131
1132#[cfg(test)]
1133mod normalized_glyph_name_tests {
1134    use super::normalized_glyph_name;
1135
1136    #[test]
1137    fn maps_space_aliases() {
1138        assert_eq!(normalized_glyph_name("nbspace"), "space");
1139        assert_eq!(normalized_glyph_name("nonbreakingspace"), "space");
1140    }
1141
1142    #[test]
1143    fn maps_hyphen_aliases() {
1144        assert_eq!(normalized_glyph_name("sfthyphen"), "hyphen");
1145        assert_eq!(normalized_glyph_name("softhyphen"), "hyphen");
1146    }
1147
1148    #[test]
1149    fn preserves_unrelated_names() {
1150        assert_eq!(normalized_glyph_name("A"), "A");
1151        assert_eq!(normalized_glyph_name(".notdef"), ".notdef");
1152        assert_eq!(normalized_glyph_name("hyphen"), "hyphen");
1153        assert_eq!(normalized_glyph_name("space"), "space");
1154    }
1155}
1156
1157#[cfg(test)]
1158mod glyph_name_to_unicode_tests {
1159    use super::{
1160        BfString, glyph_name_to_bfstring, glyph_name_to_string, glyph_name_to_unicode,
1161        unicode_string_from_name,
1162    };
1163
1164    #[test]
1165    fn standard_agl_name() {
1166        assert_eq!(glyph_name_to_unicode("A"), Some('A'));
1167        assert_eq!(glyph_name_to_unicode("space"), Some(' '));
1168        assert_eq!(glyph_name_to_unicode("hyphen"), Some('-'));
1169    }
1170
1171    #[test]
1172    fn ligature_name_underscore_joined() {
1173        // AGL maps `fi` directly (U+FB01); the underscore form is what
1174        // CFF fonts without a ToUnicode map tend to emit for the same
1175        // ligature. We want the decomposed string so readers see "fi".
1176        assert_eq!(glyph_name_to_string("f_i"), Some("fi".to_string()));
1177        assert_eq!(glyph_name_to_string("f_f_i"), Some("ffi".to_string()));
1178        assert_eq!(glyph_name_to_string("A_B_C"), Some("ABC".to_string()));
1179    }
1180
1181    #[test]
1182    fn ligature_name_with_suffix() {
1183        // Variant suffix must be stripped before splitting on `_`.
1184        assert_eq!(glyph_name_to_string("f_i.alt"), Some("fi".to_string()));
1185    }
1186
1187    #[test]
1188    fn ligature_name_falls_back_to_single_char_path() {
1189        // Direct AGL hit should still win and yield the precomposed char.
1190        assert_eq!(glyph_name_to_string("fi"), Some("\u{FB01}".to_string()));
1191        assert_eq!(glyph_name_to_string("A"), Some("A".to_string()));
1192    }
1193
1194    #[test]
1195    fn ligature_name_rejects_unresolvable_component() {
1196        // If any segment is unknown we return None rather than a partial
1197        // string — a partial would be more confusing than a drop.
1198        assert!(glyph_name_to_string("A_totallyUnknownGlyph").is_none());
1199        assert!(glyph_name_to_string("_").is_none());
1200        assert!(glyph_name_to_string("A__B").is_none());
1201    }
1202
1203    #[test]
1204    fn uni_prefix() {
1205        assert_eq!(glyph_name_to_unicode("uni0041"), Some('A'));
1206        assert_eq!(glyph_name_to_unicode("uni00E9"), Some('é'));
1207    }
1208
1209    #[test]
1210    fn u_prefix() {
1211        assert_eq!(glyph_name_to_unicode("u0041"), Some('A'));
1212        assert_eq!(glyph_name_to_unicode("u2022"), Some('•'));
1213    }
1214
1215    #[test]
1216    fn variant_suffix_stripped() {
1217        assert_eq!(glyph_name_to_unicode("A.swash"), Some('A'));
1218        assert_eq!(glyph_name_to_unicode("comma.alt"), Some(','));
1219        assert_eq!(glyph_name_to_unicode("space.narrow"), Some(' '));
1220    }
1221
1222    #[test]
1223    fn variant_suffix_with_uni_prefix() {
1224        assert_eq!(glyph_name_to_unicode("uni0041.ss01"), Some('A'));
1225    }
1226
1227    #[test]
1228    fn a_decimal_glyph_name() {
1229        assert_eq!(glyph_name_to_unicode("a65"), Some('A'));
1230        assert_eq!(glyph_name_to_unicode("a32"), Some(' '));
1231        assert_eq!(glyph_name_to_unicode("a97"), Some('a'));
1232    }
1233
1234    #[test]
1235    fn a_decimal_rejects_control_chars() {
1236        assert_eq!(glyph_name_to_unicode("a0"), None);
1237        assert_eq!(glyph_name_to_unicode("a7"), None);
1238    }
1239
1240    #[test]
1241    fn unknown_name_returns_none() {
1242        assert_eq!(glyph_name_to_unicode("xyzzynonexistent"), None);
1243    }
1244
1245    #[test]
1246    fn bfstring_keeps_single_chars_and_strings_their_shape() {
1247        assert_eq!(glyph_name_to_bfstring("A"), Some(BfString::Char('A')));
1248        assert_eq!(
1249            glyph_name_to_bfstring("f_i"),
1250            Some(BfString::String("fi".to_string()))
1251        );
1252        assert_eq!(glyph_name_to_bfstring("xyzzynonexistent"), None);
1253    }
1254
1255    /// The phase-4 rows (C5c) are plain characters: the name reads as the
1256    /// mathematical angle bracket with no mark origin, set aside or not.
1257    /// Without the rows the names read as today: no AGL rule knows them, so
1258    /// the code keeps its old text (`h`/`i` in cmsy10).
1259    #[test]
1260    fn the_angle_bracket_rows_read_as_plain_characters() {
1261        for (name, bracket) in [
1262            ("angbracketleft", '\u{27E8}'),
1263            ("angbracketright", '\u{27E9}'),
1264        ] {
1265            assert_eq!(super::agl_name_to_string(name), None, "{name}");
1266            assert_eq!(
1267                glyph_name_to_bfstring(name),
1268                Some(BfString::Char(bracket)),
1269                "{name}"
1270            );
1271            assert_eq!(super::glyph_name_reading(name).map(|r| r.1), Some(false));
1272            assert_eq!(
1273                super::without_mark_rows(|| glyph_name_to_bfstring(name)),
1274                Some(BfString::Char(bracket))
1275            );
1276        }
1277    }
1278
1279    /// The phase-5 row (C5b) is a plain character: `visiblespace` reads as
1280    /// U+2423 OPEN BOX with no mark origin, set aside or not. Without the row
1281    /// the name reads as today: no AGL rule knows it, so the glyph reads as
1282    /// nothing.
1283    #[test]
1284    fn the_visiblespace_row_reads_as_the_open_box() {
1285        let name = "visiblespace";
1286        assert_eq!(super::agl_name_to_string(name), None);
1287        assert_eq!(
1288            glyph_name_to_bfstring(name),
1289            Some(BfString::Char('\u{2423}'))
1290        );
1291        assert_eq!(super::glyph_name_reading(name).map(|r| r.1), Some(false));
1292        assert_eq!(
1293            super::without_mark_rows(|| glyph_name_to_bfstring(name)),
1294            Some(BfString::Char('\u{2423}'))
1295        );
1296    }
1297
1298    /// Each combining-mark row reads as its bare mark, with its origin beside
1299    /// it: set aside, the name reads as no row at all, while the same mark
1300    /// from a `uni` name stays (C4 rule 6, C5a, table shape, review (c)). A
1301    /// plain row reads as its character whether the rows stand or not.
1302    #[test]
1303    fn a_combining_mark_row_carries_its_origin() {
1304        for (name, mark, uni) in [
1305            ("circlecopyrt", '\u{20DD}', "uni20DD"),
1306            ("negationslash", '\u{0338}', "uni0338"),
1307        ] {
1308            let named = glyph_name_to_bfstring(name);
1309            assert_eq!(named, Some(BfString::Char(mark)), "{name}");
1310            assert_eq!(super::glyph_name_reading(name).map(|r| r.1), Some(true));
1311            assert_eq!(
1312                super::row_mark_of(&named, || glyph_name_to_bfstring(name)),
1313                Some(mark)
1314            );
1315            let elsewhere = glyph_name_to_bfstring(uni);
1316            assert_eq!(elsewhere, Some(BfString::Char(mark)));
1317            assert_eq!(
1318                super::row_mark_of(&elsewhere, || glyph_name_to_bfstring(uni)),
1319                None
1320            );
1321            assert_eq!(
1322                super::without_mark_rows(|| glyph_name_to_bfstring(name)),
1323                None
1324            );
1325            // A table entry the row wrote counts as absent while set aside.
1326            let entry = Some(super::NamedEntry {
1327                text: BfString::Char(mark),
1328                from_mark_row: true,
1329            });
1330            assert_eq!(super::table_entry(&entry), named);
1331            assert_eq!(
1332                super::without_mark_rows(|| super::table_entry(&entry)),
1333                None
1334            );
1335        }
1336        assert_eq!(
1337            super::without_mark_rows(|| glyph_name_to_bfstring("square")),
1338            Some(BfString::Char('\u{25A1}'))
1339        );
1340        assert_eq!(
1341            glyph_name_to_bfstring("square"),
1342            Some(BfString::Char('\u{25A1}'))
1343        );
1344    }
1345
1346    #[test]
1347    fn uni_sequence_spells_each_group() {
1348        assert_eq!(
1349            unicode_string_from_name("uni00410042"),
1350            Some("AB".to_string())
1351        );
1352        assert_eq!(unicode_string_from_name("uni20AC"), Some("€".to_string()));
1353        assert_eq!(glyph_name_to_string("uni00410042"), Some("AB".to_string()));
1354    }
1355
1356    #[test]
1357    fn uni_sequence_strips_the_variant_suffix_first() {
1358        // The suffix itself holds an underscore, so splitting before
1359        // stripping would resolve `tnum` as a ligature component.
1360        assert_eq!(
1361            glyph_name_to_string("uni00410042.tnum_lnum"),
1362            Some("AB".to_string())
1363        );
1364        assert_eq!(glyph_name_to_string("uni20AC.tnum"), Some("€".to_string()));
1365    }
1366
1367    #[test]
1368    fn u_prefix_holds_four_to_six_digits() {
1369        assert_eq!(unicode_string_from_name("u0041"), Some("A".to_string()));
1370        assert_eq!(unicode_string_from_name("u1F600"), Some("😀".to_string()));
1371        assert_eq!(unicode_string_from_name("u01F600"), Some("😀".to_string()));
1372        assert_eq!(unicode_string_from_name("u041"), None);
1373        assert_eq!(unicode_string_from_name("u00410042"), None);
1374        assert_eq!(glyph_name_to_string("u0041_u0042"), Some("AB".to_string()));
1375    }
1376
1377    #[test]
1378    fn malformed_uni_names_are_refused() {
1379        assert_eq!(unicode_string_from_name("uni"), None);
1380        assert_eq!(unicode_string_from_name("uni00"), None);
1381        assert_eq!(unicode_string_from_name("uni00410"), None);
1382        assert_eq!(unicode_string_from_name("uni00ZZ"), None);
1383        assert_eq!(unicode_string_from_name("uniD800"), None);
1384        assert_eq!(glyph_name_to_string("uni00ZZ"), None);
1385        assert_eq!(glyph_name_to_string("uniD800"), None);
1386    }
1387
1388    #[test]
1389    fn ligature_components_may_spell_sequences() {
1390        // A component may itself spell several code points -- the
1391        // single-character path reads neither of these first halves.
1392        assert_eq!(
1393            glyph_name_to_string("uni00410042_uni0043"),
1394            Some("ABC".to_string())
1395        );
1396        assert_eq!(
1397            glyph_name_to_string("uni00410042_X"),
1398            Some("ABX".to_string())
1399        );
1400        assert_eq!(
1401            glyph_name_to_string("uni0041_uni0042"),
1402            Some("AB".to_string())
1403        );
1404        assert_eq!(glyph_name_to_string("A_uni0042"), Some("AB".to_string()));
1405        assert_eq!(glyph_name_to_string("A_u0042"), Some("AB".to_string()));
1406    }
1407}
1408
1409#[cfg(test)]
1410mod fallback_font_query_tests {
1411    use super::*;
1412
1413    fn query_with(name: &str, flags: u32, weight: u32) -> FallbackFontQuery {
1414        let mut q = FallbackFontQuery {
1415            post_script_name: Some(name.to_string()),
1416            font_weight: weight,
1417            ..Default::default()
1418        };
1419
1420        let font_flags = FontFlags::from_bits_truncate(flags);
1421        q.is_fixed_pitch = font_flags.contains(FontFlags::FIXED_PITCH);
1422        q.is_serif = font_flags.contains(FontFlags::SERIF);
1423        q.is_italic = font_flags.contains(FontFlags::ITALIC);
1424        q.is_small_cap = font_flags.contains(FontFlags::SMALL_CAP);
1425
1426        q.is_bold |= q.font_weight >= 700;
1427
1428        if let Some(name) = &q.post_script_name {
1429            let lower = name.to_ascii_lowercase();
1430            q.is_italic |=
1431                lower.contains("italic") || lower.contains("oblique") || lower.contains("slant");
1432            q.is_bold |= lower.contains("bold")
1433                || lower.contains("demi")
1434                || lower.contains("semibold")
1435                || lower.contains("heavy")
1436                || lower.contains("black");
1437        }
1438        q
1439    }
1440
1441    #[test]
1442    fn fixed_pitch_flag_selects_courier() {
1443        let q = query_with("LetterGothic", FontFlags::FIXED_PITCH.bits(), 400);
1444        assert!(matches!(q.pick_standard_font(), StandardFont::Courier));
1445    }
1446
1447    #[test]
1448    fn demi_in_name_selects_bold() {
1449        let q = query_with("FranklinGothic-Demi", FontFlags::SERIF.bits(), 400);
1450        assert!(q.is_bold);
1451        assert!(matches!(q.pick_standard_font(), StandardFont::TimesBold));
1452    }
1453
1454    #[test]
1455    fn oblique_detected_as_italic() {
1456        let q = query_with("HelveticaNeue-LightOblique", 0, 400);
1457        assert!(q.is_italic);
1458        assert!(matches!(
1459            q.pick_standard_font(),
1460            StandardFont::HelveticaOblique
1461        ));
1462    }
1463
1464    #[test]
1465    fn font_weight_700_detected_as_bold() {
1466        let q = query_with("CustomFont", 0, 700);
1467        assert!(q.is_bold);
1468        assert!(matches!(
1469            q.pick_standard_font(),
1470            StandardFont::HelveticaBold
1471        ));
1472    }
1473
1474    #[test]
1475    fn semibold_detected_as_bold() {
1476        let q = query_with("AGaramond-Semibold", FontFlags::SERIF.bits(), 400);
1477        assert!(q.is_bold);
1478        assert!(matches!(q.pick_standard_font(), StandardFont::TimesBold));
1479    }
1480}