raw: unpack 10/12/14-bit rows by groups, keep a single strip's buffer

Bit-packed uncompressed raw data was read one sample at a time through
a bit accumulator. Rows of 10, 12 and 14-bit samples are now read a
whole group (4x10, 2x12, 4x14 bits = 5, 3, 7 bytes) at a time; the
row tail still goes through the general reader. A file whose only
segment is the whole image row for row no longer copies it into a
second full-size buffer: the decoded Vec becomes the plane. The
predictor 2 undo works in place instead of on a copy.

The summed-segment charge (#4959) and the per-segment limits are
unchanged. A test compares the grouped reader with the plain bit
reader for 10/12/14 bits, 1 and 3 samples and several widths.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76
2026-10-25 14:00:24 +01:00
co-authored by Claude Sonnet 5.5
parent b0622d20ca
commit e15d68bfbb
+105 -4
View File
@@ -286,7 +286,7 @@ pub(crate) fn read_plane(t: &Tiff, ifd: &Ifd, limits: &Limits, layout: JpegLayou
// The TIFF predictor runs after unpacking, per row, per sample plane.
match t.tag_uint(ifd, tag::PREDICTOR).unwrap_or(1) {
1 => Ok(v),
2 => Ok(undelta(&v, samples, s.w * samples, rows, bits)),
2 => Ok(undelta(v, samples, s.w * samples, rows, bits)),
3 => Err(RawError::unsupported("floating-point predictor (compression 8, predictor 3)")),
p => Err(RawError::malformed(format!("unknown predictor {p}"))),
}
@@ -303,7 +303,21 @@ pub(crate) fn read_plane(t: &Tiff, ifd: &Ifd, limits: &Limits, layout: JpegLayou
_ => lossless_jpeg(src, s, rows, samples, layout, limits),
}
});
let mut data = vec![0u16; width * height * samples];
// One segment holding the whole image row for row (a single strip): its buffer is the plane, no
// second full-size buffer and no copy. The scatter below then has no segment left to place.
let whole = matches!(segments.as_slice(), [s] if s.x == 0 && s.y == 0 && s.w == width && s.h >= height);
let mut decoded = decoded.into_iter();
let taken = if whole {
let mut d = decoded.next().ok_or_else(|| RawError::malformed("raw segment missing"))??;
if d.len() < width * height * samples {
return Err(RawError::malformed("raw segment is smaller than the image"));
}
d.truncate(width * height * samples);
Some(d)
} else {
None
};
let mut data = taken.unwrap_or_else(|| vec![0u16; width * height * samples]);
for (s, d) in segments.iter().zip(decoded) {
let d = d?;
let rows = s.h.min(height - s.y);
@@ -464,9 +478,8 @@ fn lossy_jpeg(src: &[u8], s: &Segment, rows: usize, samples: usize, bits: u32, t
/// Reverses TIFF predictor 2 (horizontal differencing) on interleaved samples: each sample
/// gains its channel's predecessor in the row, wrapping at the sample's container (8 or 16 bits).
fn undelta(v: &[u16], plane: usize, row_samples: usize, rows: usize, bits: u32) -> Vec<u16> {
fn undelta(mut out: Vec<u16>, plane: usize, row_samples: usize, rows: usize, bits: u32) -> Vec<u16> {
let mask: u16 = if bits <= 8 { 0xFF } else { 0xFFFF };
let mut out = v.to_vec();
for row in out.chunks_mut(row_samples.max(1)).take(rows) {
for i in plane..row.len() {
if let (Some(&prev), Some(&cur)) = (row.get(i - plane), row.get(i)) {
@@ -495,6 +508,18 @@ fn unpack(src: &[u8], w: usize, rows: usize, samples: usize, bits: u32, le: bool
if src.len() < row_bytes * rows {
return Err(truncated());
}
// The common packings, a whole group of samples per fixed run of bytes (MSB first).
let per_row = w * samples;
match bits {
12 | 14 | 10 => {
let mut out = vec![0u16; n];
for (orow, row) in out.chunks_exact_mut(per_row.max(1)).zip(src.chunks_exact(row_bytes.max(1))) {
unpack_row_msb(row, orow, bits);
}
return Ok(out);
}
_ => {}
}
let mut out = Vec::with_capacity(n);
for r in 0..rows {
let row = &src[r * row_bytes..(r + 1) * row_bytes];
@@ -513,10 +538,86 @@ fn unpack(src: &[u8], w: usize, rows: usize, samples: usize, bits: u32, le: bool
Ok(out)
}
/// One MSB-first bit-packed row of 10, 12 or 14-bit samples (`row` holds at least
/// `out.len() * bits / 8` bytes, rounded up; missing bytes read as 0, as [`unpack`] does).
fn unpack_row_msb(row: &[u8], out: &mut [u16], bits: u32) {
let mask = (1u64 << bits) - 1;
// Samples per group and bytes per group: 4 × 10 = 40, 2 × 12 = 24, 4 × 14 = 56 bits.
let (g, gb) = match bits {
10 => (4, 5),
12 => (2, 3),
_ => (4, 7),
};
let groups = out.len() / g;
for (o, b) in out[..groups * g].chunks_exact_mut(g).zip(row.chunks_exact(gb)) {
let mut acc = 0u64;
for &x in b {
acc = (acc << 8) | u64::from(x);
}
for (k, v) in o.iter_mut().enumerate() {
*v = ((acc >> ((g - 1 - k) as u32 * bits)) & mask) as u16;
}
}
// The tail (fewer than a group): the general reader.
let mut acc: u32 = 0;
let mut have = 0u32;
let mut it = row.iter().skip(groups * gb);
for v in &mut out[groups * g..] {
while have < bits {
acc = (acc << 8) | u32::from(*it.next().unwrap_or(&0));
have += 8;
}
have -= bits;
*v = ((acc >> have) & ((1 << bits) - 1)) as u16;
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The grouped 10/12/14-bit readers equal the plain bit reader on every row width.
#[test]
fn grouped_unpack_matches_the_bit_reader() {
let plain = |src: &[u8], w: usize, rows: usize, bits: u32| {
let row_bytes = (w * bits as usize).div_ceil(8);
let mut out = Vec::new();
for r in 0..rows {
let row = &src[r * row_bytes..(r + 1) * row_bytes];
let (mut acc, mut have, mut it) = (0u32, 0u32, row.iter());
for _ in 0..w {
while have < bits {
acc = (acc << 8) | u32::from(*it.next().unwrap_or(&0));
have += 8;
}
have -= bits;
out.push(((acc >> have) & ((1 << bits) - 1)) as u16);
}
}
out
};
let mut s = 7u64;
for bits in [10u32, 12, 14] {
for samples in [1usize, 3] {
// Widths whose packed rows are shorter than 16-bit containers (else `unpack` reads containers).
for w in [4usize, 5, 7, 8, 13, 64, 67] {
let rows = 3;
let src: Vec<u8> = (0..(w * samples * bits as usize).div_ceil(8) * rows)
.map(|_| {
s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
(s >> 40) as u8
})
.collect();
assert_eq!(
unpack(&src, w, rows, samples, bits, true).unwrap(),
plain(&src, w * samples, rows, bits),
"{bits} bits, {samples} samples, {w} wide"
);
}
}
}
}
/// #4959: a decoded segment is retained until the scatter, so the segments are charged
/// together. Each of these is far inside the budget on its own; their sum is not.
#[test]