use crate::image::decode_array::DecodeMap;
use crate::image::scanline;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Depth {
One,
Two,
Four,
Eight,
Sixteen,
}
impl Depth {
#[must_use]
pub const fn new(bpc: u32) -> Option<Self> {
match bpc {
1 => Some(Self::One),
2 => Some(Self::Two),
4 => Some(Self::Four),
8 => Some(Self::Eight),
16 => Some(Self::Sixteen),
_ => None,
}
}
#[must_use]
pub const fn bits(self) -> u32 {
match self {
Self::One => 1,
Self::Two => 2,
Self::Four => 4,
Self::Eight => 8,
Self::Sixteen => 16,
}
}
#[must_use]
pub const fn levels(self) -> usize {
1usize << self.bits()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Packed {
data: Box<[u8]>,
depth: Depth,
components: usize,
pitch: usize,
width: usize,
height: u32,
table: Box<[u8]>,
}
impl Packed {
#[must_use]
#[expect(
clippy::too_many_arguments,
reason = "packed samples are exactly this geometry, and naming each \
piece is what keeps the row walk from re-deriving any of it"
)]
pub fn new(
data: Box<[u8]>,
depth: Depth,
components: usize,
pitch: usize,
width: u32,
height: u32,
space: &crate::color::ColorSpace,
decode: Option<&pdfrum_object::Array>,
) -> Self {
Self::with_map(
data,
depth,
components,
pitch,
width,
height,
&DecodeMap::new(Some(space), components, depth.bits(), decode),
)
}
pub(crate) fn with_map(
data: Box<[u8]>,
depth: Depth,
components: usize,
pitch: usize,
width: u32,
height: u32,
decode: &DecodeMap,
) -> Self {
let levels = depth.levels();
let mut table = vec![0u8; components.saturating_mul(levels)].into_boxed_slice();
for component in 0..components {
for raw in 0..levels {
#[expect(
clippy::cast_precision_loss,
reason = "a table index below 65 536 is exact in f32"
)]
let value = decode.apply(component, raw as f32);
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "the clamp bounds the product to 0..=255"
)]
let byte = (value.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
if let Some(slot) = table.get_mut(component * levels + raw) {
*slot = byte;
}
}
}
Self {
data,
depth,
components,
pitch,
width: width as usize,
height,
table,
}
}
#[must_use]
pub const fn components(&self) -> usize {
self.components
}
#[must_use]
pub fn byte_size(&self) -> usize {
self.data.len() + self.table.len()
}
#[must_use]
pub fn truncated(&self) -> bool {
self.pitch
.checked_mul(self.height as usize)
.is_none_or(|want| self.data.len() < want)
}
}
#[derive(Debug)]
pub struct Unpacked<'a> {
packed: &'a Packed,
buf: Vec<u8>,
line: Vec<u8>,
y: u32,
}
impl<'a> Unpacked<'a> {
#[must_use]
pub fn new(packed: &'a Packed) -> Self {
Self {
buf: vec![0u8; packed.width.saturating_mul(packed.components)],
line: vec![0u8; packed.pitch],
packed,
y: 0,
}
}
pub fn next_row(&mut self) -> Option<&[u8]> {
if self.y >= self.packed.height {
return None;
}
let y = self.y as usize;
self.y += 1;
let p = self.packed;
let availability = scanline::scanline_into(&p.data, y, p.pitch, &mut self.line);
if availability == scanline::Availability::Absent {
self.buf.fill(0);
return Some(&self.buf);
}
let levels = p.depth.levels();
let table = &*p.table;
let line = &*self.line;
match p.depth {
Depth::Eight => {
let mut base = 0usize;
let mut component = 0usize;
for (slot, &raw) in self.buf.iter_mut().zip(line) {
*slot = table.get(base + usize::from(raw)).copied().unwrap_or(0);
component += 1;
base += levels;
if component == p.components {
component = 0;
base = 0;
}
}
if let Some(tail) = self.buf.get_mut(line.len()..) {
tail.fill(0);
}
}
Depth::Sixteen => {
let mut base = 0usize;
let mut component = 0usize;
for (i, slot) in self.buf.iter_mut().enumerate() {
let hi = line.get(i * 2).copied().unwrap_or(0);
let lo = line.get(i * 2 + 1).copied().unwrap_or(0);
let raw = usize::from(hi) * 256 + usize::from(lo);
*slot = table.get(base + raw).copied().unwrap_or(0);
component += 1;
base += levels;
if component == p.components {
component = 0;
base = 0;
}
}
}
Depth::One | Depth::Two | Depth::Four => {
let bits = p.depth.bits();
let per_byte = (8 / bits) as usize;
let mask = u32::from(u8::MAX) >> (8 - bits);
let mut base = 0usize;
let mut component = 0usize;
for (chunk, byte_index) in self.buf.chunks_mut(per_byte).zip(0usize..) {
let byte = u32::from(line.get(byte_index).copied().unwrap_or(0));
for (slot, k) in chunk.iter_mut().zip(0u32..) {
let shift = 8 - bits - k * bits;
let raw = ((byte >> shift) & mask) as usize;
*slot = table.get(base + raw).copied().unwrap_or(0);
component += 1;
base += levels;
if component == p.components {
component = 0;
base = 0;
}
}
}
}
}
Some(&self.buf)
}
#[must_use]
pub fn collect_all(mut self) -> Box<[u8]> {
let p = self.packed;
let stride = p.width.saturating_mul(p.components);
let mut out =
Vec::with_capacity(stride.saturating_mul(usize::try_from(p.height).unwrap_or(0)));
while let Some(row) = self.next_row() {
out.extend_from_slice(row);
}
out.into_boxed_slice()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::ColorSpace;
fn map(components: usize, bpc: u32) -> DecodeMap {
DecodeMap::new(Some(&ColorSpace::DeviceGray), components, bpc, None)
}
fn packed(data: &[u8], bpc: u32, components: usize, width: u32, height: u32) -> Packed {
let depth = Depth::new(bpc).expect("a real depth");
let pitch = (width as usize * components * bpc as usize).div_ceil(8);
Packed::with_map(
data.into(),
depth,
components,
pitch,
width,
height,
&map(components, bpc),
)
}
#[test]
fn eight_bit_samples_pass_through_unchanged() {
let p = packed(&[0, 128, 255, 7], 8, 1, 2, 2);
let mut u = Unpacked::new(&p);
assert_eq!(u.next_row(), Some(&[0, 128][..]));
assert_eq!(u.next_row(), Some(&[255, 7][..]));
assert_eq!(u.next_row(), None);
}
#[test]
fn one_bit_samples_widen_to_black_and_white() {
let p = packed(&[0b1010_0000], 1, 1, 4, 1);
let mut u = Unpacked::new(&p);
assert_eq!(u.next_row(), Some(&[255, 0, 255, 0][..]));
}
#[test]
fn four_bit_samples_span_the_range() {
let p = packed(&[0x0F, 0x80], 4, 1, 4, 1);
let mut u = Unpacked::new(&p);
assert_eq!(u.next_row(), Some(&[0, 255, 136, 0][..]));
}
#[test]
fn sixteen_bit_samples_keep_their_high_byte() {
let p = packed(&[0xFF, 0xFF, 0x00, 0x00], 16, 1, 2, 1);
let mut u = Unpacked::new(&p);
assert_eq!(u.next_row(), Some(&[255, 0][..]));
}
#[test]
fn an_absent_row_is_zero_and_a_short_one_is_padded() {
let p = packed(&[1, 2, 3, 4, 5], 8, 1, 2, 4);
let mut u = Unpacked::new(&p);
assert_eq!(u.next_row(), Some(&[1, 2][..]));
assert_eq!(u.next_row(), Some(&[3, 4][..]));
assert_eq!(u.next_row(), Some(&[5, 0][..]));
assert_eq!(u.next_row(), Some(&[0, 0][..]));
assert_eq!(u.next_row(), None);
assert!(p.truncated());
}
#[test]
fn a_decode_inversion_is_in_the_table() {
let decode = pdfrum_object::Array::of([
pdfrum_object::Object::Real(1.0),
pdfrum_object::Object::Real(0.0),
]);
let p = Packed::new(
vec![0, 255].into(),
Depth::Eight,
1,
2,
2,
1,
&ColorSpace::DeviceGray,
Some(&decode),
);
let mut u = Unpacked::new(&p);
assert_eq!(u.next_row(), Some(&[255, 0][..]));
}
#[test]
fn collecting_every_row_is_the_whole_image() {
let p = packed(&[1, 2, 3, 4], 8, 1, 2, 2);
assert_eq!(&*Unpacked::new(&p).collect_all(), &[1, 2, 3, 4]);
}
}