use std::collections::BTreeMap;
use std::io::{self, Seek, Write};
use std::sync::Arc;
use zpdf_core::{ObjectId, PdfDict, PdfName, PdfObject, Result};
use zpdf_document::{InkAnnotDict, PdfDocument};
mod serialize;
use serialize::{serialize_dict, serialize_object, serialize_stream};
pub mod forms;
pub mod metadata;
pub mod pages;
pub mod stamp;
pub use forms::FormFiller;
pub use metadata::InfoUpdate;
pub use stamp::{jpeg_dimensions, StampImage, StampItem};
enum PendingObject {
Object(PdfObject),
Stream(PdfDict, Vec<u8>),
}
enum XrefKind {
Table,
Stream,
}
pub struct IncrementalWriter {
doc: PdfDocument,
next_obj_num: u32,
pending: BTreeMap<u32, (u16, PendingObject)>,
original_xref_pos: u64,
catalog_ref: ObjectId,
xref_kind: XrefKind,
info_ref_override: Option<ObjectId>,
}
impl IncrementalWriter {
pub fn new(original: Vec<u8>) -> Result<Self> {
let original_arc: Arc<[u8]> = original.into();
let doc = PdfDocument::open(original_arc)?;
if doc.is_encrypted() {
return Err(unsupported("cannot incrementally update an encrypted document").into());
}
let file = doc.file();
let size = file
.trailer
.get("Size")
.and_then(|o| match o {
PdfObject::Integer(n) => Some(*n as u32),
_ => None,
})
.or_else(|| {
file.all_object_ids().iter().map(|id| id.0 + 1).max()
})
.ok_or_else(|| invalid_data("trailer missing /Size"))?;
let root = file
.trailer
.get_ref("Root")
.map_err(|_| invalid_data("trailer missing /Root"))?;
let original_xref_pos = find_startxref(file.data())
.ok_or_else(|| invalid_data("could not find startxref in original PDF"))?;
let xref_kind = detect_xref_kind(file.data(), original_xref_pos);
Ok(Self {
doc,
next_obj_num: size,
pending: BTreeMap::new(),
original_xref_pos,
catalog_ref: root,
xref_kind,
info_ref_override: None,
})
}
pub fn document(&self) -> &PdfDocument {
&self.doc
}
pub fn add_object(&mut self, obj: &PdfObject) -> (u32, u32) {
let num = self.next_obj_num;
self.next_obj_num += 1;
self.pending
.insert(num, (0, PendingObject::Object(obj.clone())));
(num, 0)
}
pub fn add_stream(&mut self, dict: &PdfDict, data: &[u8]) -> (u32, u32) {
let num = self.next_obj_num;
self.next_obj_num += 1;
self.pending
.insert(num, (0, PendingObject::Stream(dict.clone(), data.to_vec())));
(num, 0)
}
pub fn add_flate_stream(&mut self, dict: &PdfDict, raw: &[u8]) -> (u32, u32) {
let mut dict = dict.clone();
dict.insert(
PdfName::new("Filter"),
PdfObject::Name(PdfName::new("FlateDecode")),
);
self.add_stream(&dict, &flate_compress(raw))
}
pub fn overwrite_object(&mut self, id: ObjectId, obj: PdfObject) {
self.pending
.insert(id.0, (id.1, PendingObject::Object(obj)));
}
pub fn resolve_current(&self, id: ObjectId) -> Result<PdfObject> {
match self.pending.get(&id.0) {
Some((_, PendingObject::Object(obj))) => Ok(obj.clone()),
Some((_, PendingObject::Stream(dict, data))) => Ok(PdfObject::Stream(
zpdf_core::PdfStream::new(dict.clone(), data.clone()),
)),
None => self.doc.file().resolve(id),
}
}
pub(crate) fn deref_current(&self, obj: &PdfObject) -> PdfObject {
match obj {
PdfObject::Ref(r) => self.resolve_current(*r).unwrap_or(PdfObject::Null),
other => other.clone(),
}
}
pub(crate) fn set_info_ref(&mut self, id: ObjectId) {
self.info_ref_override = Some(id);
}
pub fn add_ink_annotation_to_page(
&mut self,
page_index: usize,
annot_dict: &InkAnnotDict,
appearance_stream: &[u8],
) -> Result<()> {
let page_id = self.page_id(page_index)?;
let appearance_dict = self.build_appearance_dict(annot_dict);
let ap_ref = self.add_stream(&appearance_dict, appearance_stream);
let annot_pdf_dict = self.build_annot_dict(annot_dict, ap_ref);
let annot_ref = self.add_object(&PdfObject::Dict(annot_pdf_dict));
self.append_page_annot(page_id, ObjectId(annot_ref.0, annot_ref.1 as u16))
}
pub(crate) fn page_id(&self, page_index: usize) -> Result<ObjectId> {
Ok(self
.doc
.page(page_index)
.map_err(|_| invalid_data(&format!("page {} not found", page_index)))?
.id)
}
fn append_page_annot(&mut self, page_id: ObjectId, annot_ref: ObjectId) -> Result<()> {
let page_obj = self.resolve_current(page_id)?;
let mut page_dict = page_obj.as_dict()?.clone();
let mut annots = match page_dict.get("Annots") {
Some(PdfObject::Ref(r)) => match self.resolve_current(*r) {
Ok(obj) => obj.as_array().ok().map(|a| a.to_vec()).unwrap_or_default(),
Err(_) => Vec::new(),
},
Some(PdfObject::Array(arr)) => arr.to_vec(),
_ => Vec::new(),
};
annots.push(PdfObject::Ref(annot_ref));
page_dict.insert(PdfName::new("Annots"), PdfObject::Array(annots));
self.overwrite_object(page_id, PdfObject::Dict(page_dict));
Ok(())
}
fn build_appearance_dict(&self, annot: &InkAnnotDict) -> PdfDict {
let mut dict = PdfDict::new();
dict.insert(
PdfName("Type".to_string()),
PdfObject::Name(PdfName("XObject".to_string())),
);
dict.insert(
PdfName("Subtype".to_string()),
PdfObject::Name(PdfName("Form".to_string())),
);
dict.insert(PdfName("FormType".to_string()), PdfObject::Integer(1));
dict.insert(
PdfName("BBox".to_string()),
PdfObject::Array(vec![
PdfObject::Real(annot.rect.x0),
PdfObject::Real(annot.rect.y0),
PdfObject::Real(annot.rect.x1),
PdfObject::Real(annot.rect.y1),
]),
);
dict
}
fn build_annot_dict(&self, annot: &InkAnnotDict, ap_ref: (u32, u32)) -> PdfDict {
let mut dict = PdfDict::new();
dict.insert(
PdfName("Type".to_string()),
PdfObject::Name(PdfName("Annot".to_string())),
);
dict.insert(
PdfName("Subtype".to_string()),
PdfObject::Name(PdfName("Ink".to_string())),
);
dict.insert(
PdfName("Rect".to_string()),
PdfObject::Array(vec![
PdfObject::Real(annot.rect.x0),
PdfObject::Real(annot.rect.y0),
PdfObject::Real(annot.rect.x1),
PdfObject::Real(annot.rect.y1),
]),
);
let ink_list: Vec<PdfObject> = annot
.ink_list
.iter()
.map(|stroke| {
let coords: Vec<PdfObject> = stroke
.iter()
.flat_map(|&(x, y)| vec![PdfObject::Real(x), PdfObject::Real(y)])
.collect();
PdfObject::Array(coords)
})
.collect();
dict.insert(PdfName("InkList".to_string()), PdfObject::Array(ink_list));
let (r, g, b) = annot.color;
dict.insert(
PdfName("C".to_string()),
PdfObject::Array(vec![
PdfObject::Real(r),
PdfObject::Real(g),
PdfObject::Real(b),
]),
);
let mut bs = PdfDict::new();
bs.insert(PdfName("W".to_string()), PdfObject::Real(annot.width));
dict.insert(PdfName("BS".to_string()), PdfObject::Dict(bs));
let mut ap = PdfDict::new();
ap.insert(
PdfName("N".to_string()),
PdfObject::Ref(ObjectId(ap_ref.0, ap_ref.1 as u16)),
);
dict.insert(PdfName("AP".to_string()), PdfObject::Dict(ap));
dict
}
pub fn write<W: Write + Seek>(&self, mut out: W) -> io::Result<()> {
let original = self.doc.file().data();
out.write_all(original)?;
if !matches!(original.last(), Some(b'\n') | Some(b'\r')) {
out.write_all(b"\n")?;
}
let mut xref_entries: Vec<(u32, u16, u64)> = Vec::new();
for (&num, (gen, pending)) in &self.pending {
let offset = out.stream_position()?;
xref_entries.push((num, *gen, offset));
let bytes = match pending {
PendingObject::Object(obj) => serialize_object(num, *gen as u32, obj),
PendingObject::Stream(dict, data) => serialize_stream(num, *gen as u32, dict, data),
};
out.write_all(&bytes)?;
}
let xref_pos = out.stream_position()?;
match self.xref_kind {
XrefKind::Table => {
self.write_xref_table(&mut out, &xref_entries)?;
self.write_trailer(&mut out)?;
}
XrefKind::Stream => self.write_xref_stream(&mut out, &xref_entries, xref_pos)?,
}
writeln!(out, "startxref")?;
writeln!(out, "{}", xref_pos)?;
writeln!(out, "%%EOF")?;
Ok(())
}
fn trailer_dict(&self, size: u32) -> PdfDict {
let mut trailer = PdfDict::new();
trailer.insert(PdfName::new("Size"), PdfObject::Integer(size as i64));
trailer.insert(
PdfName::new("Prev"),
PdfObject::Integer(self.original_xref_pos as i64),
);
trailer.insert(PdfName::new("Root"), PdfObject::Ref(self.catalog_ref));
let orig = &self.doc.file().trailer;
match self.info_ref_override {
Some(id) => {
trailer.insert(PdfName::new("Info"), PdfObject::Ref(id));
}
None => {
if let Some(PdfObject::Ref(r)) = orig.get("Info") {
trailer.insert(PdfName::new("Info"), PdfObject::Ref(*r));
}
}
}
if let Some(id) = orig.get("ID") {
trailer.insert(PdfName::new("ID"), id.clone());
}
trailer
}
fn write_xref_table<W: Write>(
&self,
out: &mut W,
entries: &[(u32, u16, u64)],
) -> io::Result<()> {
writeln!(out, "xref")?;
for run in contiguous_runs(entries) {
writeln!(out, "{} {}", run[0].0, run.len())?;
for (_, gen, offset) in run {
writeln!(out, "{:010} {:05} n ", offset, gen)?;
}
}
Ok(())
}
fn write_trailer<W: Write>(&self, out: &mut W) -> io::Result<()> {
writeln!(out, "trailer")?;
let mut buf = Vec::new();
serialize_dict(&mut buf, &self.trailer_dict(self.next_obj_num));
out.write_all(&buf)?;
writeln!(out)?;
Ok(())
}
fn write_xref_stream<W: Write>(
&self,
out: &mut W,
entries: &[(u32, u16, u64)],
xref_pos: u64,
) -> io::Result<()> {
let stream_num = self.next_obj_num;
let mut all: Vec<(u32, u16, u64)> = entries.to_vec();
all.push((stream_num, 0, xref_pos));
let mut data = Vec::with_capacity(all.len() * 7);
let mut index = Vec::new();
for run in contiguous_runs(&all) {
index.push(PdfObject::Integer(run[0].0 as i64));
index.push(PdfObject::Integer(run.len() as i64));
for (_, gen, offset) in run {
data.push(1u8); data.extend_from_slice(&(*offset as u32).to_be_bytes());
data.extend_from_slice(&gen.to_be_bytes());
}
}
let mut dict = self.trailer_dict(stream_num + 1);
dict.insert(PdfName::new("Type"), PdfObject::Name(PdfName::new("XRef")));
dict.insert(
PdfName::new("W"),
PdfObject::Array(vec![
PdfObject::Integer(1),
PdfObject::Integer(4),
PdfObject::Integer(2),
]),
);
dict.insert(PdfName::new("Index"), PdfObject::Array(index));
dict.insert(
PdfName::new("Filter"),
PdfObject::Name(PdfName::new("FlateDecode")),
);
let bytes = serialize_stream(stream_num, 0, &dict, &flate_compress(&data));
out.write_all(&bytes)?;
Ok(())
}
pub fn delete_annotation(&mut self, page_index: usize, annot_id: ObjectId) -> Result<()> {
let page_id = self.page_id(page_index)?;
let page_obj = self.resolve_current(page_id)?;
let mut page_dict = page_obj.as_dict()?.clone();
let annots = match page_dict.get("Annots") {
Some(PdfObject::Ref(r)) => match self.resolve_current(*r) {
Ok(obj) => obj.as_array().ok().map(|a| a.to_vec()).unwrap_or_default(),
Err(_) => Vec::new(),
},
Some(PdfObject::Array(arr)) => arr.to_vec(),
_ => return Ok(()), };
let filtered: Vec<PdfObject> = annots
.into_iter()
.filter(|obj| match obj {
PdfObject::Ref(r) => *r != annot_id,
_ => true,
})
.collect();
page_dict.insert(PdfName::new("Annots"), PdfObject::Array(filtered));
self.overwrite_object(page_id, PdfObject::Dict(page_dict));
Ok(())
}
pub fn update_annotation_rect(
&mut self,
annot_id: ObjectId,
new_rect: zpdf_core::Rect,
) -> Result<()> {
let annot_obj = self.resolve_current(annot_id)?;
let mut annot_dict = annot_obj.as_dict()?.clone();
annot_dict.insert(
PdfName::new("Rect"),
PdfObject::Array(vec![
PdfObject::Real(new_rect.x0),
PdfObject::Real(new_rect.y0),
PdfObject::Real(new_rect.x1),
PdfObject::Real(new_rect.y1),
]),
);
self.overwrite_object(annot_id, PdfObject::Dict(annot_dict));
Ok(())
}
pub fn update_annotation_color(
&mut self,
annot_id: ObjectId,
color: (f64, f64, f64),
) -> Result<()> {
let annot_obj = self.resolve_current(annot_id)?;
let mut annot_dict = annot_obj.as_dict()?.clone();
annot_dict.insert(
PdfName::new("C"),
PdfObject::Array(vec![
PdfObject::Real(color.0),
PdfObject::Real(color.1),
PdfObject::Real(color.2),
]),
);
self.overwrite_object(annot_id, PdfObject::Dict(annot_dict));
Ok(())
}
pub fn update_annotation_contents(&mut self, annot_id: ObjectId, content: &str) -> Result<()> {
let annot_obj = self.resolve_current(annot_id)?;
let mut annot_dict = annot_obj.as_dict()?.clone();
let encoded = if content.is_ascii() {
content.as_bytes().to_vec()
} else {
let mut bytes = vec![0xfe, 0xff]; for ch in content.encode_utf16() {
bytes.push((ch >> 8) as u8);
bytes.push((ch & 0xff) as u8);
}
bytes
};
annot_dict.insert(
PdfName::new("Contents"),
PdfObject::String(zpdf_core::PdfString(encoded)),
);
self.overwrite_object(annot_id, PdfObject::Dict(annot_dict));
Ok(())
}
pub fn update_annotation_border_width(&mut self, annot_id: ObjectId, width: f64) -> Result<()> {
let annot_obj = self.resolve_current(annot_id)?;
let mut annot_dict = annot_obj.as_dict()?.clone();
let width = width.max(0.1);
let mut bs = match annot_dict.get("BS") {
Some(PdfObject::Dict(d)) => d.clone(),
_ => PdfDict::new(),
};
bs.insert(PdfName::new("W"), PdfObject::Real(width));
annot_dict.insert(PdfName::new("BS"), PdfObject::Dict(bs));
self.overwrite_object(annot_id, PdfObject::Dict(annot_dict));
Ok(())
}
}
fn contiguous_runs(entries: &[(u32, u16, u64)]) -> Vec<&[(u32, u16, u64)]> {
let mut runs = Vec::new();
let mut start = 0;
for i in 1..=entries.len() {
if i == entries.len() || entries[i].0 != entries[i - 1].0 + 1 {
runs.push(&entries[start..i]);
start = i;
}
}
runs
}
fn flate_compress(raw: &[u8]) -> Vec<u8> {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
let _ = enc.write_all(raw);
enc.finish().unwrap_or_default()
}
pub(crate) fn invalid_data(msg: &str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, msg.to_string())
}
pub(crate) fn unsupported(msg: &str) -> io::Error {
io::Error::new(io::ErrorKind::Unsupported, msg.to_string())
}
fn find_startxref(data: &[u8]) -> Option<u64> {
let tail = &data[data.len().saturating_sub(512)..];
let s = String::from_utf8_lossy(tail);
s.rfind("startxref").and_then(|pos| {
let after = &s[pos + "startxref".len()..];
after
.lines()
.nth(1) .and_then(|line| line.trim().parse::<u64>().ok())
})
}
fn detect_xref_kind(data: &[u8], pos: u64) -> XrefKind {
let start = (pos as usize).min(data.len());
let slice = &data[start..];
let trimmed = slice
.iter()
.position(|b| !b.is_ascii_whitespace())
.map(|i| &slice[i..])
.unwrap_or(&[]);
if trimmed.starts_with(b"xref") {
XrefKind::Table
} else {
XrefKind::Stream
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use zpdf_core::{ObjectId, Rect};
fn create_test_pdf_with_annotation() -> Vec<u8> {
let pdf = b"%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Annots [4 0 R] >>
endobj
4 0 obj
<< /Type /Annot /Subtype /Ink /Rect [100 100 200 200] /C [0 0 0] /InkList [[[100 100 200 200]]] /BS << /W 2 >> >>
endobj
xref
0 5
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000223 00000 n
trailer
<< /Size 5 /Root 1 0 R >>
startxref
362
%%EOF
";
pdf.to_vec()
}
#[test]
fn test_delete_annotation() {
let pdf = create_test_pdf_with_annotation();
let mut writer = IncrementalWriter::new(pdf).unwrap();
writer.delete_annotation(0, ObjectId(4, 0)).unwrap();
let mut output = Cursor::new(Vec::new());
writer.write(&mut output).unwrap();
let doc = PdfDocument::open(output.into_inner()).unwrap();
let page = doc.page(0).unwrap();
let annots = page.annots.len();
assert_eq!(annots, 0, "Annotation should be deleted");
}
#[test]
fn test_update_annotation_rect() {
let pdf = create_test_pdf_with_annotation();
let mut writer = IncrementalWriter::new(pdf).unwrap();
let new_rect = Rect {
x0: 150.0,
y0: 250.0,
x1: 350.0,
y1: 450.0,
};
writer
.update_annotation_rect(ObjectId(4, 0), new_rect)
.unwrap();
let mut output = Cursor::new(Vec::new());
writer.write(&mut output).unwrap();
let result = PdfDocument::open(output.into_inner());
assert!(result.is_ok(), "Updated PDF should be valid");
}
#[test]
fn test_update_annotation_color() {
let pdf = create_test_pdf_with_annotation();
let mut writer = IncrementalWriter::new(pdf).unwrap();
writer
.update_annotation_color(ObjectId(4, 0), (1.0, 0.0, 0.0))
.unwrap();
let mut output = Cursor::new(Vec::new());
writer.write(&mut output).unwrap();
let result = PdfDocument::open(output.into_inner());
assert!(result.is_ok(), "Updated PDF should be valid");
}
#[test]
fn test_update_annotation_border_width() {
let pdf = create_test_pdf_with_annotation();
let mut writer = IncrementalWriter::new(pdf).unwrap();
writer
.update_annotation_border_width(ObjectId(4, 0), 5.0)
.unwrap();
let mut output = Cursor::new(Vec::new());
writer.write(&mut output).unwrap();
let result = PdfDocument::open(output.into_inner());
assert!(result.is_ok(), "Updated PDF should be valid");
}
}