use pdfrum_common::{Diagnostics, Limits};
use pdfrum_object::{Dict, Name, Resolve, names};
use crate::error::Error;
use crate::{
PredictorParams, decode_ascii_hex, decode_ascii85, decode_flate, decode_lzw, decode_run_length,
predictor,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Filter {
Flate,
Lzw,
AsciiHex,
Ascii85,
RunLength,
CcittFax,
Jbig2,
Dct,
Jpx,
Crypt,
}
impl Filter {
#[must_use]
pub fn from_name(n: &Name) -> Option<Filter> {
let table = [
(names::FLATE_DECODE, Filter::Flate),
(names::FL, Filter::Flate),
(names::LZW_DECODE, Filter::Lzw),
(names::LZW, Filter::Lzw),
(names::ASCII85_DECODE, Filter::Ascii85),
(names::A85, Filter::Ascii85),
(names::ASCII_HEX_DECODE, Filter::AsciiHex),
(names::AHX, Filter::AsciiHex),
(names::RUN_LENGTH_DECODE, Filter::RunLength),
(names::RL, Filter::RunLength),
(names::CCITT_FAX_DECODE, Filter::CcittFax),
(names::CCF, Filter::CcittFax),
(names::DCT_DECODE, Filter::Dct),
(names::DCT, Filter::Dct),
(names::JPX_DECODE, Filter::Jpx),
(names::JBIG2_DECODE, Filter::Jbig2),
(names::CRYPT, Filter::Crypt),
];
table
.iter()
.find_map(|(name, filter)| (*name == n).then_some(*filter))
}
#[must_use]
pub fn canonical_name(self) -> &'static Name {
match self {
Filter::Flate => names::FLATE_DECODE,
Filter::Lzw => names::LZW_DECODE,
Filter::AsciiHex => names::ASCII_HEX_DECODE,
Filter::Ascii85 => names::ASCII85_DECODE,
Filter::RunLength => names::RUN_LENGTH_DECODE,
Filter::CcittFax => names::CCITT_FAX_DECODE,
Filter::Jbig2 => names::JBIG2_DECODE,
Filter::Dct => names::DCT_DECODE,
Filter::Jpx => names::JPX_DECODE,
Filter::Crypt => names::CRYPT,
}
}
#[must_use]
pub fn is_image_codec(self) -> bool {
matches!(
self,
Filter::CcittFax | Filter::Jbig2 | Filter::Dct | Filter::Jpx
)
}
#[must_use]
pub fn is_chainable(self) -> bool {
matches!(
self,
Filter::Flate | Filter::Lzw | Filter::Ascii85 | Filter::AsciiHex | Filter::RunLength
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum DecodeOutput {
Bytes(Vec<u8>),
Image(NeedsImageCodec),
}
#[derive(Debug, Clone, PartialEq)]
pub struct NeedsImageCodec {
pub filter: Option<Filter>,
pub name: Name,
pub params: Dict,
}
pub fn decode(
filter: Filter,
input: &[u8],
params: &Dict,
r: &impl Resolve,
limits: &Limits,
diags: &mut Diagnostics,
) -> Result<DecodeOutput, Error> {
let bytes = match filter {
Filter::Crypt => input.to_vec(),
Filter::AsciiHex => decode_ascii_hex(input).0,
Filter::Ascii85 => decode_ascii85(input)?.0,
Filter::RunLength => decode_run_length(input, diags)?.0,
Filter::Flate => {
let params = PredictorParams::from_dict(params, r)?;
let raw = decode_flate(input, 0, limits, diags)?;
predictor(raw, params)?
}
Filter::Lzw => {
let predictor_params = PredictorParams::from_dict(params, r)?;
let raw = decode_lzw(input, params_early_change(params, r), limits, diags)?;
predictor(raw, predictor_params)?
}
Filter::CcittFax | Filter::Jbig2 | Filter::Dct | Filter::Jpx => {
return Ok(DecodeOutput::Image(NeedsImageCodec {
filter: Some(filter),
name: filter.canonical_name().clone(),
params: params.clone(),
}));
}
};
Ok(DecodeOutput::Bytes(bytes))
}
fn params_early_change(params: &Dict, r: &impl Resolve) -> bool {
params.int(names::EARLY_CHANGE, r).unwrap_or(1) != 0
}
pub(crate) fn params_dict(obj: Option<&pdfrum_object::Object>, r: &impl Resolve) -> Dict {
let Some(obj) = obj else {
return Dict::new();
};
obj.resolve(r)
.ok()
.and_then(|resolved| resolved.as_direct().and_then(|o| o.as_dict().cloned()))
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::{DecodeOutput, Filter, decode};
use pdfrum_common::{Diagnostics, Limits};
use pdfrum_object::{Dict, Name, NoResolve, names};
fn bytes(out: DecodeOutput) -> Vec<u8> {
match out {
DecodeOutput::Bytes(b) => b,
DecodeOutput::Image(need) => panic!("expected bytes, got {need:?}"),
}
}
fn run(filter: Filter, input: &[u8]) -> Vec<u8> {
let mut diags = Diagnostics::default();
bytes(
decode(
filter,
input,
&Dict::new(),
&NoResolve,
&Limits::default(),
&mut diags,
)
.expect("decodes"),
)
}
#[test]
fn every_spelling_of_every_filter_name() {
let cases: [(&str, Option<Filter>); 20] = [
("FlateDecode", Some(Filter::Flate)),
("Fl", Some(Filter::Flate)),
("LZWDecode", Some(Filter::Lzw)),
("LZW", Some(Filter::Lzw)),
("ASCII85Decode", Some(Filter::Ascii85)),
("A85", Some(Filter::Ascii85)),
("ASCIIHexDecode", Some(Filter::AsciiHex)),
("AHx", Some(Filter::AsciiHex)),
("RunLengthDecode", Some(Filter::RunLength)),
("RL", Some(Filter::RunLength)),
("CCITTFaxDecode", Some(Filter::CcittFax)),
("CCF", Some(Filter::CcittFax)),
("DCTDecode", Some(Filter::Dct)),
("DCT", Some(Filter::Dct)),
("JPXDecode", Some(Filter::Jpx)),
("JBIG2Decode", Some(Filter::Jbig2)),
("Crypt", Some(Filter::Crypt)),
("FooBar", None),
("FlateEncode", None),
("", None),
];
for (spelling, expected) in cases {
assert_eq!(
Filter::from_name(&Name::from(spelling)),
expected,
"/{spelling}"
);
}
}
#[test]
fn abbreviations_canonicalize_for_the_image_path() {
assert_eq!(Filter::Dct.canonical_name(), names::DCT_DECODE);
assert_eq!(Filter::CcittFax.canonical_name(), names::CCITT_FAX_DECODE);
assert_eq!(Filter::Flate.canonical_name(), names::FLATE_DECODE);
}
#[test]
fn image_codecs_and_chainable_filters_partition_the_enum() {
let all = [
Filter::Flate,
Filter::Lzw,
Filter::AsciiHex,
Filter::Ascii85,
Filter::RunLength,
Filter::CcittFax,
Filter::Jbig2,
Filter::Dct,
Filter::Jpx,
Filter::Crypt,
];
for f in all {
assert!(!(f.is_image_codec() && f.is_chainable()), "{f:?}");
}
assert!(!Filter::Crypt.is_image_codec());
assert!(!Filter::Crypt.is_chainable());
}
#[test]
fn crypt_decodes_as_the_identity() {
assert_eq!(
run(Filter::Crypt, b"\x00\x01\xffplain"),
b"\x00\x01\xffplain"
);
}
#[test]
fn image_codecs_punt_with_no_data_of_their_own() {
let mut diags = Diagnostics::default();
for f in [Filter::Dct, Filter::Jpx, Filter::Jbig2, Filter::CcittFax] {
let out = decode(
f,
b"whatever",
&Dict::new(),
&NoResolve,
&Limits::default(),
&mut diags,
)
.expect("punting never fails");
match out {
DecodeOutput::Image(need) => {
assert_eq!(need.filter, Some(f));
assert_eq!(&need.name, f.canonical_name());
}
DecodeOutput::Bytes(b) => panic!("expected a punt, got {b:?}"),
}
}
}
#[test]
fn the_text_filters_route_to_their_decoders() {
assert_eq!(run(Filter::AsciiHex, b"48656C6C6F>"), b"Hello");
assert_eq!(run(Filter::Ascii85, b"FCfN8~>"), b"test");
assert_eq!(run(Filter::RunLength, &[0, b'x', 128]), b"x");
}
}