use std::io::{self, Write};
use crate::encrypt::EncryptionState;
use crate::error::PdfError;
#[derive(Clone, Debug)]
pub enum Object {
Null,
Bool(bool),
Integer(i64),
Real(f64),
Name(String),
LiteralString(Vec<u8>),
HexString(Vec<u8>),
Array(Vec<Object>),
Dict(Dict),
Reference(ObjectId),
Stream(Stream),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ObjectId {
pub number: u32,
pub generation: u16,
}
impl ObjectId {
pub const fn new(number: u32) -> Self {
Self {
number,
generation: 0,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Dict {
entries: Vec<(String, Object)>,
}
impl Dict {
pub fn new() -> Self {
Self::default()
}
pub fn set(&mut self, key: &str, value: Object) -> &mut Self {
if let Some(slot) = self.entries.iter_mut().find(|(k, _)| k == key) {
slot.1 = value;
} else {
self.entries.push((key.to_owned(), value));
}
self
}
pub fn with(mut self, key: &str, value: Object) -> Self {
self.set(key, value);
self
}
pub fn entries(&self) -> &[(String, Object)] {
&self.entries
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[derive(Clone, Debug)]
pub struct Stream {
pub dict: Dict,
pub data: Vec<u8>,
}
impl Stream {
pub fn new(dict: Dict, data: Vec<u8>) -> Self {
Self { dict, data }
}
}
#[derive(Clone, Debug)]
pub struct IndirectObject {
pub id: ObjectId,
pub object: Object,
}
#[derive(Default)]
pub struct Document {
objects: Vec<IndirectObject>,
next_id: u32,
pub root: Option<ObjectId>,
pub info: Option<ObjectId>,
pub encryption: Option<EncryptionState>,
pub xref_stream: bool,
pub object_stream: bool,
pub prev_xref_offset: Option<u64>,
pub min_size: Option<u32>,
pub xref_only_ids: Option<Vec<u32>>,
}
impl Document {
pub fn new() -> Self {
Self {
objects: Vec::new(),
next_id: 1,
root: None,
info: None,
encryption: None,
xref_stream: false,
object_stream: false,
prev_xref_offset: None,
min_size: None,
xref_only_ids: None,
}
}
pub fn set_next_id(&mut self, next_id: u32) {
self.next_id = next_id;
}
pub fn next_id(&self) -> u32 {
self.next_id
}
pub fn allocate_id(&mut self) -> ObjectId {
let id = ObjectId::new(self.next_id);
self.next_id += 1;
id
}
pub fn add_object(&mut self, id: ObjectId, object: Object) {
self.objects.push(IndirectObject { id, object });
}
pub fn add(&mut self, object: Object) -> ObjectId {
let id = self.allocate_id();
self.add_object(id, object);
id
}
pub fn object_count(&self) -> usize {
self.objects.len()
}
pub fn object_mut(&mut self, id: ObjectId) -> Option<&mut Object> {
self.objects
.iter_mut()
.find(|o| o.id == id)
.map(|o| &mut o.object)
}
pub fn write_to(&self, out: &mut Vec<u8>) -> Result<(), PdfError> {
let root = self
.root
.ok_or_else(|| PdfError::other("Document::write_to: missing /Root reference"))?;
if self.object_stream && !self.xref_stream {
return Err(PdfError::other(
"Document::write_to: object_stream=true requires xref_stream=true (ObjStm \
containers can only be referenced from a /Type /XRef stream — \
ISO 32000-1 §7.5.7)",
));
}
if self.prev_xref_offset.is_none() {
let header_version: &[u8] = if self
.encryption
.as_ref()
.map(|e| e.handler.revision >= 5)
.unwrap_or(false)
{
b"%PDF-2.0\n"
} else if self.xref_stream {
b"%PDF-1.5\n"
} else {
b"%PDF-1.4\n"
};
out.extend_from_slice(header_version);
out.extend_from_slice(b"%\xE2\xE3\xCF\xD3\n");
}
let mut objects_to_emit: Vec<IndirectObject> = self.objects.clone();
let mut compressed_map: std::collections::HashMap<u32, (u32, u32)> =
std::collections::HashMap::new();
let objstm_id_opt: Option<ObjectId> = if self.object_stream {
let mut compressible: Vec<IndirectObject> = Vec::new();
let mut keep: Vec<IndirectObject> = Vec::new();
for ind in objects_to_emit.drain(..) {
let is_stream = matches!(ind.object, Object::Stream(_));
let is_root = ind.id == root;
if !is_stream && !is_root {
compressible.push(ind);
} else {
keep.push(ind);
}
}
objects_to_emit = keep;
if compressible.is_empty() {
None
} else {
let max_kept = objects_to_emit
.iter()
.map(|o| o.id.number)
.max()
.unwrap_or(0);
let max_compressed = compressible.iter().map(|o| o.id.number).max().unwrap_or(0);
let mut next_id = max_kept
.max(max_compressed)
.max(self.next_id.saturating_sub(1))
+ 1;
if self.encryption.is_some() {
next_id += 1;
}
let objstm_id = ObjectId::new(next_id);
let mut bodies: Vec<Vec<u8>> = Vec::with_capacity(compressible.len());
for ind in &compressible {
let mut b = Vec::new();
write_object(&mut b, &ind.object).map_err(PdfError::Io)?;
bodies.push(b);
}
let mut header = String::new();
let mut running = 0usize;
for (ind, body) in compressible.iter().zip(bodies.iter()) {
if !header.is_empty() {
header.push(' ');
}
header.push_str(&format!("{} {}", ind.id.number, running));
running += body.len();
}
header.push(' ');
let header_bytes = header.into_bytes();
let first = header_bytes.len();
let n_compressed = compressible.len();
let mut payload =
Vec::with_capacity(first + bodies.iter().map(|b| b.len()).sum::<usize>());
payload.extend_from_slice(&header_bytes);
for body in &bodies {
payload.extend_from_slice(body);
}
let compressed = flate_compress(&payload);
let dict = Dict::new()
.with("Type", Object::Name("ObjStm".into()))
.with("N", Object::Integer(n_compressed as i64))
.with("First", Object::Integer(first as i64))
.with("Filter", Object::Name("FlateDecode".into()));
objects_to_emit.push(IndirectObject {
id: objstm_id,
object: Object::Stream(Stream::new(dict, compressed)),
});
for (idx, ind) in compressible.into_iter().enumerate() {
compressed_map.insert(ind.id.number, (objstm_id.number, idx as u32));
}
Some(objstm_id)
}
} else {
None
};
let encrypt_id_opt: Option<ObjectId> = if let Some(state) = &self.encryption {
let max_id_now = objects_to_emit
.iter()
.map(|o| o.id.number)
.max()
.unwrap_or(0);
let id = ObjectId::new(max_id_now + 1);
for ind in &mut objects_to_emit {
encrypt_object_in_place(&mut ind.object, ind.id, state)?;
}
objects_to_emit.push(IndirectObject {
id,
object: Object::Dict(state.encrypt_dict.clone()),
});
Some(id)
} else {
None
};
let xref_stream_id_opt: Option<ObjectId> = if self.xref_stream {
let mut max_existing = objects_to_emit
.iter()
.map(|o| o.id.number)
.max()
.unwrap_or(0);
if self.next_id.saturating_sub(1) > max_existing {
max_existing = self.next_id - 1;
}
if let Some(top) = compressed_map.keys().max() {
if *top > max_existing {
max_existing = *top;
}
}
Some(ObjectId::new(max_existing + 1))
} else {
None
};
objects_to_emit.sort_by_key(|o| o.id.number);
let body_max_id = objects_to_emit
.last()
.map(|o| o.id.number as usize)
.unwrap_or(0);
let mut max_id = match xref_stream_id_opt {
Some(id) => id.number as usize,
None => body_max_id,
};
if let Some(top) = compressed_map.keys().max() {
max_id = max_id.max(*top as usize);
}
if let Some(min) = self.min_size {
if (min as usize) > max_id + 1 {
max_id = (min as usize).saturating_sub(1);
}
}
let mut offsets: Vec<u64> = vec![0; max_id + 1];
for ind in &objects_to_emit {
let off = out.len() as u64;
offsets[ind.id.number as usize] = off;
write_indirect(out, ind).map_err(PdfError::Io)?;
}
let xref_off = out.len() as u64;
let subsections: Vec<(u32, u32)> = match &self.xref_only_ids {
Some(ids) => Self::group_into_subsections(ids),
None => vec![(0, (max_id + 1) as u32)],
};
if let Some(xref_stream_id) = xref_stream_id_opt {
offsets[xref_stream_id.number as usize] = xref_off;
self.write_xref_stream(
out,
xref_stream_id,
root,
encrypt_id_opt,
objstm_id_opt,
&offsets,
&compressed_map,
max_id,
&subsections,
)?;
} else {
out.extend_from_slice(b"xref\n");
for (start, count) in &subsections {
let header_line = format!("{} {}\n", start, count);
out.extend_from_slice(header_line.as_bytes());
for id in *start..(*start + *count) {
if id == 0 {
out.extend_from_slice(b"0000000000 65535 f \n");
} else {
let off = offsets.get(id as usize).copied().unwrap_or(0);
let line = format!("{:010} {:05} n \n", off, 0);
out.extend_from_slice(line.as_bytes());
}
}
}
out.extend_from_slice(b"trailer\n");
let trailer_dict = self.build_trailer_dict(root, encrypt_id_opt, max_id);
let trailer = Object::Dict(trailer_dict);
write_object(out, &trailer).map_err(PdfError::Io)?;
out.extend_from_slice(b"\n");
}
out.extend_from_slice(b"startxref\n");
out.extend_from_slice(format!("{}\n", xref_off).as_bytes());
out.extend_from_slice(b"%%EOF\n");
Ok(())
}
fn group_into_subsections(ids: &[u32]) -> Vec<(u32, u32)> {
let mut all = Vec::with_capacity(ids.len() + 1);
all.push(0);
all.extend_from_slice(ids);
all.sort_unstable();
all.dedup();
let mut out: Vec<(u32, u32)> = Vec::new();
let mut iter = all.iter().copied();
let Some(mut start) = iter.next() else {
return out;
};
let mut prev = start;
let mut count: u32 = 1;
for v in iter {
if v == prev + 1 {
count += 1;
} else {
out.push((start, count));
start = v;
count = 1;
}
prev = v;
}
out.push((start, count));
out
}
fn build_trailer_dict(
&self,
root: ObjectId,
encrypt_id_opt: Option<ObjectId>,
max_id: usize,
) -> Dict {
let mut trailer_dict = Dict::new()
.with("Size", Object::Integer((max_id + 1) as i64))
.with("Root", Object::Reference(root));
if let Some(info_id) = self.info {
trailer_dict.set("Info", Object::Reference(info_id));
}
if let Some(prev) = self.prev_xref_offset {
trailer_dict.set("Prev", Object::Integer(prev as i64));
}
if let (Some(eid), Some(state)) = (encrypt_id_opt, &self.encryption) {
trailer_dict.set("Encrypt", Object::Reference(eid));
let id_array = Object::Array(vec![
Object::LiteralString(state.file_id.clone()),
Object::LiteralString(state.file_id.clone()),
]);
trailer_dict.set("ID", id_array);
}
trailer_dict
}
#[allow(clippy::too_many_arguments)]
fn write_xref_stream(
&self,
out: &mut Vec<u8>,
xref_id: ObjectId,
root: ObjectId,
encrypt_id_opt: Option<ObjectId>,
objstm_id_opt: Option<ObjectId>,
offsets: &[u64],
compressed_map: &std::collections::HashMap<u32, (u32, u32)>,
max_id: usize,
subsections: &[(u32, u32)],
) -> Result<(), PdfError> {
const W: [usize; 3] = [1, 4, 2];
let entry_width = W[0] + W[1] + W[2];
let mut emit_ids: Vec<u32> = Vec::new();
for (start, count) in subsections {
for id in *start..(*start + *count) {
emit_ids.push(id);
}
}
let n_entries = emit_ids.len();
let mut raw_table = Vec::with_capacity(n_entries * entry_width);
for id in &emit_ids {
if *id == 0 {
raw_table.push(0);
raw_table.extend_from_slice(&0u32.to_be_bytes());
raw_table.extend_from_slice(&65535u16.to_be_bytes());
} else if let Some((container, idx)) = compressed_map.get(id).copied() {
raw_table.push(2);
raw_table.extend_from_slice(&container.to_be_bytes());
raw_table.extend_from_slice(&(idx as u16).to_be_bytes());
} else {
let off = offsets.get(*id as usize).copied().unwrap_or(0);
if off > u32::MAX as u64 {
return Err(PdfError::other(format!(
"Document::write_xref_stream: object {id} offset {off} exceeds 32-bit\
limit — bump /W[1] to 8 bytes"
)));
}
raw_table.push(1);
raw_table.extend_from_slice(&(off as u32).to_be_bytes());
raw_table.extend_from_slice(&0u16.to_be_bytes());
}
}
let mut predicted = Vec::with_capacity(n_entries * (entry_width + 1));
let mut prev = vec![0u8; entry_width];
for chunk in raw_table.chunks_exact(entry_width) {
predicted.push(0x02); for i in 0..entry_width {
predicted.push(chunk[i].wrapping_sub(prev[i]));
}
prev.copy_from_slice(chunk);
}
let compressed = flate_compress(&predicted);
let trailer_dict = self.build_trailer_dict(root, encrypt_id_opt, max_id);
let mut index_array: Vec<Object> = Vec::with_capacity(subsections.len() * 2);
for (start, count) in subsections {
index_array.push(Object::Integer(*start as i64));
index_array.push(Object::Integer(*count as i64));
}
let mut stream_dict = Dict::new()
.with("Type", Object::Name("XRef".into()))
.with("Filter", Object::Name("FlateDecode".into()))
.with(
"DecodeParms",
Object::Dict(
Dict::new()
.with("Predictor", Object::Integer(12))
.with("Columns", Object::Integer(entry_width as i64)),
),
)
.with(
"W",
Object::Array(vec![
Object::Integer(W[0] as i64),
Object::Integer(W[1] as i64),
Object::Integer(W[2] as i64),
]),
)
.with("Index", Object::Array(index_array));
for (k, v) in trailer_dict.entries() {
stream_dict.set(k, v.clone());
}
let stream = Stream::new(stream_dict, compressed);
let indirect = IndirectObject {
id: xref_id,
object: Object::Stream(stream),
};
write_indirect(out, &indirect).map_err(PdfError::Io)?;
let _ = objstm_id_opt;
Ok(())
}
}
fn flate_compress(input: &[u8]) -> Vec<u8> {
crate::zlib::flate_compress(input)
}
fn encrypt_object_in_place(
obj: &mut Object,
id: ObjectId,
state: &EncryptionState,
) -> Result<(), PdfError> {
match obj {
Object::LiteralString(s) | Object::HexString(s) => {
*s = state.handler.encrypt_object(id, s, &state.aes_iv)?;
}
Object::Array(items) => {
for item in items {
encrypt_object_in_place(item, id, state)?;
}
}
Object::Dict(d) => {
encrypt_dict_in_place(d, id, state)?;
}
Object::Stream(s) => {
encrypt_dict_in_place(&mut s.dict, id, state)?;
if has_identity_crypt_filter(&s.dict) {
return Ok(());
}
s.data = state.handler.encrypt_object(id, &s.data, &state.aes_iv)?;
}
_ => {}
}
Ok(())
}
fn has_identity_crypt_filter(dict: &Dict) -> bool {
let filter = dict
.entries()
.iter()
.find(|(k, _)| k == "Filter")
.map(|(_, v)| v);
let parms = dict
.entries()
.iter()
.find(|(k, _)| k == "DecodeParms")
.map(|(_, v)| v);
let crypt_pos: Option<usize> = match filter {
Some(Object::Name(s)) if s == "Crypt" => Some(0),
Some(Object::Array(items)) => items
.iter()
.position(|f| matches!(f, Object::Name(n) if n == "Crypt")),
_ => None,
};
let Some(idx) = crypt_pos else {
return false;
};
let parms_dict = match parms {
Some(Object::Dict(d)) if idx == 0 => Some(d.clone()),
Some(Object::Array(items)) => match items.get(idx) {
Some(Object::Dict(d)) => Some(d.clone()),
_ => None,
},
_ => None,
};
let Some(d) = parms_dict else {
return true;
};
match d
.entries()
.iter()
.find(|(k, _)| k == "Name")
.map(|(_, v)| v)
{
Some(Object::Name(s)) => s == "Identity",
None => true,
_ => false,
}
}
fn encrypt_dict_in_place(
d: &mut Dict,
id: ObjectId,
state: &EncryptionState,
) -> Result<(), PdfError> {
let mut new_entries: Vec<(String, Object)> = Vec::with_capacity(d.entries().len());
for (k, v) in d.entries() {
let mut v = v.clone();
encrypt_object_in_place(&mut v, id, state)?;
new_entries.push((k.clone(), v));
}
*d = Dict::default();
for (k, v) in new_entries {
d.set(&k, v);
}
Ok(())
}
pub(crate) fn write_object_to(out: &mut Vec<u8>, obj: &Object) -> io::Result<()> {
write_object(out, obj)
}
pub(crate) fn take_objects(doc: &mut Document) -> Vec<IndirectObject> {
std::mem::take(&mut doc.objects)
}
fn write_indirect(out: &mut Vec<u8>, ind: &IndirectObject) -> io::Result<()> {
let header = format!("{} {} obj\n", ind.id.number, ind.id.generation);
out.write_all(header.as_bytes())?;
write_object(out, &ind.object)?;
out.write_all(b"\nendobj\n")?;
Ok(())
}
fn write_object(out: &mut Vec<u8>, obj: &Object) -> io::Result<()> {
match obj {
Object::Null => out.write_all(b"null"),
Object::Bool(b) => out.write_all(if *b { b"true" } else { b"false" }),
Object::Integer(n) => out.write_all(format!("{}", n).as_bytes()),
Object::Real(f) => out.write_all(format_real(*f).as_bytes()),
Object::Name(s) => {
out.write_all(b"/")?;
for &b in s.as_bytes() {
let needs_escape = matches!(
b,
0x00..=0x20 | 0x23 | 0x25 | 0x28 | 0x29 | 0x2F | 0x3C | 0x3E | 0x5B | 0x5D
| 0x7B | 0x7D | 0x7F..=0xFF
);
if needs_escape {
out.write_all(format!("#{:02X}", b).as_bytes())?;
} else {
out.write_all(&[b])?;
}
}
Ok(())
}
Object::LiteralString(bytes) => {
out.write_all(b"(")?;
for &b in bytes {
match b {
b'\\' => out.write_all(br"\\")?,
b'(' => out.write_all(br"\(")?,
b')' => out.write_all(br"\)")?,
b'\n' => out.write_all(br"\n")?,
b'\r' => out.write_all(br"\r")?,
b'\t' => out.write_all(br"\t")?,
_ => out.write_all(&[b])?,
}
}
out.write_all(b")")
}
Object::HexString(bytes) => {
out.write_all(b"<")?;
for b in bytes {
out.write_all(format!("{:02X}", b).as_bytes())?;
}
out.write_all(b">")
}
Object::Array(items) => {
out.write_all(b"[")?;
for (i, it) in items.iter().enumerate() {
if i > 0 {
out.write_all(b" ")?;
}
write_object(out, it)?;
}
out.write_all(b"]")
}
Object::Dict(d) => write_dict(out, d),
Object::Reference(id) => {
out.write_all(format!("{} {} R", id.number, id.generation).as_bytes())
}
Object::Stream(s) => {
let mut d = s.dict.clone();
d.set("Length", Object::Integer(s.data.len() as i64));
write_dict(out, &d)?;
out.write_all(b"\nstream\n")?;
out.write_all(&s.data)?;
out.write_all(b"\nendstream")
}
}
}
fn write_dict(out: &mut Vec<u8>, d: &Dict) -> io::Result<()> {
out.write_all(b"<<")?;
for (k, v) in &d.entries {
out.write_all(b" /")?;
out.write_all(k.as_bytes())?;
out.write_all(b" ")?;
write_object(out, v)?;
}
out.write_all(b" >>")
}
pub fn write_dict_to(out: &mut Vec<u8>, d: &Dict) -> Result<(), PdfError> {
write_dict(out, d).map_err(PdfError::Io)
}
fn format_real(f: f64) -> String {
if !f.is_finite() {
return "0".to_string();
}
if f.fract() == 0.0 && f.abs() < 1e16 {
return format!("{}", f as i64);
}
let s = format!("{:.6}", f);
let trimmed = s.trim_end_matches('0').trim_end_matches('.');
if trimmed.is_empty() || trimmed == "-" {
"0".to_string()
} else {
trimmed.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn write_one(obj: &Object) -> Vec<u8> {
let mut buf = Vec::new();
write_object(&mut buf, obj).unwrap();
buf
}
#[test]
fn primitives_serialize() {
assert_eq!(write_one(&Object::Null), b"null");
assert_eq!(write_one(&Object::Bool(true)), b"true");
assert_eq!(write_one(&Object::Bool(false)), b"false");
assert_eq!(write_one(&Object::Integer(42)), b"42");
assert_eq!(write_one(&Object::Integer(-7)), b"-7");
}
#[test]
fn real_numbers_have_no_trailing_zeros() {
assert_eq!(write_one(&Object::Real(0.0)), b"0");
assert_eq!(write_one(&Object::Real(1.0)), b"1");
assert_eq!(write_one(&Object::Real(0.5)), b"0.5");
assert_eq!(write_one(&Object::Real(-1.25)), b"-1.25");
assert_eq!(write_one(&Object::Real(2.345678987654)), b"2.345679");
}
#[test]
fn names_are_slash_prefixed() {
assert_eq!(write_one(&Object::Name("Pages".into())), b"/Pages");
let escaped = write_one(&Object::Name("a b".into()));
assert_eq!(escaped, b"/a#20b");
}
#[test]
fn arrays_have_space_separated_items() {
let a = Object::Array(vec![
Object::Integer(1),
Object::Integer(2),
Object::Real(0.5),
]);
assert_eq!(write_one(&a), b"[1 2 0.5]");
}
#[test]
fn dicts_iterate_in_insertion_order() {
let d = Dict::new()
.with("Type", Object::Name("Pages".into()))
.with("Count", Object::Integer(1));
assert_eq!(write_one(&Object::Dict(d)), b"<< /Type /Pages /Count 1 >>");
}
#[test]
fn streams_serialize_with_length() {
let body = b"hello".to_vec();
let s = Stream::new(Dict::new(), body);
let bytes = write_one(&Object::Stream(s));
let needle = b"/Length 5";
assert!(
bytes.windows(needle.len()).any(|w| w == needle),
"expected /Length 5 in {:?}",
String::from_utf8_lossy(&bytes)
);
assert!(bytes.windows(7).any(|w| w == b"stream\n"));
assert!(bytes.windows(9).any(|w| w == b"endstream"));
}
#[test]
fn document_writes_full_pdf_envelope() {
let mut doc = Document::new();
let pages_id = doc.allocate_id();
let catalog = Object::Dict(
Dict::new()
.with("Type", Object::Name("Catalog".into()))
.with("Pages", Object::Reference(pages_id)),
);
let catalog_id = doc.add(catalog);
doc.add_object(
pages_id,
Object::Dict(
Dict::new()
.with("Type", Object::Name("Pages".into()))
.with("Count", Object::Integer(0))
.with("Kids", Object::Array(Vec::new())),
),
);
doc.root = Some(catalog_id);
let mut bytes = Vec::new();
doc.write_to(&mut bytes).unwrap();
assert!(bytes.starts_with(b"%PDF-1.4\n"));
assert!(bytes.ends_with(b"%%EOF\n"));
assert!(bytes.windows(5).any(|w| w == b"xref\n"));
assert!(bytes.windows(8).any(|w| w == b"trailer\n"));
assert!(bytes.windows(10).any(|w| w == b"startxref\n"));
}
}