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}