use crate::error::PdfError;
use crate::objects::PdfDict;
pub const MAX_DECODED_STREAM_BYTES: usize = 512 * 1024 * 1024;
#[derive(Debug, Clone, Copy)]
pub struct DecodeBudget {
limit: usize,
}
impl Default for DecodeBudget {
fn default() -> Self {
Self {
limit: MAX_DECODED_STREAM_BYTES,
}
}
}
impl DecodeBudget {
pub const fn new(limit: usize) -> Self {
Self { limit }
}
pub const fn limit(&self) -> usize {
self.limit
}
pub fn for_stream(dict: &PdfDict) -> Self {
let declared = declared_image_bytes(dict)
.or_else(|| declared_embedded_file_bytes(dict))
.unwrap_or(0);
Self {
limit: declared.max(MAX_DECODED_STREAM_BYTES),
}
}
fn check(&self, produced: usize) -> Result<(), PdfError> {
if produced > self.limit {
return Err(PdfError::DecompressionError(format!(
"decompressed stream exceeds the {} byte limit",
self.limit
)));
}
Ok(())
}
fn reserve_hint(&self, want: usize) -> usize {
want.min(self.limit)
}
}
fn declared_image_bytes(dict: &PdfDict) -> Option<usize> {
use stet_graphics::image_limits::{
validate_bits_per_component, validate_image_dimension, validate_image_size,
};
let width = validate_image_dimension(dict.get_int(b"Width"))?;
let height = validate_image_dimension(dict.get_int(b"Height"))?;
let pixels = validate_image_size(width, height)?;
let bpc = validate_bits_per_component(dict.get_int(b"BitsPerComponent"))? as usize;
let components = match dict.get_name(b"ColorSpace") {
Some(b"DeviceGray" | b"G" | b"CalGray") => 1,
Some(b"DeviceRGB" | b"RGB" | b"CalRGB" | b"Lab") => 3,
_ => 4,
};
let row_bits = (width as usize)
.saturating_mul(components)
.saturating_mul(bpc);
let row_bytes = row_bits.div_ceil(8);
Some(row_bytes.saturating_mul(pixels / width as usize))
}
fn declared_embedded_file_bytes(dict: &PdfDict) -> Option<usize> {
let size = dict.get_dict(b"Params")?.get_int(b"Size")?;
usize::try_from(size).ok()
}
#[derive(Debug, Clone, PartialEq)]
pub enum Filter {
FlateDecode,
LZWDecode,
ASCIIHexDecode,
ASCII85Decode,
RunLengthDecode,
DCTDecode,
CCITTFaxDecode,
JPXDecode,
JBIG2Decode,
}
pub fn parse_filters(
dict: &PdfDict,
resolver: Option<&crate::resolver::Resolver>,
) -> Result<(Vec<Filter>, Vec<Option<PdfDict>>), PdfError> {
let filter_obj = match dict.get(b"Filter") {
Some(obj) => obj,
None => return Ok((Vec::new(), Vec::new())),
};
let resolved_filter;
let filter_obj = if let crate::objects::PdfObj::Ref(_, _) = filter_obj {
if let Some(r) = resolver {
resolved_filter = r.deref(filter_obj).unwrap_or_else(|_| filter_obj.clone());
&resolved_filter
} else {
filter_obj
}
} else {
filter_obj
};
let filter_names: Vec<&[u8]> = match filter_obj {
crate::objects::PdfObj::Name(n) => vec![n.as_slice()],
crate::objects::PdfObj::Array(arr) => {
arr.iter()
.filter_map(|o| {
if let Some(n) = o.as_name() {
return Some(n);
}
None
})
.collect()
}
_ => return Ok((Vec::new(), Vec::new())),
};
let mut filters = Vec::new();
for name in &filter_names {
filters.push(filter_from_name(name)?);
}
let dp_obj = dict.get(b"DecodeParms");
let resolved_dp;
let dp_obj = match dp_obj {
Some(crate::objects::PdfObj::Ref(_, _)) if resolver.is_some() => {
resolved_dp = resolver.unwrap().deref(dp_obj.unwrap()).ok();
resolved_dp.as_ref()
}
other => other,
};
let parms = match dp_obj {
Some(crate::objects::PdfObj::Dict(d)) => vec![Some(d.clone())],
Some(crate::objects::PdfObj::Array(arr)) => arr
.iter()
.map(|o| match o {
crate::objects::PdfObj::Dict(d) => Some(d.clone()),
crate::objects::PdfObj::Ref(_, _) if resolver.is_some() => resolver
.unwrap()
.deref(o)
.ok()
.and_then(|r| r.as_dict().cloned()),
_ => None,
})
.collect(),
_ => vec![None; filters.len()],
};
let mut parms = parms;
while parms.len() < filters.len() {
parms.push(None);
}
for (i, filter) in filters.iter().enumerate() {
if *filter != Filter::CCITTFaxDecode {
continue;
}
let dp = parms[i].get_or_insert_with(PdfDict::new);
if dp.get_int(b"Columns").is_none()
&& let Some(w) = dict.get_int(b"Width")
{
dp.insert(b"Columns".to_vec(), crate::objects::PdfObj::Int(w));
}
if dp.get_int(b"Rows").is_none()
&& let Some(h) = dict.get_int(b"Height")
{
dp.insert(b"Rows".to_vec(), crate::objects::PdfObj::Int(h));
}
}
Ok((filters, parms))
}
fn filter_from_name(name: &[u8]) -> Result<Filter, PdfError> {
match name {
b"FlateDecode" | b"Fl" => Ok(Filter::FlateDecode),
b"LZWDecode" | b"LZW" => Ok(Filter::LZWDecode),
b"ASCIIHexDecode" | b"AHx" => Ok(Filter::ASCIIHexDecode),
b"ASCII85Decode" | b"A85" => Ok(Filter::ASCII85Decode),
b"RunLengthDecode" | b"RL" => Ok(Filter::RunLengthDecode),
b"DCTDecode" | b"DCT" => Ok(Filter::DCTDecode),
b"CCITTFaxDecode" | b"CCF" => Ok(Filter::CCITTFaxDecode),
b"JPXDecode" | b"JPX" => Ok(Filter::JPXDecode),
b"JBIG2Decode" | b"JBIG2" => Ok(Filter::JBIG2Decode),
_ if name.starts_with(b"Flate") => Ok(Filter::FlateDecode),
_ if name.starts_with(b"LZW") => Ok(Filter::LZWDecode),
_ if name.starts_with(b"ASCIIHex") => Ok(Filter::ASCIIHexDecode),
_ if name.starts_with(b"ASCII85") => Ok(Filter::ASCII85Decode),
_ if name.starts_with(b"RunLength") => Ok(Filter::RunLengthDecode),
_ if name.starts_with(b"CCITT") => Ok(Filter::CCITTFaxDecode),
_ if name.starts_with(b"JPX") => Ok(Filter::JPXDecode),
_ if name.starts_with(b"JBIG2") => Ok(Filter::JBIG2Decode),
_ => Err(PdfError::UnsupportedFilter(
String::from_utf8_lossy(name).into(),
)),
}
}
pub fn decode_stream(
raw_data: &[u8],
filters: &[Filter],
decode_parms: &[Option<PdfDict>],
jbig2_globals: Option<&[u8]>,
) -> Result<Vec<u8>, PdfError> {
decode_stream_bounded(
raw_data,
filters,
decode_parms,
jbig2_globals,
DecodeBudget::default(),
)
}
pub fn decode_stream_bounded(
raw_data: &[u8],
filters: &[Filter],
decode_parms: &[Option<PdfDict>],
jbig2_globals: Option<&[u8]>,
budget: DecodeBudget,
) -> Result<Vec<u8>, PdfError> {
let mut data = raw_data.to_vec();
for (i, filter) in filters.iter().enumerate() {
let parms = decode_parms.get(i).and_then(|p| p.as_ref());
data = match filter {
Filter::FlateDecode => decode_flate(&data, parms, budget)?,
Filter::LZWDecode => decode_lzw(&data, parms, budget)?,
Filter::ASCIIHexDecode => decode_ascii_hex(&data)?,
Filter::ASCII85Decode => decode_ascii85(&data)?,
Filter::RunLengthDecode => decode_run_length(&data, budget)?,
Filter::DCTDecode => decode_dct(&data)?,
Filter::CCITTFaxDecode => decode_ccittfax(&data, parms)?,
#[cfg(feature = "jpx")]
Filter::JPXDecode => decode_jpx(&data)?,
#[cfg(not(feature = "jpx"))]
Filter::JPXDecode => {
return Err(PdfError::UnsupportedFilter("JPXDecode (disabled)".into()));
}
Filter::JBIG2Decode => decode_jbig2(&data, jbig2_globals)?,
};
budget.check(data.len())?;
}
Ok(data)
}
fn decode_flate(
data: &[u8],
parms: Option<&PdfDict>,
budget: DecodeBudget,
) -> Result<Vec<u8>, PdfError> {
let (zlib_output, zlib_clean, _) = decode_flate_inner(data, true, budget);
let output = if zlib_clean {
zlib_output?
} else {
let zlib_data = zlib_output.unwrap_or_default();
if data.len() > 2 {
let (raw_output, _, _) = decode_flate_inner(&data[2..], false, budget);
let raw_data = raw_output.unwrap_or_default();
if raw_data.len() > zlib_data.len()
&& raw_data[..zlib_data.len()] == zlib_data[..]
&& looks_like_valid_continuation(&raw_data, zlib_data.len())
{
raw_data
} else if !zlib_data.is_empty() {
zlib_data
} else if !raw_data.is_empty() {
raw_data
} else {
return Err(PdfError::DecompressionError(
"flate: decompression failed".into(),
));
}
} else if !zlib_data.is_empty() {
zlib_data
} else {
return Err(PdfError::DecompressionError(
"flate: decompression failed".into(),
));
}
};
if let Some(parms) = parms {
let predictor = parms.get_int(b"Predictor").unwrap_or(1);
if predictor > 1 {
return apply_predictor(&output, parms, predictor);
}
}
Ok(output)
}
fn looks_like_valid_continuation(data: &[u8], start: usize) -> bool {
if start >= data.len() {
return false;
}
let sample = &data[start..data.len().min(start + 64)];
let printable = sample
.iter()
.filter(|&&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
.count();
printable * 5 >= sample.len() * 4
}
fn decode_flate_inner(
data: &[u8],
zlib: bool,
budget: DecodeBudget,
) -> (Result<Vec<u8>, PdfError>, bool, usize) {
use flate2::Decompress;
let mut decompressor = Decompress::new(zlib);
let mut output = Vec::with_capacity(budget.reserve_hint(data.len().saturating_mul(3)));
let mut buf = [0u8; 8192];
let mut input_offset = 0;
loop {
let before_in = decompressor.total_in() as usize;
let before_out = decompressor.total_out() as usize;
let result = decompressor.decompress(
&data[input_offset..],
&mut buf,
flate2::FlushDecompress::None,
);
let consumed = decompressor.total_in() as usize - before_in;
let produced = decompressor.total_out() as usize - before_out;
input_offset += consumed;
output.extend_from_slice(&buf[..produced]);
if let Err(e) = budget.check(output.len()) {
return (Err(e), true, input_offset);
}
match result {
Ok(status) => match status {
flate2::Status::StreamEnd => return (Ok(output), true, input_offset),
flate2::Status::Ok | flate2::Status::BufError => {
if consumed == 0 && produced == 0 {
return (Ok(output), true, input_offset);
}
}
},
Err(_) if !output.is_empty() => {
return (Ok(output), false, input_offset);
}
Err(e) => {
return (
Err(PdfError::DecompressionError(format!("flate: {e}"))),
false,
input_offset,
);
}
}
}
}
fn decode_lzw(
data: &[u8],
parms: Option<&PdfDict>,
budget: DecodeBudget,
) -> Result<Vec<u8>, PdfError> {
let early_change = parms.and_then(|p| p.get_int(b"EarlyChange")).unwrap_or(1) != 0;
let output = lzw_decode(data, early_change, budget)?;
if let Some(parms) = parms {
let predictor = parms.get_int(b"Predictor").unwrap_or(1);
if predictor > 1 {
return apply_predictor(&output, parms, predictor);
}
}
Ok(output)
}
const LZW_CLEAR_TABLE: usize = 256;
const LZW_EOD: usize = 257;
const LZW_MAX_ENTRIES: usize = 4096;
const LZW_INITIAL_SIZE: usize = 258;
fn lzw_decode(data: &[u8], early_change: bool, budget: DecodeBudget) -> Result<Vec<u8>, PdfError> {
let failed = || PdfError::DecompressionError("lzw: decode failed".into());
let mut table = LzwTable::new(early_change);
let mut bit_size = table.code_length();
let mut reader = LzwBitReader::new(data);
let mut decoded = Vec::new();
let mut prev: Option<usize> = None;
loop {
let next = match reader.read(bit_size) {
Some(code) => code as usize,
None => {
return Ok(decoded);
}
};
match next {
LZW_CLEAR_TABLE => {
table.clear();
prev = None;
bit_size = table.code_length();
}
LZW_EOD => return Ok(decoded),
new => {
if new > table.size() {
if decoded.is_empty() {
return Err(failed());
}
return Ok(decoded);
}
if new < table.size() {
let entry = table.get(new).ok_or_else(failed)?;
let first_byte = entry[0];
decoded.extend_from_slice(entry);
if let Some(prev_code) = prev {
table.register(prev_code, first_byte);
}
} else if new == table.size() && prev.is_some() {
let prev_code = prev.unwrap();
let prev_entry = table.get(prev_code).ok_or_else(failed)?;
let first_byte = prev_entry[0];
let new_entry = table.register(prev_code, first_byte).ok_or_else(failed)?;
decoded.extend_from_slice(new_entry);
} else {
if decoded.is_empty() {
return Err(failed());
}
return Ok(decoded);
}
budget.check(decoded.len())?;
bit_size = table.code_length();
prev = Some(new);
}
}
}
}
struct LzwTable {
early_change: bool,
entries: Vec<Option<Vec<u8>>>,
}
impl LzwTable {
fn new(early_change: bool) -> Self {
let mut entries: Vec<_> = (0..=255u8).map(|b| Some(vec![b])).collect();
entries.push(None); entries.push(None); Self {
early_change,
entries,
}
}
fn push(&mut self, entry: Vec<u8>) -> Option<&[u8]> {
if self.entries.len() >= LZW_MAX_ENTRIES {
None
} else {
self.entries.push(Some(entry));
self.entries.last()?.as_deref()
}
}
fn register(&mut self, prev: usize, new_byte: u8) -> Option<&[u8]> {
let prev_entry = self.get(prev)?;
let mut new_entry = Vec::with_capacity(prev_entry.len() + 1);
new_entry.extend(prev_entry);
new_entry.push(new_byte);
self.push(new_entry)
}
fn get(&self, index: usize) -> Option<&[u8]> {
self.entries.get(index)?.as_deref()
}
fn clear(&mut self) {
self.entries.truncate(LZW_INITIAL_SIZE);
}
fn size(&self) -> usize {
self.entries.len()
}
fn code_length(&self) -> u8 {
let adjusted = self.entries.len() + if self.early_change { 1 } else { 0 };
if adjusted >= 2048 {
12
} else if adjusted >= 1024 {
11
} else if adjusted >= 512 {
10
} else {
9
}
}
}
struct LzwBitReader<'a> {
data: &'a [u8],
bit_pos: usize,
}
impl<'a> LzwBitReader<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, bit_pos: 0 }
}
fn read(&mut self, bit_size: u8) -> Option<u32> {
let byte_pos = self.bit_pos / 8;
if byte_pos >= self.data.len() {
return None;
}
let bit_offset = self.bit_pos % 8;
let end_byte = (self.bit_pos + bit_size as usize - 1) / 8;
let mut buf = [0u8; 8];
for (i, b) in buf.iter_mut().enumerate().take(end_byte - byte_pos + 1) {
*b = *self.data.get(byte_pos + i)?;
}
let bits = u64::from_be_bytes(buf);
let shift = 64 - bit_offset - bit_size as usize;
let mask = (1u64 << bit_size) - 1;
let value = ((bits >> shift) & mask) as u32;
self.bit_pos += bit_size as usize;
Some(value)
}
}
fn decode_ascii_hex(data: &[u8]) -> Result<Vec<u8>, PdfError> {
let mut result = Vec::with_capacity(data.len() / 2);
let mut high: Option<u8> = None;
for &b in data {
if b == b'>' {
break;
}
if b.is_ascii_whitespace() {
continue;
}
let nibble = hex_digit(b)
.ok_or_else(|| PdfError::DecompressionError(format!("invalid hex digit: 0x{b:02x}")))?;
match high {
None => high = Some(nibble),
Some(h) => {
result.push(h << 4 | nibble);
high = None;
}
}
}
if let Some(h) = high {
result.push(h << 4);
}
Ok(result)
}
fn decode_ascii85(data: &[u8]) -> Result<Vec<u8>, PdfError> {
let mut result = Vec::with_capacity(data.len() * 4 / 5);
let mut tuple: u64 = 0;
let mut count = 0u8;
for &b in data {
if b == b'~' {
break; }
if b.is_ascii_whitespace() {
continue;
}
if b == b'z' && count == 0 {
result.extend_from_slice(&[0, 0, 0, 0]);
continue;
}
if !(b'!'..=b'u').contains(&b) {
continue; }
tuple = tuple * 85 + (b - b'!') as u64;
count += 1;
if count == 5 {
result.push((tuple >> 24) as u8);
result.push((tuple >> 16) as u8);
result.push((tuple >> 8) as u8);
result.push(tuple as u8);
tuple = 0;
count = 0;
}
}
if count > 0 {
for _ in count..5 {
tuple = tuple * 85 + 84; }
for i in 0..(count - 1) {
result.push((tuple >> (24 - i * 8)) as u8);
}
}
Ok(result)
}
fn decode_run_length(data: &[u8], budget: DecodeBudget) -> Result<Vec<u8>, PdfError> {
let mut result = Vec::new();
let mut i = 0;
while i < data.len() {
budget.check(result.len())?;
let length_byte = data[i];
i += 1;
if length_byte < 128 {
let count = length_byte as usize + 1;
if i + count > data.len() {
break;
}
result.extend_from_slice(&data[i..i + count]);
i += count;
} else if length_byte > 128 {
if i >= data.len() {
break;
}
let count = 257 - length_byte as usize;
let val = data[i];
i += 1;
for _ in 0..count {
result.push(val);
}
} else {
break;
}
}
Ok(result)
}
fn decode_dct(data: &[u8]) -> Result<Vec<u8>, PdfError> {
use jpeg_decoder::Decoder;
#[cfg(target_arch = "wasm32")]
if let Some(pixels) = decode_dct_via_zune(data) {
return Ok(pixels);
}
let mut decoder = Decoder::new(data);
if has_adobe_rgb_marker(data) || is_raw_rgb_jpeg(data) {
decoder.set_color_transform(jpeg_decoder::ColorTransform::RGB);
} else if needs_ycck_override(data) {
decoder.set_color_transform(jpeg_decoder::ColorTransform::YCCK);
}
let pixels = match decoder.decode() {
Ok(p) => p,
Err(e) => {
if let Some(pixels) = decode_dct_zune(data) {
return Ok(pixels);
}
if let Some(patched) = patch_jpeg_dnl_height(data) {
return decode_dct(&patched);
}
if let Some(pixels) = decode_dct_tolerant(data) {
return Ok(pixels);
}
{
let mut padded = data.to_vec();
if padded.last() == Some(&0xFF) {
padded.pop();
}
padded.extend_from_slice(&[0xFF, 0xD9]);
let mut retry_dec = Decoder::new(&padded[..]);
if has_adobe_rgb_marker(&padded) || is_raw_rgb_jpeg(&padded) {
retry_dec.set_color_transform(jpeg_decoder::ColorTransform::RGB);
} else if needs_ycck_override(&padded) {
retry_dec.set_color_transform(jpeg_decoder::ColorTransform::YCCK);
}
if let Ok(pixels) = retry_dec.decode() {
if let Some(info) = retry_dec.info()
&& info.pixel_format == jpeg_decoder::PixelFormat::CMYK32
{
let mut result = pixels;
for b in result.iter_mut() {
*b = 255 - *b;
}
return Ok(result);
}
return Ok(pixels);
}
}
return Err(PdfError::DecompressionError(format!("DCTDecode: {e}")));
}
};
if let Some(info) = decoder.info()
&& info.pixel_format == jpeg_decoder::PixelFormat::CMYK32
{
let mut result = pixels;
for b in result.iter_mut() {
*b = 255 - *b;
}
return Ok(result);
}
Ok(pixels)
}
fn catch_silent<F, T>(f: F) -> Option<T>
where
F: FnOnce() -> Option<T> + std::panic::UnwindSafe,
{
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let result = std::panic::catch_unwind(f).ok().flatten();
std::panic::set_hook(prev);
result
}
#[cfg(target_arch = "wasm32")]
fn decode_dct_via_zune(data: &[u8]) -> Option<Vec<u8>> {
use zune_jpeg::JpegDecoder;
let n_comps = jpeg_dimensions_and_components(data)
.map(|(_, _, n)| n)
.unwrap_or(3);
let out_cs = match n_comps {
1 => zune_core::colorspace::ColorSpace::Luma,
4 => zune_core::colorspace::ColorSpace::CMYK,
_ => zune_core::colorspace::ColorSpace::RGB,
};
let options = zune_core::options::DecoderOptions::default().jpeg_set_out_colorspace(out_cs);
let mut decoder = JpegDecoder::new_with_options(std::io::Cursor::new(data), options);
decoder.decode().ok()
}
fn decode_dct_zune(data: &[u8]) -> Option<Vec<u8>> {
use zune_jpeg::JpegDecoder;
let data = data.to_vec();
catch_silent(move || {
let options = zune_core::options::DecoderOptions::default()
.jpeg_set_out_colorspace(zune_core::colorspace::ColorSpace::LumaA);
let mut decoder = JpegDecoder::new_with_options(std::io::Cursor::new(&data), options);
decoder.decode().ok()
})
}
fn decode_dct_tolerant(data: &[u8]) -> Option<Vec<u8>> {
let n_comps = jpeg_dimensions_and_components(data)
.map(|(_, _, n)| n)
.unwrap_or(3);
let data = data.to_vec();
catch_silent(move || {
use zune_jpeg::JpegDecoder;
let out_cs = match n_comps {
1 => zune_core::colorspace::ColorSpace::Luma,
4 => zune_core::colorspace::ColorSpace::CMYK,
_ => zune_core::colorspace::ColorSpace::RGB,
};
let options = zune_core::options::DecoderOptions::default()
.set_strict_mode(false)
.jpeg_set_out_colorspace(out_cs);
let mut decoder = JpegDecoder::new_with_options(std::io::Cursor::new(&data), options);
decoder.decode().ok()
})
}
fn patch_jpeg_dnl_height(data: &[u8]) -> Option<Vec<u8>> {
let dnl_height = {
let mut pos = 0;
let mut found = None;
while pos + 4 < data.len() {
if data[pos] == 0xFF && data[pos + 1] == 0xDC {
if pos + 5 < data.len() {
let h = ((data[pos + 4] as u16) << 8) | data[pos + 5] as u16;
found = Some((pos, h));
}
break;
}
pos += 1;
}
found
};
let (dnl_pos, height) = dnl_height?;
if height == 0 {
return None;
}
let mut patched = data.to_vec();
let mut pos = 2; while pos + 8 < patched.len() {
if patched[pos] != 0xFF {
pos += 1;
continue;
}
let marker = patched[pos + 1];
if (0xC0..=0xC3).contains(&marker) {
patched[pos + 5] = (height >> 8) as u8;
patched[pos + 6] = (height & 0xFF) as u8;
break;
}
if marker == 0xDA {
break; }
if pos + 3 < patched.len() {
let seg_len = ((patched[pos + 2] as usize) << 8) | patched[pos + 3] as usize;
pos += 2 + seg_len;
} else {
break;
}
}
if dnl_pos + 6 <= patched.len() {
patched.drain(dnl_pos..dnl_pos + 6);
}
Some(patched)
}
pub fn patch_jpeg_sof_height(data: &mut [u8], new_height: u16) {
if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
return;
}
let mut pos = 2;
while pos + 4 < data.len() {
if data[pos] != 0xFF {
pos += 1;
continue;
}
let marker = data[pos + 1];
if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC {
if pos + 6 < data.len() {
data[pos + 5] = (new_height >> 8) as u8;
data[pos + 6] = (new_height & 0xFF) as u8;
}
return;
}
if marker == 0xDA {
return; }
let seg_len = if pos + 3 < data.len() {
((data[pos + 2] as usize) << 8) | data[pos + 3] as usize
} else {
return;
};
pos += 2 + seg_len;
}
}
pub(crate) fn jpeg_dimensions_and_components(data: &[u8]) -> Option<(u32, u32, u8)> {
if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
return None;
}
let mut pos = 2;
while pos + 4 < data.len() {
if data[pos] != 0xFF {
pos += 1;
continue;
}
let marker = data[pos + 1];
if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC {
if pos + 9 < data.len() {
let h = ((data[pos + 5] as u32) << 8) | data[pos + 6] as u32;
let w = ((data[pos + 7] as u32) << 8) | data[pos + 8] as u32;
let n = data[pos + 9];
return Some((w, h, n));
}
}
if marker == 0xDA {
break;
}
let seg_len = ((data[pos + 2] as usize) << 8) | data[pos + 3] as usize;
pos += 2 + seg_len;
}
None
}
pub fn jpeg_dimensions(data: &[u8]) -> Option<(u32, u32)> {
if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
return None;
}
let mut pos = 2;
while pos + 4 < data.len() {
if data[pos] != 0xFF {
pos += 1;
continue;
}
let marker = data[pos + 1];
if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC {
if pos + 9 < data.len() {
let mut h = ((data[pos + 5] as u32) << 8) | data[pos + 6] as u32;
let w = ((data[pos + 7] as u32) << 8) | data[pos + 8] as u32;
if h == 0 || h == 0xFFFF {
if let Some(dnl_h) = find_dnl_height(data) {
h = dnl_h as u32;
}
}
return Some((w, h));
}
}
if marker == 0xDA {
break; }
let seg_len = ((data[pos + 2] as usize) << 8) | data[pos + 3] as usize;
pos += 2 + seg_len;
}
None
}
fn find_dnl_height(data: &[u8]) -> Option<u16> {
let mut pos = 0;
while pos + 5 < data.len() {
if data[pos] == 0xFF && data[pos + 1] == 0xDC && pos + 5 < data.len() {
return Some(((data[pos + 4] as u16) << 8) | data[pos + 5] as u16);
}
pos += 1;
}
None
}
fn has_adobe_rgb_marker(data: &[u8]) -> bool {
let mut has_ct0 = false;
let mut uniform_sampling = false;
let mut i = 2; while i + 4 < data.len() {
if data[i] != 0xFF {
break;
}
let marker = data[i + 1];
if marker == 0xDA {
break; }
let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
if i + 2 + len > data.len() {
break;
}
if marker == 0xEE && len >= 14 {
let color_transform = data[i + 2 + 13];
has_ct0 = color_transform == 0;
}
if (marker == 0xC0 || marker == 0xC2) && i + 9 < data.len() {
let ncomp = data[i + 9] as usize;
if ncomp == 3 && i + 10 + ncomp * 3 <= data.len() {
let s0 = data[i + 11]; let s1 = data[i + 14]; let s2 = data[i + 17]; uniform_sampling = s0 == s1 && s1 == s2;
}
}
i += 2 + len;
}
has_ct0 && uniform_sampling
}
fn is_raw_rgb_jpeg(data: &[u8]) -> bool {
let mut has_jfif = false;
let mut has_adobe = false;
let mut non_standard_ids = false;
let mut uniform_sampling = false;
let mut n_components = 0u8;
let mut i = 2; while i + 4 < data.len() {
if data[i] != 0xFF {
break;
}
let marker = data[i + 1];
if marker == 0xDA {
break;
}
let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
if i + 2 + len > data.len() {
break;
}
if marker == 0xE0 && len >= 7 && &data[i + 4..i + 9] == b"JFIF\x00" {
has_jfif = true;
}
if marker == 0xEE && len >= 7 && &data[i + 4..i + 9] == b"Adobe" {
has_adobe = true;
}
if (marker == 0xC0 || marker == 0xC2) && i + 9 < data.len() {
n_components = data[i + 9];
if n_components == 3 && i + 10 + 9 <= data.len() {
let id0 = data[i + 10];
let id1 = data[i + 13];
let id2 = data[i + 16];
non_standard_ids = !(id0 == 1 && id1 == 2 && id2 == 3);
let s0 = data[i + 11];
let s1 = data[i + 14];
let s2 = data[i + 17];
uniform_sampling = s0 == s1 && s1 == s2;
}
}
i += 2 + len;
}
n_components == 3 && non_standard_ids && uniform_sampling && !has_jfif && !has_adobe
}
fn needs_ycck_override(data: &[u8]) -> bool {
let mut last_ct = None;
let mut decoder_would_miss = false;
let mut n_components = 0u8;
let mut i = 2; while i + 4 < data.len() {
if data[i] != 0xFF {
break;
}
let marker = data[i + 1];
if marker == 0xDA {
break; }
let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
if i + 2 + len > data.len() {
break;
}
if marker == 0xEE && len >= 14 && &data[i + 4..i + 9] == b"Adobe" {
let ct = data[i + 2 + 13];
last_ct = Some(ct);
decoder_would_miss = data[i + 9] != 0;
}
if (marker == 0xC0 || marker == 0xC2) && i + 9 < data.len() {
n_components = data[i + 9];
}
i += 2 + len;
}
last_ct == Some(2) && decoder_would_miss && n_components == 4
}
fn decode_ccittfax(data: &[u8], parms: Option<&PdfDict>) -> Result<Vec<u8>, PdfError> {
use crate::objects::PdfObj;
let k = parms.and_then(|p| p.get_int(b"K")).unwrap_or(0) as i32;
let columns = parms.and_then(|p| p.get_int(b"Columns")).unwrap_or(1728) as u16;
let rows_limit = parms.and_then(|p| p.get_int(b"Rows")).unwrap_or(0) as u32;
let end_of_block = parms
.and_then(|p| match p.get(b"EndOfBlock") {
Some(PdfObj::Bool(b)) => Some(*b),
_ => None,
})
.unwrap_or(true);
let black_is1 = parms
.and_then(|p| match p.get(b"BlackIs1") {
Some(PdfObj::Bool(b)) => Some(*b),
_ => None,
})
.unwrap_or(false);
let encoded_byte_align = parms
.and_then(|p| match p.get(b"EncodedByteAlign") {
Some(PdfObj::Bool(b)) => Some(*b),
_ => None,
})
.unwrap_or(false);
let encoding = if k < 0 {
hayro_ccitt::EncodingMode::Group4
} else if k == 0 {
hayro_ccitt::EncodingMode::Group3_1D
} else {
hayro_ccitt::EncodingMode::Group3_2D { k: k as u32 }
};
let settings = hayro_ccitt::DecodeSettings {
columns: columns as u32,
rows: if rows_limit > 0 { rows_limit } else { u32::MAX },
end_of_block,
end_of_line: false,
rows_are_byte_aligned: encoded_byte_align,
encoding,
invert_black: false,
};
decode_ccitt_hayro(data, &settings, black_is1)
}
struct CcittByteDecoder {
output: Vec<u8>,
current_byte: u8,
bit_pos: u8,
black_is1: bool,
}
impl CcittByteDecoder {
fn new(black_is1: bool) -> Self {
Self {
output: Vec::new(),
current_byte: 0,
bit_pos: 0,
black_is1,
}
}
fn flush_byte(&mut self) {
if self.bit_pos > 0 {
let remaining = 8 - self.bit_pos;
self.current_byte <<= remaining;
if !self.black_is1 {
self.current_byte |= (1u8 << remaining) - 1;
}
self.output.push(self.current_byte);
self.current_byte = 0;
self.bit_pos = 0;
}
}
}
impl hayro_ccitt::Decoder for CcittByteDecoder {
fn push_pixel(&mut self, white: bool) {
let bit = if self.black_is1 { !white } else { white };
self.current_byte = (self.current_byte << 1) | (bit as u8);
self.bit_pos += 1;
if self.bit_pos == 8 {
self.output.push(self.current_byte);
self.current_byte = 0;
self.bit_pos = 0;
}
}
fn push_pixel_chunk(&mut self, white: bool, chunk_count: u32) {
if self.bit_pos != 0 {
for _ in 0..chunk_count * 8 {
self.push_pixel(white);
}
return;
}
let byte = if (self.black_is1 && !white) || (!self.black_is1 && white) {
0xFF
} else {
0x00
};
for _ in 0..chunk_count {
self.output.push(byte);
}
}
fn next_line(&mut self) {
self.flush_byte();
}
}
fn decode_ccitt_hayro(
data: &[u8],
settings: &hayro_ccitt::DecodeSettings,
black_is1: bool,
) -> Result<Vec<u8>, PdfError> {
let mut decoder = CcittByteDecoder::new(black_is1);
let hayro_err = hayro_ccitt::decode(data, &mut decoder, settings).err();
if let Some(e) = hayro_err
&& e != hayro_ccitt::DecodeError::UnexpectedEof
{
let fallback = decode_ccitt_fax(data, settings, black_is1);
use std::sync::atomic::{AtomicBool, Ordering};
static WARNED: AtomicBool = AtomicBool::new(false);
if fallback.len() > decoder.output.len() {
if !WARNED.swap(true, Ordering::Relaxed) {
eprintln!(
"[CCITT] hayro-ccitt error: {} — fell back to `fax` crate",
e
);
}
return Ok(fallback);
}
if !WARNED.swap(true, Ordering::Relaxed) {
eprintln!("[CCITT] decode warning: {} (using partial data)", e);
}
}
Ok(decoder.output)
}
fn decode_ccitt_fax(
data: &[u8],
settings: &hayro_ccitt::DecodeSettings,
black_is1: bool,
) -> Vec<u8> {
let width = settings.columns as u16;
let row_bytes = settings.columns.div_ceil(8) as usize;
let mut out: Vec<u8> = Vec::new();
let white_byte: u8 = if black_is1 { 0x00 } else { 0xFF };
let black_byte: u8 = !white_byte;
let rows_limit = if settings.rows == u32::MAX || settings.rows == 0 {
None
} else {
Some(settings.rows.min(u16::MAX as u32) as u16)
};
let mut emit_row = |transitions: &[u16]| {
let mut row = vec![white_byte; row_bytes];
let mut color_white = true;
let mut cursor: u16 = 0;
let iter = transitions.iter().copied().chain(std::iter::once(width));
for next in iter {
let end = next.min(width);
if !color_white && end > cursor {
fill_bits(&mut row, cursor as usize, end as usize, black_byte != 0);
}
color_white = !color_white;
cursor = end;
if cursor >= width {
break;
}
}
out.extend_from_slice(&row);
};
match settings.encoding {
hayro_ccitt::EncodingMode::Group4 => {
let _ = fax::decoder::decode_g4(data.iter().copied(), width, rows_limit, &mut emit_row);
}
hayro_ccitt::EncodingMode::Group3_1D | hayro_ccitt::EncodingMode::Group3_2D { .. } => {
let _ = fax::decoder::decode_g3(data.iter().copied(), &mut emit_row);
}
}
if let Some(target_rows) = rows_limit {
let expected = row_bytes * target_rows as usize;
if out.len() < expected {
out.resize(expected, white_byte);
}
}
out
}
fn fill_bits(row: &mut [u8], start: usize, end: usize, black_is_one: bool) {
if end <= start {
return;
}
for x in start..end {
let byte = x / 8;
let bit = 0x80u8 >> (x % 8);
if black_is_one {
row[byte] |= bit;
} else {
row[byte] &= !bit;
}
}
}
fn decode_jbig2(data: &[u8], globals: Option<&[u8]>) -> Result<Vec<u8>, PdfError> {
#[cfg(not(target_arch = "wasm32"))]
let image = {
let data_owned = data.to_vec();
let globals_owned = globals.map(|g| g.to_vec());
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let result = hayro_jbig2::decode_embedded(&data_owned, globals_owned.as_deref());
let _ = tx.send(result);
});
let timeout_secs = 5 + (data.len() as u64 / (1024 * 1024)) * 5;
rx.recv_timeout(std::time::Duration::from_secs(timeout_secs))
.map_err(|_| PdfError::DecompressionError("JBIG2: decode timed out".into()))?
.map_err(|e| PdfError::DecompressionError(format!("JBIG2: {e}")))?
};
#[cfg(target_arch = "wasm32")]
let image = hayro_jbig2::decode_embedded(data, globals)
.map_err(|e| PdfError::DecompressionError(format!("JBIG2: {e}")))?;
let row_bytes = (image.width as usize).div_ceil(8);
let mut packed = vec![0xFFu8; row_bytes * image.height as usize];
for y in 0..image.height as usize {
for x in 0..image.width as usize {
if image.data[y * image.width as usize + x] {
packed[y * row_bytes + x / 8] &= !(0x80 >> (x % 8));
}
}
}
Ok(packed)
}
#[cfg(feature = "jpx")]
fn decode_jpx(data: &[u8]) -> Result<Vec<u8>, PdfError> {
if data.is_empty() {
return Ok(Vec::new());
}
let image = hayro_jpeg2000::Image::new(data, &hayro_jpeg2000::DecodeSettings::default())
.map_err(|e| PdfError::DecompressionError(format!("JPXDecode: {e}")))?;
image
.decode()
.map_err(|e| PdfError::DecompressionError(format!("JPXDecode: {e}")))
}
#[cfg(feature = "jpx")]
pub fn decode_jpx_no_palette(data: &[u8]) -> Result<(Vec<u8>, u8), PdfError> {
if data.is_empty() {
return Ok((Vec::new(), 8));
}
let settings = hayro_jpeg2000::DecodeSettings {
resolve_palette_indices: false,
..Default::default()
};
let image = hayro_jpeg2000::Image::new(data, &settings)
.map_err(|e| PdfError::DecompressionError(format!("JPXDecode: {e}")))?;
let bit_depth = image.original_bit_depth();
let pixels = image
.decode()
.map_err(|e| PdfError::DecompressionError(format!("JPXDecode: {e}")))?;
Ok((pixels, bit_depth))
}
#[cfg(feature = "jpx")]
pub fn jpx_color_info(data: &[u8]) -> Option<(u8, bool)> {
let image =
hayro_jpeg2000::Image::new(data, &hayro_jpeg2000::DecodeSettings::default()).ok()?;
Some((image.color_space().num_channels(), image.has_alpha()))
}
#[cfg(feature = "jpx")]
pub fn jpx_dimensions(data: &[u8]) -> Option<(u32, u32)> {
let image =
hayro_jpeg2000::Image::new(data, &hayro_jpeg2000::DecodeSettings::default()).ok()?;
Some((image.width(), image.height()))
}
pub fn decode_pre_jpx(raw: &[u8], dict: &crate::objects::PdfDict) -> Vec<u8> {
let (filters, parms) = parse_filters(dict, None).unwrap_or_default();
let pre_count = filters
.iter()
.take_while(|f| !matches!(f, Filter::JPXDecode))
.count();
if pre_count == 0 {
return raw.to_vec();
}
let pre_parms: Vec<_> = parms.into_iter().take(pre_count).collect();
decode_stream(raw, &filters[..pre_count], &pre_parms, None).unwrap_or_else(|_| raw.to_vec())
}
const MAX_PREDICTOR_COLUMNS: i64 = 100_000;
const MAX_PREDICTOR_COLORS: i64 = 32;
fn validate_decode_parm(value: Option<i64>, default: i64, max: i64) -> Option<usize> {
let v = value.unwrap_or(default);
if v >= 1 && v <= max {
usize::try_from(v).ok()
} else {
None
}
}
fn apply_predictor(data: &[u8], parms: &PdfDict, predictor: i64) -> Result<Vec<u8>, PdfError> {
let (Some(columns), Some(colors), Some(bpc)) = (
validate_decode_parm(parms.get_int(b"Columns"), 1, MAX_PREDICTOR_COLUMNS),
validate_decode_parm(parms.get_int(b"Colors"), 1, MAX_PREDICTOR_COLORS),
validate_decode_parm(
parms.get_int(b"BitsPerComponent"),
8,
stet_graphics::image_limits::MAX_BITS_PER_COMPONENT,
),
) else {
return Ok(data.to_vec());
};
let Some(bytes_per_pixel) = colors.checked_mul(bpc).map(|b| b.div_ceil(8)) else {
return Ok(data.to_vec());
};
let Some(row_bytes) = columns
.checked_mul(colors)
.and_then(|c| c.checked_mul(bpc))
.map(|b| b.div_ceil(8))
else {
return Ok(data.to_vec());
};
if row_bytes == 0 || bytes_per_pixel == 0 {
return Ok(data.to_vec());
}
if predictor == 2 {
if bpc < 8 {
apply_tiff_predictor_subbyte(data, columns, colors, bpc, row_bytes)
} else if bpc == 16 {
apply_tiff_predictor_16bit(data, columns, colors, row_bytes)
} else {
apply_tiff_predictor(data, row_bytes, bytes_per_pixel)
}
} else if predictor >= 10 {
apply_png_predictor(data, row_bytes, bytes_per_pixel)
} else {
Ok(data.to_vec())
}
}
fn apply_tiff_predictor_subbyte(
data: &[u8],
columns: usize,
colors: usize,
bpc: usize,
row_bytes: usize,
) -> Result<Vec<u8>, PdfError> {
let samples_per_row = columns * colors;
let mask = (1u8 << bpc) - 1; let mut result = Vec::with_capacity(data.len());
for row in data.chunks(row_bytes) {
let mut out_row = vec![0u8; row.len()];
out_row[..row.len()].copy_from_slice(row);
let mut prev = vec![0u8; colors];
for col in 0..columns {
for c in 0..colors {
let sample_idx = col * colors + c;
if sample_idx >= samples_per_row {
break;
}
let bit_offset = sample_idx * bpc;
let byte_idx = bit_offset / 8;
let bit_pos = 8 - bpc - (bit_offset % 8); if byte_idx >= row.len() {
break;
}
let encoded = (row[byte_idx] >> bit_pos) & mask;
let decoded = (encoded.wrapping_add(prev[c])) & mask;
prev[c] = decoded;
out_row[byte_idx] = (out_row[byte_idx] & !(mask << bit_pos)) | (decoded << bit_pos);
}
}
result.extend_from_slice(&out_row);
}
Ok(result)
}
fn apply_tiff_predictor_16bit(
data: &[u8],
columns: usize,
colors: usize,
row_bytes: usize,
) -> Result<Vec<u8>, PdfError> {
let mut result = Vec::with_capacity(data.len());
for row in data.chunks(row_bytes) {
let mut out_row = vec![0u8; row.len()];
let mut prev = vec![0u16; colors];
for col in 0..columns {
for c in 0..colors {
let byte_idx = (col * colors + c) * 2;
if byte_idx + 1 >= row.len() {
break;
}
let encoded = u16::from_be_bytes([row[byte_idx], row[byte_idx + 1]]);
let decoded = encoded.wrapping_add(prev[c]);
prev[c] = decoded;
let [hi, lo] = decoded.to_be_bytes();
out_row[byte_idx] = hi;
out_row[byte_idx + 1] = lo;
}
}
result.extend_from_slice(&out_row);
}
Ok(result)
}
fn apply_tiff_predictor(
data: &[u8],
row_bytes: usize,
bytes_per_pixel: usize,
) -> Result<Vec<u8>, PdfError> {
let mut result = Vec::with_capacity(data.len());
for row in data.chunks(row_bytes) {
let mut out_row = vec![0u8; row.len()];
for i in 0..row.len() {
let left = if i >= bytes_per_pixel {
out_row[i - bytes_per_pixel]
} else {
0
};
out_row[i] = row[i].wrapping_add(left);
}
result.extend_from_slice(&out_row);
}
Ok(result)
}
fn apply_png_predictor(
data: &[u8],
row_bytes: usize,
bytes_per_pixel: usize,
) -> Result<Vec<u8>, PdfError> {
let stride = row_bytes + 1;
if row_bytes > 0
&& !data.is_empty()
&& data.len().is_multiple_of(row_bytes)
&& !data.len().is_multiple_of(stride)
{
return Ok(data.to_vec());
}
let num_rows = data.len() / stride;
let mut result = Vec::with_capacity(num_rows * row_bytes);
let mut prev_row = vec![0u8; row_bytes];
for row_idx in 0..num_rows {
let row_start = row_idx * stride;
if row_start >= data.len() {
break;
}
let filter_type = data[row_start];
let row_data = &data[row_start + 1..std::cmp::min(row_start + stride, data.len())];
let mut out_row = vec![0u8; row_data.len()];
match filter_type {
0 => {
out_row.copy_from_slice(row_data);
}
1 => {
for i in 0..row_data.len() {
let left = if i >= bytes_per_pixel {
out_row[i - bytes_per_pixel]
} else {
0
};
out_row[i] = row_data[i].wrapping_add(left);
}
}
2 => {
for i in 0..row_data.len() {
let up = if i < prev_row.len() { prev_row[i] } else { 0 };
out_row[i] = row_data[i].wrapping_add(up);
}
}
3 => {
for i in 0..row_data.len() {
let left = if i >= bytes_per_pixel {
out_row[i - bytes_per_pixel] as u16
} else {
0
};
let up = if i < prev_row.len() {
prev_row[i] as u16
} else {
0
};
out_row[i] = row_data[i].wrapping_add(((left + up) / 2) as u8);
}
}
4 => {
for i in 0..row_data.len() {
let left = if i >= bytes_per_pixel {
out_row[i - bytes_per_pixel]
} else {
0
};
let up = if i < prev_row.len() { prev_row[i] } else { 0 };
let up_left = if i >= bytes_per_pixel && i - bytes_per_pixel < prev_row.len() {
prev_row[i - bytes_per_pixel]
} else {
0
};
out_row[i] = row_data[i].wrapping_add(paeth(left, up, up_left));
}
}
_ => {
out_row.copy_from_slice(row_data);
}
}
prev_row[..out_row.len()].copy_from_slice(&out_row);
result.extend_from_slice(&out_row);
}
Ok(result)
}
fn paeth(a: u8, b: u8, c: u8) -> u8 {
let a = a as i16;
let b = b as i16;
let c = c as i16;
let p = a + b - c;
let pa = (p - a).abs();
let pb = (p - b).abs();
let pc = (p - c).abs();
if pa <= pb && pa <= pc {
a as u8
} else if pb <= pc {
b as u8
} else {
c as u8
}
}
fn hex_digit(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flate_round_trip() {
use flate2::Compression;
use flate2::write::ZlibEncoder;
use std::io::Write;
let original = b"Hello, PDF world! This is a test of FlateDecode.";
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(original).unwrap();
let compressed = enc.finish().unwrap();
let decoded = decode_flate(&compressed, None, DecodeBudget::default()).unwrap();
assert_eq!(&decoded, original);
}
#[test]
fn ascii_hex_decode() {
let decoded = decode_ascii_hex(b"48656C6C6F>").unwrap();
assert_eq!(&decoded, b"Hello");
}
#[test]
fn ascii_hex_odd_digits() {
let decoded = decode_ascii_hex(b"ABC>").unwrap();
assert_eq!(decoded, vec![0xAB, 0xC0]);
}
#[test]
fn ascii85_decode() {
let decoded = decode_ascii85(b"FCfN8~>").unwrap();
assert_eq!(&decoded, b"test");
}
#[test]
fn ascii85_z_shortcut() {
let decoded = decode_ascii85(b"z~>").unwrap();
assert_eq!(decoded, vec![0, 0, 0, 0]);
}
#[test]
fn run_length_decode() {
let data = vec![2, b'A', b'B', b'C', 253, b'X', 128];
let decoded = decode_run_length(&data, DecodeBudget::default()).unwrap();
assert_eq!(&decoded, b"ABCXXXX");
}
#[test]
fn png_predictor_none() {
let data = vec![0, 10, 20, 30];
let result = apply_png_predictor(&data, 3, 1).unwrap();
assert_eq!(result, vec![10, 20, 30]);
}
#[test]
fn png_predictor_sub() {
let data = vec![1, 5, 3, 4];
let result = apply_png_predictor(&data, 3, 1).unwrap();
assert_eq!(result, vec![5, 8, 12]);
}
#[test]
fn png_predictor_up() {
let data = vec![0, 10, 20, 30, 2, 5, 5, 5];
let result = apply_png_predictor(&data, 3, 1).unwrap();
assert_eq!(result, vec![10, 20, 30, 15, 25, 35]);
}
#[test]
fn filter_chain() {
use flate2::Compression;
use flate2::write::ZlibEncoder;
use std::io::Write;
let original = b"filter chain test data";
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
enc.write_all(original).unwrap();
let compressed = enc.finish().unwrap();
let mut hex = String::new();
for b in &compressed {
hex.push_str(&format!("{b:02X}"));
}
hex.push('>');
let filters = vec![Filter::ASCIIHexDecode, Filter::FlateDecode];
let parms = vec![None, None];
let decoded = decode_stream(hex.as_bytes(), &filters, &parms, None).unwrap();
assert_eq!(&decoded, original);
}
fn zlib(data: &[u8]) -> Vec<u8> {
use flate2::Compression;
use flate2::write::ZlibEncoder;
use std::io::Write;
let mut enc = ZlibEncoder::new(Vec::new(), Compression::best());
enc.write_all(data).unwrap();
enc.finish().unwrap()
}
#[test]
fn nested_flate_chain_is_refused_rather_than_expanded() {
let mut data = zlib(&vec![0u8; 4 << 20]);
let mut filters = vec![Filter::FlateDecode];
for _ in 0..3 {
data = zlib(&data);
filters.push(Filter::FlateDecode);
}
assert!(
data.len() < 1024,
"the bomb must stay small: {}",
data.len()
);
let parms = vec![None; filters.len()];
let err =
decode_stream_bounded(&data, &filters, &parms, None, DecodeBudget::new(64 * 1024))
.unwrap_err();
assert!(
matches!(err, PdfError::DecompressionError(ref m) if m.contains("exceeds")),
"expected a budget refusal, got {err:?}"
);
}
#[test]
fn run_length_on_flate_is_refused() {
let rle = b"\x81\x00".repeat(64 << 10); let data = zlib(&rle);
let filters = [Filter::FlateDecode, Filter::RunLengthDecode];
let parms = vec![None; filters.len()];
let err =
decode_stream_bounded(&data, &filters, &parms, None, DecodeBudget::new(64 * 1024))
.unwrap_err();
assert!(matches!(err, PdfError::DecompressionError(_)), "{err:?}");
}
#[test]
fn flate_budget_overrun_is_an_error_not_a_truncation() {
let data = zlib(&vec![0u8; 4 << 20]);
let err = decode_flate(&data, None, DecodeBudget::new(4096)).unwrap_err();
assert!(matches!(err, PdfError::DecompressionError(ref m) if m.contains("exceeds")));
}
#[test]
fn lzw_output_is_bounded() {
let mut bits = Vec::new();
let mut acc: u32 = 0;
let mut nbits = 0;
for code in std::iter::once(LZW_CLEAR_TABLE).chain(std::iter::repeat_n(0usize, 512)) {
acc = (acc << 9) | code as u32;
nbits += 9;
while nbits >= 8 {
bits.push((acc >> (nbits - 8)) as u8);
nbits -= 8;
}
}
let err = decode_lzw(&bits, None, DecodeBudget::new(32)).unwrap_err();
assert!(matches!(err, PdfError::DecompressionError(ref m) if m.contains("exceeds")));
}
#[test]
fn ordinary_streams_are_unaffected() {
let original = b"q 1 0 0 1 10 10 cm BT /F1 12 Tf (hello) Tj ET Q".repeat(1000);
let data = zlib(&original);
let decoded = decode_stream_bounded(
&data,
&[Filter::FlateDecode],
&[None],
None,
DecodeBudget::default(),
)
.unwrap();
assert_eq!(decoded, original);
}
fn dict_from(src: &[u8]) -> PdfDict {
let mut lexer = crate::lexer::Lexer::new(src);
match crate::lexer::parse_object(&mut lexer).unwrap() {
crate::objects::PdfObj::Dict(d) => d,
other => panic!("expected a dict, got {other:?}"),
}
}
#[test]
fn a_dict_declaring_nothing_gets_the_general_ceiling() {
let budget = DecodeBudget::for_stream(&dict_from(b"<</Type/ObjStm/N 4/First 20>>"));
assert_eq!(budget.limit(), MAX_DECODED_STREAM_BYTES);
}
#[test]
fn a_declared_image_raster_raises_the_ceiling() {
let budget = DecodeBudget::for_stream(&dict_from(
b"<</Subtype/Image/Width 72000/Height 48000/BitsPerComponent 8/ColorSpace/DeviceCMYK>>",
));
assert_eq!(budget.limit(), 72_000usize * 48_000 * 4);
}
#[test]
fn a_small_declared_image_does_not_lower_the_ceiling() {
let budget = DecodeBudget::for_stream(&dict_from(
b"<</Subtype/Image/Width 8/Height 8/BitsPerComponent 8/ColorSpace/DeviceGray>>",
));
assert_eq!(budget.limit(), MAX_DECODED_STREAM_BYTES);
}
#[test]
fn sub_byte_rows_are_padded_to_a_byte_boundary() {
let bytes = declared_image_bytes(&dict_from(
b"<</Width 9/Height 4/BitsPerComponent 1/ColorSpace/DeviceGray>>",
))
.unwrap();
assert_eq!(bytes, 2 * 4);
}
#[test]
fn out_of_range_dimensions_do_not_raise_the_ceiling() {
for src in [
&b"<</Width 999999999/Height 999999999/BitsPerComponent 8>>"[..],
&b"<</Width -1/Height 10/BitsPerComponent 8>>"[..],
&b"<</Width 10/Height 10/BitsPerComponent 999>>"[..],
] {
assert_eq!(
DecodeBudget::for_stream(&dict_from(src)).limit(),
MAX_DECODED_STREAM_BYTES,
"{}",
String::from_utf8_lossy(src)
);
}
}
#[test]
fn an_embedded_file_size_raises_the_ceiling() {
let budget = DecodeBudget::for_stream(&dict_from(
b"<</Type/EmbeddedFile/Params<</Size 2000000000>>>>",
));
assert_eq!(budget.limit(), 2_000_000_000);
}
}