use crate::error::Result;
use lopdf::{Document, Object, ObjectId};
#[derive(Debug, Clone, Default)]
pub struct PdfACleanupReport {
pub js_actions_removed: usize,
pub embedded_files_removed: usize,
pub transparency_groups_found: usize,
pub encryption_removed: bool,
pub aa_entries_removed: usize,
pub transfer_functions_removed: usize,
pub rendering_intents_fixed: usize,
pub trailer_id_added: bool,
pub annotation_flags_fixed: usize,
pub lzw_streams_reencoded: usize,
pub ocg_fixes: usize,
pub cidtogidmap_added: usize,
pub ap_fixes: usize,
pub file_attachment_annotations_removed: usize,
pub ca_fixes: usize,
pub smask_s_fixes: usize,
pub long_string_fixes: usize,
pub invalid_lang_fixes: usize,
pub date_mismatch_fixes: usize,
pub xmp_rebuilt: usize,
}
pub fn cleanup_for_pdfa(doc: &mut Document, is_pdfa1: bool) -> Result<PdfACleanupReport> {
let mut report = PdfACleanupReport {
js_actions_removed: 0,
embedded_files_removed: 0,
transparency_groups_found: 0,
encryption_removed: false,
aa_entries_removed: 0,
transfer_functions_removed: 0,
rendering_intents_fixed: 0,
trailer_id_added: false,
annotation_flags_fixed: 0,
lzw_streams_reencoded: 0,
ocg_fixes: 0,
cidtogidmap_added: 0,
ap_fixes: 0,
file_attachment_annotations_removed: 0,
ca_fixes: 0,
smask_s_fixes: 0,
long_string_fixes: 0,
invalid_lang_fixes: 0,
date_mismatch_fixes: 0,
xmp_rebuilt: 0,
};
if doc.version.starts_with('2') || doc.version == "1.8" {
doc.version = "1.7".to_string();
}
crate::pdfa_fixups::fix_binary_inline_image_ei(doc);
report.js_actions_removed = remove_javascript(doc);
report.aa_entries_removed = remove_additional_actions(doc);
report.transparency_groups_found = count_transparency_groups(doc);
if is_pdfa1 {
report.embedded_files_removed = remove_embedded_files(doc);
}
report.encryption_removed = remove_encryption(doc);
report.transfer_functions_removed = remove_transfer_functions(doc);
report.rendering_intents_fixed = normalize_rendering_intents(doc);
report.trailer_id_added = ensure_trailer_id(doc);
report.annotation_flags_fixed = fix_annotation_flags(doc);
report.lzw_streams_reencoded = reencode_lzw_streams(doc);
report.ocg_fixes = fix_optional_content(doc);
fix_need_appearances(doc);
remove_forbidden_actions(doc);
fix_image_interpolate(doc);
fix_image_alternates(doc);
report.cidtogidmap_added = fix_cidtogidmap(doc);
report.ap_fixes = fix_annotation_ap(doc);
report.ap_fixes += fix_acroform_widget_ap(doc);
strip_ap_non_normal(doc);
report.file_attachment_annotations_removed = remove_file_attachment_annotations(doc);
report.smask_s_fixes = fix_extgstate_smask_s(doc);
report.long_string_fixes = fix_long_strings_in_content_streams(doc);
report.invalid_lang_fixes = fix_invalid_lang_in_bdc(doc);
report.date_mismatch_fixes = sync_info_xmp_dates(doc);
report.xmp_rebuilt = rebuild_broken_xmp(doc);
fix_ocg_order(doc);
fix_annotation_contents(doc);
fix_annotation_contents_type(doc);
fix_widget_actions(doc);
remove_xfa_from_acroform(doc);
fix_widget_btn_appearance(doc);
fix_non_btn_ap_n_subdict(doc);
fix_form_xobject_keys(doc);
ensure_xmp_dc_title(doc);
truncate_long_names(doc);
fix_soft_mask_colorspace(doc);
strip_forbidden_smask(doc, is_pdfa1);
ensure_page_transparency_group(doc);
fix_markinfo_without_structtreeroot(doc);
fix_unbalanced_emc(doc);
remove_halftone_names(doc);
remove_needs_rendering(doc);
remove_pressteps(doc);
remove_alternate_presentations(doc);
remove_forbidden_annotations(doc);
fix_file_spec_keys(doc);
strip_ef_from_file_specs(doc);
strip_filespec_type(doc);
neutralize_filespec_by_structure(doc);
strip_stream_external_ref_keys(doc);
remove_ocg_as_key(doc);
strip_signatures(doc);
strip_non_catalog_metadata(doc);
ensure_page_resources(doc);
promote_inline_fonts(doc);
sanitize_hash_encoded_names(doc);
Ok(report)
}
pub fn remove_javascript(doc: &mut Document) -> usize {
let mut count = 0;
if let Some(catalog_id) = get_catalog_id(doc) {
let names_id = {
if let Some(Object::Dictionary(catalog)) = doc.objects.get(&catalog_id) {
match catalog.get(b"Names").ok() {
Some(Object::Reference(id)) => Some(*id),
_ => None,
}
} else {
None
}
};
if let Some(nid) = names_id {
if let Some(Object::Dictionary(ref mut names)) = doc.objects.get_mut(&nid) {
if names.has(b"JavaScript") {
names.remove(b"JavaScript");
count += 1;
}
}
}
let has_inline_js = {
if let Some(Object::Dictionary(catalog)) = doc.objects.get(&catalog_id) {
if let Ok(Object::Dictionary(names)) = catalog.get(b"Names") {
names.has(b"JavaScript")
} else {
false
}
} else {
false
}
};
if has_inline_js {
if let Some(Object::Dictionary(ref mut catalog)) = doc.objects.get_mut(&catalog_id) {
if let Ok(Object::Dictionary(ref mut names)) = catalog.get_mut(b"Names") {
names.remove(b"JavaScript");
count += 1;
}
}
}
}
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let has_js = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
is_javascript_action(dict)
} else {
false
}
};
if has_js {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
dict.remove(b"JS");
dict.remove(b"S");
count += 1;
}
}
}
count
}
pub fn remove_additional_actions(doc: &mut Document) -> usize {
let mut count = 0;
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let has_aa = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
dict.has(b"AA")
} else {
false
}
};
if has_aa {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
dict.remove(b"AA");
count += 1;
}
}
}
if let Some(catalog_id) = get_catalog_id(doc) {
let remove_open_action = {
if let Some(Object::Dictionary(catalog)) = doc.objects.get(&catalog_id) {
match catalog.get(b"OpenAction").ok() {
Some(Object::Reference(action_id)) => {
if let Some(Object::Dictionary(action)) = doc.objects.get(action_id) {
is_javascript_action(action)
|| is_action_forbidden(action)
|| action.get(b"S").is_err()
} else {
false
}
}
Some(Object::Dictionary(action)) => {
is_javascript_action(action)
|| is_action_forbidden(action)
|| action.get(b"S").is_err()
}
_ => false,
}
} else {
false
}
};
if remove_open_action {
if let Some(Object::Dictionary(ref mut catalog)) = doc.objects.get_mut(&catalog_id) {
catalog.remove(b"OpenAction");
count += 1;
}
}
}
count
}
pub fn remove_embedded_files(doc: &mut Document) -> usize {
let mut count = 0;
let catalog_id = match get_catalog_id(doc) {
Some(id) => id,
None => return 0,
};
let names_id = {
if let Some(Object::Dictionary(catalog)) = doc.objects.get(&catalog_id) {
match catalog.get(b"Names").ok() {
Some(Object::Reference(id)) => Some(*id),
_ => None,
}
} else {
None
}
};
if let Some(nid) = names_id {
if let Some(Object::Dictionary(names)) = doc.objects.get(&nid) {
if let Ok(Object::Reference(ef_id)) = names.get(b"EmbeddedFiles") {
count += count_name_tree_entries(doc, *ef_id, 0);
}
}
}
if let Some(nid) = names_id {
if let Some(Object::Dictionary(ref mut names)) = doc.objects.get_mut(&nid) {
if names.has(b"EmbeddedFiles") {
if count == 0 {
count = 1;
}
names.remove(b"EmbeddedFiles");
}
}
}
let has_inline_ef = {
if let Some(Object::Dictionary(catalog)) = doc.objects.get(&catalog_id) {
if let Ok(Object::Dictionary(names)) = catalog.get(b"Names") {
names.has(b"EmbeddedFiles")
} else {
false
}
} else {
false
}
};
if has_inline_ef {
if count == 0 {
count = 1;
}
if let Some(Object::Dictionary(ref mut catalog)) = doc.objects.get_mut(&catalog_id) {
if let Ok(Object::Dictionary(ref mut names)) = catalog.get_mut(b"Names") {
names.remove(b"EmbeddedFiles");
}
}
}
if let Some(Object::Dictionary(ref mut catalog)) = doc.objects.get_mut(&catalog_id) {
if catalog.has(b"AF") {
catalog.remove(b"AF");
}
}
count
}
pub fn count_transparency_groups(doc: &Document) -> usize {
doc.objects
.values()
.filter(|obj| {
if let Object::Dictionary(dict) = obj {
if let Ok(Object::Dictionary(group)) = dict.get(b"Group") {
if let Ok(Object::Name(s)) = group.get(b"S") {
return s == b"Transparency";
}
}
}
false
})
.count()
}
pub fn remove_encryption(doc: &mut Document) -> bool {
if doc.trailer.has(b"Encrypt") {
doc.trailer.remove(b"Encrypt");
true
} else {
false
}
}
fn remove_transfer_functions(doc: &mut Document) -> usize {
let mut count = 0;
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let has_tr = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
dict.has(b"TR") || dict.has(b"TR2")
} else {
false
}
};
if has_tr {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
if dict.has(b"TR") {
dict.remove(b"TR");
count += 1;
}
if dict.has(b"TR2") {
dict.remove(b"TR2");
count += 1;
}
}
}
}
count
}
fn normalize_rendering_intents(doc: &mut Document) -> usize {
let valid = [
b"RelativeColorimetric" as &[u8],
b"AbsoluteColorimetric",
b"Perceptual",
b"Saturation",
];
let mut count = 0;
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let needs_ri_fix = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
if let Ok(Object::Name(ri)) = dict.get(b"RI") {
!valid.contains(&ri.as_slice())
} else {
false
}
} else {
false
}
};
if needs_ri_fix {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
dict.set("RI", Object::Name(b"RelativeColorimetric".to_vec()));
count += 1;
}
}
let needs_intent_fix = {
if let Some(Object::Stream(stream)) = doc.objects.get(&id) {
if let Ok(Object::Name(intent)) = stream.dict.get(b"Intent") {
!valid.contains(&intent.as_slice())
} else {
false
}
} else {
false
}
};
if needs_intent_fix {
if let Some(Object::Stream(ref mut stream)) = doc.objects.get_mut(&id) {
stream
.dict
.set("Intent", Object::Name(b"RelativeColorimetric".to_vec()));
count += 1;
}
}
}
count
}
fn ensure_trailer_id(doc: &mut Document) -> bool {
if let Ok(Object::Array(arr)) = doc.trailer.get(b"ID") {
let valid = arr.len() == 2
&& arr
.iter()
.all(|o| matches!(o, Object::String(s, _) if !s.is_empty()));
if valid {
return false;
}
}
let id_bytes: Vec<u8> = {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut h = DefaultHasher::new();
doc.objects.len().hash(&mut h);
let hash = h.finish();
let mut bytes = Vec::with_capacity(16);
bytes.extend_from_slice(&hash.to_be_bytes());
bytes.extend_from_slice(&hash.to_le_bytes());
bytes
};
let id1 = Object::String(id_bytes.clone(), lopdf::StringFormat::Hexadecimal);
let id2 = Object::String(id_bytes, lopdf::StringFormat::Hexadecimal);
doc.trailer.set("ID", Object::Array(vec![id1, id2]));
true
}
fn fix_annotation_flags(doc: &mut Document) -> usize {
let mut count = 0;
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let needs_fix = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
let subtype = match dict.get(b"Subtype").ok() {
Some(Object::Name(n)) => Some(n.clone()),
_ => None,
};
let is_annot = subtype.as_ref().is_some_and(|n| is_annotation_subtype(n))
|| matches!(
dict.get(b"Type").ok(),
Some(Object::Name(ref n)) if n == b"Annot"
);
let is_popup = subtype.as_deref() == Some(b"Popup");
if !is_annot {
false
} else {
let has_f = dict.get(b"F").is_ok();
let f = match dict.get(b"F").ok() {
Some(Object::Integer(v)) => *v as u32,
_ => 0,
};
if is_popup && !has_f {
false
} else {
let print_bit: u32 = 1 << 2;
let bad: u32 = (1 << 0) | (1 << 1) | (1 << 5) | (1 << 8);
(f & print_bit == 0) || (f & bad != 0)
}
}
} else {
false
}
};
if needs_fix {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
let f = match dict.get(b"F").ok() {
Some(Object::Integer(v)) => *v as u32,
_ => 0,
};
let print_bit: u32 = 1 << 2;
let bad: u32 = (1 << 0) | (1 << 1) | (1 << 5) | (1 << 8);
let new_f = (f | print_bit) & !bad;
dict.set("F", Object::Integer(new_f as i64));
count += 1;
}
}
}
let page_ids: Vec<ObjectId> = doc.get_pages().values().copied().collect();
for page_id in page_ids {
let annots_ref = {
let Some(Object::Dictionary(page)) = doc.objects.get(&page_id) else {
continue;
};
match page.get(b"Annots").ok() {
Some(Object::Reference(id)) => Some(*id),
Some(Object::Array(_)) => None,
_ => continue,
}
};
let arr_id = annots_ref.unwrap_or(page_id);
let is_inline = annots_ref.is_none();
let annots_len = if is_inline {
if let Some(Object::Dictionary(page)) = doc.objects.get(&arr_id) {
if let Ok(Object::Array(arr)) = page.get(b"Annots") {
arr.len()
} else {
0
}
} else {
0
}
} else if let Some(Object::Array(arr)) = doc.objects.get(&arr_id) {
arr.len()
} else {
0
};
for i in 0..annots_len {
let needs_fix = {
let arr = if is_inline {
let Some(Object::Dictionary(page)) = doc.objects.get(&arr_id) else {
break;
};
let Ok(Object::Array(arr)) = page.get(b"Annots") else {
break;
};
arr
} else {
let Some(Object::Array(arr)) = doc.objects.get(&arr_id) else {
break;
};
arr
};
let Object::Dictionary(annot) = &arr[i] else {
continue;
};
let subtype = match annot.get(b"Subtype").ok() {
Some(Object::Name(n)) => Some(n.clone()),
_ => None,
};
let is_annot_sub = subtype.as_ref().is_some_and(|n| is_annotation_subtype(n));
let is_popup = subtype.as_deref() == Some(b"Popup");
if !is_annot_sub {
false
} else {
let has_f = annot.get(b"F").is_ok();
let f = match annot.get(b"F").ok() {
Some(Object::Integer(v)) => *v as u32,
_ => 0,
};
if is_popup && !has_f {
false
} else {
let print_bit: u32 = 1 << 2;
let bad_bits: u32 = (1 << 0) | (1 << 1) | (1 << 5) | (1 << 8);
(f & print_bit == 0) || (f & bad_bits != 0)
}
}
};
if needs_fix {
let arr_mut = if is_inline {
let Some(Object::Dictionary(ref mut page)) = doc.objects.get_mut(&arr_id)
else {
break;
};
let Ok(Object::Array(ref mut arr)) = page.get_mut(b"Annots") else {
break;
};
arr
} else {
let Some(Object::Array(ref mut arr)) = doc.objects.get_mut(&arr_id) else {
break;
};
arr
};
if let Object::Dictionary(ref mut annot) = arr_mut[i] {
let f = match annot.get(b"F").ok() {
Some(Object::Integer(v)) => *v as u32,
_ => 0,
};
let print_bit: u32 = 1 << 2;
let bad_bits: u32 = (1 << 0) | (1 << 1) | (1 << 5) | (1 << 8);
let new_f = (f | print_bit) & !bad_bits;
annot.set("F", Object::Integer(new_f as i64));
count += 1;
}
}
}
}
count
}
fn reencode_lzw_streams(doc: &mut Document) -> usize {
let mut count = 0;
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let lzw_mode = {
if let Some(Object::Stream(stream)) = doc.objects.get(&id) {
match stream.dict.get(b"Filter").ok() {
Some(Object::Name(n)) if n == b"LZWDecode" => LzwMode::Single,
Some(Object::Array(arr)) => {
let lzw_pos = arr
.iter()
.position(|o| matches!(o, Object::Name(n) if n == b"LZWDecode"));
if let Some(pos) = lzw_pos {
LzwMode::InArray(pos)
} else {
LzwMode::None
}
}
_ => LzwMode::None,
}
} else {
LzwMode::None
}
};
match lzw_mode {
LzwMode::None => {}
LzwMode::Single => {
let decoded = {
if let Some(Object::Stream(stream)) = doc.objects.get(&id) {
stream.decompressed_content().ok()
} else {
None
}
};
let data_to_compress = if decoded.is_some() {
decoded
} else if let Some(Object::Stream(stream)) = doc.objects.get(&id) {
Some(stream.content.clone())
} else {
None
};
if let Some(data) = data_to_compress {
if let Some(compressed) = flate_compress(&data) {
if let Some(Object::Stream(ref mut stream)) = doc.objects.get_mut(&id) {
stream.set_content(compressed);
stream
.dict
.set("Filter", Object::Name(b"FlateDecode".to_vec()));
stream.dict.remove(b"DecodeParms");
count += 1;
}
}
}
}
LzwMode::InArray(pos) => {
let lzw_decoded = {
let Some(Object::Stream(stream)) = doc.objects.get(&id) else {
continue;
};
let early_change = lzw_early_change(&stream.dict, pos);
let raw = &stream.content;
lzw_decode(raw, early_change)
};
let Some(decoded_data) = lzw_decoded else {
let decoded = {
if let Some(Object::Stream(stream)) = doc.objects.get(&id) {
stream.decompressed_content().ok()
} else {
None
}
};
let data_to_compress = if decoded.is_some() {
decoded
} else if let Some(Object::Stream(stream)) = doc.objects.get(&id) {
Some(stream.content.clone())
} else {
None
};
if let Some(data) = data_to_compress {
if let Some(compressed_data) = flate_compress(&data) {
if let Some(Object::Stream(ref mut stream)) = doc.objects.get_mut(&id) {
stream.set_content(compressed_data);
stream
.dict
.set("Filter", Object::Name(b"FlateDecode".to_vec()));
stream.dict.remove(b"DecodeParms");
count += 1;
}
}
}
continue;
};
if let Some(Object::Stream(ref mut stream)) = doc.objects.get_mut(&id) {
stream.set_content(decoded_data);
let mut new_filter: Option<Object> = None;
let mut new_parms: Option<Option<Object>> = None; if let Ok(Object::Array(ref mut filters)) = stream.dict.get_mut(b"Filter") {
filters.remove(pos);
if filters.len() == 1 {
new_filter = Some(filters[0].clone());
}
}
if let Ok(Object::Array(ref mut parms)) = stream.dict.get_mut(b"DecodeParms") {
if pos < parms.len() {
parms.remove(pos);
}
if parms.len() == 1 {
let single = parms[0].clone();
new_parms = if matches!(single, Object::Null) {
Some(None)
} else {
Some(Some(single))
};
}
}
if let Some(f) = new_filter {
stream.dict.set("Filter", f);
}
match new_parms {
Some(None) => {
stream.dict.remove(b"DecodeParms");
}
Some(Some(p)) => {
stream.dict.set("DecodeParms", p);
}
None => {}
}
count += 1;
}
}
}
}
count
}
enum LzwMode {
None,
Single,
InArray(usize),
}
fn flate_compress(data: &[u8]) -> Option<Vec<u8>> {
use std::io::Write;
let mut encoder = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
if encoder.write_all(data).is_ok() {
encoder.finish().ok()
} else {
None
}
}
fn lzw_decode(data: &[u8], _early_change: bool) -> Option<Vec<u8>> {
let mut decoder = weezl::decode::Decoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8);
decoder.decode(data).ok()
}
fn lzw_early_change(dict: &lopdf::Dictionary, pos: usize) -> bool {
let parms = match dict.get(b"DecodeParms").ok() {
Some(Object::Array(arr)) => arr.get(pos).cloned(),
Some(Object::Dictionary(d)) if pos == 0 => Some(Object::Dictionary(d.clone())),
_ => None,
};
if let Some(Object::Dictionary(parms_dict)) = parms {
match parms_dict.get(b"EarlyChange").ok() {
Some(Object::Integer(0)) => false,
_ => true, }
} else {
true }
}
fn fix_need_appearances(doc: &mut Document) {
let catalog_id = match get_catalog_id(doc) {
Some(id) => id,
None => return,
};
let acroform_id = {
if let Some(Object::Dictionary(catalog)) = doc.objects.get(&catalog_id) {
match catalog.get(b"AcroForm").ok() {
Some(Object::Reference(id)) => Some(*id),
_ => None,
}
} else {
None
}
};
if let Some(af_id) = acroform_id {
if let Some(Object::Dictionary(ref mut af)) = doc.objects.get_mut(&af_id) {
if let Ok(Object::Boolean(true)) = af.get(b"NeedAppearances") {
af.set("NeedAppearances", Object::Boolean(false));
}
}
} else {
if let Some(Object::Dictionary(ref mut catalog)) = doc.objects.get_mut(&catalog_id) {
if let Ok(Object::Dictionary(ref mut af)) = catalog.get_mut(b"AcroForm") {
if let Ok(Object::Boolean(true)) = af.get(b"NeedAppearances") {
af.set("NeedAppearances", Object::Boolean(false));
}
}
}
}
}
fn fix_optional_content(doc: &mut Document) -> usize {
let mut count = 0;
let catalog_id = match get_catalog_id(doc) {
Some(id) => id,
None => return 0,
};
let (ocprops_id, ocprops_inline) = {
if let Some(Object::Dictionary(catalog)) = doc.objects.get(&catalog_id) {
match catalog.get(b"OCProperties").ok() {
Some(Object::Reference(id)) => (Some(*id), false),
Some(Object::Dictionary(_)) => (None, true),
_ => return 0,
}
} else {
return 0;
}
};
if !ocprops_inline && ocprops_id.is_none() {
return 0;
}
fn fix_d_config(d_dict: &mut lopdf::Dictionary, ocgs: &[Object], count: &mut usize) {
let name_missing_or_empty = match d_dict.get(b"Name").ok() {
None => true,
Some(Object::String(s, _)) => s.is_empty(),
_ => false,
};
if name_missing_or_empty {
d_dict.set(
"Name",
Object::String(b"Default".to_vec(), lopdf::StringFormat::Literal),
);
*count += 1;
}
if !ocgs.is_empty() {
d_dict.set("Order", Object::Array(ocgs.to_vec()));
*count += 1;
}
if d_dict.has(b"AS") {
d_dict.remove(b"AS");
*count += 1;
}
}
if ocprops_inline {
let ocgs: Vec<Object> = {
if let Some(Object::Dictionary(catalog)) = doc.objects.get(&catalog_id) {
if let Ok(Object::Dictionary(ocprops)) = catalog.get(b"OCProperties") {
match ocprops.get(b"OCGs").ok() {
Some(Object::Array(arr)) => arr.clone(),
_ => vec![],
}
} else {
vec![]
}
} else {
vec![]
}
};
if let Some(Object::Dictionary(ref mut catalog)) = doc.objects.get_mut(&catalog_id) {
if let Ok(Object::Dictionary(ref mut ocprops)) = catalog.get_mut(b"OCProperties") {
if let Ok(Object::Dictionary(ref mut d_dict)) = ocprops.get_mut(b"D") {
fix_d_config(d_dict, &ocgs, &mut count);
}
}
}
return count;
}
let ocprops_id = ocprops_id.expect("ocprops_id.is_none() branch returns above");
let ocgs: Vec<Object> = {
if let Some(Object::Dictionary(ocprops)) = doc.objects.get(&ocprops_id) {
match ocprops.get(b"OCGs").ok() {
Some(Object::Array(arr)) => arr.clone(),
_ => vec![],
}
} else {
vec![]
}
};
let d_id = {
if let Some(Object::Dictionary(ocprops)) = doc.objects.get(&ocprops_id) {
match ocprops.get(b"D").ok() {
Some(Object::Reference(id)) => Some(*id),
_ => None,
}
} else {
None
}
};
if let Some(did) = d_id {
if let Some(Object::Dictionary(ref mut d_dict)) = doc.objects.get_mut(&did) {
fix_d_config(d_dict, &ocgs, &mut count);
}
} else {
if let Some(Object::Dictionary(ref mut ocprops)) = doc.objects.get_mut(&ocprops_id) {
if let Ok(Object::Dictionary(ref mut d_dict)) = ocprops.get_mut(b"D") {
fix_d_config(d_dict, &ocgs, &mut count);
}
}
}
let (config_ids, inline_indices): (Vec<ObjectId>, Vec<usize>) = {
if let Some(Object::Dictionary(ocprops)) = doc.objects.get(&ocprops_id) {
match ocprops.get(b"Configs").ok() {
Some(Object::Array(arr)) => {
let refs = arr
.iter()
.filter_map(|o| {
if let Object::Reference(id) = o {
Some(*id)
} else {
None
}
})
.collect();
let inlines = arr
.iter()
.enumerate()
.filter_map(|(i, o)| {
if matches!(o, Object::Dictionary(_)) {
Some(i)
} else {
None
}
})
.collect();
(refs, inlines)
}
_ => (vec![], vec![]),
}
} else {
(vec![], vec![])
}
};
for config_id in config_ids {
if let Some(Object::Dictionary(ref mut config)) = doc.objects.get_mut(&config_id) {
let name_missing_or_empty = match config.get(b"Name").ok() {
None => true,
Some(Object::String(s, _)) => s.is_empty(),
_ => false,
};
if name_missing_or_empty {
config.set(
"Name",
Object::String(b"Config".to_vec(), lopdf::StringFormat::Literal),
);
count += 1;
}
if config.has(b"AS") {
config.remove(b"AS");
count += 1;
}
}
}
if !inline_indices.is_empty() {
if let Some(Object::Dictionary(ref mut ocprops)) = doc.objects.get_mut(&ocprops_id) {
if let Ok(Object::Array(ref mut configs_arr)) = ocprops.get_mut(b"Configs") {
for idx in inline_indices {
if let Some(Object::Dictionary(ref mut config)) = configs_arr.get_mut(idx) {
let name_missing_or_empty = match config.get(b"Name").ok() {
None => true,
Some(Object::String(s, _)) => s.is_empty(),
_ => false,
};
if name_missing_or_empty {
config.set(
"Name",
Object::String(b"Config".to_vec(), lopdf::StringFormat::Literal),
);
count += 1;
}
if config.has(b"AS") {
config.remove(b"AS");
count += 1;
}
}
}
}
}
}
count
}
fn is_btn_field(doc: &Document, dict: &lopdf::Dictionary) -> bool {
if let Ok(Object::Name(ft)) = dict.get(b"FT") {
return ft == b"Btn";
}
let mut current = dict.get(b"Parent").ok().cloned();
for _ in 0..10 {
let obj = match ¤t {
Some(Object::Reference(id)) => doc.objects.get(id),
_ => break,
};
if let Some(Object::Dictionary(parent)) = obj {
if let Ok(Object::Name(ft)) = parent.get(b"FT") {
return ft == b"Btn";
}
current = parent.get(b"Parent").ok().cloned();
} else {
break;
}
}
false
}
fn is_annotation_subtype(name: &[u8]) -> bool {
matches!(
name,
b"Text"
| b"Link"
| b"FreeText"
| b"Line"
| b"Square"
| b"Circle"
| b"Polygon"
| b"PolyLine"
| b"Highlight"
| b"Underline"
| b"Squiggly"
| b"StrikeOut"
| b"Stamp"
| b"Caret"
| b"Ink"
| b"FileAttachment"
| b"Sound"
| b"Movie"
| b"Widget"
| b"Screen"
| b"PrinterMark"
| b"TrapNet"
| b"Watermark"
| b"3D"
| b"Redact"
| b"Popup"
)
}
fn is_forbidden_action(s: &[u8]) -> bool {
matches!(
s,
b"Launch"
| b"Sound"
| b"Movie"
| b"SubmitForm"
| b"ResetForm"
| b"ImportData"
| b"Hide"
| b"SetOCGState"
| b"Rendition"
| b"Trans"
| b"GoTo3DView"
| b"GoToE"
| b"SetState"
| b"NoOp"
| b"NOP"
| b"Nop"
| b"JavaScript"
)
}
fn is_forbidden_named_action(dict: &lopdf::Dictionary) -> bool {
match dict.get(b"N").ok() {
Some(Object::Name(n)) => !matches!(
n.as_slice(),
b"NextPage" | b"PrevPage" | b"FirstPage" | b"LastPage"
),
_ => true,
}
}
fn is_action_forbidden(dict: &lopdf::Dictionary) -> bool {
match dict.get(b"S").ok() {
Some(Object::Name(s)) => {
if is_forbidden_action(s) {
return true;
}
if s == b"Named" && is_forbidden_named_action(dict) {
return true;
}
}
Some(Object::Stream(_)) | Some(Object::Reference(_)) => {}
Some(_) => return true,
None => {}
}
false
}
pub(crate) fn is_structure_element(dict: &lopdf::Dictionary) -> bool {
dict.get(b"Type").and_then(Object::as_name).ok() == Some(b"StructElem")
|| (dict.has(b"S") && dict.has(b"P") && !dict.has(b"Subtype"))
}
pub(crate) fn resource_dictionary_ids(doc: &Document) -> std::collections::HashSet<ObjectId> {
let mut ids = std::collections::HashSet::new();
let dict_of = |obj: &Object| -> Option<lopdf::Dictionary> {
match obj {
Object::Dictionary(d) => Some(d.clone()),
Object::Stream(s) => Some(s.dict.clone()),
_ => None,
}
};
for object in doc.objects.values() {
let Some(dict) = dict_of(object) else {
continue;
};
let resources = match dict.get(b"Resources") {
Ok(Object::Reference(id)) => {
ids.insert(*id);
match doc.objects.get(id) {
Some(Object::Dictionary(d)) => d.clone(),
_ => continue,
}
}
Ok(Object::Dictionary(d)) => d.clone(),
_ => continue,
};
for (_, value) in resources.iter() {
if let Object::Reference(id) = value {
ids.insert(*id);
}
}
}
ids
}
fn remove_forbidden_actions(doc: &mut Document) {
let resource_dicts = resource_dictionary_ids(doc);
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let needs_fix = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
is_action_forbidden(dict)
} else {
false
}
};
if needs_fix {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
dict.remove(b"S");
}
}
}
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
if resource_dicts.contains(&id) {
continue;
}
let remove_a = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
if is_structure_element(dict) {
continue;
}
match dict.get(b"A").ok() {
Some(Object::Dictionary(action)) => is_action_forbidden(action),
_ => false,
}
} else {
false
}
};
if remove_a {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
dict.remove(b"A");
}
}
}
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in &ids {
let has_next = matches!(
doc.objects.get(id),
Some(Object::Dictionary(d)) if d.has(b"Next") && d.has(b"S")
);
if has_next {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(id) {
dict.remove(b"Next");
}
}
}
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
if resource_dicts.contains(&id) {
continue;
}
let remove_a = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
if is_structure_element(dict) {
continue;
}
match dict.get(b"A").ok() {
Some(Object::Reference(action_id)) => {
match doc.objects.get(action_id) {
Some(Object::Dictionary(action)) => {
!action.has(b"S")
}
_ => false,
}
}
_ => false,
}
} else {
false
}
};
if remove_a {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
dict.remove(b"A");
}
}
}
}
fn fix_image_interpolate(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let needs_fix = {
if let Some(Object::Stream(stream)) = doc.objects.get(&id) {
let is_image = matches!(
stream.dict.get(b"Subtype").ok(),
Some(Object::Name(ref n)) if n == b"Image"
);
is_image
&& matches!(
stream.dict.get(b"Interpolate").ok(),
Some(Object::Boolean(true))
)
} else {
false
}
};
if needs_fix {
if let Some(Object::Stream(ref mut stream)) = doc.objects.get_mut(&id) {
stream.dict.set("Interpolate", Object::Boolean(false));
}
}
}
}
fn fix_image_alternates(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let needs_fix = if let Some(Object::Stream(stream)) = doc.objects.get(&id) {
matches!(
stream.dict.get(b"Subtype").ok(),
Some(Object::Name(ref n)) if n == b"Image"
) && stream.dict.get(b"Alternates").is_ok()
} else {
false
};
if needs_fix {
if let Some(Object::Stream(ref mut stream)) = doc.objects.get_mut(&id) {
stream.dict.remove(b"Alternates");
}
}
}
}
fn fix_cidtogidmap(doc: &mut Document) -> usize {
let mut count = 0;
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let needs_fix = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
let is_cidfont2 = matches!(
dict.get(b"Subtype").ok(),
Some(Object::Name(ref n)) if n == b"CIDFontType2"
);
if !is_cidfont2 {
false
} else {
match dict.get(b"CIDToGIDMap").ok() {
None => true, Some(Object::Name(n)) if n == b"Identity" => false, Some(Object::Reference(ref_id)) => {
!matches!(doc.objects.get(ref_id), Some(Object::Stream(_)))
}
Some(Object::Stream(_)) => false, _ => true, }
}
} else {
false
}
};
if needs_fix {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
dict.set("CIDToGIDMap", Object::Name(b"Identity".to_vec()));
count += 1;
}
}
}
count
}
fn fix_annotation_ap(doc: &mut Document) -> usize {
let mut count = 0;
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let needs_ca_fix = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
let is_annot = matches!(
dict.get(b"Subtype").ok(),
Some(Object::Name(ref n)) if is_annotation_subtype(n)
);
if is_annot {
match dict.get(b"CA").ok() {
Some(Object::Real(r)) => *r != 1.0,
Some(Object::Integer(i)) => *i != 1,
_ => false, }
} else {
false
}
} else {
false
}
};
if needs_ca_fix {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
dict.set("CA", Object::Real(1.0));
count += 1;
}
}
let fix_info = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
let is_annot = matches!(
dict.get(b"Subtype").ok(),
Some(Object::Name(ref n)) if is_annotation_subtype(n) && n != b"Popup" && n != b"Link"
) || matches!(
dict.get(b"Type").ok(),
Some(Object::Name(ref n)) if n == b"Annot"
);
if !is_annot {
ApFixInfo::None
} else {
let is_zero_size = match dict.get(b"Rect").ok() {
Some(Object::Array(arr)) if arr.len() == 4 => {
let nums: Vec<f64> = arr
.iter()
.filter_map(|o| match o {
Object::Integer(i) => Some(*i as f64),
Object::Real(r) => Some(*r as f64),
_ => None,
})
.collect();
nums.len() == 4
&& (nums[0] - nums[2]).abs() < 0.01
&& (nums[1] - nums[3]).abs() < 0.01
}
_ => false,
};
if is_zero_size {
ApFixInfo::None
} else {
let bbox = annot_rect_to_bbox(dict);
match dict.get(b"AP").ok() {
Some(Object::Dictionary(ap)) => {
let n_is_valid = match ap.get(b"N").ok() {
Some(Object::Stream(_)) => true,
Some(Object::Dictionary(d)) => !d.is_empty(),
Some(Object::Reference(r)) => match doc.objects.get(r) {
Some(Object::Stream(_)) => true,
Some(Object::Dictionary(d)) => !d.is_empty(),
_ => false,
},
_ => false,
};
if n_is_valid {
ApFixInfo::None
} else {
let fallback = ap
.get(b"D")
.ok()
.cloned()
.or_else(|| ap.get(b"R").ok().cloned());
ApFixInfo::InlineAp(fallback, bbox)
}
}
Some(Object::Reference(ap_id)) => ApFixInfo::RefAp(*ap_id, bbox),
_ => ApFixInfo::MissingAp(bbox), }
}
}
} else {
ApFixInfo::None
}
};
match fix_info {
ApFixInfo::InlineAp(Some(val), _) => {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
if let Ok(Object::Dictionary(ref mut ap)) = dict.get_mut(b"AP") {
ap.set("N", val);
count += 1;
}
}
}
ApFixInfo::RefAp(ap_id, bbox) => {
let needs_fix = {
if let Some(Object::Dictionary(ap)) = doc.objects.get(&ap_id) {
let n_valid = match ap.get(b"N").ok() {
Some(Object::Stream(_)) => true,
Some(Object::Dictionary(d)) => !d.is_empty(),
Some(Object::Reference(r)) => match doc.objects.get(r) {
Some(Object::Stream(_)) => true,
Some(Object::Dictionary(d)) => !d.is_empty(),
_ => false,
},
_ => false,
};
if n_valid {
None
} else {
Some(
ap.get(b"D")
.ok()
.cloned()
.or_else(|| ap.get(b"R").ok().cloned()),
)
}
} else {
None
}
};
if let Some(fallback) = needs_fix {
if let Some(val) = fallback {
if let Some(Object::Dictionary(ref mut ap)) = doc.objects.get_mut(&ap_id) {
ap.set("N", val);
count += 1;
}
} else {
let empty_stream = make_empty_form_xobject(bbox);
let stream_id = doc.add_object(Object::Stream(empty_stream));
if let Some(Object::Dictionary(ref mut ap)) = doc.objects.get_mut(&ap_id) {
ap.set("N", Object::Reference(stream_id));
count += 1;
}
}
}
}
ApFixInfo::InlineAp(None, bbox) => {
let empty_stream = make_empty_form_xobject(bbox);
let stream_id = doc.add_object(Object::Stream(empty_stream));
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
if let Ok(Object::Dictionary(ref mut ap)) = dict.get_mut(b"AP") {
ap.set("N", Object::Reference(stream_id));
count += 1;
}
}
}
ApFixInfo::MissingAp(bbox) => {
let empty_stream = make_empty_form_xobject(bbox);
let stream_id = doc.add_object(Object::Stream(empty_stream));
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
let mut ap = lopdf::Dictionary::new();
ap.set("N", Object::Reference(stream_id));
dict.set("AP", Object::Dictionary(ap));
count += 1;
}
}
_ => {}
}
}
count
}
enum ApFixInfo {
None,
InlineAp(Option<Object>, [f64; 4]),
RefAp(ObjectId, [f64; 4]),
MissingAp([f64; 4]),
}
fn annot_rect_to_bbox(dict: &lopdf::Dictionary) -> [f64; 4] {
match dict.get(b"Rect").ok() {
Some(Object::Array(arr)) if arr.len() == 4 => {
let nums: Vec<f64> = arr
.iter()
.filter_map(|o| match o {
Object::Integer(i) => Some(*i as f64),
Object::Real(r) => Some(*r as f64),
_ => None,
})
.collect();
if nums.len() == 4 {
let w = (nums[2] - nums[0]).abs();
let h = (nums[3] - nums[1]).abs();
[0.0, 0.0, w, h]
} else {
[0.0, 0.0, 1.0, 1.0]
}
}
_ => [0.0, 0.0, 1.0, 1.0],
}
}
fn make_empty_form_xobject(bbox: [f64; 4]) -> lopdf::Stream {
let mut stream_dict = lopdf::Dictionary::new();
stream_dict.set("Type", Object::Name(b"XObject".to_vec()));
stream_dict.set("Subtype", Object::Name(b"Form".to_vec()));
stream_dict.set(
"BBox",
Object::Array(vec![
Object::Real(bbox[0] as f32),
Object::Real(bbox[1] as f32),
Object::Real(bbox[2] as f32),
Object::Real(bbox[3] as f32),
]),
);
stream_dict.set("Resources", Object::Dictionary(lopdf::Dictionary::new()));
lopdf::Stream::new(stream_dict, Vec::new())
}
fn fix_acroform_widget_ap(doc: &mut Document) -> usize {
let catalog_id = match get_catalog_id(doc) {
Some(id) => id,
None => return 0,
};
let mut field_ids: Vec<ObjectId> = Vec::new();
let acroform_fields: Vec<ObjectId> = {
let Some(Object::Dictionary(catalog)) = doc.objects.get(&catalog_id) else {
return 0;
};
let af = match catalog.get(b"AcroForm").ok() {
Some(Object::Reference(id)) => {
let Some(Object::Dictionary(af)) = doc.objects.get(id) else {
return 0;
};
af
}
Some(Object::Dictionary(af)) => af,
_ => return 0,
};
match af.get(b"Fields").ok() {
Some(Object::Array(arr)) => arr
.iter()
.filter_map(|o| {
if let Object::Reference(id) = o {
Some(*id)
} else {
None
}
})
.collect(),
_ => return 0,
}
};
let mut queue: Vec<ObjectId> = acroform_fields;
let mut visited = std::collections::HashSet::new();
while let Some(fid) = queue.pop() {
if !visited.insert(fid) {
continue;
}
let Some(Object::Dictionary(dict)) = doc.objects.get(&fid) else {
continue;
};
if let Ok(Object::Array(kids)) = dict.get(b"Kids") {
for kid in kids {
if let Object::Reference(kid_id) = kid {
queue.push(*kid_id);
}
}
}
let has_rect = dict.has(b"Rect");
let is_widget = matches!(
dict.get(b"Subtype").ok(),
Some(Object::Name(ref n)) if n == b"Widget"
);
if has_rect || is_widget {
field_ids.push(fid);
}
}
let mut count = 0;
for fid in field_ids {
let needs_ap = {
let Some(Object::Dictionary(dict)) = doc.objects.get(&fid) else {
continue;
};
match dict.get(b"AP").ok() {
Some(Object::Dictionary(ap)) => {
match ap.get(b"N").ok() {
Some(Object::Stream(_)) | Some(Object::Dictionary(_)) => continue,
Some(Object::Reference(r)) => {
match doc.objects.get(r) {
Some(Object::Stream(_)) | Some(Object::Dictionary(_)) => continue,
_ => {} }
}
_ => {} }
false }
Some(Object::Reference(ap_id)) => {
if let Some(Object::Dictionary(ap)) = doc.objects.get(ap_id) {
match ap.get(b"N").ok() {
Some(Object::Stream(_)) | Some(Object::Dictionary(_)) => continue,
Some(Object::Reference(r)) => match doc.objects.get(r) {
Some(Object::Stream(_)) | Some(Object::Dictionary(_)) => continue,
_ => {}
},
_ => {}
}
}
false
}
None => true, _ => true,
}
};
if !needs_ap {
continue;
}
let bbox = {
let Some(Object::Dictionary(dict)) = doc.objects.get(&fid) else {
continue;
};
annot_rect_to_bbox(dict)
};
let is_btn = {
let Some(Object::Dictionary(dict)) = doc.objects.get(&fid) else {
continue;
};
is_btn_field(doc, dict)
};
let empty_stream = make_empty_form_xobject(bbox);
let stream_id = doc.add_object(Object::Stream(empty_stream));
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&fid) {
let mut ap = lopdf::Dictionary::new();
if is_btn {
let mut sub = lopdf::Dictionary::new();
sub.set("Off", Object::Reference(stream_id));
ap.set("N", Object::Dictionary(sub));
} else {
ap.set("N", Object::Reference(stream_id));
}
dict.set("AP", Object::Dictionary(ap));
count += 1;
}
}
count
}
fn strip_ap_non_normal(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let ap_ref = {
let Some(Object::Dictionary(dict)) = doc.objects.get(&id) else {
continue;
};
let is_annot = matches!(
dict.get(b"Subtype").ok(),
Some(Object::Name(ref n)) if is_annotation_subtype(n)
);
if !is_annot {
continue;
}
match dict.get(b"AP").ok() {
Some(Object::Dictionary(ap)) => {
if ap.has(b"D") || ap.has(b"R") {
None } else {
continue;
}
}
Some(Object::Reference(ap_id)) => Some(*ap_id),
_ => continue,
}
};
match ap_ref {
Some(ap_id) => {
if let Some(Object::Dictionary(ref mut ap)) = doc.objects.get_mut(&ap_id) {
ap.remove(b"D");
ap.remove(b"R");
}
}
None => {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
if let Ok(Object::Dictionary(ref mut ap)) = dict.get_mut(b"AP") {
ap.remove(b"D");
ap.remove(b"R");
}
}
}
}
}
}
fn fix_form_xobject_keys(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let is_form_xobject = {
let Some(Object::Stream(stream)) = doc.objects.get(&id) else {
continue;
};
matches!(
stream.dict.get(b"Subtype").ok(),
Some(Object::Name(ref n)) if n == b"Form"
)
};
if !is_form_xobject {
continue;
}
if let Some(Object::Stream(ref mut stream)) = doc.objects.get_mut(&id) {
stream.dict.remove(b"OPI");
stream.dict.remove(b"PS");
if matches!(
stream.dict.get(b"Subtype2").ok(),
Some(Object::Name(ref n)) if n == b"PS"
) {
stream.dict.remove(b"Subtype2");
}
}
}
}
fn fix_ocg_order(doc: &mut Document) {
let _ = doc;
}
fn fix_annotation_contents(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let needs_fix = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
let is_annot = matches!(
dict.get(b"Subtype").ok(),
Some(Object::Name(ref n)) if is_annotation_subtype(n) && n != b"Popup"
);
if !is_annot {
false
} else if !dict.has(b"Contents") {
true } else {
!matches!(dict.get(b"Contents").ok(), Some(Object::String(..)))
}
} else {
false
}
};
if needs_fix {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
dict.set(
"Contents",
Object::String(Vec::new(), lopdf::StringFormat::Literal),
);
}
}
}
}
fn fix_annotation_contents_type(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let needs_fix = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
let is_annot = matches!(
dict.get(b"Subtype").ok(),
Some(Object::Name(ref n)) if is_annotation_subtype(n)
);
if !is_annot {
false
} else {
match dict.get(b"Contents").ok() {
Some(Object::String(_, _)) => false, Some(_) => true, None => false, }
}
} else {
false
}
};
if needs_fix {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
dict.set(
"Contents",
Object::String(Vec::new(), lopdf::StringFormat::Literal),
);
}
}
}
}
fn fix_widget_actions(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let is_widget_with_actions = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
matches!(
dict.get(b"Subtype").ok(),
Some(Object::Name(ref n)) if n == b"Widget"
) && (dict.has(b"A") || dict.has(b"AA"))
} else {
false
}
};
if is_widget_with_actions {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
dict.remove(b"A");
dict.remove(b"AA");
}
}
}
}
fn remove_xfa_from_acroform(doc: &mut Document) {
let catalog_id = match get_catalog_id(doc) {
Some(id) => id,
None => return,
};
let acroform_id = {
if let Some(Object::Dictionary(catalog)) = doc.objects.get(&catalog_id) {
match catalog.get(b"AcroForm").ok() {
Some(Object::Reference(id)) => Some(*id),
_ => None,
}
} else {
None
}
};
if let Some(af_id) = acroform_id {
if let Some(Object::Dictionary(ref mut af)) = doc.objects.get_mut(&af_id) {
af.remove(b"XFA");
}
} else if let Some(Object::Dictionary(ref mut catalog)) = doc.objects.get_mut(&catalog_id) {
if let Ok(Object::Dictionary(ref mut af)) = catalog.get_mut(b"AcroForm") {
af.remove(b"XFA");
}
}
}
fn fix_widget_btn_appearance(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let fix_val: Option<Object> = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
let is_widget = matches!(
dict.get(b"Subtype").ok(),
Some(Object::Name(ref n)) if n == b"Widget"
);
let is_widget_btn = is_widget && is_btn_field(doc, dict);
if !is_widget_btn {
None
} else {
match dict.get(b"AP").ok() {
Some(Object::Dictionary(ap)) => match ap.get(b"N").ok() {
Some(Object::Dictionary(_)) => None,
Some(Object::Reference(ref_id)) => {
let target = doc.objects.get(ref_id);
match target {
Some(Object::Dictionary(_)) => None,
Some(Object::Stream(_)) => Some(Object::Reference(*ref_id)),
_ => Some(Object::Reference(*ref_id)),
}
}
Some(other) => Some(other.clone()),
None => None,
},
_ => None,
}
}
} else {
None
}
};
if let Some(val) = fix_val {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
if let Ok(Object::Dictionary(ref mut ap)) = dict.get_mut(b"AP") {
let mut sub = lopdf::Dictionary::new();
sub.set("Off", val.clone());
sub.set("Yes", val);
ap.set("N", Object::Dictionary(sub));
}
}
}
}
}
fn fix_non_btn_ap_n_subdict(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let fix_val: Option<Object> = {
let Some(Object::Dictionary(dict)) = doc.objects.get(&id) else {
continue;
};
let subtype = match dict.get(b"Subtype").ok() {
Some(Object::Name(ref n)) if is_annotation_subtype(n) => n.clone(),
_ => continue,
};
if subtype == b"Widget" && is_btn_field(doc, dict) {
continue;
}
match dict.get(b"AP").ok() {
Some(Object::Dictionary(ap)) => match ap.get(b"N").ok() {
Some(Object::Dictionary(sub)) => {
sub.iter().find_map(|(_, v)| {
if matches!(v, Object::Reference(_)) {
Some(v.clone())
} else {
None
}
})
}
_ => None,
},
_ => None,
}
};
if let Some(val) = fix_val {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
if let Ok(Object::Dictionary(ref mut ap)) = dict.get_mut(b"AP") {
ap.set("N", val);
}
}
}
}
}
#[allow(dead_code)]
fn fix_font_descriptor_keys(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
let mut descriptor_subtypes: Vec<(ObjectId, &'static [u8])> = Vec::new();
for &id in &ids {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
let subtype = match dict.get(b"Subtype").ok() {
Some(Object::Name(ref n)) => n.as_slice(),
_ => continue,
};
let expected_key: &'static [u8] = match subtype {
b"Type1" | b"MMType1" => b"FontFile",
b"TrueType" => b"FontFile2",
b"CIDFontType0" => b"FontFile3",
b"CIDFontType2" => b"FontFile2",
_ => continue,
};
let desc_id = match dict.get(b"FontDescriptor").ok() {
Some(Object::Reference(id)) => *id,
_ => continue,
};
descriptor_subtypes.push((desc_id, expected_key));
}
}
for (desc_id, expected_key) in descriptor_subtypes {
let wrong_keys: Vec<Vec<u8>> = {
let all_keys: &[&[u8]] = &[b"FontFile", b"FontFile2", b"FontFile3"];
if let Some(Object::Dictionary(desc)) = doc.objects.get(&desc_id) {
all_keys
.iter()
.filter(|&&k| k != expected_key && desc.has(k))
.map(|k| k.to_vec())
.collect()
} else {
continue;
}
};
if wrong_keys.is_empty() {
continue;
}
if let Some(Object::Dictionary(ref mut desc)) = doc.objects.get_mut(&desc_id) {
if !desc.has(expected_key) {
if let Some(val) = desc.get(&wrong_keys[0]).ok().cloned() {
desc.set(
std::str::from_utf8(expected_key).unwrap_or("FontFile2"),
val,
);
}
}
for wrong_key in &wrong_keys {
desc.remove(wrong_key);
}
}
}
}
#[allow(dead_code)]
fn fix_font_lastchar_widths(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let fix_info = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
let subtype = match dict.get(b"Subtype").ok() {
Some(Object::Name(ref n)) => n.as_slice(),
_ => continue,
};
if !matches!(subtype, b"Type1" | b"TrueType" | b"MMType1") {
continue;
}
if !dict.has(b"BaseFont") {
continue;
}
let first_char = match dict.get(b"FirstChar").ok() {
Some(Object::Integer(v)) => Some(*v),
_ => None,
};
let last_char = match dict.get(b"LastChar").ok() {
Some(Object::Integer(v)) => Some(*v),
_ => None,
};
let widths_len = match dict.get(b"Widths").ok() {
Some(Object::Array(arr)) => Some(arr.len() as i64),
_ => None,
};
Some((first_char, last_char, widths_len))
} else {
continue;
}
};
if let Some((first_char, last_char, widths_len)) = fix_info {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
let fc = first_char.unwrap_or(0);
let lc = last_char.unwrap_or(255);
if first_char.is_none() {
dict.set("FirstChar", Object::Integer(fc));
}
if last_char.is_none() {
dict.set("LastChar", Object::Integer(lc));
}
let expected_len = (lc - fc + 1).max(0);
match widths_len {
Some(wl) if wl != expected_len => {
let existing = match dict.get(b"Widths").ok() {
Some(Object::Array(arr)) => arr.clone(),
_ => vec![],
};
let mut new_widths = Vec::with_capacity(expected_len as usize);
for i in 0..expected_len as usize {
if i < existing.len() {
new_widths.push(existing[i].clone());
} else {
new_widths.push(Object::Integer(0));
}
}
dict.set("Widths", Object::Array(new_widths));
}
None if expected_len > 0 => {
let widths: Vec<Object> =
(0..expected_len).map(|_| Object::Integer(0)).collect();
dict.set("Widths", Object::Array(widths));
}
_ => {}
}
}
}
}
}
fn ensure_xmp_dc_title(doc: &mut Document) {
let catalog_id = match get_catalog_id(doc) {
Some(id) => id,
None => return,
};
let meta_id = {
if let Some(Object::Dictionary(cat)) = doc.objects.get(&catalog_id) {
match cat.get(b"Metadata").ok() {
Some(Object::Reference(id)) => Some(*id),
_ => None,
}
} else {
None
}
};
let meta_id = match meta_id {
Some(id) => id,
None => return, };
let xmp_bytes = {
if let Some(Object::Stream(stream)) = doc.objects.get(&meta_id) {
stream.content.clone()
} else {
return;
}
};
let xmp_str = String::from_utf8_lossy(&xmp_bytes);
if xmp_str.contains("dc:title") {
return;
}
let insertion_target = "</rdf:RDF>";
let insert_pos = match xmp_str.find(insertion_target) {
Some(pos) => pos,
None => return, };
let dc_title_block = concat!(
"<rdf:Description rdf:about=\"\"\n",
" xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n",
" <dc:title>\n",
" <rdf:Alt>\n",
" <rdf:li xml:lang=\"x-default\">Untitled</rdf:li>\n",
" </rdf:Alt>\n",
" </dc:title>\n",
"</rdf:Description>\n",
);
let mut new_xmp = String::with_capacity(xmp_str.len() + dc_title_block.len());
new_xmp.push_str(&xmp_str[..insert_pos]);
new_xmp.push_str(dc_title_block);
new_xmp.push_str(&xmp_str[insert_pos..]);
let new_bytes = new_xmp.into_bytes();
if let Some(Object::Stream(ref mut stream)) = doc.objects.get_mut(&meta_id) {
stream.set_content(new_bytes);
}
}
fn remove_needs_rendering(doc: &mut Document) {
let catalog_id = match get_catalog_id(doc) {
Some(id) => id,
None => return,
};
if let Some(Object::Dictionary(ref mut catalog)) = doc.objects.get_mut(&catalog_id) {
catalog.remove(b"NeedsRendering");
}
}
fn remove_pressteps(doc: &mut Document) {
let ids: Vec<lopdf::ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
if dict
.get(b"Type")
.ok()
.and_then(|o| {
if let Object::Name(n) = o {
Some(n.as_slice() == b"Page")
} else {
None
}
})
.unwrap_or(false)
{
dict.remove(b"PresSteps");
}
}
}
}
fn remove_alternate_presentations(doc: &mut Document) {
let catalog_id = match get_catalog_id(doc) {
Some(id) => id,
None => return,
};
let names_ref = {
if let Some(Object::Dictionary(ref cat)) = doc.objects.get(&catalog_id) {
match cat.get(b"Names").ok() {
Some(Object::Reference(r)) => Some(*r),
Some(Object::Dictionary(_)) => None, _ => return,
}
} else {
return;
}
};
if let Some(ref_id) = names_ref {
if let Some(Object::Dictionary(ref mut names)) = doc.objects.get_mut(&ref_id) {
names.remove(b"AlternatePresentations");
}
} else if let Some(Object::Dictionary(ref mut cat)) = doc.objects.get_mut(&catalog_id) {
if let Ok(Object::Dictionary(ref mut names)) = cat.get_mut(b"Names") {
names.remove(b"AlternatePresentations");
}
}
}
fn remove_forbidden_annotations(doc: &mut Document) {
let page_ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for page_id in &page_ids {
let (annots_arr, annots_ref_id) = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(page_id) {
match dict.get(b"Annots").ok() {
Some(Object::Array(arr)) => (Some(arr.clone()), None),
Some(Object::Reference(ref_id)) => {
if let Some(Object::Array(arr)) = doc.objects.get(ref_id) {
(Some(arr.clone()), Some(*ref_id))
} else {
(None, None)
}
}
_ => (None, None),
}
} else {
(None, None)
}
};
let Some(arr) = annots_arr else { continue };
let forbidden_refs: Vec<ObjectId> = arr
.iter()
.filter_map(|obj| {
if let Object::Reference(id) = obj {
if let Some(Object::Dictionary(d)) = doc.objects.get(id) {
let is_forbidden = match d.get(b"Subtype").ok() {
Some(Object::Name(ref n)) => {
matches!(n.as_slice(), b"3D" | b"Sound" | b"Screen" | b"Movie")
|| !is_annotation_subtype(n)
}
_ => true,
};
if is_forbidden {
return Some(*id);
}
}
}
None
})
.collect();
if forbidden_refs.is_empty() {
continue;
}
let retain_fn = |obj: &Object| -> bool {
if let Object::Reference(id) = obj {
!forbidden_refs.contains(id)
} else {
true
}
};
if let Some(annots_id) = annots_ref_id {
if let Some(Object::Array(ref mut arr)) = doc.objects.get_mut(&annots_id) {
arr.retain(retain_fn);
}
} else {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(page_id) {
if let Ok(Object::Array(ref mut arr)) = dict.get_mut(b"Annots") {
arr.retain(retain_fn);
}
}
}
}
}
fn fix_file_spec_keys(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let needs_fix = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
matches!(
dict.get(b"Type").ok(),
Some(Object::Name(ref n)) if n.eq_ignore_ascii_case(b"Filespec")
) && (!dict.has(b"F") || !dict.has(b"UF"))
} else {
false
}
};
if needs_fix {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
let name = dict
.get(b"F")
.ok()
.or_else(|| dict.get(b"UF").ok())
.cloned()
.unwrap_or_else(|| {
Object::String(b"attachment".to_vec(), lopdf::StringFormat::Literal)
});
if !dict.has(b"F") {
dict.set("F", name.clone());
}
if !dict.has(b"UF") {
dict.set("UF", name);
}
}
}
}
}
fn strip_ef_from_file_specs(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let needs_strip = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
matches!(
dict.get(b"Type").ok(),
Some(Object::Name(ref n)) if n.eq_ignore_ascii_case(b"Filespec")
) && dict.has(b"EF")
} else {
false
}
};
if needs_strip {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
dict.remove(b"EF");
}
}
}
}
fn strip_filespec_type(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let is_filespec = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
matches!(
dict.get(b"Type").ok(),
Some(Object::Name(ref n)) if n.eq_ignore_ascii_case(b"Filespec")
)
} else {
false
}
};
if is_filespec {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
dict.remove(b"Type");
}
}
}
}
fn neutralize_filespec_by_structure(doc: &mut Document) {
const FILESPEC_KEYS: &[&[u8]] = &[b"UF", b"DOS", b"Mac", b"Unix", b"FS"];
fn is_filespec_by_structure(dict: &lopdf::Dictionary) -> bool {
if matches!(
dict.get(b"Type").ok(),
Some(Object::Name(ref n)) if n.eq_ignore_ascii_case(b"Filespec")
) {
return false;
}
dict.has(b"F") && FILESPEC_KEYS.iter().any(|k| dict.has(k))
}
fn strip_filespec_keys(dict: &mut lopdf::Dictionary) {
for &key in FILESPEC_KEYS {
dict.remove(key);
}
dict.remove(b"F");
dict.remove(b"EF");
}
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
match doc.objects.get_mut(&id) {
Some(Object::Dictionary(dict)) => {
if is_filespec_by_structure(dict) {
strip_filespec_keys(dict);
}
if let Ok(Object::Dictionary(ref mut fs_dict)) = dict.get_mut(b"FS") {
if is_filespec_by_structure(fs_dict) {
strip_filespec_keys(fs_dict);
}
}
}
Some(Object::Stream(s)) if is_filespec_by_structure(&s.dict) => {
strip_filespec_keys(&mut s.dict);
}
_ => {}
}
}
}
fn strip_stream_external_ref_keys(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let is_embedded = {
if let Some(Object::Stream(ref s)) = doc.objects.get(&id) {
matches!(
s.dict.get(b"Type").ok(),
Some(Object::Name(ref n)) if n == b"EmbeddedFile"
)
} else {
false
}
};
if is_embedded {
continue;
}
if let Some(Object::Stream(ref mut s)) = doc.objects.get_mut(&id) {
s.dict.remove(b"F");
s.dict.remove(b"FFilter");
s.dict.remove(b"FDecodeParms");
}
}
}
fn remove_ocg_as_key(doc: &mut Document) {
let catalog_id = match get_catalog_id(doc) {
Some(id) => id,
None => return,
};
let ocprops_id = {
if let Some(Object::Dictionary(catalog)) = doc.objects.get(&catalog_id) {
match catalog.get(b"OCProperties").ok() {
Some(Object::Reference(id)) => Some(*id),
_ => None,
}
} else {
None
}
};
let ocprops_id = match ocprops_id {
Some(id) => id,
None => return,
};
let d_id = {
if let Some(Object::Dictionary(ocprops)) = doc.objects.get(&ocprops_id) {
match ocprops.get(b"D").ok() {
Some(Object::Reference(id)) => Some(*id),
_ => None,
}
} else {
None
}
};
if let Some(d_id) = d_id {
if let Some(Object::Dictionary(ref mut d)) = doc.objects.get_mut(&d_id) {
d.remove(b"AS");
}
} else if let Some(Object::Dictionary(ref mut ocprops)) = doc.objects.get_mut(&ocprops_id) {
if let Ok(Object::Dictionary(ref mut d)) = ocprops.get_mut(b"D") {
d.remove(b"AS");
}
}
let config_ids: Vec<ObjectId> = {
if let Some(Object::Dictionary(ocprops)) = doc.objects.get(&ocprops_id) {
match ocprops.get(b"Configs").ok() {
Some(Object::Array(arr)) => arr
.iter()
.filter_map(|o| {
if let Object::Reference(id) = o {
Some(*id)
} else {
None
}
})
.collect(),
_ => Vec::new(),
}
} else {
Vec::new()
}
};
for cfg_id in config_ids {
if let Some(Object::Dictionary(ref mut cfg)) = doc.objects.get_mut(&cfg_id) {
cfg.remove(b"AS");
}
}
}
fn truncate_long_names(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let dict_ref = match doc.objects.get(&id) {
Some(Object::Dictionary(d)) => Some(d),
Some(Object::Stream(s)) => Some(&s.dict),
_ => None,
};
let long_val_keys: Vec<Vec<u8>> = if let Some(dict) = dict_ref {
dict.iter()
.filter_map(|(key, val)| {
if let Object::Name(n) = val {
if n.len() > 127 {
return Some(key.clone());
}
}
None
})
.collect()
} else {
vec![]
};
for key in long_val_keys {
let truncated = match doc.objects.get(&id) {
Some(Object::Dictionary(d)) => d.get(&key).ok().and_then(|v| {
if let Object::Name(n) = v {
Some(n[..127].to_vec())
} else {
None
}
}),
Some(Object::Stream(s)) => s.dict.get(&key).ok().and_then(|v| {
if let Object::Name(n) = v {
Some(n[..127].to_vec())
} else {
None
}
}),
_ => None,
};
if let Some(truncated) = truncated {
let key_str = std::str::from_utf8(&key).unwrap_or("?");
match doc.objects.get_mut(&id) {
Some(Object::Dictionary(ref mut d)) => {
d.set(key_str, Object::Name(truncated));
}
Some(Object::Stream(ref mut s)) => {
s.dict.set(key_str, Object::Name(truncated));
}
_ => {}
}
}
}
let dict_ref2 = match doc.objects.get(&id) {
Some(Object::Dictionary(d)) => Some(d),
Some(Object::Stream(s)) => Some(&s.dict),
_ => None,
};
let long_keys: Vec<(Vec<u8>, Object)> = if let Some(dict) = dict_ref2 {
dict.iter()
.filter(|(key, _)| key.len() > 127)
.map(|(key, val)| (key.clone(), val.clone()))
.collect()
} else {
vec![]
};
if !long_keys.is_empty() {
for (old_key, val) in &long_keys {
let truncated_key = &old_key[..127];
let truncated_key_str = std::str::from_utf8(truncated_key).unwrap_or("truncated");
match doc.objects.get_mut(&id) {
Some(Object::Dictionary(ref mut d)) => {
d.remove(old_key.as_slice());
d.set(truncated_key_str, val.clone());
}
Some(Object::Stream(ref mut s)) => {
s.dict.remove(old_key.as_slice());
s.dict.set(truncated_key_str, val.clone());
}
_ => {}
}
}
}
}
}
fn fix_soft_mask_colorspace(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let needs_fix = {
if let Some(Object::Stream(stream)) = doc.objects.get(&id) {
let is_image = matches!(
stream.dict.get(b"Subtype").ok(),
Some(Object::Name(ref n)) if n == b"Image"
);
if !is_image {
false
} else {
let has_matte = stream.dict.has(b"Matte");
let cs_not_gray = match stream.dict.get(b"ColorSpace").ok() {
Some(Object::Name(ref n)) => n != b"DeviceGray",
None => false, _ => true,
};
has_matte && cs_not_gray
}
} else {
false
}
};
if needs_fix {
if let Some(Object::Stream(ref mut stream)) = doc.objects.get_mut(&id) {
stream
.dict
.set("ColorSpace", Object::Name(b"DeviceGray".to_vec()));
}
}
}
let smask_ids: Vec<ObjectId> = {
let mut ids_out = Vec::new();
for obj in doc.objects.values() {
if let Object::Stream(stream) = obj {
if let Ok(Object::Reference(smask_id)) = stream.dict.get(b"SMask") {
ids_out.push(*smask_id);
}
}
}
ids_out
};
for smask_id in smask_ids {
let needs_fix = {
if let Some(Object::Stream(stream)) = doc.objects.get(&smask_id) {
match stream.dict.get(b"ColorSpace").ok() {
Some(Object::Name(ref n)) => n != b"DeviceGray",
_ => false,
}
} else {
false
}
};
if needs_fix {
if let Some(Object::Stream(ref mut stream)) = doc.objects.get_mut(&smask_id) {
stream
.dict
.set("ColorSpace", Object::Name(b"DeviceGray".to_vec()));
}
}
}
}
fn ensure_page_transparency_group(doc: &mut Document) {
let page_ids: Vec<ObjectId> = doc.get_pages().values().copied().collect();
for page_id in page_ids {
let has_group = matches!(
doc.objects.get(&page_id),
Some(Object::Dictionary(d)) if d.has(b"Group")
);
if has_group {
continue;
}
let uses_transparency = page_uses_transparency_lopdf(doc, page_id);
if !uses_transparency {
continue;
}
if let Some(Object::Dictionary(ref mut page_dict)) = doc.objects.get_mut(&page_id) {
let mut group = lopdf::Dictionary::new();
group.set("Type", Object::Name(b"Group".to_vec()));
group.set("S", Object::Name(b"Transparency".to_vec()));
group.set("CS", Object::Name(b"DeviceRGB".to_vec()));
page_dict.set("Group", Object::Dictionary(group));
}
}
}
fn page_uses_transparency_lopdf(doc: &Document, page_id: ObjectId) -> bool {
let page_dict = match doc.objects.get(&page_id) {
Some(Object::Dictionary(d)) => d,
_ => return false,
};
let resources = match page_dict.get(b"Resources").ok() {
Some(Object::Dictionary(d)) => d.clone(),
Some(Object::Reference(id)) => match doc.objects.get(id) {
Some(Object::Dictionary(d)) => d.clone(),
_ => return false,
},
_ => return false,
};
if let Ok(gs_obj) = resources.get(b"ExtGState") {
let gs_dict = match gs_obj {
Object::Dictionary(d) => Some(d.clone()),
Object::Reference(id) => match doc.objects.get(id) {
Some(Object::Dictionary(d)) => Some(d.clone()),
_ => None,
},
_ => None,
};
if let Some(gs_dict) = gs_dict {
for (_, val) in gs_dict.iter() {
let gs = match val {
Object::Dictionary(d) => d,
Object::Reference(id) => match doc.objects.get(id) {
Some(Object::Dictionary(d)) => d,
_ => continue,
},
_ => continue,
};
if let Ok(smask) = gs.get(b"SMask") {
match smask {
Object::Name(n) if n == b"None" => {}
Object::Name(_) | Object::Dictionary(_) | Object::Reference(_) => {
return true;
}
_ => {}
}
}
if let Ok(Object::Name(bm)) = gs.get(b"BM") {
if bm != b"Normal" && bm != b"Compatible" {
return true;
}
}
if let Ok(ca_obj) = gs.get(b"CA") {
if let Some(v) = obj_as_f64(ca_obj) {
if v < 1.0 {
return true;
}
}
}
if let Ok(ca_obj) = gs.get(b"ca") {
if let Some(v) = obj_as_f64(ca_obj) {
if v < 1.0 {
return true;
}
}
}
}
}
}
if let Ok(xobj_obj) = resources.get(b"XObject") {
let xobj_dict = match xobj_obj {
Object::Dictionary(d) => Some(d.clone()),
Object::Reference(id) => match doc.objects.get(id) {
Some(Object::Dictionary(d)) => Some(d.clone()),
_ => None,
},
_ => None,
};
if let Some(xobj_dict) = xobj_dict {
for (_, val) in xobj_dict.iter() {
let stream_dict = match val {
Object::Reference(id) => match doc.objects.get(id) {
Some(Object::Stream(s)) => &s.dict,
_ => continue,
},
_ => continue,
};
let subtype = stream_dict.get(b"Subtype").ok();
let is_image = matches!(subtype, Some(Object::Name(ref n)) if n == b"Image");
let is_form = matches!(subtype, Some(Object::Name(ref n)) if n == b"Form");
if is_image && stream_dict.has(b"SMask") {
return true;
}
if is_form {
if let Ok(Object::Dictionary(group)) = stream_dict.get(b"Group") {
if matches!(group.get(b"S").ok(), Some(Object::Name(ref n)) if n == b"Transparency")
{
return true;
}
}
}
}
}
}
false
}
fn obj_as_f64(obj: &Object) -> Option<f64> {
match obj {
Object::Real(v) => Some(*v as f64),
Object::Integer(v) => Some(*v as f64),
_ => None,
}
}
fn fix_markinfo_without_structtreeroot(doc: &mut Document) {
let catalog_id = match get_catalog_id(doc) {
Some(id) => id,
None => return,
};
let (has_valid_structtreeroot, markinfo_id) = {
let catalog = match doc.objects.get(&catalog_id) {
Some(Object::Dictionary(d)) => d,
_ => return,
};
let has_valid_str = match catalog.get(b"StructTreeRoot").ok() {
Some(Object::Dictionary(_)) => true,
Some(Object::Reference(id)) => {
matches!(doc.objects.get(id), Some(Object::Dictionary(_)))
}
_ => false,
};
let mi_id = match catalog.get(b"MarkInfo").ok() {
Some(Object::Reference(id)) => Some(*id),
_ => None,
};
(has_valid_str, mi_id)
};
if has_valid_structtreeroot {
return; }
if let Some(Object::Dictionary(ref mut catalog)) = doc.objects.get_mut(&catalog_id) {
if let Ok(Object::Dictionary(ref mi)) = catalog.get(b"MarkInfo") {
if matches!(mi.get(b"Marked").ok(), Some(Object::Boolean(true))) {
catalog.remove(b"MarkInfo");
}
}
}
if let Some(mi_id) = markinfo_id {
let should_remove = matches!(
doc.objects.get(&mi_id),
Some(Object::Dictionary(d)) if matches!(d.get(b"Marked").ok(), Some(Object::Boolean(true)))
);
if should_remove {
if let Some(Object::Dictionary(ref mut catalog)) = doc.objects.get_mut(&catalog_id) {
catalog.remove(b"MarkInfo");
}
}
}
if let Some(Object::Dictionary(ref mut catalog)) = doc.objects.get_mut(&catalog_id) {
if catalog.has(b"StructTreeRoot") {
catalog.remove(b"StructTreeRoot");
}
}
}
fn strip_forbidden_smask(doc: &mut Document, is_pdfa1: bool) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let should_strip = {
if let Some(Object::Stream(stream)) = doc.objects.get(&id) {
let subtype = stream.dict.get(b"Subtype").and_then(Object::as_name).ok();
(subtype == Some(b"Form") || (is_pdfa1 && subtype == Some(b"Image")))
&& stream.dict.has(b"SMask")
} else {
false
}
};
if should_strip {
if let Some(Object::Stream(ref mut stream)) = doc.objects.get_mut(&id) {
stream.dict.remove(b"SMask");
}
}
}
}
pub fn fix_unbalanced_emc(doc: &mut Document) {
let page_ids: Vec<ObjectId> = doc.get_pages().values().copied().collect();
let mut page_groups: Vec<(ObjectId, Vec<ObjectId>)> = Vec::new();
for &page_id in &page_ids {
let Some(Object::Dictionary(page)) = doc.objects.get(&page_id) else {
continue;
};
let mut ids = Vec::new();
match page.get(b"Contents").ok() {
Some(Object::Reference(id)) => {
match doc.objects.get(id) {
Some(Object::Array(arr)) => {
for item in arr {
if let Object::Reference(sid) = item {
ids.push(*sid);
}
}
}
_ => ids.push(*id),
}
}
Some(Object::Array(arr)) => {
for item in arr {
if let Object::Reference(id) = item {
ids.push(*id);
}
}
}
_ => {}
}
if !ids.is_empty() {
page_groups.push((page_id, ids));
}
}
for (page_id, ids) in page_groups {
let mut combined = Vec::new();
for &id in &ids {
let Some(Object::Stream(s)) = doc.objects.get(&id) else {
continue;
};
let Ok(c) = s.get_plain_content() else {
continue;
};
if !combined.is_empty() {
combined.push(b'\n');
combined.push(b'\n');
}
combined.extend_from_slice(&c);
}
let fixed = fix_emc_in_bytes(&combined);
if fixed == combined {
continue;
}
if ids.len() == 1 {
if let Some(Object::Stream(ref mut s)) = doc.objects.get_mut(&ids[0]) {
s.set_plain_content(fixed);
}
} else {
let new_stream = lopdf::Stream::new(lopdf::dictionary! {}, fixed);
let new_id = doc.add_object(Object::Stream(new_stream));
if let Some(Object::Dictionary(ref mut page)) = doc.objects.get_mut(&page_id) {
page.set("Contents", Object::Reference(new_id));
}
}
}
let form_and_pattern_ids: Vec<ObjectId> = doc
.objects
.iter()
.filter_map(|(id, obj)| {
if let Object::Stream(s) = obj {
let is_form =
s.dict.get(b"Subtype").ok().and_then(|o| o.as_name().ok()) == Some(b"Form");
let is_pattern = s
.dict
.get(b"PatternType")
.ok()
.and_then(|o| o.as_i64().ok())
== Some(1);
if is_form || is_pattern {
Some(*id)
} else {
None
}
} else {
None
}
})
.collect();
let mut visited: std::collections::HashSet<ObjectId> =
form_and_pattern_ids.iter().copied().collect();
let mut queue: Vec<ObjectId> = form_and_pattern_ids.clone();
while let Some(form_id) = queue.pop() {
let xobject_refs = collect_xobject_refs_from_form(doc, form_id);
for ref_id in xobject_refs {
if visited.insert(ref_id) {
queue.push(ref_id);
}
}
}
for id in visited {
let content = {
let Some(Object::Stream(s)) = doc.objects.get(&id) else {
continue;
};
match s.get_plain_content() {
Ok(c) => c,
Err(_) => continue,
}
};
let fixed = fix_emc_in_bytes(&content);
if fixed != content {
if let Some(Object::Stream(ref mut s)) = doc.objects.get_mut(&id) {
s.set_plain_content(fixed);
}
}
}
}
fn collect_xobject_refs_from_form(doc: &Document, form_id: ObjectId) -> Vec<ObjectId> {
let mut refs = Vec::new();
let content = match doc.objects.get(&form_id) {
Some(Object::Stream(s)) => match s.get_plain_content() {
Ok(c) => c,
Err(_) => return refs,
},
_ => return refs,
};
let resources = match doc.objects.get(&form_id) {
Some(Object::Stream(s)) => s.dict.get(b"Resources").ok().cloned(),
_ => None,
};
let mut i = 0;
while i < content.len() {
if content[i].is_ascii_whitespace() {
i += 1;
continue;
}
if content[i] == b'%' {
while i < content.len() && content[i] != b'\n' && content[i] != b'\r' {
i += 1;
}
continue;
}
if content[i] == b'(' {
let Some(eind) = end_of_literal_string(&content, i) else {
break;
};
i = eind;
continue;
}
if content[i] == b'<' && content.get(i + 1) != Some(&b'<') {
i += 1;
while i < content.len() && content[i] != b'>' {
i += 1;
}
if i < content.len() {
i += 1;
}
continue;
}
let tok_start = i;
while i < content.len()
&& !content[i].is_ascii_whitespace()
&& content[i] != b'('
&& content[i] != b'<'
&& content[i] != b'>'
&& content[i] != b'/'
&& content[i] != b'%'
&& content[i] != b'['
&& content[i] != b']'
{
i += 1;
}
if tok_start == i {
i += 1;
continue;
}
let token = &content[tok_start..i];
if token == b"Do" {
while i < content.len() && content[i].is_ascii_whitespace() {
i += 1;
}
let name_start = i;
while i < content.len()
&& !content[i].is_ascii_whitespace()
&& content[i] != b'('
&& content[i] != b'<'
&& content[i] != b'>'
&& content[i] != b'/'
&& content[i] != b'%'
&& content[i] != b'['
&& content[i] != b']'
{
i += 1;
}
if name_start < i {
let name_tok = &content[name_start..i];
if let Some(name) = name_tok.strip_prefix(b"/") {
if let Some(ref resources) = resources {
if let Some(xobj_ref) = lookup_xobject_in_resources(doc, resources, name) {
if let Some(Object::Stream(s)) = doc.objects.get(&xobj_ref) {
if s.dict.get(b"Subtype").ok().and_then(|o| o.as_name().ok())
== Some(b"Form")
{
refs.push(xobj_ref);
}
}
}
}
}
}
}
}
refs
}
fn lookup_xobject_in_resources(
doc: &Document,
resources: &Object,
name: &[u8],
) -> Option<ObjectId> {
let xobjects = match resources {
Object::Dictionary(dict) => dict.get(b"XObject").ok().cloned(),
Object::Reference(r) => doc.objects.get(r).cloned(),
_ => None,
};
let xobjects = xobjects?;
match xobjects {
Object::Dictionary(dict) => {
if let Ok(Object::Reference(ref_id)) = dict.get(name) {
Some(*ref_id)
} else if let Ok(Object::Name(_)) = dict.get(name) {
None
} else {
None
}
}
Object::Reference(r) => {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&r) {
if let Ok(Object::Reference(ref_id)) = dict.get(name) {
Some(*ref_id)
} else {
None
}
} else {
None
}
}
_ => None,
}
}
fn fix_emc_in_bytes(data: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(data.len());
let mut i = 0;
let mut depth: i32 = 0;
while i < data.len() {
if data[i].is_ascii_whitespace() {
out.push(data[i]);
i += 1;
continue;
}
if data[i] == b'%' {
while i < data.len() && data[i] != b'\n' && data[i] != b'\r' {
out.push(data[i]);
i += 1;
}
continue;
}
if data[i] == b'(' {
let start = i;
let Some(eind) = end_of_literal_string(data, i) else {
out.extend_from_slice(&data[start..]);
break;
};
i = eind;
out.extend_from_slice(&data[start..i]);
continue;
}
if data[i] == b'<' && data.get(i + 1) != Some(&b'<') {
let start = i;
i += 1;
while i < data.len() && data[i] != b'>' {
i += 1;
}
if i < data.len() {
i += 1;
}
out.extend_from_slice(&data[start..i]);
continue;
}
if data[i] == b'>' {
while i < data.len() && data[i] == b'>' {
out.push(data[i]);
i += 1;
}
continue;
}
let tok_start = i;
while i < data.len()
&& !data[i].is_ascii_whitespace()
&& data[i] != b'('
&& data[i] != b'<'
&& data[i] != b'>'
&& data[i] != b'/'
&& data[i] != b'%'
&& data[i] != b'['
&& data[i] != b']'
{
i += 1;
}
if tok_start == i {
out.push(data[i]);
i += 1;
continue;
}
let token = &data[tok_start..i];
if token == b"BMC" || token == b"BDC" {
depth += 1;
out.extend_from_slice(token);
} else if token == b"EMC" {
if depth > 0 {
depth -= 1;
out.extend_from_slice(token);
}
} else {
out.extend_from_slice(token);
}
}
while depth > 0 {
out.extend_from_slice(b"\nEMC");
depth -= 1;
}
out
}
fn remove_halftone_names(doc: &mut Document) {
fn is_halftone_dict(dict: &lopdf::Dictionary) -> bool {
dict.has(b"HalftoneType")
|| matches!(
dict.get(b"Type").ok(),
Some(Object::Name(ref n)) if n == b"Halftone"
)
}
fn clean_halftone_dict(dict: &mut lopdf::Dictionary) {
dict.remove(b"HalftoneName");
dict.remove(b"TransferFunction");
}
let allowed_type5_keys: &[&[u8]] = &[
b"Type",
b"HalftoneType",
b"Cyan",
b"Magenta",
b"Yellow",
b"Black",
b"Default",
];
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let dict = match doc.objects.get(&id) {
Some(Object::Dictionary(d)) => Some(d),
Some(Object::Stream(s)) => Some(&s.dict),
_ => None,
};
let needs_fix = dict
.map(|d| is_halftone_dict(d) && (d.has(b"HalftoneName") || d.has(b"TransferFunction")))
.unwrap_or(false);
if needs_fix {
match doc.objects.get_mut(&id) {
Some(Object::Dictionary(ref mut d)) => clean_halftone_dict(d),
Some(Object::Stream(ref mut s)) => clean_halftone_dict(&mut s.dict),
_ => {}
}
}
let is_type5 = match doc.objects.get(&id) {
Some(Object::Dictionary(d)) => {
is_halftone_dict(d)
&& matches!(d.get(b"HalftoneType").ok(), Some(Object::Integer(5)))
}
_ => false,
};
if is_type5 {
let remove_keys: Vec<Vec<u8>> = match doc.objects.get(&id) {
Some(Object::Dictionary(d)) => d
.iter()
.filter_map(|(k, _)| {
if !allowed_type5_keys.contains(&k.as_slice()) {
Some(k.clone())
} else {
None
}
})
.collect(),
_ => Vec::new(),
};
if !remove_keys.is_empty() {
if let Some(Object::Dictionary(ref mut d)) = doc.objects.get_mut(&id) {
for key in remove_keys {
d.remove(&key);
}
}
}
}
let inline_keys: Vec<Vec<u8>> = match doc.objects.get(&id) {
Some(Object::Dictionary(d)) if is_halftone_dict(d) => d
.iter()
.filter_map(|(k, v)| {
if let Object::Dictionary(sub) = v {
if is_halftone_dict(sub)
&& (sub.has(b"HalftoneName") || sub.has(b"TransferFunction"))
{
return Some(k.clone());
}
}
None
})
.collect(),
_ => Vec::new(),
};
for key in inline_keys {
if let Some(Object::Dictionary(ref mut d)) = doc.objects.get_mut(&id) {
if let Ok(Object::Dictionary(ref mut sub)) = d.get_mut(&key) {
clean_halftone_dict(sub);
}
}
}
}
}
fn ensure_page_resources(doc: &mut Document) {
let page_ids: Vec<ObjectId> = doc.get_pages().values().copied().collect();
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in &page_ids {
let needs_resources = matches!(
doc.objects.get(id),
Some(Object::Dictionary(d)) if !d.has(b"Resources")
);
if needs_resources {
if let Some(Object::Dictionary(ref mut d)) = doc.objects.get_mut(id) {
d.set("Resources", Object::Dictionary(lopdf::Dictionary::new()));
}
}
}
for id in ids {
let needs_resources = match doc.objects.get(&id) {
Some(Object::Stream(s)) => {
matches!(
s.dict.get(b"Subtype").ok(),
Some(Object::Name(ref n)) if n == b"Form"
) && !s.dict.has(b"Resources")
}
_ => false,
};
if needs_resources {
if let Some(Object::Stream(ref mut s)) = doc.objects.get_mut(&id) {
s.dict
.set("Resources", Object::Dictionary(lopdf::Dictionary::new()));
}
}
}
}
fn strip_signatures(doc: &mut Document) {
let catalog_id = match get_catalog_id(doc) {
Some(id) => id,
None => return,
};
if let Some(Object::Dictionary(ref mut catalog)) = doc.objects.get_mut(&catalog_id) {
catalog.remove(b"Perms");
}
let acroform_id = {
if let Some(Object::Dictionary(catalog)) = doc.objects.get(&catalog_id) {
match catalog.get(b"AcroForm").ok() {
Some(Object::Reference(id)) => Some(*id),
_ => None,
}
} else {
None
}
};
if let Some(af_id) = acroform_id {
if let Some(Object::Dictionary(ref mut af)) = doc.objects.get_mut(&af_id) {
af.remove(b"SigFlags");
}
}
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let is_sig_field = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
matches!(
dict.get(b"FT").ok(),
Some(Object::Name(ref n)) if n == b"Sig"
) && dict.has(b"V")
} else {
false
}
};
if is_sig_field {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
dict.remove(b"V");
}
}
let is_sig_dict = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
matches!(
dict.get(b"Type").ok(),
Some(Object::Name(ref n)) if n == b"Sig"
) && (dict.has(b"ByteRange") || dict.has(b"Contents"))
} else {
false
}
};
if is_sig_dict {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
dict.remove(b"ByteRange");
dict.remove(b"Contents");
dict.remove(b"Filter");
dict.remove(b"SubFilter");
}
}
}
}
pub fn fix_pdf_header(data: &mut Vec<u8>) {
let mut shift: i64 = 0;
if data.len() >= 9 && &data[0..5] == b"%PDF-" {
if data[5] == b'2' {
data[5] = b'1';
data[7] = b'7';
}
if data[5] == b'1' && data[7] == b'8' {
data[7] = b'7';
}
if data[5] == b'1' && data[7] < b'4' {
data[7] = b'4';
}
if data[6] != b'.' || !data[5].is_ascii_digit() || !data[7].is_ascii_digit() {
data[5] = b'1';
data[6] = b'.';
data[7] = b'7';
}
if data.len() > 8 && data[8] != b'\n' && data[8] != b'\r' {
if let Some(eol_pos) = data[8..].iter().position(|&b| b == b'\n' || b == b'\r') {
let eol_pos = eol_pos + 8;
let removed = eol_pos - 8;
data.drain(8..eol_pos);
shift -= removed as i64;
}
}
}
if let Some(pos) = data.iter().position(|&b| b == b'\n') {
let next = pos + 1;
if next < data.len() && data[next] == b'%' {
let end = data[next..]
.iter()
.position(|&b| b == b'\n' || b == b'\r')
.unwrap_or(5);
let has_high = data[next..next + end]
.iter()
.filter(|&&b| b >= 0x80)
.count()
>= 4;
if has_high {
if shift != 0 {
adjust_xref_offsets(data, shift);
}
return;
}
}
let comment = b"\x25\xe2\xe3\xcf\xd3\x0a";
data.splice(next..next, comment.iter().copied());
shift += comment.len() as i64;
}
if shift != 0 {
adjust_xref_offsets(data, shift);
}
}
pub fn fix_startxref(data: &mut Vec<u8>) {
fix_xref_eol(data);
let Some(sx_pos) = find_last(data, b"startxref") else {
return;
};
let val_start = sx_pos + b"startxref".len();
let val_start = data[val_start..]
.iter()
.position(|b| b.is_ascii_digit())
.map(|p| val_start + p)
.unwrap_or(val_start);
let val_end = data[val_start..]
.iter()
.position(|b| !b.is_ascii_digit())
.map(|p| val_start + p)
.unwrap_or(data.len());
let current_sx: usize = std::str::from_utf8(&data[val_start..val_end])
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
if current_sx < data.len()
&& data.len() - current_sx >= 4
&& &data[current_sx..current_sx + 4] == b"xref"
{
return; }
let search_end = sx_pos.min(data.len());
if let Some(real_xref) = find_last(&data[..search_end], b"xref") {
let is_standalone =
real_xref == 0 || data[real_xref - 1] == b'\n' || data[real_xref - 1] == b'\r';
if is_standalone {
let new_sx_bytes = real_xref.to_string().into_bytes();
data.splice(val_start..val_end, new_sx_bytes);
}
}
}
fn adjust_xref_offsets(data: &mut Vec<u8>, shift: i64) {
let Some(sx_pos) = find_last(data, b"startxref") else {
return;
};
let val_start = sx_pos + b"startxref".len();
let val_start = data[val_start..]
.iter()
.position(|b| b.is_ascii_digit())
.map(|p| val_start + p)
.unwrap_or(val_start);
let val_end = data[val_start..]
.iter()
.position(|b| !b.is_ascii_digit())
.map(|p| val_start + p)
.unwrap_or(data.len());
let old_sx: i64 = std::str::from_utf8(&data[val_start..val_end])
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let new_sx = old_sx + shift;
let new_sx_bytes = new_sx.to_string().into_bytes();
data.splice(val_start..val_end, new_sx_bytes);
let xref_start = if new_sx >= 0 { new_sx as usize } else { return };
if xref_start >= data.len()
|| &data[xref_start..xref_start + 4.min(data.len() - xref_start)] != b"xref"
{
return;
}
let mut pos = xref_start + 4;
while pos < data.len() {
while pos < data.len() && (data[pos] == b'\n' || data[pos] == b'\r' || data[pos] == b' ') {
pos += 1;
}
if pos >= data.len() || data[pos] == b't' {
break;
}
let line_end = data[pos..]
.iter()
.position(|&b| b == b'\n' || b == b'\r')
.map(|p| pos + p)
.unwrap_or(data.len());
let line = &data[pos..line_end];
let parts: Vec<&[u8]> = line.split(|&b| b == b' ').collect();
let count: usize = if parts.len() >= 2 {
std::str::from_utf8(parts[1])
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(0)
} else {
break;
};
pos = line_end;
while pos < data.len() && (data[pos] == b'\n' || data[pos] == b'\r') {
pos += 1;
}
for _ in 0..count {
if pos + 20 > data.len() {
break;
}
let flag = data[pos + 17];
if flag == b'n' {
let old_offset: i64 = std::str::from_utf8(&data[pos..pos + 10])
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let new_offset = old_offset + shift;
let formatted = format!("{:010}", new_offset);
data[pos..pos + 10].copy_from_slice(formatted.as_bytes());
}
pos += 20;
}
}
}
fn find_last(data: &[u8], needle: &[u8]) -> Option<usize> {
if needle.len() > data.len() {
return None;
}
for i in (0..=data.len() - needle.len()).rev() {
if &data[i..i + needle.len()] == needle {
return Some(i);
}
}
None
}
fn is_javascript_action(dict: &lopdf::Dictionary) -> bool {
match dict.get(b"S").ok() {
Some(Object::Name(s)) => s == b"JavaScript",
_ => false,
}
}
fn get_catalog_id(doc: &Document) -> Option<ObjectId> {
match doc.trailer.get(b"Root").ok()? {
Object::Reference(id) => Some(*id),
_ => None,
}
}
fn count_name_tree_entries(doc: &Document, tree_id: ObjectId, depth: usize) -> usize {
if depth > 64 {
return 0;
}
if let Some(Object::Dictionary(tree)) = doc.objects.get(&tree_id) {
if let Ok(Object::Array(names)) = tree.get(b"Names") {
return names.len() / 2;
}
if let Ok(Object::Array(kids)) = tree.get(b"Kids") {
return kids
.iter()
.map(|kid| {
if let Object::Reference(kid_id) = kid {
count_name_tree_entries(doc, *kid_id, depth + 1)
} else {
0
}
})
.sum();
}
}
0
}
fn strip_non_catalog_metadata(doc: &mut Document) {
let catalog_meta_id: Option<ObjectId> =
get_catalog_id(doc).and_then(|cat_id| match doc.objects.get(&cat_id) {
Some(Object::Dictionary(cat)) => match cat.get(b"Metadata").ok() {
Some(Object::Reference(id)) => Some(*id),
_ => None,
},
_ => None,
});
let catalog_id = get_catalog_id(doc);
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
if catalog_id == Some(id) {
continue;
}
strip_metadata_recursive(doc, id);
}
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
if catalog_meta_id == Some(id) {
continue;
}
let is_metadata_stream = matches!(
doc.objects.get(&id),
Some(Object::Stream(s)) if {
let t = s.dict.get(b"Type").ok();
let st = s.dict.get(b"Subtype").ok();
matches!(t, Some(Object::Name(ref n)) if n == b"Metadata")
&& matches!(st, Some(Object::Name(ref n)) if n == b"XML")
}
);
if is_metadata_stream {
if let Some(Object::Stream(ref mut s)) = doc.objects.get_mut(&id) {
s.content = Vec::new();
s.dict.set("Length", Object::Integer(0));
}
}
}
}
fn strip_metadata_recursive(doc: &mut Document, id: ObjectId) {
let has_metadata = matches!(
doc.objects.get(&id),
Some(Object::Dictionary(d)) if d.has(b"Metadata")
) || matches!(
doc.objects.get(&id),
Some(Object::Stream(s)) if s.dict.has(b"Metadata")
);
if has_metadata {
match doc.objects.get_mut(&id) {
Some(Object::Dictionary(ref mut d)) => {
d.remove(b"Metadata");
}
Some(Object::Stream(ref mut s)) => {
s.dict.remove(b"Metadata");
}
_ => {}
}
}
let dict_to_scan = match doc.objects.get(&id) {
Some(Object::Dictionary(d)) => Some(d.clone()),
Some(Object::Stream(s)) => Some(s.dict.clone()),
_ => None,
};
if let Some(dict) = dict_to_scan {
for (_key, value) in dict.iter() {
strip_metadata_from_value(doc, id, value);
}
}
}
fn strip_metadata_from_value(doc: &mut Document, parent_id: ObjectId, value: &Object) {
match value {
Object::Dictionary(d) => {
if d.has(b"Metadata") {
strip_metadata_in_nested_dicts(doc, parent_id);
}
for (_k, v) in d.iter() {
strip_metadata_from_value(doc, parent_id, v);
}
}
Object::Array(arr) => {
for v in arr {
strip_metadata_from_value(doc, parent_id, v);
}
}
_ => {}
}
}
fn strip_metadata_in_nested_dicts(doc: &mut Document, obj_id: ObjectId) {
fn strip_from_dict(dict: &mut lopdf::Dictionary) {
dict.remove(b"Metadata");
let keys: Vec<Vec<u8>> = dict.iter().map(|(k, _)| k.clone()).collect();
for key in keys {
match dict.get_mut(&key) {
Ok(Object::Dictionary(ref mut d)) => strip_from_dict(d),
Ok(Object::Array(ref mut arr)) => {
for item in arr.iter_mut() {
if let Object::Dictionary(ref mut d) = item {
strip_from_dict(d);
}
}
}
_ => {}
}
}
}
match doc.objects.get_mut(&obj_id) {
Some(Object::Dictionary(ref mut d)) => strip_from_dict(d),
Some(Object::Stream(ref mut s)) => strip_from_dict(&mut s.dict),
_ => {}
}
}
fn promote_inline_fonts(doc: &mut Document) {
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let inline_fonts: Vec<(Vec<u8>, lopdf::Dictionary)> = {
let font_dict = match doc.objects.get(&id) {
Some(Object::Dictionary(d)) => get_font_dict_inline(d),
Some(Object::Stream(s)) => get_font_dict_inline(&s.dict),
_ => None,
};
let Some(fd) = font_dict else { continue };
fd.iter()
.filter_map(|(name, val)| {
if let Object::Dictionary(d) = val {
if d.has(b"BaseFont") || d.has(b"Subtype") {
Some((name.clone(), d.clone()))
} else {
None
}
} else {
None
}
})
.collect()
};
if inline_fonts.is_empty() {
continue;
}
let mut replacements: Vec<(Vec<u8>, ObjectId)> = Vec::new();
for (name, dict) in inline_fonts {
let new_id = doc.add_object(Object::Dictionary(dict));
replacements.push((name, new_id));
}
let set_in_dict = |font_dict: &mut lopdf::Dictionary, repls: &[(Vec<u8>, ObjectId)]| {
for (name, new_id) in repls {
let key = String::from_utf8_lossy(name).to_string();
font_dict.set(key, Object::Reference(*new_id));
}
};
match doc.objects.get_mut(&id) {
Some(Object::Dictionary(ref mut d)) => {
if let Ok(Object::Dictionary(ref mut res)) = d.get_mut(b"Resources") {
if let Ok(Object::Dictionary(ref mut fd)) = res.get_mut(b"Font") {
set_in_dict(fd, &replacements);
}
}
}
Some(Object::Stream(ref mut s)) => {
if let Ok(Object::Dictionary(ref mut res)) = s.dict.get_mut(b"Resources") {
if let Ok(Object::Dictionary(ref mut fd)) = res.get_mut(b"Font") {
set_in_dict(fd, &replacements);
}
}
}
_ => {}
}
}
}
fn sanitize_hash_encoded_names(doc: &mut Document) {
use std::collections::HashMap;
type RenameEntry = (Vec<u8>, Vec<u8>, Vec<u8>); let mut rename_map: HashMap<ObjectId, Vec<RenameEntry>> = HashMap::new();
let mut safe_counter = 0u32;
let page_ids: Vec<(u32, ObjectId)> = doc.get_pages().into_iter().collect();
let mut resources_to_scan: Vec<(ObjectId, Vec<ObjectId>)> = Vec::new();
for &(_page_num, page_id) in &page_ids {
let (res_entries, content_ids) = match doc.objects.get(&page_id) {
Some(Object::Dictionary(d)) => {
let contents = collect_content_stream_ids(d);
let res = collect_resource_sub_dicts_with_hash_keys(doc, d);
(res, contents)
}
_ => continue,
};
if !res_entries.is_empty() {
resources_to_scan.push((page_id, content_ids));
for (category, old_name) in res_entries {
safe_counter += 1;
let new_name = format!("XSafe{safe_counter}").into_bytes();
rename_map
.entry(page_id)
.or_default()
.push((category, old_name, new_name));
}
}
}
let all_ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in &all_ids {
let is_form_xobj = match doc.objects.get(id) {
Some(Object::Stream(s)) => {
matches!(s.dict.get(b"Subtype").ok(), Some(Object::Name(n)) if n == b"Form")
}
_ => false,
};
if !is_form_xobj {
continue;
}
let res_entries = match doc.objects.get(id) {
Some(Object::Stream(s)) => collect_resource_sub_dicts_with_hash_keys(doc, &s.dict),
_ => Vec::new(),
};
if !res_entries.is_empty() {
resources_to_scan.push((*id, vec![*id]));
for (category, old_name) in res_entries {
safe_counter += 1;
let new_name = format!("XSafe{safe_counter}").into_bytes();
rename_map
.entry(*id)
.or_default()
.push((category, old_name, new_name));
}
}
}
if rename_map.is_empty() {
return;
}
for (owner_id, renames) in &rename_map {
for (category, old_name, new_name) in renames {
rename_resource_key(doc, *owner_id, category, old_name, new_name);
}
}
for (owner_id, content_ids) in &resources_to_scan {
if let Some(renames) = rename_map.get(owner_id) {
for &content_id in content_ids {
patch_content_stream_names(doc, content_id, renames);
}
}
}
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let needs_fix = match doc.objects.get(&id) {
Some(Object::Dictionary(d)) => dict_has_hash_names(d),
Some(Object::Stream(s)) => dict_has_hash_names(&s.dict),
_ => false,
};
if !needs_fix {
continue;
}
let fixed = match doc.objects.get(&id).cloned() {
Some(Object::Dictionary(d)) => Some(Object::Dictionary(fix_hash_names_in_dict(d))),
Some(Object::Stream(mut s)) => {
s.dict = fix_hash_names_in_dict(s.dict);
Some(Object::Stream(s))
}
_ => None,
};
if let Some(obj) = fixed {
doc.objects.insert(id, obj);
}
}
}
fn collect_content_stream_ids(page_dict: &lopdf::Dictionary) -> Vec<ObjectId> {
match page_dict.get(b"Contents").ok() {
Some(Object::Reference(id)) => vec![*id],
Some(Object::Array(arr)) => arr
.iter()
.filter_map(|o| {
if let Object::Reference(id) = o {
Some(*id)
} else {
None
}
})
.collect(),
_ => Vec::new(),
}
}
fn collect_resource_sub_dicts_with_hash_keys(
doc: &Document,
parent_dict: &lopdf::Dictionary,
) -> Vec<(Vec<u8>, Vec<u8>)> {
let res_dict = match parent_dict.get(b"Resources").ok() {
Some(Object::Dictionary(d)) => d,
Some(Object::Reference(id)) => match doc.objects.get(id) {
Some(Object::Dictionary(d)) => d,
_ => return Vec::new(),
},
_ => return Vec::new(),
};
let mut results = Vec::new();
let categories: &[&[u8]] = &[
b"XObject",
b"Font",
b"ExtGState",
b"ColorSpace",
b"Pattern",
b"Shading",
];
for category in categories {
let sub_dict = match res_dict.get(category).ok() {
Some(Object::Dictionary(d)) => d,
Some(Object::Reference(id)) => match doc.objects.get(id) {
Some(Object::Dictionary(d)) => d,
_ => continue,
},
_ => continue,
};
for (key, _) in sub_dict.iter() {
if key_has_unsafe_bytes(key) {
results.push((category.to_vec(), key.clone()));
}
}
}
results
}
fn key_has_unsafe_bytes(key: &[u8]) -> bool {
key.iter().any(|&b| !is_safe_pdf_name_byte(b))
}
fn is_safe_pdf_name_byte(b: u8) -> bool {
matches!(b, b'!'..=b'~')
&& !matches!(
b,
b'#' | b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
)
}
fn rename_resource_key(
doc: &mut Document,
owner_id: ObjectId,
category: &[u8],
old_name: &[u8],
new_name: &[u8],
) {
let res_ref = match doc.objects.get(&owner_id) {
Some(Object::Dictionary(d)) => match d.get(b"Resources").ok() {
Some(Object::Reference(id)) => Some(*id),
_ => None,
},
Some(Object::Stream(s)) => match s.dict.get(b"Resources").ok() {
Some(Object::Reference(id)) => Some(*id),
_ => None,
},
_ => return,
};
if let Some(res_id) = res_ref {
rename_in_resources_dict(doc, res_id, category, old_name, new_name);
} else {
let mut obj = doc.objects.get(&owner_id).cloned();
let changed = match &mut obj {
Some(Object::Dictionary(d)) => {
rename_in_inline_resources(d, category, old_name, new_name)
}
Some(Object::Stream(s)) => {
rename_in_inline_resources(&mut s.dict, category, old_name, new_name)
}
_ => false,
};
if changed {
if let Some(o) = obj {
doc.objects.insert(owner_id, o);
}
}
}
}
fn rename_in_resources_dict(
doc: &mut Document,
res_id: ObjectId,
category: &[u8],
old_name: &[u8],
new_name: &[u8],
) {
let sub_ref = match doc.objects.get(&res_id) {
Some(Object::Dictionary(d)) => match d.get(category).ok() {
Some(Object::Reference(id)) => Some(*id),
_ => None,
},
_ => return,
};
if let Some(sub_id) = sub_ref {
if let Some(Object::Dictionary(d)) = doc.objects.get(&sub_id).cloned() {
let mut d = d;
if let Some(val) = d.remove(old_name) {
d.set(String::from_utf8_lossy(new_name).as_ref(), val);
doc.objects.insert(sub_id, Object::Dictionary(d));
}
}
} else {
if let Some(Object::Dictionary(res)) = doc.objects.get(&res_id).cloned() {
let mut res = res;
if let Some(Object::Dictionary(sub)) = res.remove(category) {
let mut sub = sub;
if let Some(val) = sub.remove(old_name) {
sub.set(String::from_utf8_lossy(new_name).as_ref(), val);
}
res.set(
String::from_utf8_lossy(category).as_ref(),
Object::Dictionary(sub),
);
doc.objects.insert(res_id, Object::Dictionary(res));
}
}
}
}
fn rename_in_inline_resources(
parent: &mut lopdf::Dictionary,
category: &[u8],
old_name: &[u8],
new_name: &[u8],
) -> bool {
if let Ok(Object::Dictionary(res)) = parent.get_mut(b"Resources") {
if let Ok(Object::Dictionary(sub)) = res.get_mut(category) {
if let Some(val) = sub.remove(old_name) {
sub.set(String::from_utf8_lossy(new_name).as_ref(), val);
return true;
}
}
}
false
}
fn patch_content_stream_names(
doc: &mut Document,
stream_id: ObjectId,
renames: &[(Vec<u8>, Vec<u8>, Vec<u8>)],
) {
let content = match doc.objects.get(&stream_id) {
Some(Object::Stream(s)) => {
let mut s_clone = s.clone();
if s_clone.decompress().is_err() {
return;
}
s_clone.content.clone()
}
_ => return,
};
let mut patched = content.clone();
let mut changed = false;
for (_category, old_name, new_name) in renames {
let mut replace = Vec::with_capacity(1 + new_name.len());
replace.push(b'/');
replace.extend_from_slice(new_name);
for variant in encode_name_variants(old_name) {
let mut search = Vec::with_capacity(1 + variant.len());
search.push(b'/');
search.extend_from_slice(&variant);
let before = patched.clone();
patched = replace_name_in_stream(&patched, &search, &replace);
if patched != before {
changed = true;
}
}
if old_name.iter().any(|b| !is_safe_pdf_name_byte(*b)) {
let mut search_raw = Vec::with_capacity(1 + old_name.len());
search_raw.push(b'/');
search_raw.extend_from_slice(old_name);
let before = patched.clone();
patched = replace_name_in_stream(&patched, &search_raw, &replace);
if patched != before {
changed = true;
}
}
}
if changed {
if let Some(Object::Stream(s)) = doc.objects.get_mut(&stream_id) {
s.set_plain_content(patched);
}
}
}
fn encode_name_variants(name: &[u8]) -> Vec<Vec<u8>> {
let upper = encode_name_hex(name, b"0123456789ABCDEF");
let lower = encode_name_hex(name, b"0123456789abcdef");
let short = encode_name_hex_short(name);
let mut variants = vec![upper.clone()];
if lower != upper {
variants.push(lower);
}
if let Some(s) = short {
if !variants.contains(&s) {
variants.push(s);
}
}
variants
}
fn encode_name_hex(name: &[u8], hex_table: &[u8; 16]) -> Vec<u8> {
let mut result = Vec::with_capacity(name.len() * 3);
for &b in name {
if is_safe_pdf_name_byte(b) {
result.push(b);
} else {
result.push(b'#');
result.push(hex_table[(b >> 4) as usize]);
result.push(hex_table[(b & 0x0F) as usize]);
}
}
result
}
fn encode_name_hex_short(name: &[u8]) -> Option<Vec<u8>> {
let has_single_digit = name.iter().any(|&b| !is_safe_pdf_name_byte(b) && b < 16);
if !has_single_digit {
return None;
}
let mut result = Vec::with_capacity(name.len() * 3);
let hex = b"0123456789ABCDEF";
for &b in name {
if is_safe_pdf_name_byte(b) {
result.push(b);
} else if b < 16 {
result.push(b'#');
result.push(hex[b as usize]);
} else {
result.push(b'#');
result.push(hex[(b >> 4) as usize]);
result.push(hex[(b & 0x0F) as usize]);
}
}
Some(result)
}
fn replace_name_in_stream(data: &[u8], search: &[u8], replace: &[u8]) -> Vec<u8> {
if search.is_empty() {
return data.to_vec();
}
let mut result = Vec::with_capacity(data.len());
let mut i = 0;
while i < data.len() {
if i + search.len() <= data.len() && &data[i..i + search.len()] == search {
let after = if i + search.len() < data.len() {
data[i + search.len()]
} else {
b' ' };
if is_pdf_delimiter(after) {
result.extend_from_slice(replace);
i += search.len();
continue;
}
}
result.push(data[i]);
i += 1;
}
result
}
fn is_pdf_delimiter(b: u8) -> bool {
matches!(
b,
b' ' | b'\t'
| b'\n'
| b'\r'
| b'/'
| b'['
| b']'
| b'('
| b')'
| b'<'
| b'>'
| b'{'
| b'}'
| b'%'
)
}
fn dict_has_hash_names(dict: &lopdf::Dictionary) -> bool {
for (key, val) in dict.iter() {
if key_has_unsafe_bytes(key) {
return true;
}
match val {
Object::Name(n) if key_has_unsafe_bytes(n) => return true,
Object::Dictionary(d) if dict_has_hash_names(d) => return true,
Object::Array(arr) => {
for item in arr {
if let Object::Name(n) = item {
if key_has_unsafe_bytes(n) {
return true;
}
}
}
}
_ => {}
}
}
false
}
fn fix_hash_names_in_dict(dict: lopdf::Dictionary) -> lopdf::Dictionary {
let mut new_dict = lopdf::Dictionary::new();
for (key, val) in dict.into_iter() {
let fixed_key = safe_decode_hash_name(&key);
let fixed_val = fix_hash_name_value(val);
new_dict.set(fixed_key, fixed_val);
}
new_dict
}
fn fix_hash_name_value(val: Object) -> Object {
match val {
Object::Name(n) => Object::Name(safe_decode_hash_name_bytes(&n)),
Object::Dictionary(d) => Object::Dictionary(fix_hash_names_in_dict(d)),
Object::Array(arr) => Object::Array(arr.into_iter().map(fix_hash_name_value).collect()),
other => other,
}
}
fn safe_decode_hash_name(name: &[u8]) -> String {
String::from_utf8(safe_decode_hash_name_bytes(name))
.unwrap_or_else(|e| String::from_utf8_lossy(&e.into_bytes()).into_owned())
}
fn safe_decode_hash_name_bytes(name: &[u8]) -> Vec<u8> {
let mut result = Vec::with_capacity(name.len());
let mut i = 0;
while i < name.len() {
if name[i] == b'#' && i + 2 < name.len() {
let hi = hex_digit(name[i + 1]);
let lo = hex_digit(name[i + 2]);
if let (Some(h), Some(l)) = (hi, lo) {
let decoded = h * 16 + l;
if is_safe_pdf_name_byte(decoded) {
result.push(decoded);
} else {
result.push(b'_');
}
i += 3;
continue;
}
}
if is_safe_pdf_name_byte(name[i]) {
result.push(name[i]);
} else {
result.push(b'_');
}
i += 1;
}
result
}
fn hex_digit(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
fn get_font_dict_inline(dict: &lopdf::Dictionary) -> Option<&lopdf::Dictionary> {
match dict.get(b"Resources").ok()? {
Object::Dictionary(res) => match res.get(b"Font").ok()? {
Object::Dictionary(fd) => Some(fd),
_ => None,
},
_ => None,
}
}
fn remove_file_attachment_annotations(doc: &mut Document) -> usize {
let mut count = 0;
let mut file_attachment_ids = std::collections::HashSet::new();
let page_ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for &page_id in &page_ids {
let (annots_arr, annots_ref_id) = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&page_id) {
match dict.get(b"Annots").ok() {
Some(Object::Array(arr)) => (Some(arr.clone()), None),
Some(Object::Reference(ref_id)) => {
if let Some(Object::Array(arr)) = doc.objects.get(ref_id) {
(Some(arr.clone()), Some(*ref_id))
} else {
(None, None)
}
}
_ => (None, None),
}
} else {
(None, None)
}
};
let Some(arr) = annots_arr else { continue };
let page_fa_refs: Vec<ObjectId> = arr
.iter()
.filter_map(|obj| {
let id = match obj {
Object::Reference(id) => *id,
_ => return None,
};
if let Some(Object::Dictionary(d)) = doc.objects.get(&id) {
if matches!(
d.get(b"Subtype").ok(),
Some(Object::Name(ref n)) if n == b"FileAttachment"
) {
return Some(id);
}
}
None
})
.collect();
if page_fa_refs.is_empty() {
continue;
}
for id in &page_fa_refs {
file_attachment_ids.insert(*id);
}
let retain_fn = |obj: &Object| -> bool {
if let Object::Reference(id) = obj {
!page_fa_refs.contains(id)
} else {
true
}
};
if let Some(annots_id) = annots_ref_id {
if let Some(Object::Array(ref mut arr)) = doc.objects.get_mut(&annots_id) {
let before = arr.len();
arr.retain(retain_fn);
count += before - arr.len();
}
} else if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&page_id) {
if let Ok(Object::Array(ref mut arr)) = dict.get_mut(b"Annots") {
let before = arr.len();
arr.retain(retain_fn);
count += before - arr.len();
}
}
}
for id in file_attachment_ids {
if let Some(Object::Dictionary(dict)) = doc.objects.remove(&id) {
if let Ok(Object::Reference(fs_id)) = dict.get(b"FS") {
if let Some(Object::Dictionary(fs_dict)) = doc.objects.remove(fs_id) {
if let Ok(Object::Dictionary(ef_dict)) = fs_dict.get(b"EF") {
for (_, obj) in ef_dict.iter() {
if let Object::Reference(stream_id) = obj {
doc.objects.remove(stream_id);
}
}
}
}
}
}
}
count
}
fn fix_extgstate_smask_s(doc: &mut Document) -> usize {
let mut count = 0;
let ids: Vec<ObjectId> = doc.objects.keys().copied().collect();
for id in ids {
let needs_fix = {
if let Some(Object::Dictionary(dict)) = doc.objects.get(&id) {
let has_smask_dict = matches!(dict.get(b"SMask").ok(), Some(Object::Dictionary(_)));
if !has_smask_dict {
false
} else if let Ok(Object::Dictionary(smask)) = dict.get(b"SMask") {
match smask.get(b"S").ok() {
Some(Object::Name(ref n)) => n != b"Alpha" && n != b"Luminosity",
_ => false,
}
} else {
false
}
} else {
false
}
};
if needs_fix {
if let Some(Object::Dictionary(ref mut dict)) = doc.objects.get_mut(&id) {
if let Ok(Object::Dictionary(ref mut smask)) = dict.get_mut(b"SMask") {
smask.set("S", Object::Name(b"Alpha".to_vec()));
count += 1;
}
}
}
}
count
}
fn end_of_literal_string(data: &[u8], open: usize) -> Option<usize> {
debug_assert_eq!(data.get(open), Some(&b'('));
let mut i = open + 1;
let mut nest = 1i32;
while i < data.len() {
match data[i] {
b'\\' => {
i += 2;
continue;
}
b'(' => nest += 1,
b')' => {
nest -= 1;
if nest == 0 {
return Some(i + 1);
}
}
_ => {}
}
i += 1;
}
None
}
fn fix_long_strings_in_content_streams(doc: &mut Document) -> usize {
let mut count = 0;
let page_ids: Vec<ObjectId> = doc.get_pages().values().copied().collect();
for &page_id in &page_ids {
let content_ids: Vec<ObjectId> = {
if let Some(Object::Dictionary(page)) = doc.objects.get(&page_id) {
match page.get(b"Contents").ok() {
Some(Object::Reference(id)) => vec![*id],
Some(Object::Array(arr)) => arr
.iter()
.filter_map(|o| {
if let Object::Reference(id) = o {
Some(*id)
} else {
None
}
})
.collect(),
_ => vec![],
}
} else {
vec![]
}
};
for content_id in content_ids {
if let Some(Object::Stream(ref mut stream)) = doc.objects.get_mut(&content_id) {
let content = match stream.get_plain_content() {
Ok(c) => c,
Err(_) => continue,
};
let truncated = truncate_long_strings_in_content(&content);
if truncated != content {
stream.set_plain_content(truncated);
count += 1;
}
}
}
}
count
}
fn truncate_long_strings_in_content(content: &[u8]) -> Vec<u8> {
if content.windows(2).any(|w| w == b"BI") {
return crate::inline_image::truncate_image_stream_strings(content);
}
const MAX_STRING_LEN: usize = 32767;
let mut result = Vec::with_capacity(content.len());
let mut i = 0;
while i < content.len() {
if content[i] == b'(' {
let start = i;
let inner_start = start + 1;
let Some(eind) = end_of_literal_string(content, start) else {
result.extend_from_slice(&content[start..]);
break;
};
i = eind;
let inner_len = i - 1 - inner_start;
if inner_len > MAX_STRING_LEN {
let mut end = inner_start + MAX_STRING_LEN;
let mut slashes = 0;
while end - slashes > inner_start && content[end - slashes - 1] == b'\\' {
slashes += 1;
}
if slashes % 2 == 1 {
end -= 1;
}
result.push(b'(');
result.extend_from_slice(&content[inner_start..end]);
result.push(b')');
} else {
result.extend_from_slice(&content[start..i]);
}
} else if content[i] == b'<' && content.get(i + 1) != Some(&b'<') {
let start = i;
i += 1;
while i < content.len() && content[i] != b'>' {
i += 1;
}
if i < content.len() {
i += 1; }
let str_len = i - start;
if str_len > MAX_STRING_LEN * 2 {
result.extend_from_slice(
&content[start..(start + MAX_STRING_LEN * 2).min(content.len())],
);
result.push(b'>');
} else {
result.extend_from_slice(&content[start..i.min(content.len())]);
}
} else {
result.push(content[i]);
i += 1;
}
}
result
}
fn fix_invalid_lang_in_bdc(doc: &mut Document) -> usize {
let mut count = 0;
let page_ids: Vec<ObjectId> = doc.get_pages().values().copied().collect();
for &page_id in &page_ids {
let content_ids: Vec<ObjectId> = {
if let Some(Object::Dictionary(page)) = doc.objects.get(&page_id) {
match page.get(b"Contents").ok() {
Some(Object::Reference(id)) => vec![*id],
Some(Object::Array(arr)) => arr
.iter()
.filter_map(|o| {
if let Object::Reference(id) = o {
Some(*id)
} else {
None
}
})
.collect(),
_ => vec![],
}
} else {
vec![]
}
};
for content_id in content_ids {
if let Some(Object::Stream(ref mut stream)) = doc.objects.get_mut(&content_id) {
let content = match stream.get_plain_content() {
Ok(c) => c,
Err(_) => continue,
};
let fixed = fix_lang_in_content_stream(&content);
if fixed != content {
stream.set_plain_content(fixed);
count += 1;
}
}
}
}
count
}
fn fix_lang_in_content_stream(content: &[u8]) -> Vec<u8> {
let mut result = Vec::with_capacity(content.len());
let mut i = 0;
while i < content.len() {
if content[i].is_ascii_whitespace() {
result.push(content[i]);
i += 1;
continue;
}
if content[i] == b'%' {
while i < content.len() && content[i] != b'\n' && content[i] != b'\r' {
result.push(content[i]);
i += 1;
}
continue;
}
let tok_start = i;
while i < content.len()
&& !content[i].is_ascii_whitespace()
&& content[i] != b'('
&& content[i] != b'<'
&& content[i] != b'>'
&& content[i] != b'/'
&& content[i] != b'%'
&& content[i] != b'['
&& content[i] != b']'
{
i += 1;
}
if tok_start == i {
result.push(content[i]);
i += 1;
continue;
}
let token = &content[tok_start..i];
if token == b"BDC" {
result.extend_from_slice(token);
i = tok_start + 3;
while i < content.len() && content[i].is_ascii_whitespace() {
result.push(content[i]);
i += 1;
}
if i < content.len() && content[i] == b'/' {
let name_start = i;
i += 1;
while i < content.len()
&& !content[i].is_ascii_whitespace()
&& content[i] != b'('
&& content[i] != b'<'
&& content[i] != b'>'
&& content[i] != b'/'
&& content[i] != b'%'
&& content[i] != b'['
&& content[i] != b']'
{
i += 1;
}
let name = &content[name_start..i];
if name == b"/Lang" {
result.extend_from_slice(name);
while i < content.len() && content[i].is_ascii_whitespace() {
result.push(content[i]);
i += 1;
}
if i < content.len() && content[i] == b'(' {
let str_start = i;
i += 1;
while i < content.len() && content[i] != b')' {
i += 1;
}
if i < content.len() {
i += 1; }
let lang_end = i.min(content.len());
let lang =
&content[str_start + 1..lang_end.saturating_sub(1).max(str_start + 1)];
let lang_str = String::from_utf8_lossy(lang);
if !is_valid_bcp47(&lang_str) {
if let Some(normalized) = normalize_bcp47(&lang_str) {
result.push(b'(');
result.extend_from_slice(normalized.as_bytes());
result.push(b')');
} else {
result.push(b'(');
result.extend_from_slice(b"en");
result.push(b')');
}
} else {
result.extend_from_slice(&content[str_start..lang_end]);
}
} else {
}
} else {
result.extend_from_slice(name);
}
} else {
}
} else {
result.extend_from_slice(token);
}
}
result
}
fn is_valid_bcp47(tag: &str) -> bool {
if tag.is_empty() {
return false;
}
let parts: Vec<&str> = tag.split('-').collect();
if parts.is_empty() {
return false;
}
let first = parts[0];
if first.len() < 2 || first.len() > 3 {
return false;
}
if !first.chars().all(|c| c.is_ascii_lowercase()) {
return false;
}
if first == "x" {
return parts.len() == 1;
}
for part in &parts[1..] {
if part.len() == 1 && part.starts_with('X') {
continue;
}
if part.len() < 2 || part.len() > 8 {
return false;
}
if !part.chars().all(|c| c.is_ascii_alphanumeric()) {
return false;
}
}
true
}
fn normalize_bcp47(tag: &str) -> Option<String> {
if tag.is_empty() {
return None;
}
let parts: Vec<&str> = tag.split('-').collect();
if parts.is_empty() {
return None;
}
let first = parts[0].to_lowercase();
if first.len() < 2 || first.len() > 3 {
return None;
}
let mut result = first;
for part in &parts[1..] {
if part.is_empty() {
continue;
}
if *part == "X" {
result.push_str("-X");
break;
}
let normalized_part: String = part
.chars()
.enumerate()
.map(|(i, c)| {
if i == 0 {
c.to_uppercase().next().unwrap_or(c)
} else {
c.to_lowercase().next().unwrap_or(c)
}
})
.collect();
if normalized_part.len() >= 2 && normalized_part.len() <= 8 {
result.push('-');
result.push_str(&normalized_part);
}
}
Some(result)
}
fn sync_info_xmp_dates(doc: &mut Document) -> usize {
let catalog_id = match get_catalog_id(doc) {
Some(id) => id,
None => return 0,
};
let meta_id = {
if let Some(Object::Dictionary(cat)) = doc.objects.get(&catalog_id) {
match cat.get(b"Metadata").ok() {
Some(Object::Reference(id)) => Some(*id),
_ => None,
}
} else {
None
}
};
let meta_id = match meta_id {
Some(id) => id,
None => return 0,
};
let xmp_bytes = {
if let Some(Object::Stream(stream)) = doc.objects.get(&meta_id) {
stream.content.clone()
} else {
return 0;
}
};
let xmp_str = String::from_utf8_lossy(&xmp_bytes);
let xmp_create_date = extract_xmp_date(&xmp_str, "xmp:CreateDate");
let xmp_modify_date = extract_xmp_date(&xmp_str, "xmp:ModifyDate");
if xmp_create_date.is_none() && xmp_modify_date.is_none() {
return 0;
}
let mut count = 0;
let info_id = doc
.objects
.iter()
.find(|(_, obj)| {
if let Object::Dictionary(d) = obj {
d.get(b"Type").ok().and_then(|o| o.as_name().ok()) == Some(b"Info")
} else {
false
}
})
.map(|(id, _)| *id);
if let Some(info_id) = info_id {
if let Some(Object::Dictionary(ref mut info)) = doc.objects.get_mut(&info_id) {
if let Some(create_date) = xmp_create_date {
if let Ok(current) = info.get(b"CreationDate") {
let current_str = date_to_string(current);
if current_str != create_date {
info.set(
"CreationDate",
Object::String(create_date.into_bytes(), lopdf::StringFormat::Literal),
);
count += 1;
}
}
}
if let Some(modify_date) = xmp_modify_date {
if let Ok(current) = info.get(b"ModDate") {
let current_str = date_to_string(current);
if current_str != modify_date {
info.set(
"ModDate",
Object::String(modify_date.into_bytes(), lopdf::StringFormat::Literal),
);
count += 1;
}
}
}
}
}
count
}
fn extract_xmp_date(xmp: &str, property: &str) -> Option<String> {
let search = format!("<{}>", property);
let search_close = format!("</{}>", property);
let start = xmp.find(&search).map(|p| p + search.len())?;
let end = xmp[start..].find(&search_close)?;
let date_str = &xmp[start..start + end];
let date_str = date_str.trim();
if date_str.is_empty() {
return None;
}
Some(date_str.to_string())
}
fn date_to_string(obj: &Object) -> String {
match obj {
Object::String(s, _) => String::from_utf8_lossy(s).to_string(),
_ => String::new(),
}
}
fn rebuild_broken_xmp(doc: &mut Document) -> usize {
let catalog_id = match get_catalog_id(doc) {
Some(id) => id,
None => return 0,
};
let meta_id = {
if let Some(Object::Dictionary(cat)) = doc.objects.get(&catalog_id) {
match cat.get(b"Metadata").ok() {
Some(Object::Reference(id)) => Some(*id),
_ => None,
}
} else {
None
}
};
let meta_id = match meta_id {
Some(id) => id,
None => return 0,
};
let xmp_bytes = {
if let Some(Object::Stream(stream)) = doc.objects.get(&meta_id) {
stream.content.clone()
} else {
return 0;
}
};
let xmp_str = String::from_utf8_lossy(&xmp_bytes);
if is_valid_rdf(&xmp_str) {
return 0;
}
let info_values = extract_info_for_xmp(doc);
let new_xmp = build_xmp_from_info(&info_values);
if let Some(Object::Stream(ref mut stream)) = doc.objects.get_mut(&meta_id) {
stream.set_content(new_xmp.into_bytes());
}
1
}
fn is_valid_rdf(xmp: &str) -> bool {
xmp.contains("<rdf:RDF") && xmp.contains("</rdf:RDF>")
|| xmp.contains("<rdf:Description") && xmp.contains("</rdf:Description>")
}
struct InfoValues {
title: Option<String>,
creator: Option<String>,
creation_date: Option<String>,
modification_date: Option<String>,
}
fn extract_info_for_xmp(doc: &Document) -> InfoValues {
let info_id = doc
.objects
.iter()
.find(|(_, obj)| {
if let Object::Dictionary(d) = obj {
d.get(b"Type").ok().and_then(|o| o.as_name().ok()) == Some(b"Info")
} else {
false
}
})
.map(|(id, _)| *id);
let mut info_values = InfoValues {
title: None,
creator: None,
creation_date: None,
modification_date: None,
};
if let Some(info_id) = info_id {
if let Some(Object::Dictionary(info)) = doc.objects.get(&info_id) {
if let Ok(Object::String(s, _)) = info.get(b"Title") {
let title = String::from_utf8_lossy(s).to_string();
if !title.is_empty() {
info_values.title = Some(title);
}
}
if let Ok(Object::String(s, _)) = info.get(b"Author") {
let author = String::from_utf8_lossy(s).to_string();
if !author.is_empty() {
info_values.creator = Some(author);
}
}
if let Ok(Object::String(s, _)) = info.get(b"CreationDate") {
let date = String::from_utf8_lossy(s).to_string();
if !date.is_empty() {
info_values.creation_date = Some(date);
}
}
if let Ok(Object::String(s, _)) = info.get(b"ModDate") {
let date = String::from_utf8_lossy(s).to_string();
if !date.is_empty() {
info_values.modification_date = Some(date);
}
}
}
}
info_values
}
fn build_xmp_from_info(info: &InfoValues) -> String {
let mut xmp = String::from(
r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:xmp="http://ns.adobe.com/xap/1.0/">
"#,
);
xmp.push_str(" <pdfaid:part>2</pdfaid:part>\n");
xmp.push_str(" <pdfaid:conformance>B</pdfaid:conformance>\n");
if let Some(ref title) = info.title {
let escaped_title = escape_xml_string(title);
xmp.push_str(&format!(
r#" <dc:title>
<rdf:Alt>
<rdf:li xml:lang="x-default">{}</rdf:li>
</rdf:Alt>
</dc:title>
"#,
escaped_title
));
} else {
xmp.push_str(
r#" <dc:title>
<rdf:Alt>
<rdf:li xml:lang="x-default">Untitled</rdf:li>
</rdf:Alt>
</dc:title>
"#,
);
}
if let Some(ref creator) = info.creator {
let escaped_creator = escape_xml_string(creator);
xmp.push_str(&format!(
r#" <dc:creator>
<rdf:Seq>
<rdf:li>{}</rdf:li>
</rdf:Seq>
</dc:creator>
"#,
escaped_creator
));
} else {
xmp.push_str(
r#" <dc:creator>
<rdf:Seq>
<rdf:li>Unknown</rdf:li>
</rdf:Seq>
</dc:creator>
"#,
);
}
if let Some(ref create_date) = info.creation_date {
let xmp_date = normalize_date_for_xmp(create_date);
xmp.push_str(&format!(
r#" <xmp:CreateDate>{}</xmp:CreateDate>
"#,
xmp_date
));
} else {
xmp.push_str(" <xmp:CreateDate>1970-01-01T00:00:00Z</xmp:CreateDate>\n");
}
if let Some(ref mod_date) = info.modification_date {
let xmp_date = normalize_date_for_xmp(mod_date);
xmp.push_str(&format!(
r#" <xmp:ModifyDate>{}</xmp:ModifyDate>
"#,
xmp_date
));
} else {
xmp.push_str(" <xmp:ModifyDate>1970-01-01T00:00:00Z</xmp:ModifyDate>\n");
}
xmp.push_str(
r#"</rdf:Description>
</rdf:RDF>
</x:xmpmeta><?xpacket end="w"?>"#,
);
xmp
}
fn escape_xml_string(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn normalize_date_for_xmp(date: &str) -> String {
let cleaned = date.trim();
if cleaned.is_empty() {
return "1970-01-01T00:00:00Z".to_string();
}
if cleaned.len() >= 19
&& cleaned.chars().nth(4) == Some('D')
&& cleaned.chars().nth(7) == Some('\'')
{
let year = &cleaned[0..4];
let month = &cleaned[5..7];
let day = &cleaned[8..10];
let hour = &cleaned[11..13];
let minute = &cleaned[14..16];
let second = &cleaned[17..19];
return format!("{}:{}:{}T{}:{}:{}Z", year, month, day, hour, minute, second);
}
if cleaned.len() >= 14
&& cleaned.chars().nth(4) == Some('-')
&& cleaned.chars().nth(7) == Some('-')
{
let year = &cleaned[0..4];
let month = &cleaned[5..7];
let day = &cleaned[8..10];
let hour = if cleaned.len() >= 16 {
&cleaned[11..13]
} else {
"00"
};
let minute = if cleaned.len() >= 19 {
&cleaned[14..16]
} else {
"00"
};
let second = if cleaned.len() >= 22 {
&cleaned[17..19]
} else {
"00"
};
return format!("{}:{}:{}T{}:{}:{}Z", year, month, day, hour, minute, second);
}
cleaned.to_string()
}
fn fix_xref_eol(data: &mut Vec<u8>) {
if let Some(pos) = find_last(data, b"xref") {
if pos == 0 || data[pos - 1].is_ascii_whitespace() {
let next = pos + 4;
if next < data.len() && data[next] == b' ' {
let mut end = next;
while end < data.len() && data[end] == b' ' {
end += 1;
}
if end > next {
data.drain(next..end);
}
}
}
}
if let Some(pos) = find_last(data, b"startxref") {
let next = pos + 9;
if next < data.len() && data[next] == b' ' {
let mut end = next;
while end < data.len() && data[end] == b' ' {
end += 1;
}
if end > next {
data.drain(next..end);
}
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn end_of_literal_string_reads_escapes_the_way_the_spec_does() {
assert_eq!(super::end_of_literal_string(b"(a\\\\)rest", 0), Some(5));
assert_eq!(super::end_of_literal_string(b"(a\\)b)rest", 0), Some(6));
assert_eq!(super::end_of_literal_string(b"(a(b)c)rest", 0), Some(7));
assert_eq!(super::end_of_literal_string(b"(a\\)b", 0), None);
assert_eq!(super::end_of_literal_string(b"(a\\", 0), None);
}
#[test]
fn an_escaped_backslash_does_not_swallow_the_string_after_it() {
let mut content = Vec::new();
content.extend_from_slice(b"(a\\\\)Tj\n(");
content.extend(std::iter::repeat_n(b'A', 40_000));
content.extend_from_slice(b")Tj\nET\n");
let uit = super::truncate_long_strings_in_content(&content);
assert!(
uit.len() < content.len(),
"the second string is over the limit and must be cut; if the first \
string swallowed it, nothing is cut and the page keeps a \
40000-byte literal"
);
assert!(
uit.starts_with(b"(a\\\\)Tj\n("),
"the escaped string itself must come through untouched"
);
assert!(
uit.ends_with(b")Tj\nET\n"),
"and the operators after it must survive"
);
}
#[test]
fn an_over_long_string_is_cut_but_the_operators_after_it_survive() {
let mut content = Vec::new();
content.extend_from_slice(b"BT\n(");
content.extend(std::iter::repeat_n(b'A', 40_000));
content.extend_from_slice(b")Tj\n(kort)Tj\nET\n");
let uit = super::truncate_long_strings_in_content(&content);
assert!(uit.len() < content.len(), "the long string must be cut");
assert!(
uit.ends_with(b")Tj\n(kort)Tj\nET\n"),
"everything after the long string must still be there — cutting the \
stream is data loss, not a fix; got tail {:?}",
String::from_utf8_lossy(&uit[uit.len().saturating_sub(24)..])
);
}
#[test]
fn inline_binary_parentheses_are_not_long_strings() {
let mut content = b"q BI /IM true /W 1 /H 1 /F /CCF ID (".to_vec();
content.extend(std::iter::repeat_n(b'X', 40000));
content.extend_from_slice(b")\nEI Q BT (tail) Tj ET");
assert_eq!(super::truncate_long_strings_in_content(&content), content);
}
#[test]
fn an_unterminated_string_leaves_the_content_alone() {
let mut content = Vec::new();
content.extend_from_slice(b"BT\n(nooit gesloten ");
content.extend(std::iter::repeat_n(b'B', 40_000));
content.extend_from_slice(b"\nET\n");
let uit = super::truncate_long_strings_in_content(&content);
assert_eq!(
uit, content,
"a string we cannot find the end of is one we did not parse; \
copy it through rather than cut the page"
);
}
use super::*;
use lopdf::{dictionary, Stream};
fn make_basic_doc() -> Document {
let mut doc = Document::with_version("1.7");
let pages_id = doc.new_object_id();
let content = Stream::new(dictionary! {}, b"BT /F1 12 Tf (Hello) Tj ET".to_vec());
let content_id = doc.add_object(Object::Stream(content));
let page = dictionary! {
"Type" => "Page",
"Parent" => Object::Reference(pages_id),
"MediaBox" => Object::Array(vec![
Object::Integer(0), Object::Integer(0),
Object::Integer(612), Object::Integer(792),
]),
"Contents" => Object::Reference(content_id),
};
let page_id = doc.add_object(Object::Dictionary(page));
let pages = dictionary! {
"Type" => "Pages",
"Count" => Object::Integer(1),
"Kids" => Object::Array(vec![Object::Reference(page_id)]),
};
doc.objects.insert(pages_id, Object::Dictionary(pages));
let catalog = dictionary! {
"Type" => "Catalog",
"Pages" => Object::Reference(pages_id),
};
let catalog_id = doc.add_object(Object::Dictionary(catalog));
doc.trailer.set("Root", Object::Reference(catalog_id));
doc
}
fn add_link_annotation_with_action(doc: &mut Document, action: lopdf::Dictionary) -> ObjectId {
let page_id = doc.page_iter().next().expect("basic doc page");
let annot_id = doc.add_object(Object::Dictionary(dictionary! {
"Type" => Object::Name(b"Annot".to_vec()),
"Subtype" => Object::Name(b"Link".to_vec()),
"Rect" => Object::Array(vec![
Object::Integer(0),
Object::Integer(0),
Object::Integer(100),
Object::Integer(20),
]),
"A" => Object::Dictionary(action),
}));
if let Ok(Object::Dictionary(page)) = doc.get_object_mut(page_id) {
page.set("Annots", Object::Array(vec![Object::Reference(annot_id)]));
}
annot_id
}
fn action_dict(action_type: &[u8]) -> lopdf::Dictionary {
match action_type {
b"GoTo" => dictionary! {
"S" => Object::Name(b"GoTo".to_vec()),
"D" => Object::Array(vec![Object::Integer(0), Object::Name(b"Fit".to_vec())]),
},
b"URI" => dictionary! {
"S" => Object::Name(b"URI".to_vec()),
"URI" => Object::String(
b"https://example.com".to_vec(),
lopdf::StringFormat::Literal,
),
},
other => dictionary! {
"S" => Object::Name(other.to_vec()),
"F" => Object::String(
b"https://example.com/upload".to_vec(),
lopdf::StringFormat::Literal,
),
},
}
}
fn annotation_action_name(doc: &Document, annot_id: ObjectId) -> Option<Vec<u8>> {
let annot = doc.get_dictionary(annot_id).ok()?;
let action = annot.get(b"A").ok()?.as_dict().ok()?;
let Object::Name(name) = action.get(b"S").ok()? else {
return None;
};
Some(name.clone())
}
#[test]
fn test_remove_javascript_empty() {
let mut doc = make_basic_doc();
let count = remove_javascript(&mut doc);
assert_eq!(count, 0);
}
#[test]
fn test_remove_javascript_action() {
let mut doc = make_basic_doc();
let js_action = dictionary! {
"S" => Object::Name(b"JavaScript".to_vec()),
"JS" => Object::String(b"app.alert('hello')".to_vec(), lopdf::StringFormat::Literal),
};
doc.add_object(Object::Dictionary(js_action));
let count = remove_javascript(&mut doc);
assert!(count >= 1);
}
#[test]
fn test_remove_additional_actions() {
let mut doc = make_basic_doc();
let aa_dict = dictionary! {
"O" => Object::Dictionary(dictionary! {
"S" => Object::Name(b"JavaScript".to_vec()),
"JS" => Object::String(b"console.println()".to_vec(), lopdf::StringFormat::Literal),
}),
};
let page_with_aa = dictionary! {
"Type" => "Page",
"AA" => Object::Dictionary(aa_dict),
};
doc.add_object(Object::Dictionary(page_with_aa));
let count = remove_additional_actions(&mut doc);
assert!(count >= 1);
}
#[test]
fn forbidden_submitform_launch_importdata_actions_are_removed() {
for action_type in [
b"SubmitForm".as_slice(),
b"Launch".as_slice(),
b"ImportData".as_slice(),
] {
let mut doc = make_basic_doc();
let annot_id = add_link_annotation_with_action(&mut doc, action_dict(action_type));
remove_forbidden_actions(&mut doc);
let annot = doc.get_dictionary(annot_id).expect("annotation dict");
assert!(
annot.get(b"A").is_err(),
"{:?} action must be inert after hardening",
std::str::from_utf8(action_type).unwrap()
);
}
}
#[test]
fn goto_and_uri_actions_remain_unchanged() {
for action_type in [b"GoTo".as_slice(), b"URI".as_slice()] {
let mut doc = make_basic_doc();
let annot_id = add_link_annotation_with_action(&mut doc, action_dict(action_type));
remove_forbidden_actions(&mut doc);
assert_eq!(
annotation_action_name(&doc, annot_id).as_deref(),
Some(action_type)
);
}
}
#[test]
fn test_remove_embedded_files() {
let mut doc = make_basic_doc();
let catalog_id = get_catalog_id(&doc).unwrap();
let ef_tree = dictionary! {
"Names" => Object::Array(vec![
Object::String(b"test.txt".to_vec(), lopdf::StringFormat::Literal),
Object::Null,
]),
};
let ef_id = doc.add_object(Object::Dictionary(ef_tree));
let names = dictionary! {
"EmbeddedFiles" => Object::Reference(ef_id),
};
let names_id = doc.add_object(Object::Dictionary(names));
if let Some(Object::Dictionary(ref mut catalog)) = doc.objects.get_mut(&catalog_id) {
catalog.set("Names", Object::Reference(names_id));
}
let count = remove_embedded_files(&mut doc);
assert!(count >= 1);
if let Some(Object::Dictionary(names_dict)) = doc.objects.get(&names_id) {
assert!(!names_dict.has(b"EmbeddedFiles"));
}
}
#[test]
fn test_remove_encryption() {
let mut doc = make_basic_doc();
doc.trailer.set("Encrypt", Object::Reference((99, 0)));
assert!(remove_encryption(&mut doc));
assert!(!doc.trailer.has(b"Encrypt"));
}
#[test]
fn test_remove_encryption_none() {
let mut doc = make_basic_doc();
assert!(!remove_encryption(&mut doc));
}
#[test]
fn test_transparency_groups() {
let mut doc = make_basic_doc();
let group = dictionary! {
"S" => Object::Name(b"Transparency".to_vec()),
"CS" => Object::Name(b"DeviceRGB".to_vec()),
};
let xobj = dictionary! {
"Type" => Object::Name(b"XObject".to_vec()),
"Subtype" => Object::Name(b"Form".to_vec()),
"Group" => Object::Dictionary(group),
};
doc.add_object(Object::Dictionary(xobj));
assert_eq!(count_transparency_groups(&doc), 1);
}
#[test]
fn test_full_cleanup() {
let mut doc = make_basic_doc();
let js = dictionary! {
"S" => Object::Name(b"JavaScript".to_vec()),
"JS" => Object::String(b"void(0)".to_vec(), lopdf::StringFormat::Literal),
};
doc.add_object(Object::Dictionary(js));
let group = dictionary! {
"S" => Object::Name(b"Transparency".to_vec()),
};
let xobj = dictionary! {
"Group" => Object::Dictionary(group),
};
doc.add_object(Object::Dictionary(xobj));
let report = cleanup_for_pdfa(&mut doc, true).unwrap();
assert!(report.js_actions_removed >= 1);
assert_eq!(report.transparency_groups_found, 1);
}
#[test]
fn test_remove_transfer_functions() {
let mut doc = make_basic_doc();
let gs = dictionary! {
"TR" => Object::Name(b"Identity".to_vec()),
};
doc.add_object(Object::Dictionary(gs));
assert_eq!(remove_transfer_functions(&mut doc), 1);
}
#[test]
fn test_ensure_trailer_id() {
let mut doc = make_basic_doc();
assert!(!doc.trailer.has(b"ID"));
assert!(ensure_trailer_id(&mut doc));
assert!(doc.trailer.has(b"ID"));
assert!(!ensure_trailer_id(&mut doc));
}
#[test]
fn test_fix_annotation_flags() {
let mut doc = make_basic_doc();
let annot = dictionary! {
"Subtype" => Object::Name(b"Text".to_vec()),
"F" => Object::Integer(0), };
doc.add_object(Object::Dictionary(annot));
assert_eq!(fix_annotation_flags(&mut doc), 1);
}
#[test]
fn test_fix_cidtogidmap() {
let mut doc = make_basic_doc();
let cidfont = dictionary! {
"Type" => "Font",
"Subtype" => Object::Name(b"CIDFontType2".to_vec()),
"BaseFont" => "TestFont",
};
doc.add_object(Object::Dictionary(cidfont));
assert_eq!(fix_cidtogidmap(&mut doc), 1);
assert_eq!(fix_cidtogidmap(&mut doc), 0);
}
#[test]
fn test_fix_annotation_ap() {
let mut doc = make_basic_doc();
let ap = dictionary! {
"D" => Object::Reference((99, 0)),
};
let annot = dictionary! {
"Subtype" => Object::Name(b"Stamp".to_vec()),
"AP" => Object::Dictionary(ap),
"F" => Object::Integer(4),
};
doc.add_object(Object::Dictionary(annot));
assert_eq!(fix_annotation_ap(&mut doc), 1);
}
#[test]
fn test_fix_annotation_contents() {
let mut doc = make_basic_doc();
let annot = dictionary! {
"Subtype" => Object::Name(b"Text".to_vec()),
"F" => Object::Integer(4),
};
doc.add_object(Object::Dictionary(annot));
fix_annotation_contents(&mut doc);
for obj in doc.objects.values() {
if let Object::Dictionary(dict) = obj {
if matches!(dict.get(b"Subtype").ok(), Some(Object::Name(ref n)) if n == b"Text") {
assert!(dict.has(b"Contents"));
}
}
}
}
#[test]
fn test_fix_annotation_contents_type() {
let mut doc = make_basic_doc();
let annot = dictionary! {
"Subtype" => Object::Name(b"Text".to_vec()),
"Contents" => Object::Name(b"SomeValue".to_vec()),
"F" => Object::Integer(4),
};
doc.add_object(Object::Dictionary(annot));
fix_annotation_contents_type(&mut doc);
for obj in doc.objects.values() {
if let Object::Dictionary(dict) = obj {
if matches!(dict.get(b"Subtype").ok(), Some(Object::Name(ref n)) if n == b"Text") {
assert!(
matches!(dict.get(b"Contents").ok(), Some(Object::String(_, _))),
"/Contents should be a String"
);
}
}
}
}
#[test]
fn test_fix_font_descriptor_keys() {
let mut doc = make_basic_doc();
let font_stream_id =
doc.add_object(Object::Stream(Stream::new(dictionary! {}, vec![0u8; 10])));
let desc = dictionary! {
"Type" => "FontDescriptor",
"FontFile" => Object::Reference(font_stream_id),
};
let desc_id = doc.add_object(Object::Dictionary(desc));
let font = dictionary! {
"Type" => "Font",
"Subtype" => Object::Name(b"TrueType".to_vec()),
"FontDescriptor" => Object::Reference(desc_id),
};
doc.add_object(Object::Dictionary(font));
fix_font_descriptor_keys(&mut doc);
if let Some(Object::Dictionary(d)) = doc.objects.get(&desc_id) {
assert!(!d.has(b"FontFile"), "FontFile should be removed");
assert!(d.has(b"FontFile2"), "FontFile2 should be added");
}
}
#[test]
fn test_ensure_xmp_dc_title() {
let mut doc = make_basic_doc();
let catalog_id = get_catalog_id(&doc).unwrap();
let xmp = br#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/">
<pdfaid:part>2</pdfaid:part>
<pdfaid:conformance>B</pdfaid:conformance>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta><?xpacket end="w"?>"#;
let meta_stream = Stream::new(
dictionary! {
"Type" => "Metadata",
"Subtype" => "XML",
},
xmp.to_vec(),
);
let meta_id = doc.add_object(Object::Stream(meta_stream));
if let Some(Object::Dictionary(ref mut cat)) = doc.objects.get_mut(&catalog_id) {
cat.set("Metadata", Object::Reference(meta_id));
}
ensure_xmp_dc_title(&mut doc);
if let Some(Object::Stream(stream)) = doc.objects.get(&meta_id) {
let content = String::from_utf8_lossy(&stream.content);
assert!(
content.contains("dc:title"),
"XMP should now contain dc:title"
);
}
}
#[test]
fn test_ensure_xmp_dc_title_already_present() {
let mut doc = make_basic_doc();
let catalog_id = get_catalog_id(&doc).unwrap();
let xmp = br#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:title><rdf:Alt><rdf:li xml:lang="x-default">My Title</rdf:li></rdf:Alt></dc:title>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta><?xpacket end="w"?>"#;
let meta_stream = Stream::new(
dictionary! {
"Type" => "Metadata",
"Subtype" => "XML",
},
xmp.to_vec(),
);
let meta_id = doc.add_object(Object::Stream(meta_stream));
if let Some(Object::Dictionary(ref mut cat)) = doc.objects.get_mut(&catalog_id) {
cat.set("Metadata", Object::Reference(meta_id));
}
let original_len = {
if let Some(Object::Stream(s)) = doc.objects.get(&meta_id) {
s.content.len()
} else {
0
}
};
ensure_xmp_dc_title(&mut doc);
if let Some(Object::Stream(stream)) = doc.objects.get(&meta_id) {
assert_eq!(
stream.content.len(),
original_len,
"should not modify XMP when dc:title exists"
);
}
}
#[test]
fn test_fix_font_lastchar_widths_missing() {
let mut doc = make_basic_doc();
let font = dictionary! {
"Type" => "Font",
"Subtype" => Object::Name(b"Type1".to_vec()),
"BaseFont" => "TestFont",
"FirstChar" => Object::Integer(32),
};
let font_id = doc.add_object(Object::Dictionary(font));
fix_font_lastchar_widths(&mut doc);
if let Some(Object::Dictionary(d)) = doc.objects.get(&font_id) {
assert!(d.has(b"LastChar"), "LastChar should be added");
assert!(d.has(b"Widths"), "Widths should be added");
if let Ok(Object::Array(w)) = d.get(b"Widths") {
let lc = match d.get(b"LastChar").ok() {
Some(Object::Integer(v)) => *v,
_ => 0,
};
assert_eq!(w.len() as i64, lc - 32 + 1, "Widths length should match");
}
}
}
#[test]
fn test_fix_font_widths_wrong_length() {
let mut doc = make_basic_doc();
let font = dictionary! {
"Type" => "Font",
"Subtype" => Object::Name(b"TrueType".to_vec()),
"BaseFont" => "TestFont",
"FirstChar" => Object::Integer(0),
"LastChar" => Object::Integer(9),
"Widths" => Object::Array(vec![Object::Integer(500); 5]), };
let font_id = doc.add_object(Object::Dictionary(font));
fix_font_lastchar_widths(&mut doc);
if let Some(Object::Dictionary(d)) = doc.objects.get(&font_id) {
if let Ok(Object::Array(w)) = d.get(b"Widths") {
assert_eq!(w.len(), 10, "Widths should be padded to 10");
}
}
}
#[test]
fn test_truncate_long_names() {
let mut doc = make_basic_doc();
let long_name = vec![b'A'; 200];
let dict = dictionary! {
"SomeKey" => Object::Name(long_name),
};
let id = doc.add_object(Object::Dictionary(dict));
truncate_long_names(&mut doc);
if let Some(Object::Dictionary(d)) = doc.objects.get(&id) {
if let Ok(Object::Name(n)) = d.get(b"SomeKey") {
assert!(n.len() <= 127, "Name should be truncated to 127 bytes");
}
}
}
#[test]
fn test_truncate_long_names_short_ok() {
let mut doc = make_basic_doc();
let dict = dictionary! {
"Key" => Object::Name(b"ShortName".to_vec()),
};
let id = doc.add_object(Object::Dictionary(dict));
truncate_long_names(&mut doc);
if let Some(Object::Dictionary(d)) = doc.objects.get(&id) {
if let Ok(Object::Name(n)) = d.get(b"Key") {
assert_eq!(n, b"ShortName");
}
}
}
#[test]
fn test_fix_soft_mask_colorspace() {
let mut doc = make_basic_doc();
let smask_stream = Stream::new(
dictionary! {
"Subtype" => Object::Name(b"Image".to_vec()),
"ColorSpace" => Object::Name(b"DeviceRGB".to_vec()),
"Matte" => Object::Array(vec![Object::Integer(0)]),
"Width" => Object::Integer(10),
"Height" => Object::Integer(10),
"BitsPerComponent" => Object::Integer(8),
},
vec![0u8; 100],
);
let smask_id = doc.add_object(Object::Stream(smask_stream));
fix_soft_mask_colorspace(&mut doc);
if let Some(Object::Stream(s)) = doc.objects.get(&smask_id) {
assert_eq!(
s.dict.get(b"ColorSpace").ok(),
Some(&Object::Name(b"DeviceGray".to_vec())),
"Soft mask should be DeviceGray"
);
}
}
#[test]
fn test_fix_soft_mask_via_smask_ref() {
let mut doc = make_basic_doc();
let smask_stream = Stream::new(
dictionary! {
"Subtype" => Object::Name(b"Image".to_vec()),
"ColorSpace" => Object::Name(b"DeviceRGB".to_vec()),
"Width" => Object::Integer(10),
"Height" => Object::Integer(10),
"BitsPerComponent" => Object::Integer(8),
},
vec![0u8; 100],
);
let smask_id = doc.add_object(Object::Stream(smask_stream));
let parent_stream = Stream::new(
dictionary! {
"Subtype" => Object::Name(b"Image".to_vec()),
"ColorSpace" => Object::Name(b"DeviceRGB".to_vec()),
"Width" => Object::Integer(10),
"Height" => Object::Integer(10),
"BitsPerComponent" => Object::Integer(8),
"SMask" => Object::Reference(smask_id),
},
vec![0u8; 300],
);
doc.add_object(Object::Stream(parent_stream));
fix_soft_mask_colorspace(&mut doc);
if let Some(Object::Stream(s)) = doc.objects.get(&smask_id) {
assert_eq!(
s.dict.get(b"ColorSpace").ok(),
Some(&Object::Name(b"DeviceGray".to_vec())),
"SMask referenced image should be DeviceGray"
);
}
}
#[test]
fn test_version_fix() {
let mut doc = make_basic_doc();
doc.version = "2.0".to_string();
let _ = cleanup_for_pdfa(&mut doc, false).unwrap();
assert_eq!(doc.version, "1.7");
}
#[test]
fn test_fix_pdf_header_v2() {
let mut data = b"%PDF-2.0\ntest".to_vec();
fix_pdf_header(&mut data);
assert!(data.starts_with(b"%PDF-1.7"));
}
#[test]
fn fix_lang_does_not_duplicate_bdc_without_lang() {
let input = b"/Span <</MCID 0 >> BDC (x) Tj EMC\n";
let out = fix_lang_in_content_stream(input);
assert_eq!(out.as_slice(), input);
}
#[test]
fn fix_lang_preserves_actualtext_dict() {
let input = b"/Span<</ActualText<FEFF0020>>> BDC ( ) Tj EMC\n";
let out = fix_lang_in_content_stream(input);
assert_eq!(out.as_slice(), input);
}
}