1use std::fmt;
2
3use rustc_hash::FxHashMap;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
6pub struct ObjectId {
7 pub number: u32,
8 pub generation: u16,
9}
10
11impl ObjectId {
12 pub fn new(number: u32, generation: u16) -> Self {
13 Self { number, generation }
14 }
15}
16
17impl fmt::Display for ObjectId {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 write!(f, "{} {} R", self.number, self.generation)
20 }
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24pub struct PdfName(pub String);
25
26impl PdfName {
27 pub fn new(s: &str) -> Self {
28 Self(s.to_string())
29 }
30
31 pub fn as_str(&self) -> &str {
32 &self.0
33 }
34}
35
36impl fmt::Display for PdfName {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 write!(f, "/{}", self.0)
39 }
40}
41
42#[derive(Debug, Clone, PartialEq)]
43pub struct PdfString(pub Vec<u8>);
44
45impl PdfString {
46 pub fn from_literal(s: &str) -> Self {
47 Self(s.as_bytes().to_vec())
48 }
49
50 pub fn from_bytes(bytes: &[u8]) -> Self {
51 Self(bytes.to_vec())
52 }
53
54 pub fn as_bytes(&self) -> &[u8] {
55 &self.0
56 }
57}
58
59pub fn escape_literal_string_into(out: &mut Vec<u8>, bytes: &[u8]) {
67 for &b in bytes {
68 match b {
69 b'(' => out.extend_from_slice(b"\\("),
70 b')' => out.extend_from_slice(b"\\)"),
71 b'\\' => out.extend_from_slice(b"\\\\"),
72 b'\n' => out.extend_from_slice(b"\\n"),
73 b'\r' => out.extend_from_slice(b"\\r"),
74 b'\t' => out.extend_from_slice(b"\\t"),
75 8 => out.extend_from_slice(b"\\b"),
76 12 => out.extend_from_slice(b"\\f"),
77 0x20..=0x7e => out.push(b),
78 b => {
79 out.push(b'\\');
81 out.push(b'0' + (b >> 6));
82 out.push(b'0' + ((b >> 3) & 7));
83 out.push(b'0' + (b & 7));
84 }
85 }
86 }
87}
88
89pub fn escape_literal_string(bytes: &[u8]) -> String {
92 let mut out = Vec::with_capacity(bytes.len() + 8);
93 escape_literal_string_into(&mut out, bytes);
94 String::from_utf8(out).expect("escaped literal string is ASCII")
96}
97
98impl fmt::Display for PdfString {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 write!(f, "({})", escape_literal_string(&self.0))
101 }
102}
103
104#[derive(Debug, Clone, PartialEq)]
105pub struct PdfArray(pub Vec<PdfObject>);
106
107impl PdfArray {
108 pub fn new() -> Self {
109 Self(Vec::new())
110 }
111
112 pub fn push(&mut self, obj: PdfObject) {
113 self.0.push(obj);
114 }
115
116 pub fn get(&self, index: usize) -> Option<&PdfObject> {
117 self.0.get(index)
118 }
119
120 pub fn len(&self) -> usize {
121 self.0.len()
122 }
123
124 pub fn is_empty(&self) -> bool {
125 self.0.is_empty()
126 }
127}
128
129impl Default for PdfArray {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135#[derive(Debug, Clone, PartialEq)]
141pub struct PdfDictionary {
142 entries: FxHashMap<PdfName, PdfObject>,
143}
144
145impl PdfDictionary {
146 pub fn new() -> Self {
147 Self {
148 entries: FxHashMap::default(),
149 }
150 }
151
152 pub fn insert(&mut self, key: &str, value: PdfObject) {
153 self.entries.insert(PdfName::new(key), value);
154 }
155
156 pub fn get(&self, key: &str) -> Option<&PdfObject> {
157 self.entries.get(&PdfName::new(key))
158 }
159
160 pub fn get_name(&self, key: &str) -> Option<&PdfName> {
161 match self.get(key) {
162 Some(PdfObject::Name(n)) => Some(n),
163 _ => None,
164 }
165 }
166
167 pub fn get_integer(&self, key: &str) -> Option<i64> {
168 match self.get(key) {
169 Some(PdfObject::Integer(i)) => Some(*i),
170 _ => None,
171 }
172 }
173
174 pub fn get_array(&self, key: &str) -> Option<&PdfArray> {
175 match self.get(key) {
176 Some(PdfObject::Array(a)) => Some(a),
177 _ => None,
178 }
179 }
180
181 pub fn get_dict(&self, key: &str) -> Option<&PdfDictionary> {
182 match self.get(key) {
183 Some(PdfObject::Dictionary(d)) => Some(d),
184 _ => None,
185 }
186 }
187
188 pub fn get_string_bytes(&self, key: &str) -> Option<&[u8]> {
190 self.get(key)
191 .and_then(|o| o.as_string())
192 .map(PdfString::as_bytes)
193 }
194
195 pub fn len(&self) -> usize {
196 self.entries.len()
197 }
198
199 pub fn is_empty(&self) -> bool {
200 self.entries.is_empty()
201 }
202
203 pub fn iter(&self) -> impl Iterator<Item = (&PdfName, &PdfObject)> {
204 self.entries.iter()
205 }
206}
207
208impl Default for PdfDictionary {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214#[derive(Debug, Clone, PartialEq)]
215pub struct PdfStream {
216 pub dictionary: PdfDictionary,
217 pub data: Vec<u8>,
218}
219
220impl PdfStream {
221 pub fn new(data: Vec<u8>) -> Self {
222 Self {
223 dictionary: PdfDictionary::new(),
224 data,
225 }
226 }
227
228 pub fn with_dict(dictionary: PdfDictionary, data: Vec<u8>) -> Self {
229 Self { dictionary, data }
230 }
231
232 pub fn length(&self) -> usize {
233 self.data.len()
234 }
235}
236
237#[derive(Debug, Clone, PartialEq)]
238pub enum PdfObject {
239 Null,
240 Boolean(bool),
241 Integer(i64),
242 Real(f64),
243 Name(PdfName),
244 String(PdfString),
245 Array(PdfArray),
246 Dictionary(PdfDictionary),
247 Stream(PdfStream),
248 Reference(ObjectId),
249}
250
251impl PdfObject {
252 pub fn is_null(&self) -> bool {
253 matches!(self, Self::Null)
254 }
255
256 pub fn as_bool(&self) -> Option<bool> {
257 match self {
258 Self::Boolean(b) => Some(*b),
259 _ => None,
260 }
261 }
262
263 pub fn as_integer(&self) -> Option<i64> {
264 match self {
265 Self::Integer(i) => Some(*i),
266 _ => None,
267 }
268 }
269
270 pub fn as_real(&self) -> Option<f64> {
271 match self {
272 Self::Real(r) => Some(*r),
273 Self::Integer(i) => Some(*i as f64),
274 _ => None,
275 }
276 }
277
278 pub fn as_name(&self) -> Option<&PdfName> {
279 match self {
280 Self::Name(n) => Some(n),
281 _ => None,
282 }
283 }
284
285 pub fn as_string(&self) -> Option<&PdfString> {
286 match self {
287 Self::String(s) => Some(s),
288 _ => None,
289 }
290 }
291
292 pub fn as_array(&self) -> Option<&PdfArray> {
293 match self {
294 Self::Array(a) => Some(a),
295 _ => None,
296 }
297 }
298
299 pub fn as_dict(&self) -> Option<&PdfDictionary> {
300 match self {
301 Self::Dictionary(d) => Some(d),
302 _ => None,
303 }
304 }
305
306 pub fn as_stream(&self) -> Option<&PdfStream> {
307 match self {
308 Self::Stream(s) => Some(s),
309 _ => None,
310 }
311 }
312
313 pub fn as_reference(&self) -> Option<ObjectId> {
314 match self {
315 Self::Reference(id) => Some(*id),
316 _ => None,
317 }
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn escape_literal_handles_all_special_bytes() {
327 let input: &[u8] = b"a(b)c\\d\ne\rf\t";
328 let escaped = escape_literal_string(input);
329 assert_eq!(escaped, "a\\(b\\)c\\\\d\\ne\\rf\\t");
330 }
331
332 #[test]
333 fn escape_literal_uses_octal_for_control_bytes() {
334 assert_eq!(
335 escape_literal_string(&[0x01, 0x07, 0x0b]),
336 "\\001\\007\\013"
337 );
338 }
339
340 #[test]
341 fn escape_literal_octal_escapes_non_ascii() {
342 assert_eq!(escape_literal_string(&[0x80]), "\\200");
345 assert_eq!(escape_literal_string(&[0xc3, 0xa9]), "\\303\\251");
346 assert_eq!(escape_literal_string(&[0xff]), "\\377");
347 }
348
349 #[test]
350 fn string_display_escapes_parens_and_backslash() {
351 let s = PdfString::from_literal("(a) \\ b");
352 assert_eq!(s.to_string(), "(\\(a\\) \\\\ b)");
353 }
354
355 #[test]
356 fn string_display_escapes_line_breaks() {
357 let s = PdfString::from_bytes(b"line1\nline2\r\nline3");
358 assert_eq!(s.to_string(), "(line1\\nline2\\r\\nline3)");
359 }
360}