Skip to main content

otf_pdf_syntax/
serialize.rs

1//! Writing objects back as PDF syntax.
2//!
3//! Output is deterministic (dictionary keys are sorted, numbers have one
4//! spelling) so the same object always serializes to the same bytes.
5
6use crate::lexer::is_regular;
7use crate::object::{Dict, Name, Object, PdfString};
8
9/// Appends `object` in PDF syntax. A stream is written as its dictionary
10/// followed by `stream`, its raw bytes and `endstream`.
11pub fn write_object(object: &Object, out: &mut Vec<u8>) {
12    match object {
13        Object::Null => out.extend_from_slice(b"null"),
14        Object::Bool(true) => out.extend_from_slice(b"true"),
15        Object::Bool(false) => out.extend_from_slice(b"false"),
16        Object::Int(i) => out.extend_from_slice(i.to_string().as_bytes()),
17        Object::Real(r) => write_real(*r, out),
18        Object::String(s) => write_string(s, out),
19        Object::Name(n) => write_name(n, out),
20        Object::Array(items) => {
21            out.push(b'[');
22            for (i, item) in items.iter().enumerate() {
23                if i > 0 {
24                    out.push(b' ');
25                }
26                write_object(item, out);
27            }
28            out.push(b']');
29        }
30        Object::Dict(d) => write_dict(d, out),
31        Object::Stream(s) => {
32            let mut dict = s.dict.clone();
33            dict.insert("Length", s.raw.len() as i64);
34            write_dict(&dict, out);
35            out.extend_from_slice(b"\nstream\n");
36            out.extend_from_slice(&s.raw);
37            out.extend_from_slice(b"\nendstream");
38        }
39        Object::Ref(id) => {
40            out.extend_from_slice(format!("{} {} R", id.num, id.generation).as_bytes())
41        }
42    }
43}
44
45/// `object` in PDF syntax, as a byte vector.
46pub fn to_bytes(object: &Object) -> Vec<u8> {
47    let mut out = Vec::new();
48    write_object(object, &mut out);
49    out
50}
51
52fn write_dict(dict: &Dict, out: &mut Vec<u8>) {
53    out.extend_from_slice(b"<<");
54    for (key, value) in dict.iter() {
55        write_name(key, out);
56        out.push(b' ');
57        write_object(value, out);
58        out.push(b' ');
59    }
60    out.extend_from_slice(b">>");
61}
62
63/// A real with up to six decimals, no exponent, no trailing zeros; a
64/// non-finite value (which PDF cannot express) is written as 0.
65fn write_real(r: f64, out: &mut Vec<u8>) {
66    if !r.is_finite() {
67        out.push(b'0');
68        return;
69    }
70    let s = format!("{r:.6}");
71    let s = s.trim_end_matches('0').trim_end_matches('.');
72    let s = if s == "-0" || s.is_empty() { "0" } else { s };
73    out.extend_from_slice(s.as_bytes());
74}
75
76fn write_name(name: &Name, out: &mut Vec<u8>) {
77    out.push(b'/');
78    for &b in name.as_bytes() {
79        if is_regular(b) && b != b'#' && (b'!'..=b'~').contains(&b) {
80            out.push(b);
81        } else {
82            out.extend_from_slice(format!("#{b:02X}").as_bytes());
83        }
84    }
85}
86
87fn write_string(s: &PdfString, out: &mut Vec<u8>) {
88    // Hex when it was hex, or when mostly unprintable (shorter and clearer).
89    let unprintable = s
90        .bytes
91        .iter()
92        .filter(|&&b| !(0x20..0x7f).contains(&b))
93        .count();
94    if s.hex || unprintable * 4 > s.bytes.len() {
95        out.push(b'<');
96        for b in &s.bytes {
97            out.extend_from_slice(format!("{b:02x}").as_bytes());
98        }
99        out.push(b'>');
100        return;
101    }
102    out.push(b'(');
103    for &b in &s.bytes {
104        match b {
105            b'(' | b')' | b'\\' => out.extend_from_slice(&[b'\\', b]),
106            b'\n' => out.extend_from_slice(b"\\n"),
107            b'\r' => out.extend_from_slice(b"\\r"),
108            0x20..=0x7e => out.push(b),
109            _ => out.extend_from_slice(format!("\\{b:03o}").as_bytes()),
110        }
111    }
112    out.push(b')');
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::limits::{Limits, Strictness};
119    use crate::object::{ObjId, Stream};
120    use crate::parser::Parser;
121
122    fn text(o: &Object) -> String {
123        String::from_utf8(to_bytes(o)).unwrap()
124    }
125
126    fn reparse(o: &Object) -> Object {
127        let limits = Limits::default();
128        Parser::new(&to_bytes(o), 0, &limits, Strictness::Strict)
129            .parse_object()
130            .unwrap()
131    }
132
133    #[test]
134    fn scalars() {
135        assert_eq!(text(&Object::Null), "null");
136        assert_eq!(text(&Object::Bool(false)), "false");
137        assert_eq!(text(&Object::Int(-3)), "-3");
138        assert_eq!(text(&Object::Real(0.5)), "0.5");
139        assert_eq!(text(&Object::Real(2.0)), "2");
140        assert_eq!(text(&Object::Real(-0.0000001)), "0");
141        assert_eq!(text(&Object::Real(f64::NAN)), "0");
142        assert_eq!(text(&Object::Real(1e20)), "100000000000000000000");
143        assert_eq!(text(&Object::Ref(ObjId::new(4, 1))), "4 1 R");
144    }
145
146    #[test]
147    fn names_are_escaped() {
148        assert_eq!(
149            text(&Object::Name(Name::from("A B#(c)"))),
150            "/A#20B#23#28c#29"
151        );
152        assert_eq!(text(&Object::Name(Name::new(vec![0xe9]))), "/#E9");
153    }
154
155    #[test]
156    fn strings() {
157        assert_eq!(
158            text(&Object::String(PdfString::literal("a(b)\\\n"))),
159            "(a\\(b\\)\\\\\\n)"
160        );
161        assert_eq!(
162            text(&Object::String(PdfString::literal(vec![
163                b'a', 0xe9, b'b', b'c', b'd'
164            ]))),
165            "(a\\351bcd)"
166        );
167        assert_eq!(
168            text(&Object::String(PdfString::literal(vec![0, 1, 2]))),
169            "<000102>"
170        );
171        assert_eq!(text(&Object::String(PdfString::hex("A"))), "<41>");
172    }
173
174    #[test]
175    fn containers_round_trip() {
176        let mut d = Dict::new();
177        d.insert("Type", Name::from("Page"));
178        d.insert(
179            "Kids",
180            vec![Object::Ref(ObjId::new(1, 0)), Object::Real(1.25)],
181        );
182        d.insert("S", PdfString::literal(b"(a\xffbc)".to_vec()));
183        let o = Object::Dict(d);
184        assert_eq!(reparse(&o), o);
185        assert_eq!(
186            text(&o),
187            "<</Kids [1 0 R 1.25] /S (\\(a\\377bc\\)) /Type /Page >>"
188        );
189    }
190
191    #[test]
192    fn streams_carry_their_length() {
193        let s = Object::Stream(Stream {
194            dict: Dict::new(),
195            raw: b"abc".to_vec().into(),
196        });
197        assert_eq!(text(&s), "<</Length 3 >>\nstream\nabc\nendstream");
198    }
199}