Skip to main content

pdfboss_write/
writer.rs

1//! The object-level PDF writer: numbered objects in, finished file bytes
2//! out. Handles the header, stream `/Length` bookkeeping, optional Flate
3//! compression, object streams, both cross-reference styles, the trailer,
4//! a deterministic `/ID`, and optional AES-256 encryption on the way out.
5//!
6//! Determinism contract: the same sequence of calls with the same options
7//! produces byte-identical output; the `/ID` derives from a SHA-256 of the
8//! emitted body. Unencrypted output reads no clock or RNG. Encrypted
9//! output is deterministic too, but only under a caller-supplied
10//! deterministic RNG. [`Encryptor::aes256`] draws from the operating
11//! system's random source instead, exactly like every other real-world
12//! encryption key.
13
14use std::io::Write;
15
16use flate2::write::ZlibEncoder;
17use flate2::Compression;
18use pdfboss_core::crypt::Sha256;
19use pdfboss_core::{block_on, Dict, Encryptor, Name, ObjRef, Object, Stream};
20
21use crate::error::{Error, Result};
22use crate::ser::{serialize_dict, serialize_object};
23use crate::sink::{AsyncByteSink, Immediate};
24
25/// Which cross-reference flavor `finish` emits.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum XrefStyle {
28    /// A classic `xref` table with a `trailer` dictionary (readable by
29    /// PDF 1.0-era consumers).
30    Table,
31    /// A cross-reference stream (`/Type /XRef`, PDF 1.5+), the compact
32    /// modern form.
33    #[default]
34    Stream,
35}
36
37/// Options governing file emission.
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub struct WriteOptions {
40    /// Cross-reference flavor. Object streams require [`XrefStyle::Stream`].
41    pub xref: XrefStyle,
42    /// Flate-compress stream data that carries no filter of its own.
43    pub compress: bool,
44    /// Pack non-stream objects into object streams (only effective with
45    /// [`XrefStyle::Stream`]).
46    pub object_streams: bool,
47    /// PDF version written in the header.
48    pub version: (u8, u8),
49}
50
51impl Default for WriteOptions {
52    fn default() -> WriteOptions {
53        WriteOptions {
54            xref: XrefStyle::Stream,
55            compress: true,
56            object_streams: true,
57            version: (1, 7),
58        }
59    }
60}
61
62/// Accumulates numbered objects and serializes them into a complete PDF
63/// file. Objects are numbered in the order they are first claimed
64/// (`put`, `put_stream`, or `reserve`), starting at 1, generation 0.
65pub struct Writer {
66    options: WriteOptions,
67    slots: Vec<Slot>,
68    info: Option<ObjRef>,
69    /// Set by [`Writer::new_encrypted`]: the encryptor every emitted
70    /// object (save the `/Encrypt` dictionary and the `/Type /XRef`
71    /// stream) is run through, plus the complete `/Encrypt` dictionary
72    /// itself.
73    encryption: Option<(Encryptor, Dict)>,
74}
75
76impl std::fmt::Debug for Writer {
77    /// [`Encryptor`] holds a boxed RNG closure with no useful `Debug`, so
78    /// this reports only whether encryption is configured.
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        f.debug_struct("Writer")
81            .field("options", &self.options)
82            .field("slots", &self.slots)
83            .field("info", &self.info)
84            .field("encrypted", &self.encryption.is_some())
85            .finish()
86    }
87}
88
89/// One numbered object: reserved, or holding its body.
90#[derive(Debug)]
91enum Slot {
92    Reserved,
93    Filled(Object),
94}
95
96impl Writer {
97    /// Creates a writer with the given options.
98    pub fn new(options: WriteOptions) -> Writer {
99        Writer {
100            options,
101            slots: Vec::new(),
102            info: None,
103            encryption: None,
104        }
105    }
106
107    /// Creates a writer that encrypts every object it emits under
108    /// `encryptor`, except the `/Encrypt` dictionary itself, the `/ID`
109    /// trailer strings, and the `/Type /XRef` cross-reference stream
110    /// (ISO 32000-2 §7.6.2, which never encrypts the handshake objects a
111    /// reader needs before it has a file key). `encrypt_dict` is the
112    /// complete `/Encrypt` dictionary to place in the trailer; both
113    /// values come from [`Encryptor::aes256`] or
114    /// [`Encryptor::aes256_with_rng`]. `finish` reserves one extra object
115    /// number for `encrypt_dict` and adds the trailer's `/Encrypt` entry;
116    /// `WriteOptions` itself carries no encryption state.
117    pub fn new_encrypted(
118        options: WriteOptions,
119        encryptor: Encryptor,
120        encrypt_dict: Dict,
121    ) -> Writer {
122        Writer {
123            options,
124            slots: Vec::new(),
125            info: None,
126            encryption: Some((encryptor, encrypt_dict)),
127        }
128    }
129
130    /// Claims an object number now to be filled later — for cycles like
131    /// the page tree, where children point at a parent not yet built.
132    pub fn reserve(&mut self) -> ObjRef {
133        self.push(Slot::Reserved)
134    }
135
136    /// Adds a complete object and returns its reference.
137    pub fn put(&mut self, obj: Object) -> ObjRef {
138        self.push(Slot::Filled(obj))
139    }
140
141    /// Adds a stream object. `/Length` is computed on emission; when
142    /// [`WriteOptions::compress`] is set and `dict` names no `/Filter`,
143    /// the data is Flate-compressed and `/Filter /FlateDecode` added.
144    pub fn put_stream(&mut self, mut dict: Dict, data: Vec<u8>) -> ObjRef {
145        let data = compress_into(&mut dict, data, self.options.compress);
146        self.push(Slot::Filled(Object::Stream(Stream { dict, data })))
147    }
148
149    /// Adds a stream object without touching its filters — for data that
150    /// is already encoded (e.g. a JPEG passed through as `/DCTDecode`,
151    /// with `/Filter` set by the caller). `/Length` is still computed.
152    pub fn put_stream_raw(&mut self, dict: Dict, data: Vec<u8>) -> ObjRef {
153        self.push(Slot::Filled(Object::Stream(Stream { dict, data })))
154    }
155
156    /// Fills a previously [`reserve`](Writer::reserve)d object.
157    pub fn fill(&mut self, r: ObjRef, obj: Object) -> Result<()> {
158        if r.gen != 0 {
159            return Err(Error::Other(format!(
160                "cannot fill {} {} R: this writer only issues generation 0",
161                r.num, r.gen
162            )));
163        }
164        if r.num == 0 || r.num as usize > self.slots.len() {
165            return Err(Error::Other(format!(
166                "cannot fill {} 0 R: this writer never allocated that object number",
167                r.num
168            )));
169        }
170        let slot = &mut self.slots[r.num as usize - 1];
171        if matches!(slot, Slot::Filled(_)) {
172            return Err(Error::AlreadyFilled(r));
173        }
174        *slot = Slot::Filled(obj);
175        Ok(())
176    }
177
178    fn push(&mut self, slot: Slot) -> ObjRef {
179        self.slots.push(slot);
180        ObjRef {
181            num: self.slots.len() as u32,
182            gen: 0,
183        }
184    }
185
186    /// Registers the document information dictionary for the trailer.
187    pub fn set_info(&mut self, info: ObjRef) {
188        self.info = Some(info);
189    }
190
191    /// The [`WriteOptions::compress`] value this writer was created with.
192    pub fn compress(&self) -> bool {
193        self.options.compress
194    }
195
196    /// Serializes everything into a complete PDF file: header with binary
197    /// comment, all objects (packed into object streams where options
198    /// allow), the cross-reference, and the trailer with `root`, the
199    /// registered info dictionary, and a `/ID` pair derived from a
200    /// SHA-256 of the emitted body.
201    pub fn finish(self, root: ObjRef) -> Result<Vec<u8>> {
202        block_on(self.finish_into_with(root, Vec::new()))
203    }
204
205    /// [`Writer::finish`] streaming into a [`std::io::Write`]: the same
206    /// bytes, delivered in bounded chunks, so the whole file never sits in
207    /// one buffer. Unlike `finish`, an error can leave a prefix of the
208    /// file already written to `out`. No flush is performed.
209    pub fn finish_into(self, root: ObjRef, out: impl Write) -> Result<()> {
210        block_on(self.finish_into_with(root, Immediate(out)))?;
211        Ok(())
212    }
213
214    /// [`Writer::finish`] streaming into any [`AsyncByteSink`] — the
215    /// asynchronous twin of [`Writer::finish_into`], and the one emission
216    /// implementation all three finishes drive. Bytes arrive in bounded
217    /// chunks (per header, object and cross-reference section; a stream's
218    /// data is its own chunk). An error can leave a prefix of the file
219    /// already written. Hands the sink back unflushed.
220    pub async fn finish_into_with<S: AsyncByteSink>(self, root: ObjRef, sink: S) -> Result<S> {
221        let Writer {
222            options,
223            slots,
224            info,
225            mut encryption,
226        } = self;
227        let mut bodies = Vec::with_capacity(slots.len());
228        for (index, slot) in slots.into_iter().enumerate() {
229            match slot {
230                Slot::Reserved => {
231                    return Err(Error::Unfilled(ObjRef {
232                        num: index as u32 + 1,
233                        gen: 0,
234                    }))
235                }
236                Slot::Filled(obj) => bodies.push(obj),
237            }
238        }
239        let (encryptor, encrypt_dict) = split_encryption(&mut encryption);
240        let mut emit = Emit::new(sink);
241        match options.xref {
242            XrefStyle::Table => {
243                emit_table(
244                    options,
245                    &bodies,
246                    root,
247                    info,
248                    encryptor,
249                    encrypt_dict,
250                    &mut emit,
251                )
252                .await?
253            }
254            XrefStyle::Stream => {
255                emit_stream(
256                    options,
257                    &bodies,
258                    root,
259                    info,
260                    encryptor,
261                    encrypt_dict,
262                    &mut emit,
263                )
264                .await?
265            }
266        }
267        Ok(emit.sink)
268    }
269}
270
271/// Splits an owned `(Encryptor, Dict)` pair into the mutable encryptor
272/// borrow `emit_table`/`emit_stream` hold through every `write_indirect`
273/// call (the RNG draws a fresh IV per string and stream) and a shared
274/// borrow of the dict, cloned once into its own object when the trailer
275/// needs it.
276fn split_encryption(
277    encryption: &mut Option<(Encryptor, Dict)>,
278) -> (Option<&mut Encryptor>, Option<&Dict>) {
279    match encryption {
280        Some((enc, dict)) => (Some(enc), Some(&*dict)),
281        None => (None, None),
282    }
283}
284
285/// Counts and hashes every byte on its way to the sink: cross-reference
286/// offsets come from `count` and the `/ID` digest from `hasher`, so
287/// emission never needs the finished file in one buffer.
288struct Emit<S> {
289    sink: S,
290    count: usize,
291    hasher: Sha256,
292}
293
294impl<S: AsyncByteSink> Emit<S> {
295    fn new(sink: S) -> Emit<S> {
296        Emit {
297            sink,
298            count: 0,
299            hasher: Sha256::new(),
300        }
301    }
302
303    async fn write(&mut self, bytes: &[u8]) -> Result<()> {
304        self.hasher.update(bytes);
305        self.count += bytes.len();
306        self.sink.write_all(bytes).await
307    }
308
309    /// The `/ID` array at this point of emission: two identical 16-byte
310    /// strings from a SHA-256 of every byte written so far.
311    fn file_id(&self) -> Object {
312        let digest = self.hasher.clone().finalize();
313        let id = Object::String(digest[..16].to_vec());
314        Object::Array(vec![id.clone(), id])
315    }
316}
317
318/// Objects a single object stream may hold before the next one starts.
319const OBJSTM_CAPACITY: usize = 200;
320
321/// One cross-reference row for the stream flavor: a top-level object at a
322/// byte offset (type 1) or an object packed into an object stream (type 2).
323#[derive(Clone, Copy)]
324enum Row {
325    Top(u32),
326    Packed { container: u32, index: u16 },
327}
328
329/// Emits the classic-table flavor: bodies, the `/Encrypt` dictionary when
330/// encrypting (its own extra object number, always last, never itself
331/// encrypted), `xref` table, `trailer` dictionary, `startxref` and `%%EOF`.
332async fn emit_table<S: AsyncByteSink>(
333    options: WriteOptions,
334    bodies: &[Object],
335    root: ObjRef,
336    info: Option<ObjRef>,
337    mut encryptor: Option<&mut Encryptor>,
338    encrypt_dict: Option<&Dict>,
339    emit: &mut Emit<S>,
340) -> Result<()> {
341    let mut head = Vec::new();
342    write_header(&mut head, options.version);
343    emit.write(&head).await?;
344    let mut offsets = Vec::with_capacity(bodies.len() + 1);
345    for (index, body) in bodies.iter().enumerate() {
346        offsets.push(emit.count);
347        let num = index as u32 + 1;
348        write_indirect_maybe_encrypted(emit, num, body, encryptor.as_deref_mut()).await?;
349    }
350    let mut encrypt_ref = None;
351    if let Some(dict) = encrypt_dict {
352        let num = bodies.len() as u32 + 1;
353        offsets.push(emit.count);
354        write_indirect(emit, num, &Object::Dict(dict.clone())).await?;
355        encrypt_ref = Some(ObjRef { num, gen: 0 });
356    }
357    let id = emit.file_id();
358    let xref_off = emit.count;
359    let mut section = format!("xref\n0 {}\n", offsets.len() + 1).into_bytes();
360    section.extend_from_slice(b"0000000000 65535 f\r\n");
361    let size = offsets.len() as i64 + 1;
362    for offset in offsets {
363        let offset = table_offset(offset)?;
364        section.extend_from_slice(format!("{offset:010} 00000 n\r\n").as_bytes());
365    }
366    section.extend_from_slice(b"trailer\n");
367    let mut trailer = trailer_dict(size, root, info, id);
368    if let Some(r) = encrypt_ref {
369        trailer.insert(literal("Encrypt"), Object::Ref(r));
370    }
371    serialize_dict(&trailer, &mut section)?;
372    section.extend_from_slice(format!("\nstartxref\n{xref_off}\n%%EOF").as_bytes());
373    emit.write(&section).await
374}
375
376/// Emits the cross-reference-stream flavor: bodies (non-stream objects
377/// packed into object streams when the option is set), then the
378/// `/Encrypt` dictionary when encrypting, then a `/Type /XRef` stream as
379/// the last object, `startxref` and `%%EOF`. Object numbering is dense
380/// from 0 through the xref stream itself, so `/Index` is never needed.
381/// Neither the `/Encrypt` dictionary nor the xref stream is ever
382/// encrypted; an object-stream container is encrypted as a whole, after
383/// [`build_objstm`] packs its members in plaintext.
384async fn emit_stream<S: AsyncByteSink>(
385    options: WriteOptions,
386    bodies: &[Object],
387    root: ObjRef,
388    info: Option<ObjRef>,
389    mut encryptor: Option<&mut Encryptor>,
390    encrypt_dict: Option<&Dict>,
391    emit: &mut Emit<S>,
392) -> Result<()> {
393    let mut head = Vec::new();
394    write_header(&mut head, options.version);
395    emit.write(&head).await?;
396    let user_count = bodies.len() as u32;
397
398    let packed: Vec<u32> = if options.object_streams {
399        bodies
400            .iter()
401            .enumerate()
402            .filter(|(_, body)| !matches!(body, Object::Stream(_)))
403            .map(|(index, _)| index as u32 + 1)
404            .collect()
405    } else {
406        Vec::new()
407    };
408    let chunks: Vec<&[u32]> = packed.chunks(OBJSTM_CAPACITY).collect();
409
410    let mut rows: Vec<Row> = vec![Row::Top(0); bodies.len()];
411    for (c, chunk) in chunks.iter().enumerate() {
412        for (index, num) in chunk.iter().enumerate() {
413            rows[*num as usize - 1] = Row::Packed {
414                container: user_count + c as u32 + 1,
415                index: index as u16,
416            };
417        }
418    }
419
420    for (index, body) in bodies.iter().enumerate() {
421        if matches!(rows[index], Row::Packed { .. }) {
422            continue;
423        }
424        rows[index] = Row::Top(field_offset(emit.count)?);
425        let num = index as u32 + 1;
426        write_indirect_maybe_encrypted(emit, num, body, encryptor.as_deref_mut()).await?;
427    }
428
429    let mut container_offsets = Vec::with_capacity(chunks.len());
430    for (c, chunk) in chunks.iter().enumerate() {
431        let pairs: Vec<(u32, &Object)> = chunk
432            .iter()
433            .map(|&num| (num, &bodies[num as usize - 1]))
434            .collect();
435        let container = build_objstm(&pairs, options.compress)?;
436        container_offsets.push(field_offset(emit.count)?);
437        let num = user_count + c as u32 + 1;
438        let mut container = Object::Stream(container);
439        if let Some(encryptor) = encryptor.as_deref_mut() {
440            encryptor.encrypt_object(&mut container, num, 0);
441        }
442        write_indirect(emit, num, &container).await?;
443    }
444
445    let mut next_num = user_count + chunks.len() as u32 + 1;
446    let mut encrypt_row = None;
447    if let Some(dict) = encrypt_dict {
448        let offset = field_offset(emit.count)?;
449        write_indirect(emit, next_num, &Object::Dict(dict.clone())).await?;
450        encrypt_row = Some((
451            ObjRef {
452                num: next_num,
453                gen: 0,
454            },
455            offset,
456        ));
457        next_num += 1;
458    }
459    let xref_num = next_num;
460    let id = emit.file_id();
461    let xref_off = field_offset(emit.count)?;
462
463    let mut data = Vec::with_capacity(7 * (xref_num as usize + 1));
464    push_row(&mut data, 0, 0, 65535);
465    for row in rows {
466        match row {
467            Row::Top(offset) => push_row(&mut data, 1, offset, 0),
468            Row::Packed { container, index } => push_row(&mut data, 2, container, index),
469        }
470    }
471    for offset in container_offsets {
472        push_row(&mut data, 1, offset, 0);
473    }
474    if let Some((_, offset)) = encrypt_row {
475        push_row(&mut data, 1, offset, 0);
476    }
477    push_row(&mut data, 1, xref_off, 0);
478
479    let mut dict = trailer_dict(xref_num as i64 + 1, root, info, id);
480    if let Some((r, _)) = encrypt_row {
481        dict.insert(literal("Encrypt"), Object::Ref(r));
482    }
483    dict.insert(literal("Type"), Object::Name(literal("XRef")));
484    dict.insert(
485        literal("W"),
486        Object::Array(vec![Object::Int(1), Object::Int(4), Object::Int(2)]),
487    );
488    let data = compress_into(&mut dict, data, options.compress);
489    write_indirect(emit, xref_num, &Object::Stream(Stream { dict, data })).await?;
490    emit.write(format!("startxref\n{xref_off}\n%%EOF").as_bytes())
491        .await
492}
493
494/// `%PDF-M.m` plus the binary comment marking the file as 8-bit data.
495fn write_header(out: &mut Vec<u8>, version: (u8, u8)) {
496    out.extend_from_slice(format!("%PDF-{}.{}\n", version.0, version.1).as_bytes());
497    out.extend_from_slice(b"%\xE2\xE3\xCF\xD3\n");
498}
499
500/// Emits `num 0 obj` through `endobj`. Every top-level `Object::Stream` —
501/// however it entered the writer — is framed as a stream with a direct
502/// `/Length` of its stored byte count; everything else serializes through
503/// [`crate::ser`]. A stream's data goes to the sink as its own chunk,
504/// borrowed rather than copied; everything else is one chunk per object.
505async fn write_indirect<S: AsyncByteSink>(
506    emit: &mut Emit<S>,
507    num: u32,
508    obj: &Object,
509) -> Result<()> {
510    let mut lead = format!("{num} 0 obj\n").into_bytes();
511    match obj {
512        Object::Stream(s) => {
513            let mut dict = s.dict.clone();
514            dict.insert(literal("Length"), Object::Int(s.data.len() as i64));
515            serialize_dict(&dict, &mut lead)?;
516            lead.extend_from_slice(b"\nstream\n");
517            emit.write(&lead).await?;
518            emit.write(&s.data).await?;
519            emit.write(b"\nendstream\nendobj\n").await
520        }
521        direct => {
522            serialize_object(direct, &mut lead)?;
523            lead.extend_from_slice(b"\nendobj\n");
524            emit.write(&lead).await
525        }
526    }
527}
528
529/// [`write_indirect`], first encrypting a clone of `obj` under `encryptor`
530/// when set. Every body a writer holds passes through here except the
531/// `/Encrypt` dictionary and the `/Type /XRef` stream, which call
532/// `write_indirect` directly and so stay exempt structurally rather than
533/// by a runtime check.
534async fn write_indirect_maybe_encrypted<S: AsyncByteSink>(
535    emit: &mut Emit<S>,
536    num: u32,
537    obj: &Object,
538    encryptor: Option<&mut Encryptor>,
539) -> Result<()> {
540    match encryptor {
541        Some(encryptor) => {
542            let mut obj = obj.clone();
543            encryptor.encrypt_object(&mut obj, num, 0);
544            write_indirect(emit, num, &obj).await
545        }
546        None => write_indirect(emit, num, obj).await,
547    }
548}
549
550/// Serializes one object-stream container from `(num, body)` pairs: `2·N`
551/// header integers, then the bodies, each followed by a space so adjacent
552/// tokens cannot fuse.
553fn build_objstm(pairs: &[(u32, &Object)], compress: bool) -> Result<Stream> {
554    let mut header = Vec::new();
555    let mut payload = Vec::new();
556    for (num, body) in pairs {
557        header.extend_from_slice(format!("{num} {} ", payload.len()).as_bytes());
558        serialize_object(body, &mut payload)?;
559        payload.push(b' ');
560    }
561    let mut dict = Dict::new();
562    dict.insert(literal("Type"), Object::Name(literal("ObjStm")));
563    dict.insert(literal("N"), Object::Int(pairs.len() as i64));
564    dict.insert(literal("First"), Object::Int(header.len() as i64));
565    header.extend_from_slice(&payload);
566    let data = compress_into(&mut dict, header, compress);
567    Ok(Stream { dict, data })
568}
569
570/// The shared trailer entries: `/Size`, `/Root`, the optional `/Info`, and
571/// the `/ID` pair.
572fn trailer_dict(size: i64, root: ObjRef, info: Option<ObjRef>, id: Object) -> Dict {
573    let mut trailer = Dict::new();
574    trailer.insert(literal("Size"), Object::Int(size));
575    trailer.insert(literal("Root"), Object::Ref(root));
576    if let Some(info) = info {
577        trailer.insert(literal("Info"), Object::Ref(info));
578    }
579    trailer.insert(literal("ID"), id);
580    trailer
581}
582
583/// Flate-compresses `data` and records `/Filter /FlateDecode` in `dict`
584/// when `compress` is set and the dictionary names no filter of its own;
585/// otherwise the data passes through untouched.
586fn compress_into(dict: &mut Dict, data: Vec<u8>, compress: bool) -> Vec<u8> {
587    if !compress || dict.get("Filter").is_some() {
588        return data;
589    }
590    dict.insert(literal("Filter"), Object::Name(literal("FlateDecode")));
591    let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
592    encoder
593        .write_all(&data)
594        .expect("writing into a Vec cannot fail");
595    encoder
596        .finish()
597        .expect("finishing an in-memory zlib stream cannot fail")
598}
599
600/// One `[1 4 2]` cross-reference-stream row.
601fn push_row(rows: &mut Vec<u8>, kind: u8, second: u32, third: u16) {
602    rows.push(kind);
603    rows.extend_from_slice(&second.to_be_bytes());
604    rows.extend_from_slice(&third.to_be_bytes());
605}
606
607/// A byte position as the 4-byte offset field of the cross-reference.
608fn field_offset(position: usize) -> Result<u32> {
609    u32::try_from(position)
610        .map_err(|_| Error::Other("file offset exceeds the 4-byte xref field".to_string()))
611}
612
613/// A byte position as the 10-digit offset field of a classic xref table
614/// (ISO 32000-1 §7.5.4 mandates exactly-20-byte entries; a wider offset
615/// would silently desynchronize every later entry).
616fn table_offset(position: usize) -> Result<usize> {
617    if position as u64 <= 9_999_999_999 {
618        return Ok(position);
619    }
620    Err(Error::Other(
621        "file offset exceeds the 10-digit xref table field".to_string(),
622    ))
623}
624
625/// A `Name` from a string literal.
626fn literal(text: &str) -> Name {
627    Name(text.to_string())
628}
629
630#[cfg(test)]
631mod tests {
632    use pdfboss_core::xref::load_xref;
633    use pdfboss_core::{Dict, Document, Name, ObjRef, Object, Stream};
634
635    use super::*;
636    use crate::error::Error;
637
638    const CONTENT: &[u8] = b"BT /F1 12 Tf 72 720 Td (Hello, writer) Tj ET";
639
640    fn name(text: &str) -> Name {
641        Name(text.to_string())
642    }
643
644    fn table_options() -> WriteOptions {
645        WriteOptions {
646            xref: XrefStyle::Table,
647            compress: false,
648            object_streams: false,
649            version: (1, 7),
650        }
651    }
652
653    fn stream_options() -> WriteOptions {
654        WriteOptions {
655            xref: XrefStyle::Stream,
656            compress: false,
657            object_streams: false,
658            version: (1, 5),
659        }
660    }
661
662    fn objstm_options() -> WriteOptions {
663        WriteOptions {
664            xref: XrefStyle::Stream,
665            compress: true,
666            object_streams: true,
667            version: (1, 7),
668        }
669    }
670
671    struct Refs {
672        content: ObjRef,
673        pages: ObjRef,
674        page: ObjRef,
675        root: ObjRef,
676    }
677
678    fn page_dict(pages: ObjRef, content: ObjRef) -> Dict {
679        let mut page = Dict::new();
680        page.insert(name("Type"), Object::Name(name("Page")));
681        page.insert(name("Parent"), Object::Ref(pages));
682        page.insert(
683            name("MediaBox"),
684            Object::Array(vec![
685                Object::Int(0),
686                Object::Int(0),
687                Object::Int(612),
688                Object::Int(792),
689            ]),
690        );
691        page.insert(name("Contents"), Object::Ref(content));
692        page
693    }
694
695    /// Builds a one-page document around the given content stream without
696    /// finishing it, going through `put_stream_raw` when `raw` is set.
697    fn build(
698        options: WriteOptions,
699        content_dict: Dict,
700        content_data: Vec<u8>,
701        raw: bool,
702    ) -> (Writer, Refs) {
703        let mut w = Writer::new(options);
704        let content = if raw {
705            w.put_stream_raw(content_dict, content_data)
706        } else {
707            w.put_stream(content_dict, content_data)
708        };
709        let pages = w.reserve();
710        let page = w.put(Object::Dict(page_dict(pages, content)));
711        let mut tree = Dict::new();
712        tree.insert(name("Type"), Object::Name(name("Pages")));
713        tree.insert(name("Kids"), Object::Array(vec![Object::Ref(page)]));
714        tree.insert(name("Count"), Object::Int(1));
715        w.fill(pages, Object::Dict(tree))
716            .expect("pages slot is fillable");
717        let mut catalog = Dict::new();
718        catalog.insert(name("Type"), Object::Name(name("Catalog")));
719        catalog.insert(name("Pages"), Object::Ref(pages));
720        let root = w.put(Object::Dict(catalog));
721        (
722            w,
723            Refs {
724                content,
725                pages,
726                page,
727                root,
728            },
729        )
730    }
731
732    /// [`build`], finished into bytes.
733    fn skeleton(
734        options: WriteOptions,
735        content_dict: Dict,
736        content_data: Vec<u8>,
737        raw: bool,
738    ) -> (Vec<u8>, Refs) {
739        let (w, refs) = build(options, content_dict, content_data, raw);
740        let bytes = w.finish(refs.root).expect("minimal document finishes");
741        (bytes, refs)
742    }
743
744    fn minimal_pdf(options: WriteOptions) -> (Vec<u8>, Refs) {
745        skeleton(options, Dict::new(), CONTENT.to_vec(), false)
746    }
747
748    fn assert_minimal_loads(bytes: &[u8], refs: &Refs) -> Document {
749        let doc = Document::load(bytes.to_vec()).expect("document loads");
750        assert_eq!(doc.page_count(), 1);
751        let page = doc.page(0).expect("page 0 exists");
752        assert_eq!(page.object_ref(), Some(refs.page));
753        assert_eq!(page.dict(), &page_dict(refs.pages, refs.content));
754        assert_eq!(page.content(&doc).expect("content decodes"), CONTENT);
755        doc
756    }
757
758    fn count_occurrences(haystack: &[u8], needle: &[u8]) -> usize {
759        haystack
760            .windows(needle.len())
761            .filter(|window| *window == needle)
762            .count()
763    }
764
765    #[test]
766    fn table_mode_minimal_document_loads() {
767        let (bytes, refs) = minimal_pdf(table_options());
768        assert!(bytes.starts_with(b"%PDF-1.7\n%\xE2\xE3\xCF\xD3\n"));
769        assert!(bytes.ends_with(b"%%EOF"));
770        assert_eq!(count_occurrences(&bytes, b"xref\n0 5\n"), 1);
771        assert_eq!(count_occurrences(&bytes, b"0000000000 65535 f\r\n"), 1);
772        assert_eq!(count_occurrences(&bytes, b"trailer\n"), 1);
773        assert_minimal_loads(&bytes, &refs);
774    }
775
776    #[test]
777    fn table_mode_ignores_object_streams_option() {
778        let options = WriteOptions {
779            object_streams: true,
780            ..table_options()
781        };
782        let (bytes, refs) = minimal_pdf(options);
783        assert_eq!(count_occurrences(&bytes, b"/ObjStm"), 0);
784        assert_minimal_loads(&bytes, &refs);
785    }
786
787    #[test]
788    fn stream_mode_minimal_document_loads() {
789        let (bytes, refs) = minimal_pdf(stream_options());
790        assert!(bytes.starts_with(b"%PDF-1.5\n%\xE2\xE3\xCF\xD3\n"));
791        assert!(bytes.ends_with(b"%%EOF"));
792        assert_eq!(count_occurrences(&bytes, b"/ObjStm"), 0);
793        assert_eq!(count_occurrences(&bytes, b"/XRef"), 1);
794        assert_minimal_loads(&bytes, &refs);
795    }
796
797    #[test]
798    fn object_streams_pack_and_resolve() {
799        let (bytes, refs) = minimal_pdf(objstm_options());
800        assert_eq!(count_occurrences(&bytes, b"/ObjStm"), 1);
801        let doc = assert_minimal_loads(&bytes, &refs);
802        let root = doc
803            .resolve(&Object::Ref(refs.root))
804            .expect("catalog resolves");
805        let catalog = root.as_dict().expect("catalog is a dictionary");
806        assert_eq!(catalog.get_name("Type"), Some(&name("Catalog")));
807        assert_eq!(catalog.get_ref("Pages"), Some(refs.pages));
808    }
809
810    #[test]
811    fn object_streams_chunk_at_two_hundred() {
812        let options = WriteOptions {
813            compress: false,
814            ..objstm_options()
815        };
816        let mut w = Writer::new(options);
817        let content = w.put_stream(Dict::new(), CONTENT.to_vec());
818        let int_refs: Vec<ObjRef> = (0..205).map(|i| w.put(Object::Int(i))).collect();
819        let pages = w.reserve();
820        let page = w.put(Object::Dict(page_dict(pages, content)));
821        let mut tree = Dict::new();
822        tree.insert(name("Type"), Object::Name(name("Pages")));
823        tree.insert(name("Kids"), Object::Array(vec![Object::Ref(page)]));
824        tree.insert(name("Count"), Object::Int(1));
825        w.fill(pages, Object::Dict(tree))
826            .expect("pages slot is fillable");
827        let mut catalog = Dict::new();
828        catalog.insert(name("Type"), Object::Name(name("Catalog")));
829        catalog.insert(name("Pages"), Object::Ref(pages));
830        let root = w.put(Object::Dict(catalog));
831        let bytes = w.finish(root).expect("document finishes");
832        assert_eq!(count_occurrences(&bytes, b"/ObjStm"), 2);
833        let doc = Document::load(bytes).expect("document loads");
834        assert_eq!(
835            doc.resolve(&Object::Ref(int_refs[0])).expect("resolves"),
836            Object::Int(0)
837        );
838        assert_eq!(
839            doc.resolve(&Object::Ref(int_refs[204])).expect("resolves"),
840            Object::Int(204)
841        );
842        assert_eq!(doc.page_count(), 1);
843    }
844
845    #[test]
846    fn refs_ascend_from_one_in_call_order() {
847        let mut w = Writer::new(table_options());
848        assert_eq!(w.put(Object::Null), ObjRef { num: 1, gen: 0 });
849        assert_eq!(w.reserve(), ObjRef { num: 2, gen: 0 });
850        assert_eq!(
851            w.put_stream(Dict::new(), Vec::new()),
852            ObjRef { num: 3, gen: 0 }
853        );
854        assert_eq!(
855            w.put_stream_raw(Dict::new(), Vec::new()),
856            ObjRef { num: 4, gen: 0 }
857        );
858    }
859
860    #[test]
861    fn fill_twice_reports_already_filled() {
862        let mut w = Writer::new(table_options());
863        let r = w.reserve();
864        w.fill(r, Object::Int(1)).expect("first fill lands");
865        match w.fill(r, Object::Int(2)) {
866            Err(Error::AlreadyFilled(seen)) => assert_eq!(seen, r),
867            other => panic!("expected AlreadyFilled, got {other:?}"),
868        }
869    }
870
871    #[test]
872    fn fill_rejects_foreign_and_wrong_generation_refs() {
873        let mut w = Writer::new(table_options());
874        let r = w.reserve();
875        let unallocated = w.fill(ObjRef { num: 99, gen: 0 }, Object::Null);
876        assert!(matches!(unallocated, Err(Error::Other(msg)) if msg.contains("99")));
877        let zero = w.fill(ObjRef { num: 0, gen: 0 }, Object::Null);
878        assert!(matches!(zero, Err(Error::Other(msg)) if !msg.is_empty()));
879        let wrong_gen = w.fill(ObjRef { num: r.num, gen: 1 }, Object::Null);
880        assert!(matches!(wrong_gen, Err(Error::Other(msg)) if msg.contains("generation")));
881    }
882
883    #[test]
884    fn finish_with_unfilled_reserve_reports_the_ref() {
885        let mut w = Writer::new(table_options());
886        let root = w.put(Object::Dict(Dict::new()));
887        let reserved = w.reserve();
888        match w.finish(root) {
889            Err(Error::Unfilled(seen)) => assert_eq!(seen, reserved),
890            other => panic!("expected Unfilled, got {other:?}"),
891        }
892    }
893
894    #[test]
895    fn nested_stream_surfaces_from_finish() {
896        for options in [table_options(), objstm_options()] {
897            let mut w = Writer::new(options);
898            let root = w.put(Object::Array(vec![Object::Stream(Stream {
899                dict: Dict::new(),
900                data: b"x".to_vec(),
901            })]));
902            assert!(matches!(w.finish(root), Err(Error::NestedStream)));
903        }
904    }
905
906    #[test]
907    fn compressed_stream_round_trips() {
908        let options = WriteOptions {
909            compress: true,
910            ..table_options()
911        };
912        let data: Vec<u8> = b"q 0.5 0 0 0.5 36 36 cm Q\n".repeat(40);
913        let (bytes, refs) = skeleton(options, Dict::new(), data.clone(), false);
914        let doc = Document::load(bytes).expect("document loads");
915        let resolved = doc
916            .resolve(&Object::Ref(refs.content))
917            .expect("content stream resolves");
918        let stream = resolved.as_stream().expect("content is a stream");
919        assert_eq!(stream.dict.get_name("Filter"), Some(&name("FlateDecode")));
920        assert_eq!(
921            stream.dict.get_int("Length"),
922            Some(stream.data.len() as i64)
923        );
924        assert!(stream.data.len() < data.len());
925        assert_eq!(doc.stream_data(stream).expect("stream decodes"), data);
926    }
927
928    #[test]
929    fn preset_filter_is_not_recompressed() {
930        let options = WriteOptions {
931            compress: true,
932            ..table_options()
933        };
934        let payload = b"Hello writer";
935        let encoded: Vec<u8> = payload
936            .iter()
937            .flat_map(|b| format!("{b:02X}").into_bytes())
938            .chain(*b">")
939            .collect();
940        let mut dict = Dict::new();
941        dict.insert(name("Filter"), Object::Name(name("ASCIIHexDecode")));
942        let (bytes, refs) = skeleton(options, dict, encoded.clone(), false);
943        let doc = Document::load(bytes).expect("document loads");
944        let resolved = doc
945            .resolve(&Object::Ref(refs.content))
946            .expect("content stream resolves");
947        let stream = resolved.as_stream().expect("content is a stream");
948        assert_eq!(stream.data, encoded, "pre-filtered data stays untouched");
949        assert_eq!(
950            stream.dict.get_name("Filter"),
951            Some(&name("ASCIIHexDecode"))
952        );
953        assert_eq!(doc.stream_data(stream).expect("stream decodes"), payload);
954    }
955
956    #[test]
957    fn put_stream_raw_never_compresses() {
958        let options = WriteOptions {
959            compress: true,
960            ..table_options()
961        };
962        let (bytes, refs) = skeleton(options, Dict::new(), CONTENT.to_vec(), true);
963        let doc = Document::load(bytes).expect("document loads");
964        let resolved = doc
965            .resolve(&Object::Ref(refs.content))
966            .expect("content stream resolves");
967        let stream = resolved.as_stream().expect("content is a stream");
968        assert_eq!(stream.data, CONTENT);
969        assert!(stream.dict.get("Filter").is_none());
970        assert_eq!(stream.dict.get_int("Length"), Some(CONTENT.len() as i64));
971    }
972
973    #[test]
974    fn table_offsets_past_ten_digits_are_rejected() {
975        assert_eq!(table_offset(9_999_999_999).ok(), Some(9_999_999_999));
976        assert!(table_offset(10_000_000_000).is_err());
977    }
978
979    #[test]
980    fn id_derives_from_the_emitted_content() {
981        let (a, refs) = minimal_pdf(table_options());
982        assert_eq!(refs.root.gen, 0);
983        let (b, other_refs) = skeleton(
984            table_options(),
985            Dict::new(),
986            b"BT /F1 12 Tf 72 720 Td (Hello, other) Tj ET".to_vec(),
987            false,
988        );
989        assert_eq!(other_refs.root.gen, 0);
990        let id_of = |bytes: &[u8]| {
991            let xref = load_xref(bytes).expect("xref loads");
992            let id = xref.trailer.get_array("ID").expect("/ID array present");
993            id[0]
994                .as_str_bytes()
995                .expect("/ID entry is a string")
996                .to_vec()
997        };
998        assert_ne!(
999            id_of(&a),
1000            id_of(&b),
1001            "/ID must depend on the emitted content"
1002        );
1003    }
1004
1005    #[test]
1006    fn id_pair_is_present_and_identical() {
1007        for options in [table_options(), stream_options(), objstm_options()] {
1008            let (bytes, refs) = minimal_pdf(options);
1009            assert_eq!(refs.root.gen, 0);
1010            let xref = load_xref(&bytes).expect("xref loads");
1011            let id = xref.trailer.get_array("ID").expect("/ID array present");
1012            assert_eq!(id.len(), 2);
1013            let first = id[0].as_str_bytes().expect("/ID entry is a string");
1014            let second = id[1].as_str_bytes().expect("/ID entry is a string");
1015            assert_eq!(first.len(), 16);
1016            assert_eq!(first, second);
1017        }
1018    }
1019
1020    #[test]
1021    fn output_is_deterministic() {
1022        for options in [table_options(), stream_options(), objstm_options()] {
1023            let (first, refs) = minimal_pdf(options);
1024            let (second, again) = minimal_pdf(options);
1025            assert_eq!(refs.content, again.content);
1026            assert_eq!(first, second, "options {options:?} must be deterministic");
1027        }
1028    }
1029
1030    #[test]
1031    fn finish_into_matches_finish() {
1032        for options in [table_options(), stream_options(), objstm_options()] {
1033            let (bytes, _) = minimal_pdf(options);
1034            let (w, refs) = build(options, Dict::new(), CONTENT.to_vec(), false);
1035            let mut out = Vec::new();
1036            w.finish_into(refs.root, &mut out)
1037                .expect("finish_into succeeds");
1038            assert_eq!(out, bytes, "options {options:?}");
1039        }
1040    }
1041
1042    #[test]
1043    fn finish_into_with_matches_finish() {
1044        for options in [table_options(), stream_options(), objstm_options()] {
1045            let (bytes, _) = minimal_pdf(options);
1046            let (w, refs) = build(options, Dict::new(), CONTENT.to_vec(), false);
1047            let sink = pdfboss_core::block_on(w.finish_into_with(refs.root, Vec::new()))
1048                .expect("finish_into_with succeeds");
1049            assert_eq!(sink, bytes, "options {options:?}");
1050        }
1051    }
1052
1053    /// Records every chunk it is handed, so tests can see how emission
1054    /// arrives — the write happens eagerly, the future is already complete.
1055    struct Recording {
1056        chunks: Vec<Vec<u8>>,
1057    }
1058
1059    impl crate::sink::AsyncByteSink for Recording {
1060        fn write_all<'a>(
1061            &'a mut self,
1062            buf: &'a [u8],
1063        ) -> pdfboss_core::source::BoxFuture<'a, Result<()>> {
1064            self.chunks.push(buf.to_vec());
1065            Box::pin(std::future::ready(Ok(())))
1066        }
1067    }
1068
1069    /// Emission must actually stream: many bounded chunks, never one
1070    /// whole-file buffer — and their concatenation must be the `finish`
1071    /// bytes exactly.
1072    #[test]
1073    fn emission_arrives_in_bounded_chunks() {
1074        for options in [table_options(), stream_options(), objstm_options()] {
1075            let (bytes, _) = minimal_pdf(options);
1076            let (w, refs) = build(options, Dict::new(), CONTENT.to_vec(), false);
1077            let sink = pdfboss_core::block_on(
1078                w.finish_into_with(refs.root, Recording { chunks: Vec::new() }),
1079            )
1080            .expect("finish_into_with succeeds");
1081            assert_eq!(sink.chunks.concat(), bytes, "options {options:?}");
1082            assert!(
1083                sink.chunks.len() > 3,
1084                "options {options:?}: emission must arrive in many chunks, got {}",
1085                sink.chunks.len()
1086            );
1087            assert!(
1088                sink.chunks.iter().all(|chunk| chunk.len() < bytes.len()),
1089                "options {options:?}: no chunk may be the whole file"
1090            );
1091        }
1092    }
1093
1094    /// The emission future over an owned sink must be `Send + 'static`,
1095    /// so it can cross a runtime's `spawn` — the write-side counterpart of
1096    /// the source module's by-value rule.
1097    #[test]
1098    fn finish_into_with_over_an_owned_sink_is_spawnable() {
1099        fn assert_send_static<F: std::future::Future + Send + 'static>(_: &F) {}
1100
1101        let (w, refs) = build(stream_options(), Dict::new(), CONTENT.to_vec(), false);
1102        let future = w.finish_into_with(refs.root, Vec::new());
1103        assert_send_static(&future);
1104        let bytes = pdfboss_core::block_on(future).expect("emission succeeds");
1105        assert!(bytes.ends_with(b"%%EOF"));
1106    }
1107
1108    /// An unfilled reserve must surface from the streaming finishes too,
1109    /// before any byte reaches the sink.
1110    #[test]
1111    fn finish_into_with_reports_unfilled_reserves() {
1112        let mut w = Writer::new(table_options());
1113        let root = w.put(Object::Dict(Dict::new()));
1114        let reserved = w.reserve();
1115        let sink = Recording { chunks: Vec::new() };
1116        match pdfboss_core::block_on(w.finish_into_with(root, sink)) {
1117            Err(Error::Unfilled(seen)) => assert_eq!(seen, reserved),
1118            other => panic!("expected Unfilled, got {:?}", other.map(|s| s.chunks.len())),
1119        }
1120    }
1121}