use crate::encrypt::Encryptor;
use pdfrum_crypt::CryptClass;
use pdfrum_object::{
Array, Dict, Name, Object, PdfString, Stream, encode_string_hex, encode_string_literal,
fmt_int, fmt_number, name_encode, names,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Exempt {
No,
Yes,
}
pub(crate) fn write_object(out: &mut Vec<u8>, obj: &Object, enc: Option<&Encryptor<'_>>) {
match obj {
Object::Null => out.extend_from_slice(b" null"),
Object::Bool(b) => {
out.push(b' ');
out.extend_from_slice(if *b { b"true" } else { b"false" });
}
Object::Int(i) => {
out.push(b' ');
out.extend_from_slice(fmt_int(*i).as_bytes());
}
Object::Real(r) => {
out.push(b' ');
out.extend_from_slice(fmt_number(*r).as_bytes());
}
Object::Str(s) => write_string(out, s, enc, Exempt::No),
Object::Name(n) => write_name(out, n),
Object::Array(a) => write_array(out, a, enc),
Object::Dict(d) => write_dict(out, d, enc),
Object::Stream(s) => write_stream(out, s, enc),
Object::Ref(r) => {
out.push(b' ');
out.extend_from_slice(r.num.to_string().as_bytes());
out.extend_from_slice(b" 0 R ");
}
}
}
pub(crate) fn write_name(out: &mut Vec<u8>, name: &Name) {
out.push(b'/');
out.extend_from_slice(&name_encode(name.as_bytes()));
}
fn write_string(out: &mut Vec<u8>, s: &PdfString, enc: Option<&Encryptor<'_>>, exempt: Exempt) {
let bytes = match (enc, exempt) {
(Some(e), Exempt::No) => e.encrypt(CryptClass::String, &s.bytes),
_ => s.bytes.to_vec(),
};
if s.hex {
out.extend_from_slice(&encode_string_hex(&bytes));
} else {
out.extend_from_slice(&encode_string_literal(&bytes));
}
}
fn write_array(out: &mut Vec<u8>, a: &Array, enc: Option<&Encryptor<'_>>) {
out.push(b'[');
for value in a.iter() {
write_object(out, value, enc);
}
out.push(b']');
}
pub(crate) fn write_dict(out: &mut Vec<u8>, d: &Dict, enc: Option<&Encryptor<'_>>) {
let signature = is_signature_dict(d);
out.extend_from_slice(b"<<");
for (key, value) in d.iter() {
write_name(out, key);
if signature
&& key == names::CONTENTS
&& let Object::Str(s) = value
{
write_string(out, s, enc, Exempt::Yes);
continue;
}
write_object(out, value, enc);
}
out.extend_from_slice(b">>");
}
fn is_signature_dict(d: &Dict) -> bool {
let key = if d.contains_key(names::TYPE) {
names::TYPE
} else {
names::FT
};
d.name(key).is_some_and(|n| n.as_bytes() == b"Sig")
}
fn write_stream(out: &mut Vec<u8>, s: &Stream, enc: Option<&Encryptor<'_>>) {
let encoded = crate::write::stream::encode(s, enc);
write_dict(out, &encoded.dict, enc);
out.extend_from_slice(b"stream\r\n");
out.extend_from_slice(&encoded.data);
out.extend_from_slice(b"\r\nendstream");
}
pub(crate) fn write_indirect(
out: &mut Vec<u8>,
num: u32,
obj: &Object,
enc: Option<&Encryptor<'_>>,
) {
out.extend_from_slice(num.to_string().as_bytes());
out.extend_from_slice(b" 0 obj\r\n");
write_object(out, obj, enc);
out.extend_from_slice(b"\r\nendobj\r\n");
}
#[cfg(test)]
mod tests {
use super::{write_indirect, write_object};
use pdfrum_object::{Array, Dict, Name, ObjRef, Object, PdfString, names};
fn bytes(obj: &Object) -> Vec<u8> {
let mut out = Vec::new();
write_object(&mut out, obj, None);
out
}
fn text(obj: &Object) -> String {
String::from_utf8_lossy(&bytes(obj)).into_owned()
}
#[test]
fn scalars_carry_a_leading_space() {
assert_eq!(text(&Object::Null), " null");
assert_eq!(text(&Object::Bool(true)), " true");
assert_eq!(text(&Object::Bool(false)), " false");
assert_eq!(text(&Object::Int(1245)), " 1245");
assert_eq!(text(&Object::Real(9.003_45)), " 9.00345");
}
#[test]
fn integers_spell_through_the_c_int_view() {
assert_eq!(text(&Object::Int(4_294_967_295)), " -1");
}
#[test]
fn a_reference_is_bracketed_by_spaces_and_always_generation_zero() {
assert_eq!(text(&Object::Ref(ObjRef::new(9, 0))), " 9 0 R ");
assert_eq!(text(&Object::Ref(ObjRef::new(9, 42))), " 9 0 R ");
}
#[test]
fn names_escape_and_may_be_empty() {
assert_eq!(text(&Object::Name(Name::from("Simple"))), "/Simple");
assert_eq!(
text(&Object::Name(Name::from("Property Name With Space"))),
"/Property#20Name#20With#20Space"
);
assert_eq!(text(&Object::Name(Name::from(""))), "/");
assert_eq!(text(&Object::Name(Name::from("A#B"))), "/A#23B");
}
#[test]
fn strings_keep_the_spelling_they_were_parsed_with() {
assert_eq!(text(&Object::Str(PdfString::literal(b"hi"))), "(hi)");
assert_eq!(text(&Object::Str(PdfString::hex(b"\x12\xAC"))), "<12AC>");
}
#[test]
fn arrays_and_dicts_write_no_separators() {
let arr = Object::Array(Array::of([Object::Int(1), Object::Int(2), Object::Int(3)]));
assert_eq!(text(&arr), "[ 1 2 3]");
let dict = Object::Dict(Dict::from_pairs([
(Name::from("A"), Object::Int(1)),
(Name::from("B"), Object::Name(Name::from("C"))),
]));
assert_eq!(text(&dict), "<</A 1/B/C>>");
}
#[test]
fn reference_trailing_space_separates_the_next_key() {
let dict = Object::Dict(Dict::from_pairs([
(names::INFO.clone(), Object::Ref(ObjRef::new(9, 0))),
(names::ROOT.clone(), Object::Ref(ObjRef::new(11, 0))),
(names::SIZE.clone(), Object::Int(36)),
(names::ID.clone(), Object::Array(Array::new())),
]));
assert_eq!(text(&dict), "<</Info 9 0 R /Root 11 0 R /Size 36/ID[]>>");
}
#[test]
fn dict_keys_keep_insertion_order() {
let dict = Object::Dict(Dict::from_pairs([
(Name::from("Z"), Object::Int(1)),
(Name::from("A"), Object::Int(2)),
(Name::from("M"), Object::Int(3)),
]));
assert_eq!(text(&dict), "<</Z 1/A 2/M 3>>");
}
#[test]
fn indirect_frame_is_always_generation_zero() {
let mut out = Vec::new();
write_indirect(&mut out, 7, &Object::Int(1), None);
assert_eq!(String::from_utf8_lossy(&out), "7 0 obj\r\n 1\r\nendobj\r\n");
}
}