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    ) -> Option<Self> {
431        let f_type = match dict.get::<Name>(SUBTYPE)?.deref() {
432            TYPE1 | MM_TYPE1 => FontType::Type1(Rc::new(Type1Font::new(
433                dict,
434                font_resolver,
435                cmap_resolver,
436                warning_sink,
437            )?)),
438            // PDFBOX-5463: PDF viewers seem to accept OpenType as well.
439            TRUE_TYPE | OPEN_TYPE => FontType::TrueType(Rc::new(TrueTypeFont::new(
440                dict,
441                font_resolver,
442                cmap_resolver,
443                warning_sink,
444            )?)),
445            TYPE0 => FontType::Type0(Rc::new(Type0Font::new(
446                dict,
447                font_resolver,
448                cmap_resolver,
449                warning_sink,
450            )?)),
451            TYPE3 => FontType::Type3(Rc::new(Type3::new(dict, cmap_resolver, warning_sink)?)),
452            f => {
453                warn!(
454                    "unimplemented font type {:?}",
455                    std::str::from_utf8(f).unwrap_or("unknown type")
456                );
457
458                return None;
459            }
460        };
461
462        let cache_key = dict.cache_key();
463
464        Some(Self(cache_key, f_type, None))
465    }
466
467    pub(crate) fn new_standard(
468        standard_font: StandardFont,
469        font_resolver: &FontResolverFn,
470    ) -> Option<Self> {
471        let font = Type1Font::new_standard(standard_font, font_resolver)?;
472
473        Some(Self(0, FontType::Type1(Rc::new(font)), None))
474    }
475
476    /// Name the resource this font was set from (`Tf`'s operand): the name
477    /// extraction reads back to tell the engine's own OCR layer apart from
478    /// author text set in the same face. Stamped on the clone the `Tf`
479    /// handler takes out of the cache, so the cached font stays nameless and
480    /// one resource's name never leaks onto another's.
481    pub(crate) fn set_resource_name(&mut self, name: &str) {
482        self.2 = Some(Rc::from(name));
483    }
484
485    pub(crate) fn map_code(&self, code: u32) -> GlyphId {
486        match &self.1 {
487            FontType::Type1(f) => {
488                debug_assert!(code <= u8::MAX as u32);
489
490                f.map_code(code as u8)
491            }
492            FontType::TrueType(t) => {
493                debug_assert!(code <= u8::MAX as u32);
494
495                t.map_code(code as u8)
496            }
497            FontType::Type0(t) => t.map_code(code),
498            FontType::Type3(t) => {
499                debug_assert!(code <= u8::MAX as u32);
500
501                t.map_code(code as u8)
502            }
503        }
504    }
505
506    pub(crate) fn get_glyph(
507        &self,
508        glyph: GlyphId,
509        char_code: u32,
510        ctx: &mut Context<'a>,
511        resources: &Resources<'a>,
512        origin_displacement: Vec2,
513    ) -> (Glyph<'a>, Affine) {
514        let glyph_transform = ctx.get().text_state.full_transform()
515            * Affine::scale(1.0 / UNITS_PER_EM as f64)
516            * Affine::translate(origin_displacement);
517
518        let glyph = match &self.1 {
519            FontType::Type1(t) => {
520                let font = OutlineFont::Type1(t.clone());
521                let text = font.char_code_to_unicode(char_code);
522                Glyph::Outline(OutlineGlyph {
523                    id: glyph,
524                    font,
525                    char_code,
526                    resource_name: self.2.clone(),
527                    marks: Some(ctx.marks.site()),
528                    text,
529                })
530            }
531            FontType::TrueType(t) => {
532                let font = OutlineFont::TrueType(t.clone());
533                let text = font.char_code_to_unicode(char_code);
534                Glyph::Outline(OutlineGlyph {
535                    id: glyph,
536                    font,
537                    char_code,
538                    resource_name: self.2.clone(),
539                    marks: Some(ctx.marks.site()),
540                    text,
541                })
542            }
543            FontType::Type0(t) => {
544                let font = OutlineFont::Type0(t.clone());
545                let text = font.char_code_to_unicode(char_code);
546                Glyph::Outline(OutlineGlyph {
547                    id: glyph,
548                    font,
549                    char_code,
550                    resource_name: self.2.clone(),
551                    marks: Some(ctx.marks.site()),
552                    text,
553                })
554            }
555            FontType::Type3(t) => {
556                let shape_glyph = Type3Glyph {
557                    nesting_depth: ctx.nesting_depth(),
558                    font: t.clone(),
559                    glyph_id: glyph,
560                    state: ctx.get().clone(),
561                    parent_resources: resources.clone(),
562                    cache: ctx.object_cache.clone(),
563                    xref: ctx.xref,
564                    settings: ctx.settings.clone(),
565                    char_code,
566                };
567
568                Glyph::Type3(Box::new(shape_glyph))
569            }
570        };
571
572        (glyph, glyph_transform)
573    }
574
575    pub(crate) fn code_advance(&self, code: u32) -> Vec2 {
576        match &self.1 {
577            FontType::Type1(t) => {
578                debug_assert!(code <= u8::MAX as u32);
579
580                Vec2::new(t.glyph_width(code as u8).unwrap_or(0.0) as f64, 0.0)
581            }
582            FontType::TrueType(t) => {
583                debug_assert!(code <= u8::MAX as u32);
584
585                Vec2::new(t.glyph_width(code as u8) as f64, 0.0)
586            }
587            FontType::Type0(t) => t.code_advance(code),
588            FontType::Type3(t) => {
589                debug_assert!(code <= u8::MAX as u32);
590
591                Vec2::new(t.glyph_width(code as u8) as f64, 0.0)
592            }
593        }
594    }
595
596    pub(crate) fn origin_displacement(&self, code: u32) -> Vec2 {
597        match &self.1 {
598            FontType::Type1(_) => Vec2::default(),
599            FontType::TrueType(_) => Vec2::default(),
600            FontType::Type0(t) => t.origin_displacement(code),
601            FontType::Type3(_) => Vec2::default(),
602        }
603    }
604
605    pub(crate) fn read_code(&self, bytes: &[u8], offset: usize) -> (u32, usize) {
606        match &self.1 {
607            FontType::Type1(_) => (bytes[offset] as u32, 1),
608            FontType::TrueType(_) => (bytes[offset] as u32, 1),
609            FontType::Type0(t) => t.read_code(bytes, offset),
610            FontType::Type3(_) => (bytes[offset] as u32, 1),
611        }
612    }
613
614    pub(crate) fn is_horizontal(&self) -> bool {
615        match &self.1 {
616            FontType::Type1(_) => true,
617            FontType::TrueType(_) => true,
618            FontType::Type0(t) => t.is_horizontal(),
619            FontType::Type3(_) => true,
620        }
621    }
622}
623
624impl CacheKey for Font<'_> {
625    fn cache_key(&self) -> u128 {
626        self.0
627    }
628}
629
630#[derive(Clone, Debug)]
631enum FontType<'a> {
632    Type1(Rc<Type1Font>),
633    TrueType(Rc<TrueTypeFont>),
634    Type0(Rc<Type0Font>),
635    Type3(Rc<Type3<'a>>),
636}
637
638#[derive(Debug)]
639enum Encoding {
640    Standard,
641    MacRoman,
642    WinAnsi,
643    MacExpert,
644    BuiltIn,
645}
646
647impl Encoding {
648    fn map_code(&self, code: u8) -> Option<&'static str> {
649        if code == 0 {
650            return Some(".notdef");
651        }
652        match self {
653            Self::Standard => standard::get(code),
654            Self::MacRoman => mac_roman::get(code).or_else(|| mac_os_roman::get(code)),
655            Self::WinAnsi => win_ansi::get(code),
656            Self::MacExpert => mac_expert::get(code),
657            Self::BuiltIn => None,
658        }
659    }
660}
661
662/// The font stretch.
663#[derive(Debug, Copy, Clone)]
664pub enum FontStretch {
665    /// Normal.
666    Normal,
667    /// Ultra condensed.
668    UltraCondensed,
669    /// Extra condensed.
670    ExtraCondensed,
671    /// Condensed.
672    Condensed,
673    /// Semi condensed.
674    SemiCondensed,
675    /// Semi expanded.
676    SemiExpanded,
677    /// Expanded.
678    Expanded,
679    /// Extra expanded.
680    ExtraExpanded,
681    /// Ultra expanded.
682    UltraExpanded,
683}
684
685impl FontStretch {
686    fn from_string(s: &str) -> Self {
687        match s {
688            "UltraCondensed" => Self::UltraCondensed,
689            "ExtraCondensed" => Self::ExtraCondensed,
690            "Condensed" => Self::Condensed,
691            "SemiCondensed" => Self::SemiCondensed,
692            "SemiExpanded" => Self::SemiExpanded,
693            "Expanded" => Self::Expanded,
694            "ExtraExpanded" => Self::ExtraExpanded,
695            "UltraExpanded" => Self::UltraExpanded,
696            _ => Self::Normal,
697        }
698    }
699}
700
701bitflags! {
702    /// Bitflags describing various characteristics of fonts.
703    #[derive(Debug)]
704    pub(crate) struct FontFlags: u32 {
705        const FIXED_PITCH = 1 << 0;
706        const SERIF = 1 << 1;
707        const SYMBOLIC = 1 << 2;
708        const SCRIPT = 1 << 3;
709        const NON_SYMBOLIC = 1 << 5;
710        const ITALIC = 1 << 6;
711        const ALL_CAP = 1 << 16;
712        const SMALL_CAP = 1 << 17;
713        const FORCE_BOLD = 1 << 18;
714    }
715}
716
717/// A query for a font.
718pub enum FontQuery {
719    /// A query for one of the 14 PDF standard fonts.
720    Standard(StandardFont),
721    /// A query for a font that is not embedded in the PDF file.
722    ///
723    /// Note that this type of query is currently not supported,
724    /// but will be implemented in the future.
725    Fallback(FallbackFontQuery),
726}
727
728/// A query for a font with specific properties.
729#[derive(Debug, Clone)]
730pub struct FallbackFontQuery {
731    /// The postscript name of the font.
732    pub post_script_name: Option<String>,
733    /// The name of the font.
734    pub font_name: Option<String>,
735    /// The family of the font.
736    pub font_family: Option<String>,
737    /// The stretch of the font.
738    pub font_stretch: FontStretch,
739    /// The weight of the font.
740    pub font_weight: u32,
741    /// Whether the font is monospaced.
742    pub is_fixed_pitch: bool,
743    /// Whether the font is serif.
744    pub is_serif: bool,
745    /// Whether the font is italic.
746    pub is_italic: bool,
747    /// Whether the font is bold.
748    pub is_bold: bool,
749    /// Whether the font is small cap.
750    pub is_small_cap: bool,
751    /// The character collection (registry/ordering) if this is a CID font.
752    pub character_collection: Option<CharacterCollection>,
753}
754
755impl FallbackFontQuery {
756    pub(crate) fn new(dict: &Dict<'_>) -> Self {
757        let post_script_name = dict
758            .get::<Name>(BASE_FONT)
759            .map(|n| strip_subset_prefix(n.as_str()).to_string());
760
761        let mut data = Self {
762            post_script_name,
763            ..Default::default()
764        };
765
766        if let Some(descriptor) = dict.get::<Dict<'_>>(FONT_DESC) {
767            data.font_name = dict
768                .get::<Name>(FONT_NAME)
769                .map(|n| strip_subset_prefix(n.as_str()).to_string());
770            data.font_family = descriptor
771                .get::<Name>(FONT_FAMILY)
772                .map(|n| n.as_str().to_string());
773            data.font_stretch = descriptor
774                .get::<Name>(FONT_STRETCH)
775                .map(|n| FontStretch::from_string(n.as_str()))
776                .unwrap_or(FontStretch::Normal);
777            data.font_weight = descriptor.get::<u32>(FONT_WEIGHT).unwrap_or(400);
778
779            if let Some(flags) = descriptor
780                .get::<u32>(FLAGS)
781                .map(FontFlags::from_bits_truncate)
782            {
783                data.is_fixed_pitch = flags.contains(FontFlags::FIXED_PITCH);
784                data.is_serif = flags.contains(FontFlags::SERIF);
785                data.is_italic = flags.contains(FontFlags::ITALIC);
786                data.is_small_cap = flags.contains(FontFlags::SMALL_CAP);
787            }
788        }
789
790        data.is_bold |= data.font_weight >= 700;
791
792        if let Some(name) = &data.post_script_name {
793            let lower = name.to_ascii_lowercase();
794            data.is_italic |=
795                lower.contains("italic") || lower.contains("oblique") || lower.contains("slant");
796            data.is_bold |= lower.contains("bold")
797                || lower.contains("demi")
798                || lower.contains("semibold")
799                || lower.contains("heavy")
800                || lower.contains("black");
801        }
802
803        data
804    }
805
806    /// Do a best-effort fallback to the 14 standard fonts based on the query.
807    pub fn pick_standard_font(&self) -> StandardFont {
808        if self.is_fixed_pitch {
809            match (self.is_bold, self.is_italic) {
810                (true, true) => StandardFont::CourierBoldOblique,
811                (true, false) => StandardFont::CourierBold,
812                (false, true) => StandardFont::CourierOblique,
813                (false, false) => StandardFont::Courier,
814            }
815        } else if !self.is_serif {
816            match (self.is_bold, self.is_italic) {
817                (true, true) => StandardFont::HelveticaBoldOblique,
818                (true, false) => StandardFont::HelveticaBold,
819                (false, true) => StandardFont::HelveticaOblique,
820                (false, false) => StandardFont::Helvetica,
821            }
822        } else {
823            match (self.is_bold, self.is_italic) {
824                (true, true) => StandardFont::TimesBoldItalic,
825                (true, false) => StandardFont::TimesBold,
826                (false, true) => StandardFont::TimesItalic,
827                (false, false) => StandardFont::TimesRoman,
828            }
829        }
830    }
831}
832
833impl Default for FallbackFontQuery {
834    fn default() -> Self {
835        Self {
836            post_script_name: None,
837            font_name: None,
838            font_family: None,
839            font_stretch: FontStretch::Normal,
840            font_weight: 400,
841            is_fixed_pitch: false,
842            is_serif: false,
843            is_italic: false,
844            is_bold: false,
845            is_small_cap: false,
846            character_collection: None,
847        }
848    }
849}
850
851/// Convert a glyph name to a Unicode character, if possible.
852/// Implements the Adobe Glyph List Specification with extended fallbacks
853/// for non-standard names found in real-world PDFs.
854/// <https://github.com/adobe-type-tools/agl-specification>
855pub(crate) fn glyph_name_to_unicode(name: &str) -> Option<char> {
856    // 1. Direct AGL lookup
857    if let Some(unicode_str) = glyph_names::get(name) {
858        return unicode_str.chars().next();
859    }
860
861    // 2. uni/u prefix convention
862    if let Some(c) = unicode_from_name(name) {
863        return Some(c);
864    }
865
866    // 3. AGL §2.4: strip variant suffix after period (e.g., "A.swash" → "A",
867    //    "comma.alt" → "comma"). Only the base name before the first period
868    //    maps to Unicode.
869    if let Some(dot_pos) = name.find('.') {
870        let base = &name[..dot_pos];
871        if !base.is_empty() {
872            if let Some(c) = glyph_names::get(base).and_then(|s| s.chars().next()) {
873                return Some(c);
874            }
875            if let Some(c) = unicode_from_name(base) {
876                return Some(c);
877            }
878        }
879    }
880
881    // 4. Handle "aXX" decimal glyph names (e.g., "a65" → 'A') used by some
882    //    TeX/LaTeX generated PDFs and custom encoding vectors.
883    if name.starts_with('a')
884        && name.len() >= 2
885        && let Ok(code) = name[1..].parse::<u32>()
886        && let Some(c) = char::from_u32(code)
887        && (!c.is_control() || c == ' ')
888    {
889        return Some(c);
890    }
891
892    warn!("failed to map glyph name {} to unicode", name);
893    None
894}
895
896/// AGL §2.2: resolve a glyph name to one or more Unicode code points. Handles
897/// everything `glyph_name_to_unicode` does, plus the `uniXXXX…` sequences of
898/// AGLFN §3 and underscore-joined ligature names (e.g. `f_i` → "fi",
899/// `f_f_i` → "ffi") which glyph-list-based fonts use for Latin ligatures.
900/// Returns the full string, so ligatures surface as their component code
901/// points rather than being dropped when the composite name isn't in the AGL.
902///
903/// Strips the variant suffix (`.ss01`, `.swash`, …) from the base name before
904/// splitting, matching AGL §2.4. Bails out if any component fails to resolve
905/// so we never produce a partial string (which would be worse than None).
906///
907/// Only when no AGL rule reads the name is the small AMS math-font table
908/// asked, by exact name: it adds names, it never overrides an AGL answer.
909///
910/// A row that reads as a combining mark reads as no table knows it while the
911/// rows are set aside (see `marks.rs`), which is how a reader tells a mark
912/// the row gave from the same character given by `ToUnicode` or any other
913/// source.
914pub(crate) fn glyph_name_to_string(name: &str) -> Option<String> {
915    glyph_name_reading(name).map(|(text, _)| text)
916}
917
918/// [`glyph_name_to_string`], with whether a combining-mark row of the AMS
919/// table gave the text.
920fn glyph_name_reading(name: &str) -> Option<(String, bool)> {
921    if let Some(text) = agl_name_to_string(name) {
922        return Some((text, false));
923    }
924    let c = ams_glyph_names::get(name)?;
925    if is_combining_mark(c) {
926        if mark_rows_set_aside() {
927            return None;
928        }
929        return Some((String::from(c), true));
930    }
931    Some((String::from(c), false))
932}
933
934/// The AGL half of [`glyph_name_to_string`].
935fn agl_name_to_string(name: &str) -> Option<String> {
936    // Then the `uni`/`u` sequences, which spell their own code points --
937    // including the multi-group `uni00410042` form, which the
938    // single-character path below cannot hold (#636).
939    if let Some(s) = unicode_string_from_name(name) {
940        return Some(s);
941    }
942
943    // Then the single-code-point path, which also handles `.suffix`
944    // stripping and the `aNN` decimal convention.
945    if let Some(c) = glyph_name_to_unicode(name) {
946        return Some(c.to_string());
947    }
948
949    let base = name.split_once('.').map(|(b, _)| b).unwrap_or(name);
950    if !base.contains('_') {
951        return None;
952    }
953
954    let mut out = String::new();
955    for part in base.split('_') {
956        if part.is_empty() {
957            return None;
958        }
959        let text = unicode_string_from_name(part)
960            .or_else(|| glyph_name_to_unicode(part).map(|c| c.to_string()))?;
961        out.push_str(&text);
962    }
963    (!out.is_empty()).then_some(out)
964}
965
966/// AGLFN §3: read a `uni`/`u` name as the code points it spells, however many
967/// there are.
968///
969/// `uni` takes one or more groups of four hex digits (`uni20AC` is €,
970/// `uni00410042` is `AB`); `u` takes a single group of four to six
971/// (`u1F600` is 😀, longer runs join with underscores: `u0041_u0042`). The
972/// variant suffix of AGL §2.4 is stripped first, so `uni20AC.tnum` reads like
973/// `uni20AC`.
974///
975/// A name that is not well-formed under those rules -- a short group, a
976/// non-hex digit, a surrogate -- is refused rather than guessed at, and the
977/// caller falls through to the next step of the chain.
978pub(crate) fn unicode_string_from_name(name: &str) -> Option<String> {
979    let base = name.split_once('.').map(|(b, _)| b).unwrap_or(name);
980    if let Some(rest) = base.strip_prefix("uni") {
981        if rest.len() < 4 || !rest.len().is_multiple_of(4) {
982            return None;
983        }
984        let mut out = String::new();
985        for chunk in rest.as_bytes().chunks(4) {
986            let digits = core::str::from_utf8(chunk).ok()?;
987            let value = u32::from_str_radix(digits, 16).ok()?;
988            out.push(char::from_u32(value)?);
989        }
990        return Some(out);
991    }
992    if let Some(rest) = base.strip_prefix('u') {
993        if !(4..=6).contains(&rest.len()) {
994            return None;
995        }
996        let value = u32::from_str_radix(rest, 16).ok()?;
997        return Some(char::from_u32(value)?.to_string());
998    }
999    None
1000}
1001
1002pub(crate) fn unicode_from_name(name: &str) -> Option<char> {
1003    let convert = |input: &str| u32::from_str_radix(input, 16).ok().and_then(char::from_u32);
1004
1005    name.starts_with("uni")
1006        .then(|| name.get(3..).and_then(convert))
1007        .or_else(|| {
1008            name.starts_with("u")
1009                .then(|| name.get(1..).and_then(convert))
1010        })
1011        .flatten()
1012}
1013
1014pub(crate) fn read_to_unicode(
1015    dict: &Dict<'_>,
1016    cmap_resolver: &CMapResolverFn,
1017    warning_sink: &WarningSinkFn,
1018) -> Option<CMap> {
1019    dict.get::<Stream<'_>>(TO_UNICODE)
1020        .and_then(|s| decode_or_warn(&s, warning_sink))
1021        // See PDFJS-11915, where `Identity-H` is used for `ToUnicode`. I don't
1022        // believe it's valid, but at least mupdf seems to be able to deal with it.
1023        .or_else(|| {
1024            dict.get::<Name>(TO_UNICODE)
1025                .and_then(|name| (cmap_resolver)(CMapName::from_bytes(name.as_ref())))
1026                .map(|d| d.to_vec())
1027        })
1028        .and_then(|data| {
1029            let cmap_resolver = cmap_resolver.clone();
1030            CMap::parse(&data, move |name| (cmap_resolver)(name))
1031        })
1032}
1033
1034/// A glyph name read as the text it spells, in cmap-string form.
1035///
1036/// Single-code-point names answer as [`BfString::Char`] so the common case
1037/// keeps its shape; names that spell several code points (an underscore
1038/// ligature like `f_i`, a multi-group `uni00410042`, an AGL sequence entry)
1039/// answer as [`BfString::String`]. `None` when no rule reads the name.
1040pub(crate) fn glyph_name_to_bfstring(name: &str) -> Option<BfString> {
1041    glyph_name_to_string(name).map(bfstring_of)
1042}
1043
1044/// `text` in cmap-string form: one code point as [`BfString::Char`].
1045fn bfstring_of(text: String) -> BfString {
1046    let mut chars = text.chars();
1047    match (chars.next(), chars.next()) {
1048        (Some(c), None) => BfString::Char(c),
1049        _ => BfString::String(text),
1050    }
1051}
1052
1053/// True when a ToUnicode value holds nothing but control characters
1054/// (U+0000-U+001F, U+007F-U+009F). Such a value carries no text: a simple
1055/// font asks its glyph name instead, and keeps the value only when the name
1056/// reads as nothing. An empty value is not control-only.
1057pub(crate) fn is_control_only(value: &BfString) -> bool {
1058    let is_control = |c: char| matches!(c, '\u{0}'..='\u{1F}' | '\u{7F}'..='\u{9F}');
1059    match value {
1060        BfString::Char(c) => is_control(*c),
1061        BfString::String(s) => !s.is_empty() && s.chars().all(is_control),
1062    }
1063}
1064
1065/// Build a 256-entry Unicode map from the font's `/Encoding` entry.
1066/// Used as a fallback when no `/ToUnicode` CMap is present. Each entry
1067/// carries whether a combining-mark row gave it, so that it can be set aside
1068/// with the rows (see [`table_entry`]).
1069pub(crate) fn synthesize_unicode_map_from_encoding(
1070    dict: &Dict<'_>,
1071) -> Option<[Option<NamedEntry>; 256]> {
1072    let (base_encoding, differences) = true_type::read_encoding(dict);
1073    // Only synthesize if there is something to map (encoding name or differences).
1074    // For BuiltIn with no differences we cannot reliably guess the mapping.
1075    if matches!(base_encoding, Encoding::BuiltIn) && differences.is_empty() {
1076        return None;
1077    }
1078    let mut table: [Option<NamedEntry>; 256] = std::array::from_fn(|_| None);
1079    for code in 0u8..=255 {
1080        let glyph_name = differences
1081            .get(&code)
1082            .map(String::as_str)
1083            .or_else(|| base_encoding.map_code(code));
1084        if let Some(name) = glyph_name {
1085            table[code as usize] =
1086                glyph_name_reading(name).map(|(text, from_mark_row)| NamedEntry {
1087                    text: bfstring_of(text),
1088                    from_mark_row,
1089                });
1090        }
1091    }
1092    Some(table)
1093}
1094
1095// When mapping to glyphs, some fonts might only have a glyph for the "normalized"
1096// name. These aliases come from the Adobe Glyph List (AGL) and are safe
1097// synonyms: they are consulted only when the literal lookup already failed, so
1098// adding new entries broadens recovery without overriding correct mappings.
1099pub(crate) fn normalized_glyph_name(mut name: &str) -> &str {
1100    if name == "nbspace" || name == "nonbreakingspace" {
1101        name = "space";
1102    }
1103
1104    if name == "sfthyphen" || name == "softhyphen" {
1105        name = "hyphen";
1106    }
1107
1108    name
1109}
1110
1111#[cfg(test)]
1112mod normalized_glyph_name_tests {
1113    use super::normalized_glyph_name;
1114
1115    #[test]
1116    fn maps_space_aliases() {
1117        assert_eq!(normalized_glyph_name("nbspace"), "space");
1118        assert_eq!(normalized_glyph_name("nonbreakingspace"), "space");
1119    }
1120
1121    #[test]
1122    fn maps_hyphen_aliases() {
1123        assert_eq!(normalized_glyph_name("sfthyphen"), "hyphen");
1124        assert_eq!(normalized_glyph_name("softhyphen"), "hyphen");
1125    }
1126
1127    #[test]
1128    fn preserves_unrelated_names() {
1129        assert_eq!(normalized_glyph_name("A"), "A");
1130        assert_eq!(normalized_glyph_name(".notdef"), ".notdef");
1131        assert_eq!(normalized_glyph_name("hyphen"), "hyphen");
1132        assert_eq!(normalized_glyph_name("space"), "space");
1133    }
1134}
1135
1136#[cfg(test)]
1137mod glyph_name_to_unicode_tests {
1138    use super::{
1139        BfString, glyph_name_to_bfstring, glyph_name_to_string, glyph_name_to_unicode,
1140        unicode_string_from_name,
1141    };
1142
1143    #[test]
1144    fn standard_agl_name() {
1145        assert_eq!(glyph_name_to_unicode("A"), Some('A'));
1146        assert_eq!(glyph_name_to_unicode("space"), Some(' '));
1147        assert_eq!(glyph_name_to_unicode("hyphen"), Some('-'));
1148    }
1149
1150    #[test]
1151    fn ligature_name_underscore_joined() {
1152        // AGL maps `fi` directly (U+FB01); the underscore form is what
1153        // CFF fonts without a ToUnicode map tend to emit for the same
1154        // ligature. We want the decomposed string so readers see "fi".
1155        assert_eq!(glyph_name_to_string("f_i"), Some("fi".to_string()));
1156        assert_eq!(glyph_name_to_string("f_f_i"), Some("ffi".to_string()));
1157        assert_eq!(glyph_name_to_string("A_B_C"), Some("ABC".to_string()));
1158    }
1159
1160    #[test]
1161    fn ligature_name_with_suffix() {
1162        // Variant suffix must be stripped before splitting on `_`.
1163        assert_eq!(glyph_name_to_string("f_i.alt"), Some("fi".to_string()));
1164    }
1165
1166    #[test]
1167    fn ligature_name_falls_back_to_single_char_path() {
1168        // Direct AGL hit should still win and yield the precomposed char.
1169        assert_eq!(glyph_name_to_string("fi"), Some("\u{FB01}".to_string()));
1170        assert_eq!(glyph_name_to_string("A"), Some("A".to_string()));
1171    }
1172
1173    #[test]
1174    fn ligature_name_rejects_unresolvable_component() {
1175        // If any segment is unknown we return None rather than a partial
1176        // string — a partial would be more confusing than a drop.
1177        assert!(glyph_name_to_string("A_totallyUnknownGlyph").is_none());
1178        assert!(glyph_name_to_string("_").is_none());
1179        assert!(glyph_name_to_string("A__B").is_none());
1180    }
1181
1182    #[test]
1183    fn uni_prefix() {
1184        assert_eq!(glyph_name_to_unicode("uni0041"), Some('A'));
1185        assert_eq!(glyph_name_to_unicode("uni00E9"), Some('é'));
1186    }
1187
1188    #[test]
1189    fn u_prefix() {
1190        assert_eq!(glyph_name_to_unicode("u0041"), Some('A'));
1191        assert_eq!(glyph_name_to_unicode("u2022"), Some('•'));
1192    }
1193
1194    #[test]
1195    fn variant_suffix_stripped() {
1196        assert_eq!(glyph_name_to_unicode("A.swash"), Some('A'));
1197        assert_eq!(glyph_name_to_unicode("comma.alt"), Some(','));
1198        assert_eq!(glyph_name_to_unicode("space.narrow"), Some(' '));
1199    }
1200
1201    #[test]
1202    fn variant_suffix_with_uni_prefix() {
1203        assert_eq!(glyph_name_to_unicode("uni0041.ss01"), Some('A'));
1204    }
1205
1206    #[test]
1207    fn a_decimal_glyph_name() {
1208        assert_eq!(glyph_name_to_unicode("a65"), Some('A'));
1209        assert_eq!(glyph_name_to_unicode("a32"), Some(' '));
1210        assert_eq!(glyph_name_to_unicode("a97"), Some('a'));
1211    }
1212
1213    #[test]
1214    fn a_decimal_rejects_control_chars() {
1215        assert_eq!(glyph_name_to_unicode("a0"), None);
1216        assert_eq!(glyph_name_to_unicode("a7"), None);
1217    }
1218
1219    #[test]
1220    fn unknown_name_returns_none() {
1221        assert_eq!(glyph_name_to_unicode("xyzzynonexistent"), None);
1222    }
1223
1224    #[test]
1225    fn bfstring_keeps_single_chars_and_strings_their_shape() {
1226        assert_eq!(glyph_name_to_bfstring("A"), Some(BfString::Char('A')));
1227        assert_eq!(
1228            glyph_name_to_bfstring("f_i"),
1229            Some(BfString::String("fi".to_string()))
1230        );
1231        assert_eq!(glyph_name_to_bfstring("xyzzynonexistent"), None);
1232    }
1233
1234    /// The phase-4 rows (C5c) are plain characters: the name reads as the
1235    /// mathematical angle bracket with no mark origin, set aside or not.
1236    /// Without the rows the names read as today: no AGL rule knows them, so
1237    /// the code keeps its old text (`h`/`i` in cmsy10).
1238    #[test]
1239    fn the_angle_bracket_rows_read_as_plain_characters() {
1240        for (name, bracket) in [
1241            ("angbracketleft", '\u{27E8}'),
1242            ("angbracketright", '\u{27E9}'),
1243        ] {
1244            assert_eq!(super::agl_name_to_string(name), None, "{name}");
1245            assert_eq!(
1246                glyph_name_to_bfstring(name),
1247                Some(BfString::Char(bracket)),
1248                "{name}"
1249            );
1250            assert_eq!(super::glyph_name_reading(name).map(|r| r.1), Some(false));
1251            assert_eq!(
1252                super::without_mark_rows(|| glyph_name_to_bfstring(name)),
1253                Some(BfString::Char(bracket))
1254            );
1255        }
1256    }
1257
1258    /// The phase-5 row (C5b) is a plain character: `visiblespace` reads as
1259    /// U+2423 OPEN BOX with no mark origin, set aside or not. Without the row
1260    /// the name reads as today: no AGL rule knows it, so the glyph reads as
1261    /// nothing.
1262    #[test]
1263    fn the_visiblespace_row_reads_as_the_open_box() {
1264        let name = "visiblespace";
1265        assert_eq!(super::agl_name_to_string(name), None);
1266        assert_eq!(
1267            glyph_name_to_bfstring(name),
1268            Some(BfString::Char('\u{2423}'))
1269        );
1270        assert_eq!(super::glyph_name_reading(name).map(|r| r.1), Some(false));
1271        assert_eq!(
1272            super::without_mark_rows(|| glyph_name_to_bfstring(name)),
1273            Some(BfString::Char('\u{2423}'))
1274        );
1275    }
1276
1277    /// Each combining-mark row reads as its bare mark, with its origin beside
1278    /// it: set aside, the name reads as no row at all, while the same mark
1279    /// from a `uni` name stays (C4 rule 6, C5a, table shape, review (c)). A
1280    /// plain row reads as its character whether the rows stand or not.
1281    #[test]
1282    fn a_combining_mark_row_carries_its_origin() {
1283        for (name, mark, uni) in [
1284            ("circlecopyrt", '\u{20DD}', "uni20DD"),
1285            ("negationslash", '\u{0338}', "uni0338"),
1286        ] {
1287            let named = glyph_name_to_bfstring(name);
1288            assert_eq!(named, Some(BfString::Char(mark)), "{name}");
1289            assert_eq!(super::glyph_name_reading(name).map(|r| r.1), Some(true));
1290            assert_eq!(
1291                super::row_mark_of(&named, || glyph_name_to_bfstring(name)),
1292                Some(mark)
1293            );
1294            let elsewhere = glyph_name_to_bfstring(uni);
1295            assert_eq!(elsewhere, Some(BfString::Char(mark)));
1296            assert_eq!(
1297                super::row_mark_of(&elsewhere, || glyph_name_to_bfstring(uni)),
1298                None
1299            );
1300            assert_eq!(
1301                super::without_mark_rows(|| glyph_name_to_bfstring(name)),
1302                None
1303            );
1304            // A table entry the row wrote counts as absent while set aside.
1305            let entry = Some(super::NamedEntry {
1306                text: BfString::Char(mark),
1307                from_mark_row: true,
1308            });
1309            assert_eq!(super::table_entry(&entry), named);
1310            assert_eq!(
1311                super::without_mark_rows(|| super::table_entry(&entry)),
1312                None
1313            );
1314        }
1315        assert_eq!(
1316            super::without_mark_rows(|| glyph_name_to_bfstring("square")),
1317            Some(BfString::Char('\u{25A1}'))
1318        );
1319        assert_eq!(
1320            glyph_name_to_bfstring("square"),
1321            Some(BfString::Char('\u{25A1}'))
1322        );
1323    }
1324
1325    #[test]
1326    fn uni_sequence_spells_each_group() {
1327        assert_eq!(
1328            unicode_string_from_name("uni00410042"),
1329            Some("AB".to_string())
1330        );
1331        assert_eq!(unicode_string_from_name("uni20AC"), Some("€".to_string()));
1332        assert_eq!(glyph_name_to_string("uni00410042"), Some("AB".to_string()));
1333    }
1334
1335    #[test]
1336    fn uni_sequence_strips_the_variant_suffix_first() {
1337        // The suffix itself holds an underscore, so splitting before
1338        // stripping would resolve `tnum` as a ligature component.
1339        assert_eq!(
1340            glyph_name_to_string("uni00410042.tnum_lnum"),
1341            Some("AB".to_string())
1342        );
1343        assert_eq!(glyph_name_to_string("uni20AC.tnum"), Some("€".to_string()));
1344    }
1345
1346    #[test]
1347    fn u_prefix_holds_four_to_six_digits() {
1348        assert_eq!(unicode_string_from_name("u0041"), Some("A".to_string()));
1349        assert_eq!(unicode_string_from_name("u1F600"), Some("😀".to_string()));
1350        assert_eq!(unicode_string_from_name("u01F600"), Some("😀".to_string()));
1351        assert_eq!(unicode_string_from_name("u041"), None);
1352        assert_eq!(unicode_string_from_name("u00410042"), None);
1353        assert_eq!(glyph_name_to_string("u0041_u0042"), Some("AB".to_string()));
1354    }
1355
1356    #[test]
1357    fn malformed_uni_names_are_refused() {
1358        assert_eq!(unicode_string_from_name("uni"), None);
1359        assert_eq!(unicode_string_from_name("uni00"), None);
1360        assert_eq!(unicode_string_from_name("uni00410"), None);
1361        assert_eq!(unicode_string_from_name("uni00ZZ"), None);
1362        assert_eq!(unicode_string_from_name("uniD800"), None);
1363        assert_eq!(glyph_name_to_string("uni00ZZ"), None);
1364        assert_eq!(glyph_name_to_string("uniD800"), None);
1365    }
1366
1367    #[test]
1368    fn ligature_components_may_spell_sequences() {
1369        // A component may itself spell several code points -- the
1370        // single-character path reads neither of these first halves.
1371        assert_eq!(
1372            glyph_name_to_string("uni00410042_uni0043"),
1373            Some("ABC".to_string())
1374        );
1375        assert_eq!(
1376            glyph_name_to_string("uni00410042_X"),
1377            Some("ABX".to_string())
1378        );
1379        assert_eq!(
1380            glyph_name_to_string("uni0041_uni0042"),
1381            Some("AB".to_string())
1382        );
1383        assert_eq!(glyph_name_to_string("A_uni0042"), Some("AB".to_string()));
1384        assert_eq!(glyph_name_to_string("A_u0042"), Some("AB".to_string()));
1385    }
1386}
1387
1388#[cfg(test)]
1389mod fallback_font_query_tests {
1390    use super::*;
1391
1392    fn query_with(name: &str, flags: u32, weight: u32) -> FallbackFontQuery {
1393        let mut q = FallbackFontQuery {
1394            post_script_name: Some(name.to_string()),
1395            font_weight: weight,
1396            ..Default::default()
1397        };
1398
1399        let font_flags = FontFlags::from_bits_truncate(flags);
1400        q.is_fixed_pitch = font_flags.contains(FontFlags::FIXED_PITCH);
1401        q.is_serif = font_flags.contains(FontFlags::SERIF);
1402        q.is_italic = font_flags.contains(FontFlags::ITALIC);
1403        q.is_small_cap = font_flags.contains(FontFlags::SMALL_CAP);
1404
1405        q.is_bold |= q.font_weight >= 700;
1406
1407        if let Some(name) = &q.post_script_name {
1408            let lower = name.to_ascii_lowercase();
1409            q.is_italic |=
1410                lower.contains("italic") || lower.contains("oblique") || lower.contains("slant");
1411            q.is_bold |= lower.contains("bold")
1412                || lower.contains("demi")
1413                || lower.contains("semibold")
1414                || lower.contains("heavy")
1415                || lower.contains("black");
1416        }
1417        q
1418    }
1419
1420    #[test]
1421    fn fixed_pitch_flag_selects_courier() {
1422        let q = query_with("LetterGothic", FontFlags::FIXED_PITCH.bits(), 400);
1423        assert!(matches!(q.pick_standard_font(), StandardFont::Courier));
1424    }
1425
1426    #[test]
1427    fn demi_in_name_selects_bold() {
1428        let q = query_with("FranklinGothic-Demi", FontFlags::SERIF.bits(), 400);
1429        assert!(q.is_bold);
1430        assert!(matches!(q.pick_standard_font(), StandardFont::TimesBold));
1431    }
1432
1433    #[test]
1434    fn oblique_detected_as_italic() {
1435        let q = query_with("HelveticaNeue-LightOblique", 0, 400);
1436        assert!(q.is_italic);
1437        assert!(matches!(
1438            q.pick_standard_font(),
1439            StandardFont::HelveticaOblique
1440        ));
1441    }
1442
1443    #[test]
1444    fn font_weight_700_detected_as_bold() {
1445        let q = query_with("CustomFont", 0, 700);
1446        assert!(q.is_bold);
1447        assert!(matches!(
1448            q.pick_standard_font(),
1449            StandardFont::HelveticaBold
1450        ));
1451    }
1452
1453    #[test]
1454    fn semibold_detected_as_bold() {
1455        let q = query_with("AGaramond-Semibold", FontFlags::SERIF.bits(), 400);
1456        assert!(q.is_bold);
1457        assert!(matches!(q.pick_standard_font(), StandardFont::TimesBold));
1458    }
1459}