use super::{Common, interpolate};
use crate::names;
use pdfrum_common::{Diagnostics, Limits};
use pdfrum_filters::decode_chain;
use pdfrum_object::{Resolve, Stream};
const VALID_BITS_PER_SAMPLE: [u32; 8] = [1, 2, 4, 8, 12, 16, 24, 32];
#[derive(Debug, Clone, PartialEq)]
pub struct Sampled {
pub domain: Box<[f32]>,
pub range: Box<[f32]>,
pub outputs: usize,
pub sizes: Box<[u32]>,
pub bits_per_sample: u32,
pub encode: Box<[f32]>,
pub decode: Box<[f32]>,
pub samples: Box<[u8]>,
pub sample_max: u32,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct BitReader<'a> {
data: &'a [u8],
bit_pos: u64,
}
impl<'a> BitReader<'a> {
pub(crate) fn new(data: &'a [u8]) -> Self {
Self { data, bit_pos: 0 }
}
pub(crate) fn seek_bits(&mut self, bits: u64) {
self.bit_pos = bits;
}
pub(crate) fn byte_align(&mut self) {
self.bit_pos = self.bit_pos.div_ceil(8) * 8;
}
pub(crate) fn remaining(&self) -> u64 {
let total = (self.data.len() as u64).saturating_mul(8);
total.saturating_sub(self.bit_pos)
}
pub(crate) fn read(&mut self, n: u32) -> u32 {
let mut out = 0u32;
for _ in 0..n.min(32) {
let byte = self
.data
.get(usize::try_from(self.bit_pos / 8).unwrap_or(usize::MAX))
.copied()
.unwrap_or(0);
let shift = 7u32 - u32::try_from(self.bit_pos % 8).unwrap_or(0);
out = (out << 1) | u32::from((byte >> shift) & 1);
self.bit_pos = self.bit_pos.saturating_add(1);
}
out
}
}
impl Sampled {
pub(super) fn load<R: Resolve>(
stream: &Stream,
common: &Common,
r: &R,
limits: &Limits,
diags: &mut Diagnostics,
) -> Option<Self> {
let outputs = common.outputs();
if outputs == 0 {
return None;
}
let inputs = common.inputs();
let bits_per_sample = u32::try_from(stream.dict.int(names::BITS_PER_SAMPLE, r)?).ok()?;
if !VALID_BITS_PER_SAMPLE.contains(&bits_per_sample) {
return None;
}
let size_array = stream.dict.array(names::SIZE, r)?;
if size_array.is_empty() {
return None;
}
let encode_array = stream.dict.array(names::ENCODE, r);
let mut sizes = Vec::with_capacity(inputs);
let mut encode = Vec::with_capacity(inputs * 2);
let mut total_samples: u64 = 1;
for i in 0..inputs {
let size = size_array.int_at(i).unwrap_or(0);
if size <= 0 {
return None;
}
let size = u32::try_from(size).ok()?;
sizes.push(size);
total_samples = total_samples.checked_mul(u64::from(size))?;
if let Some(a) = &encode_array {
encode.push(a.number_at_or_zero(i * 2));
encode.push(a.number_at_or_zero(i * 2 + 1));
} else {
encode.push(0.0);
#[expect(
clippy::cast_precision_loss,
reason = "sample counts far below f32's exact integer range"
)]
let top = if size == 1 { 1.0 } else { (size - 1) as f32 };
encode.push(top);
}
}
let total_bits = total_samples
.checked_mul(u64::from(bits_per_sample))?
.checked_mul(u64::try_from(outputs).ok()?)?;
if total_bits == 0 || total_bits > u64::from(u32::MAX) {
return None;
}
let total_bytes = usize::try_from(total_bits.div_ceil(8)).ok()?;
let samples: Box<[u8]> = decode_chain(stream, total_bytes, r, limits, diags)
.data
.into();
if total_bytes > samples.len() {
return None;
}
let decode_array = stream.dict.array(names::DECODE, r);
let decode: Box<[f32]> = (0..outputs * 2)
.map(|i| match &decode_array {
Some(a) => a.number_at_or_zero(i),
None => common.range.get(i).copied().unwrap_or(0.0),
})
.collect();
Some(Self {
domain: common.domain.clone(),
range: common.range.clone(),
outputs,
sizes: sizes.into(),
bits_per_sample,
encode: encode.into(),
decode,
samples,
sample_max: if bits_per_sample >= 32 {
u32::MAX
} else {
(1u32 << bits_per_sample) - 1
},
})
}
pub(super) fn eval(&self, input: &[f32], out: &mut [f32]) -> bool {
let inputs = self.sizes.len();
let mut blocksize = vec![0u64; inputs];
let mut acc = 1u64;
for (i, slot) in blocksize.iter_mut().enumerate() {
*slot = acc;
acc = acc.saturating_mul(u64::from(self.sizes.get(i).copied().unwrap_or(1)));
}
let mut lower = vec![0u64; inputs];
let mut frac = vec![0f32; inputs];
for i in 0..inputs {
let size = self.sizes.get(i).copied().unwrap_or(1);
let top = f32::from(u16::try_from(size.saturating_sub(1)).unwrap_or(u16::MAX));
let e = interpolate(
input.get(i).copied().unwrap_or(0.0),
self.domain.get(i * 2).copied().unwrap_or(0.0),
self.domain.get(i * 2 + 1).copied().unwrap_or(0.0),
self.encode.get(i * 2).copied().unwrap_or(0.0),
self.encode.get(i * 2 + 1).copied().unwrap_or(0.0),
);
let clamped = if e.is_nan() { 0.0 } else { e.clamp(0.0, top) };
let floor = clamped.floor();
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "the clamp bounds the value to 0.0..=size-1"
)]
let low = floor as u64;
let f = if size <= 1 { 0.0 } else { clamped - floor };
if let Some(slot) = lower.get_mut(i) {
*slot = low.min(u64::from(size.saturating_sub(1)));
}
if let Some(slot) = frac.get_mut(i) {
*slot = f;
}
}
let bps = u64::from(self.bits_per_sample);
let outputs = u64::try_from(self.outputs).unwrap_or(0);
let varying: Vec<usize> = (0..inputs)
.filter(|j| frac.get(*j).copied().unwrap_or(0.0) != 0.0)
.collect();
if varying.len() > 20 {
return false;
}
let corners = 1u32 << varying.len();
for i in 0..self.outputs {
let mut value = 0f32;
for corner in 0..corners {
let mut weight = 1f32;
let mut pos = 0u64;
for j in 0..inputs {
let size = self.sizes.get(j).copied().unwrap_or(1);
let low = lower.get(j).copied().unwrap_or(0);
let f = frac.get(j).copied().unwrap_or(0.0);
let upper = varying
.iter()
.position(|v| *v == j)
.is_some_and(|bit| corner >> bit & 1 == 1);
let (step, w) = if upper { (1u64, f) } else { (0u64, 1.0 - f) };
weight *= w;
let at = (low + step).min(u64::from(size.saturating_sub(1)));
pos = pos
.saturating_add(at.saturating_mul(blocksize.get(j).copied().unwrap_or(1)));
}
if weight == 0.0 {
continue;
}
let Some(bits) = pos
.checked_mul(outputs)
.and_then(|p| p.checked_add(u64::try_from(i).unwrap_or(0)))
.and_then(|p| p.checked_mul(bps))
else {
return false;
};
let mut reader = BitReader::new(&self.samples);
reader.seek_bits(bits);
value = weight.mul_add(sample_as_f32(reader.read(self.bits_per_sample)), value);
}
if let Some(slot) = out.get_mut(i) {
*slot = interpolate(
value,
0.0,
sample_as_f32(self.sample_max),
self.decode.get(i * 2).copied().unwrap_or(0.0),
self.decode.get(i * 2 + 1).copied().unwrap_or(0.0),
);
}
}
true
}
}
#[expect(
clippy::cast_precision_loss,
reason = "matching the C++'s uint32-to-float widening, including its loss"
)]
fn sample_as_f32(v: u32) -> f32 {
v as f32
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unreadable_literal,
clippy::float_cmp,
clippy::indexing_slicing,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "test fixtures quote oracle vectors verbatim and compare exactly"
)]
use super::{BitReader, Sampled};
use crate::function::Function;
fn ramp() -> Sampled {
Sampled {
domain: Box::from(&[0.0f32, 1.0][..]),
range: Box::from(&[0.0f32, 1.0][..]),
outputs: 1,
sizes: Box::from(&[4u32][..]),
bits_per_sample: 8,
encode: Box::from(&[0.0f32, 3.0][..]),
decode: Box::from(&[0.0f32, 1.0][..]),
samples: Box::from(&[0u8, 85, 170, 255][..]),
sample_max: 255,
}
}
#[test]
fn bit_reader_is_msb_first_and_reads_zero_past_the_end() {
let mut r = BitReader::new(&[0b1010_0000, 0xFF]);
assert_eq!(r.read(1), 1);
assert_eq!(r.read(1), 0);
assert_eq!(r.read(2), 0b10);
let mut r = BitReader::new(&[0x12, 0x34]);
assert_eq!(r.read(16), 0x1234);
assert_eq!(r.read(8), 0);
assert_eq!(r.remaining(), 0);
}
#[test]
fn byte_align_rounds_up() {
let mut r = BitReader::new(&[0xFF; 4]);
r.read(3);
r.byte_align();
assert_eq!(r.remaining(), 24);
r.byte_align();
assert_eq!(r.remaining(), 24);
}
#[test]
fn a_one_input_ramp_interpolates_exactly() {
let f = Function::Sampled(ramp());
let mut out = [0.0f32];
f.eval(&[0.0], &mut out).expect("evaluates");
assert!(out[0].abs() < 1e-6);
f.eval(&[1.0], &mut out).expect("evaluates");
assert!((out[0] - 1.0).abs() < 1e-6);
f.eval(&[1.5 / 3.0], &mut out).expect("evaluates");
assert!((out[0] - 0.5).abs() < 0.01, "got {}", out[0]);
}
#[test]
fn a_two_input_function_blends_every_corner_not_a_tangent_plane() {
let sampled = Sampled {
domain: Box::from(&[0.0f32, 1.0, 0.0, 1.0][..]),
range: Box::from(&[0.0f32, 1.0][..]),
outputs: 1,
sizes: Box::from(&[2u32, 2][..]),
bits_per_sample: 8,
encode: Box::from(&[0.0f32, 1.0, 0.0, 1.0][..]),
decode: Box::from(&[0.0f32, 1.0][..]),
samples: Box::from(&[0u8, 0, 0, 255][..]),
sample_max: 255,
};
let f = Function::Sampled(sampled);
let mut out = [0.0f32];
f.eval(&[0.0, 0.0], &mut out).expect("evaluates");
assert!(out[0].abs() < 1e-6, "got {}", out[0]);
f.eval(&[1.0, 1.0], &mut out).expect("evaluates");
assert!((out[0] - 1.0).abs() < 1e-6, "got {}", out[0]);
f.eval(&[0.5, 0.5], &mut out).expect("evaluates");
assert!((out[0] - 0.25).abs() < 1e-6, "got {}", out[0]);
f.eval(&[1.0, 0.5], &mut out).expect("evaluates");
assert!((out[0] - 0.5).abs() < 1e-6, "got {}", out[0]);
}
#[test]
fn a_negative_encoded_input_clamps_to_the_bottom_cell() {
let mut sampled = ramp();
sampled.encode = Box::from(&[-5.0f32, -1.0][..]);
let f = Function::Sampled(sampled);
let mut out = [0.0f32];
f.eval(&[0.0], &mut out).expect("evaluates");
assert!(out[0].abs() < 1e-6, "got {}", out[0]);
}
#[test]
fn a_single_sample_axis_weighs_one_rather_than_multiplying() {
let sampled = Sampled {
domain: Box::from(&[0.0f32, 1.0][..]),
range: Box::from(&[0.0f32, 1.0][..]),
outputs: 1,
sizes: Box::from(&[1u32][..]),
bits_per_sample: 8,
encode: Box::from(&[0.0f32, 1.0][..]),
decode: Box::from(&[0.0f32, 1.0][..]),
samples: Box::from(&[255u8][..]),
sample_max: 255,
};
let f = Function::Sampled(sampled);
let mut out = [0.0f32];
f.eval(&[1.0], &mut out).expect("evaluates");
assert!((out[0] - 1.0).abs() < 1e-6, "got {}", out[0]);
f.eval(&[0.0], &mut out).expect("evaluates");
assert!((out[0] - 1.0).abs() < 1e-6, "got {}", out[0]);
}
}