#[cfg(test)]
use pdfrum_object::Name;
use pdfrum_object::{Array, Dict, Object, names};
use crate::write::object::{write_dict, write_name, write_object};
use crate::write::reach::SUPPRESSED_TRAILER_KEYS;
use crate::write::xref::{ObjectOffsets, stream_record};
#[derive(Debug, Clone, Copy)]
pub(crate) struct TrailerParts<'a> {
pub(crate) source: &'a Dict,
pub(crate) id: &'a Array,
pub(crate) last_object_number: u32,
pub(crate) prev: Option<u64>,
pub(crate) encrypt: Option<u32>,
}
pub(crate) fn build(parts: TrailerParts<'_>) -> Dict {
let mut out = Dict::new();
for (key, value) in parts.source.iter() {
if SUPPRESSED_TRAILER_KEYS.contains(&key) {
continue;
}
out.push(key.clone(), value.clone());
}
if let Some(num) = parts.encrypt {
out.push(
names::ENCRYPT.clone(),
Object::Ref(pdfrum_object::ObjRef::new(num, 0)),
);
}
out.push(
names::SIZE.clone(),
Object::Int(i64::from(parts.last_object_number).saturating_add(1)),
);
if let Some(prev) = parts.prev {
out.push(
names::PREV.clone(),
Object::Int(i64::try_from(prev).unwrap_or(i64::MAX)),
);
}
out.push(names::ID.clone(), Object::Array(parts.id.clone()));
out
}
pub(crate) fn write_classic(out: &mut Vec<u8>, trailer: &Dict) {
out.extend_from_slice(b"trailer\r\n");
write_dict(out, trailer, None);
}
pub(crate) fn write_stream(
out: &mut Vec<u8>,
num: u32,
trailer: &Dict,
offsets: &ObjectOffsets,
written: &[u32],
) {
let mut payload = Vec::with_capacity(written.len().saturating_mul(5));
for n in written {
stream_record(&mut payload, offsets.get(*n).unwrap_or(0));
}
out.extend_from_slice(format!("{num} 0 obj <<").as_bytes());
for (key, value) in trailer.iter() {
if key == names::SIZE {
continue;
}
write_name(out, key);
write_object(out, value, None);
}
write_name(out, names::TYPE);
write_name(out, names::XREF);
write_name(out, names::SIZE);
let size = trailer
.direct_int(names::SIZE)
.unwrap_or(0)
.saturating_add(1);
write_object(out, &Object::Int(size), None);
out.extend_from_slice(b"/W[0 4 1]/Index[");
for n in written {
out.extend_from_slice(format!("{n} 1 ").as_bytes());
}
out.extend_from_slice(b"]/Length ");
out.extend_from_slice(payload.len().to_string().as_bytes());
out.extend_from_slice(b">>stream\r\n");
out.extend_from_slice(&payload);
out.extend_from_slice(b"\r\nendstream\r\nendobj\r\n");
}
pub(crate) fn write_tail(out: &mut Vec<u8>, xref_start: u64) {
out.extend_from_slice(b"\r\nstartxref\r\n");
out.extend_from_slice(xref_start.to_string().as_bytes());
out.extend_from_slice(b"\r\n%%EOF\r\n");
}
#[cfg(test)]
pub(crate) fn from_scratch(root: u32, info: Option<u32>) -> Dict {
let mut out = Dict::new();
out.push(
names::ROOT.clone(),
Object::Ref(pdfrum_object::ObjRef::new(root, 0)),
);
if let Some(info) = info {
out.push(
names::INFO.clone(),
Object::Ref(pdfrum_object::ObjRef::new(info, 0)),
);
}
out
}
#[must_use]
#[cfg(test)]
pub(crate) fn is_suppressed(key: &Name) -> bool {
SUPPRESSED_TRAILER_KEYS.contains(&key)
}
#[cfg(test)]
mod tests {
use super::{TrailerParts, build, is_suppressed, write_classic, write_stream, write_tail};
use crate::write::xref::ObjectOffsets;
use pdfrum_object::{Array, Dict, Name, ObjRef, Object, PdfString, names};
fn source() -> Dict {
Dict::from_pairs([
(names::ROOT.clone(), Object::Ref(ObjRef::new(11, 0))),
(names::INFO.clone(), Object::Ref(ObjRef::new(9, 0))),
(names::SIZE.clone(), Object::Int(999)),
(names::PREV.clone(), Object::Int(4242)),
(names::ENCRYPT.clone(), Object::Ref(ObjRef::new(5, 0))),
(names::TYPE.clone(), Object::Name(names::XREF.clone())),
(names::W.clone(), Object::Int(1)),
])
}
fn id() -> Array {
Array::of([
Object::Str(PdfString::hex(b"0123456789ABCDEF")),
Object::Str(PdfString::hex(b"FEDCBA9876543210")),
])
}
fn text(f: impl FnOnce(&mut Vec<u8>)) -> String {
let mut out = Vec::new();
f(&mut out);
String::from_utf8_lossy(&out).into_owned()
}
#[test]
fn the_suppressed_keys_are_dropped_and_recomputed() {
let src = source();
let ids = id();
let out = build(TrailerParts {
source: &src,
id: &ids,
last_object_number: 35,
prev: None,
encrypt: None,
});
assert_eq!(out.reference(names::ROOT), Some(ObjRef::new(11, 0)));
assert_eq!(out.reference(names::INFO), Some(ObjRef::new(9, 0)));
assert_eq!(out.direct_int(names::SIZE), Some(36));
assert!(!out.contains_key(names::PREV));
assert!(!out.contains_key(names::ENCRYPT));
assert!(!out.contains_key(names::TYPE));
assert!(!out.contains_key(names::W));
}
#[test]
fn an_unrecognised_key_survives() {
let src = Dict::from_pairs([
(names::ROOT.clone(), Object::Ref(ObjRef::new(1, 0))),
(Name::from("Foo"), Object::Name(Name::from(""))),
]);
let ids = id();
let out = build(TrailerParts {
source: &src,
id: &ids,
last_object_number: 1,
prev: None,
encrypt: None,
});
let bytes = text(|o| write_classic(o, &out));
assert!(bytes.contains("/Foo/"), "got {bytes}");
}
#[test]
fn prev_is_written_only_when_asked_for() {
let src = source();
let ids = id();
let with = build(TrailerParts {
source: &src,
id: &ids,
last_object_number: 1,
prev: Some(1234),
encrypt: None,
});
assert_eq!(with.direct_int(names::PREV), Some(1234));
}
#[test]
fn encrypt_names_the_object_it_was_written_as() {
let src = source();
let ids = id();
let out = build(TrailerParts {
source: &src,
id: &ids,
last_object_number: 40,
prev: None,
encrypt: Some(41),
});
assert_eq!(out.reference(names::ENCRYPT), Some(ObjRef::new(41, 0)));
}
#[test]
fn a_classic_trailer_reads_the_way_the_golden_does() {
let src = Dict::from_pairs([
(names::INFO.clone(), Object::Ref(ObjRef::new(9, 0))),
(names::ROOT.clone(), Object::Ref(ObjRef::new(11, 0))),
]);
let ids = Array::of([
Object::Str(PdfString::hex(
b"\xD8\x89\xEB\x6B\x9A\xDF\x88\xE5\xED\xA7\xDC\x08\xFE\x85\x97\x8B",
)),
Object::Str(PdfString::hex([0u8; 16])),
]);
let out = build(TrailerParts {
source: &src,
id: &ids,
last_object_number: 35,
prev: None,
encrypt: None,
});
let bytes = text(|o| write_classic(o, &out));
assert!(bytes.starts_with("trailer\r\n<<"));
assert!(bytes.contains("/Info 9 0 R "));
assert!(bytes.contains("/Root 11 0 R "));
assert!(bytes.contains("/Size 36"));
assert!(bytes.contains("/ID[<D889EB6B9ADF88E5EDA7DC08FE85978B><"));
assert!(bytes.ends_with(">]>>"), "got {bytes}");
}
#[test]
fn the_tail_names_the_offset_and_ends_the_file() {
assert_eq!(
text(|o| write_tail(o, 1234)),
"\r\nstartxref\r\n1234\r\n%%EOF\r\n"
);
}
#[test]
fn an_xref_stream_declares_the_true_record_length() {
let src = Dict::from_pairs([(names::ROOT.clone(), Object::Ref(ObjRef::new(1, 0)))]);
let ids = id();
let trailer = build(TrailerParts {
source: &src,
id: &ids,
last_object_number: 4,
prev: None,
encrypt: None,
});
let mut offsets = ObjectOffsets::new();
offsets.set(2, 0x10);
offsets.set(3, 0x20);
let mut out = Vec::new();
write_stream(&mut out, 6, &trailer, &offsets, &[2, 3]);
let bytes = String::from_utf8_lossy(&out).into_owned();
assert!(bytes.starts_with("6 0 obj <<"));
assert!(bytes.contains("/Type/XRef"));
assert!(bytes.contains("/W[0 4 1]"));
assert!(bytes.contains("/Index[2 1 3 1 ]"));
assert!(bytes.contains("/Length 10>>stream\r\n"));
assert!(bytes.contains("/Size 6"));
assert!(out.ends_with(b"\r\nendstream\r\nendobj\r\n"));
assert!(out.windows(5).any(|w| w == [0, 0, 0, 0x10, 0]));
assert!(out.windows(5).any(|w| w == [0, 0, 0, 0x20, 0]));
}
#[test]
fn a_from_scratch_trailer_names_the_catalog() {
let out = super::from_scratch(1, Some(2));
assert_eq!(out.reference(names::ROOT), Some(ObjRef::new(1, 0)));
assert_eq!(out.reference(names::INFO), Some(ObjRef::new(2, 0)));
assert!(!super::from_scratch(1, None).contains_key(names::INFO));
}
#[test]
fn suppression_answers_for_each_of_the_eleven() {
for key in [
names::ENCRYPT,
names::SIZE,
names::FILTER,
names::INDEX,
names::LENGTH,
names::PREV,
names::W,
names::XREF_STM,
names::ID,
names::DECODE_PARMS,
names::TYPE,
] {
assert!(is_suppressed(key), "{key:?}");
}
assert!(!is_suppressed(names::ROOT));
assert!(!is_suppressed(names::INFO));
}
}