use pdfrum_object::{Resolve, names};
use crate::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PredictorKind {
None,
Tiff,
Png,
}
impl PredictorKind {
#[must_use]
pub fn from_predictor(n: i64) -> PredictorKind {
if n >= 10 {
PredictorKind::Png
} else if n == 2 {
PredictorKind::Tiff
} else {
PredictorKind::None
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PredictorParams {
pub kind: PredictorKind,
pub colors: u32,
pub bits_per_component: u32,
pub columns: u32,
}
impl Default for PredictorParams {
fn default() -> Self {
Self {
kind: PredictorKind::None,
colors: 1,
bits_per_component: 8,
columns: 1,
}
}
}
impl PredictorParams {
pub fn from_dict(d: &pdfrum_object::Dict, r: &impl Resolve) -> Result<Self, Error> {
let predictor = d.int(names::PREDICTOR, r).unwrap_or(0);
let colors = d.int(names::COLORS, r).unwrap_or(1);
let bits_per_component = d.int(names::BITS_PER_COMPONENT, r).unwrap_or(8);
let columns = d.int(names::COLUMNS, r).unwrap_or(1);
if colors < 0 || bits_per_component < 0 || columns < 0 {
return Err(Error::BadPredictorParams(
"/Colors, /BitsPerComponent and /Columns must not be negative",
));
}
let bits = i64::from(i32::MAX) - 7;
if columns
.checked_mul(colors)
.and_then(|n| n.checked_mul(bits_per_component))
.is_none_or(|n| n > bits)
{
return Err(Error::BadPredictorParams(
"/Columns * /Colors * /BitsPerComponent is too large for a row",
));
}
let fit = |n: i64| u32::try_from(n).map_err(|_| Error::SizeOverflow);
Ok(Self {
kind: PredictorKind::from_predictor(predictor),
colors: fit(colors)?,
bits_per_component: fit(bits_per_component)?,
columns: fit(columns)?,
})
}
fn row_size(self) -> Option<usize> {
let bits = u64::from(self.bits_per_component)
.checked_mul(u64::from(self.colors))?
.checked_mul(u64::from(self.columns))?
.checked_add(7)?;
usize::try_from(bits / 8).ok()
}
fn bytes_per_pixel(self) -> usize {
let bits = u64::from(self.colors) * u64::from(self.bits_per_component) + 7;
(bits / 8) as usize
}
}
pub fn predictor(data: Vec<u8>, params: PredictorParams) -> Result<Vec<u8>, Error> {
match params.kind {
PredictorKind::None => Ok(data),
PredictorKind::Png => png_predictor(&data, params),
PredictorKind::Tiff => tiff_predictor(data, params),
}
}
fn png_predictor(src: &[u8], params: PredictorParams) -> Result<Vec<u8>, Error> {
let row_size = params.row_size().ok_or(Error::SizeOverflow)?;
if row_size == 0 {
return Err(Error::BadPredictorParams("the row is zero bytes wide"));
}
let src_row_size = row_size + 1;
let row_count = (src.len() + row_size) / src_row_size;
if row_count == 0 {
return Err(Error::BadPredictorParams(
"the data is shorter than one row's tag byte",
));
}
let last_row_size = src.len() % src_row_size;
let mut dest_size = row_size.checked_mul(row_count).ok_or(Error::SizeOverflow)?;
if last_row_size != 0 {
dest_size = dest_size
.checked_sub(src_row_size - last_row_size)
.ok_or(Error::SizeOverflow)?;
}
let bpp = params.bytes_per_pixel();
let mut out: Vec<u8> = Vec::with_capacity(dest_size);
let mut at = 0usize;
let mut previous: Option<usize> = None;
for _ in 0..row_count {
let Some(&tag) = src.get(at) else { break };
let row = src.get(at + 1..).unwrap_or_default();
let len = row_size.min(row.len());
let row = row.get(..len).unwrap_or_default();
let start = out.len();
for i in 0..len {
let raw = row.get(i).copied().unwrap_or(0);
let left = if i >= bpp {
out.get(start + i - bpp).copied().unwrap_or(0)
} else {
0
};
let up = previous.and_then(|p| out.get(p + i)).copied().unwrap_or(0);
let upper_left = if i >= bpp {
previous
.and_then(|p| out.get(p + i - bpp))
.copied()
.unwrap_or(0)
} else {
0
};
let value = match tag {
1 => raw.wrapping_add(left),
2 => raw.wrapping_add(up),
3 => raw.wrapping_add(u8::midpoint(up, left)),
4 => raw.wrapping_add(paeth(left, up, upper_left)),
_ => raw,
};
out.push(value);
}
previous = Some(start);
at += len + 1;
}
Ok(out)
}
fn paeth(a: u8, b: u8, c: u8) -> u8 {
let p = i32::from(a) + i32::from(b) - i32::from(c);
let pa = (p - i32::from(a)).abs();
let pb = (p - i32::from(b)).abs();
let pc = (p - i32::from(c)).abs();
if pa <= pb && pa <= pc {
a
} else if pb <= pc {
b
} else {
c
}
}
fn tiff_predictor(mut data: Vec<u8>, params: PredictorParams) -> Result<Vec<u8>, Error> {
let row_size = params.row_size().ok_or(Error::SizeOverflow)?;
if row_size == 0 {
return Err(Error::BadPredictorParams("the row is zero bytes wide"));
}
let mut at = 0usize;
while at < data.len() {
let len = row_size.min(data.len() - at);
if let Some(row) = data.get_mut(at..at + len) {
tiff_predict_row(row, params);
}
at += len;
}
Ok(data)
}
fn tiff_predict_row(row: &mut [u8], params: PredictorParams) {
if params.bits_per_component == 1 {
tiff_predict_row_1bpc(row, params);
return;
}
let stride = (params.bits_per_component as usize * params.colors as usize) / 8;
if params.bits_per_component == 16 {
let mut i = stride;
while i + 1 < row.len() {
let previous = u16::from_be_bytes([
row.get(i - stride).copied().unwrap_or(0),
row.get(i - stride + 1).copied().unwrap_or(0),
]);
let current = u16::from_be_bytes([
row.get(i).copied().unwrap_or(0),
row.get(i + 1).copied().unwrap_or(0),
]);
let sum = current.wrapping_add(previous).to_be_bytes();
if let Some(slot) = row.get_mut(i..i + 2) {
slot.copy_from_slice(&sum);
}
i += 2;
}
return;
}
for i in stride..row.len() {
let previous = row.get(i - stride).copied().unwrap_or(0);
if let Some(slot) = row.get_mut(i) {
*slot = slot.wrapping_add(previous);
}
}
}
fn tiff_predict_row_1bpc(row: &mut [u8], params: PredictorParams) {
let declared =
u64::from(params.bits_per_component) * u64::from(params.colors) * u64::from(params.columns);
let available = row.len().saturating_mul(8);
let row_bits = usize::try_from(declared)
.unwrap_or(usize::MAX)
.min(available);
let mut previous = 0usize;
for i in 1..row_bits {
let bit = |at: usize| row.get(at / 8).map_or(0, |byte| (byte >> (7 - at % 8)) & 1);
let set = bit(i) ^ bit(previous);
if let Some(byte) = row.get_mut(i / 8) {
let mask = 1u8 << (7 - i % 8);
if set == 1 {
*byte |= mask;
} else {
*byte &= !mask;
}
}
previous = i;
}
}
#[cfg(test)]
mod tests {
use super::{PredictorKind, PredictorParams, paeth, predictor};
use crate::Error;
use pdfrum_object::{Dict, Name, NoResolve, Object, names};
fn parms(pairs: &[(&Name, i64)]) -> Dict {
Dict::from_pairs(
pairs
.iter()
.map(|(key, value)| ((*key).clone(), Object::Int(*value))),
)
}
fn png(colors: u32, bits_per_component: u32, columns: u32) -> PredictorParams {
PredictorParams {
kind: PredictorKind::Png,
colors,
bits_per_component,
columns,
}
}
fn tiff(colors: u32, bits_per_component: u32, columns: u32) -> PredictorParams {
PredictorParams {
kind: PredictorKind::Tiff,
colors,
bits_per_component,
columns,
}
}
#[test]
fn absent_parameters_give_the_defaults_and_no_predictor() {
let params = PredictorParams::from_dict(&Dict::new(), &NoResolve).expect("valid");
assert_eq!(params, PredictorParams::default());
assert_eq!(params.kind, PredictorKind::None);
assert_eq!(
(params.colors, params.bits_per_component, params.columns),
(1, 8, 1)
);
}
#[test]
fn the_predictor_number_classifies_by_range() {
let cases: [(i64, PredictorKind); 8] = [
(0, PredictorKind::None),
(1, PredictorKind::None),
(2, PredictorKind::Tiff),
(3, PredictorKind::None),
(9, PredictorKind::None),
(10, PredictorKind::Png),
(15, PredictorKind::Png),
(100, PredictorKind::Png),
];
for (n, expected) in cases {
let d = parms(&[
(names::PREDICTOR, n),
(names::COLORS, 3),
(names::BITS_PER_COMPONENT, 8),
(names::COLUMNS, 4),
]);
let params = PredictorParams::from_dict(&d, &NoResolve).expect("valid");
assert_eq!(params.kind, expected, "/Predictor {n}");
}
}
#[test]
fn negative_geometry_is_rejected() {
for (key, value) in [
(names::COLORS, -1i64),
(names::BITS_PER_COMPONENT, -8),
(names::COLUMNS, -1),
] {
let d = parms(&[(names::PREDICTOR, 12), (key, value)]);
assert!(
matches!(
PredictorParams::from_dict(&d, &NoResolve),
Err(Error::BadPredictorParams(_))
),
"{key:?} = {value}"
);
}
}
#[test]
fn a_row_too_wide_for_a_signed_row_size_is_rejected() {
let d = parms(&[
(names::PREDICTOR, 12),
(names::COLORS, 0x1_0000),
(names::BITS_PER_COMPONENT, 32),
(names::COLUMNS, 0x1_0000),
]);
assert!(matches!(
PredictorParams::from_dict(&d, &NoResolve),
Err(Error::BadPredictorParams(_))
));
}
#[test]
fn geometry_is_validated_even_when_no_predictor_is_selected() {
let d = parms(&[(names::PREDICTOR, 0), (names::COLORS, -1)]);
assert!(PredictorParams::from_dict(&d, &NoResolve).is_err());
}
#[test]
fn a_zero_width_row_fails_at_predict_time_not_parse_time() {
let d = parms(&[
(names::PREDICTOR, 12),
(names::COLORS, 0),
(names::BITS_PER_COMPONENT, 8),
(names::COLUMNS, 4),
]);
let params = PredictorParams::from_dict(&d, &NoResolve).expect("parses");
assert_eq!(params.colors, 0);
assert!(matches!(
predictor(vec![0; 16], params),
Err(Error::BadPredictorParams(_))
));
}
#[test]
fn no_predictor_returns_the_data_untouched() {
let data = vec![1u8, 2, 3, 4, 5];
assert_eq!(
predictor(data.clone(), PredictorParams::default()).expect("ok"),
data
);
}
#[test]
fn each_png_row_tag_over_a_known_image() {
let params = png(3, 8, 3);
let src = [0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9];
assert_eq!(
predictor(src.to_vec(), params).expect("ok"),
vec![1, 2, 3, 4, 5, 6, 7, 8, 9]
);
let src = [1u8, 1, 2, 3, 10, 10, 10, 100, 100, 100];
assert_eq!(
predictor(src.to_vec(), params).expect("ok"),
vec![1, 2, 3, 11, 12, 13, 111, 112, 113]
);
let src = [
0u8, 10, 20, 30, 40, 50, 60, 70, 80, 90, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, ];
assert_eq!(
predictor(src.to_vec(), params).expect("ok"),
vec![
10, 20, 30, 40, 50, 60, 70, 80, 90, 11, 21, 31, 41, 51, 61, 71, 81, 91
]
);
let src = [
0u8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1,
];
let out = predictor(src.to_vec(), params).expect("ok");
assert_eq!(out.get(9), Some(&5));
assert_eq!(out.get(12), Some(&7));
let src = [
0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
assert_eq!(
predictor(src.to_vec(), params).expect("ok").get(9..),
Some(&[1u8, 2, 3, 4, 5, 6, 7, 8, 9][..])
);
}
#[test]
fn an_invalid_row_tag_copies_verbatim() {
let params = png(1, 8, 4);
for tag in [0u8, 5, 6, 7, 200, 255] {
let src = [tag, 9, 8, 7, 6];
assert_eq!(
predictor(src.to_vec(), params).expect("never fails"),
vec![9, 8, 7, 6],
"tag {tag}"
);
}
}
#[test]
fn a_truncated_final_row_is_predicted_over_the_bytes_it_has() {
let params = png(1, 8, 4); let src = [
0u8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 5,
];
let out = predictor(src.to_vec(), params).expect("ok");
assert_eq!(out.len(), 4 + 4 + 2, "row_size * (rows - 1) + last_row_len");
assert_eq!(out.get(8..), Some(&[5u8, 10][..]));
}
#[test]
fn data_shorter_than_one_row_still_yields_one_row() {
let params = png(1, 8, 100); let src = [0u8, 1, 2, 3];
let out = predictor(src.to_vec(), params).expect("ok");
assert_eq!(out, vec![1, 2, 3], "input.len() - 1 bytes");
}
#[test]
fn png_data_with_only_a_tag_byte_yields_an_empty_row() {
let params = png(1, 8, 4);
assert_eq!(predictor(vec![2], params).expect("ok"), Vec::<u8>::new());
}
#[test]
fn empty_png_data_has_no_rows_at_all() {
let params = png(1, 8, 4);
assert!(matches!(
predictor(Vec::new(), params),
Err(Error::BadPredictorParams(_))
));
}
#[test]
fn png_stride_follows_the_sample_size() {
let params = png(3, 16, 2); let mut src = vec![1u8]; src.extend([0u8; 6]); src.extend([1u8; 6]); let out = predictor(src, params).expect("ok");
assert_eq!(out.get(6..), Some(&[1u8; 6][..]));
let params = png(1, 1, 8);
let out = predictor(vec![1, 0b1010_1010], params).expect("ok");
assert_eq!(out, vec![0b1010_1010]);
}
#[test]
fn tiff_eight_bit_differencing_uses_the_colour_count_as_its_stride() {
let params = tiff(3, 8, 3); let data = vec![1u8, 2, 3, 1, 1, 1, 1, 1, 1];
assert_eq!(
predictor(data, params).expect("ok"),
vec![1, 2, 3, 2, 3, 4, 3, 4, 5]
);
}
#[test]
fn tiff_sixteen_bit_differencing_adds_big_endian_samples() {
let params = tiff(1, 16, 3); let data = vec![0x01, 0x00, 0x00, 0x01, 0x00, 0x02];
assert_eq!(
predictor(data, params).expect("ok"),
vec![0x01, 0x00, 0x01, 0x01, 0x01, 0x03]
);
}
#[test]
fn tiff_one_bit_differencing_accumulates_by_xor() {
let params = tiff(1, 1, 8); assert_eq!(
predictor(vec![0b1000_0000], params).expect("ok"),
vec![0b1111_1111]
);
assert_eq!(
predictor(vec![0b0000_0000], params).expect("ok"),
vec![0b0000_0000]
);
assert_eq!(
predictor(vec![0b1010_1010], params).expect("ok"),
vec![0b1100_1100]
);
}
#[test]
fn tiff_one_bit_stops_at_the_bytes_that_exist() {
let params = tiff(1, 1, 32);
let out = predictor(vec![0b1000_0000], params).expect("never overruns");
assert_eq!(out, vec![0b1111_1111]);
}
#[test]
fn a_tiff_partial_final_row_is_predicted_over_its_own_bytes() {
let params = tiff(1, 8, 4); let data = vec![1u8, 1, 1, 1, 5, 5];
assert_eq!(
predictor(data, params).expect("ok"),
vec![1, 2, 3, 4, 5, 10]
);
}
#[test]
fn a_zero_width_tiff_row_is_rejected() {
let params = tiff(0, 8, 4);
assert!(matches!(
predictor(vec![1, 2, 3], params),
Err(Error::BadPredictorParams(_))
));
}
#[test]
fn tiff_with_a_zero_stride_doubles_every_byte() {
for bits_per_component in [2u32, 4] {
let params = tiff(1, bits_per_component, 8);
let data = vec![3u8, 100, 200];
assert_eq!(
predictor(data, params).expect("ok"),
vec![6, 200, 144],
"bpc {bits_per_component}"
);
}
}
#[test]
fn paeth_picks_the_closest_neighbour() {
assert_eq!(paeth(0, 0, 0), 0);
assert_eq!(paeth(10, 20, 30), 10);
assert_eq!(paeth(200, 100, 150), 150);
assert_eq!(paeth(1, 2, 3), 1);
assert_eq!(paeth(5, 5, 200), 5);
assert_eq!(paeth(0, 40, 0), 40);
}
}