1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum SaveMode {
36 #[default]
38 Full,
39 Incremental,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct Encryption {
46 pub user_password: Vec<u8>,
48 pub owner_password: Vec<u8>,
50 pub permissions: Permissions,
52 pub encrypt_metadata: bool,
54}
55
56#[derive(Clone)]
58pub struct SaveOptions {
59 pub mode: SaveMode,
61 pub version: Version,
64 pub object_streams: bool,
67 pub compress: bool,
69 pub encryption: Option<Encryption>,
71 pub document_id: Option<[u8; 16]>,
73 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
106const OBJSTM_SIZE: usize = 100;
108const MIN_COMPRESS: usize = 32;
110
111struct 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#[derive(Debug, Clone, Copy)]
134enum Slot {
135 Free(u16),
136 At(u64, u16),
137 In(u32, u32),
138}
139
140struct View<'a> {
143 store: &'a Store,
144 extra: HashMap<ObjId, Object>,
145 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 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 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 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 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 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 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 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 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 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 out.hash = Md5::new();
406
407 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
458fn 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 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
505fn 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
525fn 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
551fn 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 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
603fn 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
628fn 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
711fn 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}