mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge pull request #1137 from vangel76/psd-optimise
# Conflicts: # crates/psd/src/lib.rs
This commit is contained in:
commit
ac9d4eb72f
3 files changed
+791
-50
No files matched your search
+112
-39
@@ -15,6 +15,7 @@ pub mod placed;
|
||||
mod read;
|
||||
pub mod write;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use descriptor::{DValue, Descriptor};
|
||||
@@ -835,14 +836,26 @@ impl Psd {
|
||||
let mut r = Reader::at(&self.data[..end], offset);
|
||||
let comp = r.u16()?;
|
||||
decode_pixels(w, h)?;
|
||||
let mut planes = self.open_planes(comp, &mut r, 1, 1, w, h)?;
|
||||
if let PlaneStream::Zip(v) = &mut planes {
|
||||
v.resize(self.row_bytes(w) * h, 0);
|
||||
}
|
||||
let raw = self.decode_plane(comp, &planes, &mut r, 0, w, h)?;
|
||||
let raw = self.plane_bytes(comp, &mut r, w, h)?;
|
||||
Ok(self.samples(&raw, w, h))
|
||||
}
|
||||
|
||||
/// The raw bytes (`row_bytes(w)` × `h`, ZIP prediction undone) of one channel whose pixel data
|
||||
/// follows `r`'s position (after the compression field). Uncompressed data is borrowed, not
|
||||
/// copied.
|
||||
fn plane_bytes<'a>(&self, comp: u16, r: &mut Reader<'a>, w: usize, h: usize) -> Result<Cow<'a, [u8]>> {
|
||||
let rowb = self.row_bytes(w);
|
||||
Ok(match self.open_planes(comp, r, 1, 1, w, h)? {
|
||||
PlaneStream::Raw => Cow::Borrowed(r.bytes(rowb * h)?),
|
||||
PlaneStream::Rle(counts) => Cow::Owned(Self::unpack_plane(&counts, 0, r, rowb, h)?),
|
||||
PlaneStream::Zip(mut v) => {
|
||||
v.resize(rowb * h, 0);
|
||||
self.zip_plane(comp, &mut v, 0, w, h)?;
|
||||
Cow::Owned(v)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Prepare the pixel data at `r` (just after the compression field) of `total` planes of
|
||||
/// `w`×`h` samples, of which the first `want` are decoded: the RLE row sizes of every plane,
|
||||
/// or the inflated ZIP data, which holds all planes one after another.
|
||||
@@ -867,30 +880,38 @@ impl Psd {
|
||||
})
|
||||
}
|
||||
|
||||
/// The raw bytes (`row_bytes(w)` × `h`, ZIP prediction undone) of plane `c`; raw and RLE
|
||||
/// planes are read from `r` in order.
|
||||
fn decode_plane(&self, comp: u16, planes: &PlaneStream, r: &mut Reader, c: usize, w: usize, h: usize) -> Result<Vec<u8>> {
|
||||
/// The raw bytes (`row_bytes(w)` × `h`, ZIP prediction undone) of plane `c`: raw and RLE
|
||||
/// planes are read from `r` in order, raw ones borrowed from the file data, ZIP ones from the
|
||||
/// inflated buffer, un-predicted in place.
|
||||
fn decode_plane<'x, 'a: 'x>(&self, comp: u16, planes: &'x mut PlaneStream, r: &mut Reader<'a>, c: usize, w: usize, h: usize) -> Result<Cow<'x, [u8]>> {
|
||||
let rowb = self.row_bytes(w);
|
||||
Ok(match planes {
|
||||
PlaneStream::Raw => r.bytes(rowb * h)?.to_vec(),
|
||||
PlaneStream::Rle(counts) => {
|
||||
let mut out = Vec::with_capacity(rowb * h);
|
||||
for row in 0..h {
|
||||
let n = *counts.get(c * h + row).ok_or(Error::Truncated)?;
|
||||
unpack_bits(r.bytes(n)?, rowb, &mut out)?;
|
||||
}
|
||||
out
|
||||
}
|
||||
PlaneStream::Zip(v) => {
|
||||
let mut plane = v.get(c * rowb * h..(c + 1) * rowb * h).ok_or(Error::Truncated)?.to_vec();
|
||||
if comp == 3 {
|
||||
self.unpredict(&mut plane, w, h);
|
||||
}
|
||||
plane
|
||||
}
|
||||
PlaneStream::Raw => Cow::Borrowed(r.bytes(rowb * h)?),
|
||||
PlaneStream::Rle(counts) => Cow::Owned(Self::unpack_plane(counts, c, r, rowb, h)?),
|
||||
PlaneStream::Zip(v) => Cow::Borrowed(self.zip_plane(comp, v, c, w, h)?),
|
||||
})
|
||||
}
|
||||
|
||||
/// Unpack the `h` RLE rows of plane `c` (row byte counts `counts`, plane after plane).
|
||||
fn unpack_plane(counts: &[usize], c: usize, r: &mut Reader, rowb: usize, h: usize) -> Result<Vec<u8>> {
|
||||
let mut out = Vec::with_capacity(rowb * h);
|
||||
for row in 0..h {
|
||||
let n = *counts.get(c * h + row).ok_or(Error::Truncated)?;
|
||||
unpack_bits(r.bytes(n)?, rowb, &mut out)?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Plane `c` of the inflated ZIP data of consecutive planes, un-predicted in place (`comp` 3).
|
||||
fn zip_plane<'v>(&self, comp: u16, v: &'v mut [u8], c: usize, w: usize, h: usize) -> Result<&'v [u8]> {
|
||||
let rowb = self.row_bytes(w);
|
||||
let plane = v.get_mut(c * rowb * h..(c + 1) * rowb * h).ok_or(Error::Truncated)?;
|
||||
if comp == 3 {
|
||||
self.unpredict(plane, w, h);
|
||||
}
|
||||
Ok(plane)
|
||||
}
|
||||
|
||||
fn row_bytes(&self, w: usize) -> usize {
|
||||
if self.depth == 1 { w.div_ceil(8) } else { w * self.bytes_per_sample() }
|
||||
}
|
||||
@@ -908,12 +929,16 @@ impl Psd {
|
||||
}
|
||||
}
|
||||
32 => {
|
||||
// One scratch row for the whole plane.
|
||||
let mut planar = vec![0u8; w * 4];
|
||||
#[cfg(test)]
|
||||
counters::bump(&counters::SCRATCH);
|
||||
for row in v.chunks_exact_mut(w * 4).take(h) {
|
||||
for i in 1..row.len() {
|
||||
row[i] = row[i].wrapping_add(row[i - 1]);
|
||||
}
|
||||
// Bytes are stored plane by plane (all first bytes, then all second bytes…).
|
||||
let planar = row.to_vec();
|
||||
planar.copy_from_slice(row);
|
||||
for x in 0..w {
|
||||
for b in 0..4 {
|
||||
row[x * 4 + b] = planar[b * w + x];
|
||||
@@ -999,9 +1024,9 @@ impl Psd {
|
||||
let cc = self.mode.color_channels();
|
||||
let want = (cc + usize::from(self.merged_alpha)).min(nch);
|
||||
let mut planes: Vec<Option<Vec<f32>>> = vec![None; nch];
|
||||
let stream = self.open_planes(comp, &mut r, nch, want, w, h)?;
|
||||
let mut stream = self.open_planes(comp, &mut r, nch, want, w, h)?;
|
||||
for (c, p) in planes.iter_mut().enumerate().take(want) {
|
||||
*p = Some(self.samples(&self.decode_plane(comp, &stream, &mut r, c, w, h)?, w, h));
|
||||
*p = Some(self.samples(&self.decode_plane(comp, &mut stream, &mut r, c, w, h)?, w, h));
|
||||
}
|
||||
let alpha = if self.merged_alpha && nch > cc { planes[cc].take() } else { None };
|
||||
let mut data = Vec::with_capacity(w * h);
|
||||
@@ -1077,19 +1102,41 @@ impl Psd {
|
||||
_ => None,
|
||||
};
|
||||
let def = m.default_color as f32 / 255.0;
|
||||
for y in 0..oh {
|
||||
let py = out_rect.top + y as i32;
|
||||
for x in 0..ow {
|
||||
let px = out_rect.left + x as i32;
|
||||
let inside = py >= m.rect.top && py < m.rect.bottom && px >= m.rect.left && px < m.rect.right;
|
||||
let mut v = match (&mask, inside) {
|
||||
(Some(mk), true) => mk[(py - m.rect.top) as usize * mw + (px - m.rect.left) as usize],
|
||||
_ => def,
|
||||
};
|
||||
if m.inverted {
|
||||
v = 1.0 - v;
|
||||
// The factor outside the mask rect, or everywhere when there is no mask data.
|
||||
let outside = if m.inverted { 1.0 - def } else { def };
|
||||
let scale = |px: &mut [[f32; 4]], v: f32| {
|
||||
if v != 1.0 {
|
||||
for p in px {
|
||||
p[3] *= v;
|
||||
}
|
||||
}
|
||||
};
|
||||
match &mask {
|
||||
None => scale(&mut out, outside),
|
||||
Some(mk) => {
|
||||
// The columns of the output that fall inside the mask rect (the same for every row).
|
||||
let lo = (i64::from(m.rect.left) - i64::from(out_rect.left)).clamp(0, ow as i64);
|
||||
let hi = (i64::from(m.rect.right) - i64::from(out_rect.left)).clamp(lo, ow as i64);
|
||||
let (lo, hi) = (lo as usize, hi as usize);
|
||||
for (y, row) in out.chunks_exact_mut(ow.max(1)).take(oh).enumerate() {
|
||||
let py = i64::from(out_rect.top) + y as i64;
|
||||
#[cfg(test)]
|
||||
counters::bump(&counters::INSIDE);
|
||||
if py < i64::from(m.rect.top) || py >= i64::from(m.rect.bottom) {
|
||||
scale(row, outside);
|
||||
continue;
|
||||
}
|
||||
let (left, rest) = row.split_at_mut(lo);
|
||||
let (mid, right) = rest.split_at_mut(hi - lo);
|
||||
scale(left, outside);
|
||||
scale(right, outside);
|
||||
// The mask row under `mid`: its first column is output column `lo`.
|
||||
let base = (py - i64::from(m.rect.top)) as usize * mw + (i64::from(out_rect.left) + lo as i64 - i64::from(m.rect.left)) as usize;
|
||||
for (i, p) in mid.iter_mut().enumerate() {
|
||||
let v = mk.get(base + i).copied().unwrap_or(def);
|
||||
p[3] *= if m.inverted { 1.0 - v } else { v };
|
||||
}
|
||||
}
|
||||
out[y * ow + x][3] *= v;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1216,3 +1263,29 @@ fn lab_to_rgb(l: f32, a: f32, b: f32) -> [f32; 3] {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod tests_opt;
|
||||
|
||||
/// Test-only call counters (per thread), to prove what the optimised paths avoid doing.
|
||||
#[cfg(test)]
|
||||
pub(crate) mod counters {
|
||||
use std::cell::Cell;
|
||||
use std::thread::LocalKey;
|
||||
|
||||
thread_local! {
|
||||
/// Scratch rows allocated by the 32-bit ZIP un-predictor.
|
||||
pub static SCRATCH: Cell<usize> = const { Cell::new(0) };
|
||||
/// `inside the mask rect` evaluations of the layer-mask loop.
|
||||
pub static INSIDE: Cell<usize> = const { Cell::new(0) };
|
||||
/// `square_to_quad` solves.
|
||||
pub static QUAD: Cell<usize> = const { Cell::new(0) };
|
||||
}
|
||||
|
||||
pub fn bump(c: &'static LocalKey<Cell<usize>>) {
|
||||
c.with(|c| c.set(c.get() + 1));
|
||||
}
|
||||
/// Read and reset.
|
||||
pub fn take(c: &'static LocalKey<Cell<usize>>) -> usize {
|
||||
c.with(|c| c.replace(0))
|
||||
}
|
||||
}
|
||||
+30
-11
@@ -206,7 +206,9 @@ fn style_map(style: &str, b: f64, x: f64, y: f64, fw: f64, fh: f64) -> [f64; 2]
|
||||
|
||||
/// The projective map taking the unit square's corners (0,0), (1,0), (1,1), (0,1) to `q`
|
||||
/// (Heckbert's square-to-quad), as a 3×3 matrix applied to (u, v, 1).
|
||||
fn square_to_quad(q: [[f64; 2]; 4]) -> [[f64; 3]; 3] {
|
||||
pub(crate) fn square_to_quad(q: [[f64; 2]; 4]) -> [[f64; 3]; 3] {
|
||||
#[cfg(test)]
|
||||
crate::counters::bump(&crate::counters::QUAD);
|
||||
let [p0, p1, p2, p3] = q;
|
||||
let sx = p0[0] - p1[0] + p2[0] - p3[0];
|
||||
let sy = p0[1] - p1[1] + p2[1] - p3[1];
|
||||
@@ -221,6 +223,11 @@ fn square_to_quad(q: [[f64; 2]; 4]) -> [[f64; 3]; 3] {
|
||||
[[p1[0] - p0[0] + g * p1[0], p3[0] - p0[0] + h * p3[0], p0[0]], [p1[1] - p0[1] + g * p1[1], p3[1] - p0[1] + h * p3[1], p0[1]], [g, h, 1.0]]
|
||||
}
|
||||
|
||||
/// The baked size of placed content with document-space bounds `bb` at `k` pixels per document pixel.
|
||||
fn size_for(bb: [f64; 4], k: f64) -> (u32, u32) {
|
||||
(((bb[2] - bb[0]) * k).ceil().clamp(1.0, 16384.0) as u32, ((bb[3] - bb[1]) * k).ceil().clamp(1.0, 16384.0) as u32)
|
||||
}
|
||||
|
||||
impl SmartObject {
|
||||
/// Whether the corner quad is a parallelogram (an affine placement).
|
||||
pub fn is_affine(&self) -> bool {
|
||||
@@ -237,11 +244,15 @@ impl SmartObject {
|
||||
|
||||
/// A content point (pixels of a `w`×`h` content) in document pixels: the warp, then the quad.
|
||||
pub fn map(&self, p: [f64; 2], w: f64, h: f64) -> [f64; 2] {
|
||||
self.map_with(&square_to_quad(self.quad), p, w, h)
|
||||
}
|
||||
|
||||
/// [`Self::map`] with the quad's projective matrix already solved (`square_to_quad(self.quad)`).
|
||||
fn map_with(&self, m: &[[f64; 3]; 3], p: [f64; 2], w: f64, h: f64) -> [f64; 2] {
|
||||
let p = match &self.warp {
|
||||
Some(wp) => wp.apply(p, w, h),
|
||||
None => p,
|
||||
};
|
||||
let m = square_to_quad(self.quad);
|
||||
let (u, v) = (p[0] / w.max(1e-9), p[1] / h.max(1e-9));
|
||||
let z = m[2][0] * u + m[2][1] * v + m[2][2];
|
||||
let z = if z.abs() < 1e-12 { 1e-12 } else { z };
|
||||
@@ -250,11 +261,15 @@ impl SmartObject {
|
||||
|
||||
/// The document-space bounds (x0, y0, x1, y1) of the placed (warped) content.
|
||||
pub fn placed_bounds(&self, w: f64, h: f64) -> [f64; 4] {
|
||||
self.bounds_with(&square_to_quad(self.quad), w, h)
|
||||
}
|
||||
|
||||
fn bounds_with(&self, m: &[[f64; 3]; 3], w: f64, h: f64) -> [f64; 4] {
|
||||
let n = 32;
|
||||
let mut b = [f64::MAX, f64::MAX, f64::MIN, f64::MIN];
|
||||
for j in 0..=n {
|
||||
for i in 0..=n {
|
||||
let p = self.map([i as f64 / n as f64 * w, j as f64 / n as f64 * h], w, h);
|
||||
let p = self.map_with(m, [i as f64 / n as f64 * w, j as f64 / n as f64 * h], w, h);
|
||||
b = [b[0].min(p[0]), b[1].min(p[1]), b[2].max(p[0]), b[3].max(p[1])];
|
||||
}
|
||||
}
|
||||
@@ -263,8 +278,7 @@ impl SmartObject {
|
||||
|
||||
/// The baked size for a `w`×`h` content at `k` pixels per document pixel.
|
||||
pub fn placed_size(&self, w: f64, h: f64, k: f64) -> (u32, u32) {
|
||||
let bb = self.placed_bounds(w, h);
|
||||
(((bb[2] - bb[0]) * k).ceil().clamp(1.0, 16384.0) as u32, ((bb[3] - bb[1]) * k).ceil().clamp(1.0, 16384.0) as u32)
|
||||
size_for(self.placed_bounds(w, h), k)
|
||||
}
|
||||
|
||||
/// Bake the content (straight RGBA) as placed: warped and mapped by the quad, rendered at
|
||||
@@ -275,10 +289,12 @@ impl SmartObject {
|
||||
if content.width == 0 || content.height == 0 {
|
||||
return None;
|
||||
}
|
||||
let bb = self.placed_bounds(cw, ch);
|
||||
// The quad is solved once for the bounds, the size and the whole grid.
|
||||
let m = square_to_quad(self.quad);
|
||||
let bb = self.bounds_with(&m, cw, ch);
|
||||
let (ow, oh) = match size {
|
||||
Some((w, h)) => (w.max(1), h.max(1)),
|
||||
None => self.placed_size(cw, ch, k),
|
||||
None => size_for(bb, k),
|
||||
};
|
||||
let (ow, oh) = (ow as usize, oh as usize);
|
||||
// A forward grid, filled triangle by triangle with inverse barycentric sampling.
|
||||
@@ -288,7 +304,7 @@ impl SmartObject {
|
||||
for j in 0..=n {
|
||||
for i in 0..=n {
|
||||
let s = [i as f64 / n as f64 * cw, j as f64 / n as f64 * ch];
|
||||
let d = self.map(s, cw, ch);
|
||||
let d = self.map_with(&m, s, cw, ch);
|
||||
dst.push([(d[0] - bb[0]) * k, (d[1] - bb[1]) * k]);
|
||||
src.push(s);
|
||||
}
|
||||
@@ -319,6 +335,10 @@ impl SmartObject {
|
||||
if den.abs() < 1e-12 {
|
||||
continue;
|
||||
}
|
||||
// Loop-invariant per triangle: the source corners and the barycentric coefficients.
|
||||
let [sa, sb, sc] = tri.map(|t| src[t]);
|
||||
let (l1y, l1x) = (b[1] - c[1], c[0] - b[0]);
|
||||
let (l2y, l2x) = (c[1] - a[1], a[0] - c[0]);
|
||||
let x0 = a[0].min(b[0]).min(c[0]).floor().max(0.0) as usize;
|
||||
let x1 = (a[0].max(b[0]).max(c[0]).ceil() as usize).min(ow);
|
||||
let y0 = a[1].min(b[1]).min(c[1]).floor().max(0.0) as usize;
|
||||
@@ -326,13 +346,12 @@ impl SmartObject {
|
||||
for y in y0..y1 {
|
||||
for x in x0..x1 {
|
||||
let (px, py) = (x as f64 + 0.5, y as f64 + 0.5);
|
||||
let l1 = ((b[1] - c[1]) * (px - c[0]) + (c[0] - b[0]) * (py - c[1])) / den;
|
||||
let l2 = ((c[1] - a[1]) * (px - c[0]) + (a[0] - c[0]) * (py - c[1])) / den;
|
||||
let l1 = (l1y * (px - c[0]) + l1x * (py - c[1])) / den;
|
||||
let l2 = (l2y * (px - c[0]) + l2x * (py - c[1])) / den;
|
||||
let l3 = 1.0 - l1 - l2;
|
||||
if l1 < -1e-9 || l2 < -1e-9 || l3 < -1e-9 {
|
||||
continue;
|
||||
}
|
||||
let [sa, sb, sc] = tri.map(|t| src[t]);
|
||||
let sx = sa[0] * l1 + sb[0] * l2 + sc[0] * l3;
|
||||
let sy = sa[1] * l1 + sb[1] * l2 + sc[1] * l3;
|
||||
out[y * ow + x] = sample(sx, sy);
|
||||
|
||||
@@ -0,0 +1,649 @@
|
||||
//! The optimised decode / mask / placed-bake paths against the implementations they replaced
|
||||
//! (kept below verbatim as references), and counters for the work they no longer do.
|
||||
|
||||
use super::counters::{self, INSIDE, QUAD, SCRATCH};
|
||||
use super::placed::{Warp, square_to_quad};
|
||||
use super::write::*;
|
||||
use super::*;
|
||||
|
||||
// ------------------------------------------------------------------ the replaced code
|
||||
|
||||
fn ref_unpredict(depth: u16, v: &mut [u8], w: usize, h: usize) -> usize {
|
||||
let mut allocs = 0;
|
||||
match depth {
|
||||
16 => {
|
||||
for row in v.chunks_exact_mut(w * 2).take(h) {
|
||||
let mut prev = 0u16;
|
||||
for c in row.as_chunks_mut::<2>().0 {
|
||||
let x = u16::from_be_bytes([c[0], c[1]]).wrapping_add(prev);
|
||||
c.copy_from_slice(&x.to_be_bytes());
|
||||
prev = x;
|
||||
}
|
||||
}
|
||||
}
|
||||
32 => {
|
||||
for row in v.chunks_exact_mut(w * 4).take(h) {
|
||||
for i in 1..row.len() {
|
||||
row[i] = row[i].wrapping_add(row[i - 1]);
|
||||
}
|
||||
let planar = row.to_vec();
|
||||
allocs += 1;
|
||||
for x in 0..w {
|
||||
for b in 0..4 {
|
||||
row[x * 4 + b] = planar[b * w + x];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
for row in v.chunks_exact_mut(w).take(h) {
|
||||
for i in 1..row.len() {
|
||||
row[i] = row[i].wrapping_add(row[i - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
allocs
|
||||
}
|
||||
|
||||
fn ref_decode_channel(p: &Psd, offset: usize, len: usize, w: usize, h: usize) -> Result<Vec<f32>> {
|
||||
let end = offset.checked_add(len).filter(|&e| e <= p.data.len()).ok_or(Error::Truncated)?;
|
||||
let mut r = Reader::at(&p.data[..end], offset);
|
||||
let comp = r.u16()?;
|
||||
decode_pixels(w, h)?;
|
||||
let rowb = p.row_bytes(w);
|
||||
let raw = match comp {
|
||||
0 => r.bytes(rowb * h)?.to_vec(),
|
||||
1 => {
|
||||
let mut counts = Vec::with_capacity(h.min(r.remaining() / 2));
|
||||
for _ in 0..h {
|
||||
counts.push(if p.psb { r.u32()? as usize } else { r.u16()? as usize });
|
||||
}
|
||||
let mut out = Vec::with_capacity(rowb * h);
|
||||
for c in counts {
|
||||
unpack_bits(r.bytes(c)?, rowb, &mut out)?;
|
||||
}
|
||||
out
|
||||
}
|
||||
2 | 3 => {
|
||||
let rest = r.bytes(r.remaining())?;
|
||||
let mut v = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(rest, rowb * h).map_err(|e| Error::Invalid(format!("zip: {e:?}")))?;
|
||||
v.resize(rowb * h, 0);
|
||||
if comp == 3 {
|
||||
ref_unpredict(p.depth, &mut v, w, h);
|
||||
}
|
||||
v
|
||||
}
|
||||
c => return Err(Error::Unsupported(format!("compression {c}"))),
|
||||
};
|
||||
Ok(p.samples(&raw, w, h))
|
||||
}
|
||||
|
||||
fn ref_composite(p: &Psd) -> Result<Pixels> {
|
||||
let (w, h) = (p.width as usize, p.height as usize);
|
||||
decode_pixels(w, h)?;
|
||||
let mut r = Reader::at(&p.data, p.merged_offset);
|
||||
let comp = r.u16()?;
|
||||
let nch = p.channels as usize;
|
||||
let cc = p.mode.color_channels();
|
||||
let want = (cc + usize::from(p.merged_alpha)).min(nch);
|
||||
let rowb = p.row_bytes(w);
|
||||
let mut planes: Vec<Option<Vec<f32>>> = vec![None; nch];
|
||||
match comp {
|
||||
0 => {
|
||||
for pl in planes.iter_mut().take(want) {
|
||||
*pl = Some(p.samples(r.bytes(rowb * h)?, w, h));
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
let mut counts = Vec::with_capacity((nch * h).min(r.remaining() / 2));
|
||||
for _ in 0..nch * h {
|
||||
counts.push(if p.psb { r.u32()? as usize } else { r.u16()? as usize });
|
||||
}
|
||||
for (c, pl) in planes.iter_mut().enumerate().take(want) {
|
||||
let mut out = Vec::with_capacity(rowb * h);
|
||||
for row in 0..h {
|
||||
unpack_bits(r.bytes(counts[c * h + row])?, rowb, &mut out)?;
|
||||
}
|
||||
*pl = Some(p.samples(&out, w, h));
|
||||
}
|
||||
}
|
||||
2 | 3 => {
|
||||
let rest = r.bytes(r.remaining())?;
|
||||
let v = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(rest, want * rowb * h).map_err(|e| Error::Invalid(format!("zip: {e:?}")))?;
|
||||
for (c, pl) in planes.iter_mut().enumerate().take(want) {
|
||||
let mut plane = v.get(c * rowb * h..(c + 1) * rowb * h).ok_or(Error::Truncated)?.to_vec();
|
||||
if comp == 3 {
|
||||
ref_unpredict(p.depth, &mut plane, w, h);
|
||||
}
|
||||
*pl = Some(p.samples(&plane, w, h));
|
||||
}
|
||||
}
|
||||
c => return Err(Error::Unsupported(format!("compression {c}"))),
|
||||
}
|
||||
let alpha = if p.merged_alpha && nch > cc { planes[cc].take() } else { None };
|
||||
let mut data = Vec::with_capacity(w * h);
|
||||
for i in 0..w * h {
|
||||
let [r, g, b] = p.to_rgb(&planes, i);
|
||||
let a = alpha.as_ref().map_or(1.0, |a| a[i]);
|
||||
data.push([r, g, b, a]);
|
||||
}
|
||||
Ok(Pixels { width: w as u32, height: h as u32, data })
|
||||
}
|
||||
|
||||
fn ref_layer_pixels(p: &Psd, index: usize, canvas: bool) -> Result<Pixels> {
|
||||
let l = p.layers.get(index).ok_or_else(|| Error::Invalid(format!("no layer {index}")))?;
|
||||
let doc = Rect { top: 0, left: 0, bottom: p.height as i32, right: p.width as i32 };
|
||||
let fill_rect =
|
||||
if l.fill.is_some() && !l.has_pixels() { l.mask.filter(|m| !m.rect.is_empty() && m.default_color == 0).map_or(doc, |m| m.rect) } else { l.rect };
|
||||
let out_rect = if canvas {
|
||||
doc
|
||||
} else if fill_rect.is_empty() {
|
||||
Rect { top: 0, left: 0, bottom: 1, right: 1 }
|
||||
} else {
|
||||
fill_rect
|
||||
};
|
||||
let (ow, oh) = (out_rect.width() as usize, out_rect.height() as usize);
|
||||
let mut out = vec![[0.0f32; 4]; decode_pixels(ow, oh)?];
|
||||
let src = fill_rect;
|
||||
let (sw, sh) = (src.width() as usize, src.height() as usize);
|
||||
if sw > 0 && sh > 0 {
|
||||
let mut planes: Vec<Option<Vec<f32>>> = vec![None; p.mode.color_channels()];
|
||||
let mut alpha = None;
|
||||
if l.has_pixels() {
|
||||
for c in &l.channels {
|
||||
match c.id {
|
||||
-1 => alpha = Some(ref_decode_channel(p, c.offset, c.len, sw, sh)?),
|
||||
id if id >= 0 && (id as usize) < planes.len() => planes[id as usize] = Some(ref_decode_channel(p, c.offset, c.len, sw, sh)?),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let fill = l.fill.map(|c| c.map(|v| v as f32));
|
||||
let span = |s0: i32, n: usize, o0: i32, on: usize| {
|
||||
let off = i64::from(o0) - i64::from(s0);
|
||||
let lo = off.clamp(0, n as i64);
|
||||
let hi = (off + on as i64).clamp(lo, n as i64);
|
||||
(lo as usize..hi as usize, off)
|
||||
};
|
||||
let (ys, oy) = span(src.top, sh, out_rect.top, oh);
|
||||
let (xs, ox) = span(src.left, sw, out_rect.left, ow);
|
||||
for y in ys {
|
||||
let dy = (y as i64 - oy) as usize;
|
||||
for x in xs.clone() {
|
||||
let dx = (x as i64 - ox) as usize;
|
||||
let i = y * sw + x;
|
||||
let (rgb, a) = match fill {
|
||||
Some(f) if !l.has_pixels() => (f, 1.0),
|
||||
_ => (p.to_rgb(&planes, i), alpha.as_ref().map_or(1.0, |a| a[i])),
|
||||
};
|
||||
out[dy * ow + dx] = [rgb[0], rgb[1], rgb[2], a];
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(m) = l.mask.filter(|m| !m.disabled) {
|
||||
let mc = l.channels.iter().find(|c| c.id == -2);
|
||||
let (mw, mh) = (m.rect.width() as usize, m.rect.height() as usize);
|
||||
let mask = match mc {
|
||||
Some(c) if mw > 0 && mh > 0 => Some(ref_decode_channel(p, c.offset, c.len, mw, mh)?),
|
||||
_ => None,
|
||||
};
|
||||
let def = m.default_color as f32 / 255.0;
|
||||
for y in 0..oh {
|
||||
let py = out_rect.top + y as i32;
|
||||
for x in 0..ow {
|
||||
let px = out_rect.left + x as i32;
|
||||
let inside = py >= m.rect.top && py < m.rect.bottom && px >= m.rect.left && px < m.rect.right;
|
||||
let mut v = match (&mask, inside) {
|
||||
(Some(mk), true) => mk[(py - m.rect.top) as usize * mw + (px - m.rect.left) as usize],
|
||||
_ => def,
|
||||
};
|
||||
if m.inverted {
|
||||
v = 1.0 - v;
|
||||
}
|
||||
out[y * ow + x][3] *= v;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Pixels { width: ow as u32, height: oh as u32, data: out })
|
||||
}
|
||||
|
||||
fn ref_map(so: &SmartObject, p: [f64; 2], w: f64, h: f64) -> [f64; 2] {
|
||||
let p = match &so.warp {
|
||||
Some(wp) => wp.apply(p, w, h),
|
||||
None => p,
|
||||
};
|
||||
let m = square_to_quad(so.quad);
|
||||
let (u, v) = (p[0] / w.max(1e-9), p[1] / h.max(1e-9));
|
||||
let z = m[2][0] * u + m[2][1] * v + m[2][2];
|
||||
let z = if z.abs() < 1e-12 { 1e-12 } else { z };
|
||||
[(m[0][0] * u + m[0][1] * v + m[0][2]) / z, (m[1][0] * u + m[1][1] * v + m[1][2]) / z]
|
||||
}
|
||||
|
||||
fn ref_placed_bounds(so: &SmartObject, w: f64, h: f64) -> [f64; 4] {
|
||||
let n = 32;
|
||||
let mut b = [f64::MAX, f64::MAX, f64::MIN, f64::MIN];
|
||||
for j in 0..=n {
|
||||
for i in 0..=n {
|
||||
let p = ref_map(so, [i as f64 / n as f64 * w, j as f64 / n as f64 * h], w, h);
|
||||
b = [b[0].min(p[0]), b[1].min(p[1]), b[2].max(p[0]), b[3].max(p[1])];
|
||||
}
|
||||
}
|
||||
[b[0].floor(), b[1].floor(), b[2].ceil(), b[3].ceil()]
|
||||
}
|
||||
|
||||
fn ref_placed_size(so: &SmartObject, w: f64, h: f64, k: f64) -> (u32, u32) {
|
||||
let bb = ref_placed_bounds(so, w, h);
|
||||
(((bb[2] - bb[0]) * k).ceil().clamp(1.0, 16384.0) as u32, ((bb[3] - bb[1]) * k).ceil().clamp(1.0, 16384.0) as u32)
|
||||
}
|
||||
|
||||
fn ref_render_placed(so: &SmartObject, content: &Pixels, k: f64, size: Option<(u32, u32)>) -> Option<(Pixels, [f64; 2])> {
|
||||
let (cw, ch) = (content.width as f64, content.height as f64);
|
||||
if content.width == 0 || content.height == 0 {
|
||||
return None;
|
||||
}
|
||||
let bb = ref_placed_bounds(so, cw, ch);
|
||||
let (ow, oh) = match size {
|
||||
Some((w, h)) => (w.max(1), h.max(1)),
|
||||
None => ref_placed_size(so, cw, ch, k),
|
||||
};
|
||||
let (ow, oh) = (ow as usize, oh as usize);
|
||||
let n = 48usize;
|
||||
let mut dst = Vec::with_capacity((n + 1) * (n + 1));
|
||||
let mut src = Vec::with_capacity((n + 1) * (n + 1));
|
||||
for j in 0..=n {
|
||||
for i in 0..=n {
|
||||
let s = [i as f64 / n as f64 * cw, j as f64 / n as f64 * ch];
|
||||
let d = ref_map(so, s, cw, ch);
|
||||
dst.push([(d[0] - bb[0]) * k, (d[1] - bb[1]) * k]);
|
||||
src.push(s);
|
||||
}
|
||||
}
|
||||
let mut out = vec![[0.0f32; 4]; ow * oh];
|
||||
let sample = |x: f64, y: f64| -> [f32; 4] {
|
||||
let (fx, fy) = ((x - 0.5).clamp(0.0, cw - 1.0), (y - 0.5).clamp(0.0, ch - 1.0));
|
||||
let (x0, y0) = (fx.floor() as usize, fy.floor() as usize);
|
||||
let (x1, y1) = ((x0 + 1).min(content.width as usize - 1), (y0 + 1).min(content.height as usize - 1));
|
||||
let (a, b) = ((fx - x0 as f64) as f32, (fy - y0 as f64) as f32);
|
||||
let at = |x: usize, y: usize| {
|
||||
let p = content.data[y * content.width as usize + x];
|
||||
[p[0] * p[3], p[1] * p[3], p[2] * p[3], p[3]]
|
||||
};
|
||||
let (p00, p10, p01, p11) = (at(x0, y0), at(x1, y0), at(x0, y1), at(x1, y1));
|
||||
let mut o = [0.0; 4];
|
||||
for c in 0..4 {
|
||||
o[c] = (p00[c] * (1.0 - a) + p10[c] * a) * (1.0 - b) + (p01[c] * (1.0 - a) + p11[c] * a) * b;
|
||||
}
|
||||
o
|
||||
};
|
||||
let idx = |i: usize, j: usize| j * (n + 1) + i;
|
||||
for j in 0..n {
|
||||
for i in 0..n {
|
||||
for tri in [[idx(i, j), idx(i + 1, j), idx(i + 1, j + 1)], [idx(i, j), idx(i + 1, j + 1), idx(i, j + 1)]] {
|
||||
let [a, b, c] = tri.map(|t| dst[t]);
|
||||
let den = (b[1] - c[1]) * (a[0] - c[0]) + (c[0] - b[0]) * (a[1] - c[1]);
|
||||
if den.abs() < 1e-12 {
|
||||
continue;
|
||||
}
|
||||
let x0 = a[0].min(b[0]).min(c[0]).floor().max(0.0) as usize;
|
||||
let x1 = (a[0].max(b[0]).max(c[0]).ceil() as usize).min(ow);
|
||||
let y0 = a[1].min(b[1]).min(c[1]).floor().max(0.0) as usize;
|
||||
let y1 = (a[1].max(b[1]).max(c[1]).ceil() as usize).min(oh);
|
||||
for y in y0..y1 {
|
||||
for x in x0..x1 {
|
||||
let (px, py) = (x as f64 + 0.5, y as f64 + 0.5);
|
||||
let l1 = ((b[1] - c[1]) * (px - c[0]) + (c[0] - b[0]) * (py - c[1])) / den;
|
||||
let l2 = ((c[1] - a[1]) * (px - c[0]) + (a[0] - c[0]) * (py - c[1])) / den;
|
||||
let l3 = 1.0 - l1 - l2;
|
||||
if l1 < -1e-9 || l2 < -1e-9 || l3 < -1e-9 {
|
||||
continue;
|
||||
}
|
||||
let [sa, sb, sc] = tri.map(|t| src[t]);
|
||||
let sx = sa[0] * l1 + sb[0] * l2 + sc[0] * l3;
|
||||
let sy = sa[1] * l1 + sb[1] * l2 + sc[1] * l3;
|
||||
out[y * ow + x] = sample(sx, sy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for p in &mut out {
|
||||
if p[3] > 0.0 {
|
||||
for c in 0..3 {
|
||||
p[c] /= p[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
Some((Pixels { width: ow as u32, height: oh as u32, data: out }, [bb[0], bb[1]]))
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ fixtures
|
||||
|
||||
fn bits(p: &Pixels) -> Vec<[u32; 4]> {
|
||||
p.data.iter().map(|px| px.map(f32::to_bits)).collect()
|
||||
}
|
||||
|
||||
/// Deterministic samples in 0..1 for a `w`×`h` plane, as file bytes at `depth`.
|
||||
fn synth_plane(depth: u16, w: usize, h: usize, seed: usize) -> Vec<u8> {
|
||||
let mut out = vec![];
|
||||
for i in 0..w * h {
|
||||
let v = ((i * 7 + seed * 13) % 101) as f32 / 100.0;
|
||||
match depth {
|
||||
8 => out.push((v * 255.0).round() as u8),
|
||||
16 => out.extend(((v * 65535.0).round() as u16).to_be_bytes()),
|
||||
_ => out.extend(v.to_be_bytes()),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The delta coding that `comp` 3 undoes (the inverse of `Psd::unpredict`).
|
||||
fn predict(raw: &[u8], w: usize, depth: u16) -> Vec<u8> {
|
||||
let mut out = raw.to_vec();
|
||||
match depth {
|
||||
16 => {
|
||||
for row in out.chunks_exact_mut(w * 2) {
|
||||
let mut prev = 0u16;
|
||||
for c in row.as_chunks_mut::<2>().0 {
|
||||
let x = u16::from_be_bytes([c[0], c[1]]);
|
||||
c.copy_from_slice(&x.wrapping_sub(prev).to_be_bytes());
|
||||
prev = x;
|
||||
}
|
||||
}
|
||||
}
|
||||
32 => {
|
||||
for row in out.chunks_exact_mut(w * 4) {
|
||||
let inter = row.to_vec();
|
||||
for x in 0..w {
|
||||
for b in 0..4 {
|
||||
row[b * w + x] = inter[x * 4 + b];
|
||||
}
|
||||
}
|
||||
for i in (1..row.len()).rev() {
|
||||
row[i] = row[i].wrapping_sub(row[i - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
for row in out.chunks_exact_mut(w) {
|
||||
for i in (1..row.len()).rev() {
|
||||
row[i] = row[i].wrapping_sub(row[i - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A pixel-data stream (compression field first) of consecutive `w`×`h` planes.
|
||||
fn stream(planes: &[Vec<u8>], w: usize, h: usize, depth: u16, comp: u16) -> Vec<u8> {
|
||||
let mut out = comp.to_be_bytes().to_vec();
|
||||
let rowb = planes.first().map_or(0, |p| p.len() / h.max(1));
|
||||
match comp {
|
||||
0 => planes.iter().for_each(|p| out.extend(p)),
|
||||
1 => {
|
||||
let rows: Vec<Vec<u8>> = planes.iter().flat_map(|p| p.chunks(rowb.max(1)).map(pack_bits)).collect();
|
||||
rows.iter().for_each(|r| out.extend((r.len() as u16).to_be_bytes()));
|
||||
rows.iter().for_each(|r| out.extend(r));
|
||||
}
|
||||
_ => {
|
||||
let all: Vec<u8> = planes.iter().flat_map(|p| if comp == 3 { predict(p, w, depth) } else { p.clone() }).collect();
|
||||
out.extend(miniz_oxide::deflate::compress_to_vec_zlib(&all, 6));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A small RGB document with a background, a hidden box and a masked layer (8 bit, raw).
|
||||
fn base() -> Psd {
|
||||
let mut d = WDoc::new(40, 30);
|
||||
d.rle = false;
|
||||
let grad: Vec<[f32; 4]> = (0..1200).map(|i| [(i % 40) as f32 / 39.0, (i / 40) as f32 / 29.0, 0.25, 1.0]).collect();
|
||||
let bg = WLayer::pixels("Background", Rect::new(0, 0, 40, 30), grad);
|
||||
let mut red = WLayer::solid("Red", Rect::new(5, 6, 10, 8), [1.0, 0.0, 0.0, 0.5]);
|
||||
red.hidden = true;
|
||||
let mut masked = WLayer::solid("Masked", Rect::new(20, 10, 12, 12), [0.0, 0.5, 1.0, 1.0]);
|
||||
let data = (0..144).map(|i| if i % 12 < 6 { 1.0 } else { 0.25 }).collect();
|
||||
masked.mask = Some(WMask { rect: Rect::new(20, 10, 12, 12), data, default_color: 0, disabled: false });
|
||||
d.layers = vec![bg, red, masked];
|
||||
Psd::parse(write(&d)).unwrap()
|
||||
}
|
||||
|
||||
/// `p` with every channel and the merged image re-encoded at `depth` with compression `comp`.
|
||||
fn reencode(p: &Psd, depth: u16, comp: u16) -> Psd {
|
||||
let mut z = p.clone();
|
||||
z.depth = depth;
|
||||
let mut data = p.data.to_vec();
|
||||
for (n, (l, zl)) in p.layers.iter().zip(&mut z.layers).enumerate() {
|
||||
for (c, zc) in l.channels.iter().zip(&mut zl.channels) {
|
||||
let r = if c.id == -2 { l.mask.map(|m| m.rect).unwrap_or(l.rect) } else { l.rect };
|
||||
let (w, h) = (r.width() as usize, r.height() as usize);
|
||||
zc.offset = data.len();
|
||||
data.extend(stream(&[synth_plane(depth, w, h, n * 5 + (c.id + 2) as usize)], w, h, depth, comp));
|
||||
zc.len = data.len() - zc.offset;
|
||||
}
|
||||
}
|
||||
let (w, h) = (p.width as usize, p.height as usize);
|
||||
let planes: Vec<Vec<u8>> = (0..usize::from(p.channels)).map(|c| synth_plane(depth, w, h, 100 + c)).collect();
|
||||
z.merged_offset = data.len();
|
||||
data.extend(stream(&planes, w, h, depth, comp));
|
||||
z.data = data.into();
|
||||
z
|
||||
}
|
||||
|
||||
fn variants() -> impl Iterator<Item = (u16, u16)> {
|
||||
[8u16, 16, 32].into_iter().flat_map(|d| [0u16, 1, 2, 3].into_iter().map(move |c| (d, c)))
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ decoding
|
||||
|
||||
#[test]
|
||||
fn unpredict_matches_the_reference_and_allocates_one_scratch_row() {
|
||||
for depth in [8u16, 16, 32] {
|
||||
let p = Psd { depth, ..base() };
|
||||
let (w, h) = (13usize, 9usize);
|
||||
let bps = usize::from(depth / 8);
|
||||
let mut a: Vec<u8> = (0..w * h * bps).map(|i| (i * 31 % 251) as u8).collect();
|
||||
let mut b = a.clone();
|
||||
counters::take(&SCRATCH);
|
||||
p.unpredict(&mut a, w, h);
|
||||
let scratch = counters::take(&SCRATCH);
|
||||
let old_allocs = ref_unpredict(depth, &mut b, w, h);
|
||||
assert_eq!(a, b, "{depth} bit");
|
||||
// The reference copied every row; the new code uses one scratch row (32 bit only).
|
||||
assert_eq!((scratch, old_allocs), if depth == 32 { (1, h) } else { (0, 0) }, "{depth} bit");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_planes_are_borrowed_and_zip_planes_are_unpredicted_in_place() {
|
||||
let p = base();
|
||||
// Raw: the sample bytes are a view into the file data.
|
||||
let q = reencode(&p, 8, 0);
|
||||
let c = q.layers[0].channels[0];
|
||||
let mut r = Reader::at(&q.data, c.offset);
|
||||
let comp = r.u16().unwrap();
|
||||
let plane = q.plane_bytes(comp, &mut r, 40, 30).unwrap();
|
||||
let Cow::Borrowed(b) = plane else { panic!("raw plane was copied") };
|
||||
let file = q.data.as_ptr_range();
|
||||
assert!(file.contains(&b.as_ptr()) && b.len() == 1200);
|
||||
// ZIP: the plane handed out is a sub-slice of the inflated buffer, not a copy.
|
||||
let mut v: Vec<u8> = (0..3 * 40 * 30).map(|i| (i % 255) as u8).collect();
|
||||
let range = v.as_ptr_range();
|
||||
for comp in [2, 3] {
|
||||
let got = q.zip_plane(comp, &mut v, 1, 40, 30).unwrap();
|
||||
assert_eq!(got.len(), 1200);
|
||||
assert!(range.contains(&got.as_ptr()), "compression {comp}");
|
||||
}
|
||||
assert!(q.zip_plane(2, &mut v, 3, 40, 30).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_and_composite_match_the_reference() {
|
||||
let p = base();
|
||||
for (depth, comp) in variants() {
|
||||
let q = reencode(&p, depth, comp);
|
||||
let what = format!("{depth} bit, compression {comp}");
|
||||
assert_eq!(bits(&q.composite().unwrap()), bits(&ref_composite(&q).unwrap()), "composite {what}");
|
||||
for i in 0..q.layers.len() {
|
||||
for canvas in [false, true] {
|
||||
assert_eq!(bits(&q.layer_pixels(i, canvas).unwrap()), bits(&ref_layer_pixels(&q, i, canvas).unwrap()), "layer {i} canvas {canvas}, {what}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_match_the_reference_on_damaged_data() {
|
||||
let p = base();
|
||||
for (depth, comp) in variants() {
|
||||
let mut q = reencode(&p, depth, comp);
|
||||
// Cut the file in the middle of the merged image / the last channel.
|
||||
let cut = q.data.len() - 7;
|
||||
q.data = q.data[..cut].into();
|
||||
assert_eq!(q.composite().map(|p| bits(&p)), ref_composite(&q).map(|p| bits(&p)), "{depth} bit, compression {comp}");
|
||||
assert_eq!(q.layer_pixels(2, true).map(|p| bits(&p)), ref_layer_pixels(&q, 2, true).map(|p| bits(&p)), "{depth} bit, compression {comp}");
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ the layer mask
|
||||
|
||||
fn rect_at(left: i32, top: i32) -> Rect {
|
||||
Rect::new(left, top, 12, 12)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_mask_matches_the_reference_for_every_mask_shape() {
|
||||
let p = base();
|
||||
for (depth, comp) in [(8, 1), (16, 2), (32, 3), (8, 0)] {
|
||||
let q = reencode(&p, depth, comp);
|
||||
// Moved so it overlaps the layer / canvas partly, wholly, or not at all (the data is 12×12).
|
||||
for (left, top) in [(20, 10), (14, 4), (-5, -3), (34, 24), (25, 12), (300, 300), (-300, 5)] {
|
||||
for default_color in [0u8, 255, 90] {
|
||||
for inverted in [false, true] {
|
||||
for disabled in [false, true] {
|
||||
for with_data in [true, false] {
|
||||
let mut r = q.clone();
|
||||
r.layers[2].mask = Some(LayerMask { rect: rect_at(left, top), default_color, disabled, inverted });
|
||||
if !with_data {
|
||||
r.layers[2].channels.retain(|c| c.id != -2);
|
||||
}
|
||||
for canvas in [false, true] {
|
||||
let what = format!(
|
||||
"{depth} bit comp {comp}, mask at ({left},{top}) default {default_color} inverted {inverted} disabled {disabled} data {with_data} canvas {canvas}"
|
||||
);
|
||||
assert_eq!(bits(&r.layer_pixels(2, canvas).unwrap()), bits(&ref_layer_pixels(&r, 2, canvas).unwrap()), "{what}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_mask_evaluates_inside_per_row_and_skips_no_ops() {
|
||||
let q = reencode(&base(), 8, 0);
|
||||
let with = |default_color, inverted, data: bool| {
|
||||
let mut r = q.clone();
|
||||
r.layers[2].mask = Some(LayerMask { rect: rect_at(20, 10), default_color, disabled: false, inverted });
|
||||
if !data {
|
||||
r.layers[2].channels.retain(|c| c.id != -2);
|
||||
}
|
||||
r
|
||||
};
|
||||
// With mask data: one inside test per output row (the reference tested every pixel: 40 × 30).
|
||||
counters::take(&INSIDE);
|
||||
with(0, false, true).layer_pixels(2, true).unwrap();
|
||||
assert_eq!(counters::take(&INSIDE), 30);
|
||||
// Own bounds: 12 rows.
|
||||
with(0, false, true).layer_pixels(2, false).unwrap();
|
||||
assert_eq!(counters::take(&INSIDE), 12);
|
||||
// No mask data: no per-pixel or per-row test at all (a constant factor, skipped when 1.0).
|
||||
with(255, false, false).layer_pixels(2, true).unwrap();
|
||||
with(0, true, false).layer_pixels(2, true).unwrap();
|
||||
with(0, false, false).layer_pixels(2, true).unwrap();
|
||||
assert_eq!(counters::take(&INSIDE), 0);
|
||||
// A no-op mask factor leaves the alpha untouched.
|
||||
let plain = with(255, false, false).layer_pixels(2, false).unwrap();
|
||||
let mut none = q.clone();
|
||||
none.layers[2].mask = None;
|
||||
assert_eq!(bits(&plain), bits(&none.layer_pixels(2, false).unwrap()));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ placed layers
|
||||
|
||||
fn warps() -> Vec<Option<Warp>> {
|
||||
let mut mesh = vec![];
|
||||
for j in 0..4 {
|
||||
for i in 0..4 {
|
||||
mesh.push([i as f64 * 20.0 / 3.0 + if j == 1 || j == 2 { 2.0 } else { 0.0 }, j as f64 * 10.0 / 3.0]);
|
||||
}
|
||||
}
|
||||
vec![
|
||||
None,
|
||||
Some(Warp { style: "warpArc".into(), bend: 40.0, bounds: [0.0, 0.0, 20.0, 10.0], ..Default::default() }),
|
||||
Some(Warp { style: "warpCustom".into(), bounds: [0.0, 0.0, 20.0, 10.0], cols: 1, rows: 1, mesh, ..Default::default() }),
|
||||
]
|
||||
}
|
||||
|
||||
fn content(w: u32, h: u32) -> Pixels {
|
||||
Pixels {
|
||||
width: w,
|
||||
height: h,
|
||||
data: (0..w * h).map(|i| [(i % w) as f32 / w as f32, (i / w) as f32 / h as f32, 0.5, if i % 7 == 0 { 0.4 } else { 1.0 }]).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placed_bake_matches_the_reference() {
|
||||
let quads = [
|
||||
[[10.0, 10.0], [50.0, 0.0], [50.0, 40.0], [10.0, 30.0]],
|
||||
[[0.0, 0.0], [20.0, 0.0], [20.0, 10.0], [0.0, 10.0]],
|
||||
[[3.0, 4.0], [33.0, 9.0], [28.0, 29.0], [-2.0, 21.0]],
|
||||
[[0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0]],
|
||||
];
|
||||
let c = content(20, 10);
|
||||
for quad in quads {
|
||||
for warp in warps() {
|
||||
let so = SmartObject { uuid: "x".into(), quad, size: None, warp };
|
||||
let what = format!("{quad:?} {:?}", so.warp.as_ref().map(|w| &w.style));
|
||||
assert_eq!(so.placed_bounds(20.0, 10.0), ref_placed_bounds(&so, 20.0, 10.0), "{what}");
|
||||
for k in [0.5, 1.0, 2.0] {
|
||||
assert_eq!(so.placed_size(20.0, 10.0, k), ref_placed_size(&so, 20.0, 10.0, k), "{what} k {k}");
|
||||
for size in [None, Some((30, 25)), Some((0, 0))] {
|
||||
let got = so.render_placed(&c, k, size);
|
||||
let want = ref_render_placed(&so, &c, k, size);
|
||||
assert_eq!(
|
||||
got.as_ref().map(|(p, o)| (p.width, p.height, bits(p), *o)),
|
||||
want.as_ref().map(|(p, o)| (p.width, p.height, bits(p), *o)),
|
||||
"{what} k {k} size {size:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
for p in [[0.0, 0.0], [10.0, 5.0], [20.0, 10.0], [7.5, 2.25]] {
|
||||
assert_eq!(so.map(p, 20.0, 10.0), ref_map(&so, p, 20.0, 10.0), "{what} {p:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(SmartObject { uuid: "x".into(), quad: [[0.0; 2]; 4], size: None, warp: None }.render_placed(&content(0, 0), 1.0, None).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placed_bake_solves_the_quad_once() {
|
||||
let so = SmartObject { uuid: "x".into(), quad: [[10.0, 10.0], [50.0, 0.0], [50.0, 40.0], [10.0, 30.0]], size: None, warp: None };
|
||||
counters::take(&QUAD);
|
||||
so.placed_bounds(20.0, 10.0);
|
||||
assert_eq!(counters::take(&QUAD), 1, "33×33 mapped points, one solve (was 1089)");
|
||||
so.placed_size(20.0, 10.0, 1.0);
|
||||
assert_eq!(counters::take(&QUAD), 1);
|
||||
// The bounds, the size and the 49×49 grid of one bake share a single solve (was 1089 × 2 + 2401).
|
||||
so.render_placed(&content(20, 10), 1.0, None).unwrap();
|
||||
assert_eq!(counters::take(&QUAD), 1);
|
||||
so.render_placed(&content(20, 10), 1.0, Some((40, 40))).unwrap();
|
||||
assert_eq!(counters::take(&QUAD), 1);
|
||||
// A single `map` still solves for itself.
|
||||
so.map([1.0, 1.0], 20.0, 10.0);
|
||||
assert_eq!(counters::take(&QUAD), 1);
|
||||
}
|
||||
Reference in new issue
Block a user