Skip to main content

pdfrum_font/
lib.rs

1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4// Every byte reaching this crate came from an untrusted font program or font
5// dictionary: index with `get()`, never with `[]`.
6#![warn(clippy::indexing_slicing)]
7// Font units are integers carried as floats, character codes are `u8`s cut out
8// of wider values, and the metric normalizers are the C++'s own integer
9// arithmetic. Each conversion below is pinned by a test.
10#![allow(
11    clippy::cast_possible_truncation,
12    clippy::cast_precision_loss,
13    clippy::cast_sign_loss,
14    clippy::cast_possible_wrap
15)]
16
17// Every module is private and the `pub use` block below is the whole surface.
18// A type a sibling crate names is re-exported here; a function
19// only this crate uses is not.
20mod cid;
21mod descriptor;
22mod encoding;
23mod error;
24mod fallback;
25mod glyphs;
26mod ids;
27mod load;
28mod names;
29mod simple;
30mod subst;
31#[cfg(test)]
32mod test_resolve;
33#[cfg(test)]
34mod testfonts;
35mod tounicode;
36mod type3;
37mod widths;
38
39pub use cid::{CidTransform, Type0Font, cid_transform_to_float};
40pub use encoding::FaceEncoding;
41pub use encoding::adobe_name_from_unicode;
42pub use error::Error;
43pub use fallback::GlyphFallback;
44pub use glyphs::{
45    Charmap, CharmapId, Face, GlyphCache, GlyphKey, GlyphSource, SynthGlyph, em_adjust,
46};
47pub use ids::{CharCode, Cid, FontFlags, FontId, Gid};
48pub use simple::SimpleFont;
49pub use subst::{
50    Charset, StandardFont, SubstFont, SubstitutionOptions, canonical_font_name,
51    charset_from_unicode,
52};
53
54pub use load::{CharItem, Font, FontCache, load, load_with_options};
55pub use pdfrum_type1::FontFile as Type1FontFile;
56pub use tounicode::invert_to_unicode;
57pub use type3::{MAX_TYPE3_DEPTH, Type3Font};
58
59/// A Type 1 program as the `/FontFile` stream a PDF writer stores, with the
60/// ISO 32000-1 table 127 lengths that partition it.
61///
62/// See [`pdfrum_type1::font_file`]: a PFB is unwrapped into the raw program
63/// its records carry, because the container's framing is not part of the font.
64#[must_use]
65pub fn type1_font_file(bytes: &[u8]) -> Type1FontFile {
66    pdfrum_type1::font_file(bytes)
67}
68
69#[cfg(test)]
70mod tests {
71    // Test expectations are exact values by design.
72    #![allow(clippy::float_cmp)]
73
74    use super::names;
75    use super::*;
76    use crate::load::wants_chinese_cid_rescue;
77    use pdfrum_common::{Diagnostics, Limits};
78    use pdfrum_object::{Dict, Name, NoResolve, Object};
79
80    fn simple_dict(subtype: &str, base: &str) -> Dict {
81        Dict::from_pairs([
82            (names::SUBTYPE.clone(), Object::Name(Name::from(subtype))),
83            (names::BASE_FONT.clone(), Object::Name(Name::from(base))),
84        ])
85    }
86
87    #[test]
88    fn a_missing_subtype_loads_as_a_type1_font() {
89        let dict = Dict::from_pairs([(
90            names::BASE_FONT.clone(),
91            Object::Name(Name::from("Helvetica")),
92        )]);
93        let font = load(
94            &dict,
95            &NoResolve,
96            &FontCache::new(),
97            &Limits::default(),
98            &mut Diagnostics::default(),
99        )
100        .expect("a simple font always constructs");
101        assert!(matches!(font, Font::Simple(_)));
102    }
103
104    #[test]
105    fn garbage_subtypes_also_load_as_type1() {
106        for subtype in ["Type1", "MMType1", "NotAFontType", ""] {
107            let font = load(
108                &simple_dict(subtype, "Helvetica"),
109                &NoResolve,
110                &FontCache::new(),
111                &Limits::default(),
112                &mut Diagnostics::default(),
113            );
114            assert!(matches!(font, Some(Font::Simple(_))), "{subtype}");
115        }
116    }
117
118    #[test]
119    fn a_type3_subtype_loads_as_type3() {
120        let font = load(
121            &simple_dict("Type3", ""),
122            &NoResolve,
123            &FontCache::new(),
124            &Limits::default(),
125            &mut Diagnostics::default(),
126        )
127        .expect("Type3 always constructs");
128        assert!(font.type3().is_some());
129        // A Type3 font has no outlines at all, by construction.
130        assert!(font.glyph_path(Gid(0)).is_none());
131    }
132
133    #[test]
134    fn a_type0_font_without_descendants_fails_to_load() {
135        // The one font kind whose load can fail, and the reason the public
136        // entry point returns `Option`.
137        assert!(
138            load(
139                &simple_dict("Type0", "Foo"),
140                &NoResolve,
141                &FontCache::new(),
142                &Limits::default(),
143                &mut Diagnostics::default(),
144            )
145            .is_none()
146        );
147    }
148
149    #[test]
150    fn the_chinese_name_rescue_reroutes_a_truetype_font() {
151        // 宋体 in GBK, with no descriptor at all.
152        let mut name = vec![0xcb, 0xce, 0xcc, 0xe5];
153        name.extend_from_slice(b"-Extra");
154        let dict = Dict::from_pairs([
155            (names::SUBTYPE.clone(), Object::Name(Name::from("TrueType"))),
156            (names::BASE_FONT.clone(), Object::Name(Name::new(name))),
157        ]);
158        assert!(wants_chinese_cid_rescue(&dict, &NoResolve));
159    }
160
161    #[test]
162    fn the_chinese_rescue_does_not_fire_for_an_embedded_font() {
163        let desc = Dict::from_pairs([(
164            names::FONT_FILE2.clone(),
165            Object::Ref(pdfrum_object::ObjRef::new(7, 0)),
166        )]);
167        let dict = Dict::from_pairs([
168            (names::SUBTYPE.clone(), Object::Name(Name::from("TrueType"))),
169            (
170                names::BASE_FONT.clone(),
171                Object::Name(Name::new(vec![0xcb, 0xce, 0xcc, 0xe5])),
172            ),
173            (names::FONT_DESCRIPTOR.clone(), Object::Dict(desc)),
174        ]);
175        assert!(!wants_chinese_cid_rescue(&dict, &NoResolve));
176    }
177
178    #[test]
179    fn a_name_shorter_than_four_bytes_never_matches() {
180        let dict = Dict::from_pairs([
181            (names::SUBTYPE.clone(), Object::Name(Name::from("TrueType"))),
182            (names::BASE_FONT.clone(), Object::Name(Name::from("ab"))),
183        ]);
184        assert!(!wants_chinese_cid_rescue(&dict, &NoResolve));
185    }
186
187    #[test]
188    fn the_standard_fourteen_all_load_and_name_themselves() {
189        let cache = FontCache::new();
190        for which in subst::ALL_STANDARD_FONTS {
191            let font = Font::load_standard(which, &cache);
192            assert_eq!(
193                font.base_font_name(),
194                subst::canonical_font_name(which).as_bytes(),
195                "{which:?}"
196            );
197            assert!(font.glyph_path(Gid(1)).is_some() || font.glyph_path(Gid(2)).is_some());
198        }
199    }
200
201    #[test]
202    fn a_standard_font_round_trips_ascii_both_ways() {
203        let font = Font::load_standard(StandardFont::Times, &FontCache::new());
204        for ch in "The quick brown fox! 0123".chars() {
205            let Some(code) = font.char_code_from_unicode(ch) else {
206                panic!("{ch:?} should be encodable in a Latin font");
207            };
208            assert_eq!(
209                font.unicode_from_charcode(code).as_slice(),
210                [ch],
211                "{ch:?} did not round-trip"
212            );
213        }
214    }
215
216    #[test]
217    fn char_code_from_unicode_declines_what_the_font_cannot_express() {
218        let font = Font::load_standard(StandardFont::Helvetica, &FontCache::new());
219        for ch in ['\u{4e00}', '\u{3042}', '\u{10000}'] {
220            assert_eq!(font.char_code_from_unicode(ch), None, "{ch:?}");
221        }
222    }
223
224    #[test]
225    fn append_char_writes_one_byte_for_a_simple_font() {
226        let font = Font::load_standard(StandardFont::Helvetica, &FontCache::new());
227        let mut out = Vec::new();
228        for ch in "Hello, world!".chars() {
229            let code = font
230                .char_code_from_unicode(ch)
231                .unwrap_or_else(|| panic!("{ch:?} is encodable"));
232            font.append_char(&mut out, code);
233        }
234        assert_eq!(out, b"Hello, world!");
235    }
236
237    #[test]
238    fn append_char_writes_two_bytes_for_an_identity_composite_font() {
239        // A composite font's codespace decides the width, which is the reason
240        // `append_char` is a method rather than a byte cast at the call site.
241        let descendant = Dict::from_pairs([
242            (
243                names::SUBTYPE.clone(),
244                Object::Name(Name::from("CIDFontType0")),
245            ),
246            (names::BASE_FONT.clone(), Object::Name(Name::from("Test"))),
247        ]);
248        let dict = Dict::from_pairs([
249            (names::SUBTYPE.clone(), Object::Name(Name::from("Type0"))),
250            (
251                names::ENCODING.clone(),
252                Object::Name(Name::from("Identity-H")),
253            ),
254            (
255                names::DESCENDANT_FONTS.clone(),
256                Object::Array(pdfrum_object::Array::of([Object::Dict(descendant)])),
257            ),
258        ]);
259        let font = load(
260            &dict,
261            &NoResolve,
262            &FontCache::new(),
263            &Limits::default(),
264            &mut Diagnostics::default(),
265        )
266        .expect("a Type0 font with one descendant loads");
267
268        let mut out = Vec::new();
269        font.append_char(&mut out, CharCode(0x0041));
270        assert_eq!(out, vec![0x00, 0x41]);
271    }
272
273    #[test]
274    fn the_courier_widths_are_the_fixed_six_hundred() {
275        let font = Font::load_standard(StandardFont::CourierBold, &FontCache::new());
276        for ch in "iWm ".chars() {
277            let code = font.char_code_from_unicode(ch).expect("encodable");
278            assert_eq!(font.char_width(code), 600.0, "{ch:?}");
279        }
280    }
281
282    #[test]
283    fn font_ids_are_distinct() {
284        let cache = FontCache::new();
285        let a = cache.next_id();
286        let b = cache.next_id();
287        assert_ne!(a, b);
288    }
289
290    #[test]
291    fn a_non_embedded_truetype_falls_back_to_arial_for_an_unmapped_code() {
292        // `bug_1442723`: `/Subtype /TrueType /BaseFont /Symbol` with no
293        // `/ToUnicode`. Codes the ladder cannot place take Arial, not `.notdef`.
294        let font = load(
295            &simple_dict("TrueType", "Symbol"),
296            &NoResolve,
297            &FontCache::new(),
298            &Limits::default(),
299            &mut Diagnostics::default(),
300        )
301        .expect("a simple font always constructs");
302        assert!(!font.should_use_own_glyph(None));
303        assert!(!font.should_use_own_glyph(Some(Gid(0))));
304        let fb = font.glyph_fallback().expect("Arial is a built-in stand-in");
305        assert!(fb.gid(&['A'], CharCode(u32::from(b'A'))).is_some());
306    }
307
308    #[test]
309    fn a_truetype_symbol_code_keeps_its_encoding_unicode() {
310        // `bug_1442723`: `/Subtype /TrueType /BaseFont /Symbol`, Flags
311        // symbolic, no `/Encoding`. The ladder's Unicode table stays 0
312        // because `MsSymbol` has no glyph names; the encoding table still
313        // names U+2297 for 0xD9, which the Arial stand-in looks up.
314        let desc = Dict::from_pairs([(names::FLAGS.clone(), Object::Int(6))]);
315        let dict = Dict::from_pairs([
316            (names::SUBTYPE.clone(), Object::Name(Name::from("TrueType"))),
317            (names::BASE_FONT.clone(), Object::Name(Name::from("Symbol"))),
318            (names::FONT_DESCRIPTOR.clone(), Object::Dict(desc)),
319        ]);
320        let font = load(
321            &dict,
322            &NoResolve,
323            &FontCache::new(),
324            &Limits::default(),
325            &mut Diagnostics::default(),
326        )
327        .expect("a simple font always constructs");
328        let chars = font.unicode_from_charcode(CharCode(0xD9));
329        assert_eq!(chars.as_slice(), &['\u{2227}']);
330    }
331}
332
333#[cfg(test)]
334mod send_sync {
335    //! Every public type is `Send + Sync`, so rendering pages in parallel
336    //! with `rayon` needs no wrapper. A compile failure here is the whole
337    //! test.
338
339    use super::*;
340    use pdfrum_object::ObjRef;
341    use std::sync::Arc;
342
343    const fn assert_send_sync<T: Send + Sync>() {}
344
345    #[test]
346    fn every_public_type_is_send_and_sync() {
347        assert_send_sync::<Font>();
348        assert_send_sync::<CharItem>();
349        assert_send_sync::<SimpleFont>();
350        assert_send_sync::<Type0Font>();
351        assert_send_sync::<Type3Font>();
352        assert_send_sync::<FontCache>();
353        assert_send_sync::<GlyphCache>();
354        assert_send_sync::<GlyphKey>();
355        assert_send_sync::<GlyphSource>();
356        assert_send_sync::<crate::tounicode::ToUnicode>();
357        assert_send_sync::<crate::descriptor::FontDescriptor>();
358        assert_send_sync::<crate::ids::GlyphName>();
359        assert_send_sync::<SubstFont>();
360        assert_send_sync::<SubstitutionOptions>();
361        assert_send_sync::<crate::widths::CidWidths>();
362        assert_send_sync::<crate::widths::VerticalMetrics>();
363        assert_send_sync::<crate::cid::CidToGid>();
364        assert_send_sync::<Error>();
365        assert_send_sync::<subst::FontRequest>();
366        assert_send_sync::<subst::Substitution>();
367        assert_send_sync::<subst::TestFontDb>();
368        assert_send_sync::<subst::SystemFontDb>();
369        assert_send_sync::<FontCache>();
370    }
371
372    /// The cache exists so a font is loaded once per reference, and so every
373    /// later ask gets *that* instance: text extraction's duplicate
374    /// suppression compares fonts by pointer, so a fresh instance per page
375    /// would silently stop it firing.
376    #[test]
377    fn one_reference_loads_once_and_shares_the_instance() {
378        let cache = FontCache::new();
379        let reference = ObjRef::new(7, 0);
380        let mut loads = 0;
381
382        let first = cache
383            .get_or_load(reference, || {
384                loads += 1;
385                Some(Font::load_standard(StandardFont::Helvetica, &cache))
386            })
387            .expect("the standard font always loads");
388        let second = cache
389            .get_or_load(reference, || {
390                loads += 1;
391                Some(Font::load_standard(StandardFont::Helvetica, &cache))
392            })
393            .expect("the cached font is still there");
394
395        assert_eq!(loads, 1, "the second ask must not reach the loader");
396        assert!(
397            Arc::ptr_eq(&first, &second),
398            "both asks must yield one instance"
399        );
400    }
401
402    /// A different reference is a different font, and `None` is as cacheable
403    /// an answer as a font: a dictionary that will not load is stable.
404    #[test]
405    fn a_second_reference_loads_separately_and_a_failure_is_cached() {
406        let cache = FontCache::new();
407        let mut loads = 0;
408        let mut load = |cache: &FontCache, reference| {
409            cache.get_or_load(reference, || {
410                loads += 1;
411                Some(Font::load_standard(StandardFont::Helvetica, cache))
412            })
413        };
414
415        let first = load(&cache, ObjRef::new(7, 0)).expect("loads");
416        let second = load(&cache, ObjRef::new(8, 0)).expect("loads");
417        assert_eq!(loads, 2, "two references are two fonts");
418        assert!(!Arc::ptr_eq(&first, &second));
419
420        let mut failures = 0;
421        let missing = ObjRef::new(9, 0);
422        for _ in 0..2 {
423            assert!(
424                cache
425                    .get_or_load(missing, || {
426                        failures += 1;
427                        None
428                    })
429                    .is_none()
430            );
431        }
432        assert_eq!(failures, 1, "a failure is derived once, not per page");
433    }
434
435    /// The cache is what many threads share, so two threads asking at once
436    /// must both come away with a font and neither must panic.
437    #[test]
438    fn threads_sharing_one_cache_all_get_a_font() {
439        let cache = Arc::new(FontCache::new());
440        let reference = ObjRef::new(7, 0);
441        std::thread::scope(|scope| {
442            let handles: Vec<_> = (0..8)
443                .map(|_| {
444                    let cache = Arc::clone(&cache);
445                    scope.spawn(move || {
446                        cache
447                            .get_or_load(reference, || {
448                                Some(Font::load_standard(StandardFont::Helvetica, &cache))
449                            })
450                            .is_some()
451                    })
452                })
453                .collect();
454            for handle in handles {
455                assert!(handle.join().expect("no thread panics"));
456            }
457        });
458        // Whoever inserted first is the shared instance from then on, so a
459        // ninth ask after the race reaches no loader at all.
460        let after = cache.get_or_load(reference, || panic!("must be cached by now"));
461        assert!(after.is_some());
462    }
463}