use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq)]
pub enum Object {
Null,
Boolean(bool),
Integer(i64),
Real(f64),
String(Vec<u8>),
Name(String),
Array(Vec<Object>),
Dictionary(std::collections::HashMap<String, Object>),
Stream {
dict: std::collections::HashMap<String, Object>,
data: bytes::Bytes,
},
Reference(ObjectRef),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
pub struct ObjectRef {
pub id: u32,
pub generation: u16,
}
impl ObjectRef {
pub fn new(id: u32, generation: u16) -> Self {
Self { id, generation }
}
}
impl std::fmt::Display for ObjectRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {} R", self.id, self.generation)
}
}
pub fn encode_pdf_text_string(s: &str) -> Vec<u8> {
if s.chars().all(|c| (c as u32) <= 0x00FF) {
s.chars().map(|c| c as u8).collect()
} else {
let mut buf = vec![0xFE_u8, 0xFF];
for unit in s.encode_utf16() {
buf.push((unit >> 8) as u8);
buf.push((unit & 0xFF) as u8);
}
buf
}
}
impl Object {
pub fn text_string(s: impl AsRef<str>) -> Self {
Object::String(encode_pdf_text_string(s.as_ref()))
}
pub fn type_name(&self) -> &'static str {
match self {
Object::Null => "Null",
Object::Boolean(_) => "Boolean",
Object::Integer(_) => "Integer",
Object::Real(_) => "Real",
Object::String(_) => "String",
Object::Name(_) => "Name",
Object::Array(_) => "Array",
Object::Dictionary(_) => "Dictionary",
Object::Stream { .. } => "Stream",
Object::Reference(_) => "Reference",
}
}
pub fn as_integer(&self) -> Option<i64> {
match self {
Object::Integer(i) => Some(*i),
_ => None,
}
}
pub fn as_name(&self) -> Option<&str> {
match self {
Object::Name(s) => Some(s),
_ => None,
}
}
pub fn as_dict(&self) -> Option<&std::collections::HashMap<String, Object>> {
match self {
Object::Dictionary(d) => Some(d),
Object::Stream { dict, .. } => Some(dict),
_ => None,
}
}
pub fn as_array(&self) -> Option<&Vec<Object>> {
match self {
Object::Array(arr) => Some(arr),
_ => None,
}
}
pub fn as_reference(&self) -> Option<ObjectRef> {
match self {
Object::Reference(r) => Some(*r),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Object::Boolean(b) => Some(*b),
_ => None,
}
}
pub fn as_real(&self) -> Option<f64> {
match self {
Object::Real(r) => Some(*r),
_ => None,
}
}
pub fn as_string(&self) -> Option<&[u8]> {
match self {
Object::String(s) => Some(s),
_ => None,
}
}
pub fn is_null(&self) -> bool {
matches!(self, Object::Null)
}
pub fn decode_stream_data(&self) -> Result<Vec<u8>> {
self.decode_stream_data_with_decryption(None, 0, 0)
}
pub(crate) fn decode_image_stream_data(&self, expected_filter_output_size: usize) -> Result<Vec<u8>> {
self.decode_stream_data_with_decryption_and_expected_size(None, 0, 0, Some(expected_filter_output_size))
}
pub fn decode_stream_data_with_decryption(
&self,
decryption_fn: Option<&dyn Fn(&[u8]) -> Result<Vec<u8>>>,
obj_num: u32,
gen_num: u32,
) -> Result<Vec<u8>> {
self.decode_stream_data_with_decryption_and_expected_size(decryption_fn, obj_num, gen_num, None)
}
pub(crate) fn decode_stream_data_with_decryption_and_expected_size(
&self,
decryption_fn: Option<&dyn Fn(&[u8]) -> Result<Vec<u8>>>,
obj_num: u32,
gen_num: u32,
expected_filter_output_size: Option<usize>,
) -> Result<Vec<u8>> {
match self {
Object::Stream { dict, data } => {
let decrypted_data = decrypt_stream_data(data, decryption_fn, obj_num, gen_num)?;
let filters = dict.get("Filter").map(extract_filter_names).unwrap_or_default();
if filters.is_empty() {
Ok(decrypted_data)
} else {
let decode_params = extract_decode_params(dict.get("DecodeParms"));
if let Some(expected_size) = expected_filter_output_size {
crate::decoders::decode_stream_with_params_and_expected_size(
&decrypted_data,
&filters,
decode_params.as_ref(),
expected_size,
)
} else {
crate::decoders::decode_stream_with_params(&decrypted_data, &filters, decode_params.as_ref())
}
}
}
Object::Dictionary(dict) => {
tracing::warn!("dictionary used where stream expected; treating as empty stream");
let filters = dict.get("Filter").map(extract_filter_names).unwrap_or_default();
if filters.is_empty() {
Ok(Vec::new())
} else {
let decode_params = extract_decode_params(dict.get("DecodeParms"));
crate::decoders::decode_stream_with_params(&[], &filters, decode_params.as_ref())
}
}
_ => Err(Error::InvalidObjectType {
expected: "Stream".to_string(),
found: self.type_name().to_string(),
}),
}
}
}
fn decrypt_stream_data(
data: &[u8],
decryption_fn: Option<&dyn Fn(&[u8]) -> Result<Vec<u8>>>,
obj_num: u32,
gen_num: u32,
) -> Result<Vec<u8>> {
let Some(decrypt) = decryption_fn else {
return Ok(data.to_vec());
};
tracing::debug!(
object_id = obj_num,
generation = gen_num,
bytes = data.len(),
"decrypting stream"
);
match decrypt(data) {
Ok(data) => {
tracing::debug!(bytes = data.len(), "decryption successful");
Ok(data)
}
Err(error) => {
tracing::error!(
object_id = obj_num,
generation = gen_num,
error_code = error.telemetry_code(),
error_offset = ?error.telemetry_offset(),
"decryption failed"
);
Err(error)
}
}
}
fn extract_filter_names(filter_obj: &Object) -> Vec<String> {
match filter_obj {
Object::Name(name) => vec![name.clone()],
Object::Array(arr) => arr
.iter()
.filter_map(|obj| obj.as_name().map(|s| s.to_string()))
.collect(),
_ => vec![],
}
}
pub(crate) fn extract_decode_params(params_obj: Option<&Object>) -> Option<crate::decoders::DecodeParams> {
let dict = match params_obj? {
Object::Dictionary(d) => d,
Object::Array(arr) => arr.iter().filter_map(|obj| obj.as_dict()).next()?,
_ => return None,
};
let predictor = dict.get("Predictor").and_then(|obj| obj.as_integer()).unwrap_or(1);
let columns = dict.get("Columns").and_then(|obj| obj.as_integer()).unwrap_or(1) as usize;
let colors = dict.get("Colors").and_then(|obj| obj.as_integer()).unwrap_or(1) as usize;
let bits_per_component = dict
.get("BitsPerComponent")
.and_then(|obj| obj.as_integer())
.unwrap_or(8) as usize;
Some(crate::decoders::DecodeParams {
predictor,
columns,
colors,
bits_per_component,
})
}
pub fn extract_ccitt_params(params_obj: Option<&Object>) -> Option<crate::decoders::CcittParams> {
extract_ccitt_params_with_width(params_obj, None)
}
pub fn extract_ccitt_params_with_width(
params_obj: Option<&Object>,
image_width: Option<u32>,
) -> Option<crate::decoders::CcittParams> {
let dict = match params_obj? {
Object::Dictionary(dict) => dict,
Object::Array(params) => params.iter().find_map(Object::as_dict)?,
_ => return None,
};
ccitt_params_from_dict(dict, image_width)
}
pub(crate) fn extract_ccitt_params_with_width_at_index(
params_obj: Option<&Object>,
filter_index: usize,
image_width: Option<u32>,
) -> Option<crate::decoders::CcittParams> {
let dict = match params_obj? {
Object::Dictionary(d) => d,
Object::Array(arr) => arr.get(filter_index)?.as_dict()?,
_ => return None,
};
ccitt_params_from_dict(dict, image_width)
}
fn ccitt_params_from_dict(
dict: &std::collections::HashMap<String, Object>,
image_width: Option<u32>,
) -> Option<crate::decoders::CcittParams> {
let k = dict.get("K").and_then(|obj| obj.as_integer()).unwrap_or(0);
let columns = dict
.get("Columns")
.and_then(|obj| obj.as_integer())
.map(|v| v as u32)
.or(image_width)
.unwrap_or(1);
let rows = dict.get("Rows").and_then(|obj| obj.as_integer()).map(|v| v as u32);
let black_is_1 = dict.get("BlackIs1").and_then(|obj| obj.as_bool()).unwrap_or(false);
let end_of_line = dict.get("EndOfLine").and_then(|obj| obj.as_bool()).unwrap_or(false);
let encoded_byte_align = dict
.get("EncodedByteAlign")
.and_then(|obj| obj.as_bool())
.unwrap_or(false);
let end_of_block = dict.get("EndOfBlock").and_then(|obj| obj.as_bool()).unwrap_or(true);
Some(crate::decoders::CcittParams {
k,
columns,
rows,
black_is_1,
end_of_line,
encoded_byte_align,
end_of_block,
})
}
#[cfg(test)]
mod tests;