1use std::borrow::Cow;
2use std::fmt;
3
4use rustc_hash::FxHashMap;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct ObjectId {
8 pub number: u32,
9 pub generation: u16,
10}
11
12impl ObjectId {
13 pub fn new(number: u32, generation: u16) -> Self {
14 Self { number, generation }
15 }
16}
17
18impl fmt::Display for ObjectId {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 write!(f, "{} {} R", self.number, self.generation)
21 }
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25pub struct PdfName(Cow<'static, str>);
26
27const COMMON_KEYS: [&str; 44] = [
32 "Author",
33 "BaseFont",
34 "Catalog",
35 "CIDFontType2",
36 "CIDSystemInfo",
37 "Contents",
38 "Count",
39 "CreationDate",
40 "Creator",
41 "CropBox",
42 "DescendantFonts",
43 "Encoding",
44 "Encrypt",
45 "Filter",
46 "First",
47 "FirstChar",
48 "Font",
49 "FontDescriptor",
50 "FontFile2",
51 "Image",
52 "Index",
53 "Info",
54 "Kids",
55 "LastChar",
56 "Length",
57 "MediaBox",
58 "ModDate",
59 "N",
60 "Name",
61 "ObjStm",
62 "Ordering",
63 "Page",
64 "Pages",
65 "Parent",
66 "ProcSet",
67 "Producer",
68 "Registry",
69 "Resources",
70 "Root",
71 "Rotate",
72 "Size",
73 "Subtype",
74 "Supplement",
75 "Type",
76];
77
78impl PdfName {
79 pub fn new(s: &str) -> Self {
84 if COMMON_KEYS.binary_search(&s).is_ok() {
85 let idx = COMMON_KEYS.binary_search(&s).unwrap();
86 return Self(Cow::Borrowed(COMMON_KEYS[idx]));
87 }
88 Self(Cow::Owned(s.to_string()))
89 }
90
91 pub fn as_str(&self) -> &str {
92 self.0.as_ref()
93 }
94}
95
96impl fmt::Display for PdfName {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 write!(f, "/{}", self.0)
99 }
100}
101
102#[derive(Debug, Clone, PartialEq)]
103pub struct PdfString(pub Vec<u8>);
104
105impl PdfString {
106 pub fn from_literal(s: &str) -> Self {
107 Self(s.as_bytes().to_vec())
108 }
109
110 pub fn from_bytes(bytes: &[u8]) -> Self {
111 Self(bytes.to_vec())
112 }
113
114 pub fn as_bytes(&self) -> &[u8] {
115 &self.0
116 }
117}
118
119pub fn escape_literal_string_into(out: &mut Vec<u8>, bytes: &[u8]) {
127 for &b in bytes {
128 match b {
129 b'(' => out.extend_from_slice(b"\\("),
130 b')' => out.extend_from_slice(b"\\)"),
131 b'\\' => out.extend_from_slice(b"\\\\"),
132 b'\n' => out.extend_from_slice(b"\\n"),
133 b'\r' => out.extend_from_slice(b"\\r"),
134 b'\t' => out.extend_from_slice(b"\\t"),
135 8 => out.extend_from_slice(b"\\b"),
136 12 => out.extend_from_slice(b"\\f"),
137 0x20..=0x7e => out.push(b),
138 b => {
139 out.push(b'\\');
141 out.push(b'0' + (b >> 6));
142 out.push(b'0' + ((b >> 3) & 7));
143 out.push(b'0' + (b & 7));
144 }
145 }
146 }
147}
148
149pub fn escape_literal_string(bytes: &[u8]) -> String {
152 let mut out = Vec::with_capacity(bytes.len() + 8);
153 escape_literal_string_into(&mut out, bytes);
154 String::from_utf8(out).expect("escaped literal string is ASCII")
156}
157
158impl fmt::Display for PdfString {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 write!(f, "({})", escape_literal_string(&self.0))
161 }
162}
163
164#[derive(Debug, Clone, PartialEq)]
165pub struct PdfArray(pub Vec<PdfObject>);
166
167impl PdfArray {
168 pub fn new() -> Self {
169 Self(Vec::new())
170 }
171
172 pub fn with_capacity(capacity: usize) -> Self {
175 Self(Vec::with_capacity(capacity))
176 }
177
178 pub fn push(&mut self, obj: PdfObject) {
179 self.0.push(obj);
180 }
181
182 pub fn get(&self, index: usize) -> Option<&PdfObject> {
183 self.0.get(index)
184 }
185
186 pub fn len(&self) -> usize {
187 self.0.len()
188 }
189
190 pub fn is_empty(&self) -> bool {
191 self.0.is_empty()
192 }
193}
194
195impl Default for PdfArray {
196 fn default() -> Self {
197 Self::new()
198 }
199}
200
201#[derive(Debug, Clone, PartialEq)]
207pub struct PdfDictionary {
208 entries: FxHashMap<PdfName, PdfObject>,
209}
210
211impl PdfDictionary {
212 pub fn new() -> Self {
213 Self {
214 entries: FxHashMap::default(),
215 }
216 }
217
218 pub fn with_capacity(capacity: usize) -> Self {
222 Self {
223 entries: FxHashMap::with_capacity_and_hasher(capacity, Default::default()),
224 }
225 }
226
227 pub fn insert(&mut self, key: &str, value: PdfObject) {
228 self.entries.insert(PdfName::new(key), value);
229 }
230
231 pub fn get(&self, key: &str) -> Option<&PdfObject> {
232 self.entries.get(&PdfName::new(key))
233 }
234
235 pub fn get_name(&self, key: &str) -> Option<&PdfName> {
236 match self.get(key) {
237 Some(PdfObject::Name(n)) => Some(n),
238 _ => None,
239 }
240 }
241
242 pub fn get_integer(&self, key: &str) -> Option<i64> {
243 match self.get(key) {
244 Some(PdfObject::Integer(i)) => Some(*i),
245 _ => None,
246 }
247 }
248
249 pub fn get_array(&self, key: &str) -> Option<&PdfArray> {
250 match self.get(key) {
251 Some(PdfObject::Array(a)) => Some(a),
252 _ => None,
253 }
254 }
255
256 pub fn get_dict(&self, key: &str) -> Option<&PdfDictionary> {
257 match self.get(key) {
258 Some(PdfObject::Dictionary(d)) => Some(d),
259 _ => None,
260 }
261 }
262
263 pub fn get_string_bytes(&self, key: &str) -> Option<&[u8]> {
265 self.get(key)
266 .and_then(|o| o.as_string())
267 .map(PdfString::as_bytes)
268 }
269
270 pub fn len(&self) -> usize {
271 self.entries.len()
272 }
273
274 pub fn is_empty(&self) -> bool {
275 self.entries.is_empty()
276 }
277
278 pub fn iter(&self) -> impl Iterator<Item = (&PdfName, &PdfObject)> {
279 self.entries.iter()
280 }
281}
282
283impl Default for PdfDictionary {
284 fn default() -> Self {
285 Self::new()
286 }
287}
288
289#[derive(Debug, Clone, PartialEq)]
290pub struct PdfStream {
291 pub dictionary: PdfDictionary,
292 pub data: Vec<u8>,
293}
294
295impl PdfStream {
296 pub fn new(data: Vec<u8>) -> Self {
297 Self {
298 dictionary: PdfDictionary::new(),
299 data,
300 }
301 }
302
303 pub fn with_dict(dictionary: PdfDictionary, data: Vec<u8>) -> Self {
304 Self { dictionary, data }
305 }
306
307 pub fn length(&self) -> usize {
308 self.data.len()
309 }
310}
311
312#[derive(Debug, Clone, PartialEq)]
313pub enum PdfObject {
314 Null,
315 Boolean(bool),
316 Integer(i64),
317 Real(f64),
318 Name(PdfName),
319 String(PdfString),
320 Array(PdfArray),
321 Dictionary(PdfDictionary),
322 Stream(PdfStream),
323 Reference(ObjectId),
324}
325
326impl PdfObject {
327 pub fn is_null(&self) -> bool {
328 matches!(self, Self::Null)
329 }
330
331 pub fn as_bool(&self) -> Option<bool> {
332 match self {
333 Self::Boolean(b) => Some(*b),
334 _ => None,
335 }
336 }
337
338 pub fn as_integer(&self) -> Option<i64> {
339 match self {
340 Self::Integer(i) => Some(*i),
341 _ => None,
342 }
343 }
344
345 pub fn as_real(&self) -> Option<f64> {
346 match self {
347 Self::Real(r) => Some(*r),
348 Self::Integer(i) => Some(*i as f64),
349 _ => None,
350 }
351 }
352
353 pub fn as_name(&self) -> Option<&PdfName> {
354 match self {
355 Self::Name(n) => Some(n),
356 _ => None,
357 }
358 }
359
360 pub fn as_string(&self) -> Option<&PdfString> {
361 match self {
362 Self::String(s) => Some(s),
363 _ => None,
364 }
365 }
366
367 pub fn as_array(&self) -> Option<&PdfArray> {
368 match self {
369 Self::Array(a) => Some(a),
370 _ => None,
371 }
372 }
373
374 pub fn as_dict(&self) -> Option<&PdfDictionary> {
375 match self {
376 Self::Dictionary(d) => Some(d),
377 _ => None,
378 }
379 }
380
381 pub fn as_stream(&self) -> Option<&PdfStream> {
382 match self {
383 Self::Stream(s) => Some(s),
384 _ => None,
385 }
386 }
387
388 pub fn as_reference(&self) -> Option<ObjectId> {
389 match self {
390 Self::Reference(id) => Some(*id),
391 _ => None,
392 }
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
401 fn escape_literal_handles_all_special_bytes() {
402 let input: &[u8] = b"a(b)c\\d\ne\rf\t";
403 let escaped = escape_literal_string(input);
404 assert_eq!(escaped, "a\\(b\\)c\\\\d\\ne\\rf\\t");
405 }
406
407 #[test]
408 fn escape_literal_uses_octal_for_control_bytes() {
409 assert_eq!(
410 escape_literal_string(&[0x01, 0x07, 0x0b]),
411 "\\001\\007\\013"
412 );
413 }
414
415 #[test]
416 fn escape_literal_octal_escapes_non_ascii() {
417 assert_eq!(escape_literal_string(&[0x80]), "\\200");
420 assert_eq!(escape_literal_string(&[0xc3, 0xa9]), "\\303\\251");
421 assert_eq!(escape_literal_string(&[0xff]), "\\377");
422 }
423
424 #[test]
425 fn string_display_escapes_parens_and_backslash() {
426 let s = PdfString::from_literal("(a) \\ b");
427 assert_eq!(s.to_string(), "(\\(a\\) \\\\ b)");
428 }
429
430 #[test]
431 fn string_display_escapes_line_breaks() {
432 let s = PdfString::from_bytes(b"line1\nline2\r\nline3");
433 assert_eq!(s.to_string(), "(line1\\nline2\\r\\nline3)");
434 }
435}