Skip to main content

pdfboss_write/
ser.rs

1//! COS object serialization: `Object` values to PDF syntax bytes
2//! (ISO 32000 §7.3). Output is deterministic — dictionary keys are emitted
3//! in sorted order regardless of insertion order.
4//!
5//! Streams are deliberately absent here: a stream is only legal as an
6//! indirect object and its `/Length` bookkeeping belongs to the
7//! [`Writer`](crate::Writer), so a nested `Object::Stream` is an error.
8
9use pdfboss_core::{Dict, Name, Object};
10
11use crate::error::{Error, Result};
12
13/// Serializes any non-stream object into `out`.
14///
15/// Numbers are written without exponents; reals trim trailing zeros and
16/// never produce `-0`. Non-finite reals serialize as `0` (PDF has no
17/// representation for them).
18pub fn serialize_object(obj: &Object, out: &mut Vec<u8>) -> Result<()> {
19    match obj {
20        Object::Null => out.extend_from_slice(b"null"),
21        Object::Bool(true) => out.extend_from_slice(b"true"),
22        Object::Bool(false) => out.extend_from_slice(b"false"),
23        Object::Int(i) => out.extend_from_slice(i.to_string().as_bytes()),
24        Object::Real(r) => write_real(*r, out),
25        Object::String(bytes) => write_string(bytes, out),
26        Object::Name(n) => {
27            nul_free(n)?;
28            write_name(&n.0, out);
29        }
30        Object::Array(items) => {
31            out.push(b'[');
32            for (i, item) in items.iter().enumerate() {
33                if i > 0 {
34                    out.push(b' ');
35                }
36                serialize_object(item, out)?;
37            }
38            out.push(b']');
39        }
40        Object::Dict(d) => serialize_dict(d, out)?,
41        Object::Ref(r) => {
42            out.extend_from_slice(r.num.to_string().as_bytes());
43            out.push(b' ');
44            out.extend_from_slice(r.gen.to_string().as_bytes());
45            out.extend_from_slice(b" R");
46        }
47        Object::Stream(_) => return Err(Error::NestedStream),
48    }
49    Ok(())
50}
51
52/// Serializes a dictionary with `<< … >>` delimiters, keys sorted
53/// bytewise for deterministic output.
54pub fn serialize_dict(dict: &Dict, out: &mut Vec<u8>) -> Result<()> {
55    out.extend_from_slice(b"<<");
56    let mut entries: Vec<(&Name, &Object)> = dict.iter().collect();
57    entries.sort_unstable_by(|a, b| a.0.cmp(b.0));
58    for (key, value) in entries {
59        nul_free(key)?;
60        out.push(b' ');
61        write_name(&key.0, out);
62        out.push(b' ');
63        serialize_object(value, out)?;
64    }
65    out.extend_from_slice(b" >>");
66    Ok(())
67}
68
69/// Writes a name object with its leading solidus, escaping every byte
70/// outside the regular range as `#xx` (delimiters, whitespace, `#` itself,
71/// and anything outside `0x21..=0x7E`).
72pub fn write_name(name: &str, out: &mut Vec<u8>) {
73    out.push(b'/');
74    for &byte in name.as_bytes() {
75        if (0x21..=0x7E).contains(&byte) && !b"()<>[]{}/%#".contains(&byte) {
76            out.push(byte);
77            continue;
78        }
79        out.push(b'#');
80        push_hex(byte, out);
81    }
82}
83
84/// Writes a string object. Byte content that is printable ASCII (plus tab,
85/// newline and carriage return) uses the literal form with `\`-escapes;
86/// anything else uses the hex form. The choice is a pure function of the
87/// bytes, keeping output deterministic.
88pub fn write_string(bytes: &[u8], out: &mut Vec<u8>) {
89    let literal = bytes
90        .iter()
91        .all(|&b| (0x20..=0x7E).contains(&b) || matches!(b, b'\n' | b'\r' | b'\t'));
92    if !literal {
93        out.push(b'<');
94        for &byte in bytes {
95            push_hex(byte, out);
96        }
97        out.push(b'>');
98        return;
99    }
100    out.push(b'(');
101    for &byte in bytes {
102        match byte {
103            b'\\' | b'(' | b')' => {
104                out.push(b'\\');
105                out.push(byte);
106            }
107            b'\n' => out.extend_from_slice(b"\\n"),
108            b'\r' => out.extend_from_slice(b"\\r"),
109            b'\t' => out.extend_from_slice(b"\\t"),
110            other => out.push(other),
111        }
112    }
113    out.push(b')');
114}
115
116/// Writes a real number in plain decimal: no exponent, trailing zeros
117/// trimmed, `-0` normalized to `0`, non-finite values written as `0`.
118/// Whole values write as integers (`72` not `72.0`).
119pub fn write_real(value: f64, out: &mut Vec<u8>) {
120    if !value.is_finite() || value == 0.0 {
121        out.push(b'0');
122        return;
123    }
124    if value.fract() == 0.0 && value.abs() < 9_007_199_254_740_992.0 {
125        out.extend_from_slice((value as i64).to_string().as_bytes());
126        return;
127    }
128    out.extend_from_slice(plain_decimal(&value.to_string()).as_bytes());
129}
130
131/// Like [`write_real`], for `f32` values (content-stream operands): the
132/// shortest decimal that parses back to the identical `f32`.
133pub fn write_real_f32(value: f32, out: &mut Vec<u8>) {
134    if !value.is_finite() || value == 0.0 {
135        out.push(b'0');
136        return;
137    }
138    out.extend_from_slice(plain_decimal(&value.to_string()).as_bytes());
139}
140
141/// Rejects a name containing NUL: ISO 32000 forbids the `#00` escape, so
142/// such a name has no legal spelling. Object and dictionary serialization
143/// validate here; [`write_name`] itself stays infallible for callers whose
144/// names are structurally NUL-free (content-stream resource names).
145fn nul_free(name: &Name) -> Result<()> {
146    if name.0.bytes().all(|b| b != 0) {
147        return Ok(());
148    }
149    Err(Error::Other(format!(
150        "name {:?} contains NUL, which has no legal escape in a name",
151        name.0
152    )))
153}
154
155const HEX: &[u8; 16] = b"0123456789ABCDEF";
156
157fn push_hex(byte: u8, out: &mut Vec<u8>) {
158    out.push(HEX[usize::from(byte >> 4)]);
159    out.push(HEX[usize::from(byte & 0x0F)]);
160}
161
162fn plain_decimal(formatted: &str) -> String {
163    let expanded = match formatted.split_once(['e', 'E']) {
164        None => formatted.to_string(),
165        Some((mantissa, exponent)) => expand_exponent(mantissa, exponent),
166    };
167    if !expanded.contains('.') {
168        return expanded;
169    }
170    expanded
171        .trim_end_matches('0')
172        .trim_end_matches('.')
173        .to_string()
174}
175
176fn expand_exponent(mantissa: &str, exponent: &str) -> String {
177    let exp: i32 = exponent
178        .parse()
179        .expect("float formatting produced a malformed exponent");
180    let (sign, unsigned) = match mantissa.strip_prefix('-') {
181        Some(rest) => ("-", rest),
182        None => ("", mantissa),
183    };
184    let (int_part, frac_part) = match unsigned.split_once('.') {
185        Some(parts) => parts,
186        None => (unsigned, ""),
187    };
188    let digits = format!("{int_part}{frac_part}");
189    let point = int_part.len() as i32 + exp;
190    if point <= 0 {
191        let zeros = "0".repeat(point.unsigned_abs() as usize);
192        return format!("{sign}0.{zeros}{digits}");
193    }
194    let point = point as usize;
195    if point >= digits.len() {
196        let zeros = "0".repeat(point - digits.len());
197        return format!("{sign}{digits}{zeros}");
198    }
199    format!("{sign}{}.{}", &digits[..point], &digits[point..])
200}
201
202#[cfg(test)]
203mod tests {
204    use pdfboss_core::parser::{NoResolve, Parser};
205    use pdfboss_core::{Dict, Name, ObjRef, Object, Stream};
206
207    use super::*;
208    use crate::error::Error;
209
210    fn name(text: &str) -> Name {
211        Name(text.to_string())
212    }
213
214    fn ser(obj: &Object) -> String {
215        let mut out = Vec::new();
216        serialize_object(obj, &mut out).expect("serializable object");
217        String::from_utf8(out).expect("serialized syntax is UTF-8")
218    }
219
220    fn real_str(value: f64) -> String {
221        let mut out = Vec::new();
222        write_real(value, &mut out);
223        String::from_utf8(out).expect("real syntax is ASCII")
224    }
225
226    fn real32_str(value: f32) -> String {
227        let mut out = Vec::new();
228        write_real_f32(value, &mut out);
229        String::from_utf8(out).expect("real syntax is ASCII")
230    }
231
232    fn name_str(text: &str) -> String {
233        let mut out = Vec::new();
234        write_name(text, &mut out);
235        String::from_utf8(out).expect("name syntax is ASCII")
236    }
237
238    fn string_str(bytes: &[u8]) -> String {
239        let mut out = Vec::new();
240        write_string(bytes, &mut out);
241        String::from_utf8(out).expect("string syntax is ASCII")
242    }
243
244    #[test]
245    fn scalars_serialize_exactly() {
246        assert_eq!(ser(&Object::Null), "null");
247        assert_eq!(ser(&Object::Bool(true)), "true");
248        assert_eq!(ser(&Object::Bool(false)), "false");
249        assert_eq!(ser(&Object::Int(42)), "42");
250        assert_eq!(ser(&Object::Int(-7)), "-7");
251        assert_eq!(ser(&Object::Int(i64::MIN)), "-9223372036854775808");
252        assert_eq!(ser(&Object::Int(i64::MAX)), "9223372036854775807");
253    }
254
255    #[test]
256    fn real_pins_exact_bytes() {
257        assert_eq!(real_str(0.0), "0");
258        assert_eq!(real_str(-0.0), "0");
259        assert_eq!(real_str(f64::NAN), "0");
260        assert_eq!(real_str(f64::INFINITY), "0");
261        assert_eq!(real_str(f64::NEG_INFINITY), "0");
262        assert_eq!(real_str(72.0), "72");
263        assert_eq!(real_str(-72.0), "-72");
264        assert_eq!(real_str(0.5), "0.5");
265        assert_eq!(real_str(-0.25), "-0.25");
266        assert_eq!(real_str(0.1), "0.1");
267        assert_eq!(real_str(1e-7), "0.0000001");
268        assert_eq!(real_str(1.5e-10), "0.00000000015");
269        assert_eq!(real_str(9007199254740991.0), "9007199254740991");
270        assert_eq!(real_str(-9007199254740991.0), "-9007199254740991");
271        assert_eq!(real_str(9007199254740992.0), "9007199254740992");
272        assert_eq!(real_str(1e300), format!("1{}", "0".repeat(300)));
273    }
274
275    #[test]
276    fn real_f32_round_trips_hard_cases() {
277        let cases = [
278            0.1f32,
279            1e-7f32,
280            16777216.0f32,
281            f32::MIN_POSITIVE,
282            3.4e38f32,
283            1073741824.0f32,
284        ];
285        for value in cases {
286            let text = real32_str(value);
287            assert!(
288                !text.contains(['e', 'E']),
289                "{value} produced exponent form {text}"
290            );
291            let parsed: f32 = text.parse().expect("output parses as f32");
292            assert_eq!(parsed.to_bits(), value.to_bits(), "{value} -> {text}");
293        }
294        assert_eq!(real32_str(-0.0f32), "0");
295        assert_eq!(real32_str(f32::NAN), "0");
296        assert_eq!(real32_str(f32::INFINITY), "0");
297    }
298
299    #[test]
300    fn real_f32_pins_exact_bytes() {
301        assert_eq!(real32_str(0.1f32), "0.1");
302        assert_eq!(real32_str(1e-7f32), "0.0000001");
303        assert_eq!(real32_str(16777216.0f32), "16777216");
304        assert_eq!(real32_str(1073741824.0f32), "1073741800");
305        assert_eq!(real32_str(3.4e38f32), format!("34{}", "0".repeat(37)));
306        assert_eq!(
307            real32_str(f32::MIN_POSITIVE),
308            format!("0.{}11754944", "0".repeat(37))
309        );
310    }
311
312    #[test]
313    fn plain_decimal_expands_exponents() {
314        assert_eq!(plain_decimal("1e300"), format!("1{}", "0".repeat(300)));
315        assert_eq!(plain_decimal("3.4e38"), format!("34{}", "0".repeat(37)));
316        assert_eq!(
317            plain_decimal("1.1754944e-38"),
318            format!("0.{}11754944", "0".repeat(37))
319        );
320        assert_eq!(plain_decimal("-2.5e3"), "-2500");
321        assert_eq!(plain_decimal("1.25e2"), "125");
322        assert_eq!(plain_decimal("1.25e1"), "12.5");
323        assert_eq!(plain_decimal("1.25e-1"), "0.125");
324        assert_eq!(plain_decimal("1e-1"), "0.1");
325        assert_eq!(plain_decimal("2.5E3"), "2500");
326        assert_eq!(plain_decimal("0.5"), "0.5");
327        assert_eq!(plain_decimal("1.50"), "1.5");
328        assert_eq!(plain_decimal("2.0"), "2");
329    }
330
331    #[test]
332    fn names_escape_irregular_bytes() {
333        assert_eq!(name_str("Type"), "/Type");
334        assert_eq!(name_str(""), "/");
335        assert_eq!(name_str("A B"), "/A#20B");
336        assert_eq!(name_str("A#B"), "/A#23B");
337        assert_eq!(name_str("A(B)"), "/A#28B#29");
338        assert_eq!(name_str("a/b"), "/a#2Fb");
339        assert_eq!(name_str("x[y]z"), "/x#5By#5Dz");
340        assert_eq!(name_str("{}"), "/#7B#7D");
341        assert_eq!(name_str("<>"), "/#3C#3E");
342        assert_eq!(name_str("%"), "/#25");
343        assert_eq!(name_str("Ä"), "/#C3#84");
344        assert_eq!(name_str("é"), "/#C3#A9");
345        assert_eq!(name_str("\u{7F}"), "/#7F");
346        assert_eq!(name_str("~!$&*+-.;=?@^"), "/~!$&*+-.;=?@^");
347    }
348
349    #[test]
350    fn strings_use_literal_form_with_escapes() {
351        assert_eq!(string_str(b"Hello"), "(Hello)");
352        assert_eq!(string_str(b""), "()");
353        assert_eq!(string_str(b"a(b)c"), "(a\\(b\\)c)");
354        assert_eq!(string_str(b"a\\b"), "(a\\\\b)");
355        assert_eq!(string_str(b"a\tb\nc\rd"), "(a\\tb\\nc\\rd)");
356    }
357
358    #[test]
359    fn strings_fall_back_to_hex_form() {
360        assert_eq!(string_str(&[0x00, 0xFF, 0x41]), "<00FF41>");
361        assert_eq!(string_str(&[0x7F]), "<7F>");
362        assert_eq!(string_str(&[0x1F]), "<1F>");
363        let long: Vec<u8> = (0u8..=255).collect();
364        let hex: String = (0u8..=255).map(|b| format!("{b:02X}")).collect();
365        assert_eq!(string_str(&long), format!("<{hex}>"));
366    }
367
368    #[test]
369    fn arrays_separate_items_with_single_spaces() {
370        assert_eq!(ser(&Object::Array(vec![])), "[]");
371        let arr = Object::Array(vec![
372            Object::Int(1),
373            Object::Real(2.5),
374            Object::Name(name("X")),
375        ]);
376        assert_eq!(ser(&arr), "[1 2.5 /X]");
377        let nested = Object::Array(vec![
378            Object::Array(vec![Object::Int(1), Object::Int(2)]),
379            Object::Array(vec![Object::Int(3)]),
380        ]);
381        assert_eq!(ser(&nested), "[[1 2] [3]]");
382    }
383
384    #[test]
385    fn dicts_sort_keys_bytewise() {
386        assert_eq!(ser(&Object::Dict(Dict::new())), "<< >>");
387
388        let mut d = Dict::new();
389        d.insert(name("Z"), Object::Int(2));
390        d.insert(name("A"), Object::Int(1));
391        assert_eq!(ser(&Object::Dict(d)), "<< /A 1 /Z 2 >>");
392
393        let mut d = Dict::new();
394        d.insert(name("a"), Object::Int(4));
395        d.insert(name("B"), Object::Int(3));
396        d.insert(name("AB"), Object::Int(2));
397        d.insert(name("AA"), Object::Int(1));
398        assert_eq!(ser(&Object::Dict(d)), "<< /AA 1 /AB 2 /B 3 /a 4 >>");
399    }
400
401    #[test]
402    fn nested_containers_serialize_recursively() {
403        let mut inner = Dict::new();
404        inner.insert(name("X"), Object::Int(1));
405        let mut outer = Dict::new();
406        outer.insert(name("D"), Object::Dict(inner));
407        outer.insert(
408            name("Arr"),
409            Object::Array(vec![Object::Null, Object::Bool(false)]),
410        );
411        assert_eq!(
412            ser(&Object::Dict(outer)),
413            "<< /Arr [null false] /D << /X 1 >> >>"
414        );
415    }
416
417    #[test]
418    fn refs_serialize_as_num_gen_r() {
419        assert_eq!(ser(&Object::Ref(ObjRef { num: 12, gen: 3 })), "12 3 R");
420        assert_eq!(ser(&Object::Ref(ObjRef { num: 1, gen: 0 })), "1 0 R");
421    }
422
423    #[test]
424    fn nested_streams_are_errors() {
425        let stream = Object::Stream(Stream {
426            dict: Dict::new(),
427            data: b"x".to_vec(),
428        });
429        let mut out = Vec::new();
430        let top = serialize_object(&stream, &mut out);
431        assert!(matches!(top, Err(Error::NestedStream)));
432
433        let in_array = serialize_object(&Object::Array(vec![stream.clone()]), &mut out);
434        assert!(matches!(in_array, Err(Error::NestedStream)));
435
436        let mut d = Dict::new();
437        d.insert(name("S"), stream);
438        let in_dict = serialize_object(&Object::Dict(d), &mut out);
439        assert!(matches!(in_dict, Err(Error::NestedStream)));
440    }
441
442    #[test]
443    fn serialized_objects_parse_back_equal() {
444        let mut inner = Dict::new();
445        inner.insert(name("Z"), Object::Int(1));
446        inner.insert(
447            name("A"),
448            Object::Array(vec![Object::Real(0.5), Object::Null]),
449        );
450        let mut outer = Dict::new();
451        outer.insert(
452            name("Kids"),
453            Object::Array(vec![Object::Ref(ObjRef { num: 7, gen: 0 })]),
454        );
455        outer.insert(name("Inner"), Object::Dict(inner));
456        outer.insert(name("Weird Name#"), Object::String(b"a(b)\\c".to_vec()));
457
458        let objects = vec![
459            Object::Null,
460            Object::Bool(true),
461            Object::Bool(false),
462            Object::Int(-42),
463            Object::Real(2.5),
464            Object::Real(-0.125),
465            Object::String(b"a(b)\\c".to_vec()),
466            Object::String(b"tab\there".to_vec()),
467            Object::String(vec![0u8, 255, 128]),
468            Object::Name(name("Weird Name#\u{C4}")),
469            Object::Ref(ObjRef { num: 12, gen: 3 }),
470            Object::Array(vec![]),
471            Object::Dict(Dict::new()),
472            Object::Dict(outer),
473        ];
474        for obj in objects {
475            let mut out = Vec::new();
476            serialize_object(&obj, &mut out).expect("serializable object");
477            let parsed = Parser::new(&out)
478                .parse_object(&NoResolve)
479                .expect("serialized bytes parse");
480            assert_eq!(
481                parsed,
482                obj,
483                "round-trip of {}",
484                String::from_utf8_lossy(&out)
485            );
486        }
487    }
488
489    #[test]
490    fn integral_reals_parse_back_as_ints() {
491        let mut out = Vec::new();
492        serialize_object(&Object::Real(72.0), &mut out).expect("serializable object");
493        assert_eq!(out, b"72");
494        let parsed = Parser::new(&out)
495            .parse_object(&NoResolve)
496            .expect("serialized bytes parse");
497        assert_eq!(parsed, Object::Int(72));
498    }
499
500    #[test]
501    fn names_with_nul_bytes_are_rejected() {
502        let mut out = Vec::new();
503        let err = serialize_object(&Object::Name(name("a\0b")), &mut out)
504            .expect_err("a NUL in a name must not serialize");
505        assert!(err.to_string().contains("NUL"), "{err}");
506        let mut d = Dict::new();
507        d.insert(name("a\0b"), Object::Int(1));
508        let mut out = Vec::new();
509        assert!(serialize_dict(&d, &mut out).is_err());
510    }
511}