fn main() {
let path = match std::env::args().nth(1) {
Some(p) => p,
None => {
eprintln!("usage: text_layer <pdf>");
std::process::exit(2);
}
};
let bytes = std::fs::read(&path).expect("read the pdf");
let name = std::path::Path::new(&path)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| path.clone());
match lopdf::Document::load_mem(&bytes) {
Err(e) => {
eprintln!("1. lopdf load: FAILED — {e}");
eprintln!(" (the browser reports this as \"no embedded text layer\")");
eprintln!(
" xref repair: {}",
docling_pdf::textparse::xref_repair_status(&bytes)
);
probe_tail(&bytes);
}
Ok(doc) => {
eprintln!(
"1. lopdf load: ok — {} page(s), version {}, encrypted: {}",
doc.get_pages().len(),
doc.version,
doc.is_encrypted()
);
}
}
let pages = docling_pdf::textparse::pdf_textlines(&bytes);
let cells: usize = pages.iter().map(|(_, _, c)| c.len()).sum();
eprintln!("2. text lines: {cells} across {} page(s)", pages.len());
if cells == 0 {
eprintln!(
" where it is lost:{}",
docling_pdf::textparse::content_diagnosis(&bytes)
);
}
for (i, (_, _, c)) in pages.iter().enumerate().take(3) {
if let Some(first) = c.iter().find(|c| !c.text.trim().is_empty()) {
eprintln!(" page {}: first line {:?}", i + 1, first.text);
}
}
match docling_pdf::convert_text_layer(&bytes, &name) {
Ok(doc) => {
let md = doc.export_to_markdown();
eprintln!(
"3. convert_text_layer: {} node(s), {} chars — {}",
doc.nodes.len(),
md.len(),
if doc.nodes.is_empty() {
"EMPTY: a browser build would fall back to OCR here"
} else {
"the browser would use this directly (no OCR)"
}
);
println!("{md}");
}
Err(e) => {
eprintln!("3. convert_text_layer: FAILED — {e}");
std::process::exit(1);
}
}
}
fn probe_tail(bytes: &[u8]) {
let find_last = |needle: &[u8]| {
bytes
.windows(needle.len())
.rposition(|w| w == needle)
.map(|i| (i, needle.len()))
};
let printable = |b: &[u8]| -> String {
b.iter()
.map(|&c| {
if (0x20..0x7f).contains(&c) {
c as char
} else if c == b'\n' || c == b'\r' {
'\u{23ce}'
} else {
'.'
}
})
.collect()
};
eprintln!(" file: {} bytes", bytes.len());
eprintln!(
" markers: trailer={} startxref={} xref={} /XRef={} %%EOF={}",
find_last(b"trailer").is_some(),
find_last(b"startxref").is_some(),
find_last(b"xref").is_some(),
find_last(b"/XRef").is_some(),
find_last(b"%%EOF").is_some(),
);
if let Some((i, len)) = find_last(b"%%EOF") {
let after = bytes.len() - (i + len);
eprintln!(" bytes after the last %%EOF: {after}");
}
let Some((sx, _)) = find_last(b"startxref") else {
eprintln!(" no startxref at all — the xref would have to be rebuilt by scanning objects");
return;
};
let tail = &bytes[sx..bytes.len().min(sx + 60)];
eprintln!(" last startxref block: {:?}", printable(tail));
let digits: String = tail
.iter()
.skip(b"startxref".len())
.skip_while(|c| c.is_ascii_whitespace())
.take_while(|c| c.is_ascii_digit())
.map(|&c| c as char)
.collect();
match digits.parse::<usize>() {
Ok(off) if off < bytes.len() => {
let end = bytes.len().min(off + 120);
eprintln!(" at offset {off} -> {:?}", printable(&bytes[off..end]));
}
Ok(off) => eprintln!(" startxref points to {off}, past the end of the file"),
Err(_) => eprintln!(" startxref carries no parseable offset"),
}
}