pub(crate) fn accept_doc(doc: lopdf::Document) -> Option<lopdf::Document> {
if doc.objects.is_empty() {
None
} else {
Some(doc)
}
}
pub(crate) fn try_load(data: &[u8]) -> Option<lopdf::Document> {
lopdf::Document::load_mem(data).ok().and_then(accept_doc)
}
pub(crate) fn raw_has_hash_names(data: &[u8]) -> bool {
let mut i = 0;
while i + 3 < data.len() {
if data[i] == b'/' {
i += 1;
while i < data.len() && data[i].is_ascii_alphanumeric() {
i += 1;
}
if i < data.len() && data[i] == b'#' {
return true;
}
} else {
i += 1;
}
}
false
}
pub(crate) fn sanitize_hash_names_raw(data: &[u8]) -> Vec<u8> {
let mut result = data.to_vec();
let mut i = 0;
let mut in_string = 0i32;
let mut in_hex_string = false;
while i < result.len() {
if result[i] == b'(' && !in_hex_string {
in_string += 1;
i += 1;
continue;
}
if result[i] == b')' && in_string > 0 {
in_string -= 1;
i += 1;
continue;
}
if result[i] == b'\\' && in_string > 0 {
i += 2;
continue;
}
if in_string > 0 {
i += 1;
continue;
}
if result[i] == b'<' && i + 1 < result.len() && result[i + 1] == b'<' {
i += 2;
continue;
}
if result[i] == b'>' && i + 1 < result.len() && result[i + 1] == b'>' {
i += 2;
continue;
}
if result[i] == b'<' {
in_hex_string = true;
i += 1;
continue;
}
if result[i] == b'>' && in_hex_string {
in_hex_string = false;
i += 1;
continue;
}
if in_hex_string {
i += 1;
continue;
}
if result[i] == b'%' {
while i < result.len() && result[i] != b'\n' && result[i] != b'\r' {
i += 1;
}
continue;
}
if result[i] == b'/' {
i += 1;
while i < result.len() && !is_name_delimiter(result[i]) {
if result[i] == b'#' {
result[i] = b'_';
}
i += 1;
}
continue;
}
i += 1;
}
result
}
pub(crate) fn is_name_delimiter(b: u8) -> bool {
matches!(
b,
b' ' | b'\t'
| b'\n'
| b'\r'
| b'\0'
| b'/'
| b'['
| b']'
| b'('
| b')'
| b'<'
| b'>'
| b'{'
| b'}'
| b'%'
)
}
pub(crate) fn try_repair_for_lopdf(data: &[u8]) -> Option<lopdf::Document> {
let offset = data.windows(5).position(|w| w == b"%PDF-")?;
if offset > 0 {
let trimmed = &data[offset..];
if let Some(doc) = try_load(trimmed) {
return Some(doc);
}
return try_repair_xref(trimmed);
}
try_repair_xref(data)
}
pub(crate) fn try_repair_xref(data: &[u8]) -> Option<lopdf::Document> {
let search_start = data.len().saturating_sub(4096);
let tail = &data[search_start..];
let startxref_pos = tail
.windows(9)
.rposition(|w| w == b"startxref")
.map(|p| search_start + p);
if let Some(sxr) = startxref_pos {
let after = &data[sxr + 9..];
let offset_str: String = after
.iter()
.skip_while(|b| b.is_ascii_whitespace())
.take_while(|b| b.is_ascii_digit())
.map(|&b| b as char)
.collect();
if let Ok(xref_offset) = offset_str.parse::<usize>() {
if xref_offset < data.len() {
let at_offset = &data[xref_offset..];
let has_valid_xref = if at_offset.starts_with(b"xref") {
let after_keyword = &at_offset[4..];
let trimmed = after_keyword
.iter()
.position(|b| !b.is_ascii_whitespace())
.map(|p| &after_keyword[p..])
.unwrap_or(b"");
!trimmed.starts_with(b"trailer")
} else if at_offset.len() > 10 && at_offset[0].is_ascii_digit() {
at_offset
.windows(5)
.take(20)
.any(|w| w == b"0 obj" || w == b"0 OBJ")
} else {
false
};
if !has_valid_xref {
if let Some(real_xref) = find_last_xref_pos(data) {
let mut repaired = data.to_vec();
repaired.truncate(sxr);
repaired.extend_from_slice(
format!("startxref\n{real_xref}\n%%EOF\n").as_bytes(),
);
if let Some(doc) = try_load(&repaired) {
return Some(doc);
}
if let Some(double_repaired) = try_fix_xref_line_endings(&repaired) {
if let Some(doc) = try_load(&double_repaired) {
return Some(doc);
}
}
}
}
}
}
}
let last_512 = &data[data.len().saturating_sub(512)..];
let has_eof = last_512.windows(5).any(|w| w == b"%%EOF");
if !has_eof {
if let Some(real_xref) = find_last_xref_pos(data) {
let mut repaired = data.to_vec();
repaired.extend_from_slice(format!("\nstartxref\n{real_xref}\n%%EOF\n").as_bytes());
if let Some(doc) = try_load(&repaired) {
return Some(doc);
}
}
}
if let Some(repaired) = try_rebuild_trailer(data) {
if let Some(doc) = try_load(&repaired) {
return Some(doc);
}
}
if let Some(repaired) = try_fix_trailer_size(data) {
if let Some(doc) = try_load(&repaired) {
return Some(doc);
}
}
if let Some(repaired) = try_fix_startxref_comment(data) {
if let Some(doc) = try_load(&repaired) {
return Some(doc);
}
}
if let Some(repaired) = try_fix_xref_line_endings(data) {
if let Some(doc) = try_load(&repaired) {
return Some(doc);
}
}
if let Some(doc) = try_rebuild_xref_from_objects(data) {
return Some(doc);
}
None
}
pub(crate) fn try_rebuild_xref_from_objects(data: &[u8]) -> Option<lopdf::Document> {
let mut objects: Vec<(u32, usize)> = Vec::new(); let mut root_ref: Option<u32> = None;
let mut info_ref: Option<u32> = None;
let mut pos = 0;
while pos + 10 < data.len() {
if data[pos].is_ascii_digit() {
let at_line_start = pos == 0 || data[pos - 1] == b'\n' || data[pos - 1] == b'\r';
if at_line_start {
let end = std::cmp::min(pos + 20, data.len());
let chunk = &data[pos..end];
if let Some(obj_info) = parse_obj_marker(chunk) {
objects.push((obj_info.0, pos));
let obj_end = std::cmp::min(pos + 65536, data.len());
let obj_data = &data[pos..obj_end];
let has_catalog = obj_data
.windows(b"/Type /Catalog".len())
.any(|w| w == b"/Type /Catalog")
|| obj_data
.windows(b"/Type/Catalog".len())
.any(|w| w == b"/Type/Catalog");
if has_catalog {
root_ref = Some(obj_info.0);
}
if obj_data
.windows(8)
.any(|w| w == b"/Author " || w == b"/Creator")
&& obj_data
.windows(14)
.any(|w| w == b"/CreationDate " || w == b"/ModDate ")
&& info_ref.is_none()
{
info_ref = Some(obj_info.0);
}
pos += 5;
continue;
}
}
}
pos += 1;
}
if root_ref.is_none() {
root_ref = extract_root_from_trailer(data);
}
if objects.is_empty() || root_ref.is_none() {
return None;
}
objects.sort_by_key(|&(num, _)| num);
objects.dedup_by_key(|o| o.0);
let max_obj = objects.last().map(|o| o.0).unwrap_or(0);
let size = max_obj + 1;
let mut xref_entries = Vec::new();
xref_entries.push(format!("{:010} {:05} f\r\n", 0, 65535));
let obj_map: std::collections::HashMap<u32, usize> = objects.iter().cloned().collect();
for num in 1..size {
if let Some(&offset) = obj_map.get(&num) {
xref_entries.push(format!("{:010} {:05} n\r\n", offset, 0));
} else {
xref_entries.push(format!("{:010} {:05} f\r\n", 0, 0));
}
}
let header_start = data.windows(5).position(|w| w == b"%PDF-").unwrap_or(0);
let body = &data[header_start..];
let xref_cut = find_last_xref_pos(body);
let last_endobj = body.windows(6).rposition(|w| w == b"endobj").map(|p| p + 6);
let body_end = match (xref_cut, last_endobj) {
(Some(xp), Some(le)) if le > xp => body.len(),
(Some(xp), _) => xp,
_ => body.len(),
};
let mut repaired = body[..body_end].to_vec();
if !repaired.ends_with(b"\n") {
repaired.push(b'\n');
}
let xref_pos = repaired.len();
repaired.extend_from_slice(format!("xref\n0 {size}\n").as_bytes());
for entry in &xref_entries {
repaired.extend_from_slice(entry.as_bytes());
}
let root = root_ref.unwrap();
repaired.extend_from_slice(b"trailer\n<<\n");
repaired.extend_from_slice(format!("/Size {size}\n/Root {root} 0 R\n").as_bytes());
if let Some(info) = info_ref {
repaired.extend_from_slice(format!("/Info {info} 0 R\n").as_bytes());
}
repaired.extend_from_slice(b">>\n");
repaired.extend_from_slice(format!("startxref\n{xref_pos}\n%%EOF\n").as_bytes());
try_load(&repaired)
}
pub(crate) fn extract_root_from_trailer(data: &[u8]) -> Option<u32> {
let mut search_end = data.len();
while search_end >= 7 {
let Some(pos) = data[..search_end].windows(7).rposition(|w| w == b"trailer") else {
break;
};
let after = &data[pos + 7..];
if let Some(root_offset) = after.windows(5).position(|w| w == b"/Root") {
let after_root = &after[root_offset + 5..];
let digits_start = after_root
.iter()
.position(|b| b.is_ascii_digit())
.unwrap_or(after_root.len());
let digits: Vec<u8> = after_root[digits_start..]
.iter()
.take_while(|b| b.is_ascii_digit())
.copied()
.collect();
if let Ok(s) = std::str::from_utf8(&digits) {
if let Ok(n) = s.parse::<u32>() {
if n > 0 {
return Some(n);
}
}
}
}
search_end = pos;
}
None
}
pub(crate) fn parse_obj_marker(data: &[u8]) -> Option<(u32, u16)> {
let s = std::str::from_utf8(data).ok()?;
let mut parts = s.split_whitespace();
let num: u32 = parts.next()?.parse().ok()?;
let gen: u16 = parts.next()?.parse().ok()?;
let keyword = parts.next()?;
if keyword == "obj" {
Some((num, gen))
} else {
None
}
}
pub(crate) fn try_rebuild_trailer(data: &[u8]) -> Option<Vec<u8>> {
let xref_pos = find_last_xref_pos(data)?;
let after_xref = &data[xref_pos..];
let header_end = after_xref
.windows(1)
.skip(5) .position(|w| w[0] == b'\n')
.map(|p| p + 5 + 1)?;
let header_line = std::str::from_utf8(&after_xref[5..header_end]).ok()?;
let parts: Vec<&str> = header_line.split_whitespace().collect();
if parts.len() != 2 {
return None;
}
let _start: usize = parts[0].parse().ok()?;
let count: usize = parts[1].parse().ok()?;
let trailer_pos = data[xref_pos..].windows(7).position(|w| w == b"trailer")?;
let abs_trailer = xref_pos + trailer_pos;
let after_trailer = &data[abs_trailer + 7..];
let dict_start = after_trailer
.iter()
.position(|&b| !b.is_ascii_whitespace())?;
let dict_data = &after_trailer[dict_start..];
let is_corrupt = dict_data.starts_with(b"<<<") || !dict_data.starts_with(b"<<");
if !is_corrupt {
return None;
}
let entries_start = xref_pos + header_end;
let mut root_ref = None;
for i in 1..count {
let entry_offset = entries_start + i * 20;
if entry_offset + 20 > data.len() {
break;
}
let entry = &data[entry_offset..entry_offset + 20];
if entry[17] == b'f' {
continue; }
let offset_str = std::str::from_utf8(&entry[..10]).ok()?;
let obj_offset: usize = offset_str.trim().parse().ok()?;
if obj_offset + 20 >= data.len() {
continue;
}
let obj_data = &data[obj_offset..std::cmp::min(obj_offset + 4096, data.len())];
if obj_data
.windows(14)
.any(|w| w == b"/Type /Catalog" || w == b"/Type/Catalog")
{
root_ref = Some(i);
break;
}
}
let root_ref = root_ref?;
let mut repaired = data[..abs_trailer].to_vec();
repaired.extend_from_slice(
format!(
"trailer\n<< /Size {count} /Root {root_ref} 0 R >>\nstartxref\n{xref_pos}\n%%EOF\n"
)
.as_bytes(),
);
Some(repaired)
}
pub(crate) fn try_fix_trailer_size(data: &[u8]) -> Option<Vec<u8>> {
let trailer_pos = data.windows(7).rposition(|w| w == b"trailer")?;
let after_trailer = &data[trailer_pos..];
let size_pos = after_trailer.windows(5).position(|w| w == b"/Size")?;
let abs_size = trailer_pos + size_pos;
let after_size = &data[abs_size + 5..];
let val_start = after_size.iter().position(|b| !b.is_ascii_whitespace())?;
if after_size[val_start].is_ascii_digit() {
return None;
}
let xref_pos = find_last_xref_pos(data)?;
let after_xref = &data[xref_pos..];
let header_end = after_xref
.windows(1)
.skip(5)
.position(|w| w[0] == b'\n')
.map(|p| p + 5 + 1)?;
let header_line = std::str::from_utf8(&after_xref[5..header_end]).ok()?;
let parts: Vec<&str> = header_line.split_whitespace().collect();
if parts.len() != 2 {
return None;
}
let count: usize = parts[1].parse().ok()?;
let val_end = after_size[val_start..]
.iter()
.position(|&b| b == b'/' || b == b'>')
.unwrap_or(after_size.len() - val_start);
let mut repaired = data[..abs_size + 5].to_vec();
repaired.push(b' ');
repaired.extend_from_slice(count.to_string().as_bytes());
repaired.push(b' ');
repaired.extend_from_slice(&data[abs_size + 5 + val_start + val_end..]);
Some(repaired)
}
pub(crate) fn try_fix_startxref_comment(data: &[u8]) -> Option<Vec<u8>> {
let search_start = data.len().saturating_sub(4096);
let sxr_pos = data[search_start..]
.windows(9)
.rposition(|w| w == b"startxref")
.map(|p| search_start + p)?;
let after = &data[sxr_pos + 9..];
let digits_start = after.iter().position(|b| b.is_ascii_digit())?;
let digits_end = after[digits_start..]
.iter()
.position(|b| !b.is_ascii_digit())
.unwrap_or(after.len() - digits_start);
let remainder = &after[digits_start + digits_end..];
let has_garbage = remainder
.iter()
.take_while(|&&b| b != b'%' || remainder.windows(5).any(|w| w != b"%%EOF"))
.any(|b| !b.is_ascii_whitespace() && *b != b'%');
let after_digits = &after[digits_start + digits_end..];
let trimmed = after_digits
.iter()
.skip_while(|b| b.is_ascii_whitespace())
.cloned()
.collect::<Vec<u8>>();
let needs_fix = !trimmed.is_empty()
&& !trimmed.starts_with(b"%%EOF")
&& (trimmed[0] == b'%' || has_garbage);
if !needs_fix {
return None;
}
let offset_str = std::str::from_utf8(&after[digits_start..digits_start + digits_end]).ok()?;
let mut repaired = data[..sxr_pos].to_vec();
repaired.extend_from_slice(format!("startxref\n{offset_str}\n%%EOF\n").as_bytes());
Some(repaired)
}
pub(crate) fn try_fix_xref_line_endings(data: &[u8]) -> Option<Vec<u8>> {
let xref_pos = find_last_xref_pos(data)?;
let after = &data[xref_pos + 4..]; let header_start = after.iter().position(|b| !b.is_ascii_whitespace())?;
let header_end = after[header_start..]
.iter()
.position(|&b| b == b'\n')
.map(|p| header_start + p + 1)?;
let header_line = std::str::from_utf8(&after[header_start..header_end]).ok()?;
let parts: Vec<&str> = header_line.split_whitespace().collect();
if parts.len() != 2 {
return None;
}
let count: usize = parts[1].parse().ok()?;
let trailer_pos = data[xref_pos..]
.windows(7)
.position(|w| w == b"trailer")
.map(|p| xref_pos + p)?;
let entries_start = xref_pos + 4 + header_end;
let entries_data = &data[entries_start..trailer_pos];
let entries: Vec<&[u8]> = entries_data
.split(|&b| b == b'\n')
.filter(|e| !e.is_empty())
.collect();
if entries.len() != count {
return None;
}
let has_cr = entries.iter().any(|e| e.ends_with(b"\r"));
if has_cr {
let all_correct = entries.iter().all(|e| e.ends_with(b"\r") && e.len() == 19);
if all_correct {
return None;
}
}
let mut repaired = data[..entries_start].to_vec();
for entry in &entries {
let stripped = if entry.ends_with(b"\r") {
&entry[..entry.len() - 1]
} else {
entry
};
if stripped.len() == 18 {
repaired.extend_from_slice(stripped);
} else {
let s = std::str::from_utf8(stripped).ok()?;
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.len() != 3 {
return None;
}
repaired.extend_from_slice(
format!("{:0>10} {:0>5} {}", parts[0], parts[1], parts[2]).as_bytes(),
);
}
repaired.extend_from_slice(b"\r\n");
}
repaired.extend_from_slice(&data[trailer_pos..]);
Some(repaired)
}
pub(crate) fn find_last_xref_pos(data: &[u8]) -> Option<usize> {
let mut search_end = data.len();
while search_end >= 4 {
if let Some(pos) = data[..search_end].windows(4).rposition(|w| w == b"xref") {
if pos >= 5 && &data[pos - 5..pos] == b"start" {
search_end = pos;
continue;
}
if pos == 0 || data[pos - 1] == b'\n' || data[pos - 1] == b'\r' {
let after = &data[pos + 4..];
let non_ws = after
.iter()
.position(|b| !b.is_ascii_whitespace())
.map(|p| &after[p..])
.unwrap_or(b"");
if non_ws.starts_with(b"trailer") {
search_end = pos;
continue;
}
return Some(pos);
}
search_end = pos;
} else {
break;
}
}
None
}
pub(crate) fn fix_wrong_root(doc: &mut lopdf::Document) {
let catalog_id = find_catalog_object(doc);
let root_id = match doc.trailer.get(b"Root").ok() {
Some(lopdf::Object::Reference(id)) => *id,
_ => {
if let Some(id) = catalog_id {
doc.trailer.set("Root", lopdf::Object::Reference(id));
}
return;
}
};
let is_catalog = match doc.objects.get(&root_id) {
Some(lopdf::Object::Dictionary(d)) => {
matches!(d.get(b"Type").ok(), Some(lopdf::Object::Name(n)) if n == b"Catalog")
}
_ => false,
};
if is_catalog {
return; }
if let Some(id) = catalog_id {
doc.trailer.set("Root", lopdf::Object::Reference(id));
}
}
pub(crate) fn find_catalog_object(doc: &lopdf::Document) -> Option<lopdf::ObjectId> {
let mut catalog_without_pages: Option<lopdf::ObjectId> = None;
let mut pages_holder: Option<lopdf::ObjectId> = None;
for (id, obj) in &doc.objects {
if let lopdf::Object::Dictionary(d) = obj {
let is_catalog =
matches!(d.get(b"Type").ok(), Some(lopdf::Object::Name(n)) if n == b"Catalog");
let has_pages = d.has(b"Pages");
if is_catalog && has_pages {
return Some(*id);
}
if is_catalog && catalog_without_pages.is_none() {
catalog_without_pages = Some(*id);
}
if has_pages && pages_holder.is_none() {
pages_holder = Some(*id);
}
}
}
catalog_without_pages.or(pages_holder)
}
pub(crate) fn try_fix_missing_page_types(doc: &mut lopdf::Document) -> bool {
let page_ids = collect_page_tree_leaves(doc);
if page_ids.is_empty() {
return false;
}
let mut fixed = false;
for page_id in &page_ids {
if let Ok(dict) = doc.get_dictionary_mut(*page_id) {
let needs_fix = !matches!(dict.get_type(), Ok(t) if t == b"Page");
if needs_fix {
let has_media_box = dict.get(b"MediaBox").is_ok();
let has_contents = dict.get(b"Contents").is_ok();
let has_parent = dict.get(b"Parent").is_ok();
if has_media_box || has_contents || has_parent {
dict.set("Type", lopdf::Object::Name(b"Page".to_vec()));
fixed = true;
}
}
}
}
if fixed {
!doc.get_pages().is_empty()
} else {
false
}
}
pub(crate) fn normalize_page_tree_types(doc: &mut lopdf::Document) -> usize {
let pages_id = match doc
.catalog()
.and_then(|cat| cat.get(b"Pages"))
.and_then(lopdf::Object::as_reference)
{
Ok(id) => id,
Err(_) => return 0,
};
let mut visited = std::collections::HashSet::new();
let mut stack = vec![pages_id];
let mut set_pages: Vec<lopdf::ObjectId> = Vec::new();
let mut set_page: Vec<lopdf::ObjectId> = Vec::new();
let limit = doc.objects.len().min(10_000);
while let Some(id) = stack.pop() {
if !visited.insert(id) || visited.len() > limit {
continue;
}
let dict = match doc.get_dictionary(id) {
Ok(d) => d,
Err(_) => continue,
};
let kids: Vec<lopdf::ObjectId> = match dict.get(b"Kids").and_then(lopdf::Object::as_array) {
Ok(arr) => arr.iter().filter_map(|o| o.as_reference().ok()).collect(),
Err(_) => Vec::new(),
};
if !kids.is_empty() {
let is_pages = matches!(dict.get_type(), Ok(t) if t == b"Pages");
if !is_pages {
set_pages.push(id);
}
for kid in kids {
stack.push(kid);
}
continue;
}
let looks_page = dict.get(b"MediaBox").is_ok()
|| dict.get(b"Contents").is_ok()
|| dict.get(b"Parent").is_ok();
let is_page = matches!(dict.get_type(), Ok(t) if t == b"Page");
if looks_page && !is_page {
set_page.push(id);
}
}
let mut fixed = 0usize;
for id in set_pages {
if let Ok(dict) = doc.get_dictionary_mut(id) {
dict.set("Type", lopdf::Object::Name(b"Pages".to_vec()));
fixed += 1;
}
}
for id in set_page {
if let Ok(dict) = doc.get_dictionary_mut(id) {
dict.set("Type", lopdf::Object::Name(b"Page".to_vec()));
fixed += 1;
}
}
fixed
}
pub(crate) fn strip_null_page_kids(doc: &mut lopdf::Document) {
let pages_ids: Vec<lopdf::ObjectId> = doc
.objects
.iter()
.filter_map(|(id, obj)| {
let lopdf::Object::Dictionary(dict) = obj else {
return None;
};
let Ok(lopdf::Object::Array(kids)) = dict.get(b"Kids") else {
return None;
};
let has_null = kids
.iter()
.any(|k| matches!(k, lopdf::Object::Reference(id) if id.0 == 0));
if has_null {
Some(*id)
} else {
None
}
})
.collect();
for id in pages_ids {
let filtered: Vec<lopdf::Object> = {
let dict = match doc.objects.get(&id) {
Some(lopdf::Object::Dictionary(d)) => d,
_ => continue,
};
let Ok(lopdf::Object::Array(kids)) = dict.get(b"Kids") else {
continue;
};
kids.iter()
.filter(|k| !matches!(k, lopdf::Object::Reference(r) if r.0 == 0))
.cloned()
.collect()
};
if let Some(lopdf::Object::Dictionary(dict)) = doc.objects.get_mut(&id) {
let count = filtered.len() as i64;
dict.set("Kids", lopdf::Object::Array(filtered));
dict.set("Count", lopdf::Object::Integer(count));
}
}
}
pub(crate) fn collect_page_tree_leaves(doc: &lopdf::Document) -> Vec<lopdf::ObjectId> {
let mut leaves = Vec::new();
let pages_id = match doc
.catalog()
.and_then(|cat| cat.get(b"Pages"))
.and_then(lopdf::Object::as_reference)
{
Ok(id) => id,
Err(_) => return leaves,
};
let mut stack = vec![pages_id];
let mut visited = std::collections::HashSet::new();
let limit = doc.objects.len().min(10_000);
while let Some(id) = stack.pop() {
if !visited.insert(id) || visited.len() > limit {
continue;
}
let dict = match doc.get_dictionary(id) {
Ok(d) => d,
Err(_) => continue,
};
if let Ok(kids) = dict.get(b"Kids").and_then(lopdf::Object::as_array) {
for kid in kids {
if let Ok(kid_id) = kid.as_reference() {
stack.push(kid_id);
}
}
} else {
leaves.push(id);
}
}
leaves
}