Skip to content

Commit a558df6

Browse files
artizclaude
andcommitted
docling-pdf: probe the xref/trailer when a PDF will not load
A real invoice fails at the very first stage — lopdf reports "invalid file trailer" while pdfium reads the same file fine — so the browser spends ~10 s on OCR for a document whose text layer is right there. The failure says nothing about which part of the cross-reference machinery is wrong, and the file is personal, so the diagnosis has to come from the reporter's own machine. On a load failure the example now dumps what the parser tripped over: which markers exist (trailer / startxref / xref / /XRef / %%EOF), how many bytes trail the final %%EOF, the last startxref block, and the bytes at the offset it points to. A classic xref table, an xref stream, an offset past the end of the file and plain garbage each read differently there and call for different repairs. Exercised on two deliberately broken copies of a corpus PDF: appended junk after %%EOF still loads (lopdf tolerates it), while a corrupted startxref offset reports "startxref points to 999999999, past the end of the file". Refs #157 Signed-off-by: artiz <artem.kustikov@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EY5KAiquN4YpVf2PXEQkVT
1 parent 425615b commit a558df6

1 file changed

Lines changed: 68 additions & 0 deletions

File tree

crates/docling-pdf/examples/text_layer.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ fn main() {
3535
Err(e) => {
3636
eprintln!("1. lopdf load: FAILED — {e}");
3737
eprintln!(" (the browser reports this as \"no embedded text layer\")");
38+
probe_tail(&bytes);
3839
}
3940
Ok(doc) => {
4041
eprintln!(
@@ -80,3 +81,70 @@ fn main() {
8081
}
8182
}
8283
}
84+
85+
/// When the document will not load, show the cross-reference machinery the
86+
/// parser choked on: the last `startxref`, where it points, and what actually
87+
/// sits there. A classic `xref` table, an xref *stream* (`/Type /XRef`), bytes
88+
/// past the end of the file or plain garbage each point at a different repair,
89+
/// and this is enough to tell them apart without handing the PDF around.
90+
fn probe_tail(bytes: &[u8]) {
91+
let find_last = |needle: &[u8]| {
92+
bytes
93+
.windows(needle.len())
94+
.rposition(|w| w == needle)
95+
.map(|i| (i, needle.len()))
96+
};
97+
let printable = |b: &[u8]| -> String {
98+
b.iter()
99+
.map(|&c| {
100+
if (0x20..0x7f).contains(&c) {
101+
c as char
102+
} else if c == b'\n' || c == b'\r' {
103+
'\u{23ce}'
104+
} else {
105+
'.'
106+
}
107+
})
108+
.collect()
109+
};
110+
111+
eprintln!(" file: {} bytes", bytes.len());
112+
eprintln!(
113+
" markers: trailer={} startxref={} xref={} /XRef={} %%EOF={}",
114+
find_last(b"trailer").is_some(),
115+
find_last(b"startxref").is_some(),
116+
find_last(b"xref").is_some(),
117+
find_last(b"/XRef").is_some(),
118+
find_last(b"%%EOF").is_some(),
119+
);
120+
121+
// Trailing bytes after the final %%EOF confuse a strict tail scan.
122+
if let Some((i, len)) = find_last(b"%%EOF") {
123+
let after = bytes.len() - (i + len);
124+
eprintln!(" bytes after the last %%EOF: {after}");
125+
}
126+
127+
let Some((sx, _)) = find_last(b"startxref") else {
128+
eprintln!(" no startxref at all — the xref would have to be rebuilt by scanning objects");
129+
return;
130+
};
131+
let tail = &bytes[sx..bytes.len().min(sx + 60)];
132+
eprintln!(" last startxref block: {:?}", printable(tail));
133+
134+
// The number after `startxref` is the byte offset of the xref section.
135+
let digits: String = tail
136+
.iter()
137+
.skip(b"startxref".len())
138+
.skip_while(|c| c.is_ascii_whitespace())
139+
.take_while(|c| c.is_ascii_digit())
140+
.map(|&c| c as char)
141+
.collect();
142+
match digits.parse::<usize>() {
143+
Ok(off) if off < bytes.len() => {
144+
let end = bytes.len().min(off + 120);
145+
eprintln!(" at offset {off} -> {:?}", printable(&bytes[off..end]));
146+
}
147+
Ok(off) => eprintln!(" startxref points to {off}, past the end of the file"),
148+
Err(_) => eprintln!(" startxref carries no parseable offset"),
149+
}
150+
}

0 commit comments

Comments
 (0)