use crate::encrypt::Encryptor;
use pdfrum_crypt::CryptClass;
use pdfrum_filters::encode_flate;
use pdfrum_object::{Dict, Name, Object, Stream, names};
pub(crate) struct Encoded {
pub(crate) dict: Dict,
pub(crate) data: Vec<u8>,
}
pub(crate) fn encode(s: &Stream, enc: Option<&Encryptor<'_>>) -> Encoded {
let metadata = is_metadata(&s.dict);
let has_filter = s.dict.contains_key(names::FILTER);
let want_flate = !metadata;
let want_cipher = !metadata || enc.is_some_and(Encryptor::encrypts_metadata);
let (mut dict, mut data) = match (has_filter, want_flate) {
(false, true) => {
let encoded = encode_flate(s.data.as_bytes());
let mut d = without(&s.dict, names::DECODE_PARMS);
set(
&mut d,
names::FILTER,
Object::Name(names::FLATE_DECODE.clone()),
);
(d, encoded)
}
(true, false) => (without(&s.dict, names::FILTER), s.data.as_bytes().to_vec()),
(true, true) | (false, false) => (s.dict.clone(), s.data.as_bytes().to_vec()),
};
if let Some(e) = enc
&& want_cipher
{
data = e.encrypt(CryptClass::Stream, &data);
}
update_length(&mut dict, data.len());
Encoded { dict, data }
}
fn is_metadata(dict: &Dict) -> bool {
dict.name(names::TYPE).is_some_and(|n| n == names::METADATA)
&& dict.name(names::SUBTYPE).is_some_and(|n| n == names::XML)
}
fn without(dict: &Dict, key: &Name) -> Dict {
Dict::from_pairs(
dict.iter()
.filter(|(k, _)| k != key)
.map(|(k, v)| (k.clone(), v.clone())),
)
}
fn set(dict: &mut Dict, key: &Name, value: Object) {
let existing: Vec<(Name, Object)> = dict.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
if existing.iter().any(|(k, _)| k == key) {
*dict = Dict::from_pairs(existing.into_iter().map(|(k, v)| {
if &k == key {
(k, value.clone())
} else {
(k, v)
}
}));
} else {
dict.push(key.clone(), value);
}
}
fn update_length(dict: &mut Dict, len: usize) {
let want = i64::try_from(len).unwrap_or(i64::MAX);
if dict.direct_int(names::LENGTH) == Some(want) {
return;
}
set(dict, names::LENGTH, Object::Int(want));
}
#[cfg(test)]
mod tests {
use super::encode;
use pdfrum_common::{Diagnostics, Limits};
use pdfrum_filters::decode_flate;
use pdfrum_object::{ByteSpan, Dict, Name, Object, Stream, names};
use std::sync::Arc;
fn stream(pairs: impl IntoIterator<Item = (Name, Object)>, data: &[u8]) -> Stream {
let file: Arc<[u8]> = Arc::from(data);
Stream::new(Dict::from_pairs(pairs), ByteSpan::whole(file))
}
#[test]
fn row_filter_present_copies_the_stream_verbatim() {
let s = stream(
[
(
names::FILTER.clone(),
Object::Name(names::FLATE_DECODE.clone()),
),
(names::LENGTH.clone(), Object::Int(5)),
],
b"\x01\x02\x03\x04\x05",
);
let out = encode(&s, None);
assert_eq!(out.data, b"\x01\x02\x03\x04\x05");
assert_eq!(
out.dict.name(names::FILTER),
Some(names::FLATE_DECODE),
"an already-compressed stream keeps its filter"
);
assert_eq!(out.dict.direct_int(names::LENGTH), Some(5));
}
#[test]
fn an_empty_filtered_stream_keeps_length_zero() {
let s = stream(
[
(
names::FILTER.clone(),
Object::Name(names::FLATE_DECODE.clone()),
),
(names::LENGTH.clone(), Object::Int(0)),
],
b"",
);
let out = encode(&s, None);
assert!(out.data.is_empty());
assert_eq!(out.dict.direct_int(names::LENGTH), Some(0));
}
#[test]
fn row_no_filter_compresses_and_rewrites_the_dictionary() {
let payload = vec![b'x'; 4000];
let s = stream(
[
(names::LENGTH.clone(), Object::Int(4000)),
(
names::DECODE_PARMS.clone(),
Object::Dict(Dict::from_pairs([(names::COLUMNS.clone(), Object::Int(4))])),
),
],
&payload,
);
let out = encode(&s, None);
assert_eq!(out.dict.name(names::FILTER), Some(names::FLATE_DECODE));
assert!(
!out.dict.contains_key(names::DECODE_PARMS),
"/DecodeParms described the old payload and must go"
);
assert_eq!(
out.dict.direct_int(names::LENGTH),
Some(i64::try_from(out.data.len()).expect("fits")),
);
assert!(out.data.len() < payload.len());
let round = decode_flate(
&out.data,
0,
&Limits::default(),
&mut Diagnostics::default(),
)
.expect("our own output decodes");
assert_eq!(round, payload);
}
#[test]
fn a_metadata_stream_is_never_compressed() {
let meta = [
(names::TYPE.clone(), Object::Name(names::METADATA.clone())),
(names::SUBTYPE.clone(), Object::Name(names::XML.clone())),
(names::LENGTH.clone(), Object::Int(9)),
];
let out = encode(&stream(meta.clone(), b"<x:xmpmeta/>"), None);
assert!(!out.dict.contains_key(names::FILTER));
assert_eq!(out.data, b"<x:xmpmeta/>");
assert_eq!(out.dict.direct_int(names::LENGTH), Some(12));
let mut filtered: Vec<_> = meta.to_vec();
filtered.push((
names::FILTER.clone(),
Object::Name(names::FLATE_DECODE.clone()),
));
let out = encode(&stream(filtered, b"compressed"), None);
assert!(
!out.dict.contains_key(names::FILTER),
"row 2 strips the filter it will not honour"
);
}
#[test]
fn length_always_describes_the_emitted_bytes() {
for (pairs, data) in [
(vec![], &b"abc"[..]),
(vec![(names::LENGTH.clone(), Object::Int(999))], &b"abc"[..]),
(
vec![(
names::FILTER.clone(),
Object::Name(names::FLATE_DECODE.clone()),
)],
&b"\x01\x02"[..],
),
] {
let out = encode(&stream(pairs, data), None);
assert_eq!(
out.dict.direct_int(names::LENGTH),
Some(i64::try_from(out.data.len()).expect("fits")),
);
}
}
#[test]
fn rewriting_length_does_not_shuffle_the_key_order() {
let s = stream(
[
(names::TYPE.clone(), Object::Name(Name::from("Thing"))),
(names::LENGTH.clone(), Object::Int(999)),
(names::N.clone(), Object::Int(4)),
],
b"abc",
);
let out = encode(&s, None);
let keys: Vec<&Name> = out.dict.keys().collect();
assert_eq!(
keys,
vec![names::TYPE, names::LENGTH, names::N, names::FILTER]
);
}
}