Skip to main content

otf_pdf_doc/
save.rs

1//! Saving (docs/PLAN.md §4, "Save").
2//!
3//! * [`SaveMode::Full`] rewrites the document: only objects reachable from
4//!   the trailer are kept, they are renumbered densely in a deterministic
5//!   order, unfiltered streams are compressed, non-stream objects are packed
6//!   into object streams, and an xref stream indexes it all. It can encrypt
7//!   with AES-256.
8//! * [`SaveMode::Incremental`] copies the original bytes unchanged and
9//!   appends only what changed plus a new xref section, as signatures need.
10//!
11//! Output is deterministic: the same document and options give the same
12//! bytes. The document ID defaults to an MD5 of the content written, and
13//! encryption draws its randomness from the injected [`Entropy`].
14
15use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
16use std::io::Write as _;
17use std::sync::Arc;
18
19use md5::{Digest, Md5};
20use otf_pdf_syntax::crypt::{CryptMethod, SecurityHandler};
21use otf_pdf_syntax::filters::filter_chain;
22use otf_pdf_syntax::serialize::{to_bytes, write_object};
23use otf_pdf_syntax::xref::XrefEntry;
24use otf_pdf_syntax::{
25    DiagnosticKind, Dict, Entropy, Error, LimitKind, Name, ObjId, Object, PdfString, Permissions,
26    Result, Sink, Stream, Version,
27};
28
29use crate::document::Document;
30use crate::pages::{standalone_page_dict, tree_is_sound};
31use crate::store::{Change, Store};
32
33/// How to save.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum SaveMode {
36    /// Rewrite and compact the whole document.
37    #[default]
38    Full,
39    /// Append changes to the original bytes.
40    Incremental,
41}
42
43/// Encryption for a full save: always AES-256 (revision 6).
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct Encryption {
46    /// The password needed to open the document (may be empty).
47    pub user_password: Vec<u8>,
48    /// The password that lifts the permission restrictions.
49    pub owner_password: Vec<u8>,
50    /// What a user-password reader may do.
51    pub permissions: Permissions,
52    /// Whether the XMP metadata stream is encrypted too.
53    pub encrypt_metadata: bool,
54}
55
56/// Options for [`Document::save`].
57#[derive(Clone)]
58pub struct SaveOptions {
59    /// Full rewrite or incremental append.
60    pub mode: SaveMode,
61    /// Output version for a full save (1.7 by default, or 2.0). An
62    /// incremental save keeps the original header.
63    pub version: Version,
64    /// Pack non-stream objects into object streams with an xref stream
65    /// (full save). Without it, a classic xref table is written.
66    pub object_streams: bool,
67    /// Flate-compress streams that have no filter.
68    pub compress: bool,
69    /// Encrypt the output (full save only).
70    pub encryption: Option<Encryption>,
71    /// The document ID to write; by default it is derived from the content.
72    pub document_id: Option<[u8; 16]>,
73    /// Randomness for encryption keys, salts and IVs. Required whenever
74    /// something is encrypted.
75    pub entropy: Option<Arc<dyn Entropy>>,
76}
77
78impl Default for SaveOptions {
79    fn default() -> Self {
80        Self {
81            mode: SaveMode::Full,
82            version: Version::V1_7,
83            object_streams: true,
84            compress: true,
85            encryption: None,
86            document_id: None,
87            entropy: None,
88        }
89    }
90}
91
92impl std::fmt::Debug for SaveOptions {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        f.debug_struct("SaveOptions")
95            .field("mode", &self.mode)
96            .field("version", &self.version)
97            .field("object_streams", &self.object_streams)
98            .field("compress", &self.compress)
99            .field("encryption", &self.encryption.is_some())
100            .field("document_id", &self.document_id)
101            .field("entropy", &self.entropy.is_some())
102            .finish()
103    }
104}
105
106/// Objects per object stream.
107const OBJSTM_SIZE: usize = 100;
108/// Streams shorter than this are not worth compressing.
109const MIN_COMPRESS: usize = 32;
110
111/// Counts bytes and hashes them (for the default document ID) on their
112/// way to the sink.
113struct Out<'s> {
114    sink: &'s mut dyn Sink,
115    pos: u64,
116    hash: Md5,
117}
118
119impl Out<'_> {
120    fn write(&mut self, bytes: &[u8]) -> Result<()> {
121        self.sink.write_all(bytes)?;
122        self.hash.update(bytes);
123        self.pos += bytes.len() as u64;
124        Ok(())
125    }
126
127    fn digest(&self) -> [u8; 16] {
128        self.hash.clone().finalize().into()
129    }
130}
131
132/// Where each written object went.
133#[derive(Debug, Clone, Copy)]
134enum Slot {
135    Free(u16),
136    At(u64, u16),
137    In(u32, u32),
138}
139
140/// The store as a full save sees it: plus objects synthesized for the
141/// output (a catalog given directly in the trailer, a rebuilt page tree).
142struct View<'a> {
143    store: &'a Store,
144    extra: HashMap<ObjId, Object>,
145    /// The catalog's id (new if it was a direct dictionary).
146    root: ObjId,
147}
148
149impl View<'_> {
150    fn get(&self, id: ObjId) -> Result<Arc<Object>> {
151        match self.extra.get(&id) {
152            Some(obj) => Ok(Arc::new(obj.clone())),
153            None => self.store.get(id),
154        }
155    }
156}
157
158impl Document {
159    /// Saves the document into `sink`.
160    pub fn save_to(&self, sink: &mut dyn Sink, options: &SaveOptions) -> Result<()> {
161        let mut out = Out {
162            sink,
163            pos: 0,
164            hash: Md5::new(),
165        };
166        match options.mode {
167            SaveMode::Full => self.save_full(&mut out, options),
168            SaveMode::Incremental => self.save_incremental(&mut out, options),
169        }
170    }
171
172    /// Saves the document and returns the bytes.
173    pub fn save(&self, options: &SaveOptions) -> Result<Vec<u8>> {
174        let mut bytes = Vec::new();
175        self.save_to(&mut bytes, options)?;
176        Ok(bytes)
177    }
178
179    fn save_full(&self, out: &mut Out<'_>, options: &SaveOptions) -> Result<()> {
180        let store = &self.store;
181        let entropy = options.entropy.as_deref();
182        let security = match &options.encryption {
183            Some(e) => {
184                let entropy = entropy.ok_or_else(needs_entropy)?;
185                Some(SecurityHandler::new_aes256(
186                    &e.user_password,
187                    &e.owner_password,
188                    e.permissions,
189                    e.encrypt_metadata,
190                    entropy,
191                ))
192            }
193            None => None,
194        };
195
196        let trailer = store.trailer();
197        let view = self.save_view()?;
198        let root = view.root;
199
200        // Reachable objects, breadth-first from the trailer, numbered densely.
201        let mut roots = vec![root];
202        roots.extend(trailer.get_ref(b"Info"));
203        let (order, numbers) = reachable(&view, &roots)?;
204
205        let mut prepared: Vec<(u32, Object)> = Vec::with_capacity(order.len());
206        for old in &order {
207            let mut obj = (*view.get(*old)?).clone();
208            remap(&mut obj, &numbers);
209            if *old == root
210                && let Object::Dict(catalog) = &mut obj
211            {
212                catalog.remove(b"Version");
213                if security.is_some() && options.version < Version::V2_0 {
214                    catalog.insert("Extensions", aes256_extension());
215                }
216            }
217            if let Object::Stream(s) = &mut obj {
218                prepare_stream(s, options.compress)?;
219            }
220            // Every id in `order` was numbered by `reachable`.
221            let Some(&num) = numbers.get(old) else {
222                continue;
223            };
224            prepared.push((num, obj));
225        }
226
227        let mut slots: BTreeMap<u32, Slot> = BTreeMap::new();
228        slots.insert(0, Slot::Free(65535));
229        // The comment of bytes ≥ 128 marks the file as binary (§7.5.2).
230        let mut header = format!("%PDF-{}\n%", options.version).into_bytes();
231        header.extend_from_slice(&[0xe2, 0xe3, 0xcf, 0xd3, b'\n']);
232        out.write(&header)?;
233
234        let (direct, packed): (Vec<_>, Vec<_>) = prepared
235            .into_iter()
236            .partition(|(_, o)| !options.object_streams || matches!(o, Object::Stream(_)));
237        let sec = security.as_ref().map(|(h, _)| h);
238        for (num, mut obj) in direct {
239            if let Some(h) = sec {
240                encrypt_object(h, &mut obj, entropy.ok_or_else(needs_entropy)?)?;
241            }
242            slots.insert(num, Slot::At(out.pos, 0));
243            write_indirect(out, ObjId::new(num, 0), &obj)?;
244        }
245
246        let mut next = order.len() as u32 + 1;
247        for chunk in packed.chunks(OBJSTM_SIZE) {
248            let stm_num = next;
249            next += 1;
250            let mut header = Vec::new();
251            let mut body = Vec::new();
252            for (index, (num, obj)) in chunk.iter().enumerate() {
253                header.extend_from_slice(format!("{num} {} ", body.len()).as_bytes());
254                write_object(obj, &mut body);
255                body.push(b'\n');
256                slots.insert(*num, Slot::In(stm_num, index as u32));
257            }
258            let mut dict = Dict::new();
259            dict.insert("Type", Name::from("ObjStm"));
260            dict.insert("N", chunk.len() as i64);
261            dict.insert("First", header.len() as i64);
262            header.extend_from_slice(&body);
263            let mut stream = Object::Stream(Stream {
264                dict,
265                raw: header.into(),
266            });
267            if let Object::Stream(s) = &mut stream {
268                compress(s)?;
269            }
270            if let Some(h) = sec {
271                encrypt_object(h, &mut stream, entropy.ok_or_else(needs_entropy)?)?;
272            }
273            slots.insert(stm_num, Slot::At(out.pos, 0));
274            write_indirect(out, ObjId::new(stm_num, 0), &stream)?;
275        }
276
277        let mut trailer_out = Dict::new();
278        let root_num = numbers.get(&root).copied().ok_or(Error::NoRoot)?;
279        trailer_out.insert("Root", ObjId::new(root_num, 0));
280        if let Some(info) = trailer.get_ref(b"Info").and_then(|i| numbers.get(&i)) {
281            trailer_out.insert("Info", ObjId::new(*info, 0));
282        }
283        if let Some((_, encrypt_dict)) = &security {
284            let num = next;
285            next += 1;
286            slots.insert(num, Slot::At(out.pos, 0));
287            write_indirect(out, ObjId::new(num, 0), &Object::Dict(encrypt_dict.clone()))?;
288            trailer_out.insert("Encrypt", ObjId::new(num, 0));
289        }
290        let id = options.document_id.unwrap_or_else(|| out.digest());
291        trailer_out.insert(
292            "ID",
293            vec![
294                Object::String(PdfString::hex(id.to_vec())),
295                Object::String(PdfString::hex(id.to_vec())),
296            ],
297        );
298        write_xref(out, &slots, trailer_out, next, options.object_streams, 0)
299    }
300
301    /// The view a full save writes from. A catalog given directly in the
302    /// trailer gets an object number; a page tree that needed repair is
303    /// replaced by a flat one holding the pages found, with their inherited
304    /// attributes written into each page.
305    fn save_view(&self) -> Result<View<'_>> {
306        let store = &self.store;
307        let mut extra = HashMap::new();
308        let mut next = store.next_number();
309        let mut fresh = || {
310            let id = ObjId::new(next, 0);
311            next += 1;
312            id
313        };
314        let (root, mut catalog) = match store.trailer().get(b"Root") {
315            Some(Object::Ref(id)) => (*id, self.catalog()),
316            Some(Object::Dict(d)) => (fresh(), d.clone()),
317            _ => return Err(Error::NoRoot),
318        };
319        let mut catalog_changed = !matches!(store.trailer().get(b"Root"), Some(Object::Ref(_)));
320
321        let pages = self.pages()?;
322        // Checked directly, not through diagnostics: those are capped and
323        // could miss the repair.
324        let repaired = !tree_is_sound(store, &catalog, &pages);
325        if repaired {
326            let tree = fresh();
327            let mut kids = Vec::with_capacity(pages.len());
328            for page in pages.iter() {
329                let id = page.id.unwrap_or_else(&mut fresh);
330                let mut dict = standalone_page_dict(page);
331                dict.insert("Parent", tree);
332                extra.insert(id, Object::Dict(dict));
333                kids.push(Object::Ref(id));
334            }
335            let mut node = Dict::new();
336            node.insert("Type", Name::from("Pages"));
337            node.insert("Count", kids.len() as i64);
338            node.insert("Kids", kids);
339            extra.insert(tree, Object::Dict(node));
340            catalog.insert("Pages", tree);
341            catalog_changed = true;
342        }
343        if catalog_changed {
344            extra.insert(root, Object::Dict(catalog));
345        }
346        Ok(View { store, extra, root })
347    }
348
349    fn save_incremental(&self, out: &mut Out<'_>, options: &SaveOptions) -> Result<()> {
350        let store = &self.store;
351        let file = store.file();
352        if options.encryption.is_some() {
353            return Err(Error::UnsupportedEncryption(
354                "encryption cannot change in an incremental save; save in full".into(),
355            ));
356        }
357        // Appending to a damaged revision chain makes it worse for every
358        // reader: refuse if the xref needed any repair. Wrong entries only
359        // show when objects load, so load them all first (the whole file is
360        // read for the copy anyway).
361        for (&num, entry) in file.entries() {
362            if let XrefEntry::InUse { generation, .. } = *entry {
363                let _ = file.get(ObjId::new(num, generation));
364            }
365        }
366        let damaged = file.diagnostics().iter().any(|d| {
367            matches!(
368                d.kind,
369                DiagnosticKind::XrefRebuilt
370                    | DiagnosticKind::BadXrefEntry
371                    | DiagnosticKind::BadXrefSection
372                    | DiagnosticKind::BadStartxref
373                    | DiagnosticKind::XrefLoop
374            )
375        });
376        if damaged {
377            return Err(Error::syntax(
378                0,
379                "the file's cross-reference data needed repair, so there is no sound revision to append to; save in full",
380            ));
381        }
382        let prev = file
383            .startxref()?
384            .ok_or_else(|| Error::syntax(0, "no startxref to append to; save in full"))?;
385        let security = file.security();
386        if let Some(sec) = security
387            && !sec.can_encrypt()
388        {
389            return Err(Error::UnsupportedEncryption(
390                "the file uses RC4 or AES-128, which is never written; save in full".into(),
391            ));
392        }
393        let encrypt_ref = store.trailer().get_ref(b"Encrypt");
394        // Offsets in a file with junk before its header may count from the
395        // header; appended ones must match.
396        let shift = file.offset_shift()?;
397
398        let base = file.read_all()?;
399        out.sink.write_all(&base)?;
400        out.pos = base.len() as u64;
401        if !base.ends_with(b"\n") && !base.ends_with(b"\r") {
402            out.write(b"\n")?;
403        }
404        // The default ID part is a hash of what this update appends.
405        out.hash = Md5::new();
406
407        // Object 0, the head of the free list, keeps every section non-empty
408        // (a section needs at least one subsection, §7.5.4).
409        let mut slots: BTreeMap<u32, Slot> = BTreeMap::new();
410        slots.insert(0, Slot::Free(65535));
411        for (id, change) in store.overlay() {
412            match change {
413                Change::Set(obj) => {
414                    let mut obj = (**obj).clone();
415                    if let Object::Stream(s) = &mut obj {
416                        prepare_stream(s, options.compress)?;
417                    }
418                    if let Some(sec) = security
419                        && Some(*id) != encrypt_ref
420                    {
421                        let entropy = options.entropy.as_deref().ok_or_else(needs_entropy)?;
422                        encrypt_object(sec, &mut obj, entropy)?;
423                    }
424                    slots.insert(id.num, Slot::At(out.pos - shift, id.generation));
425                    write_indirect(out, *id, &obj)?;
426                }
427                Change::Deleted => {
428                    slots.insert(id.num, Slot::Free(id.generation.saturating_add(1)));
429                }
430            }
431        }
432
433        let mut trailer = store.trailer().clone();
434        for key in [&b"Prev"[..], b"XRefStm"] {
435            trailer.remove(key);
436        }
437        trailer.insert("Prev", prev as i64);
438        let new_part = options.document_id.unwrap_or_else(|| out.digest());
439        let first = trailer
440            .get_array(b"ID")
441            .and_then(|a| a.first().cloned())
442            .filter(|o| o.as_string().is_some())
443            .unwrap_or_else(|| Object::String(PdfString::hex(new_part.to_vec())));
444        trailer.insert(
445            "ID",
446            vec![first, Object::String(PdfString::hex(new_part.to_vec()))],
447        );
448        let size = trailer
449            .get_int(b"Size")
450            .and_then(|s| u32::try_from(s).ok())
451            .unwrap_or(0)
452            .max(store.next_number());
453        let as_stream = file.newest_section_is_stream()?.unwrap_or(false);
454        write_xref(out, &slots, trailer, size, as_stream, shift)
455    }
456}
457
458/// Object ids reachable from `roots`, in breadth-first order, and their new
459/// numbers. Objects that cannot be read are left out; references to them
460/// become `null`. Identical streams share one number.
461fn reachable(view: &View<'_>, roots: &[ObjId]) -> Result<(Vec<ObjId>, HashMap<ObjId, u32>)> {
462    let limits = view.store.limits();
463    let mut order = Vec::new();
464    let mut numbers = HashMap::new();
465    let mut seen = HashSet::new();
466    let mut streams: HashMap<[u8; 16], ObjId> = HashMap::new();
467    let mut queue: VecDeque<ObjId> = roots.iter().copied().collect();
468    while let Some(id) = queue.pop_front() {
469        if !seen.insert(id) {
470            continue;
471        }
472        limits.charge(1)?;
473        let Ok(obj) = view.get(id) else { continue };
474        // A stream identical to one already kept (a font or image copied in
475        // by two merges) is written once.
476        if let Object::Stream(_) = &*obj {
477            let key: [u8; 16] = Md5::digest(to_bytes(&obj)).into();
478            let same = streams
479                .get(&key)
480                .filter(|&&kept: &&ObjId| view.get(kept).is_ok_and(|k| *k == *obj));
481            if let Some(&kept) = same
482                && let Some(&n) = numbers.get(&kept)
483            {
484                numbers.insert(id, n);
485                continue;
486            }
487            streams.entry(key).or_insert(id);
488        }
489        if order.len() >= limits.max_objects as usize {
490            return Err(Error::Limit(LimitKind::Objects));
491        }
492        order.push(id);
493        numbers.insert(id, order.len() as u32);
494        let mut refs = Vec::new();
495        collect_refs(&obj, &mut refs);
496        queue.extend(refs.into_iter().filter(|r| !seen.contains(r)));
497    }
498    Ok((order, numbers))
499}
500
501fn needs_entropy() -> Error {
502    Error::UnsupportedEncryption("encrypting needs an entropy source (SaveOptions::entropy)".into())
503}
504
505/// `/Extensions` declaring AES-256 in a 1.7 file (Adobe extension level 8).
506fn aes256_extension() -> Dict {
507    let mut adbe = Dict::new();
508    adbe.insert("BaseVersion", Name::from("1.7"));
509    adbe.insert("ExtensionLevel", 8i64);
510    let mut ext = Dict::new();
511    ext.insert("ADBE", adbe);
512    ext
513}
514
515fn collect_refs(object: &Object, out: &mut Vec<ObjId>) {
516    match object {
517        Object::Ref(id) => out.push(*id),
518        Object::Array(items) => items.iter().for_each(|i| collect_refs(i, out)),
519        Object::Dict(d) => d.iter().for_each(|(_, v)| collect_refs(v, out)),
520        Object::Stream(s) => s.dict.iter().for_each(|(_, v)| collect_refs(v, out)),
521        _ => {}
522    }
523}
524
525/// Rewrites references through `numbers`; unknown targets become `null`.
526fn remap(object: &mut Object, numbers: &HashMap<ObjId, u32>) {
527    match object {
528        Object::Ref(id) => {
529            *object = match numbers.get(id) {
530                Some(&n) => Object::Ref(ObjId::new(n, 0)),
531                None => Object::Null,
532            }
533        }
534        Object::Array(items) => items.iter_mut().for_each(|i| remap(i, numbers)),
535        Object::Dict(d) => remap_dict(d, numbers),
536        Object::Stream(s) => remap_dict(&mut s.dict, numbers),
537        _ => {}
538    }
539}
540
541fn remap_dict(dict: &mut Dict, numbers: &HashMap<ObjId, u32>) {
542    let keys: Vec<Name> = dict.iter().map(|(k, _)| k.clone()).collect();
543    for key in keys {
544        if let Some(mut v) = dict.remove(key.as_bytes()) {
545            remap(&mut v, numbers);
546            dict.insert(key, v);
547        }
548    }
549}
550
551/// Drops `/Length` (rewritten from the data) and a `/Crypt` filter (the
552/// data is already decrypted), and compresses unfiltered data.
553fn prepare_stream(stream: &mut Stream, compress_unfiltered: bool) -> Result<()> {
554    stream.dict.remove(b"Length");
555    let chain = filter_chain(&stream.dict);
556    if chain.iter().any(|f| f.name.as_bytes() == b"Crypt") {
557        let kept: Vec<_> = chain
558            .into_iter()
559            .filter(|f| f.name.as_bytes() != b"Crypt")
560            .collect();
561        stream.dict.remove(b"Filter");
562        stream.dict.remove(b"DecodeParms");
563        if !kept.is_empty() {
564            stream.dict.insert(
565                "Filter",
566                kept.iter()
567                    .map(|f| Object::Name(f.name.clone()))
568                    .collect::<Vec<_>>(),
569            );
570            if kept.iter().any(|f| f.params.is_some()) {
571                stream.dict.insert(
572                    "DecodeParms",
573                    kept.into_iter()
574                        .map(|f| f.params.map_or(Object::Null, Object::Dict))
575                        .collect::<Vec<_>>(),
576                );
577            }
578        }
579    }
580    let unfiltered = !stream.dict.contains_key(b"Filter");
581    // XMP stays readable without decoding, as PDF/A requires.
582    if compress_unfiltered
583        && unfiltered
584        && stream.raw.len() >= MIN_COMPRESS
585        && !stream.dict.is_type(b"Metadata")
586    {
587        compress(stream)?;
588    }
589    Ok(())
590}
591
592fn compress(stream: &mut Stream) -> Result<()> {
593    let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
594    e.write_all(&stream.raw)
595        .and_then(|()| e.finish())
596        .map(|data| {
597            stream.raw = data.into();
598            stream.dict.insert("Filter", Name::from("FlateDecode"));
599        })
600        .map_err(|_| Error::Filter("compression failed"))
601}
602
603/// Encrypts an object's strings and, for a stream, its data.
604fn encrypt_object(sec: &SecurityHandler, obj: &mut Object, entropy: &dyn Entropy) -> Result<()> {
605    sec.encrypt_strings(obj, entropy)?;
606    if let Object::Stream(s) = obj {
607        let clear =
608            s.dict.is_type(b"XRef") || (s.dict.is_type(b"Metadata") && !sec.encrypt_metadata());
609        let method = if clear {
610            CryptMethod::Identity
611        } else if s.dict.is_type(b"EmbeddedFile") {
612            sec.embedded_file_method()
613        } else {
614            sec.stream_method()
615        };
616        s.raw = sec.encrypt(method, &s.raw, entropy)?.into();
617    }
618    Ok(())
619}
620
621fn write_indirect(out: &mut Out<'_>, id: ObjId, obj: &Object) -> Result<()> {
622    let mut bytes = format!("{} {} obj\n", id.num, id.generation).into_bytes();
623    write_object(obj, &mut bytes);
624    bytes.extend_from_slice(b"\nendobj\n");
625    out.write(&bytes)
626}
627
628/// Writes the xref section (stream or table), trailer, `startxref` and
629/// `%%EOF`. `size` is one past the highest object number; `shift` is
630/// subtracted from the section's own offset (see `File::offset_shift`).
631fn write_xref(
632    out: &mut Out<'_>,
633    slots: &BTreeMap<u32, Slot>,
634    mut trailer: Dict,
635    size: u32,
636    as_stream: bool,
637    shift: u64,
638) -> Result<()> {
639    let xref_at = out.pos - shift;
640    if as_stream {
641        let own = size;
642        let size = size + 1;
643        let mut slots = slots.clone();
644        slots.insert(own, Slot::At(xref_at, 0));
645        let max_offset = slots
646            .values()
647            .map(|s| match *s {
648                Slot::At(o, _) => o,
649                Slot::In(n, i) => u64::from(n.max(i)),
650                Slot::Free(_) => 0,
651            })
652            .max()
653            .unwrap_or(0);
654        let w1 = bytes_needed(max_offset);
655        let w2 = 2;
656        let mut data = Vec::new();
657        let mut index = Vec::new();
658        for (first, run) in runs(&slots) {
659            index.push(Object::Int(i64::from(first)));
660            index.push(Object::Int(run.len() as i64));
661            for slot in run {
662                let (kind, a, b) = match slot {
663                    Slot::Free(generation) => (0u64, 0u64, u64::from(generation)),
664                    Slot::At(offset, generation) => (1, offset, u64::from(generation)),
665                    Slot::In(stream, i) => (2, u64::from(stream), u64::from(i)),
666                };
667                data.push(kind as u8);
668                data.extend_from_slice(a.to_be_bytes().get(8 - w1..).unwrap_or_default());
669                data.extend_from_slice(b.to_be_bytes().get(8 - w2..).unwrap_or_default());
670            }
671        }
672        trailer.insert("Type", Name::from("XRef"));
673        trailer.insert("Size", i64::from(size));
674        trailer.insert(
675            "W",
676            vec![
677                Object::Int(1),
678                Object::Int(w1 as i64),
679                Object::Int(w2 as i64),
680            ],
681        );
682        trailer.insert("Index", index);
683        let mut stream = Stream {
684            dict: trailer,
685            raw: data.into(),
686        };
687        compress(&mut stream)?;
688        write_indirect(out, ObjId::new(own, 0), &Object::Stream(stream))?;
689    } else {
690        let mut table = b"xref\n".to_vec();
691        for (first, run) in runs(slots) {
692            table.extend_from_slice(format!("{first} {}\n", run.len()).as_bytes());
693            for slot in run {
694                let line = match slot {
695                    Slot::Free(generation) => format!("0000000000 {generation:05} f\r\n"),
696                    Slot::At(offset, generation) => format!("{offset:010} {generation:05} n\r\n"),
697                    Slot::In(..) => format!("0000000000 {:05} f\r\n", 0),
698                };
699                table.extend_from_slice(line.as_bytes());
700            }
701        }
702        trailer.insert("Size", i64::from(size));
703        table.extend_from_slice(b"trailer\n");
704        write_object(&Object::Dict(trailer), &mut table);
705        table.push(b'\n');
706        out.write(&table)?;
707    }
708    out.write(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes())
709}
710
711/// Splits slots into runs of consecutive object numbers.
712fn runs(slots: &BTreeMap<u32, Slot>) -> Vec<(u32, Vec<Slot>)> {
713    let mut out: Vec<(u32, Vec<Slot>)> = Vec::new();
714    for (&num, &slot) in slots {
715        match out.last_mut() {
716            Some((first, run)) if *first + run.len() as u32 == num => run.push(slot),
717            _ => out.push((num, vec![slot])),
718        }
719    }
720    out
721}
722
723fn bytes_needed(value: u64) -> usize {
724    (8 - value.leading_zeros() as usize / 8).max(1)
725}
726
727#[cfg(test)]
728mod tests {
729    use super::*;
730
731    #[test]
732    fn widths_and_runs() {
733        assert_eq!(bytes_needed(0), 1);
734        assert_eq!(bytes_needed(255), 1);
735        assert_eq!(bytes_needed(256), 2);
736        assert_eq!(bytes_needed(u64::MAX), 8);
737        let mut slots = BTreeMap::new();
738        for n in [0, 1, 2, 5, 6, 9] {
739            slots.insert(n, Slot::Free(0));
740        }
741        let r: Vec<_> = runs(&slots)
742            .into_iter()
743            .map(|(f, r)| (f, r.len()))
744            .collect();
745        assert_eq!(r, [(0, 3), (5, 2), (9, 1)]);
746    }
747
748    #[test]
749    fn remapping_drops_dangling_references() {
750        let mut numbers = HashMap::new();
751        numbers.insert(ObjId::new(7, 2), 1);
752        let mut obj = Object::Array(vec![
753            Object::Ref(ObjId::new(7, 2)),
754            Object::Ref(ObjId::new(8, 0)),
755        ]);
756        remap(&mut obj, &numbers);
757        assert_eq!(
758            obj,
759            Object::Array(vec![Object::Ref(ObjId::new(1, 0)), Object::Null])
760        );
761    }
762
763    #[test]
764    fn crypt_filter_is_dropped_and_small_streams_stay_raw() {
765        let mut dict = Dict::new();
766        dict.insert(
767            "Filter",
768            vec![
769                Object::Name(Name::from("Crypt")),
770                Object::Name(Name::from("FlateDecode")),
771            ],
772        );
773        dict.insert("Length", 99i64);
774        let mut s = Stream {
775            dict,
776            raw: b"x".to_vec().into(),
777        };
778        prepare_stream(&mut s, true).unwrap();
779        assert_eq!(
780            s.dict.get(b"Filter"),
781            Some(&Object::Array(vec![Object::Name(Name::from(
782                "FlateDecode"
783            ))]))
784        );
785        assert!(s.dict.get(b"Length").is_none());
786
787        let mut small = Stream {
788            dict: Dict::new(),
789            raw: b"tiny".to_vec().into(),
790        };
791        prepare_stream(&mut small, true).unwrap();
792        assert!(small.dict.get(b"Filter").is_none());
793    }
794}