use std::io::Write as _;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use super::reader::PdfReader;
use crate::{Result, RevenantError};
pub(crate) type RawObject = (Vec<u8>, u32);
pub const CMS_RESERVED_SIZE: usize = 8192;
pub const CMS_HEX_SIZE: usize = CMS_RESERVED_SIZE * 2;
pub const BYTERANGE_PLACEHOLDER: &str = "/ByteRange [ 0 0 0 0]";
pub const ANNOT_FLAGS_SIG_WIDGET: u32 = 4 | 128;
#[must_use]
pub fn pdf_string(text: &str) -> String {
use std::fmt::Write as _;
let mut result = String::with_capacity(text.len());
let mut replaced = 0usize;
for ch in text.chars() {
let code = ch as u32;
match ch {
'\\' => result.push_str("\\\\"),
'(' => result.push_str("\\("),
')' => result.push_str("\\)"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
_ if code < 0x20 || code == 0x7F => {
let _ = write!(result, "\\{code:03o}");
}
_ if code > 0xFF => {
result.push('?');
replaced += 1;
}
_ => result.push(ch),
}
}
if replaced > 0 {
log::warn!("pdf_string: {replaced} non-Latin1 character(s) replaced with '?' in: {text:?}");
}
result
}
#[must_use]
pub(super) fn pdf_text_string(text: &str) -> String {
use std::fmt::Write as _;
if text.is_ascii() {
return format!("({})", pdf_string(text));
}
let mut hex = String::from("FEFF");
for unit in text.encode_utf16() {
let _ = write!(hex, "{unit:04X}");
}
format!("<{hex}>")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FontObjNums {
pub font: u32, pub cidfont: u32, pub font_desc: u32, pub font_file: u32, pub tounicode: u32, }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FormObjNums {
pub ap: u32, pub frm: u32, pub n0: u32, pub n2: u32, }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VisibleObjNums {
pub fonts: FontObjNums,
pub forms: FormObjNums,
pub img: Option<u32>,
pub smask: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SigObjectNums {
pub sig: u32,
pub annot: u32,
pub visible: Option<VisibleObjNums>,
pub new_size: u32,
}
impl SigObjectNums {
#[must_use]
pub fn allocate(prev_size: u32, has_image: bool, has_smask: bool, visible: bool) -> Self {
fn take(next: &mut u32) -> u32 {
let n = *next;
*next += 1;
n
}
let mut next = prev_size;
let sig = take(&mut next);
let annot = take(&mut next);
if !visible {
return Self {
sig,
annot,
visible: None,
new_size: next,
};
}
let fonts = FontObjNums {
font: take(&mut next),
cidfont: take(&mut next),
font_desc: take(&mut next),
font_file: take(&mut next),
tounicode: take(&mut next),
};
let forms = FormObjNums {
ap: take(&mut next),
frm: take(&mut next),
n0: take(&mut next),
n2: take(&mut next),
};
let img = has_image.then(|| take(&mut next));
let smask = (has_image && has_smask).then(|| take(&mut next));
Self {
sig,
annot,
visible: Some(VisibleObjNums {
fonts,
forms,
img,
smask,
}),
new_size: next,
}
}
}
pub fn build_page_override(
reader: &PdfReader,
page_obj_num: u32,
annots_list: &str,
) -> Result<Vec<u8>> {
reader.object_override(
page_obj_num,
"/Annots",
&format!(" /Annots [{annots_list}]"),
)
}
pub(super) fn deflate(data: &[u8]) -> Result<Vec<u8>> {
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder
.write_all(data)
.map_err(|e| RevenantError::Pdf(format!("Deflate compression failed: {e}")))?;
encoder
.finish()
.map_err(|e| RevenantError::Pdf(format!("Deflate compression failed: {e}")))
}
pub fn build_catalog_override(
reader: &PdfReader,
root_obj_num: u32,
annot_obj_num: u32,
) -> Result<Vec<u8>> {
reader.catalog_override_with_sig_field(root_obj_num, annot_obj_num)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn byterange_placeholder_matches_real_value_width() {
let real = format!(
"/ByteRange [{:>10} {:>10} {:>10} {:>10}]",
0, 12345, 67890, 111
);
assert_eq!(real.len(), BYTERANGE_PLACEHOLDER.len());
assert_eq!(BYTERANGE_PLACEHOLDER.len(), 56);
let zeros = format!("/ByteRange [{:>10} {:>10} {:>10} {:>10}]", 0, 0, 0, 0);
assert_eq!(zeros, BYTERANGE_PLACEHOLDER);
}
#[test]
fn cms_sizes() {
assert_eq!(CMS_HEX_SIZE, 16384);
assert_eq!(ANNOT_FLAGS_SIG_WIDGET, 132);
}
#[test]
fn pdf_string_escapes_specials() {
assert_eq!(pdf_string("a(b)c\\d"), "a\\(b\\)c\\\\d");
assert_eq!(pdf_string("line\tbreak"), "line\\tbreak");
assert_eq!(pdf_string("\u{01}"), "\\001");
}
#[test]
fn pdf_string_replaces_non_latin1() {
assert_eq!(pdf_string("Ա"), "?");
assert_eq!(pdf_string("é"), "é");
}
#[test]
fn pdf_text_string_ascii_uses_literal_form() {
assert_eq!(pdf_text_string("John Doe"), "(John Doe)");
assert_eq!(pdf_text_string("a(b)\\c"), "(a\\(b\\)\\\\c)");
}
#[test]
fn pdf_text_string_non_ascii_uses_utf16be_with_bom() {
assert_eq!(pdf_text_string("Ա"), "<FEFF0531>");
assert_eq!(pdf_text_string("é"), "<FEFF00E9>");
let out = pdf_text_string("Բարեւ");
assert!(out.starts_with("<FEFF") && out.ends_with('>'), "{out}");
assert_eq!(out.len(), 1 + 4 + "Բարեւ".chars().count() * 4 + 1);
}
#[test]
fn allocate_invisible_only_sig_and_annot() {
let n = SigObjectNums::allocate(10, false, false, false);
assert_eq!(n.sig, 10);
assert_eq!(n.annot, 11);
assert!(n.visible.is_none());
assert_eq!(n.new_size, 12);
}
#[test]
fn allocate_visible_no_image() {
let n = SigObjectNums::allocate(10, false, false, true);
let v = n.visible.expect("visible");
assert_eq!(n.sig, 10);
assert_eq!(n.annot, 11);
assert_eq!(v.fonts.font, 12);
assert_eq!(v.fonts.tounicode, 16);
assert_eq!(v.forms.ap, 17);
assert_eq!(v.forms.n2, 20);
assert!(v.img.is_none());
assert!(v.smask.is_none());
assert_eq!(n.new_size, 21);
}
#[test]
fn allocate_visible_with_image_and_smask() {
let n = SigObjectNums::allocate(10, true, true, true);
let v = n.visible.expect("visible");
assert_eq!(v.img, Some(21));
assert_eq!(v.smask, Some(22));
assert_eq!(n.new_size, 23);
}
#[test]
fn allocate_image_without_smask() {
let n = SigObjectNums::allocate(10, true, false, true);
let v = n.visible.expect("visible");
assert_eq!(v.img, Some(21));
assert!(v.smask.is_none());
assert_eq!(n.new_size, 22);
}
}