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#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct SaveOptions {
91 /// Whether to append or rewrite.
92 pub mode: SaveMode,
93 /// Keep the original bytes as the file's prefix. Only an incremental save
94 /// reads this; clearing it there turns the save into a rewrite that keeps
95 /// the appended shape.
96 pub keep_original: bool,
97 /// Drop the security handler and `/Encrypt`, writing the document in the
98 /// clear.
99 ///
100 /// Off by default: an encrypted document saves encrypted under its own
101 /// handler, and opens with the password it was opened with. Setting this
102 /// is the explicit way to decrypt one on the way out — and it forces a
103 /// full save, since plaintext cannot be appended behind ciphertext.
104 ///
105 /// Has no effect on an unencrypted document.
106 pub remove_security: bool,
107 /// Subset newly embedded fonts, dropping the glyphs no page shows.
108 ///
109 /// Off by default. When set, every font this save writes as a **new**
110 /// object and that a show operator on some page draws with is replaced by
111 /// a subset carrying only the glyphs still used, named `ABCDEF+Original`
112 /// after ISO 32000-1 §9.6.4.
113 ///
114 /// **What it subsets**: a `/Type0` font whose descendant is a
115 /// `CIDFontType2` with an embedded `/FontFile2`. Nothing else — a Type 1
116 /// (`/FontFile`) or `OpenType`-CFF (`/FontFile3`, or an `OTTO` program)
117 /// font is left alone, and so is a *simple* TrueType font, whose codes
118 /// reach glyphs through a `cmap` the subsetter removes.
119 ///
120 /// **What it does not disturb**: the character codes on the page, the
121 /// CIDs they map to, `/W`, and `/ToUnicode`. The subsetter renumbers
122 /// glyphs, and a rewritten `/CIDToGIDMap` absorbs that renumbering at the
123 /// one place ISO 32000-1 §9.7.4.2 already provides for it — so **no
124 /// content stream is regenerated**, and text extraction over the saved
125 /// file is unchanged.
126 ///
127 /// A font whose program will not subset, or whose subset would not be
128 /// smaller, is written unchanged.
129 pub subset_new_fonts: bool,
130 /// The version to declare in the header. 1.0 through 1.7 are honoured;
131 /// anything outside that range, and `None`, keep the document's own.
132 pub version: Option<PdfVersion>,
133 /// Where `/ID` and subset tags come from.
134 pub id_source: IdSource,
135 /// Encrypt an unencrypted document on the way out: AES-256, revision 6,
136 /// under these passwords and permissions. `None` leaves the document as
137 /// it is. A document that is already encrypted cannot be re-keyed here:
138 /// asking for it is [`Error::EncryptedSaveUnsupported`].
139 pub encrypt: Option<Encryption>,
140}
141
142/// How a document is to be encrypted on save (ISO 32000-2 §7.6.4.4).
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct Encryption {
145 /// Opens the document with the rights `permissions` grants. Empty means
146 /// anyone can open it.
147 pub user_password: Vec<u8>,
148 /// Opens the document with every right. Empty means the user password
149 /// serves as both.
150 pub owner_password: Vec<u8>,
151 /// What a reader who opened with the user password may do.
152 pub permissions: pdfrum_crypt::Permissions,
153 /// Whether the document's XMP metadata stream is enciphered too.
154 pub encrypt_metadata: bool,
155}
156
157impl Default for SaveOptions {
158 fn default() -> Self {
159 Self {
160 mode: SaveMode::Full,
161 keep_original: true,
162 remove_security: false,
163 subset_new_fonts: false,
164 version: None,
165 id_source: IdSource::Random,
166 encrypt: None,
167 }
168 }
169}
170
171/// A sink that remembers how many bytes have gone through it.
172///
173/// Every cross-reference offset is a byte count from the start of the output,
174/// so the writer needs a running total. This is the C++'s buffered archive
175/// minus its hand-rolled 32 KiB buffer — buffering is the caller's choice,
176/// through a `BufWriter`.
177struct Counting<W: Write> {
178 inner: W,
179 written: u64,
180}
181
182impl<W: Write> Counting<W> {
183 fn new(inner: W) -> Self {
184 Self { inner, written: 0 }
185 }
186
187 fn write(&mut self, bytes: &[u8]) -> Result<(), Error> {
188 self.inner.write_all(bytes)?;
189 self.written = self.written.saturating_add(bytes.len() as u64);
190 Ok(())
191 }
192
193 /// The offset the next byte will land at.
194 const fn offset(&self) -> u64 {
195 self.written
196 }
197}
198
199/// Write `doc` to `out`.
200///
201/// # Errors
202///
203/// [`Error::EncryptedSaveUnsupported`] when the document declares `/Encrypt`
204/// but this reader never derived a key for it — an `/Identity` crypt filter,
205/// or a handler we opened as [`pdfrum_crypt::SecurityHandler::Identity`] —
206/// and `remove_security` was not set, because re-declaring a cipher over
207/// plaintext would produce a file nothing could open. And [`Error::Io`] when
208/// the sink refuses the bytes.
209///
210/// Damage in the input is not an error: an object that cannot be fetched is
211/// dropped from both the body and the cross-reference, exactly as the C++
212/// writer drops it.
213pub fn save(doc: &EditDoc<'_>, opts: &SaveOptions, out: &mut impl Write) -> Result<(), Error> {
214 let base = doc.base();
215
216 // The document's own handler, when the save is to stay encrypted. A
217 // `/Encrypt` we could not key — `/Identity`, or a filter this reader
218 // answered with the identity handler — would be re-declared over
219 // plaintext, which is the one shape that opens for nobody.
220 let SecurityPlan {
221 declared,
222 keep_security,
223 fresh,
224 } = security_plan(base, opts)?;
225 let handler = base.security_handler();
226
227 let id = file_id(base, opts);
228
229 // Three things force a full save. A rebuilt cross-reference has no
230 // previous section to name in `/Prev`; a rekey makes the original bytes
231 // unreadable under the new key; and removing security means the appended
232 // objects would be plaintext behind ciphertext.
233 let forced_full = base.xref_was_rebuilt()
234 || id.rekeyed
235 || (declared && opts.remove_security)
236 || fresh.is_some();
237 let incremental = opts.mode == SaveMode::Incremental && !forced_full;
238
239 // ---- the security seam ----
240 //
241 // The number the `/Encrypt` dictionary will be written as decides two
242 // things at once: which object the encryptor skips, and which one the
243 // body loops leave to the dedicated stage below.
244 let slot = choose_slot(
245 doc,
246 base,
247 fresh.as_ref().map(|(dict, _)| dict),
248 keep_security,
249 );
250 let encrypt_number = slot.as_ref().map(|s| s.number);
251 let active_handler = fresh.as_ref().map_or(handler, |(_, h)| h);
252 let security = if keep_security || fresh.is_some() {
253 Some(encrypt::Security {
254 handler: active_handler,
255 ivs: encrypt::IvSource::from_os()?,
256 encrypt_object: encrypt_number,
257 })
258 } else {
259 None
260 };
261
262 let mut sink = Counting::new(out);
263 let mut offsets = ObjectOffsets::new();
264
265 // ---- header, or the original bytes ----
266 write_front(&mut sink, base, opts, incremental)?;
267
268 // ---- partition ----
269 let (old_nums, new_nums) = partition(doc, incremental);
270
271 // ---- old objects, garbage-collected ----
272 //
273 // The trailer is the edited one: an `/Info` the session added is reached
274 // from it and named by it.
275 let trailer_dict = doc.trailer();
276 let reach = reach::walk(&trailer_dict, base.trailer_object_number(), doc);
277 for num in old_nums {
278 // A full save keeps only what the trailer can still reach.
279 if !reach.is_reachable(num) || encrypt_number == Some(num) {
280 continue;
281 }
282 write_one(&mut sink, &mut offsets, doc, num, security.as_ref())?;
283 }
284
285 // ---- new objects, written whether or not anything points at them ----
286 //
287 // The font subsetter is a lookup in this loop and nothing more, which is
288 // the shape `WriteNewObjs` (`:203-226`) has: it produces replacement
289 // objects for the font ones among the new numbers, and each object is
290 // written through the map. It may also mint the `/CIDToGIDMap` that
291 // absorbs the glyph renumbering, so the numbers it added are appended to
292 // this loop's list before it runs.
293 let mut new_nums = new_nums;
294 let overrides = subset_fonts(doc, opts, encrypt_number, &mut new_nums);
295 for num in new_nums.iter().copied() {
296 // A newly added object is written even when nothing references it:
297 // the caller added it on purpose, and the sweep above cannot see an
298 // intent that has not been wired up yet.
299 if encrypt_number == Some(num) {
300 continue;
301 }
302 match overrides.get(&num) {
303 Some(object) => write_override(&mut sink, &mut offsets, num, object, security.as_ref()),
304 None => write_one(&mut sink, &mut offsets, doc, num, security.as_ref()),
305 }?;
306 }
307
308 // ---- the encrypt dictionary ----
309 //
310 // Written here rather than by the loops above, whether the file held it
311 // inline or indirectly, for a reason that is not about encryption at all:
312 // it must be written **from the plaintext copy the trailer lookup found**,
313 // not from the object store. The store deciphers every string it hands
314 // out and has no exemption for this object, so fetching `/Encrypt`
315 // through it yields `/O` and `/U` run through a cipher keyed by the very
316 // material they carry. The C++ never has to think about this — it keeps
317 // the dictionary in a field beside the handler and writes that.
318 //
319 // A file that wrote the dictionary inline additionally needs the fresh
320 // object number `encrypt_slot` minted, since ISO 32000-1 §7.6.1 requires
321 // the trailer name it by reference.
322 if let Some(EncryptSlot { number, dict }) = &slot {
323 offsets.set(*number, sink.offset());
324 let mut bytes = Vec::new();
325 // And no encryptor, which is the rule ISO 32000-1 §7.6.1 states: a
326 // reader parses this dictionary before it has a key.
327 object::write_indirect(&mut bytes, *number, &Object::Dict(dict.clone()), None);
328 sink.write(&bytes)?;
329 if incremental && !new_nums.contains(number) {
330 // Appended without re-sorting. Safe for a promoted dictionary
331 // because its number is above everything already there, and for
332 // an indirect one because the guard above kept it out.
333 new_nums.push(*number);
334 }
335 }
336
337 let last_written = offsets.last();
338
339 // ---- cross-reference ----
340 let xref_start = sink.offset();
341 let as_stream = incremental && base.main_xref_is_stream();
342 let written: Vec<u32> = new_nums
343 .iter()
344 .copied()
345 .filter(|n| offsets.contains(*n))
346 .collect();
347 if !as_stream {
348 let mut table = Vec::new();
349 if incremental {
350 xref::classic_delta(&mut table, &offsets, &written);
351 } else {
352 xref::classic_full(&mut table, &offsets, last_written);
353 }
354 sink.write(&table)?;
355 }
356
357 // ---- trailer ----
358 let dict = trailer::build(trailer::TrailerParts {
359 source: &trailer_dict,
360 id: &id.array,
361 last_object_number: last_written,
362 prev: (incremental && base.last_xref_offset() > 0).then(|| base.last_xref_offset()),
363 encrypt: slot.as_ref().map(|s| s.number),
364 });
365
366 let mut tail = Vec::new();
367 if as_stream {
368 // The trailer object's own number comes from the document, not from
369 // the highest object written, so it can sit above `/Size − 2`.
370 let num = doc.last_object_number().saturating_add(1);
371 trailer::write_stream(&mut tail, num, &dict, &offsets, &written);
372 } else {
373 trailer::write_classic(&mut tail, &dict);
374 }
375 trailer::write_tail(&mut tail, xref_start);
376 sink.write(&tail)?;
377
378 Ok(())
379}
380
381/// Split the objects in play into the ones written from the file's own table
382/// and the ones written as additions.
383///
384/// A **full** save treats an object as new when the cross-reference does not
385/// name it, or names its slot free. Everything else is old — even an object
386/// the caller replaced, because the old path re-fetches through the overlay
387/// and so sees the replacement anyway.
388///
389/// An **incremental** save treats every object in play as new, because the
390/// appended section must carry a fresh copy of anything that changed. That is
391/// why an incremental save's size grows with how much of the document has
392/// been touched.
393fn partition(doc: &EditDoc<'_>, incremental: bool) -> (Vec<u32>, Vec<u32>) {
394 let base = doc.base();
395 let xref = base.xref();
396
397 if incremental {
398 let mut new: Vec<u32> = doc.edited().map(|(n, _)| n).collect();
399 new.sort_unstable();
400 new.dedup();
401 return (Vec::new(), new);
402 }
403
404 let last = xref.last_object_number();
405 let old: Vec<u32> = (1..=last)
406 .filter(|n| !doc.is_removed(*n))
407 .filter(|n| !matches!(xref.entry(*n), None | Some(pdfrum_parser::Entry::Free)))
408 .collect();
409
410 let mut new: Vec<u32> = doc
411 .edited()
412 .map(|(n, _)| n)
413 .filter(|n| {
414 !xref.is_valid_object_number(*n)
415 || matches!(xref.entry(*n), None | Some(pdfrum_parser::Entry::Free))
416 })
417 .collect();
418 new.sort_unstable();
419 new.dedup();
420 (old, new)
421}
422
423/// Where the `/Encrypt` dictionary goes on this save, and what to write there.
424///
425/// `dict` is the **plaintext** dictionary, taken from the trailer lookup that
426/// reads it through a store deciphering nothing — see the writing stage for
427/// why fetching it the ordinary way would corrupt it.
428#[derive(Debug, Clone)]
429struct EncryptSlot {
430 /// The object number the trailer's `/Encrypt` will point at.
431 number: u32,
432 /// The dictionary to write there.
433 dict: pdfrum_object::Dict,
434}
435
436/// Decide the `/Encrypt` dictionary's object number for this save.
437///
438/// A trailer naming it by reference already answers the question. One holding
439/// it inline does not, so the number is minted one past everything in play —
440/// which is what makes the incremental append-without-sorting sound, and what
441/// ISO 32000-1 §7.6.1 requires, since the trailer must name it by reference.
442///
443/// `None` when the trailer's `/Encrypt` is neither a dictionary nor a
444/// reference: there is no dictionary to point at, so the save writes no
445/// `/Encrypt`, and `save`'s plaintext check has already refused the one shape
446/// where that would produce an unopenable file.
447/// The trailer `/ID` this save writes, from the document's own and the
448/// options' source of fresh bytes.
449fn file_id(base: &pdfrum_parser::Document, opts: &SaveOptions) -> id::FileId {
450 id::build(
451 IdContext {
452 old: None,
453 encrypt: base.encrypt_dict().map(|(d, _)| d),
454 incremental: opts.mode == SaveMode::Incremental,
455 }
456 .with_old(base.trailer()),
457 opts.id_source,
458 )
459}
460
461/// The bytes before the first object: the original file when appending to
462/// it, a header otherwise. The original is copied verbatim; nothing in it is
463/// ever rewritten, which is what keeps signatures and byte-range digests
464/// valid.
465fn write_front(
466 sink: &mut Counting<impl Write>,
467 base: &pdfrum_parser::Document,
468 opts: &SaveOptions,
469 incremental: bool,
470) -> Result<(), Error> {
471 if incremental && opts.keep_original {
472 sink.write(base.bytes())
473 } else {
474 let mut header = Vec::new();
475 write_header(&mut header, opts.version, base.version());
476 sink.write(&header)
477 }
478}
479
480/// What the save does about security, decided before a byte is written.
481struct SecurityPlan {
482 /// The document carries an `/Encrypt` of its own.
483 declared: bool,
484 /// That handler stays in force for the output.
485 keep_security: bool,
486 /// A new handler, when the save is to encrypt an unencrypted document.
487 fresh: Option<(pdfrum_object::Dict, pdfrum_crypt::SecurityHandler)>,
488}
489
490fn security_plan(
491 base: &pdfrum_parser::Document,
492 opts: &SaveOptions,
493) -> Result<SecurityPlan, Error> {
494 let declared = base.encrypt_dict().is_some();
495 if declared && opts.encrypt.is_some() {
496 // Re-keying an encrypted document is not a save option: decrypt it
497 // (`remove_security`) and encrypt the result in a second save.
498 return Err(Error::EncryptedSaveUnsupported);
499 }
500 let keep_security = declared && !opts.remove_security;
501 if keep_security
502 && matches!(
503 base.security_handler(),
504 pdfrum_crypt::SecurityHandler::Identity
505 )
506 {
507 return Err(Error::EncryptedSaveUnsupported);
508 }
509 Ok(SecurityPlan {
510 declared,
511 keep_security,
512 fresh: fresh_encryption(opts)?,
513 })
514}
515
516/// A fresh handler when the save is to encrypt: built once, held for the
517/// writer's lifetime beside the document's own.
518fn fresh_encryption(
519 opts: &SaveOptions,
520) -> Result<Option<(pdfrum_object::Dict, pdfrum_crypt::SecurityHandler)>, Error> {
521 let Some(encryption) = &opts.encrypt else {
522 return Ok(None);
523 };
524 pdfrum_crypt::standard_r6(
525 &encryption.user_password,
526 &encryption.owner_password,
527 encryption.permissions,
528 encryption.encrypt_metadata,
529 &pdfrum_crypt::KeyMaterial::from_os().map_err(|_| Error::NoEntropy)?,
530 )
531 .map(Some)
532 .map_err(|_| Error::PasswordNotText)
533}
534
535/// Where the trailer's `/Encrypt` points: a new object for a fresh
536/// encryption, the document's own slot when its security is kept, nothing
537/// otherwise.
538fn choose_slot(
539 doc: &EditDoc<'_>,
540 base: &pdfrum_parser::Document,
541 fresh: Option<&pdfrum_object::Dict>,
542 keep_security: bool,
543) -> Option<EncryptSlot> {
544 match fresh {
545 Some(dict) => Some(EncryptSlot {
546 number: doc.last_object_number().saturating_add(1),
547 dict: dict.clone(),
548 }),
549 None => keep_security.then(|| encrypt_slot(doc, base)).flatten(),
550 }
551}
552
553fn encrypt_slot(doc: &EditDoc<'_>, base: &pdfrum_parser::Document) -> Option<EncryptSlot> {
554 let (dict, inline) = base.encrypt_dict()?;
555 let number = if inline {
556 doc.last_object_number().saturating_add(1)
557 } else {
558 base.trailer().reference(names::ENCRYPT)?.num
559 };
560 Some(EncryptSlot {
561 number,
562 dict: dict.clone(),
563 })
564}
565
566/// Run the font subsetter, if this save asked for it, and make room in the
567/// new-object list for anything it minted.
568///
569/// The map it returns is the one `WriteNewObjs` (`:203-226`) consults per
570/// object. An unset option, or a save with nothing new in it, gives an empty
571/// map and leaves `new_nums` alone.
572fn subset_fonts(
573 doc: &EditDoc<'_>,
574 opts: &SaveOptions,
575 encrypt_number: Option<u32>,
576 new_nums: &mut Vec<u32>,
577) -> font::overrides::Overrides {
578 if !opts.subset_new_fonts {
579 return font::overrides::Overrides::new();
580 }
581 // Where a `/CIDToGIDMap` the subsetter mints gets its number: one past
582 // everything in play, and past the `/Encrypt` slot too when this save is
583 // promoting an inline dictionary into a fresh number of its own.
584 let mut next = doc.last_object_number().saturating_add(1);
585 if let Some(number) = encrypt_number {
586 next = next.max(number.saturating_add(1));
587 }
588
589 let overrides = font::overrides::build(doc, new_nums, opts.id_source, &mut next);
590 // An override of an object already listed changes what is written there;
591 // one of a *minted* object adds a number the loop had not been going to
592 // visit, so the list grows and is re-sorted.
593 new_nums.extend(overrides.keys().copied());
594 new_nums.sort_unstable();
595 new_nums.dedup();
596 overrides
597}
598
599/// Write an object the subsetter produced in place of the document's own.
600///
601/// Separate from [`write_one`] because there is nothing to fetch and nothing
602/// that can fail: the object is already in hand, which is also why no offset
603/// ever has to be erased here.
604fn write_override<W: Write>(
605 sink: &mut Counting<W>,
606 offsets: &mut ObjectOffsets,
607 num: u32,
608 object: &Object,
609 security: Option<&encrypt::Security<'_>>,
610) -> Result<(), Error> {
611 offsets.set(num, sink.offset());
612 let enc = security.and_then(|s| s.for_object(num));
613 let mut bytes = Vec::new();
614 object::write_indirect(&mut bytes, num, object, enc.as_ref());
615 sink.write(&bytes)
616}
617
618/// Write one indirect object, recording where it landed.
619///
620/// The offset is recorded **before** the fetch and erased if the fetch fails,
621/// so a broken object vanishes from the body and the cross-reference together
622/// rather than leaving a table entry pointing at the next object's header.
623fn write_one<W: Write>(
624 sink: &mut Counting<W>,
625 offsets: &mut ObjectOffsets,
626 doc: &EditDoc<'_>,
627 num: u32,
628 security: Option<&encrypt::Security<'_>>,
629) -> Result<(), Error> {
630 offsets.set(num, sink.offset());
631 let Ok(obj) = doc.fetch(ObjRef::new(num, 0)) else {
632 offsets.erase(num);
633 return Ok(());
634 };
635 // A null carries no information a reader needs; the C++ writes it, but a
636 // free slot reads identically and costs nothing.
637 if obj.is_null() {
638 offsets.erase(num);
639 return Ok(());
640 }
641
642 // `for_object` is what refuses the `/Encrypt` dictionary its encryptor,
643 // so the rule lives in one place rather than at every call site.
644 let enc = security.and_then(|s| s.for_object(num));
645 let mut bytes = Vec::new();
646 object::write_indirect(&mut bytes, num, &obj, enc.as_ref());
647 sink.write(&bytes)
648}
649
650impl<'a> IdContext<'a> {
651 /// Fill in the trailer's own `/ID`, when it has one.
652 fn with_old(mut self, trailer: &'a pdfrum_object::Dict) -> Self {
653 self.old = match trailer.raw(names::ID) {
654 Some(Object::Array(a)) => Some(a),
655 _ => None,
656 };
657 self
658 }
659}