@@ -634,6 +634,22 @@ fn page_size(doc: &Document, page_id: lopdf::ObjectId) -> (f32, f32) {
634634 ( 612.0 , 792.0 )
635635}
636636
637+ /// Why the cross-reference repair did or did not fire, for the `text_layer`
638+ /// diagnostic. A PDF that will not load is indistinguishable from a scan in
639+ /// production (both convert to nothing), so the reason has to be askable.
640+ pub fn xref_repair_status ( bytes : & [ u8 ] ) -> String {
641+ if Document :: load_mem ( bytes) . is_ok ( ) {
642+ return "loads unaided; no repair needed" . into ( ) ;
643+ }
644+ match pad_short_xref_entries ( bytes) {
645+ Ok ( fixed) => match Document :: load_mem ( & fixed) {
646+ Ok ( _) => "repaired: cross-reference entries padded to 20 bytes" . into ( ) ,
647+ Err ( e) => format ! ( "padded the entries, but it still will not load: {e}" ) ,
648+ } ,
649+ Err ( why) => format ! ( "repair declined — {why}" ) ,
650+ }
651+ }
652+
637653/// Load a PDF, repairing the one malformation that otherwise costs us the whole
638654/// document: **19-byte cross-reference entries**.
639655///
@@ -651,24 +667,30 @@ fn page_size(doc: &Document, page_id: lopdf::ObjectId) -> (f32, f32) {
651667fn load_document ( bytes : & [ u8 ] ) -> Option < Document > {
652668 match Document :: load_mem ( bytes) {
653669 Ok ( doc) => Some ( doc) ,
654- Err ( _) => Document :: load_mem ( & pad_short_xref_entries ( bytes) ?) . ok ( ) ,
670+ Err ( _) => Document :: load_mem ( & pad_short_xref_entries ( bytes) . ok ( ) ?) . ok ( ) ,
655671 }
656672}
657673
658674/// Rewrite a classic cross-reference table's entries to the spec's 20 bytes,
659675/// or `None` when the file's shape makes that unsafe (see [`load_document`]).
660- fn pad_short_xref_entries ( bytes : & [ u8 ] ) -> Option < Vec < u8 > > {
676+ fn pad_short_xref_entries ( bytes : & [ u8 ] ) -> Result < Vec < u8 > , & ' static str > {
661677 // Exactly one xref section, and it must start after every object, so that
662678 // growing it shifts nothing the table's offsets refer to.
663679 let is_boundary = |i : usize | i == 0 || matches ! ( bytes[ i - 1 ] , b'\n' | b'\r' ) ;
664680 let mut starts = ( 0 ..bytes. len ( ) . saturating_sub ( 4 ) )
665681 . filter ( |& i| & bytes[ i..i + 4 ] == b"xref" && is_boundary ( i) ) ;
666- let xref_at = starts. next ( ) ?;
682+ let xref_at = starts
683+ . next ( )
684+ . ok_or ( "no classic `xref` section (an xref stream?)" ) ?;
667685 if starts. next ( ) . is_some ( ) {
668- return None ;
686+ return Err ( "more than one xref section (incremental update)" ) ;
669687 }
670- if bytes. windows ( 3 ) . rposition ( |w| w == b"obj" ) ? > xref_at {
671- return None ;
688+ let last_obj = bytes
689+ . windows ( 3 )
690+ . rposition ( |w| w == b"obj" )
691+ . ok_or ( "no objects found" ) ?;
692+ if last_obj > xref_at {
693+ return Err ( "an object follows the xref — padding would move it" ) ;
672694 }
673695
674696 let mut out = bytes[ ..xref_at] . to_vec ( ) ;
@@ -684,30 +706,37 @@ fn pad_short_xref_entries(bytes: &[u8]) -> Option<Vec<u8>> {
684706 // Either the next subsection header ("first count") or the trailer.
685707 if bytes[ i..] . starts_with ( b"trailer" ) {
686708 out. extend_from_slice ( & bytes[ i..] ) ;
687- return Some ( out) ;
709+ return Ok ( out) ;
688710 }
689- let header_end = i + bytes[ i..] . iter ( ) . position ( |c| matches ! ( c, b'\n' | b'\r' ) ) ?;
690- let header = std:: str:: from_utf8 ( & bytes[ i..header_end] ) . ok ( ) ?. trim ( ) ;
711+ let header_end = i + bytes[ i..]
712+ . iter ( )
713+ . position ( |c| matches ! ( c, b'\n' | b'\r' ) )
714+ . ok_or ( "subsection header runs off the end" ) ?;
715+ let header = std:: str:: from_utf8 ( & bytes[ i..header_end] )
716+ . map_err ( |_| "subsection header is not text" ) ?
717+ . trim ( ) ;
691718 let mut parts = header. split_whitespace ( ) ;
692- let _first: u64 = parts. next ( ) ?. parse ( ) . ok ( ) ?;
693- let count: usize = parts. next ( ) ?. parse ( ) . ok ( ) ?;
719+ let count: usize = parts
720+ . nth ( 1 )
721+ . and_then ( |c| c. parse ( ) . ok ( ) )
722+ . ok_or ( "unparseable subsection header" ) ?;
694723 if parts. next ( ) . is_some ( ) || count == 0 {
695- return None ;
724+ return Err ( "unexpected subsection header shape" ) ;
696725 }
697726 out. extend_from_slice ( header. as_bytes ( ) ) ;
698727 out. push ( b'\n' ) ;
699728 i = header_end;
700729 for _ in 0 ..count {
701730 skip_ws ( & mut i) ;
702731 // `nnnnnnnnnn ggggg n` — the 18 bytes before whatever EOL follows.
703- let entry = bytes. get ( i..i + 18 ) ?;
732+ let entry = bytes. get ( i..i + 18 ) . ok_or ( "xref entry runs off the end" ) ?;
704733 let well_formed = entry[ ..10 ] . iter ( ) . all ( u8:: is_ascii_digit)
705734 && entry[ 10 ] == b' '
706735 && entry[ 11 ..16 ] . iter ( ) . all ( u8:: is_ascii_digit)
707736 && entry[ 16 ] == b' '
708737 && matches ! ( entry[ 17 ] , b'n' | b'f' ) ;
709738 if !well_formed {
710- return None ;
739+ return Err ( "xref entry is not `nnnnnnnnnn ggggg n`" ) ;
711740 }
712741 out. extend_from_slice ( entry) ;
713742 out. extend_from_slice ( b" \n " ) ; // the spec's 2-byte EOL -> 20 bytes
@@ -1661,6 +1690,10 @@ mod xref_repair {
16611690 fn repair_declines_when_padding_would_move_objects ( ) {
16621691 let mut incremental = pdf_with_xref ( false ) ;
16631692 incremental. extend_from_slice ( b"6 0 obj<</Type/Whatever>>endobj\n " ) ;
1664- assert ! ( super :: pad_short_xref_entries( & incremental) . is_none( ) ) ;
1693+ let declined = super :: pad_short_xref_entries ( & incremental) . unwrap_err ( ) ;
1694+ assert ! (
1695+ declined. contains( "object follows the xref" ) ,
1696+ "reason: {declined}"
1697+ ) ;
16651698 }
16661699}
0 commit comments