use std::collections::HashMap;
use std::io;
use zpdf_core::{ObjectId, PdfDict, PdfName, PdfObject, PdfStream, Result};
use zpdf_parser::PdfFile;
use crate::encrypt::{EncryptionConfig, Encryptor};
use crate::serialize::{write_object, write_stream};
use crate::{flate_compress, invalid_data};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PdfaProfile {
A1b,
A2b,
}
#[derive(Debug, Clone)]
pub struct PdfaConvertConfig {
pub profile: PdfaProfile,
pub icc: Option<Vec<u8>>,
pub fallback_font: Option<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub struct RewriteOptions {
pub compress_uncompressed: bool,
pub encrypt: Option<EncryptionConfig>,
pub max_image_dimension: Option<u32>,
pub pdfa: Option<PdfaConvertConfig>,
}
impl Default for RewriteOptions {
fn default() -> Self {
Self {
compress_uncompressed: true,
encrypt: None,
max_image_dimension: None,
pdfa: None,
}
}
}
pub fn rewrite_pdf(source: &PdfFile, options: &RewriteOptions) -> Result<Vec<u8>> {
let root = source
.trailer
.get_ref("Root")
.map_err(|_| invalid_data("trailer missing /Root"))?;
let info = source.trailer.get_ref("Info").ok();
let mut map: HashMap<ObjectId, u32> = HashMap::new();
let mut order: Vec<ObjectId> = Vec::new();
let mut queue: Vec<ObjectId> = vec![root];
if let Some(info_id) = info {
queue.push(info_id);
}
for id in &queue {
map.insert(*id, 0); }
let mut head = 0;
while head < queue.len() {
let id = queue[head];
head += 1;
order.push(id);
let obj = source.resolve(id)?;
collect_refs(&obj, &mut map, &mut queue);
}
for (n, id) in order.iter().enumerate() {
map.insert(*id, (n + 1) as u32);
}
let id_first: Vec<u8> = match source.trailer.get("ID") {
Some(PdfObject::Array(a)) => match a.first() {
Some(PdfObject::String(s)) if !s.0.is_empty() => s.0.clone(),
_ => derive_file_id(source),
},
_ => derive_file_id(source),
};
let encryptor = match &options.encrypt {
Some(config) => Some(Encryptor::new(config, &id_first)?),
None => None,
};
let pdfa_edits = match &options.pdfa {
Some(cfg) => crate::pdfa_convert::prepare(source, &order, &map, cfg)?,
None => crate::pdfa_convert::PdfaEdits::default(),
};
let pdfa_active = options.pdfa.is_some();
if pdfa_active && encryptor.is_some() {
return Err(invalid_data(
"--pdfa cannot be combined with --encrypt (PDF/A forbids encryption)",
)
.into());
}
let mut out: Vec<u8> = Vec::new();
out.extend_from_slice(pdfa_edits.header.as_bytes());
out.extend_from_slice(b"\n%\xE2\xE3\xCF\xD3\n");
let mut offsets: Vec<u64> = Vec::with_capacity(order.len());
for (n, id) in order.iter().enumerate() {
let new_num = (n + 1) as u32;
offsets.push(out.len() as u64);
let mut obj = if let Some(t) = pdfa_edits.transforms.get(id) {
t.clone()
} else {
renumber(&source.resolve(*id)?, &map)
};
if let Some(max_dim) = options.max_image_dimension {
if let PdfObject::Stream(stream) = &mut obj {
if let Some(smaller) = downsample_image_stream(stream, max_dim) {
*stream = smaller;
}
}
}
if let Some(enc) = &encryptor {
let new_id = ObjectId(new_num, 0);
enc.encrypt_strings(&mut obj, new_id);
if let PdfObject::Stream(stream) = &mut obj {
let (dict, data) = prepared_stream_parts(stream, options.compress_uncompressed);
let encrypted = enc.encrypt_bytes(new_id, &data);
*stream = PdfStream {
dict,
data: encrypted.into(),
};
write_stream(&mut out, new_num, 0, &stream.dict, &stream.data)
.map_err(zpdf_core::Error::Io)?;
continue;
}
write_object(&mut out, new_num, 0, &obj).map_err(zpdf_core::Error::Io)?;
continue;
}
emit(&mut out, new_num, obj, options).map_err(zpdf_core::Error::Io)?;
}
for (num, extra) in &pdfa_edits.extras {
offsets.push(out.len() as u64);
match extra {
crate::pdfa_convert::ExtraObj::Object(obj) => {
write_object(&mut out, *num, 0, obj).map_err(zpdf_core::Error::Io)?
}
crate::pdfa_convert::ExtraObj::Stream(dict, data) => {
write_stream(&mut out, *num, 0, dict, data).map_err(zpdf_core::Error::Io)?
}
}
}
let encrypt_ref = match &encryptor {
Some(enc) => {
let num = (order.len() + 1) as u32;
offsets.push(out.len() as u64);
write_object(
&mut out,
num,
0,
&PdfObject::Dict(enc.encrypt_dict().clone()),
)
.map_err(zpdf_core::Error::Io)?;
Some(ObjectId(num, 0))
}
None => None,
};
let xref_pos = out.len();
let size = offsets.len() + 1;
out.extend_from_slice(format!("xref\n0 {size}\n").as_bytes());
out.extend_from_slice(b"0000000000 65535 f \n");
for offset in &offsets {
if *offset > 9_999_999_999 {
return Err(invalid_data("xref offset exceeds ten decimal digits").into());
}
out.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
}
let mut trailer = PdfDict::new();
trailer.insert(PdfName::new("Size"), PdfObject::Integer(size as i64));
trailer.insert(
PdfName::new("Root"),
PdfObject::Ref(ObjectId(map[&root], 0)),
);
if let Some(info_id) = info {
if let Some(&n) = map.get(&info_id) {
trailer.insert(PdfName::new("Info"), PdfObject::Ref(ObjectId(n, 0)));
}
}
if let Some(enc_ref) = encrypt_ref {
trailer.insert(PdfName::new("Encrypt"), PdfObject::Ref(enc_ref));
trailer.insert(
PdfName::new("ID"),
PdfObject::Array(vec![
PdfObject::String(zpdf_core::PdfString(id_first.clone())),
PdfObject::String(zpdf_core::PdfString(id_first)),
]),
);
} else if let Some(id_arr @ PdfObject::Array(_)) = source.trailer.get("ID") {
trailer.insert(PdfName::new("ID"), id_arr.clone());
} else if pdfa_active {
trailer.insert(
PdfName::new("ID"),
PdfObject::Array(vec![
PdfObject::String(zpdf_core::PdfString(id_first.clone())),
PdfObject::String(zpdf_core::PdfString(id_first)),
]),
);
}
out.extend_from_slice(b"trailer\n");
crate::serialize::serialize_dict(&mut out, &trailer).map_err(zpdf_core::Error::Io)?;
out.extend_from_slice(format!("\nstartxref\n{xref_pos}\n%%EOF\n").as_bytes());
Ok(out)
}
fn derive_file_id(source: &PdfFile) -> Vec<u8> {
use sha2::Digest;
let data = source.data();
let mut h = sha2::Sha256::new();
h.update((data.len() as u64).to_le_bytes());
h.update(&data[..data.len().min(1024)]);
h.finalize()[..16].to_vec()
}
fn prepared_stream_parts(stream: &PdfStream, compress: bool) -> (PdfDict, Vec<u8>) {
let has_filter = stream.dict.get("Filter").is_some();
if compress && !has_filter && stream.data.len() >= 64 {
let compressed = flate_compress(&stream.data);
if compressed.len() < stream.data.len() {
let mut dict = stream.dict.clone();
dict.insert(
PdfName::new("Filter"),
PdfObject::Name(PdfName::new("FlateDecode")),
);
return (dict, compressed);
}
}
(stream.dict.clone(), stream.data.to_vec())
}
fn downsample_image_stream(stream: &PdfStream, max_dim: u32) -> Option<PdfStream> {
let dict = &stream.dict;
if dict.get_name("Subtype").ok() != Some("Image") {
return None;
}
match dict.get("Filter") {
None => {}
Some(PdfObject::Name(n)) if n.as_str() == "FlateDecode" => {}
_ => return None,
}
if dict.get("Mask").is_some() || dict.get("Decode").is_some() {
return None;
}
if dict.get("DecodeParms").is_some() || dict.get("DP").is_some() {
return None;
}
if dict.get_i64("BitsPerComponent").ok() != Some(8) {
return None;
}
let channels: u32 = match dict.get_name("ColorSpace").ok() {
Some("DeviceRGB") => 3,
Some("DeviceGray") => 1,
_ => return None,
};
let width = u32::try_from(dict.get_i64("Width").ok()?).ok()?;
let height = u32::try_from(dict.get_i64("Height").ok()?).ok()?;
if width.max(height) <= max_dim || width == 0 || height == 0 {
return None;
}
let raw: Vec<u8> = if dict.get("Filter").is_some() {
use flate2::read::ZlibDecoder;
use std::io::Read;
let mut decoder = ZlibDecoder::new(stream.data.as_ref());
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).ok()?;
buf
} else {
stream.data.to_vec()
};
let row = (width as usize).checked_mul(channels as usize)?;
if raw.len() < row.checked_mul(height as usize)? {
return None;
}
let mut cur = raw;
let (mut w, mut h) = (width, height);
while w.max(h) > max_dim && w >= 2 && h >= 2 {
let (nw, nh) = (w / 2, h / 2);
let mut next = vec![0u8; nw as usize * nh as usize * channels as usize];
for y in 0..nh as usize {
for x in 0..nw as usize {
for c in 0..channels as usize {
let idx = |xx: usize, yy: usize| (yy * w as usize + xx) * channels as usize + c;
let sum = cur[idx(2 * x, 2 * y)] as u32
+ cur[idx(2 * x + 1, 2 * y)] as u32
+ cur[idx(2 * x, 2 * y + 1)] as u32
+ cur[idx(2 * x + 1, 2 * y + 1)] as u32;
next[(y * nw as usize + x) * channels as usize + c] = (sum / 4) as u8;
}
}
}
cur = next;
w = nw;
h = nh;
}
if (w, h) == (width, height) {
return None;
}
let mut new_dict = dict.clone();
new_dict.insert(PdfName::new("Width"), PdfObject::Integer(w as i64));
new_dict.insert(PdfName::new("Height"), PdfObject::Integer(h as i64));
new_dict.insert(
PdfName::new("Filter"),
PdfObject::Name(PdfName::new("FlateDecode")),
);
new_dict.0.remove(&PdfName::new("DecodeParms"));
Some(PdfStream {
dict: new_dict,
data: flate_compress(&cur).into(),
})
}
fn collect_refs(obj: &PdfObject, map: &mut HashMap<ObjectId, u32>, queue: &mut Vec<ObjectId>) {
match obj {
PdfObject::Ref(r) => {
if !map.contains_key(r) {
map.insert(*r, 0);
queue.push(*r);
}
}
PdfObject::Array(arr) => {
for elem in arr {
collect_refs(elem, map, queue);
}
}
PdfObject::Dict(dict) => {
for v in dict.0.values() {
collect_refs(v, map, queue);
}
}
PdfObject::Stream(stream) => {
for v in stream.dict.0.values() {
collect_refs(v, map, queue);
}
}
_ => {}
}
}
pub(crate) fn renumber(obj: &PdfObject, map: &HashMap<ObjectId, u32>) -> PdfObject {
match obj {
PdfObject::Ref(r) => match map.get(r) {
Some(&n) => PdfObject::Ref(ObjectId(n, 0)),
None => PdfObject::Null,
},
PdfObject::Array(arr) => PdfObject::Array(arr.iter().map(|e| renumber(e, map)).collect()),
PdfObject::Dict(dict) => PdfObject::Dict(renumber_dict(dict, map)),
PdfObject::Stream(stream) => PdfObject::Stream(PdfStream {
dict: renumber_dict(&stream.dict, map),
data: stream.data.clone(),
}),
other => other.clone(),
}
}
fn renumber_dict(dict: &PdfDict, map: &HashMap<ObjectId, u32>) -> PdfDict {
let mut out = PdfDict::new();
for (k, v) in &dict.0 {
out.insert(k.clone(), renumber(v, map));
}
out
}
fn emit(out: &mut Vec<u8>, num: u32, obj: PdfObject, options: &RewriteOptions) -> io::Result<()> {
match obj {
PdfObject::Stream(stream) => {
let has_filter = stream.dict.get("Filter").is_some();
if options.compress_uncompressed && !has_filter && stream.data.len() >= 64 {
let compressed = flate_compress(&stream.data);
if compressed.len() < stream.data.len() {
let mut dict = stream.dict.clone();
dict.insert(
PdfName::new("Filter"),
PdfObject::Name(PdfName::new("FlateDecode")),
);
return write_stream(out, num, 0, &dict, &compressed);
}
}
write_stream(out, num, 0, &stream.dict, &stream.data)
}
other => write_object(out, num, 0, &other),
}
}