mod header;
pub(crate) mod id;
pub(crate) mod object;
mod reach;
mod stream;
mod trailer;
mod xref;
use std::io::Write;
use pdfrum_common::PdfVersion;
use pdfrum_object::{ObjRef, Object, Resolve, names};
use crate::doc::EditDoc;
use crate::encrypt;
use crate::error::Error;
use crate::font;
use crate::write::header::write_header;
use crate::write::id::{IdContext, IdSource};
use crate::write::xref::ObjectOffsets;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SaveMode {
#[default]
Full,
Incremental,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SaveOptions {
pub mode: SaveMode,
pub keep_original: bool,
pub remove_security: bool,
pub subset_new_fonts: bool,
pub version: Option<PdfVersion>,
pub id_source: IdSource,
pub encrypt: Option<Encryption>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Encryption {
pub user_password: Vec<u8>,
pub owner_password: Vec<u8>,
pub permissions: pdfrum_crypt::Permissions,
pub encrypt_metadata: bool,
}
impl Default for SaveOptions {
fn default() -> Self {
Self {
mode: SaveMode::Full,
keep_original: true,
remove_security: false,
subset_new_fonts: false,
version: None,
id_source: IdSource::Random,
encrypt: None,
}
}
}
struct Counting<W: Write> {
inner: W,
written: u64,
}
impl<W: Write> Counting<W> {
fn new(inner: W) -> Self {
Self { inner, written: 0 }
}
fn write(&mut self, bytes: &[u8]) -> Result<(), Error> {
self.inner.write_all(bytes)?;
self.written = self.written.saturating_add(bytes.len() as u64);
Ok(())
}
const fn offset(&self) -> u64 {
self.written
}
}
pub fn save(doc: &EditDoc<'_>, opts: &SaveOptions, out: &mut impl Write) -> Result<(), Error> {
let base = doc.base();
let SecurityPlan {
declared,
keep_security,
fresh,
} = security_plan(base, opts)?;
let handler = base.security_handler();
let id = file_id(base, opts);
let forced_full = base.xref_was_rebuilt()
|| id.rekeyed
|| (declared && opts.remove_security)
|| fresh.is_some();
let incremental = opts.mode == SaveMode::Incremental && !forced_full;
let slot = choose_slot(
doc,
base,
fresh.as_ref().map(|(dict, _)| dict),
keep_security,
);
let encrypt_number = slot.as_ref().map(|s| s.number);
let active_handler = fresh.as_ref().map_or(handler, |(_, h)| h);
let security = if keep_security || fresh.is_some() {
Some(encrypt::Security {
handler: active_handler,
ivs: encrypt::IvSource::from_os()?,
encrypt_object: encrypt_number,
})
} else {
None
};
let mut sink = Counting::new(out);
let mut offsets = ObjectOffsets::new();
write_front(&mut sink, base, opts, incremental)?;
let (old_nums, new_nums) = partition(doc, incremental);
let trailer_dict = doc.trailer();
let reach = reach::walk(&trailer_dict, base.trailer_object_number(), doc);
for num in old_nums {
if !reach.is_reachable(num) || encrypt_number == Some(num) {
continue;
}
write_one(&mut sink, &mut offsets, doc, num, security.as_ref())?;
}
let mut new_nums = new_nums;
let overrides = subset_fonts(doc, opts, encrypt_number, &mut new_nums);
for num in new_nums.iter().copied() {
if encrypt_number == Some(num) {
continue;
}
match overrides.get(&num) {
Some(object) => write_override(&mut sink, &mut offsets, num, object, security.as_ref()),
None => write_one(&mut sink, &mut offsets, doc, num, security.as_ref()),
}?;
}
if let Some(EncryptSlot { number, dict }) = &slot {
offsets.set(*number, sink.offset());
let mut bytes = Vec::new();
object::write_indirect(&mut bytes, *number, &Object::Dict(dict.clone()), None);
sink.write(&bytes)?;
if incremental && !new_nums.contains(number) {
new_nums.push(*number);
}
}
let last_written = offsets.last();
let xref_start = sink.offset();
let as_stream = incremental && base.main_xref_is_stream();
let written: Vec<u32> = new_nums
.iter()
.copied()
.filter(|n| offsets.contains(*n))
.collect();
if !as_stream {
let mut table = Vec::new();
if incremental {
xref::classic_delta(&mut table, &offsets, &written);
} else {
xref::classic_full(&mut table, &offsets, last_written);
}
sink.write(&table)?;
}
let dict = trailer::build(trailer::TrailerParts {
source: &trailer_dict,
id: &id.array,
last_object_number: last_written,
prev: (incremental && base.last_xref_offset() > 0).then(|| base.last_xref_offset()),
encrypt: slot.as_ref().map(|s| s.number),
});
let mut tail = Vec::new();
if as_stream {
let num = doc.last_object_number().saturating_add(1);
trailer::write_stream(&mut tail, num, &dict, &offsets, &written);
} else {
trailer::write_classic(&mut tail, &dict);
}
trailer::write_tail(&mut tail, xref_start);
sink.write(&tail)?;
Ok(())
}
fn partition(doc: &EditDoc<'_>, incremental: bool) -> (Vec<u32>, Vec<u32>) {
let base = doc.base();
let xref = base.xref();
if incremental {
let mut new: Vec<u32> = doc.edited().map(|(n, _)| n).collect();
new.sort_unstable();
new.dedup();
return (Vec::new(), new);
}
let last = xref.last_object_number();
let old: Vec<u32> = (1..=last)
.filter(|n| !doc.is_removed(*n))
.filter(|n| !matches!(xref.entry(*n), None | Some(pdfrum_parser::Entry::Free)))
.collect();
let mut new: Vec<u32> = doc
.edited()
.map(|(n, _)| n)
.filter(|n| {
!xref.is_valid_object_number(*n)
|| matches!(xref.entry(*n), None | Some(pdfrum_parser::Entry::Free))
})
.collect();
new.sort_unstable();
new.dedup();
(old, new)
}
#[derive(Debug, Clone)]
struct EncryptSlot {
number: u32,
dict: pdfrum_object::Dict,
}
fn file_id(base: &pdfrum_parser::Document, opts: &SaveOptions) -> id::FileId {
id::build(
IdContext {
old: None,
encrypt: base.encrypt_dict().map(|(d, _)| d),
incremental: opts.mode == SaveMode::Incremental,
}
.with_old(base.trailer()),
opts.id_source,
)
}
fn write_front(
sink: &mut Counting<impl Write>,
base: &pdfrum_parser::Document,
opts: &SaveOptions,
incremental: bool,
) -> Result<(), Error> {
if incremental && opts.keep_original {
sink.write(base.bytes())
} else {
let mut header = Vec::new();
write_header(&mut header, opts.version, base.version());
sink.write(&header)
}
}
struct SecurityPlan {
declared: bool,
keep_security: bool,
fresh: Option<(pdfrum_object::Dict, pdfrum_crypt::SecurityHandler)>,
}
fn security_plan(
base: &pdfrum_parser::Document,
opts: &SaveOptions,
) -> Result<SecurityPlan, Error> {
let declared = base.encrypt_dict().is_some();
if declared && opts.encrypt.is_some() {
return Err(Error::EncryptedSaveUnsupported);
}
let keep_security = declared && !opts.remove_security;
if keep_security
&& matches!(
base.security_handler(),
pdfrum_crypt::SecurityHandler::Identity
)
{
return Err(Error::EncryptedSaveUnsupported);
}
Ok(SecurityPlan {
declared,
keep_security,
fresh: fresh_encryption(opts)?,
})
}
fn fresh_encryption(
opts: &SaveOptions,
) -> Result<Option<(pdfrum_object::Dict, pdfrum_crypt::SecurityHandler)>, Error> {
let Some(encryption) = &opts.encrypt else {
return Ok(None);
};
pdfrum_crypt::standard_r6(
&encryption.user_password,
&encryption.owner_password,
encryption.permissions,
encryption.encrypt_metadata,
&pdfrum_crypt::KeyMaterial::from_os().map_err(|_| Error::NoEntropy)?,
)
.map(Some)
.map_err(|_| Error::PasswordNotText)
}
fn choose_slot(
doc: &EditDoc<'_>,
base: &pdfrum_parser::Document,
fresh: Option<&pdfrum_object::Dict>,
keep_security: bool,
) -> Option<EncryptSlot> {
match fresh {
Some(dict) => Some(EncryptSlot {
number: doc.last_object_number().saturating_add(1),
dict: dict.clone(),
}),
None => keep_security.then(|| encrypt_slot(doc, base)).flatten(),
}
}
fn encrypt_slot(doc: &EditDoc<'_>, base: &pdfrum_parser::Document) -> Option<EncryptSlot> {
let (dict, inline) = base.encrypt_dict()?;
let number = if inline {
doc.last_object_number().saturating_add(1)
} else {
base.trailer().reference(names::ENCRYPT)?.num
};
Some(EncryptSlot {
number,
dict: dict.clone(),
})
}
fn subset_fonts(
doc: &EditDoc<'_>,
opts: &SaveOptions,
encrypt_number: Option<u32>,
new_nums: &mut Vec<u32>,
) -> font::overrides::Overrides {
if !opts.subset_new_fonts {
return font::overrides::Overrides::new();
}
let mut next = doc.last_object_number().saturating_add(1);
if let Some(number) = encrypt_number {
next = next.max(number.saturating_add(1));
}
let overrides = font::overrides::build(doc, new_nums, opts.id_source, &mut next);
new_nums.extend(overrides.keys().copied());
new_nums.sort_unstable();
new_nums.dedup();
overrides
}
fn write_override<W: Write>(
sink: &mut Counting<W>,
offsets: &mut ObjectOffsets,
num: u32,
object: &Object,
security: Option<&encrypt::Security<'_>>,
) -> Result<(), Error> {
offsets.set(num, sink.offset());
let enc = security.and_then(|s| s.for_object(num));
let mut bytes = Vec::new();
object::write_indirect(&mut bytes, num, object, enc.as_ref());
sink.write(&bytes)
}
fn write_one<W: Write>(
sink: &mut Counting<W>,
offsets: &mut ObjectOffsets,
doc: &EditDoc<'_>,
num: u32,
security: Option<&encrypt::Security<'_>>,
) -> Result<(), Error> {
offsets.set(num, sink.offset());
let Ok(obj) = doc.fetch(ObjRef::new(num, 0)) else {
offsets.erase(num);
return Ok(());
};
if obj.is_null() {
offsets.erase(num);
return Ok(());
}
let enc = security.and_then(|s| s.for_object(num));
let mut bytes = Vec::new();
object::write_indirect(&mut bytes, num, &obj, enc.as_ref());
sink.write(&bytes)
}
impl<'a> IdContext<'a> {
fn with_old(mut self, trailer: &'a pdfrum_object::Dict) -> Self {
self.old = match trailer.raw(names::ID) {
Some(Object::Array(a)) => Some(a),
_ => None,
};
self
}
}