1use std::io::Write;
11
12use flate2::write::ZlibEncoder;
13use flate2::Compression;
14use pdfboss_core::crypt::Sha256;
15use pdfboss_core::{block_on, Dict, Name, ObjRef, Object, Stream};
16
17use crate::error::{Error, Result};
18use crate::ser::{serialize_dict, serialize_object};
19use crate::sink::{AsyncByteSink, Immediate};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum XrefStyle {
24 Table,
27 #[default]
30 Stream,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq)]
35pub struct WriteOptions {
36 pub xref: XrefStyle,
38 pub compress: bool,
40 pub object_streams: bool,
43 pub version: (u8, u8),
45}
46
47impl Default for WriteOptions {
48 fn default() -> WriteOptions {
49 WriteOptions {
50 xref: XrefStyle::Stream,
51 compress: true,
52 object_streams: true,
53 version: (1, 7),
54 }
55 }
56}
57
58#[derive(Debug)]
62pub struct Writer {
63 options: WriteOptions,
64 slots: Vec<Slot>,
65 info: Option<ObjRef>,
66}
67
68#[derive(Debug)]
70enum Slot {
71 Reserved,
72 Filled(Object),
73}
74
75impl Writer {
76 pub fn new(options: WriteOptions) -> Writer {
78 Writer {
79 options,
80 slots: Vec::new(),
81 info: None,
82 }
83 }
84
85 pub fn reserve(&mut self) -> ObjRef {
88 self.push(Slot::Reserved)
89 }
90
91 pub fn put(&mut self, obj: Object) -> ObjRef {
93 self.push(Slot::Filled(obj))
94 }
95
96 pub fn put_stream(&mut self, mut dict: Dict, data: Vec<u8>) -> ObjRef {
100 let data = compress_into(&mut dict, data, self.options.compress);
101 self.push(Slot::Filled(Object::Stream(Stream { dict, data })))
102 }
103
104 pub fn put_stream_raw(&mut self, dict: Dict, data: Vec<u8>) -> ObjRef {
108 self.push(Slot::Filled(Object::Stream(Stream { dict, data })))
109 }
110
111 pub fn fill(&mut self, r: ObjRef, obj: Object) -> Result<()> {
113 if r.gen != 0 {
114 return Err(Error::Other(format!(
115 "cannot fill {} {} R: this writer only issues generation 0",
116 r.num, r.gen
117 )));
118 }
119 if r.num == 0 || r.num as usize > self.slots.len() {
120 return Err(Error::Other(format!(
121 "cannot fill {} 0 R: this writer never allocated that object number",
122 r.num
123 )));
124 }
125 let slot = &mut self.slots[r.num as usize - 1];
126 if matches!(slot, Slot::Filled(_)) {
127 return Err(Error::AlreadyFilled(r));
128 }
129 *slot = Slot::Filled(obj);
130 Ok(())
131 }
132
133 fn push(&mut self, slot: Slot) -> ObjRef {
134 self.slots.push(slot);
135 ObjRef {
136 num: self.slots.len() as u32,
137 gen: 0,
138 }
139 }
140
141 pub fn set_info(&mut self, info: ObjRef) {
143 self.info = Some(info);
144 }
145
146 pub fn finish(self, root: ObjRef) -> Result<Vec<u8>> {
152 block_on(self.finish_into_with(root, Vec::new()))
153 }
154
155 pub fn finish_into(self, root: ObjRef, out: impl Write) -> Result<()> {
160 block_on(self.finish_into_with(root, Immediate(out)))?;
161 Ok(())
162 }
163
164 pub async fn finish_into_with<S: AsyncByteSink>(self, root: ObjRef, sink: S) -> Result<S> {
171 let Writer {
172 options,
173 slots,
174 info,
175 } = self;
176 let mut bodies = Vec::with_capacity(slots.len());
177 for (index, slot) in slots.into_iter().enumerate() {
178 match slot {
179 Slot::Reserved => {
180 return Err(Error::Unfilled(ObjRef {
181 num: index as u32 + 1,
182 gen: 0,
183 }))
184 }
185 Slot::Filled(obj) => bodies.push(obj),
186 }
187 }
188 let mut emit = Emit::new(sink);
189 match options.xref {
190 XrefStyle::Table => emit_table(options, &bodies, root, info, &mut emit).await?,
191 XrefStyle::Stream => emit_stream(options, &bodies, root, info, &mut emit).await?,
192 }
193 Ok(emit.sink)
194 }
195}
196
197struct Emit<S> {
201 sink: S,
202 count: usize,
203 hasher: Sha256,
204}
205
206impl<S: AsyncByteSink> Emit<S> {
207 fn new(sink: S) -> Emit<S> {
208 Emit {
209 sink,
210 count: 0,
211 hasher: Sha256::new(),
212 }
213 }
214
215 async fn write(&mut self, bytes: &[u8]) -> Result<()> {
216 self.hasher.update(bytes);
217 self.count += bytes.len();
218 self.sink.write_all(bytes).await
219 }
220
221 fn file_id(&self) -> Object {
224 let digest = self.hasher.clone().finalize();
225 let id = Object::String(digest[..16].to_vec());
226 Object::Array(vec![id.clone(), id])
227 }
228}
229
230const OBJSTM_CAPACITY: usize = 200;
232
233#[derive(Clone, Copy)]
236enum Row {
237 Top(u32),
238 Packed { container: u32, index: u16 },
239}
240
241async fn emit_table<S: AsyncByteSink>(
244 options: WriteOptions,
245 bodies: &[Object],
246 root: ObjRef,
247 info: Option<ObjRef>,
248 emit: &mut Emit<S>,
249) -> Result<()> {
250 let mut head = Vec::new();
251 write_header(&mut head, options.version);
252 emit.write(&head).await?;
253 let mut offsets = Vec::with_capacity(bodies.len());
254 for (index, body) in bodies.iter().enumerate() {
255 offsets.push(emit.count);
256 write_indirect(emit, index as u32 + 1, body).await?;
257 }
258 let id = emit.file_id();
259 let xref_off = emit.count;
260 let mut section = format!("xref\n0 {}\n", bodies.len() + 1).into_bytes();
261 section.extend_from_slice(b"0000000000 65535 f\r\n");
262 for offset in offsets {
263 let offset = table_offset(offset)?;
264 section.extend_from_slice(format!("{offset:010} 00000 n\r\n").as_bytes());
265 }
266 section.extend_from_slice(b"trailer\n");
267 let trailer = trailer_dict(bodies.len() as i64 + 1, root, info, id);
268 serialize_dict(&trailer, &mut section)?;
269 section.extend_from_slice(format!("\nstartxref\n{xref_off}\n%%EOF").as_bytes());
270 emit.write(§ion).await
271}
272
273async fn emit_stream<S: AsyncByteSink>(
279 options: WriteOptions,
280 bodies: &[Object],
281 root: ObjRef,
282 info: Option<ObjRef>,
283 emit: &mut Emit<S>,
284) -> Result<()> {
285 let mut head = Vec::new();
286 write_header(&mut head, options.version);
287 emit.write(&head).await?;
288 let user_count = bodies.len() as u32;
289
290 let packed: Vec<u32> = if options.object_streams {
291 bodies
292 .iter()
293 .enumerate()
294 .filter(|(_, body)| !matches!(body, Object::Stream(_)))
295 .map(|(index, _)| index as u32 + 1)
296 .collect()
297 } else {
298 Vec::new()
299 };
300 let chunks: Vec<&[u32]> = packed.chunks(OBJSTM_CAPACITY).collect();
301
302 let mut rows: Vec<Row> = vec![Row::Top(0); bodies.len()];
303 for (c, chunk) in chunks.iter().enumerate() {
304 for (index, num) in chunk.iter().enumerate() {
305 rows[*num as usize - 1] = Row::Packed {
306 container: user_count + c as u32 + 1,
307 index: index as u16,
308 };
309 }
310 }
311
312 for (index, body) in bodies.iter().enumerate() {
313 if matches!(rows[index], Row::Packed { .. }) {
314 continue;
315 }
316 rows[index] = Row::Top(field_offset(emit.count)?);
317 write_indirect(emit, index as u32 + 1, body).await?;
318 }
319
320 let mut container_offsets = Vec::with_capacity(chunks.len());
321 for (c, chunk) in chunks.iter().enumerate() {
322 let pairs: Vec<(u32, &Object)> = chunk
323 .iter()
324 .map(|&num| (num, &bodies[num as usize - 1]))
325 .collect();
326 let container = build_objstm(&pairs, options.compress)?;
327 container_offsets.push(field_offset(emit.count)?);
328 write_indirect(emit, user_count + c as u32 + 1, &Object::Stream(container)).await?;
329 }
330
331 let xref_num = user_count + chunks.len() as u32 + 1;
332 let id = emit.file_id();
333 let xref_off = field_offset(emit.count)?;
334
335 let mut data = Vec::with_capacity(7 * (xref_num as usize + 1));
336 push_row(&mut data, 0, 0, 65535);
337 for row in rows {
338 match row {
339 Row::Top(offset) => push_row(&mut data, 1, offset, 0),
340 Row::Packed { container, index } => push_row(&mut data, 2, container, index),
341 }
342 }
343 for offset in container_offsets {
344 push_row(&mut data, 1, offset, 0);
345 }
346 push_row(&mut data, 1, xref_off, 0);
347
348 let mut dict = trailer_dict(xref_num as i64 + 1, root, info, id);
349 dict.insert(literal("Type"), Object::Name(literal("XRef")));
350 dict.insert(
351 literal("W"),
352 Object::Array(vec![Object::Int(1), Object::Int(4), Object::Int(2)]),
353 );
354 let data = compress_into(&mut dict, data, options.compress);
355 write_indirect(emit, xref_num, &Object::Stream(Stream { dict, data })).await?;
356 emit.write(format!("startxref\n{xref_off}\n%%EOF").as_bytes())
357 .await
358}
359
360fn write_header(out: &mut Vec<u8>, version: (u8, u8)) {
362 out.extend_from_slice(format!("%PDF-{}.{}\n", version.0, version.1).as_bytes());
363 out.extend_from_slice(b"%\xE2\xE3\xCF\xD3\n");
364}
365
366async fn write_indirect<S: AsyncByteSink>(
372 emit: &mut Emit<S>,
373 num: u32,
374 obj: &Object,
375) -> Result<()> {
376 let mut lead = format!("{num} 0 obj\n").into_bytes();
377 match obj {
378 Object::Stream(s) => {
379 let mut dict = s.dict.clone();
380 dict.insert(literal("Length"), Object::Int(s.data.len() as i64));
381 serialize_dict(&dict, &mut lead)?;
382 lead.extend_from_slice(b"\nstream\n");
383 emit.write(&lead).await?;
384 emit.write(&s.data).await?;
385 emit.write(b"\nendstream\nendobj\n").await
386 }
387 direct => {
388 serialize_object(direct, &mut lead)?;
389 lead.extend_from_slice(b"\nendobj\n");
390 emit.write(&lead).await
391 }
392 }
393}
394
395fn build_objstm(pairs: &[(u32, &Object)], compress: bool) -> Result<Stream> {
399 let mut header = Vec::new();
400 let mut payload = Vec::new();
401 for (num, body) in pairs {
402 header.extend_from_slice(format!("{num} {} ", payload.len()).as_bytes());
403 serialize_object(body, &mut payload)?;
404 payload.push(b' ');
405 }
406 let mut dict = Dict::new();
407 dict.insert(literal("Type"), Object::Name(literal("ObjStm")));
408 dict.insert(literal("N"), Object::Int(pairs.len() as i64));
409 dict.insert(literal("First"), Object::Int(header.len() as i64));
410 header.extend_from_slice(&payload);
411 let data = compress_into(&mut dict, header, compress);
412 Ok(Stream { dict, data })
413}
414
415fn trailer_dict(size: i64, root: ObjRef, info: Option<ObjRef>, id: Object) -> Dict {
418 let mut trailer = Dict::new();
419 trailer.insert(literal("Size"), Object::Int(size));
420 trailer.insert(literal("Root"), Object::Ref(root));
421 if let Some(info) = info {
422 trailer.insert(literal("Info"), Object::Ref(info));
423 }
424 trailer.insert(literal("ID"), id);
425 trailer
426}
427
428fn compress_into(dict: &mut Dict, data: Vec<u8>, compress: bool) -> Vec<u8> {
432 if !compress || dict.get("Filter").is_some() {
433 return data;
434 }
435 dict.insert(literal("Filter"), Object::Name(literal("FlateDecode")));
436 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
437 encoder
438 .write_all(&data)
439 .expect("writing into a Vec cannot fail");
440 encoder
441 .finish()
442 .expect("finishing an in-memory zlib stream cannot fail")
443}
444
445fn push_row(rows: &mut Vec<u8>, kind: u8, second: u32, third: u16) {
447 rows.push(kind);
448 rows.extend_from_slice(&second.to_be_bytes());
449 rows.extend_from_slice(&third.to_be_bytes());
450}
451
452fn field_offset(position: usize) -> Result<u32> {
454 u32::try_from(position)
455 .map_err(|_| Error::Other("file offset exceeds the 4-byte xref field".to_string()))
456}
457
458fn table_offset(position: usize) -> Result<usize> {
462 if position as u64 <= 9_999_999_999 {
463 return Ok(position);
464 }
465 Err(Error::Other(
466 "file offset exceeds the 10-digit xref table field".to_string(),
467 ))
468}
469
470fn literal(text: &str) -> Name {
472 Name(text.to_string())
473}
474
475#[cfg(test)]
476mod tests {
477 use pdfboss_core::xref::load_xref;
478 use pdfboss_core::{Dict, Document, Name, ObjRef, Object, Stream};
479
480 use super::*;
481 use crate::error::Error;
482
483 const CONTENT: &[u8] = b"BT /F1 12 Tf 72 720 Td (Hello, writer) Tj ET";
484
485 fn name(text: &str) -> Name {
486 Name(text.to_string())
487 }
488
489 fn table_options() -> WriteOptions {
490 WriteOptions {
491 xref: XrefStyle::Table,
492 compress: false,
493 object_streams: false,
494 version: (1, 7),
495 }
496 }
497
498 fn stream_options() -> WriteOptions {
499 WriteOptions {
500 xref: XrefStyle::Stream,
501 compress: false,
502 object_streams: false,
503 version: (1, 5),
504 }
505 }
506
507 fn objstm_options() -> WriteOptions {
508 WriteOptions {
509 xref: XrefStyle::Stream,
510 compress: true,
511 object_streams: true,
512 version: (1, 7),
513 }
514 }
515
516 struct Refs {
517 content: ObjRef,
518 pages: ObjRef,
519 page: ObjRef,
520 root: ObjRef,
521 }
522
523 fn page_dict(pages: ObjRef, content: ObjRef) -> Dict {
524 let mut page = Dict::new();
525 page.insert(name("Type"), Object::Name(name("Page")));
526 page.insert(name("Parent"), Object::Ref(pages));
527 page.insert(
528 name("MediaBox"),
529 Object::Array(vec![
530 Object::Int(0),
531 Object::Int(0),
532 Object::Int(612),
533 Object::Int(792),
534 ]),
535 );
536 page.insert(name("Contents"), Object::Ref(content));
537 page
538 }
539
540 fn build(
543 options: WriteOptions,
544 content_dict: Dict,
545 content_data: Vec<u8>,
546 raw: bool,
547 ) -> (Writer, Refs) {
548 let mut w = Writer::new(options);
549 let content = if raw {
550 w.put_stream_raw(content_dict, content_data)
551 } else {
552 w.put_stream(content_dict, content_data)
553 };
554 let pages = w.reserve();
555 let page = w.put(Object::Dict(page_dict(pages, content)));
556 let mut tree = Dict::new();
557 tree.insert(name("Type"), Object::Name(name("Pages")));
558 tree.insert(name("Kids"), Object::Array(vec![Object::Ref(page)]));
559 tree.insert(name("Count"), Object::Int(1));
560 w.fill(pages, Object::Dict(tree))
561 .expect("pages slot is fillable");
562 let mut catalog = Dict::new();
563 catalog.insert(name("Type"), Object::Name(name("Catalog")));
564 catalog.insert(name("Pages"), Object::Ref(pages));
565 let root = w.put(Object::Dict(catalog));
566 (
567 w,
568 Refs {
569 content,
570 pages,
571 page,
572 root,
573 },
574 )
575 }
576
577 fn skeleton(
579 options: WriteOptions,
580 content_dict: Dict,
581 content_data: Vec<u8>,
582 raw: bool,
583 ) -> (Vec<u8>, Refs) {
584 let (w, refs) = build(options, content_dict, content_data, raw);
585 let bytes = w.finish(refs.root).expect("minimal document finishes");
586 (bytes, refs)
587 }
588
589 fn minimal_pdf(options: WriteOptions) -> (Vec<u8>, Refs) {
590 skeleton(options, Dict::new(), CONTENT.to_vec(), false)
591 }
592
593 fn assert_minimal_loads(bytes: &[u8], refs: &Refs) -> Document {
594 let doc = Document::load(bytes.to_vec()).expect("document loads");
595 assert_eq!(doc.page_count(), 1);
596 let page = doc.page(0).expect("page 0 exists");
597 assert_eq!(page.object_ref(), Some(refs.page));
598 assert_eq!(page.dict(), &page_dict(refs.pages, refs.content));
599 assert_eq!(page.content(&doc).expect("content decodes"), CONTENT);
600 doc
601 }
602
603 fn count_occurrences(haystack: &[u8], needle: &[u8]) -> usize {
604 haystack
605 .windows(needle.len())
606 .filter(|window| *window == needle)
607 .count()
608 }
609
610 #[test]
611 fn table_mode_minimal_document_loads() {
612 let (bytes, refs) = minimal_pdf(table_options());
613 assert!(bytes.starts_with(b"%PDF-1.7\n%\xE2\xE3\xCF\xD3\n"));
614 assert!(bytes.ends_with(b"%%EOF"));
615 assert_eq!(count_occurrences(&bytes, b"xref\n0 5\n"), 1);
616 assert_eq!(count_occurrences(&bytes, b"0000000000 65535 f\r\n"), 1);
617 assert_eq!(count_occurrences(&bytes, b"trailer\n"), 1);
618 assert_minimal_loads(&bytes, &refs);
619 }
620
621 #[test]
622 fn table_mode_ignores_object_streams_option() {
623 let options = WriteOptions {
624 object_streams: true,
625 ..table_options()
626 };
627 let (bytes, refs) = minimal_pdf(options);
628 assert_eq!(count_occurrences(&bytes, b"/ObjStm"), 0);
629 assert_minimal_loads(&bytes, &refs);
630 }
631
632 #[test]
633 fn stream_mode_minimal_document_loads() {
634 let (bytes, refs) = minimal_pdf(stream_options());
635 assert!(bytes.starts_with(b"%PDF-1.5\n%\xE2\xE3\xCF\xD3\n"));
636 assert!(bytes.ends_with(b"%%EOF"));
637 assert_eq!(count_occurrences(&bytes, b"/ObjStm"), 0);
638 assert_eq!(count_occurrences(&bytes, b"/XRef"), 1);
639 assert_minimal_loads(&bytes, &refs);
640 }
641
642 #[test]
643 fn object_streams_pack_and_resolve() {
644 let (bytes, refs) = minimal_pdf(objstm_options());
645 assert_eq!(count_occurrences(&bytes, b"/ObjStm"), 1);
646 let doc = assert_minimal_loads(&bytes, &refs);
647 let root = doc
648 .resolve(&Object::Ref(refs.root))
649 .expect("catalog resolves");
650 let catalog = root.as_dict().expect("catalog is a dictionary");
651 assert_eq!(catalog.get_name("Type"), Some(&name("Catalog")));
652 assert_eq!(catalog.get_ref("Pages"), Some(refs.pages));
653 }
654
655 #[test]
656 fn object_streams_chunk_at_two_hundred() {
657 let options = WriteOptions {
658 compress: false,
659 ..objstm_options()
660 };
661 let mut w = Writer::new(options);
662 let content = w.put_stream(Dict::new(), CONTENT.to_vec());
663 let int_refs: Vec<ObjRef> = (0..205).map(|i| w.put(Object::Int(i))).collect();
664 let pages = w.reserve();
665 let page = w.put(Object::Dict(page_dict(pages, content)));
666 let mut tree = Dict::new();
667 tree.insert(name("Type"), Object::Name(name("Pages")));
668 tree.insert(name("Kids"), Object::Array(vec![Object::Ref(page)]));
669 tree.insert(name("Count"), Object::Int(1));
670 w.fill(pages, Object::Dict(tree))
671 .expect("pages slot is fillable");
672 let mut catalog = Dict::new();
673 catalog.insert(name("Type"), Object::Name(name("Catalog")));
674 catalog.insert(name("Pages"), Object::Ref(pages));
675 let root = w.put(Object::Dict(catalog));
676 let bytes = w.finish(root).expect("document finishes");
677 assert_eq!(count_occurrences(&bytes, b"/ObjStm"), 2);
678 let doc = Document::load(bytes).expect("document loads");
679 assert_eq!(
680 doc.resolve(&Object::Ref(int_refs[0])).expect("resolves"),
681 Object::Int(0)
682 );
683 assert_eq!(
684 doc.resolve(&Object::Ref(int_refs[204])).expect("resolves"),
685 Object::Int(204)
686 );
687 assert_eq!(doc.page_count(), 1);
688 }
689
690 #[test]
691 fn refs_ascend_from_one_in_call_order() {
692 let mut w = Writer::new(table_options());
693 assert_eq!(w.put(Object::Null), ObjRef { num: 1, gen: 0 });
694 assert_eq!(w.reserve(), ObjRef { num: 2, gen: 0 });
695 assert_eq!(
696 w.put_stream(Dict::new(), Vec::new()),
697 ObjRef { num: 3, gen: 0 }
698 );
699 assert_eq!(
700 w.put_stream_raw(Dict::new(), Vec::new()),
701 ObjRef { num: 4, gen: 0 }
702 );
703 }
704
705 #[test]
706 fn fill_twice_reports_already_filled() {
707 let mut w = Writer::new(table_options());
708 let r = w.reserve();
709 w.fill(r, Object::Int(1)).expect("first fill lands");
710 match w.fill(r, Object::Int(2)) {
711 Err(Error::AlreadyFilled(seen)) => assert_eq!(seen, r),
712 other => panic!("expected AlreadyFilled, got {other:?}"),
713 }
714 }
715
716 #[test]
717 fn fill_rejects_foreign_and_wrong_generation_refs() {
718 let mut w = Writer::new(table_options());
719 let r = w.reserve();
720 let unallocated = w.fill(ObjRef { num: 99, gen: 0 }, Object::Null);
721 assert!(matches!(unallocated, Err(Error::Other(msg)) if msg.contains("99")));
722 let zero = w.fill(ObjRef { num: 0, gen: 0 }, Object::Null);
723 assert!(matches!(zero, Err(Error::Other(msg)) if !msg.is_empty()));
724 let wrong_gen = w.fill(ObjRef { num: r.num, gen: 1 }, Object::Null);
725 assert!(matches!(wrong_gen, Err(Error::Other(msg)) if msg.contains("generation")));
726 }
727
728 #[test]
729 fn finish_with_unfilled_reserve_reports_the_ref() {
730 let mut w = Writer::new(table_options());
731 let root = w.put(Object::Dict(Dict::new()));
732 let reserved = w.reserve();
733 match w.finish(root) {
734 Err(Error::Unfilled(seen)) => assert_eq!(seen, reserved),
735 other => panic!("expected Unfilled, got {other:?}"),
736 }
737 }
738
739 #[test]
740 fn nested_stream_surfaces_from_finish() {
741 for options in [table_options(), objstm_options()] {
742 let mut w = Writer::new(options);
743 let root = w.put(Object::Array(vec![Object::Stream(Stream {
744 dict: Dict::new(),
745 data: b"x".to_vec(),
746 })]));
747 assert!(matches!(w.finish(root), Err(Error::NestedStream)));
748 }
749 }
750
751 #[test]
752 fn compressed_stream_round_trips() {
753 let options = WriteOptions {
754 compress: true,
755 ..table_options()
756 };
757 let data: Vec<u8> = b"q 0.5 0 0 0.5 36 36 cm Q\n".repeat(40);
758 let (bytes, refs) = skeleton(options, Dict::new(), data.clone(), false);
759 let doc = Document::load(bytes).expect("document loads");
760 let resolved = doc
761 .resolve(&Object::Ref(refs.content))
762 .expect("content stream resolves");
763 let stream = resolved.as_stream().expect("content is a stream");
764 assert_eq!(stream.dict.get_name("Filter"), Some(&name("FlateDecode")));
765 assert_eq!(
766 stream.dict.get_int("Length"),
767 Some(stream.data.len() as i64)
768 );
769 assert!(stream.data.len() < data.len());
770 assert_eq!(doc.stream_data(stream).expect("stream decodes"), data);
771 }
772
773 #[test]
774 fn preset_filter_is_not_recompressed() {
775 let options = WriteOptions {
776 compress: true,
777 ..table_options()
778 };
779 let payload = b"Hello writer";
780 let encoded: Vec<u8> = payload
781 .iter()
782 .flat_map(|b| format!("{b:02X}").into_bytes())
783 .chain(*b">")
784 .collect();
785 let mut dict = Dict::new();
786 dict.insert(name("Filter"), Object::Name(name("ASCIIHexDecode")));
787 let (bytes, refs) = skeleton(options, dict, encoded.clone(), false);
788 let doc = Document::load(bytes).expect("document loads");
789 let resolved = doc
790 .resolve(&Object::Ref(refs.content))
791 .expect("content stream resolves");
792 let stream = resolved.as_stream().expect("content is a stream");
793 assert_eq!(stream.data, encoded, "pre-filtered data stays untouched");
794 assert_eq!(
795 stream.dict.get_name("Filter"),
796 Some(&name("ASCIIHexDecode"))
797 );
798 assert_eq!(doc.stream_data(stream).expect("stream decodes"), payload);
799 }
800
801 #[test]
802 fn put_stream_raw_never_compresses() {
803 let options = WriteOptions {
804 compress: true,
805 ..table_options()
806 };
807 let (bytes, refs) = skeleton(options, Dict::new(), CONTENT.to_vec(), true);
808 let doc = Document::load(bytes).expect("document loads");
809 let resolved = doc
810 .resolve(&Object::Ref(refs.content))
811 .expect("content stream resolves");
812 let stream = resolved.as_stream().expect("content is a stream");
813 assert_eq!(stream.data, CONTENT);
814 assert!(stream.dict.get("Filter").is_none());
815 assert_eq!(stream.dict.get_int("Length"), Some(CONTENT.len() as i64));
816 }
817
818 #[test]
819 fn table_offsets_past_ten_digits_are_rejected() {
820 assert_eq!(table_offset(9_999_999_999).ok(), Some(9_999_999_999));
821 assert!(table_offset(10_000_000_000).is_err());
822 }
823
824 #[test]
825 fn id_derives_from_the_emitted_content() {
826 let (a, refs) = minimal_pdf(table_options());
827 assert_eq!(refs.root.gen, 0);
828 let (b, other_refs) = skeleton(
829 table_options(),
830 Dict::new(),
831 b"BT /F1 12 Tf 72 720 Td (Hello, other) Tj ET".to_vec(),
832 false,
833 );
834 assert_eq!(other_refs.root.gen, 0);
835 let id_of = |bytes: &[u8]| {
836 let xref = load_xref(bytes).expect("xref loads");
837 let id = xref.trailer.get_array("ID").expect("/ID array present");
838 id[0]
839 .as_str_bytes()
840 .expect("/ID entry is a string")
841 .to_vec()
842 };
843 assert_ne!(
844 id_of(&a),
845 id_of(&b),
846 "/ID must depend on the emitted content"
847 );
848 }
849
850 #[test]
851 fn id_pair_is_present_and_identical() {
852 for options in [table_options(), stream_options(), objstm_options()] {
853 let (bytes, refs) = minimal_pdf(options);
854 assert_eq!(refs.root.gen, 0);
855 let xref = load_xref(&bytes).expect("xref loads");
856 let id = xref.trailer.get_array("ID").expect("/ID array present");
857 assert_eq!(id.len(), 2);
858 let first = id[0].as_str_bytes().expect("/ID entry is a string");
859 let second = id[1].as_str_bytes().expect("/ID entry is a string");
860 assert_eq!(first.len(), 16);
861 assert_eq!(first, second);
862 }
863 }
864
865 #[test]
866 fn output_is_deterministic() {
867 for options in [table_options(), stream_options(), objstm_options()] {
868 let (first, refs) = minimal_pdf(options);
869 let (second, again) = minimal_pdf(options);
870 assert_eq!(refs.content, again.content);
871 assert_eq!(first, second, "options {options:?} must be deterministic");
872 }
873 }
874
875 #[test]
876 fn finish_into_matches_finish() {
877 for options in [table_options(), stream_options(), objstm_options()] {
878 let (bytes, _) = minimal_pdf(options);
879 let (w, refs) = build(options, Dict::new(), CONTENT.to_vec(), false);
880 let mut out = Vec::new();
881 w.finish_into(refs.root, &mut out)
882 .expect("finish_into succeeds");
883 assert_eq!(out, bytes, "options {options:?}");
884 }
885 }
886
887 #[test]
888 fn finish_into_with_matches_finish() {
889 for options in [table_options(), stream_options(), objstm_options()] {
890 let (bytes, _) = minimal_pdf(options);
891 let (w, refs) = build(options, Dict::new(), CONTENT.to_vec(), false);
892 let sink = pdfboss_core::block_on(w.finish_into_with(refs.root, Vec::new()))
893 .expect("finish_into_with succeeds");
894 assert_eq!(sink, bytes, "options {options:?}");
895 }
896 }
897
898 struct Recording {
901 chunks: Vec<Vec<u8>>,
902 }
903
904 impl crate::sink::AsyncByteSink for Recording {
905 fn write_all<'a>(
906 &'a mut self,
907 buf: &'a [u8],
908 ) -> pdfboss_core::source::BoxFuture<'a, Result<()>> {
909 self.chunks.push(buf.to_vec());
910 Box::pin(std::future::ready(Ok(())))
911 }
912 }
913
914 #[test]
918 fn emission_arrives_in_bounded_chunks() {
919 for options in [table_options(), stream_options(), objstm_options()] {
920 let (bytes, _) = minimal_pdf(options);
921 let (w, refs) = build(options, Dict::new(), CONTENT.to_vec(), false);
922 let sink = pdfboss_core::block_on(
923 w.finish_into_with(refs.root, Recording { chunks: Vec::new() }),
924 )
925 .expect("finish_into_with succeeds");
926 assert_eq!(sink.chunks.concat(), bytes, "options {options:?}");
927 assert!(
928 sink.chunks.len() > 3,
929 "options {options:?}: emission must arrive in many chunks, got {}",
930 sink.chunks.len()
931 );
932 assert!(
933 sink.chunks.iter().all(|chunk| chunk.len() < bytes.len()),
934 "options {options:?}: no chunk may be the whole file"
935 );
936 }
937 }
938
939 #[test]
943 fn finish_into_with_over_an_owned_sink_is_spawnable() {
944 fn assert_send_static<F: std::future::Future + Send + 'static>(_: &F) {}
945
946 let (w, refs) = build(stream_options(), Dict::new(), CONTENT.to_vec(), false);
947 let future = w.finish_into_with(refs.root, Vec::new());
948 assert_send_static(&future);
949 let bytes = pdfboss_core::block_on(future).expect("emission succeeds");
950 assert!(bytes.ends_with(b"%%EOF"));
951 }
952
953 #[test]
956 fn finish_into_with_reports_unfilled_reserves() {
957 let mut w = Writer::new(table_options());
958 let root = w.put(Object::Dict(Dict::new()));
959 let reserved = w.reserve();
960 let sink = Recording { chunks: Vec::new() };
961 match pdfboss_core::block_on(w.finish_into_with(root, sink)) {
962 Err(Error::Unfilled(seen)) => assert_eq!(seen, reserved),
963 other => panic!("expected Unfilled, got {:?}", other.map(|s| s.chunks.len())),
964 }
965 }
966}