mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-12 21:23:02 +00:00
Affinity reader: convert raster tiles with tables instead of a per-pixel Vec<f64>
Each pixel of a decoded layer was widened to a freshly allocated Vec<f64>, divided, clamped and rounded through libm. The channels are now interleaved directly: 8-bit RGBA and grey are plain copies (x / 255 * 255 rounds back to x), 16-bit goes through a 64 Ki table and CMYK through a table over (c, k), both built from the same f64 expressions, so the bytes are identical. Tile copies into a plane use one slice copy or fill per row instead of a closure per byte. The scratch charged to the pixel budget is unchanged. Tests keep the old conversion and compare it exhaustively (every byte, every 16-bit value, every CMYK colour/black pair) and the old byte copy against the row copy for many crops. Part of #9412 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b0622d20ca
commit
ace68fbb14
2 files changed
+190
-29
No files matched your search
@@ -127,31 +127,74 @@ fn decode(r: &mut Reader, bitmap: ObjId, crop: (u32, u32, u32, u32), world: Affi
|
||||
for c in 1..=channels {
|
||||
planes.push(plane(r, bitmap, c, bps, (x0, y0, w, h), &mut cache)?);
|
||||
}
|
||||
let n = (w as usize) * (h as usize);
|
||||
let mut rgba = vec![0u8; n * 4];
|
||||
let sample = |p: &Vec<u8>, i: usize| -> f64 {
|
||||
if bps == 2 {
|
||||
f64::from(u16::from_le_bytes([p.get(2 * i).copied().unwrap_or(0), p.get(2 * i + 1).copied().unwrap_or(0)])) / 65535.0
|
||||
} else {
|
||||
f64::from(p.get(i).copied().unwrap_or(0)) / 255.0
|
||||
}
|
||||
};
|
||||
for (i, px) in rgba.as_chunks_mut::<4>().0.iter_mut().enumerate() {
|
||||
let v: Vec<f64> = planes.iter().map(|p| sample(p, i)).collect();
|
||||
let (r2, g, b, a) = if channels == 1 {
|
||||
(v[0], v[0], v[0], 1.0)
|
||||
} else if channels == 5 {
|
||||
let k = 1.0 - v[3];
|
||||
((1.0 - v[0]) * k, (1.0 - v[1]) * k, (1.0 - v[2]) * k, v[4])
|
||||
} else {
|
||||
(v[0], v[1], v[2], v[3])
|
||||
};
|
||||
px.copy_from_slice(&[r2, g, b, a].map(|c| (c.clamp(0.0, 1.0) * 255.0).round() as u8));
|
||||
}
|
||||
let rgba = interleave(&planes, channels, bps, pixels);
|
||||
let transform = Affine([1.0, 0.0, 0.0, 1.0, f64::from(x0), f64::from(y0)]).then(world);
|
||||
Ok(Some(Image { width: w, height: h, pixels: Pixels::Rgba8(rgba), transform }))
|
||||
}
|
||||
|
||||
/// Quantize a unit-range sample to a byte: the one conversion every format goes through.
|
||||
fn quant(v: f64) -> u8 {
|
||||
(v.clamp(0.0, 1.0) * 255.0).round() as u8
|
||||
}
|
||||
|
||||
/// Interleave the decoded planes (`n` samples of `bps` bytes each) into RGBA8.
|
||||
///
|
||||
/// The samples used to be widened to `f64` one pixel at a time. The tables below hold the same
|
||||
/// expressions evaluated once per input value, so the bytes are identical: an 8-bit sample
|
||||
/// `x / 255 * 255` rounds back to `x`, a 16-bit one goes through a 64 Ki table, and CMYK's
|
||||
/// `(1 - c)(1 - k)` through a table over the 256 x 256 `(c, k)` pairs.
|
||||
pub(crate) fn interleave(planes: &[Vec<u8>], channels: usize, bps: usize, n: usize) -> Vec<u8> {
|
||||
let mut rgba = vec![0u8; n * 4];
|
||||
let out = rgba.as_chunks_mut::<4>().0.iter_mut();
|
||||
let p = |c: usize| planes.get(c).map_or(&[][..], Vec::as_slice);
|
||||
match (channels, bps) {
|
||||
(1, 1) => {
|
||||
for (o, &v) in out.zip(p(0)) {
|
||||
*o = [v, v, v, 255];
|
||||
}
|
||||
}
|
||||
(4, 1) => {
|
||||
let (r, g, b, a) = (p(0), p(1), p(2), p(3));
|
||||
for ((((o, &r), &g), &b), &a) in out.zip(r).zip(g).zip(b).zip(a) {
|
||||
*o = [r, g, b, a];
|
||||
}
|
||||
}
|
||||
(5, 1) => {
|
||||
let lut = cmyk_table();
|
||||
let (c, m, y, k, a) = (p(0), p(1), p(2), p(3), p(4));
|
||||
for (((((o, &c), &m), &y), &k), &a) in out.zip(c).zip(m).zip(y).zip(k).zip(a) {
|
||||
let k = usize::from(k) << 8;
|
||||
*o = [lut[k | usize::from(c)], lut[k | usize::from(m)], lut[k | usize::from(y)], a];
|
||||
}
|
||||
}
|
||||
(_, _) => {
|
||||
let lut: Vec<u8> = (0..=u16::MAX).map(|v| quant(f64::from(v) / 65535.0)).collect();
|
||||
let s16 = |c: usize| p(c).as_chunks::<2>().0.iter().map(|b| lut[usize::from(u16::from_le_bytes(*b))]);
|
||||
if channels == 1 {
|
||||
for (o, g) in out.zip(s16(0)) {
|
||||
*o = [g, g, g, 255];
|
||||
}
|
||||
} else {
|
||||
for ((((o, r), g), b), a) in out.zip(s16(0)).zip(s16(1)).zip(s16(2)).zip(s16(3)) {
|
||||
*o = [r, g, b, a];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rgba
|
||||
}
|
||||
|
||||
/// `(1 - c)(1 - k)` quantized, indexed by `k << 8 | c`.
|
||||
fn cmyk_table() -> Vec<u8> {
|
||||
let mut lut = Vec::with_capacity(256 * 256);
|
||||
for k in 0..=255u8 {
|
||||
for c in 0..=255u8 {
|
||||
lut.push(quant((1.0 - f64::from(c) / 255.0) * (1.0 - f64::from(k) / 255.0)));
|
||||
}
|
||||
}
|
||||
lut
|
||||
}
|
||||
|
||||
/// The original image file: entry `c/N` is a stream whose root (`Blck`) holds it as `Data`.
|
||||
fn original(r: &mut Reader, name: &str) -> Result<Vec<u8>, Error> {
|
||||
let bytes = r.archive.read(name)?;
|
||||
@@ -224,7 +267,7 @@ fn plane(
|
||||
cache.insert(name.clone(), data);
|
||||
}
|
||||
let tile = cache.get(&name).ok_or(Error::Malformed("pixel tile"))?;
|
||||
copy(&mut out, row, (bx0, py0, bx1, py1), (ox, oy), |x, y| tile.get(y * TILE + x).copied().unwrap_or(0));
|
||||
copy(&mut out, row, (bx0, py0, bx1, py1), (ox, oy), Some(tile), 0);
|
||||
continue;
|
||||
}
|
||||
3 => {
|
||||
@@ -238,19 +281,25 @@ fn plane(
|
||||
_ => return Err(Error::Malformed("unknown pixel tile state")),
|
||||
};
|
||||
if let Some(v) = fill {
|
||||
copy(&mut out, row, (bx0, py0, bx1, py1), (tx * TILE, ty * TILE), |_, _| v);
|
||||
copy(&mut out, row, (bx0, py0, bx1, py1), (tx * TILE, ty * TILE), None, v);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Copy the overlap of a tile at byte column `ox`, row `oy` into the cropped plane.
|
||||
fn copy(out: &mut [u8], row: usize, (bx0, py0, bx1, py1): (usize, usize, usize, usize), (ox, oy): (usize, usize), at: impl Fn(usize, usize) -> u8) {
|
||||
/// Copy the overlap of a tile at byte column `ox`, row `oy` into the cropped plane. `tile` is
|
||||
/// the tile's bytes, or `None` to fill the overlap with `fill`.
|
||||
pub(crate) fn copy(out: &mut [u8], row: usize, (bx0, py0, bx1, py1): (usize, usize, usize, usize), (ox, oy): (usize, usize), tile: Option<&[u8]>, fill: u8) {
|
||||
let (xa, xb) = (ox.max(bx0), (ox + TILE).min(bx1));
|
||||
if xa >= xb {
|
||||
return;
|
||||
}
|
||||
for y in oy.max(py0)..(oy + TILE).min(py1) {
|
||||
for x in ox.max(bx0)..(ox + TILE).min(bx1) {
|
||||
if let Some(b) = out.get_mut((y - py0) * row + (x - bx0)) {
|
||||
*b = at(x - ox, y - oy);
|
||||
}
|
||||
let start = (y - py0) * row + (xa - bx0);
|
||||
let Some(dst) = out.get_mut(start..start + (xb - xa)) else { continue };
|
||||
match tile.and_then(|t| t.get((y - oy) * TILE + (xa - ox)..(y - oy) * TILE + (xb - ox))) {
|
||||
Some(src) => dst.copy_from_slice(src),
|
||||
None => dst.fill(if tile.is_some() { 0 } else { fill }),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -734,3 +734,115 @@ mod spot_colours {
|
||||
assert!(warned(&doc, "spot colours nested deeper than 4 levels"), "{:?}", doc.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
/// The raster fast paths against the code they replaced.
|
||||
mod raster_identity {
|
||||
use crate::raster::{copy, interleave};
|
||||
|
||||
/// Deterministic filler (xorshift).
|
||||
fn bytes(n: usize, mut x: u64) -> Vec<u8> {
|
||||
(0..n)
|
||||
.map(|_| {
|
||||
x ^= x << 13;
|
||||
x ^= x >> 7;
|
||||
x ^= x << 17;
|
||||
(x >> 24) as u8
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The per-pixel `f64` conversion `decode` used before the tables.
|
||||
fn reference(planes: &[Vec<u8>], channels: usize, bps: usize, n: usize) -> Vec<u8> {
|
||||
let sample = |p: &Vec<u8>, i: usize| -> f64 {
|
||||
if bps == 2 {
|
||||
f64::from(u16::from_le_bytes([p.get(2 * i).copied().unwrap_or(0), p.get(2 * i + 1).copied().unwrap_or(0)])) / 65535.0
|
||||
} else {
|
||||
f64::from(p.get(i).copied().unwrap_or(0)) / 255.0
|
||||
}
|
||||
};
|
||||
let mut rgba = vec![0u8; n * 4];
|
||||
for (i, px) in rgba.as_chunks_mut::<4>().0.iter_mut().enumerate() {
|
||||
let v: Vec<f64> = planes.iter().map(|p| sample(p, i)).collect();
|
||||
let (r, g, b, a) = if channels == 1 {
|
||||
(v[0], v[0], v[0], 1.0)
|
||||
} else if channels == 5 {
|
||||
let k = 1.0 - v[3];
|
||||
((1.0 - v[0]) * k, (1.0 - v[1]) * k, (1.0 - v[2]) * k, v[4])
|
||||
} else {
|
||||
(v[0], v[1], v[2], v[3])
|
||||
};
|
||||
px.copy_from_slice(&[r, g, b, a].map(|c| (c.clamp(0.0, 1.0) * 255.0).round() as u8));
|
||||
}
|
||||
rgba
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eight_bit_samples_round_trip() {
|
||||
// Every byte through every position of the 4-channel and the grey path.
|
||||
let ramp: Vec<u8> = (0..=255).collect();
|
||||
let planes = vec![ramp.clone(), ramp.iter().rev().copied().collect(), ramp.clone(), ramp.iter().rev().copied().collect()];
|
||||
assert_eq!(interleave(&planes, 4, 1, 256), reference(&planes, 4, 1, 256));
|
||||
assert_eq!(interleave(&planes[..1], 1, 1, 256), reference(&planes[..1], 1, 1, 256));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sixteen_bit_samples_are_exhaustive() {
|
||||
// All 65536 values in each channel position (rotated so the channels differ).
|
||||
let all: Vec<u8> = (0..=u16::MAX).flat_map(u16::to_le_bytes).collect();
|
||||
let rot = |k: usize| -> Vec<u8> { (0..=u16::MAX).flat_map(|v| v.wrapping_add((k * 4099) as u16).to_le_bytes()).collect() };
|
||||
let planes = vec![all, rot(1), rot(2), rot(3)];
|
||||
assert_eq!(interleave(&planes, 4, 2, 65536), reference(&planes, 4, 2, 65536));
|
||||
assert_eq!(interleave(&planes[..1], 1, 2, 65536), reference(&planes[..1], 1, 2, 65536));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmyk_is_exhaustive_over_colour_and_black() {
|
||||
// Every (c, k) pair in each of the c, m and y positions; alpha is arbitrary.
|
||||
let n = 256 * 256;
|
||||
let k: Vec<u8> = (0..n).map(|i| (i >> 8) as u8).collect();
|
||||
let c: Vec<u8> = (0..n).map(|i| i as u8).collect();
|
||||
let m: Vec<u8> = (0..n).map(|i| (i as u8).wrapping_mul(7).wrapping_add(3)).collect();
|
||||
let y: Vec<u8> = (0..n).map(|i| (i as u8).wrapping_mul(13).wrapping_add(250)).collect();
|
||||
let planes = vec![c, m, y, k, bytes(n, 9)];
|
||||
assert_eq!(interleave(&planes, 5, 1, n), reference(&planes, 5, 1, n));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_planes_match_for_every_format() {
|
||||
for (channels, bps) in [(1, 1), (1, 2), (4, 1), (4, 2), (5, 1)] {
|
||||
let n = 5000;
|
||||
let planes: Vec<Vec<u8>> = (0..channels).map(|c| bytes(n * bps, 1 + c as u64)).collect();
|
||||
assert_eq!(interleave(&planes, channels, bps, n), reference(&planes, channels, bps, n), "{channels}x{bps}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The per-byte closure copy `plane` used before the row copies.
|
||||
fn copy_reference(out: &mut [u8], row: usize, (bx0, py0, bx1, py1): (usize, usize, usize, usize), (ox, oy): (usize, usize), at: impl Fn(usize, usize) -> u8) {
|
||||
for y in oy.max(py0)..(oy + 256).min(py1) {
|
||||
for x in ox.max(bx0)..(ox + 256).min(bx1) {
|
||||
if let Some(b) = out.get_mut((y - py0) * row + (x - bx0)) {
|
||||
*b = at(x - ox, y - oy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tile_copy_matches_the_byte_loop_for_every_crop() {
|
||||
let tile = bytes(256 * 256, 77);
|
||||
// Crops that cut the tile on each side, sit inside it, and miss it.
|
||||
for &(bx0, bx1, py0, py1) in &[(0, 600, 0, 600), (10, 300, 20, 200), (100, 150, 100, 130), (256, 512, 0, 256), (0, 256, 300, 400), (255, 257, 255, 257), (300, 301, 0, 1)] {
|
||||
for &(ox, oy) in &[(0, 0), (256, 0), (0, 256), (256, 256), (512, 256)] {
|
||||
let row = bx1 - bx0;
|
||||
let mut old = vec![0xAAu8; row * (py1 - py0)];
|
||||
let mut new = old.clone();
|
||||
copy_reference(&mut old, row, (bx0, py0, bx1, py1), (ox, oy), |x, y| tile[y * 256 + x]);
|
||||
copy(&mut new, row, (bx0, py0, bx1, py1), (ox, oy), Some(&tile), 0);
|
||||
assert_eq!(old, new, "tile {ox},{oy} crop {bx0}..{bx1} x {py0}..{py1}");
|
||||
copy_reference(&mut old, row, (bx0, py0, bx1, py1), (ox, oy), |_, _| 0xFF);
|
||||
copy(&mut new, row, (bx0, py0, bx1, py1), (ox, oy), None, 0xFF);
|
||||
assert_eq!(old, new, "fill {ox},{oy} crop {bx0}..{bx1} x {py0}..{py1}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user