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 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#[must_use]
61pub fn type1_font_file(bytes: &[u8]) -> Type1FontFile {
62 pdfrum_type1::font_file(bytes)
63}
64
65#[cfg(test)]
66mod tests {
67 #![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 assert!(font.glyph_path(Gid(0)).is_none());
127 }
128
129 #[test]
130 fn a_type0_font_without_descendants_fails_to_load() {
131 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 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 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 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 #[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 #[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 #[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 let after = cache.get_or_load(reference, || panic!("must be cached by now"));
415 assert!(after.is_some());
416 }
417}