Skip to content

Commit 50cab29

Browse files
codexVelli20
authored andcommitted
pdf-font: move dictionary parsing onto domain types
Move Type0 encoding, ToUnicode, CID subtype, CID ordering, fallback program, and Standard 14 parsing onto their owning types. Keep Type0 font loading focused on composing parsed font data. Co-authored-by: Codex <codex@openai.com>
1 parent b2add7e commit 50cab29

11 files changed

Lines changed: 185 additions & 249 deletions

File tree

crates/pdf-cmap/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use thiserror::Error;
33
/// Errors that can occur while parsing or resolving PDF CMaps.
44
#[derive(Debug, Error, PartialEq)]
55
pub enum CMapError {
6+
#[error("Object error while reading a CMap: {0}")]
7+
ObjectError(#[from] pdf_object::error::ObjectError),
68
#[error("Unsupported Type0 /Encoding CMap '{0}'")]
79
UnsupportedType0EncodingCMap(String),
810
#[error("Invalid Type0 /Encoding CMap: {0}")]

crates/pdf-cmap/src/predefined/mod.rs

Lines changed: 4 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -58,52 +58,6 @@ pub struct PredefinedCMap {
5858
maps: Vec<&'static GeneratedCMap>,
5959
}
6060

61-
/// Known Adobe CIDSystemInfo ordering values with bundled Unicode CMap support.
62-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63-
pub enum CidOrdering {
64-
/// Adobe-Japan1 character collection.
65-
Japan1,
66-
/// Adobe-GB1 character collection.
67-
GB1,
68-
/// Adobe-CNS1 character collection.
69-
CNS1,
70-
/// Adobe-Korea1 character collection.
71-
Korea1,
72-
}
73-
74-
impl CidOrdering {
75-
/// Resolve a known Adobe CIDSystemInfo ordering value.
76-
pub fn from_name(name: &str) -> Option<Self> {
77-
match name {
78-
"Japan1" => Some(Self::Japan1),
79-
"GB1" => Some(Self::GB1),
80-
"CNS1" => Some(Self::CNS1),
81-
"Korea1" => Some(Self::Korea1),
82-
_ => None,
83-
}
84-
}
85-
86-
/// Return whether an ordering name has bundled CJK fallback support.
87-
pub fn is_known_cjk_name(name: &str) -> bool {
88-
Self::from_name(name).is_some()
89-
}
90-
91-
/// Build a best-effort CID to Unicode map for this ordering.
92-
pub fn cid_to_unicode_map(self) -> Result<Option<HashMap<u16, char>>, CMapError> {
93-
Ok(PredefinedCMap::from_name(self.unicode_cmap_name())?
94-
.map(|cmap| cmap.cid_to_unicode_map()))
95-
}
96-
97-
fn unicode_cmap_name(self) -> &'static str {
98-
match self {
99-
Self::Japan1 => "UniJIS-UCS2-HW-H",
100-
Self::GB1 => "UniGB-UCS2-H",
101-
Self::CNS1 => "UniCNS-UCS2-H",
102-
Self::Korea1 => "UniKS-UCS2-H",
103-
}
104-
}
105-
}
106-
10761
impl PredefinedCMap {
10862
/// Resolve a predefined CMap by name.
10963
pub fn from_name(name: &str) -> Result<Option<Self>, CMapError> {
@@ -200,17 +154,6 @@ impl Type0CodeMap for PredefinedCMap {
200154
}
201155
}
202156

203-
/// Build a best-effort CID to Unicode map for a known Adobe CIDSystemInfo ordering.
204-
pub fn cid_to_unicode_map_for_ordering(
205-
ordering: &str,
206-
) -> Result<Option<HashMap<u16, char>>, CMapError> {
207-
let Some(ordering) = CidOrdering::from_name(ordering) else {
208-
return Ok(None);
209-
};
210-
211-
ordering.cid_to_unicode_map()
212-
}
213-
214157
/// Find a generated CMap by resource name.
215158
fn find_cmap(name: &str) -> Option<&'static GeneratedCMap> {
216159
generated::CMAPS
@@ -277,20 +220,12 @@ mod tests {
277220

278221
#[test]
279222
fn japan1_cid_to_unicode_includes_half_width_ascii_and_space_variants() {
280-
let map = cid_to_unicode_map_for_ordering("Japan1").unwrap().unwrap();
223+
let map = PredefinedCMap::from_name("UniJIS-UCS2-HW-H")
224+
.unwrap()
225+
.unwrap()
226+
.cid_to_unicode_map();
281227

282228
assert_eq!(map.get(&231), Some(&' '));
283229
assert_eq!(map.get(&633), Some(&'\u{2003}'));
284230
}
285-
286-
#[test]
287-
fn cid_ordering_rejects_unknown_orderings() {
288-
assert!(CidOrdering::from_name("Unknown").is_none());
289-
assert!(!CidOrdering::is_known_cjk_name("Unknown"));
290-
assert!(
291-
cid_to_unicode_map_for_ordering("Unknown")
292-
.unwrap()
293-
.is_none()
294-
);
295-
}
296231
}

crates/pdf-cmap/src/to_unicode.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
11
use std::collections::HashMap;
22

3+
use pdf_object::{dictionary::Dictionary, object_resolver::ObjectResolver};
4+
35
use crate::{cmap::parser::CMapParser, error::CMapError};
46

57
/// A parsed ToUnicode CMap that maps PDF character codes to Unicode scalar values.
68
#[derive(Debug)]
79
pub struct ToUnicodeCMap(HashMap<u16, Vec<char>>);
810

911
impl ToUnicodeCMap {
12+
/// Parse the optional `/ToUnicode` CMap from a font dictionary.
13+
pub fn from_dictionary(
14+
dictionary: &Dictionary,
15+
objects: &dyn ObjectResolver,
16+
) -> Result<Option<Self>, CMapError> {
17+
dictionary
18+
.get("ToUnicode")
19+
.and_then(|value| value.try_stream(objects).ok())
20+
.map(|stream| Self::try_from(stream.raw_data()))
21+
.transpose()
22+
}
23+
1024
/// Look up the Unicode characters for the given PDF character code.
1125
pub fn map_char_code(&self, code: u16) -> Option<&[char]> {
1226
self.0.get(&code).map(Vec::as_slice)

crates/pdf-cmap/src/type0/mod.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
33
use std::convert::TryFrom;
44

5-
use pdf_object::text_encoding::BigEndianU16Units;
5+
use pdf_object::{
6+
dictionary::Dictionary, object_resolver::ObjectResolver, object_variant::ObjectVariant,
7+
text_encoding::BigEndianU16Units,
8+
};
69

710
mod embedded;
811
mod parser;
@@ -25,6 +28,23 @@ pub enum Type0EncodingCMap {
2528
}
2629

2730
impl Type0EncodingCMap {
31+
/// Parse the optional `/Encoding` entry of a Type0 font dictionary.
32+
pub fn from_dictionary(
33+
dictionary: &Dictionary,
34+
objects: &dyn ObjectResolver,
35+
) -> Result<Option<Self>, CMapError> {
36+
dictionary
37+
.get("Encoding")
38+
.map(|value| {
39+
let resolved = objects.resolve_object(value)?;
40+
match resolved {
41+
ObjectVariant::Stream(stream) => Self::from_bytes(stream.raw_data()),
42+
_ => Self::from_name(value.try_str(objects)?),
43+
}
44+
})
45+
.transpose()
46+
}
47+
2848
/// Build a Type0 encoding CMap from a predefined CMap name.
2949
pub fn from_name(name: &str) -> Result<Self, CMapError> {
3050
if let Ok(writing_mode) = WritingMode::try_from(name) {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//! CIDFont subtype parsing.
2+
3+
use pdf_object::{
4+
dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver,
5+
};
6+
7+
use crate::error::FontError;
8+
9+
/// CIDFont subtypes supported by the parser.
10+
#[derive(Debug, Clone, Copy, PartialEq)]
11+
pub enum CidFontSubType {
12+
/// Type 1/CFF based CID-keyed font.
13+
Type0,
14+
/// TrueType based CID-keyed font.
15+
Type2,
16+
}
17+
18+
impl CidFontSubType {
19+
/// Parse the CIDFont subtype from a descendant font dictionary.
20+
pub fn from_dictionary(
21+
dictionary: &Dictionary,
22+
objects: &dyn ObjectResolver,
23+
) -> Result<Self, FontError> {
24+
match dictionary.required_str("Subtype", objects)? {
25+
"CIDFontType0" => Ok(Self::Type0),
26+
"CIDFontType2" => Ok(Self::Type2),
27+
other => Err(FontError::UnsupportedCidFontSubtype {
28+
subtype: other.to_string(),
29+
}),
30+
}
31+
}
32+
}
Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,72 @@
1-
use pdf_cmap::predefined::CidOrdering;
1+
use std::collections::HashMap;
2+
3+
use pdf_cmap::{error::CMapError, predefined::PredefinedCMap};
24
use pdf_object::{
35
dictionary::Dictionary, object_lookup::ObjectLookupExt, object_resolver::ObjectResolver,
46
};
57

68
use crate::error::FontError;
79

8-
/// Extract a supported CID ordering from a font dictionary's `/CIDSystemInfo`.
9-
///
10-
/// # Paramaters
11-
///
12-
/// - `dictionary`: The PDF font dictionary that may contain `/CIDSystemInfo`.
13-
/// - `objects`: The resolver used to dereference indirect PDF objects.
14-
///
15-
/// # Returns
16-
///
17-
/// A known [`CidOrdering`] when `/CIDSystemInfo /Ordering` is present and
18-
/// supported, or `None` when the entry is absent or unknown.
19-
pub(crate) fn cid_ordering_from_dictionary(
20-
dictionary: &Dictionary,
21-
objects: &dyn ObjectResolver,
22-
) -> Result<Option<CidOrdering>, FontError> {
23-
let Some(cid_system_info) = dictionary.optional_dictionary("CIDSystemInfo", objects)? else {
24-
return Ok(None);
25-
};
26-
27-
let Some(ordering) = cid_system_info.optional_str("Ordering", objects)? else {
28-
return Ok(None);
29-
};
30-
31-
Ok(CidOrdering::from_name(ordering))
10+
/// Known Adobe CIDSystemInfo ordering values with bundled Unicode CMap support.
11+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12+
pub(crate) enum CidOrdering {
13+
/// Adobe-Japan1 character collection.
14+
Japan1,
15+
/// Adobe-GB1 character collection.
16+
GB1,
17+
/// Adobe-CNS1 character collection.
18+
CNS1,
19+
/// Adobe-Korea1 character collection.
20+
Korea1,
21+
}
22+
23+
impl CidOrdering {
24+
/// Extract a supported CID ordering from a font dictionary's `/CIDSystemInfo`.
25+
///
26+
/// # Paramaters
27+
///
28+
/// - `dictionary`: The PDF font dictionary that may contain `/CIDSystemInfo`.
29+
/// - `objects`: The resolver used to dereference indirect PDF objects.
30+
///
31+
/// # Returns
32+
///
33+
/// A known [`CidOrdering`] when `/CIDSystemInfo /Ordering` is present and
34+
/// supported, or `None` when the entry is absent or unknown.
35+
pub(crate) fn from_dictionary(
36+
dictionary: &Dictionary,
37+
objects: &dyn ObjectResolver,
38+
) -> Result<Option<Self>, FontError> {
39+
let Some(cid_system_info) = dictionary.optional_dictionary("CIDSystemInfo", objects)?
40+
else {
41+
return Ok(None);
42+
};
43+
44+
let Some(ordering) = cid_system_info.optional_str("Ordering", objects)? else {
45+
return Ok(None);
46+
};
47+
48+
Ok(Self::from_name(ordering))
49+
}
50+
51+
/// Build a best-effort CID to Unicode map for this ordering.
52+
pub(crate) fn cid_to_unicode_map(self) -> Result<Option<HashMap<u16, char>>, CMapError> {
53+
let unicode_cmap_name = match self {
54+
Self::Japan1 => "UniJIS-UCS2-HW-H",
55+
Self::GB1 => "UniGB-UCS2-H",
56+
Self::CNS1 => "UniCNS-UCS2-H",
57+
Self::Korea1 => "UniKS-UCS2-H",
58+
};
59+
60+
Ok(PredefinedCMap::from_name(unicode_cmap_name)?.map(|cmap| cmap.cid_to_unicode_map()))
61+
}
62+
63+
fn from_name(name: &str) -> Option<Self> {
64+
match name {
65+
"Japan1" => Some(Self::Japan1),
66+
"GB1" => Some(Self::GB1),
67+
"CNS1" => Some(Self::CNS1),
68+
"Korea1" => Some(Self::Korea1),
69+
_ => None,
70+
}
71+
}
3272
}

crates/pdf-font/src/fallback.rs

Lines changed: 16 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use pdf_object::{
55
};
66

77
use crate::{
8-
cid_system_info::cid_ordering_from_dictionary,
8+
cid_system_info::CidOrdering,
99
encoding::{Encoding, FontEncoding},
1010
error::FontError,
1111
flags::FontFlags,
@@ -20,26 +20,18 @@ pub(crate) struct FallbackFontProgram {
2020
pub(crate) flags: FontFlags,
2121
}
2222

23-
/// Select fallback font bytes and metadata for a font dictionary.
24-
///
25-
/// # Paramaters
26-
///
27-
/// - `dictionary`: The PDF font dictionary that needs fallback font data.
28-
/// - `objects`: The resolver used to dereference indirect PDF objects.
29-
///
30-
/// # Returns
31-
///
32-
/// A fallback font program with borrowed bundled bytes, the selected Standard
33-
/// 14 identity, and descriptor flags.
34-
pub(crate) fn fallback_program_from_dictionary(
35-
dictionary: &Dictionary,
36-
objects: &dyn ObjectResolver,
37-
) -> Result<FallbackFontProgram, FontError> {
38-
let flags = descriptor_flags(dictionary, objects)?;
39-
let standard14 = standard14_from_dictionary(dictionary, objects, flags);
40-
let is_cjk = is_cjk_cid_font(dictionary, objects)?;
41-
42-
Ok(fallback_program(flags, standard14, is_cjk))
23+
impl FallbackFontProgram {
24+
/// Select fallback font bytes and metadata for a font dictionary.
25+
pub(crate) fn from_dictionary(
26+
dictionary: &Dictionary,
27+
objects: &dyn ObjectResolver,
28+
) -> Result<Self, FontError> {
29+
let flags = descriptor_flags(dictionary, objects)?;
30+
let standard14 = Standard14Font::from_dictionary(dictionary, objects, flags);
31+
let is_cjk = is_cjk_cid_font(dictionary, objects)?;
32+
33+
Ok(fallback_program(flags, standard14, is_cjk))
34+
}
4335
}
4436

4537
/// Build a synthetic TrueType font from fallback font data.
@@ -57,7 +49,7 @@ pub(crate) fn fallback_true_type_from_dictionary(
5749
dictionary: &Dictionary,
5850
objects: &dyn ObjectResolver,
5951
) -> Result<TrueTypeFont, FontError> {
60-
let fallback = fallback_program_from_dictionary(dictionary, objects)?;
52+
let fallback = FallbackFontProgram::from_dictionary(dictionary, objects)?;
6153
let widths = SimpleFontGlyphWidthsMap::from_dictionary(dictionary, objects)?;
6254
let encoding = simple_font_encoding(dictionary, objects);
6355
let to_unicode = to_unicode_cmap(dictionary, objects)?;
@@ -83,31 +75,14 @@ pub(crate) fn fallback_true_type_from_dictionary_best_effort(
8375
) -> TrueTypeFont {
8476
let flags = descriptor_flags(dictionary, objects).unwrap_or_default();
8577
let is_cjk = is_cjk_cid_font(dictionary, objects).unwrap_or(false);
86-
let standard14 = standard14_from_dictionary(dictionary, objects, flags);
78+
let standard14 = Standard14Font::from_dictionary(dictionary, objects, flags);
8779
let fallback = fallback_program(flags, standard14, is_cjk);
8880
let mut font = TrueTypeFont::from_bytes(fallback.font_file, Some(fallback.standard14));
8981
font.flags = fallback.flags;
9082

9183
font
9284
}
9385

94-
/// Resolve the Standard 14 identity to use for fallback substitution.
95-
///
96-
/// This prefers a readable `/BaseFont` name when it maps to a known Standard 14
97-
/// font. If `/BaseFont` is missing, malformed, or unrecognized, it falls back
98-
/// to the flag-driven Standard 14 selection.
99-
fn standard14_from_dictionary(
100-
dictionary: &Dictionary,
101-
objects: &dyn ObjectResolver,
102-
flags: FontFlags,
103-
) -> Standard14Font {
104-
dictionary
105-
.get("BaseFont")
106-
.and_then(|value| value.try_str(objects).ok())
107-
.and_then(Standard14Font::from_base_font_name)
108-
.unwrap_or_else(|| Standard14Font::from(flags))
109-
}
110-
11186
/// Build the fallback font program descriptor from already-decided inputs.
11287
///
11388
/// `standard14` selects the Standard 14 identity for simple-font fallback,
@@ -217,5 +192,5 @@ fn is_cjk_cid_font(
217192
dictionary: &Dictionary,
218193
objects: &dyn ObjectResolver,
219194
) -> Result<bool, FontError> {
220-
Ok(cid_ordering_from_dictionary(dictionary, objects)?.is_some())
195+
Ok(CidOrdering::from_dictionary(dictionary, objects)?.is_some())
221196
}

0 commit comments

Comments
 (0)