1use std::io::Write;
15
16use flate2::write::ZlibEncoder;
17use flate2::Compression;
18use pdfboss_core::crypt::Sha256;
19use pdfboss_core::{block_on, Dict, Encryptor, Name, ObjRef, Object, Stream};
20
21use crate::error::{Error, Result};
22use crate::ser::{serialize_dict, serialize_object};
23use crate::sink::{AsyncByteSink, Immediate};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum XrefStyle {
28 Table,
31 #[default]
34 Stream,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq)]
39pub struct WriteOptions {
40 pub xref: XrefStyle,
42 pub compress: bool,
44 pub object_streams: bool,
47 pub version: (u8, u8),
49}
50
51impl Default for WriteOptions {
52 fn default() -> WriteOptions {
53 WriteOptions {
54 xref: XrefStyle::Stream,
55 compress: true,
56 object_streams: true,
57 version: (1, 7),
58 }
59 }
60}
61
62pub struct Writer {
66 options: WriteOptions,
67 slots: Vec<Slot>,
68 info: Option<ObjRef>,
69 encryption: Option<(Encryptor, Dict)>,
74}
75
76impl std::fmt::Debug for Writer {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.debug_struct("Writer")
81 .field("options", &self.options)
82 .field("slots", &self.slots)
83 .field("info", &self.info)
84 .field("encrypted", &self.encryption.is_some())
85 .finish()
86 }
87}
88
89#[derive(Debug)]
91enum Slot {
92 Reserved,
93 Filled(Object),
94}
95
96impl Writer {
97 pub fn new(options: WriteOptions) -> Writer {
99 Writer {
100 options,
101 slots: Vec::new(),
102 info: None,
103 encryption: None,
104 }
105 }
106
107 pub fn new_encrypted(
118 options: WriteOptions,
119 encryptor: Encryptor,
120 encrypt_dict: Dict,
121 ) -> Writer {
122 Writer {
123 options,
124 slots: Vec::new(),
125 info: None,
126 encryption: Some((encryptor, encrypt_dict)),
127 }
128 }
129
130 pub fn reserve(&mut self) -> ObjRef {
133 self.push(Slot::Reserved)
134 }
135
136 pub fn put(&mut self, obj: Object) -> ObjRef {
138 self.push(Slot::Filled(obj))
139 }
140
141 pub fn put_stream(&mut self, mut dict: Dict, data: Vec<u8>) -> ObjRef {
145 let data = compress_into(&mut dict, data, self.options.compress);
146 self.push(Slot::Filled(Object::Stream(Stream { dict, data })))
147 }
148
149 pub fn put_stream_raw(&mut self, dict: Dict, data: Vec<u8>) -> ObjRef {
153 self.push(Slot::Filled(Object::Stream(Stream { dict, data })))
154 }
155
156 pub fn fill(&mut self, r: ObjRef, obj: Object) -> Result<()> {
158 if r.gen != 0 {
159 return Err(Error::Other(format!(
160 "cannot fill {} {} R: this writer only issues generation 0",
161 r.num, r.gen
162 )));
163 }
164 if r.num == 0 || r.num as usize > self.slots.len() {
165 return Err(Error::Other(format!(
166 "cannot fill {} 0 R: this writer never allocated that object number",
167 r.num
168 )));
169 }
170 let slot = &mut self.slots[r.num as usize - 1];
171 if matches!(slot, Slot::Filled(_)) {
172 return Err(Error::AlreadyFilled(r));
173 }
174 *slot = Slot::Filled(obj);
175 Ok(())
176 }
177
178 fn push(&mut self, slot: Slot) -> ObjRef {
179 self.slots.push(slot);
180 ObjRef {
181 num: self.slots.len() as u32,
182 gen: 0,
183 }
184 }
185
186 pub fn set_info(&mut self, info: ObjRef) {
188 self.info = Some(info);
189 }
190
191 pub fn compress(&self) -> bool {
193 self.options.compress
194 }
195
196 pub fn finish(self, root: ObjRef) -> Result<Vec<u8>> {
202 block_on(self.finish_into_with(root, Vec::new()))
203 }
204
205 pub fn finish_into(self, root: ObjRef, out: impl Write) -> Result<()> {
210 block_on(self.finish_into_with(root, Immediate(out)))?;
211 Ok(())
212 }
213
214 pub async fn finish_into_with<S: AsyncByteSink>(self, root: ObjRef, sink: S) -> Result<S> {
221 let Writer {
222 options,
223 slots,
224 info,
225 mut encryption,
226 } = self;
227 let mut bodies = Vec::with_capacity(slots.len());
228 for (index, slot) in slots.into_iter().enumerate() {
229 match slot {
230 Slot::Reserved => {
231 return Err(Error::Unfilled(ObjRef {
232 num: index as u32 + 1,
233 gen: 0,
234 }))
235 }
236 Slot::Filled(obj) => bodies.push(obj),
237 }
238 }
239 let (encryptor, encrypt_dict) = split_encryption(&mut encryption);
240 let mut emit = Emit::new(sink);
241 match options.xref {
242 XrefStyle::Table => {
243 emit_table(
244 options,
245 &bodies,
246 root,
247 info,
248 encryptor,
249 encrypt_dict,
250 &mut emit,
251 )
252 .await?
253 }
254 XrefStyle::Stream => {
255 emit_stream(
256 options,
257 &bodies,
258 root,
259 info,
260 encryptor,
261 encrypt_dict,
262 &mut emit,
263 )
264 .await?
265 }
266 }
267 Ok(emit.sink)
268 }
269}
270
271fn split_encryption(
277 encryption: &mut Option<(Encryptor, Dict)>,
278) -> (Option<&mut Encryptor>, Option<&Dict>) {
279 match encryption {
280 Some((enc, dict)) => (Some(enc), Some(&*dict)),
281 None => (None, None),
282 }
283}
284
285struct Emit<S> {
289 sink: S,
290 count: usize,
291 hasher: Sha256,
292}
293
294impl<S: AsyncByteSink> Emit<S> {
295 fn new(sink: S) -> Emit<S> {
296 Emit {
297 sink,
298 count: 0,
299 hasher: Sha256::new(),
300 }
301 }
302
303 async fn write(&mut self, bytes: &[u8]) -> Result<()> {
304 self.hasher.update(bytes);
305 self.count += bytes.len();
306 self.sink.write_all(bytes).await
307 }
308
309 fn file_id(&self) -> Object {
312 let digest = self.hasher.clone().finalize();
313 let id = Object::String(digest[..16].to_vec());
314 Object::Array(vec![id.clone(), id])
315 }
316}
317
318const OBJSTM_CAPACITY: usize = 200;
320
321#[derive(Clone, Copy)]
324enum Row {
325 Top(u32),
326 Packed { container: u32, index: u16 },
327}
328
329async fn emit_table<S: AsyncByteSink>(
333 options: WriteOptions,
334 bodies: &[Object],
335 root: ObjRef,
336 info: Option<ObjRef>,
337 mut encryptor: Option<&mut Encryptor>,
338 encrypt_dict: Option<&Dict>,
339 emit: &mut Emit<S>,
340) -> Result<()> {
341 let mut head = Vec::new();
342 write_header(&mut head, options.version);
343 emit.write(&head).await?;
344 let mut offsets = Vec::with_capacity(bodies.len() + 1);
345 for (index, body) in bodies.iter().enumerate() {
346 offsets.push(emit.count);
347 let num = index as u32 + 1;
348 write_indirect_maybe_encrypted(emit, num, body, encryptor.as_deref_mut()).await?;
349 }
350 let mut encrypt_ref = None;
351 if let Some(dict) = encrypt_dict {
352 let num = bodies.len() as u32 + 1;
353 offsets.push(emit.count);
354 write_indirect(emit, num, &Object::Dict(dict.clone())).await?;
355 encrypt_ref = Some(ObjRef { num, gen: 0 });
356 }
357 let id = emit.file_id();
358 let xref_off = emit.count;
359 let mut section = format!("xref\n0 {}\n", offsets.len() + 1).into_bytes();
360 section.extend_from_slice(b"0000000000 65535 f\r\n");
361 let size = offsets.len() as i64 + 1;
362 for offset in offsets {
363 let offset = table_offset(offset)?;
364 section.extend_from_slice(format!("{offset:010} 00000 n\r\n").as_bytes());
365 }
366 section.extend_from_slice(b"trailer\n");
367 let mut trailer = trailer_dict(size, root, info, id);
368 if let Some(r) = encrypt_ref {
369 trailer.insert(literal("Encrypt"), Object::Ref(r));
370 }
371 serialize_dict(&trailer, &mut section)?;
372 section.extend_from_slice(format!("\nstartxref\n{xref_off}\n%%EOF").as_bytes());
373 emit.write(§ion).await
374}
375
376async fn emit_stream<S: AsyncByteSink>(
385 options: WriteOptions,
386 bodies: &[Object],
387 root: ObjRef,
388 info: Option<ObjRef>,
389 mut encryptor: Option<&mut Encryptor>,
390 encrypt_dict: Option<&Dict>,
391 emit: &mut Emit<S>,
392) -> Result<()> {
393 let mut head = Vec::new();
394 write_header(&mut head, options.version);
395 emit.write(&head).await?;
396 let user_count = bodies.len() as u32;
397
398 let packed: Vec<u32> = if options.object_streams {
399 bodies
400 .iter()
401 .enumerate()
402 .filter(|(_, body)| !matches!(body, Object::Stream(_)))
403 .map(|(index, _)| index as u32 + 1)
404 .collect()
405 } else {
406 Vec::new()
407 };
408 let chunks: Vec<&[u32]> = packed.chunks(OBJSTM_CAPACITY).collect();
409
410 let mut rows: Vec<Row> = vec![Row::Top(0); bodies.len()];
411 for (c, chunk) in chunks.iter().enumerate() {
412 for (index, num) in chunk.iter().enumerate() {
413 rows[*num as usize - 1] = Row::Packed {
414 container: user_count + c as u32 + 1,
415 index: index as u16,
416 };
417 }
418 }
419
420 for (index, body) in bodies.iter().enumerate() {
421 if matches!(rows[index], Row::Packed { .. }) {
422 continue;
423 }
424 rows[index] = Row::Top(field_offset(emit.count)?);
425 let num = index as u32 + 1;
426 write_indirect_maybe_encrypted(emit, num, body, encryptor.as_deref_mut()).await?;
427 }
428
429 let mut container_offsets = Vec::with_capacity(chunks.len());
430 for (c, chunk) in chunks.iter().enumerate() {
431 let pairs: Vec<(u32, &Object)> = chunk
432 .iter()
433 .map(|&num| (num, &bodies[num as usize - 1]))
434 .collect();
435 let container = build_objstm(&pairs, options.compress)?;
436 container_offsets.push(field_offset(emit.count)?);
437 let num = user_count + c as u32 + 1;
438 let mut container = Object::Stream(container);
439 if let Some(encryptor) = encryptor.as_deref_mut() {
440 encryptor.encrypt_object(&mut container, num, 0);
441 }
442 write_indirect(emit, num, &container).await?;
443 }
444
445 let mut next_num = user_count + chunks.len() as u32 + 1;
446 let mut encrypt_row = None;
447 if let Some(dict) = encrypt_dict {
448 let offset = field_offset(emit.count)?;
449 write_indirect(emit, next_num, &Object::Dict(dict.clone())).await?;
450 encrypt_row = Some((
451 ObjRef {
452 num: next_num,
453 gen: 0,
454 },
455 offset,
456 ));
457 next_num += 1;
458 }
459 let xref_num = next_num;
460 let id = emit.file_id();
461 let xref_off = field_offset(emit.count)?;
462
463 let mut data = Vec::with_capacity(7 * (xref_num as usize + 1));
464 push_row(&mut data, 0, 0, 65535);
465 for row in rows {
466 match row {
467 Row::Top(offset) => push_row(&mut data, 1, offset, 0),
468 Row::Packed { container, index } => push_row(&mut data, 2, container, index),
469 }
470 }
471 for offset in container_offsets {
472 push_row(&mut data, 1, offset, 0);
473 }
474 if let Some((_, offset)) = encrypt_row {
475 push_row(&mut data, 1, offset, 0);
476 }
477 push_row(&mut data, 1, xref_off, 0);
478
479 let mut dict = trailer_dict(xref_num as i64 + 1, root, info, id);
480 if let Some((r, _)) = encrypt_row {
481 dict.insert(literal("Encrypt"), Object::Ref(r));
482 }
483 dict.insert(literal("Type"), Object::Name(literal("XRef")));
484 dict.insert(
485 literal("W"),
486 Object::Array(vec![Object::Int(1), Object::Int(4), Object::Int(2)]),
487 );
488 let data = compress_into(&mut dict, data, options.compress);
489 write_indirect(emit, xref_num, &Object::Stream(Stream { dict, data })).await?;
490 emit.write(format!("startxref\n{xref_off}\n%%EOF").as_bytes())
491 .await
492}
493
494fn write_header(out: &mut Vec<u8>, version: (u8, u8)) {
496 out.extend_from_slice(format!("%PDF-{}.{}\n", version.0, version.1).as_bytes());
497 out.extend_from_slice(b"%\xE2\xE3\xCF\xD3\n");
498}
499
500async fn write_indirect<S: AsyncByteSink>(
506 emit: &mut Emit<S>,
507 num: u32,
508 obj: &Object,
509) -> Result<()> {
510 let mut lead = format!("{num} 0 obj\n").into_bytes();
511 match obj {
512 Object::Stream(s) => {
513 let mut dict = s.dict.clone();
514 dict.insert(literal("Length"), Object::Int(s.data.len() as i64));
515 serialize_dict(&dict, &mut lead)?;
516 lead.extend_from_slice(b"\nstream\n");
517 emit.write(&lead).await?;
518 emit.write(&s.data).await?;
519 emit.write(b"\nendstream\nendobj\n").await
520 }
521 direct => {
522 serialize_object(direct, &mut lead)?;
523 lead.extend_from_slice(b"\nendobj\n");
524 emit.write(&lead).await
525 }
526 }
527}
528
529async fn write_indirect_maybe_encrypted<S: AsyncByteSink>(
535 emit: &mut Emit<S>,
536 num: u32,
537 obj: &Object,
538 encryptor: Option<&mut Encryptor>,
539) -> Result<()> {
540 match encryptor {
541 Some(encryptor) => {
542 let mut obj = obj.clone();
543 encryptor.encrypt_object(&mut obj, num, 0);
544 write_indirect(emit, num, &obj).await
545 }
546 None => write_indirect(emit, num, obj).await,
547 }
548}
549
550fn build_objstm(pairs: &[(u32, &Object)], compress: bool) -> Result<Stream> {
554 let mut header = Vec::new();
555 let mut payload = Vec::new();
556 for (num, body) in pairs {
557 header.extend_from_slice(format!("{num} {} ", payload.len()).as_bytes());
558 serialize_object(body, &mut payload)?;
559 payload.push(b' ');
560 }
561 let mut dict = Dict::new();
562 dict.insert(literal("Type"), Object::Name(literal("ObjStm")));
563 dict.insert(literal("N"), Object::Int(pairs.len() as i64));
564 dict.insert(literal("First"), Object::Int(header.len() as i64));
565 header.extend_from_slice(&payload);
566 let data = compress_into(&mut dict, header, compress);
567 Ok(Stream { dict, data })
568}
569
570fn trailer_dict(size: i64, root: ObjRef, info: Option<ObjRef>, id: Object) -> Dict {
573 let mut trailer = Dict::new();
574 trailer.insert(literal("Size"), Object::Int(size));
575 trailer.insert(literal("Root"), Object::Ref(root));
576 if let Some(info) = info {
577 trailer.insert(literal("Info"), Object::Ref(info));
578 }
579 trailer.insert(literal("ID"), id);
580 trailer
581}
582
583fn compress_into(dict: &mut Dict, data: Vec<u8>, compress: bool) -> Vec<u8> {
587 if !compress || dict.get("Filter").is_some() {
588 return data;
589 }
590 dict.insert(literal("Filter"), Object::Name(literal("FlateDecode")));
591 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
592 encoder
593 .write_all(&data)
594 .expect("writing into a Vec cannot fail");
595 encoder
596 .finish()
597 .expect("finishing an in-memory zlib stream cannot fail")
598}
599
600fn push_row(rows: &mut Vec<u8>, kind: u8, second: u32, third: u16) {
602 rows.push(kind);
603 rows.extend_from_slice(&second.to_be_bytes());
604 rows.extend_from_slice(&third.to_be_bytes());
605}
606
607fn field_offset(position: usize) -> Result<u32> {
609 u32::try_from(position)
610 .map_err(|_| Error::Other("file offset exceeds the 4-byte xref field".to_string()))
611}
612
613fn table_offset(position: usize) -> Result<usize> {
617 if position as u64 <= 9_999_999_999 {
618 return Ok(position);
619 }
620 Err(Error::Other(
621 "file offset exceeds the 10-digit xref table field".to_string(),
622 ))
623}
624
625fn literal(text: &str) -> Name {
627 Name(text.to_string())
628}
629
630#[cfg(test)]
631mod tests {
632 use pdfboss_core::xref::load_xref;
633 use pdfboss_core::{Dict, Document, Name, ObjRef, Object, Stream};
634
635 use super::*;
636 use crate::error::Error;
637
638 const CONTENT: &[u8] = b"BT /F1 12 Tf 72 720 Td (Hello, writer) Tj ET";
639
640 fn name(text: &str) -> Name {
641 Name(text.to_string())
642 }
643
644 fn table_options() -> WriteOptions {
645 WriteOptions {
646 xref: XrefStyle::Table,
647 compress: false,
648 object_streams: false,
649 version: (1, 7),
650 }
651 }
652
653 fn stream_options() -> WriteOptions {
654 WriteOptions {
655 xref: XrefStyle::Stream,
656 compress: false,
657 object_streams: false,
658 version: (1, 5),
659 }
660 }
661
662 fn objstm_options() -> WriteOptions {
663 WriteOptions {
664 xref: XrefStyle::Stream,
665 compress: true,
666 object_streams: true,
667 version: (1, 7),
668 }
669 }
670
671 struct Refs {
672 content: ObjRef,
673 pages: ObjRef,
674 page: ObjRef,
675 root: ObjRef,
676 }
677
678 fn page_dict(pages: ObjRef, content: ObjRef) -> Dict {
679 let mut page = Dict::new();
680 page.insert(name("Type"), Object::Name(name("Page")));
681 page.insert(name("Parent"), Object::Ref(pages));
682 page.insert(
683 name("MediaBox"),
684 Object::Array(vec![
685 Object::Int(0),
686 Object::Int(0),
687 Object::Int(612),
688 Object::Int(792),
689 ]),
690 );
691 page.insert(name("Contents"), Object::Ref(content));
692 page
693 }
694
695 fn build(
698 options: WriteOptions,
699 content_dict: Dict,
700 content_data: Vec<u8>,
701 raw: bool,
702 ) -> (Writer, Refs) {
703 let mut w = Writer::new(options);
704 let content = if raw {
705 w.put_stream_raw(content_dict, content_data)
706 } else {
707 w.put_stream(content_dict, content_data)
708 };
709 let pages = w.reserve();
710 let page = w.put(Object::Dict(page_dict(pages, content)));
711 let mut tree = Dict::new();
712 tree.insert(name("Type"), Object::Name(name("Pages")));
713 tree.insert(name("Kids"), Object::Array(vec![Object::Ref(page)]));
714 tree.insert(name("Count"), Object::Int(1));
715 w.fill(pages, Object::Dict(tree))
716 .expect("pages slot is fillable");
717 let mut catalog = Dict::new();
718 catalog.insert(name("Type"), Object::Name(name("Catalog")));
719 catalog.insert(name("Pages"), Object::Ref(pages));
720 let root = w.put(Object::Dict(catalog));
721 (
722 w,
723 Refs {
724 content,
725 pages,
726 page,
727 root,
728 },
729 )
730 }
731
732 fn skeleton(
734 options: WriteOptions,
735 content_dict: Dict,
736 content_data: Vec<u8>,
737 raw: bool,
738 ) -> (Vec<u8>, Refs) {
739 let (w, refs) = build(options, content_dict, content_data, raw);
740 let bytes = w.finish(refs.root).expect("minimal document finishes");
741 (bytes, refs)
742 }
743
744 fn minimal_pdf(options: WriteOptions) -> (Vec<u8>, Refs) {
745 skeleton(options, Dict::new(), CONTENT.to_vec(), false)
746 }
747
748 fn assert_minimal_loads(bytes: &[u8], refs: &Refs) -> Document {
749 let doc = Document::load(bytes.to_vec()).expect("document loads");
750 assert_eq!(doc.page_count(), 1);
751 let page = doc.page(0).expect("page 0 exists");
752 assert_eq!(page.object_ref(), Some(refs.page));
753 assert_eq!(page.dict(), &page_dict(refs.pages, refs.content));
754 assert_eq!(page.content(&doc).expect("content decodes"), CONTENT);
755 doc
756 }
757
758 fn count_occurrences(haystack: &[u8], needle: &[u8]) -> usize {
759 haystack
760 .windows(needle.len())
761 .filter(|window| *window == needle)
762 .count()
763 }
764
765 #[test]
766 fn table_mode_minimal_document_loads() {
767 let (bytes, refs) = minimal_pdf(table_options());
768 assert!(bytes.starts_with(b"%PDF-1.7\n%\xE2\xE3\xCF\xD3\n"));
769 assert!(bytes.ends_with(b"%%EOF"));
770 assert_eq!(count_occurrences(&bytes, b"xref\n0 5\n"), 1);
771 assert_eq!(count_occurrences(&bytes, b"0000000000 65535 f\r\n"), 1);
772 assert_eq!(count_occurrences(&bytes, b"trailer\n"), 1);
773 assert_minimal_loads(&bytes, &refs);
774 }
775
776 #[test]
777 fn table_mode_ignores_object_streams_option() {
778 let options = WriteOptions {
779 object_streams: true,
780 ..table_options()
781 };
782 let (bytes, refs) = minimal_pdf(options);
783 assert_eq!(count_occurrences(&bytes, b"/ObjStm"), 0);
784 assert_minimal_loads(&bytes, &refs);
785 }
786
787 #[test]
788 fn stream_mode_minimal_document_loads() {
789 let (bytes, refs) = minimal_pdf(stream_options());
790 assert!(bytes.starts_with(b"%PDF-1.5\n%\xE2\xE3\xCF\xD3\n"));
791 assert!(bytes.ends_with(b"%%EOF"));
792 assert_eq!(count_occurrences(&bytes, b"/ObjStm"), 0);
793 assert_eq!(count_occurrences(&bytes, b"/XRef"), 1);
794 assert_minimal_loads(&bytes, &refs);
795 }
796
797 #[test]
798 fn object_streams_pack_and_resolve() {
799 let (bytes, refs) = minimal_pdf(objstm_options());
800 assert_eq!(count_occurrences(&bytes, b"/ObjStm"), 1);
801 let doc = assert_minimal_loads(&bytes, &refs);
802 let root = doc
803 .resolve(&Object::Ref(refs.root))
804 .expect("catalog resolves");
805 let catalog = root.as_dict().expect("catalog is a dictionary");
806 assert_eq!(catalog.get_name("Type"), Some(&name("Catalog")));
807 assert_eq!(catalog.get_ref("Pages"), Some(refs.pages));
808 }
809
810 #[test]
811 fn object_streams_chunk_at_two_hundred() {
812 let options = WriteOptions {
813 compress: false,
814 ..objstm_options()
815 };
816 let mut w = Writer::new(options);
817 let content = w.put_stream(Dict::new(), CONTENT.to_vec());
818 let int_refs: Vec<ObjRef> = (0..205).map(|i| w.put(Object::Int(i))).collect();
819 let pages = w.reserve();
820 let page = w.put(Object::Dict(page_dict(pages, content)));
821 let mut tree = Dict::new();
822 tree.insert(name("Type"), Object::Name(name("Pages")));
823 tree.insert(name("Kids"), Object::Array(vec![Object::Ref(page)]));
824 tree.insert(name("Count"), Object::Int(1));
825 w.fill(pages, Object::Dict(tree))
826 .expect("pages slot is fillable");
827 let mut catalog = Dict::new();
828 catalog.insert(name("Type"), Object::Name(name("Catalog")));
829 catalog.insert(name("Pages"), Object::Ref(pages));
830 let root = w.put(Object::Dict(catalog));
831 let bytes = w.finish(root).expect("document finishes");
832 assert_eq!(count_occurrences(&bytes, b"/ObjStm"), 2);
833 let doc = Document::load(bytes).expect("document loads");
834 assert_eq!(
835 doc.resolve(&Object::Ref(int_refs[0])).expect("resolves"),
836 Object::Int(0)
837 );
838 assert_eq!(
839 doc.resolve(&Object::Ref(int_refs[204])).expect("resolves"),
840 Object::Int(204)
841 );
842 assert_eq!(doc.page_count(), 1);
843 }
844
845 #[test]
846 fn refs_ascend_from_one_in_call_order() {
847 let mut w = Writer::new(table_options());
848 assert_eq!(w.put(Object::Null), ObjRef { num: 1, gen: 0 });
849 assert_eq!(w.reserve(), ObjRef { num: 2, gen: 0 });
850 assert_eq!(
851 w.put_stream(Dict::new(), Vec::new()),
852 ObjRef { num: 3, gen: 0 }
853 );
854 assert_eq!(
855 w.put_stream_raw(Dict::new(), Vec::new()),
856 ObjRef { num: 4, gen: 0 }
857 );
858 }
859
860 #[test]
861 fn fill_twice_reports_already_filled() {
862 let mut w = Writer::new(table_options());
863 let r = w.reserve();
864 w.fill(r, Object::Int(1)).expect("first fill lands");
865 match w.fill(r, Object::Int(2)) {
866 Err(Error::AlreadyFilled(seen)) => assert_eq!(seen, r),
867 other => panic!("expected AlreadyFilled, got {other:?}"),
868 }
869 }
870
871 #[test]
872 fn fill_rejects_foreign_and_wrong_generation_refs() {
873 let mut w = Writer::new(table_options());
874 let r = w.reserve();
875 let unallocated = w.fill(ObjRef { num: 99, gen: 0 }, Object::Null);
876 assert!(matches!(unallocated, Err(Error::Other(msg)) if msg.contains("99")));
877 let zero = w.fill(ObjRef { num: 0, gen: 0 }, Object::Null);
878 assert!(matches!(zero, Err(Error::Other(msg)) if !msg.is_empty()));
879 let wrong_gen = w.fill(ObjRef { num: r.num, gen: 1 }, Object::Null);
880 assert!(matches!(wrong_gen, Err(Error::Other(msg)) if msg.contains("generation")));
881 }
882
883 #[test]
884 fn finish_with_unfilled_reserve_reports_the_ref() {
885 let mut w = Writer::new(table_options());
886 let root = w.put(Object::Dict(Dict::new()));
887 let reserved = w.reserve();
888 match w.finish(root) {
889 Err(Error::Unfilled(seen)) => assert_eq!(seen, reserved),
890 other => panic!("expected Unfilled, got {other:?}"),
891 }
892 }
893
894 #[test]
895 fn nested_stream_surfaces_from_finish() {
896 for options in [table_options(), objstm_options()] {
897 let mut w = Writer::new(options);
898 let root = w.put(Object::Array(vec![Object::Stream(Stream {
899 dict: Dict::new(),
900 data: b"x".to_vec(),
901 })]));
902 assert!(matches!(w.finish(root), Err(Error::NestedStream)));
903 }
904 }
905
906 #[test]
907 fn compressed_stream_round_trips() {
908 let options = WriteOptions {
909 compress: true,
910 ..table_options()
911 };
912 let data: Vec<u8> = b"q 0.5 0 0 0.5 36 36 cm Q\n".repeat(40);
913 let (bytes, refs) = skeleton(options, Dict::new(), data.clone(), false);
914 let doc = Document::load(bytes).expect("document loads");
915 let resolved = doc
916 .resolve(&Object::Ref(refs.content))
917 .expect("content stream resolves");
918 let stream = resolved.as_stream().expect("content is a stream");
919 assert_eq!(stream.dict.get_name("Filter"), Some(&name("FlateDecode")));
920 assert_eq!(
921 stream.dict.get_int("Length"),
922 Some(stream.data.len() as i64)
923 );
924 assert!(stream.data.len() < data.len());
925 assert_eq!(doc.stream_data(stream).expect("stream decodes"), data);
926 }
927
928 #[test]
929 fn preset_filter_is_not_recompressed() {
930 let options = WriteOptions {
931 compress: true,
932 ..table_options()
933 };
934 let payload = b"Hello writer";
935 let encoded: Vec<u8> = payload
936 .iter()
937 .flat_map(|b| format!("{b:02X}").into_bytes())
938 .chain(*b">")
939 .collect();
940 let mut dict = Dict::new();
941 dict.insert(name("Filter"), Object::Name(name("ASCIIHexDecode")));
942 let (bytes, refs) = skeleton(options, dict, encoded.clone(), false);
943 let doc = Document::load(bytes).expect("document loads");
944 let resolved = doc
945 .resolve(&Object::Ref(refs.content))
946 .expect("content stream resolves");
947 let stream = resolved.as_stream().expect("content is a stream");
948 assert_eq!(stream.data, encoded, "pre-filtered data stays untouched");
949 assert_eq!(
950 stream.dict.get_name("Filter"),
951 Some(&name("ASCIIHexDecode"))
952 );
953 assert_eq!(doc.stream_data(stream).expect("stream decodes"), payload);
954 }
955
956 #[test]
957 fn put_stream_raw_never_compresses() {
958 let options = WriteOptions {
959 compress: true,
960 ..table_options()
961 };
962 let (bytes, refs) = skeleton(options, Dict::new(), CONTENT.to_vec(), true);
963 let doc = Document::load(bytes).expect("document loads");
964 let resolved = doc
965 .resolve(&Object::Ref(refs.content))
966 .expect("content stream resolves");
967 let stream = resolved.as_stream().expect("content is a stream");
968 assert_eq!(stream.data, CONTENT);
969 assert!(stream.dict.get("Filter").is_none());
970 assert_eq!(stream.dict.get_int("Length"), Some(CONTENT.len() as i64));
971 }
972
973 #[test]
974 fn table_offsets_past_ten_digits_are_rejected() {
975 assert_eq!(table_offset(9_999_999_999).ok(), Some(9_999_999_999));
976 assert!(table_offset(10_000_000_000).is_err());
977 }
978
979 #[test]
980 fn id_derives_from_the_emitted_content() {
981 let (a, refs) = minimal_pdf(table_options());
982 assert_eq!(refs.root.gen, 0);
983 let (b, other_refs) = skeleton(
984 table_options(),
985 Dict::new(),
986 b"BT /F1 12 Tf 72 720 Td (Hello, other) Tj ET".to_vec(),
987 false,
988 );
989 assert_eq!(other_refs.root.gen, 0);
990 let id_of = |bytes: &[u8]| {
991 let xref = load_xref(bytes).expect("xref loads");
992 let id = xref.trailer.get_array("ID").expect("/ID array present");
993 id[0]
994 .as_str_bytes()
995 .expect("/ID entry is a string")
996 .to_vec()
997 };
998 assert_ne!(
999 id_of(&a),
1000 id_of(&b),
1001 "/ID must depend on the emitted content"
1002 );
1003 }
1004
1005 #[test]
1006 fn id_pair_is_present_and_identical() {
1007 for options in [table_options(), stream_options(), objstm_options()] {
1008 let (bytes, refs) = minimal_pdf(options);
1009 assert_eq!(refs.root.gen, 0);
1010 let xref = load_xref(&bytes).expect("xref loads");
1011 let id = xref.trailer.get_array("ID").expect("/ID array present");
1012 assert_eq!(id.len(), 2);
1013 let first = id[0].as_str_bytes().expect("/ID entry is a string");
1014 let second = id[1].as_str_bytes().expect("/ID entry is a string");
1015 assert_eq!(first.len(), 16);
1016 assert_eq!(first, second);
1017 }
1018 }
1019
1020 #[test]
1021 fn output_is_deterministic() {
1022 for options in [table_options(), stream_options(), objstm_options()] {
1023 let (first, refs) = minimal_pdf(options);
1024 let (second, again) = minimal_pdf(options);
1025 assert_eq!(refs.content, again.content);
1026 assert_eq!(first, second, "options {options:?} must be deterministic");
1027 }
1028 }
1029
1030 #[test]
1031 fn finish_into_matches_finish() {
1032 for options in [table_options(), stream_options(), objstm_options()] {
1033 let (bytes, _) = minimal_pdf(options);
1034 let (w, refs) = build(options, Dict::new(), CONTENT.to_vec(), false);
1035 let mut out = Vec::new();
1036 w.finish_into(refs.root, &mut out)
1037 .expect("finish_into succeeds");
1038 assert_eq!(out, bytes, "options {options:?}");
1039 }
1040 }
1041
1042 #[test]
1043 fn finish_into_with_matches_finish() {
1044 for options in [table_options(), stream_options(), objstm_options()] {
1045 let (bytes, _) = minimal_pdf(options);
1046 let (w, refs) = build(options, Dict::new(), CONTENT.to_vec(), false);
1047 let sink = pdfboss_core::block_on(w.finish_into_with(refs.root, Vec::new()))
1048 .expect("finish_into_with succeeds");
1049 assert_eq!(sink, bytes, "options {options:?}");
1050 }
1051 }
1052
1053 struct Recording {
1056 chunks: Vec<Vec<u8>>,
1057 }
1058
1059 impl crate::sink::AsyncByteSink for Recording {
1060 fn write_all<'a>(
1061 &'a mut self,
1062 buf: &'a [u8],
1063 ) -> pdfboss_core::source::BoxFuture<'a, Result<()>> {
1064 self.chunks.push(buf.to_vec());
1065 Box::pin(std::future::ready(Ok(())))
1066 }
1067 }
1068
1069 #[test]
1073 fn emission_arrives_in_bounded_chunks() {
1074 for options in [table_options(), stream_options(), objstm_options()] {
1075 let (bytes, _) = minimal_pdf(options);
1076 let (w, refs) = build(options, Dict::new(), CONTENT.to_vec(), false);
1077 let sink = pdfboss_core::block_on(
1078 w.finish_into_with(refs.root, Recording { chunks: Vec::new() }),
1079 )
1080 .expect("finish_into_with succeeds");
1081 assert_eq!(sink.chunks.concat(), bytes, "options {options:?}");
1082 assert!(
1083 sink.chunks.len() > 3,
1084 "options {options:?}: emission must arrive in many chunks, got {}",
1085 sink.chunks.len()
1086 );
1087 assert!(
1088 sink.chunks.iter().all(|chunk| chunk.len() < bytes.len()),
1089 "options {options:?}: no chunk may be the whole file"
1090 );
1091 }
1092 }
1093
1094 #[test]
1098 fn finish_into_with_over_an_owned_sink_is_spawnable() {
1099 fn assert_send_static<F: std::future::Future + Send + 'static>(_: &F) {}
1100
1101 let (w, refs) = build(stream_options(), Dict::new(), CONTENT.to_vec(), false);
1102 let future = w.finish_into_with(refs.root, Vec::new());
1103 assert_send_static(&future);
1104 let bytes = pdfboss_core::block_on(future).expect("emission succeeds");
1105 assert!(bytes.ends_with(b"%%EOF"));
1106 }
1107
1108 #[test]
1111 fn finish_into_with_reports_unfilled_reserves() {
1112 let mut w = Writer::new(table_options());
1113 let root = w.put(Object::Dict(Dict::new()));
1114 let reserved = w.reserve();
1115 let sink = Recording { chunks: Vec::new() };
1116 match pdfboss_core::block_on(w.finish_into_with(root, sink)) {
1117 Err(Error::Unfilled(seen)) => assert_eq!(seen, reserved),
1118 other => panic!("expected Unfilled, got {:?}", other.map(|s| s.chunks.len())),
1119 }
1120 }
1121}