1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4#![warn(clippy::indexing_slicing)]
7#![allow(
11 clippy::cast_possible_truncation,
12 clippy::cast_precision_loss,
13 clippy::cast_sign_loss,
14 clippy::cast_possible_wrap
15)]
16
17mod 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#[must_use]
65pub fn type1_font_file(bytes: &[u8]) -> Type1FontFile {
66 pdfrum_type1::font_file(bytes)
67}
68
69#[cfg(test)]
70mod tests {
71 #![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 assert!(font.glyph_path(Gid(0)).is_none());
131 }
132
133 #[test]
134 fn a_type0_font_without_descendants_fails_to_load() {
135 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 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 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 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 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 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 #[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 #[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 #[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 let after = cache.get_or_load(reference, || panic!("must be cached by now"));
461 assert!(after.is_some());
462 }
463}