Skip to main content

pdfrum_edit/write/
mod.rs

1//! Writing a document out (ISO 32000-1 §7.5).
2//!
3//! [`save`] is one straight-line function calling six steps. The C++ spells
4//! the same sequence as a numbered stage machine driven by a resumable
5//! `Continue()` loop — that machinery exists to support pausable saving
6//! through its public API, a facility we do not offer, so the stage numbers
7//! survive here only as the order the steps run in.
8//!
9//! ```text
10//! header → old objects → new objects → encrypt dict → xref → trailer
11//! ```
12//!
13//! # Full and incremental are one path, not two
14//!
15//! The difference is entirely in what each step does:
16//!
17//! | | full save | incremental save |
18//! |---|---|---|
19//! | header | `%PDF-1.N` + a binary comment | the original file, byte for byte |
20//! | "new" objects | those the xref does not name, or names as free | *every* object in play |
21//! | old objects | reachable objects, garbage-collected | none |
22//! | cross-reference | a full table | a delta table, or a stream |
23//! | trailer | no `/Prev` | `/Prev` naming the original's last section |
24//!
25//! Two conditions silently downgrade an incremental save to a full one, both
26//! because appending would produce a file no reader could open: a **rebuilt**
27//! cross-reference (there is no previous section to chain from) and a
28//! **changed security key** (the appended objects would be keyed differently
29//! from the bytes before them).
30//!
31//! # An encrypted document stays encrypted
32//!
33//! Objects reach the writer plaintext, because the parser deciphered them on
34//! fetch. A save under a document's own security handler puts the cipher back
35//! on with the same file key, so the saved file opens with the same password;
36//! [`crate::encrypt`] holds the exemptions and the initialisation-vector
37//! story. [`SaveOptions::remove_security`] is the
38//! explicit opt-out, and turns the save into a plaintext rewrite with no
39//! `/Encrypt` in the trailer.
40//!
41//! Two mechanics follow from `/Encrypt` having to be an indirect object
42//! (ISO 32000-1 §7.6.1). A file that wrote it **inline** in the trailer has no
43//! object number for it, so the writer promotes it to a fresh one past the
44//! highest in play. And whichever number it ends up with, that object is the
45//! one thing the encryptor never touches.
46//!
47//! # The garbage collection is the point
48//!
49//! A full save writes only what the trailer can still reach. Removing every
50//! object from a page and regenerating its content really does produce a
51//! smaller file, rather than one that still carries the images nothing points
52//! at any more.
53
54mod header;
55pub(crate) mod id;
56pub(crate) mod object;
57mod reach;
58mod stream;
59mod trailer;
60mod xref;
61
62use std::io::Write;
63
64use pdfrum_common::PdfVersion;
65use pdfrum_object::{ObjRef, Object, Resolve, names};
66
67use crate::doc::EditDoc;
68use crate::encrypt;
69use crate::error::Error;
70use crate::font;
71use crate::write::header::write_header;
72use crate::write::id::{IdContext, IdSource};
73use crate::write::xref::ObjectOffsets;
74
75/// How a document is written back out.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
77pub enum SaveMode {
78    /// Rewrite the whole file, dropping anything nothing points at.
79    #[default]
80    Full,
81    /// Append the changes after the original bytes, leaving them untouched.
82    ///
83    /// Downgraded to [`SaveMode::Full`] when the document's cross-reference
84    /// was rebuilt or its security key changed; see the module docs.
85    Incremental,
86}
87
88/// Everything a save may be asked to do differently.
89///
90/// `#[non_exhaustive]`, so a later option is an addition rather than a break;
91/// build one with [`SaveOptions::builder`], or start from
92/// [`SaveOptions::default`] and assign the fields.
93#[derive(Debug, Clone, PartialEq, Eq)]
94#[non_exhaustive]
95pub struct SaveOptions {
96    /// Whether to append or rewrite.
97    pub mode: SaveMode,
98    /// Keep the original bytes as the file's prefix. Only an incremental save
99    /// reads this; clearing it there turns the save into a rewrite that keeps
100    /// the appended shape.
101    pub keep_original: bool,
102    /// Drop the security handler and `/Encrypt`, writing the document in the
103    /// clear.
104    ///
105    /// Off by default: an encrypted document saves encrypted under its own
106    /// handler, and opens with the password it was opened with. Setting this
107    /// is the explicit way to decrypt one on the way out — and it forces a
108    /// full save, since plaintext cannot be appended behind ciphertext.
109    ///
110    /// Has no effect on an unencrypted document.
111    pub remove_security: bool,
112    /// Subset newly embedded fonts, dropping the glyphs no page shows.
113    ///
114    /// Off by default. When set, every font this save writes as a **new**
115    /// object and that a show operator on some page draws with is replaced by
116    /// a subset carrying only the glyphs still used, named `ABCDEF+Original`
117    /// after ISO 32000-1 §9.6.4.
118    ///
119    /// **What it subsets**: a `/Type0` font whose descendant is a
120    /// `CIDFontType2` with an embedded `/FontFile2`. Nothing else — a Type 1
121    /// (`/FontFile`) or `OpenType`-CFF (`/FontFile3`, or an `OTTO` program)
122    /// font is left alone, and so is a *simple* TrueType font, whose codes
123    /// reach glyphs through a `cmap` the subsetter removes.
124    ///
125    /// **What it does not disturb**: the character codes on the page, the
126    /// CIDs they map to, `/W`, and `/ToUnicode`. The subsetter renumbers
127    /// glyphs, and a rewritten `/CIDToGIDMap` absorbs that renumbering at the
128    /// one place ISO 32000-1 §9.7.4.2 already provides for it — so **no
129    /// content stream is regenerated**, and text extraction over the saved
130    /// file is unchanged.
131    ///
132    /// A font whose program will not subset, or whose subset would not be
133    /// smaller, is written unchanged.
134    pub subset_new_fonts: bool,
135    /// The version to declare in the header. 1.0 through 1.7 are honoured;
136    /// anything outside that range, and `None`, keep the document's own.
137    pub version: Option<PdfVersion>,
138    /// Where `/ID` and subset tags come from.
139    pub id_source: IdSource,
140    /// Encrypt an unencrypted document on the way out: AES-256, revision 6,
141    /// under these passwords and permissions. `None` leaves the document as
142    /// it is. A document that is already encrypted cannot be re-keyed here:
143    /// asking for it is [`Error::EncryptedSaveUnsupported`].
144    pub encrypt: Option<Encryption>,
145}
146
147/// How a document is to be encrypted on save (ISO 32000-2 §7.6.4.4).
148///
149/// `#[non_exhaustive]`; build one with [`Encryption::builder`].
150#[derive(Debug, Clone, PartialEq, Eq)]
151#[non_exhaustive]
152pub struct Encryption {
153    /// Opens the document with the rights `permissions` grants. Empty means
154    /// anyone can open it.
155    pub user_password: Vec<u8>,
156    /// Opens the document with every right. Empty means the user password
157    /// serves as both.
158    pub owner_password: Vec<u8>,
159    /// What a reader who opened with the user password may do.
160    pub permissions: pdfrum_crypt::Permissions,
161    /// Whether the document's XMP metadata stream is enciphered too.
162    pub encrypt_metadata: bool,
163}
164
165impl Default for SaveOptions {
166    fn default() -> Self {
167        Self {
168            mode: SaveMode::Full,
169            keep_original: true,
170            remove_security: false,
171            subset_new_fonts: false,
172            version: None,
173            id_source: IdSource::Random,
174            encrypt: None,
175        }
176    }
177}
178
179impl SaveOptions {
180    /// A builder over the defaults.
181    ///
182    /// ```
183    /// use pdfrum_edit::{SaveMode, SaveOptions};
184    ///
185    /// let options = SaveOptions::builder()
186    ///     .mode(SaveMode::Incremental)
187    ///     .subset_new_fonts(true)
188    ///     .build();
189    ///
190    /// assert_eq!(options.mode, SaveMode::Incremental);
191    /// assert!(options.subset_new_fonts);
192    /// ```
193    #[must_use]
194    pub fn builder() -> SaveOptionsBuilder {
195        SaveOptionsBuilder(SaveOptions::default())
196    }
197}
198
199/// Builds a [`SaveOptions`] one setting at a time.
200///
201/// Every setting starts at its [`SaveOptions::default`] value, so only the
202/// ones a caller cares about need naming.
203#[derive(Debug, Clone)]
204pub struct SaveOptionsBuilder(SaveOptions);
205
206impl SaveOptionsBuilder {
207    /// Whether to append or rewrite.
208    #[must_use]
209    pub fn mode(mut self, mode: SaveMode) -> Self {
210        self.0.mode = mode;
211        self
212    }
213
214    /// Keep the original bytes as the file's prefix.
215    #[must_use]
216    pub fn keep_original(mut self, keep: bool) -> Self {
217        self.0.keep_original = keep;
218        self
219    }
220
221    /// Drop the security handler, writing the document in the clear.
222    #[must_use]
223    pub fn remove_security(mut self, remove: bool) -> Self {
224        self.0.remove_security = remove;
225        self
226    }
227
228    /// Subset newly embedded fonts.
229    #[must_use]
230    pub fn subset_new_fonts(mut self, subset: bool) -> Self {
231        self.0.subset_new_fonts = subset;
232        self
233    }
234
235    /// The version to declare in the header.
236    #[must_use]
237    pub fn version(mut self, version: PdfVersion) -> Self {
238        self.0.version = Some(version);
239        self
240    }
241
242    /// Where `/ID` and subset tags come from.
243    #[must_use]
244    pub fn id_source(mut self, source: IdSource) -> Self {
245        self.0.id_source = source;
246        self
247    }
248
249    /// Encrypt an unencrypted document on the way out.
250    #[must_use]
251    pub fn encrypt(mut self, encryption: Encryption) -> Self {
252        self.0.encrypt = Some(encryption);
253        self
254    }
255
256    /// The options built so far.
257    #[must_use]
258    pub fn build(self) -> SaveOptions {
259        self.0
260    }
261}
262
263impl Encryption {
264    /// A builder over the defaults: no passwords, every permission granted,
265    /// and the metadata stream enciphered with the rest.
266    ///
267    /// ```
268    /// use pdfrum_edit::Encryption;
269    ///
270    /// let encryption = Encryption::builder()
271    ///     .user_password(b"open-me".to_vec())
272    ///     .encrypt_metadata(false)
273    ///     .build();
274    ///
275    /// assert_eq!(encryption.user_password, b"open-me");
276    /// assert!(!encryption.encrypt_metadata);
277    /// ```
278    #[must_use]
279    pub fn builder() -> EncryptionBuilder {
280        EncryptionBuilder(Encryption {
281            user_password: Vec::new(),
282            owner_password: Vec::new(),
283            permissions: pdfrum_crypt::Permissions::ALL,
284            encrypt_metadata: true,
285        })
286    }
287}
288
289/// Builds an [`Encryption`] one setting at a time.
290#[derive(Debug, Clone)]
291pub struct EncryptionBuilder(Encryption);
292
293impl EncryptionBuilder {
294    /// Opens the document with the rights `permissions` grants.
295    #[must_use]
296    pub fn user_password(mut self, password: Vec<u8>) -> Self {
297        self.0.user_password = password;
298        self
299    }
300
301    /// Opens the document with every right.
302    #[must_use]
303    pub fn owner_password(mut self, password: Vec<u8>) -> Self {
304        self.0.owner_password = password;
305        self
306    }
307
308    /// What a reader who opened with the user password may do.
309    #[must_use]
310    pub fn permissions(mut self, permissions: pdfrum_crypt::Permissions) -> Self {
311        self.0.permissions = permissions;
312        self
313    }
314
315    /// Whether the XMP metadata stream is enciphered too.
316    #[must_use]
317    pub fn encrypt_metadata(mut self, encrypt: bool) -> Self {
318        self.0.encrypt_metadata = encrypt;
319        self
320    }
321
322    /// The encryption built so far.
323    #[must_use]
324    pub fn build(self) -> Encryption {
325        self.0
326    }
327}
328
329/// A sink that remembers how many bytes have gone through it.
330///
331/// Every cross-reference offset is a byte count from the start of the output,
332/// so the writer needs a running total. This is the C++'s buffered archive
333/// minus its hand-rolled 32 KiB buffer — buffering is the caller's choice,
334/// through a `BufWriter`.
335struct Counting<W: Write> {
336    inner: W,
337    written: u64,
338}
339
340impl<W: Write> Counting<W> {
341    fn new(inner: W) -> Self {
342        Self { inner, written: 0 }
343    }
344
345    fn write(&mut self, bytes: &[u8]) -> Result<(), Error> {
346        self.inner.write_all(bytes)?;
347        self.written = self.written.saturating_add(bytes.len() as u64);
348        Ok(())
349    }
350
351    /// The offset the next byte will land at.
352    const fn offset(&self) -> u64 {
353        self.written
354    }
355}
356
357/// Write `doc` to `out`.
358///
359/// # Errors
360///
361/// [`Error::EncryptedSaveUnsupported`] when the document declares `/Encrypt`
362/// but this reader never derived a key for it — an `/Identity` crypt filter,
363/// or a handler we opened as [`pdfrum_crypt::SecurityHandler::Identity`] —
364/// and `remove_security` was not set, because re-declaring a cipher over
365/// plaintext would produce a file nothing could open. And [`Error::Io`] when
366/// the sink refuses the bytes.
367///
368/// Damage in the input is not an error: an object that cannot be fetched is
369/// dropped from both the body and the cross-reference, exactly as the C++
370/// writer drops it.
371pub fn save(doc: &EditDoc<'_>, opts: &SaveOptions, out: &mut impl Write) -> Result<(), Error> {
372    let base = doc.base();
373
374    // The document's own handler, when the save is to stay encrypted. A
375    // `/Encrypt` we could not key — `/Identity`, or a filter this reader
376    // answered with the identity handler — would be re-declared over
377    // plaintext, which is the one shape that opens for nobody.
378    let SecurityPlan {
379        declared,
380        keep_security,
381        fresh,
382    } = security_plan(base, opts)?;
383    let handler = base.security_handler();
384
385    let id = file_id(base, opts);
386
387    // Three things force a full save. A rebuilt cross-reference has no
388    // previous section to name in `/Prev`; a rekey makes the original bytes
389    // unreadable under the new key; and removing security means the appended
390    // objects would be plaintext behind ciphertext.
391    let forced_full = base.xref_was_rebuilt()
392        || id.rekeyed
393        || (declared && opts.remove_security)
394        || fresh.is_some();
395    let incremental = opts.mode == SaveMode::Incremental && !forced_full;
396
397    // ---- the security seam ----
398    //
399    // The number the `/Encrypt` dictionary will be written as decides two
400    // things at once: which object the encryptor skips, and which one the
401    // body loops leave to the dedicated stage below.
402    let slot = choose_slot(
403        doc,
404        base,
405        fresh.as_ref().map(|(dict, _)| dict),
406        keep_security,
407    );
408    let encrypt_number = slot.as_ref().map(|s| s.number);
409    let active_handler = fresh.as_ref().map_or(handler, |(_, h)| h);
410    let security = if keep_security || fresh.is_some() {
411        Some(encrypt::Security {
412            handler: active_handler,
413            ivs: encrypt::IvSource::from_os()?,
414            encrypt_object: encrypt_number,
415        })
416    } else {
417        None
418    };
419
420    let mut sink = Counting::new(out);
421    let mut offsets = ObjectOffsets::new();
422
423    // ---- header, or the original bytes ----
424    write_front(&mut sink, base, opts, incremental)?;
425
426    // ---- partition ----
427    let (old_nums, new_nums) = partition(doc, incremental);
428
429    // ---- old objects, garbage-collected ----
430    //
431    // The trailer is the edited one: an `/Info` the session added is reached
432    // from it and named by it.
433    let trailer_dict = doc.trailer();
434    let reach = reach::walk(&trailer_dict, base.trailer_object_number(), doc);
435    for num in old_nums {
436        // A full save keeps only what the trailer can still reach.
437        if !reach.is_reachable(num) || encrypt_number == Some(num) {
438            continue;
439        }
440        write_one(&mut sink, &mut offsets, doc, num, security.as_ref())?;
441    }
442
443    // ---- new objects, written whether or not anything points at them ----
444    //
445    // The font subsetter is a lookup in this loop and nothing more, which is
446    // the shape `WriteNewObjs` (`:203-226`) has: it produces replacement
447    // objects for the font ones among the new numbers, and each object is
448    // written through the map. It may also mint the `/CIDToGIDMap` that
449    // absorbs the glyph renumbering, so the numbers it added are appended to
450    // this loop's list before it runs.
451    let mut new_nums = new_nums;
452    let overrides = subset_fonts(doc, opts, encrypt_number, &mut new_nums);
453    for num in new_nums.iter().copied() {
454        // A newly added object is written even when nothing references it:
455        // the caller added it on purpose, and the sweep above cannot see an
456        // intent that has not been wired up yet.
457        if encrypt_number == Some(num) {
458            continue;
459        }
460        match overrides.get(&num) {
461            Some(object) => write_override(&mut sink, &mut offsets, num, object, security.as_ref()),
462            None => write_one(&mut sink, &mut offsets, doc, num, security.as_ref()),
463        }?;
464    }
465
466    // ---- the encrypt dictionary ----
467    //
468    // Written here rather than by the loops above, whether the file held it
469    // inline or indirectly, for a reason that is not about encryption at all:
470    // it must be written **from the plaintext copy the trailer lookup found**,
471    // not from the object store. The store deciphers every string it hands
472    // out and has no exemption for this object, so fetching `/Encrypt`
473    // through it yields `/O` and `/U` run through a cipher keyed by the very
474    // material they carry. The C++ never has to think about this — it keeps
475    // the dictionary in a field beside the handler and writes that.
476    //
477    // A file that wrote the dictionary inline additionally needs the fresh
478    // object number `encrypt_slot` minted, since ISO 32000-1 §7.6.1 requires
479    // the trailer name it by reference.
480    if let Some(EncryptSlot { number, dict }) = &slot {
481        offsets.set(*number, sink.offset());
482        let mut bytes = Vec::new();
483        // And no encryptor, which is the rule ISO 32000-1 §7.6.1 states: a
484        // reader parses this dictionary before it has a key.
485        object::write_indirect(&mut bytes, *number, &Object::Dict(dict.clone()), None);
486        sink.write(&bytes)?;
487        if incremental && !new_nums.contains(number) {
488            // Appended without re-sorting. Safe for a promoted dictionary
489            // because its number is above everything already there, and for
490            // an indirect one because the guard above kept it out.
491            new_nums.push(*number);
492        }
493    }
494
495    let last_written = offsets.last();
496
497    // ---- cross-reference ----
498    let xref_start = sink.offset();
499    let as_stream = incremental && base.main_xref_is_stream();
500    let written: Vec<u32> = new_nums
501        .iter()
502        .copied()
503        .filter(|n| offsets.contains(*n))
504        .collect();
505    if !as_stream {
506        let mut table = Vec::new();
507        if incremental {
508            xref::classic_delta(&mut table, &offsets, &written);
509        } else {
510            xref::classic_full(&mut table, &offsets, last_written);
511        }
512        sink.write(&table)?;
513    }
514
515    // ---- trailer ----
516    let dict = trailer::build(trailer::TrailerParts {
517        source: &trailer_dict,
518        id: &id.array,
519        last_object_number: last_written,
520        prev: (incremental && base.last_xref_offset() > 0).then(|| base.last_xref_offset()),
521        encrypt: slot.as_ref().map(|s| s.number),
522    });
523
524    let mut tail = Vec::new();
525    if as_stream {
526        // The trailer object's own number comes from the document, not from
527        // the highest object written, so it can sit above `/Size − 2`.
528        let num = doc.last_object_number().saturating_add(1);
529        trailer::write_stream(&mut tail, num, &dict, &offsets, &written);
530    } else {
531        trailer::write_classic(&mut tail, &dict);
532    }
533    trailer::write_tail(&mut tail, xref_start);
534    sink.write(&tail)?;
535
536    Ok(())
537}
538
539/// Split the objects in play into the ones written from the file's own table
540/// and the ones written as additions.
541///
542/// A **full** save treats an object as new when the cross-reference does not
543/// name it, or names its slot free. Everything else is old — even an object
544/// the caller replaced, because the old path re-fetches through the overlay
545/// and so sees the replacement anyway.
546///
547/// An **incremental** save treats every object in play as new, because the
548/// appended section must carry a fresh copy of anything that changed. That is
549/// why an incremental save's size grows with how much of the document has
550/// been touched.
551fn partition(doc: &EditDoc<'_>, incremental: bool) -> (Vec<u32>, Vec<u32>) {
552    let base = doc.base();
553    let xref = base.xref();
554
555    if incremental {
556        let mut new: Vec<u32> = doc.edited().map(|(n, _)| n).collect();
557        new.sort_unstable();
558        new.dedup();
559        return (Vec::new(), new);
560    }
561
562    let last = xref.last_object_number();
563    let old: Vec<u32> = (1..=last)
564        .filter(|n| !doc.is_removed(*n))
565        .filter(|n| !matches!(xref.entry(*n), None | Some(pdfrum_parser::Entry::Free)))
566        .collect();
567
568    let mut new: Vec<u32> = doc
569        .edited()
570        .map(|(n, _)| n)
571        .filter(|n| {
572            !xref.is_valid_object_number(*n)
573                || matches!(xref.entry(*n), None | Some(pdfrum_parser::Entry::Free))
574        })
575        .collect();
576    new.sort_unstable();
577    new.dedup();
578    (old, new)
579}
580
581/// Where the `/Encrypt` dictionary goes on this save, and what to write there.
582///
583/// `dict` is the **plaintext** dictionary, taken from the trailer lookup that
584/// reads it through a store deciphering nothing — see the writing stage for
585/// why fetching it the ordinary way would corrupt it.
586#[derive(Debug, Clone)]
587struct EncryptSlot {
588    /// The object number the trailer's `/Encrypt` will point at.
589    number: u32,
590    /// The dictionary to write there.
591    dict: pdfrum_object::Dict,
592}
593
594/// Decide the `/Encrypt` dictionary's object number for this save.
595///
596/// A trailer naming it by reference already answers the question. One holding
597/// it inline does not, so the number is minted one past everything in play —
598/// which is what makes the incremental append-without-sorting sound, and what
599/// ISO 32000-1 §7.6.1 requires, since the trailer must name it by reference.
600///
601/// `None` when the trailer's `/Encrypt` is neither a dictionary nor a
602/// reference: there is no dictionary to point at, so the save writes no
603/// `/Encrypt`, and `save`'s plaintext check has already refused the one shape
604/// where that would produce an unopenable file.
605/// The trailer `/ID` this save writes, from the document's own and the
606/// options' source of fresh bytes.
607fn file_id(base: &pdfrum_parser::Document, opts: &SaveOptions) -> id::FileId {
608    id::build(
609        IdContext {
610            old: None,
611            encrypt: base.encrypt_dict().map(|(d, _)| d),
612            incremental: opts.mode == SaveMode::Incremental,
613        }
614        .with_old(base.trailer()),
615        opts.id_source,
616    )
617}
618
619/// The bytes before the first object: the original file when appending to
620/// it, a header otherwise. The original is copied verbatim; nothing in it is
621/// ever rewritten, which is what keeps signatures and byte-range digests
622/// valid.
623fn write_front(
624    sink: &mut Counting<impl Write>,
625    base: &pdfrum_parser::Document,
626    opts: &SaveOptions,
627    incremental: bool,
628) -> Result<(), Error> {
629    if incremental && opts.keep_original {
630        sink.write(base.bytes())
631    } else {
632        let mut header = Vec::new();
633        write_header(&mut header, opts.version, base.version());
634        sink.write(&header)
635    }
636}
637
638/// What the save does about security, decided before a byte is written.
639struct SecurityPlan {
640    /// The document carries an `/Encrypt` of its own.
641    declared: bool,
642    /// That handler stays in force for the output.
643    keep_security: bool,
644    /// A new handler, when the save is to encrypt an unencrypted document.
645    fresh: Option<(pdfrum_object::Dict, pdfrum_crypt::SecurityHandler)>,
646}
647
648fn security_plan(
649    base: &pdfrum_parser::Document,
650    opts: &SaveOptions,
651) -> Result<SecurityPlan, Error> {
652    let declared = base.encrypt_dict().is_some();
653    if declared && opts.encrypt.is_some() {
654        // Re-keying an encrypted document is not a save option: decrypt it
655        // (`remove_security`) and encrypt the result in a second save.
656        return Err(Error::EncryptedSaveUnsupported);
657    }
658    let keep_security = declared && !opts.remove_security;
659    if keep_security
660        && matches!(
661            base.security_handler(),
662            pdfrum_crypt::SecurityHandler::Identity
663        )
664    {
665        return Err(Error::EncryptedSaveUnsupported);
666    }
667    Ok(SecurityPlan {
668        declared,
669        keep_security,
670        fresh: fresh_encryption(opts)?,
671    })
672}
673
674/// A fresh handler when the save is to encrypt: built once, held for the
675/// writer's lifetime beside the document's own.
676fn fresh_encryption(
677    opts: &SaveOptions,
678) -> Result<Option<(pdfrum_object::Dict, pdfrum_crypt::SecurityHandler)>, Error> {
679    let Some(encryption) = &opts.encrypt else {
680        return Ok(None);
681    };
682    pdfrum_crypt::standard_r6(
683        &encryption.user_password,
684        &encryption.owner_password,
685        encryption.permissions,
686        encryption.encrypt_metadata,
687        &pdfrum_crypt::KeyMaterial::from_os().map_err(|_| Error::NoEntropy)?,
688    )
689    .map(Some)
690    .map_err(|_| Error::PasswordNotText)
691}
692
693/// Where the trailer's `/Encrypt` points: a new object for a fresh
694/// encryption, the document's own slot when its security is kept, nothing
695/// otherwise.
696fn choose_slot(
697    doc: &EditDoc<'_>,
698    base: &pdfrum_parser::Document,
699    fresh: Option<&pdfrum_object::Dict>,
700    keep_security: bool,
701) -> Option<EncryptSlot> {
702    match fresh {
703        Some(dict) => Some(EncryptSlot {
704            number: doc.last_object_number().saturating_add(1),
705            dict: dict.clone(),
706        }),
707        None => keep_security.then(|| encrypt_slot(doc, base)).flatten(),
708    }
709}
710
711fn encrypt_slot(doc: &EditDoc<'_>, base: &pdfrum_parser::Document) -> Option<EncryptSlot> {
712    let (dict, inline) = base.encrypt_dict()?;
713    let number = if inline {
714        doc.last_object_number().saturating_add(1)
715    } else {
716        base.trailer().reference(names::ENCRYPT)?.num
717    };
718    Some(EncryptSlot {
719        number,
720        dict: dict.clone(),
721    })
722}
723
724/// Run the font subsetter, if this save asked for it, and make room in the
725/// new-object list for anything it minted.
726///
727/// The map it returns is the one `WriteNewObjs` (`:203-226`) consults per
728/// object. An unset option, or a save with nothing new in it, gives an empty
729/// map and leaves `new_nums` alone.
730fn subset_fonts(
731    doc: &EditDoc<'_>,
732    opts: &SaveOptions,
733    encrypt_number: Option<u32>,
734    new_nums: &mut Vec<u32>,
735) -> font::overrides::Overrides {
736    if !opts.subset_new_fonts {
737        return font::overrides::Overrides::new();
738    }
739    // Where a `/CIDToGIDMap` the subsetter mints gets its number: one past
740    // everything in play, and past the `/Encrypt` slot too when this save is
741    // promoting an inline dictionary into a fresh number of its own.
742    let mut next = doc.last_object_number().saturating_add(1);
743    if let Some(number) = encrypt_number {
744        next = next.max(number.saturating_add(1));
745    }
746
747    let overrides = font::overrides::build(doc, new_nums, opts.id_source, &mut next);
748    // An override of an object already listed changes what is written there;
749    // one of a *minted* object adds a number the loop had not been going to
750    // visit, so the list grows and is re-sorted.
751    new_nums.extend(overrides.keys().copied());
752    new_nums.sort_unstable();
753    new_nums.dedup();
754    overrides
755}
756
757/// Write an object the subsetter produced in place of the document's own.
758///
759/// Separate from [`write_one`] because there is nothing to fetch and nothing
760/// that can fail: the object is already in hand, which is also why no offset
761/// ever has to be erased here.
762fn write_override<W: Write>(
763    sink: &mut Counting<W>,
764    offsets: &mut ObjectOffsets,
765    num: u32,
766    object: &Object,
767    security: Option<&encrypt::Security<'_>>,
768) -> Result<(), Error> {
769    offsets.set(num, sink.offset());
770    let enc = security.and_then(|s| s.for_object(num));
771    let mut bytes = Vec::new();
772    object::write_indirect(&mut bytes, num, object, enc.as_ref());
773    sink.write(&bytes)
774}
775
776/// Write one indirect object, recording where it landed.
777///
778/// The offset is recorded **before** the fetch and erased if the fetch fails,
779/// so a broken object vanishes from the body and the cross-reference together
780/// rather than leaving a table entry pointing at the next object's header.
781fn write_one<W: Write>(
782    sink: &mut Counting<W>,
783    offsets: &mut ObjectOffsets,
784    doc: &EditDoc<'_>,
785    num: u32,
786    security: Option<&encrypt::Security<'_>>,
787) -> Result<(), Error> {
788    offsets.set(num, sink.offset());
789    let Ok(obj) = doc.fetch(ObjRef::new(num, 0)) else {
790        offsets.erase(num);
791        return Ok(());
792    };
793    // A null carries no information a reader needs; the C++ writes it, but a
794    // free slot reads identically and costs nothing.
795    if obj.is_null() {
796        offsets.erase(num);
797        return Ok(());
798    }
799
800    // `for_object` is what refuses the `/Encrypt` dictionary its encryptor,
801    // so the rule lives in one place rather than at every call site.
802    let enc = security.and_then(|s| s.for_object(num));
803    let mut bytes = Vec::new();
804    object::write_indirect(&mut bytes, num, &obj, enc.as_ref());
805    sink.write(&bytes)
806}
807
808impl<'a> IdContext<'a> {
809    /// Fill in the trailer's own `/ID`, when it has one.
810    fn with_old(mut self, trailer: &'a pdfrum_object::Dict) -> Self {
811        self.old = match trailer.raw(names::ID) {
812            Some(Object::Array(a)) => Some(a),
813            _ => None,
814        };
815        self
816    }
817}