1use crate::SOURCE_FORMAT_PDF;
34use crate::container::{Descriptor, ObjectSource, UNIVERSE};
35use crate::dra::op::PackItem;
36use crate::dra::{Op, Program};
37use crate::encode::candidates::{Candidate, CandidateKind};
38use crate::error::Result;
39use crate::integrity::sha256;
40use crate::limits::Limits;
41
42#[cfg(feature = "rans")]
43use crate::entropy::{
44 ALPHABET, CODER_ORDER0_BYTE_RANS, CODER_VERSION_1, EntropyChannelDescriptor, EntropyModel,
45 encode_channel,
46};
47
48use super::physical::{ObjRole, PdfObjectSpan, PdfPhysical, PhysicalKind, scan};
49
50pub const XREF_SLOT: u8 = u8::MAX;
52
53pub const MAX_MARKED_OBJECTS: usize = XREF_SLOT as usize;
56
57pub struct LayoutPlan {
61 pub items: Vec<PackItem>,
64 pub data: Vec<u8>,
66 pub xref_predicted: usize,
68 pub xref_literal: usize,
70 pub startxref_predicted: bool,
72}
73
74pub fn build_layout_plan(input: &[u8], limits: Limits) -> Result<Option<LayoutPlan>> {
83 let physical = match scan(input, limits) {
84 Ok(p) => p,
85 Err(_) => return Ok(None),
86 };
87 build_layout_plan_with(input, limits, &physical)
88}
89
90pub(crate) fn build_layout_plan_with(
92 input: &[u8],
93 _limits: Limits,
94 physical: &PdfPhysical,
95) -> Result<Option<LayoutPlan>> {
96 if !physical
99 .spans
100 .iter()
101 .any(|s| s.kind == PhysicalKind::XrefSection)
102 {
103 return Ok(None);
104 }
105 if physical
106 .objects
107 .iter()
108 .any(|o| o.role == ObjRole::XRefStream)
109 {
110 return Ok(None);
111 }
112 if physical.objects.len() > MAX_MARKED_OBJECTS {
113 return Ok(None);
114 }
115
116 let mut data: Vec<u8> = Vec::new();
120 let mut items: Vec<PackItem> = Vec::new();
121 let mut pos: u64 = 0;
125 let mut slot_value = [0u64; 256];
126 let mut slot_marked = [false; 256];
127 let mut xref_predicted: usize = 0;
128 let mut xref_literal: usize = 0;
129 let mut startxref_predicted = false;
130
131 for span in &physical.spans {
132 let start = span.start as usize;
133 let end = start + span.len as usize;
134 let bytes = &input[start..end];
135
136 if pos != span.start {
137 return Ok(None);
139 }
140
141 match span.kind {
142 PhysicalKind::ObjHeader => {
143 if let Some(idx) = object_index_at(&physical.objects, span.start) {
144 let slot = idx as u8;
145 items.push(PackItem::Mark { slot });
146 slot_value[slot as usize] = pos;
147 slot_marked[slot as usize] = true;
148 }
149 if !push_pack_literal(&mut data, &mut items, bytes, &mut pos) {
150 return Ok(None);
151 }
152 }
153 PhysicalKind::XrefSection => {
154 items.push(PackItem::Mark { slot: XREF_SLOT });
156 slot_value[XREF_SLOT as usize] = pos;
157 slot_marked[XREF_SLOT as usize] = true;
158
159 match parse_classic_xref(bytes) {
160 Some(pieces) => {
161 for piece in pieces {
162 match piece {
163 XrefPiece::Literal { start, len } => {
164 if !push_pack_literal(
165 &mut data,
166 &mut items,
167 &bytes[start..start + len],
168 &mut pos,
169 ) {
170 return Ok(None);
171 }
172 }
173 XrefPiece::Entry {
174 start,
175 number,
176 offset,
177 in_use,
178 } => {
179 let slot = if in_use {
180 offset.and_then(|value| {
181 predicted_slot(
182 &physical.objects,
183 &slot_value,
184 &slot_marked,
185 number,
186 value,
187 )
188 })
189 } else {
190 None
191 };
192 match slot {
193 Some(slot) => {
194 items.push(PackItem::Emit { slot, width: 10 });
195 pos += 10;
196 if !push_pack_literal(
197 &mut data,
198 &mut items,
199 &bytes[start + 10..start + 20],
200 &mut pos,
201 ) {
202 return Ok(None);
203 }
204 xref_predicted += 1;
205 }
206 None => {
207 if !push_pack_literal(
208 &mut data,
209 &mut items,
210 &bytes[start..start + 20],
211 &mut pos,
212 ) {
213 return Ok(None);
214 }
215 xref_literal += 1;
216 }
217 }
218 }
219 }
220 }
221 }
222 None => {
223 if !push_pack_literal(&mut data, &mut items, bytes, &mut pos) {
225 return Ok(None);
226 }
227 }
228 }
229 }
230 PhysicalKind::StartXref => match predict_startxref(bytes, &slot_value, &slot_marked) {
231 Some((prefix_len, width)) => {
232 if !push_pack_literal(&mut data, &mut items, &bytes[..prefix_len], &mut pos) {
233 return Ok(None);
234 }
235 items.push(PackItem::Emit {
236 slot: XREF_SLOT,
237 width,
238 });
239 pos += width as u64;
240 startxref_predicted = true;
241 }
242 None => {
243 if !push_pack_literal(&mut data, &mut items, bytes, &mut pos) {
244 return Ok(None);
245 }
246 }
247 },
248 _ => {
249 if !push_pack_literal(&mut data, &mut items, bytes, &mut pos) {
250 return Ok(None);
251 }
252 }
253 }
254 }
255
256 Ok(Some(LayoutPlan {
257 items,
258 data,
259 xref_predicted,
260 xref_literal,
261 startxref_predicted,
262 }))
263}
264
265pub fn propose_pdf_layout(input: &[u8], limits: Limits) -> Result<Option<Candidate>> {
273 let physical = match scan(input, limits) {
274 Ok(p) => p,
275 Err(_) => return Ok(None),
276 };
277 propose_pdf_layout_with(input, limits, &physical)
278}
279
280pub(crate) fn propose_pdf_layout_with(
282 input: &[u8],
283 limits: Limits,
284 physical: &PdfPhysical,
285) -> Result<Option<Candidate>> {
286 let plan = match build_layout_plan_with(input, limits, physical)? {
287 Some(p) => p,
288 None => return Ok(None),
289 };
290
291 let startxref_predicted = emit_count(&plan.items).saturating_sub(plan.xref_predicted);
294 let format_basis = format!(
295 "pdf-layout;objects={};xref_predicted={};xref_literal={};startxref_predicted={}",
296 marked_object_count(&plan.items),
297 plan.xref_predicted,
298 plan.xref_literal,
299 startxref_predicted
300 );
301
302 let descriptor = Descriptor {
303 universe: UNIVERSE.to_string(),
304 source_format: SOURCE_FORMAT_PDF,
305 format_basis,
306 models: vec![],
307 channels: vec![],
308 objects: vec![ObjectSource::Inline(plan.data)],
309 program: Program::new(vec![Op::PackSegments {
310 data_object: 0,
311 items: plan.items,
312 }]),
313 observation_index: None,
314 seek_directory: false,
315 checkpoints: None,
316 source_sha256: sha256(input),
317 source_len: input.len() as u64,
318 };
319
320 let candidate = Candidate {
321 kind: CandidateKind::PdfLayout,
322 descriptor,
323 };
324
325 let (encoded, _) = candidate.descriptor.serialize()?;
328 let parsed = match Descriptor::parse(&encoded, limits) {
329 Ok(p) => p,
330 Err(_) => return Ok(None),
331 };
332 let out = match crate::materialize::materialize(&parsed, limits) {
333 Ok(o) => o,
334 Err(_) => return Ok(None),
335 };
336 if out != input {
337 return Ok(None);
338 }
339
340 Ok(Some(candidate))
341}
342
343#[cfg(feature = "rans")]
357pub fn propose_pdf_layout_rans(input: &[u8], limits: Limits) -> Result<Option<Candidate>> {
358 let physical = match scan(input, limits) {
359 Ok(p) => p,
360 Err(_) => return Ok(None),
361 };
362 propose_pdf_layout_rans_with(input, limits, &physical)
363}
364
365#[cfg(feature = "rans")]
367pub(crate) fn propose_pdf_layout_rans_with(
368 input: &[u8],
369 limits: Limits,
370 physical: &PdfPhysical,
371) -> Result<Option<Candidate>> {
372 let plan = match build_layout_plan_with(input, limits, physical)? {
373 Some(p) => p,
374 None => return Ok(None),
375 };
376
377 let plan_bytes = crate::dra::op::encode_items(&plan.items)?;
378
379 let mut data_counts = [0u64; ALPHABET];
381 for &b in &plan.data {
382 data_counts[b as usize] += 1;
383 }
384 let data_model = EntropyModel::from_counts(&data_counts, 12)?;
385 let data_capsule = encode_channel(&data_model, &plan.data)?;
386
387 let mut plan_counts = [0u64; ALPHABET];
389 for &b in &plan_bytes {
390 plan_counts[b as usize] += 1;
391 }
392 let plan_model = EntropyModel::from_counts(&plan_counts, 12)?;
393 let plan_capsule = encode_channel(&plan_model, &plan_bytes)?;
394
395 let data_channel = EntropyChannelDescriptor {
396 coder: CODER_ORDER0_BYTE_RANS,
397 coder_version: CODER_VERSION_1,
398 scale_bits: data_model.scale_bits,
399 lane_count: 1,
400 model_id: 0,
401 symbol_count: data_capsule.symbol_count,
402 decoded_length: data_capsule.decoded_length,
403 initial_state: data_capsule.initial_state,
404 payload: data_capsule.payload,
405 };
406 let plan_channel = EntropyChannelDescriptor {
407 coder: CODER_ORDER0_BYTE_RANS,
408 coder_version: CODER_VERSION_1,
409 scale_bits: plan_model.scale_bits,
410 lane_count: 1,
411 model_id: 1,
412 symbol_count: plan_capsule.symbol_count,
413 decoded_length: plan_capsule.decoded_length,
414 initial_state: plan_capsule.initial_state,
415 payload: plan_capsule.payload,
416 };
417
418 let format_basis = format!(
419 "pdf-layout-rans;objects={};xref_predicted={};xref_literal={}",
420 marked_object_count(&plan.items),
421 plan.xref_predicted,
422 plan.xref_literal
423 );
424
425 let descriptor = Descriptor {
426 universe: UNIVERSE.to_string(),
427 source_format: SOURCE_FORMAT_PDF,
428 format_basis,
429 models: vec![data_model, plan_model],
430 channels: vec![data_channel, plan_channel],
431 objects: vec![],
432 program: Program::new(vec![Op::PackedChannels {
433 data_channel: 0,
434 plan_channel: 1,
435 declared_output_len: input.len() as u64,
436 }]),
437 observation_index: None,
438 seek_directory: false,
439 checkpoints: None,
440 source_sha256: sha256(input),
441 source_len: input.len() as u64,
442 };
443
444 let candidate = Candidate {
445 kind: CandidateKind::PdfLayoutRans,
446 descriptor,
447 };
448
449 let (encoded, _) = candidate.descriptor.serialize()?;
452 let parsed = match Descriptor::parse(&encoded, limits) {
453 Ok(p) => p,
454 Err(_) => return Ok(None),
455 };
456 let out = match crate::materialize::materialize(&parsed, limits) {
457 Ok(o) => o,
458 Err(_) => return Ok(None),
459 };
460 if out != input {
461 return Ok(None);
462 }
463
464 Ok(Some(candidate))
465}
466
467fn marked_object_count(items: &[PackItem]) -> usize {
469 items
470 .iter()
471 .filter(|item| matches!(item, PackItem::Mark { slot } if *slot != XREF_SLOT))
472 .count()
473}
474
475fn emit_count(items: &[PackItem]) -> usize {
477 items
478 .iter()
479 .filter(|item| matches!(item, PackItem::Emit { .. }))
480 .count()
481}
482
483fn push_pack_literal(
487 data: &mut Vec<u8>,
488 items: &mut Vec<PackItem>,
489 bytes: &[u8],
490 pos: &mut u64,
491) -> bool {
492 if !push_literal(items, data, bytes) {
493 return false;
494 }
495 *pos += bytes.len() as u64;
496 true
497}
498
499pub(crate) fn push_literal(items: &mut Vec<PackItem>, data: &mut Vec<u8>, bytes: &[u8]) -> bool {
506 if bytes.is_empty() {
507 return true;
508 }
509 let Ok(len) = u32::try_from(bytes.len()) else {
510 return false;
511 };
512 if let Some(PackItem::Literal { len: prev }) = items.last_mut() {
513 let Some(total) = prev.checked_add(len) else {
514 return false;
515 };
516 *prev = total;
517 } else {
518 items.push(PackItem::Literal { len });
519 }
520 data.extend_from_slice(bytes);
521 true
522}
523
524fn object_index_at(objects: &[PdfObjectSpan], start: u64) -> Option<usize> {
526 objects.iter().position(|o| o.start == start)
527}
528
529fn predicted_slot(
532 objects: &[PdfObjectSpan],
533 slot_value: &[u64; 256],
534 slot_marked: &[bool; 256],
535 number: u64,
536 value: u64,
537) -> Option<u8> {
538 objects.iter().enumerate().find_map(|(i, o)| {
539 let slot = i as u8;
540 (o.number == number && slot_marked[slot as usize] && slot_value[slot as usize] == value)
541 .then_some(slot)
542 })
543}
544
545fn predict_startxref(
549 bytes: &[u8],
550 slot_value: &[u64; 256],
551 slot_marked: &[bool; 256],
552) -> Option<(usize, u8)> {
553 if !slot_marked[XREF_SLOT as usize] {
554 return None;
555 }
556 let mut i = bytes.len();
557 while i > 0 && bytes[i - 1].is_ascii_digit() {
558 i -= 1;
559 }
560 let width = bytes.len() - i;
561 if width == 0 || width > 20 {
562 return None;
563 }
564 let value = parse_digits(&bytes[i..])?;
565 if value != slot_value[XREF_SLOT as usize] {
566 return None;
567 }
568 Some((i, width as u8))
569}
570
571pub(crate) enum XrefPiece {
574 Literal { start: usize, len: usize },
576 Entry {
578 start: usize,
580 number: u64,
582 offset: Option<u64>,
584 in_use: bool,
586 },
587}
588
589pub(crate) fn parse_classic_xref(bytes: &[u8]) -> Option<Vec<XrefPiece>> {
598 if !bytes.starts_with(b"xref") {
599 return None;
600 }
601 let mut pieces = Vec::new();
602 let mut pos = 4usize;
603
604 let eol = eol_len(&bytes[pos..])?;
606 pieces.push(XrefPiece::Literal {
607 start: 0,
608 len: pos + eol,
609 });
610 pos += eol;
611
612 let mut any = false;
613 while pos < bytes.len() {
614 let header_start = pos;
616 let (start, after_start) = parse_uint_at(bytes, pos)?;
617 pos = after_start;
618 let spaces_start = pos;
619 while pos < bytes.len() && bytes[pos] == b' ' {
620 pos += 1;
621 }
622 if pos == spaces_start {
623 return None;
624 }
625 let (count, after_count) = parse_uint_at(bytes, pos)?;
626 pos = after_count;
627 let eol = eol_len(&bytes[pos..])?;
628 let header_end = pos + eol;
629 pieces.push(XrefPiece::Literal {
630 start: header_start,
631 len: header_end - header_start,
632 });
633 pos = header_end;
634
635 for i in 0..count {
636 let end = pos.checked_add(20)?;
637 if end > bytes.len() {
638 return None;
639 }
640 let entry = &bytes[pos..end];
641 if !is_entry_shape(entry) {
642 return None;
643 }
644 let number = start.checked_add(i)?;
645 let in_use = entry[17] == b'n';
646 let offset = parse_digits(&entry[0..10]);
647 pieces.push(XrefPiece::Entry {
648 start: pos,
649 number,
650 offset,
651 in_use,
652 });
653 pos = end;
654 }
655 any = true;
656 }
657
658 if !any || pos != bytes.len() {
659 return None;
660 }
661 Some(pieces)
662}
663
664fn is_entry_shape(entry: &[u8]) -> bool {
666 if entry.len() != 20 {
667 return false;
668 }
669 if entry[10] != b' ' || entry[16] != b' ' {
670 return false;
671 }
672 if !entry[11..16].iter().all(u8::is_ascii_digit) {
673 return false;
674 }
675 if entry[17] != b'n' && entry[17] != b'f' {
676 return false;
677 }
678 matches!(
679 (entry[18], entry[19]),
680 (b'\r', b'\n') | (b'\n', b'\r') | (b' ', b'\n') | (b' ', b'\r')
681 )
682}
683
684fn eol_len(bytes: &[u8]) -> Option<usize> {
686 match bytes {
687 [b'\r', b'\n', ..] => Some(2),
688 [b'\n', ..] | [b'\r', ..] => Some(1),
689 _ => None,
690 }
691}
692
693fn parse_uint_at(bytes: &[u8], at: usize) -> Option<(u64, usize)> {
695 let mut i = at;
696 let mut value: u64 = 0;
697 while i < bytes.len() && bytes[i].is_ascii_digit() {
698 value = value
699 .checked_mul(10)?
700 .checked_add(u64::from(bytes[i] - b'0'))?;
701 i += 1;
702 }
703 if i == at {
704 return None;
705 }
706 Some((value, i))
707}
708
709pub(crate) fn parse_digits(digits: &[u8]) -> Option<u64> {
711 if digits.is_empty() {
712 return None;
713 }
714 let mut value: u64 = 0;
715 for &b in digits {
716 if !b.is_ascii_digit() {
717 return None;
718 }
719 value = value.checked_mul(10)?.checked_add(u64::from(b - b'0'))?;
720 }
721 Some(value)
722}
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727 use crate::adapter::pdf::samples::{is_negative_control, sample_pdfs};
728 use crate::container::Descriptor;
729
730 fn sample(name: &str) -> Vec<u8> {
731 sample_pdfs()
732 .into_iter()
733 .find(|(n, _)| *n == name)
734 .unwrap_or_else(|| panic!("sample {name} missing"))
735 .1
736 }
737
738 fn basis_field(basis: &str, key: &str) -> Option<u64> {
739 basis.split(';').find_map(|part| {
740 let (k, v) = part.split_once('=')?;
741 (k == key).then(|| v.parse().ok()).flatten()
742 })
743 }
744
745 fn pack_items(cand: &Candidate) -> &[PackItem] {
747 cand.descriptor
748 .program
749 .ops
750 .iter()
751 .find_map(|op| match op {
752 Op::PackSegments { items, .. } => Some(items.as_slice()),
753 _ => None,
754 })
755 .expect("layout program is one PackSegments op")
756 }
757
758 fn pack_emit_count(cand: &Candidate) -> usize {
760 pack_items(cand)
761 .iter()
762 .filter(|item| matches!(item, PackItem::Emit { .. }))
763 .count()
764 }
765
766 fn assert_materializes_exactly(name: &str, bytes: &[u8]) {
767 let cand = propose_pdf_layout(bytes, Limits::DEFAULT)
768 .unwrap()
769 .unwrap_or_else(|| panic!("{name} must propose a layout candidate"));
770 assert_eq!(cand.kind, CandidateKind::PdfLayout);
771 assert_eq!(cand.descriptor.source_format, SOURCE_FORMAT_PDF);
772 assert_eq!(cand.descriptor.source_len, bytes.len() as u64);
773 assert_eq!(
775 cand.descriptor.objects.len(),
776 1,
777 "{name} layout carries one packed data object"
778 );
779 assert!(matches!(
780 cand.descriptor.program.ops.as_slice(),
781 [Op::PackSegments { .. }]
782 ));
783 assert!(cand.descriptor.models.is_empty());
784 assert!(cand.descriptor.channels.is_empty());
785
786 let (encoded, _) = cand.descriptor.serialize().unwrap();
787 let parsed = Descriptor::parse(&encoded, Limits::DEFAULT).unwrap();
788 let out = crate::materialize::materialize(&parsed, Limits::DEFAULT).unwrap();
789 assert_eq!(out, bytes, "{name} layout candidate materializes exactly");
790 assert_eq!(sha256(&out), sha256(bytes), "{name} layout sha");
791 }
792
793 #[test]
794 fn layout_is_exact_on_corpus() {
795 let mut accepted = 0usize;
796 for (name, bytes) in sample_pdfs() {
797 if is_negative_control(name) {
798 assert!(
799 propose_pdf_layout(&bytes, Limits::DEFAULT)
800 .unwrap()
801 .is_none(),
802 "{name} is not a classic-xref PDF and must decline"
803 );
804 continue;
805 }
806 if propose_pdf_layout(&bytes, Limits::DEFAULT)
807 .unwrap()
808 .is_some()
809 {
810 assert_materializes_exactly(name, &bytes);
811 accepted += 1;
812 }
813 }
814 assert!(
815 accepted >= 3,
816 "expected several accepted classic-xref samples, found {accepted}"
817 );
818 }
819
820 #[test]
821 fn layout_v2_exact() {
822 for name in ["classic.pdf", "many.pdf", "bigtext.pdf"] {
823 assert_materializes_exactly(name, &sample(name));
824 }
825 }
826
827 #[test]
828 fn layout_v2_predicts_many_entries() {
829 let bytes = sample("many.pdf");
830 let cand = propose_pdf_layout(&bytes, Limits::DEFAULT)
831 .unwrap()
832 .unwrap();
833 let emits = pack_emit_count(&cand);
834 assert!(
835 emits >= 100,
836 "many.pdf must predict at least 100 xref offsets, got {emits}"
837 );
838 let predicted = basis_field(&cand.descriptor.format_basis, "xref_predicted")
840 .expect("format basis must report xref_predicted");
841 let startxref = basis_field(&cand.descriptor.format_basis, "startxref_predicted")
842 .expect("format basis must report startxref_predicted");
843 assert_eq!(predicted + startxref, emits as u64);
844 assert!(predicted >= 100, "many.pdf xref predictions: {predicted}");
845
846 let classic = propose_pdf_layout(&sample("classic.pdf"), Limits::DEFAULT)
848 .unwrap()
849 .unwrap();
850 assert!(pack_emit_count(&classic) > 0);
851 }
852
853 #[test]
854 fn layout_falls_back_on_bad_offset() {
855 let mut b: Vec<u8> = Vec::new();
859 b.extend_from_slice(b"%PDF-1.4\n");
860 let off1 = b.len() as u64;
861 b.extend_from_slice(b"1 0 obj\n<< /Type /Catalog >>\nendobj\n");
862 let xref = b.len() as u64;
863 let wrong = off1 + 3;
864 b.extend_from_slice(
865 format!("xref\n0 2\n0000000000 65535 f \n{wrong:010} 00000 n \n").as_bytes(),
866 );
867 b.extend_from_slice(
868 format!("trailer\n<< /Size 2 /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n").as_bytes(),
869 );
870
871 let cand = propose_pdf_layout(&b, Limits::DEFAULT).unwrap().unwrap();
872 assert_eq!(
873 basis_field(&cand.descriptor.format_basis, "xref_predicted"),
874 Some(0),
875 "a mismatched offset must never be predicted"
876 );
877
878 let emits = pack_emit_count(&cand);
880 assert_eq!(
881 emits, 1,
882 "only startxref is predicted; bad entry is literal"
883 );
884
885 let (encoded, _) = cand.descriptor.serialize().unwrap();
886 let parsed = Descriptor::parse(&encoded, Limits::DEFAULT).unwrap();
887 let out = crate::materialize::materialize(&parsed, Limits::DEFAULT).unwrap();
888 assert_eq!(out, b, "fallback must still be byte-exact");
889 }
890
891 #[test]
892 fn layout_v2_declines() {
893 assert!(
895 propose_pdf_layout(&sample("xrefstream.pdf"), Limits::DEFAULT)
896 .unwrap()
897 .is_none(),
898 "an xref-stream PDF must decline the layout candidate"
899 );
900 assert!(
903 propose_pdf_layout(&classic_with_objects(256), Limits::DEFAULT)
904 .unwrap()
905 .is_none(),
906 "256 objects exceeds the 255 markable slots"
907 );
908 assert!(
910 propose_pdf_layout(&classic_with_objects(255), Limits::DEFAULT)
911 .unwrap()
912 .is_some(),
913 "255 objects must still be markable"
914 );
915 }
916
917 fn classic_with_objects(n: usize) -> Vec<u8> {
919 let mut b: Vec<u8> = Vec::new();
920 b.extend_from_slice(b"%PDF-1.4\n");
921 let mut offsets = Vec::with_capacity(n);
922 for number in 1..=n {
923 offsets.push(b.len() as u64);
924 b.extend_from_slice(format!("{number} 0 obj\n<< >>\nendobj\n").as_bytes());
925 }
926 let xref = b.len() as u64;
927 b.extend_from_slice(format!("xref\n0 {}\n", n + 1).as_bytes());
928 b.extend_from_slice(b"0000000000 65535 f \n");
929 for &off in &offsets {
930 b.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
931 }
932 b.extend_from_slice(
933 format!(
934 "trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n",
935 n + 1
936 )
937 .as_bytes(),
938 );
939 b
940 }
941
942 #[test]
943 fn layout_v2_deterministic() {
944 for name in ["classic.pdf", "many.pdf", "bigtext.pdf"] {
945 let bytes = sample(name);
946 let a = propose_pdf_layout(&bytes, Limits::DEFAULT)
947 .unwrap()
948 .unwrap()
949 .descriptor
950 .serialize()
951 .unwrap()
952 .0;
953 let b = propose_pdf_layout(&bytes, Limits::DEFAULT)
954 .unwrap()
955 .unwrap()
956 .descriptor
957 .serialize()
958 .unwrap()
959 .0;
960 assert_eq!(a, b, "{name} layout bytes must be deterministic");
961 }
962 }
963
964 #[cfg(feature = "rans")]
967 fn assert_rans_materializes_exactly(name: &str, bytes: &[u8]) {
968 let cand = propose_pdf_layout_rans(bytes, Limits::DEFAULT)
969 .unwrap()
970 .unwrap_or_else(|| panic!("{name} must propose a layout-rANS candidate"));
971 assert_eq!(cand.kind, CandidateKind::PdfLayoutRans);
972 assert_eq!(cand.descriptor.source_format, SOURCE_FORMAT_PDF);
973 assert_eq!(cand.descriptor.source_len, bytes.len() as u64);
974 assert!(cand.descriptor.objects.is_empty());
976 assert_eq!(cand.descriptor.models.len(), 2);
977 assert_eq!(cand.descriptor.channels.len(), 2);
978 assert_eq!(cand.descriptor.channels[0].model_id, 0);
979 assert_eq!(cand.descriptor.channels[1].model_id, 1);
980 assert!(matches!(
981 cand.descriptor.program.ops.as_slice(),
982 [Op::PackedChannels {
983 data_channel: 0,
984 plan_channel: 1,
985 ..
986 }]
987 ));
988
989 let (encoded, _) = cand.descriptor.serialize().unwrap();
990 let parsed = Descriptor::parse(&encoded, Limits::DEFAULT).unwrap();
991 let out = crate::materialize::materialize(&parsed, Limits::DEFAULT).unwrap();
992 assert_eq!(out, bytes, "{name} layout-rANS materializes exactly");
993 assert_eq!(sha256(&out), sha256(bytes), "{name} layout-rANS sha");
994
995 let (forced, report) =
997 crate::encode::encode_with(bytes, Limits::DEFAULT, Some(CandidateKind::PdfLayoutRans))
998 .unwrap();
999 assert_eq!(report.kind, CandidateKind::PdfLayoutRans);
1000 let (forced_out, _) =
1001 crate::materialize::decode_to_bytes(&forced, Limits::DEFAULT).unwrap();
1002 assert_eq!(forced_out, bytes, "{name} forced layout-rANS bytes");
1003 assert_eq!(
1004 sha256(&forced_out),
1005 sha256(bytes),
1006 "{name} forced layout-rANS sha"
1007 );
1008 }
1009
1010 #[cfg(feature = "rans")]
1011 #[test]
1012 fn layout_rans_exact() {
1013 for name in ["classic.pdf", "bigtext.pdf", "many.pdf"] {
1014 assert_rans_materializes_exactly(name, &sample(name));
1015 }
1016 }
1017
1018 #[cfg(feature = "rans")]
1019 #[test]
1020 fn layout_rans_deterministic() {
1021 for name in ["classic.pdf", "bigtext.pdf", "many.pdf"] {
1022 let bytes = sample(name);
1023 let a = propose_pdf_layout_rans(&bytes, Limits::DEFAULT)
1024 .unwrap()
1025 .unwrap()
1026 .descriptor
1027 .serialize()
1028 .unwrap()
1029 .0;
1030 let b = propose_pdf_layout_rans(&bytes, Limits::DEFAULT)
1031 .unwrap()
1032 .unwrap()
1033 .descriptor
1034 .serialize()
1035 .unwrap()
1036 .0;
1037 assert_eq!(a, b, "{name} layout-rANS bytes must be deterministic");
1038 }
1039 }
1040
1041 #[cfg(feature = "rans")]
1042 #[test]
1043 fn layout_rans_declines_on_non_pdf() {
1044 for name in ["notpdf.bin", "malformed.pdf", "xrefstream.pdf"] {
1047 assert!(
1048 propose_pdf_layout_rans(&sample(name), Limits::DEFAULT)
1049 .unwrap()
1050 .is_none(),
1051 "{name} must decline the layout-rANS candidate"
1052 );
1053 }
1054 }
1055
1056 #[test]
1057 fn layout_measurements_report() {
1058 for (name, bytes) in sample_pdfs() {
1059 let Some(cand) = propose_pdf_layout(&bytes, Limits::DEFAULT).unwrap() else {
1060 eprintln!("layout[{name}]: declined");
1061 continue;
1062 };
1063 let (layout_bytes, _) = cand.descriptor.serialize().unwrap();
1064 let emits = pack_emit_count(&cand);
1065 eprintln!(
1066 "layout[{name}] source={} items={} emits={emits} layout={}",
1067 bytes.len(),
1068 pack_items(&cand).len(),
1069 layout_bytes.len(),
1070 );
1071 }
1072
1073 for name in ["classic.pdf", "bigtext.pdf", "many.pdf"] {
1074 let bytes = sample(name);
1075 let (layout_bytes, _) =
1076 crate::encode::encode_with(&bytes, Limits::DEFAULT, Some(CandidateKind::PdfLayout))
1077 .unwrap();
1078 let (raw_bytes, _) =
1079 crate::encode::encode_with(&bytes, Limits::DEFAULT, Some(CandidateKind::Raw))
1080 .unwrap();
1081 #[cfg(feature = "rans")]
1082 let (byte_rans_bytes, _) =
1083 crate::encode::encode_with(&bytes, Limits::DEFAULT, Some(CandidateKind::ByteRans))
1084 .unwrap();
1085 #[cfg(not(feature = "rans"))]
1086 let byte_rans_bytes: Vec<u8> = Vec::new();
1087 #[cfg(feature = "rans")]
1088 let (layout_rans_bytes, _) = crate::encode::encode_with(
1089 &bytes,
1090 Limits::DEFAULT,
1091 Some(CandidateKind::PdfLayoutRans),
1092 )
1093 .unwrap();
1094 #[cfg(not(feature = "rans"))]
1095 let layout_rans_bytes: Vec<u8> = Vec::new();
1096 let (_, auto) = crate::encode::encode(&bytes, Limits::DEFAULT).unwrap();
1097 eprintln!(
1098 "sizes[{name}] source={} raw={} byte_rans={} layout={} layout_rans={} auto={}({})",
1099 bytes.len(),
1100 raw_bytes.len(),
1101 byte_rans_bytes.len(),
1102 layout_bytes.len(),
1103 layout_rans_bytes.len(),
1104 auto.kind.name(),
1105 auto.encoded_len,
1106 );
1107
1108 #[cfg(feature = "rans")]
1109 {
1110 let plan = build_layout_plan(&bytes, Limits::DEFAULT).unwrap().unwrap();
1111 let plan_bytes_len = crate::dra::op::encode_items(&plan.items).unwrap().len();
1112 let cand = propose_pdf_layout_rans(&bytes, Limits::DEFAULT)
1113 .unwrap()
1114 .unwrap();
1115 let model_bytes: usize = cand
1116 .descriptor
1117 .models
1118 .iter()
1119 .map(|m| m.encode().unwrap().len())
1120 .sum();
1121 let payload_bytes: usize = cand
1122 .descriptor
1123 .channels
1124 .iter()
1125 .map(|c| c.payload.len())
1126 .sum();
1127 eprintln!(
1128 "rans[{name}] data={} plan_bytes={} models={} payloads={} total={}",
1129 plan.data.len(),
1130 plan_bytes_len,
1131 model_bytes,
1132 payload_bytes,
1133 layout_rans_bytes.len(),
1134 );
1135 }
1136 }
1137 }
1138}