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