mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 18:13:14 +00:00
craft-psd: less copying and compression work when encoding planes (#1043)
- PackBits scans each row once; RLE planes encode their rows on worker threads on native targets (rayon), keeping the exact packet boundaries and the fallback when RLE does not pay. - ZIP output grows in bounded blocks and is compacted to its encoded size, instead of reserving the raw size; native threads reuse a finished encoder's working buffers. - 16/32-bit prediction works on owned buffers in one pass. - Filter-effects planes and pattern blocks encode from borrowed rows and owned buffers without the intermediate copies. The encoded bytes are unchanged. Ported from storytold/photocraft#7797 (its crates/psd part), at a1ee8355a4651d379286cd13f92dabe4344e295f. Co-authored-by: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com>
This commit is contained in:
co-authored by
Rene Leonhardt
parent
8d3f245d2a
commit
392b8720cd
Generated
+1
@@ -1711,6 +1711,7 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"flate2",
|
||||
"proptest",
|
||||
"rayon",
|
||||
"serde",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
@@ -21,6 +21,9 @@ flate2 = { version = "1.1.10", default-features = false, features = ["zlib-rs",
|
||||
encoding_rs = { workspace = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
rayon = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = { workspace = true }
|
||||
craft-psd = { path = ".", features = ["testgen"] }
|
||||
|
||||
+230
-65
@@ -68,34 +68,90 @@ pub mod packbits {
|
||||
|
||||
/// Encodes one row, appending to `out`. Never emits the `-128` no-op.
|
||||
pub fn encode(src: &[u8], out: &mut Vec<u8>) {
|
||||
// Constant 16-bit samples with different bytes contain only literal packets.
|
||||
if src.len() >= 128
|
||||
&& let Some(&[a, b]) = src.first_chunk::<2>()
|
||||
&& a != b
|
||||
{
|
||||
let (blocks, tail) = src.as_chunks::<8>();
|
||||
if blocks.iter().all(|v| *v == [a, b, a, b, a, b, a, b]) && tail.iter().zip([a, b].into_iter().cycle()).all(|(&v, p)| v == p) {
|
||||
for chunk in src.chunks(128) {
|
||||
out.push((chunk.len() - 1) as u8);
|
||||
out.extend_from_slice(chunk);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
packets(src, |header, bytes| {
|
||||
out.push(header);
|
||||
out.extend_from_slice(bytes);
|
||||
});
|
||||
}
|
||||
|
||||
/// The encoded row length, without allocating or writing its payload.
|
||||
pub fn encoded_len(src: &[u8]) -> usize {
|
||||
let mut len = 0;
|
||||
packets(src, |_, bytes| len += 1 + bytes.len());
|
||||
len
|
||||
}
|
||||
|
||||
/// Visit PackBits run and literal packets in their deterministic encoding order.
|
||||
fn packets(src: &[u8], mut emit: impl FnMut(u8, &[u8])) {
|
||||
let n = src.len();
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
// Length of the run starting at i.
|
||||
let mut run = 1;
|
||||
while i + run < n && run < 128 && src[i + run] == src[i] {
|
||||
while i + run < n && run < 8 && src[i + run] == src[i] {
|
||||
run += 1;
|
||||
}
|
||||
if run == 8 {
|
||||
while run + 8 <= 128 && src.get(i + run..).and_then(|s| s.first_chunk::<8>()) == Some(&[src[i]; 8]) {
|
||||
run += 8;
|
||||
}
|
||||
while i + run < n && run < 128 && src[i + run] == src[i] {
|
||||
run += 1;
|
||||
}
|
||||
}
|
||||
// A literal never stops before a 2-byte run, so any run of >= 2
|
||||
// seen here starts a packet boundary: a run packet is never larger.
|
||||
if run >= 2 {
|
||||
out.push((1i16 - run as i16) as i8 as u8);
|
||||
out.push(src[i]);
|
||||
emit((1i16 - run as i16) as i8 as u8, &src[i..i + 1]);
|
||||
i += run;
|
||||
continue;
|
||||
}
|
||||
// Literal: extend until a run of >= 3 begins or 128 bytes.
|
||||
let start = i;
|
||||
let mut len = 0;
|
||||
while i < n && len < 128 {
|
||||
// Short packets keep the scalar scan; lookahead pays only on longer literals.
|
||||
while i < n && len < 16 {
|
||||
if i + 2 < n && src[i] == src[i + 1] && src[i] == src[i + 2] {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
len += 1;
|
||||
}
|
||||
out.push((len - 1) as u8);
|
||||
out.extend_from_slice(&src[start..start + len]);
|
||||
if len == 16 {
|
||||
while len + 8 <= 128 {
|
||||
let Some(&[a, b, c, d, e, f, g, h, j, k]) = src.get(i..).and_then(|s| s.first_chunk::<10>()) else { break };
|
||||
let word = u64::from_le_bytes([a, b, c, d, e, f, g, h]);
|
||||
let different = (word ^ u64::from_le_bytes([b, c, d, e, f, g, h, j])) | (word ^ u64::from_le_bytes([c, d, e, f, g, h, j, k]));
|
||||
// A zero byte marks a triple; scalar scanning keeps the first boundary.
|
||||
if different.wrapping_sub(0x0101010101010101) & !different & 0x8080808080808080 != 0 {
|
||||
break;
|
||||
}
|
||||
i += 8;
|
||||
len += 8;
|
||||
}
|
||||
while i < n && len < 128 {
|
||||
if i + 2 < n && src[i] == src[i + 1] && src[i] == src[i + 2] {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
len += 1;
|
||||
}
|
||||
}
|
||||
emit((len - 1) as u8, &src[start..start + len]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,82 +425,178 @@ fn decode_rle(data: &[u8], layout: &PlaneLayout, total: usize) -> Result<Vec<u8>
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Encode independent row chunks and join their count tables and payloads in order.
|
||||
fn encode_rle(decoded: &[u8], layout: &PlaneLayout) -> Result<Vec<u8>> {
|
||||
let rows = layout.rows()?;
|
||||
let rb = layout.row_bytes();
|
||||
let cs = layout.count_size();
|
||||
if rows == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
// Rows are independent, so they pack in parallel chunks of rows. Two passes: the first only
|
||||
// measures each packed row, so the output is allocated once at its exact size and the second
|
||||
// packs every row straight into its place. Collecting packed chunks and then copying them into
|
||||
// the output held the encoded image twice, with growth slack, at the save's peak (photocraft#6607).
|
||||
let rb = layout.row_bytes();
|
||||
let cs = layout.count_size();
|
||||
let threads = if cfg!(target_arch = "wasm32") { 1 } else { std::thread::available_parallelism().map_or(1, |v| v.get()).clamp(1, 32) };
|
||||
let chunk_rows = rows.div_ceil(threads).max(1);
|
||||
let row = |r: usize| &decoded[r * rb..(r + 1) * rb];
|
||||
let mut lens = vec![0usize; rows];
|
||||
for_each_chunk(threads, lens.chunks_mut(chunk_rows).enumerate().collect(), |(c, lens): (usize, &mut [usize])| {
|
||||
let mut scratch = Vec::with_capacity(rb + rb.div_ceil(128));
|
||||
for (i, len) in lens.iter_mut().enumerate() {
|
||||
scratch.clear();
|
||||
packbits::encode(row(c * chunk_rows + i), &mut scratch);
|
||||
*len = scratch.len();
|
||||
}
|
||||
})?;
|
||||
let mut out = vec![0u8; rows * cs + lens.iter().sum::<usize>()];
|
||||
let (counts, mut packed) = out.split_at_mut(rows * cs);
|
||||
for (count, &n) in counts.chunks_exact_mut(cs).zip(&lens) {
|
||||
if cs == 2 {
|
||||
let n = u16::try_from(n).map_err(|_| PsdError::LimitExceeded("RLE row exceeds 65535 bytes (use PSB)"))?;
|
||||
count.copy_from_slice(&n.to_be_bytes());
|
||||
} else {
|
||||
let n = u32::try_from(n).map_err(|_| PsdError::LimitExceeded("RLE row too large"))?;
|
||||
count.copy_from_slice(&n.to_be_bytes());
|
||||
}
|
||||
let step = rows.div_ceil(threads).max(1);
|
||||
let ranges: Vec<_> = (0..rows).step_by(step).map(|start| start..start.saturating_add(step).min(rows)).collect();
|
||||
// Scan packets once. Each worker owns its count table and payload; joining copies each
|
||||
// payload once, trading temporary encoded bytes for the former second packet scan.
|
||||
let mut chunks: Vec<_> = map_chunks(threads, ranges.clone(), |range| -> Result<Vec<u8>> {
|
||||
let table = range.len().checked_mul(cs).ok_or(PsdError::LimitExceeded("RLE table overflow"))?;
|
||||
let hint =
|
||||
if cfg!(target_arch = "wasm32") { table } else { table.saturating_add(range.len().saturating_mul(rb.saturating_add(rb.div_ceil(128)))) };
|
||||
encode_rle_chunk(decoded, layout, range, hint)
|
||||
})?
|
||||
.into_iter()
|
||||
.collect::<Result<_>>()?;
|
||||
if chunks.len() == 1 {
|
||||
return Ok(chunks.remove(0).into_boxed_slice().into_vec());
|
||||
}
|
||||
let mut parts = Vec::with_capacity(threads);
|
||||
for (c, lens) in lens.chunks(chunk_rows).enumerate() {
|
||||
let (part, rest) = packed.split_at_mut(lens.iter().sum());
|
||||
parts.push((c, lens, part));
|
||||
packed = rest;
|
||||
let total = chunks.iter().try_fold(0usize, |n, c| n.checked_add(c.len()).ok_or(PsdError::LimitExceeded("RLE size overflow")))?;
|
||||
let table = rows.checked_mul(cs).ok_or(PsdError::LimitExceeded("RLE table overflow"))?;
|
||||
let mut out = Vec::new();
|
||||
out.try_reserve_exact(total).map_err(|_| PsdError::OutOfMemory("not enough memory for encoded RLE"))?;
|
||||
out.resize(table, 0);
|
||||
for (range, chunk) in ranges.into_iter().zip(chunks) {
|
||||
let counts = range.len() * cs;
|
||||
out[range.start * cs..range.end * cs].copy_from_slice(&chunk[..counts]);
|
||||
out.extend_from_slice(&chunk[counts..]);
|
||||
}
|
||||
for_each_chunk(threads, parts, |(c, lens, mut part): (usize, &[usize], &mut [u8])| {
|
||||
let mut scratch = Vec::with_capacity(rb + rb.div_ceil(128));
|
||||
for (i, &n) in lens.iter().enumerate() {
|
||||
scratch.clear();
|
||||
packbits::encode(row(c * chunk_rows + i), &mut scratch);
|
||||
let (dst, rest) = std::mem::take(&mut part).split_at_mut(n);
|
||||
dst.copy_from_slice(&scratch);
|
||||
part = rest;
|
||||
}
|
||||
})?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Runs `f` on every item, each on its own thread unless `threads` is 1 (wasm32).
|
||||
fn for_each_chunk<T: Send>(threads: usize, items: Vec<T>, f: impl Fn(T) + Sync) -> Result<()> {
|
||||
if threads == 1 {
|
||||
items.into_iter().for_each(f);
|
||||
return Ok(());
|
||||
}
|
||||
std::thread::scope(|scope| {
|
||||
let handles: Vec<_> = items.into_iter().map(|item| scope.spawn(|| f(item))).collect();
|
||||
// Join every worker instead of aborting the scope with a thread panic.
|
||||
let mut ok = true;
|
||||
for h in handles {
|
||||
ok &= h.join().is_ok();
|
||||
/// Encode one worker's rows, measuring packets only when the spare capacity cannot bound them.
|
||||
fn encode_rle_chunk(decoded: &[u8], layout: &PlaneLayout, range: std::ops::Range<usize>, hint: usize) -> Result<Vec<u8>> {
|
||||
const OOM: PsdError = PsdError::OutOfMemory("not enough memory for encoded RLE");
|
||||
let rb = layout.row_bytes();
|
||||
let cs = layout.count_size();
|
||||
let table = range.len().checked_mul(cs).ok_or(PsdError::LimitExceeded("RLE table overflow"))?;
|
||||
let mut out = Vec::new();
|
||||
out.try_reserve_exact(hint.max(table)).or_else(|_| out.try_reserve_exact(table)).map_err(|_| OOM)?;
|
||||
out.resize(table, 0);
|
||||
let row_bound = rb.saturating_add(rb.div_ceil(128));
|
||||
for (i, row) in range.enumerate() {
|
||||
let src = &decoded[row * rb..(row + 1) * rb];
|
||||
if out.capacity() - out.len() < row_bound {
|
||||
let needed = packbits::encoded_len(src);
|
||||
out.try_reserve(needed).or_else(|_| out.try_reserve_exact(needed)).map_err(|_| OOM)?;
|
||||
}
|
||||
if ok { Ok(()) } else { Err(PsdError::LimitExceeded("an RLE worker panicked")) }
|
||||
})
|
||||
let start = out.len();
|
||||
packbits::encode(src, &mut out);
|
||||
let n = out.len() - start;
|
||||
let count = &mut out[i * cs..(i + 1) * cs];
|
||||
if cs == 2 {
|
||||
count.copy_from_slice(&u16::try_from(n).map_err(|_| PsdError::LimitExceeded("RLE row exceeds 65535 bytes (use PSB)"))?.to_be_bytes());
|
||||
} else {
|
||||
count.copy_from_slice(&u32::try_from(n).map_err(|_| PsdError::LimitExceeded("RLE row too large"))?.to_be_bytes());
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Ordered work on the caller's pool, including nested channel and row encoding.
|
||||
fn map_chunks<T: Send, R: Send>(threads: usize, items: Vec<T>, f: impl Fn(T) -> R + Sync) -> Result<Vec<R>> {
|
||||
if threads == 1 {
|
||||
return Ok(items.into_iter().map(f).collect());
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
use rayon::prelude::*;
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| items.into_par_iter().map(&f).collect()))
|
||||
.map_err(|_| PsdError::LimitExceeded("an RLE worker panicked"))
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Ok(items.into_iter().map(f).collect())
|
||||
}
|
||||
|
||||
/// zlib-compresses `data` at the default level.
|
||||
pub fn zip_compress(data: &[u8]) -> Vec<u8> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mut e = zip_encoder();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
// Writing into a Vec cannot fail.
|
||||
// The in-memory output always accepts the complete write.
|
||||
let _ = e.write_all(data);
|
||||
e.finish().unwrap_or_default()
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
if e.try_finish().is_err() {
|
||||
return Vec::new();
|
||||
}
|
||||
let output = std::mem::take(e.get_mut());
|
||||
// Return only the finished codec; the slot stays empty during compression.
|
||||
let _ = ZIP_ENCODER.try_with(|slot| slot.set(Some(e)));
|
||||
output.into_vec()
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
e.finish().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
thread_local! {
|
||||
/// One finished codec per thread; encoded output moves out before it is retained.
|
||||
static ZIP_ENCODER: std::cell::Cell<Option<flate2::write::ZlibEncoder<ZipOutput>>> = const { std::cell::Cell::new(None) };
|
||||
}
|
||||
|
||||
/// Reset a thread's finished encoder without reallocating its fixed-size working buffers.
|
||||
/// Nested calls and thread teardown can use a fresh encoder without accessing a busy slot.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn zip_encoder() -> flate2::write::ZlibEncoder<ZipOutput> {
|
||||
if let Some(mut encoder) = ZIP_ENCODER.try_with(std::cell::Cell::take).ok().flatten()
|
||||
&& encoder.reset(ZipOutput::default()).is_ok()
|
||||
{
|
||||
return encoder;
|
||||
}
|
||||
flate2::write::ZlibEncoder::new(ZipOutput::default(), flate2::Compression::default())
|
||||
}
|
||||
|
||||
/// Bounded native output blocks avoid reallocating large ZIP buffers as they grow.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Default)]
|
||||
struct ZipOutput {
|
||||
blocks: Vec<Vec<u8>>,
|
||||
current: Vec<u8>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl std::io::Write for ZipOutput {
|
||||
/// Accept all input bytes into bounded output blocks without reserving raw image size.
|
||||
fn write(&mut self, mut bytes: &[u8]) -> std::io::Result<usize> {
|
||||
let len = bytes.len();
|
||||
while !bytes.is_empty() {
|
||||
if self.current.len() == self.current.capacity() {
|
||||
if !self.current.is_empty() {
|
||||
self.blocks.push(std::mem::take(&mut self.current));
|
||||
}
|
||||
self.current = Vec::with_capacity(if self.blocks.is_empty() { 8192 } else { 256 << 10 });
|
||||
}
|
||||
let part = &mut self.current;
|
||||
let (head, rest) = bytes.split_at((part.capacity() - part.len()).min(bytes.len()));
|
||||
part.extend_from_slice(head);
|
||||
bytes = rest;
|
||||
}
|
||||
Ok(len)
|
||||
}
|
||||
|
||||
/// Complete a no-op flush because every write is already retained in memory.
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl ZipOutput {
|
||||
/// Join encoded ZIP blocks into one vector sized to the compressed payload.
|
||||
fn into_vec(mut self) -> Vec<u8> {
|
||||
self.blocks.push(self.current);
|
||||
if self.blocks.len() == 1 {
|
||||
return self.blocks.pop().unwrap_or_default().into_boxed_slice().into_vec();
|
||||
}
|
||||
let len = self.blocks.iter().fold(0usize, |n, c| n.saturating_add(c.len()));
|
||||
let mut out = Vec::with_capacity(len);
|
||||
for part in self.blocks {
|
||||
out.extend_from_slice(&part);
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Decompresses a zlib stream, requiring at least `expected` output bytes and
|
||||
@@ -489,6 +641,20 @@ pub fn predict(buf: &mut [u8], layout: &PlaneLayout) -> Result<()> {
|
||||
if rb == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if buf.len() >= 1 << 20 && matches!(layout.depth, 1 | 8 | 16 | 32) {
|
||||
use rayon::prelude::*;
|
||||
let threads = rayon::current_num_threads().min(32);
|
||||
let rows = buf.len() / rb;
|
||||
let step = rows.div_ceil(threads).max(1) * rb;
|
||||
return buf[..rows * rb].par_chunks_mut(step).try_for_each(|part| predict_rows(part, layout));
|
||||
}
|
||||
predict_rows(buf, layout)
|
||||
}
|
||||
|
||||
/// Apply per-row prediction, reusing scratch for float byte-plane shuffling.
|
||||
fn predict_rows(buf: &mut [u8], layout: &PlaneLayout) -> Result<()> {
|
||||
let rb = layout.row_bytes();
|
||||
let w = layout.width;
|
||||
let mut scratch = Vec::new();
|
||||
for row in buf.chunks_exact_mut(rb) {
|
||||
@@ -509,7 +675,6 @@ pub fn predict(buf: &mut [u8], layout: &PlaneLayout) -> Result<()> {
|
||||
}
|
||||
}
|
||||
32 => {
|
||||
scratch.clear();
|
||||
scratch.resize(rb, 0);
|
||||
// Split into four byte planes (MSB first) and delta the whole
|
||||
// plane-major row. Plane k's first delta uses the last byte of
|
||||
|
||||
@@ -137,13 +137,84 @@ fn layout(planes: usize, width: usize, height: usize, depth: u16, version: Versi
|
||||
PlaneLayout { planes, width, height, depth, version }
|
||||
}
|
||||
|
||||
/// Verify PackBits round trips empty, literal and repeated-byte inputs.
|
||||
fn pb_roundtrip(src: &[u8]) -> Vec<u8> {
|
||||
let enc = packbits::encode_vec(src);
|
||||
assert_eq!(packbits::encoded_len(src), enc.len());
|
||||
let dec = packbits::decode(&enc, src.len()).unwrap();
|
||||
assert_eq!(dec, src, "encoded: {enc:?}");
|
||||
enc
|
||||
}
|
||||
|
||||
/// Compare block scanning with scalar packet boundaries over varied runs and literals.
|
||||
#[test]
|
||||
fn packbits_block_scan_preserves_scalar_packets() {
|
||||
/// Encode PackBits with the previous scalar packet scan as a byte oracle.
|
||||
fn scalar(src: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < src.len() {
|
||||
let mut run = 1;
|
||||
while i + run < src.len() && run < 128 && src[i + run] == src[i] {
|
||||
run += 1;
|
||||
}
|
||||
if run >= 2 {
|
||||
out.extend_from_slice(&[(1i16 - run as i16) as u8, src[i]]);
|
||||
i += run;
|
||||
} else {
|
||||
let start = i;
|
||||
while i < src.len() && i - start < 128 {
|
||||
if i + 2 < src.len() && src[i] == src[i + 1] && src[i] == src[i + 2] {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
out.push((i - start - 1) as u8);
|
||||
out.extend_from_slice(&src[start..i]);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
let check = |src: &[u8]| {
|
||||
let expected = scalar(src);
|
||||
let mut actual = vec![42];
|
||||
packbits::encode(src, &mut actual);
|
||||
assert_eq!(&actual[1..], expected, "source {src:?}");
|
||||
assert_eq!(packbits::encoded_len(src), expected.len());
|
||||
assert!(expected.len() <= src.len() + src.len().div_ceil(128));
|
||||
};
|
||||
for n in 0..=10 {
|
||||
for mut bits in 0..3usize.pow(n) {
|
||||
let src: Vec<_> = (0..n)
|
||||
.map(|_| {
|
||||
let b = [0, 1, 255][bits % 3];
|
||||
bits /= 3;
|
||||
b
|
||||
})
|
||||
.collect();
|
||||
check(&src);
|
||||
}
|
||||
}
|
||||
for start in 0..260 {
|
||||
for run in [2, 3, 7, 8, 9, 127, 128, 129, 256] {
|
||||
let mut src: Vec<_> = (0..520).map(|i| (i % 251) as u8).collect();
|
||||
src[start..start + run].fill(255);
|
||||
check(&src);
|
||||
}
|
||||
}
|
||||
for pair in [[0, 1], [0, 255], [128, 127], [255, 255]] {
|
||||
for n in [127, 128, 129, 255, 256, 257, 12000, 12001] {
|
||||
let mut src: Vec<_> = pair.into_iter().cycle().take(n).collect();
|
||||
check(&src);
|
||||
for i in [0, 7, 8, 127.min(n - 1), 128.min(n - 1), n - 1] {
|
||||
src[i] ^= 1;
|
||||
check(&src);
|
||||
src[i] ^= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_absurd_declared_size_is_an_error() {
|
||||
let z = zip_compress(&[7u8; 64]);
|
||||
@@ -452,6 +523,24 @@ fn prediction_32bit_floats_roundtrip() {
|
||||
assert_eq!(decode_planes(Compression::ZipPrediction, &enc, &l).unwrap(), v);
|
||||
}
|
||||
|
||||
/// Compare fused float prediction with explicit byte-plane shuffling and delta coding.
|
||||
#[test]
|
||||
fn fused_prediction_matches_shuffled_byte_deltas() {
|
||||
for w in [1, 2, 3, 31, 256, 257] {
|
||||
let l = layout(2, w, 3, 32, Version::Psb);
|
||||
let mut actual: Vec<_> = (0..l.decoded_len().unwrap()).map(|i| (i * 31 + i / 7) as u8).collect();
|
||||
let expected: Vec<_> = actual
|
||||
.chunks_exact(w * 4)
|
||||
.flat_map(|row| {
|
||||
let shuffled: Vec<_> = (0..4).flat_map(|k| row.iter().skip(k).step_by(4).copied()).collect();
|
||||
(0..shuffled.len()).map(move |i| shuffled[i].wrapping_sub(if i == 0 { 0 } else { shuffled[i - 1] }))
|
||||
})
|
||||
.collect();
|
||||
predict(&mut actual, &l).unwrap();
|
||||
assert_eq!(actual, expected, "width {w}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_bomb_is_bounded() {
|
||||
let big = zip_compress(&vec![0u8; 1 << 20]);
|
||||
@@ -463,3 +552,206 @@ fn zip_bomb_is_bounded() {
|
||||
fn zip_garbage_errors() {
|
||||
assert!(zip_decompress(&[1, 2, 3, 4], 4).is_err());
|
||||
}
|
||||
|
||||
/// Compare bounded ZIP output with the original growing writer over varied inputs.
|
||||
#[test]
|
||||
fn zip_output_matches_the_growing_writer() {
|
||||
let mut seed = 0x12345678u32;
|
||||
let noise = (0..1_048_577)
|
||||
.map(|_| {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 17;
|
||||
seed ^= seed << 5;
|
||||
seed as u8
|
||||
})
|
||||
.collect();
|
||||
for bytes in [Vec::new(), vec![0; 131072], (0..131072).map(|i| (i * 31 + i / 173) as u8).collect(), noise].iter().cycle().take(12) {
|
||||
let mut old = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
old.write_all(bytes).unwrap();
|
||||
let packed = zip_compress(bytes);
|
||||
assert_eq!(packed, old.finish().unwrap());
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
assert_eq!(packed.capacity(), packed.len());
|
||||
}
|
||||
}
|
||||
|
||||
/// Workers retain only private codec state; a checked-out slot supports nested compression.
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn zip_encoder_reuse_is_private_and_retains_no_output() {
|
||||
std::thread::scope(|scope| {
|
||||
let workers: Vec<_> = (0..2)
|
||||
.map(|byte| {
|
||||
scope.spawn(move || {
|
||||
let mut seed = 0x12345678u32;
|
||||
for len in [1_048_577usize, 32769, 0, 259, 258, 257, 1, 32767, 32768, 2, 262145] {
|
||||
let bytes: Vec<_> = (0..len)
|
||||
.map(|_| {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 17;
|
||||
seed ^= seed << 5;
|
||||
if byte == 0 { 0 } else { seed as u8 }
|
||||
})
|
||||
.collect();
|
||||
let packed = zip_compress(&bytes);
|
||||
if byte == 1 && len == 1_048_577 {
|
||||
assert!(packed.len() > 256 << 10, "the retention oracle must exercise multiple output blocks");
|
||||
}
|
||||
let encoder = ZIP_ENCODER.with(std::cell::Cell::take).unwrap();
|
||||
assert_eq!(encoder.total_in(), len as u64);
|
||||
assert_eq!(encoder.total_out(), packed.len() as u64);
|
||||
assert_eq!(encoder.get_ref().blocks.capacity(), 0);
|
||||
assert_eq!(encoder.get_ref().current.capacity(), 0);
|
||||
assert_eq!(zip_decompress(&zip_compress(b"nested"), 6).unwrap(), b"nested");
|
||||
ZIP_ENCODER.with(|slot| slot.set(Some(encoder)));
|
||||
let mut fresh = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
fresh.write_all(&bytes).unwrap();
|
||||
assert_eq!(zip_compress(&bytes), fresh.finish().unwrap());
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
for worker in workers {
|
||||
worker.join().unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Compression stays byte-identical during teardown, including an unavailable native cache.
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn zip_compression_survives_thread_local_teardown() {
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
struct OnDrop(Arc<(AtomicBool, AtomicBool)>);
|
||||
impl Drop for OnDrop {
|
||||
/// Record whether teardown reached the destroyed cache and preserved fresh-stream bytes.
|
||||
fn drop(&mut self) {
|
||||
let unavailable = ZIP_ENCODER.try_with(|_| ()).is_err();
|
||||
let mut fresh = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
fresh.write_all(b"teardown").unwrap();
|
||||
let same = zip_compress(b"teardown") == fresh.finish().unwrap();
|
||||
self.0.0.store(same, Ordering::SeqCst);
|
||||
self.0.1.store(unavailable, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
thread_local! { static AFTER_CODEC: std::cell::Cell<Option<OnDrop>> = const { std::cell::Cell::new(None) }; }
|
||||
let checked = Arc::new((AtomicBool::new(false), AtomicBool::new(false)));
|
||||
let flag = checked.clone();
|
||||
std::thread::spawn(move || {
|
||||
AFTER_CODEC.with(|slot| slot.set(Some(OnDrop(flag))));
|
||||
zip_compress(b"first");
|
||||
})
|
||||
.join()
|
||||
.unwrap();
|
||||
assert!(checked.0.load(Ordering::SeqCst), "teardown must preserve the fresh encoder's bytes");
|
||||
// OS-key TLS may destroy keys in another order; native TLS runs these destructors in reverse.
|
||||
#[cfg(any(target_vendor = "apple", target_os = "linux", target_env = "msvc"))]
|
||||
assert!(checked.1.load(Ordering::SeqCst), "teardown must exercise the uncached fallback");
|
||||
}
|
||||
|
||||
/// Verify multiple writes retain their bytes and respect ZIP output block bounds.
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn zip_output_blocks_stay_bounded_across_writes() {
|
||||
let bytes = vec![42; (1 << 20) + 1];
|
||||
let mut output = ZipOutput::default();
|
||||
output.write_all(&[]).unwrap();
|
||||
for part in bytes.chunks(32767) {
|
||||
output.write_all(part).unwrap();
|
||||
}
|
||||
assert!(output.blocks.iter().chain([&output.current]).all(|b| b.capacity() <= 256 << 10));
|
||||
let packed = output.into_vec();
|
||||
assert_eq!(packed, bytes);
|
||||
assert_eq!(packed.capacity(), packed.len());
|
||||
}
|
||||
|
||||
/// Verify compressible ZIP output retains capacity proportional to encoded size.
|
||||
#[test]
|
||||
fn compressible_zip_does_not_retain_a_raw_sized_allocation() {
|
||||
let raw = vec![0; 1 << 20];
|
||||
let packed = zip_compress(&raw);
|
||||
assert!(packed.len() < 1 << 14);
|
||||
assert!(packed.capacity() < 1 << 16);
|
||||
assert_eq!(zip_decompress(&packed, raw.len()).unwrap(), raw);
|
||||
}
|
||||
|
||||
/// Verify failed RLE allocation hints fall back to incremental growth with identical bytes.
|
||||
#[test]
|
||||
fn rle_failed_hint_and_incremental_growth_preserve_bytes() {
|
||||
for version in [Version::Psd, Version::Psb] {
|
||||
for width in [0, 1, 127, 128, 129, 1000] {
|
||||
let l = layout(1, width, 9, 8, version);
|
||||
for fill in [false, true] {
|
||||
let bytes: Vec<_> = (0..l.decoded_len().unwrap()).map(|i| if fill { 0 } else { (i * 13 + i / 17) as u8 }).collect();
|
||||
let expected = encode_planes(Compression::Rle, &bytes, &l).unwrap();
|
||||
for hint in [usize::MAX, 0, l.rows().unwrap() * l.count_size()] {
|
||||
assert_eq!(encode_rle_chunk(&bytes, &l, 0..l.rows().unwrap(), hint).unwrap(), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify measured packet lengths match encoding over mixed runs and literals.
|
||||
#[test]
|
||||
fn counted_packbits_matches_mixed_runs_and_literals() {
|
||||
for n in 0..520 {
|
||||
let bytes: Vec<_> = (0..n).map(|i| if i % 23 < 9 { (i / 23) as u8 } else { (i * 31) as u8 }).collect();
|
||||
assert_eq!(packbits::encoded_len(&bytes), packbits::encode_vec(&bytes).len());
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare parallel prediction with independent serial rows at each supported depth.
|
||||
#[test]
|
||||
fn prediction_matches_independent_serial_rows() {
|
||||
for depth in [1, 8, 16, 32] {
|
||||
let layout = layout(4, if depth == 1 { 8193 } else { 257 }, 1031, depth, Version::Psb);
|
||||
let rb = layout.row_bytes();
|
||||
let bytes: Vec<_> = (0..layout.decoded_len().unwrap() + 3).map(|i| (i * 13 + i / 17) as u8).collect();
|
||||
let mut expected = bytes.clone();
|
||||
for row in expected.chunks_exact_mut(rb) {
|
||||
let src = row.to_vec();
|
||||
if depth == 16 {
|
||||
for (i, p) in row.as_chunks_mut::<2>().0.iter_mut().enumerate().skip(1) {
|
||||
let a = u16::from_be_bytes([src[i * 2], src[i * 2 + 1]]);
|
||||
let b = u16::from_be_bytes([src[i * 2 - 2], src[i * 2 - 1]]);
|
||||
*p = a.wrapping_sub(b).to_be_bytes();
|
||||
}
|
||||
} else {
|
||||
let shuffled = if depth == 32 { (0..4).flat_map(|c| src.iter().skip(c).step_by(4).copied()).collect::<Vec<_>>() } else { src };
|
||||
for (i, p) in row.iter_mut().enumerate() {
|
||||
*p = shuffled[i].wrapping_sub(if i == 0 { 0 } else { shuffled[i - 1] });
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
for threads in [1, 2, 4] {
|
||||
use rayon::prelude::*;
|
||||
let pool = rayon::ThreadPoolBuilder::new().num_threads(threads).build().unwrap();
|
||||
pool.install(|| {
|
||||
(0..3).into_par_iter().for_each(|_| {
|
||||
let mut got = bytes.clone();
|
||||
predict(&mut got, &layout).unwrap();
|
||||
assert_eq!(got, expected, "depth {depth}, pool {threads}");
|
||||
})
|
||||
});
|
||||
}
|
||||
let mut got = bytes;
|
||||
predict(&mut got, &layout).unwrap();
|
||||
assert_eq!(got, expected);
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify RLE chunks use the caller's pool and propagate worker panics as errors.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn rle_chunks_use_the_callers_pool_and_report_panics() {
|
||||
let pool = rayon::ThreadPoolBuilder::new().num_threads(2).build().unwrap();
|
||||
pool.install(|| {
|
||||
map_chunks(8, (0..64).collect(), |_| assert!(matches!(rayon::current_thread_index(), Some(0 | 1)))).unwrap();
|
||||
assert!(map_chunks(2, vec![0, 1], |_| panic!("worker")).is_err());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -189,6 +189,52 @@ impl FilterEffects {
|
||||
let mut out = Vec::new();
|
||||
out.put_u32(self.version);
|
||||
for it in &self.items {
|
||||
let section = out.begin_len(true);
|
||||
write_pascal(&mut out, it.id.as_bytes(), 1);
|
||||
out.put_u32(it.version);
|
||||
let at = out.begin_len(true);
|
||||
write_rect(&mut out, &it.rect);
|
||||
out.put_u32(it.depth);
|
||||
out.put_u32(it.max_channels);
|
||||
for i in 0..it.max_channels as usize + 2 {
|
||||
match it.slots.get(i).and_then(Option::as_ref) {
|
||||
Some(p) => {
|
||||
out.put_u32(1);
|
||||
write_plane(&mut out, p);
|
||||
}
|
||||
None => out.put_u32(0),
|
||||
}
|
||||
}
|
||||
// A 64-bit length never overflows.
|
||||
let _ = out.end_len(at, true);
|
||||
match &it.mask {
|
||||
Some((r, p)) => {
|
||||
out.put_u8(1);
|
||||
write_rect(&mut out, r);
|
||||
write_plane(&mut out, p);
|
||||
}
|
||||
None => out.put_u8(0),
|
||||
}
|
||||
let _ = out.end_len(section, true);
|
||||
out.resize(out.len().next_multiple_of(4), 0);
|
||||
}
|
||||
if self.items.is_empty() {
|
||||
// One empty section, so the block still parses.
|
||||
out.put_u64(0);
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Serialize filter-effect sections through staging buffers as a byte oracle.
|
||||
fn staged_bytes(fx: &FilterEffects) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
out.put_u32(fx.version);
|
||||
for it in &fx.items {
|
||||
let mut body = Vec::new();
|
||||
write_pascal(&mut body, it.id.as_bytes(), 1);
|
||||
body.put_u32(it.version);
|
||||
@@ -219,17 +265,12 @@ impl FilterEffects {
|
||||
out.put(&body);
|
||||
out.resize(out.len().next_multiple_of(4), 0);
|
||||
}
|
||||
if self.items.is_empty() {
|
||||
if fx.items.is_empty() {
|
||||
// One empty section, so the block still parses.
|
||||
out.put_u64(0);
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn item(id: &str, mask: bool) -> FilterEffectsItem {
|
||||
let rect = Rect { top: 1, left: 2, bottom: 5, right: 6 };
|
||||
@@ -240,6 +281,20 @@ mod tests {
|
||||
FilterEffectsItem { id: id.into(), version: 1, rect, depth: 8, max_channels: 24, slots, mask: mask.then(|| (rect, plane(128))) }
|
||||
}
|
||||
|
||||
/// Compare direct filter-effect serialization with staged bytes and section lengths.
|
||||
#[test]
|
||||
fn direct_sections_match_staged_bytes() {
|
||||
for version in [1, 2, 3] {
|
||||
let mut large = item(&"é".repeat(140), true);
|
||||
large.rect.top = -7;
|
||||
large.slots[0].as_mut().unwrap().data = vec![73; 8193];
|
||||
for items in [Vec::new(), vec![large, item("odd", false), item("four", true)]] {
|
||||
let fx = FilterEffects { version, items };
|
||||
assert_eq!(fx.to_bytes(), staged_bytes(&fx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The unpadded body of `it`'s section.
|
||||
fn section(it: &FilterEffectsItem) -> Vec<u8> {
|
||||
let b = FilterEffects { version: 3, items: vec![it.clone()] }.to_bytes();
|
||||
|
||||
+35
-26
@@ -25,6 +25,8 @@
|
||||
use crate::compression::{Compression, MAX_DECODED_BYTES, PlaneLayout, decode_planes, encode_planes};
|
||||
use crate::error::{PsdError, Result};
|
||||
use crate::header::Version;
|
||||
use crate::io::WriteExt;
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Largest pattern edge accepted (Photoshop's own limit is far lower).
|
||||
const MAX_EDGE: u32 = 30_000;
|
||||
@@ -417,7 +419,11 @@ fn zeroed_pattern_plane(len: usize) -> Result<Vec<u8>> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Append one validated pattern, backpatching lengths in its final output buffer.
|
||||
fn write_pattern(p: &PsdPattern, out: &mut Vec<u8>) -> Result<()> {
|
||||
if !matches!(p.depth, 1 | 8 | 16 | 32) {
|
||||
return Err(PsdError::Unsupported(format!("pattern depth {}", p.depth)));
|
||||
}
|
||||
if p.width > MAX_EDGE || p.height > MAX_EDGE || p.width > i16::MAX as u32 || p.height > i16::MAX as u32 {
|
||||
return Err(PsdError::LimitExceeded("pattern size"));
|
||||
}
|
||||
@@ -444,46 +450,50 @@ fn write_pattern(p: &PsdPattern, out: &mut Vec<u8>) -> Result<()> {
|
||||
out.extend_from_slice(&pal);
|
||||
}
|
||||
let rect = [0i32, 0, p.height as i32, p.width as i32];
|
||||
let mut body = Vec::new();
|
||||
out.extend_from_slice(&3u32.to_be_bytes());
|
||||
let at = out.begin_len(false);
|
||||
for v in rect {
|
||||
body.extend_from_slice(&v.to_be_bytes());
|
||||
out.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
const SLOTS: u32 = 24;
|
||||
body.extend_from_slice(&SLOTS.to_be_bytes());
|
||||
out.extend_from_slice(&SLOTS.to_be_bytes());
|
||||
let layout = PlaneLayout { planes: 1, width: p.width as usize, height: p.height as usize, depth: p.depth, version: Version::Psd };
|
||||
let array = |plane: &[u8], body: &mut Vec<u8>| -> Result<()> {
|
||||
let array = |plane: &[u8], out: &mut Vec<u8>| -> Result<()> {
|
||||
let (comp, data) = if p.depth == 8 {
|
||||
(1u8, encode_planes(Compression::Rle, plane, &layout)?)
|
||||
(1u8, Cow::Owned(encode_planes(Compression::Rle, plane, &layout)?))
|
||||
} else {
|
||||
(0u8, encode_planes(Compression::Raw, plane, &layout)?)
|
||||
let total = layout.decoded_len()?;
|
||||
if plane.len() != total {
|
||||
return Err(PsdError::invalid(format!("decoded buffer has {} bytes, layout requires {}", plane.len(), total)));
|
||||
}
|
||||
(0u8, Cow::Borrowed(plane))
|
||||
};
|
||||
body.extend_from_slice(&1u32.to_be_bytes());
|
||||
body.extend_from_slice(&((23 + data.len()) as u32).to_be_bytes());
|
||||
body.extend_from_slice(&u32::from(p.depth).to_be_bytes());
|
||||
out.extend_from_slice(&1u32.to_be_bytes());
|
||||
let len = u32::try_from(data.len()).ok().and_then(|n| n.checked_add(23)).ok_or(PsdError::LimitExceeded("pattern array length"))?;
|
||||
out.extend_from_slice(&len.to_be_bytes());
|
||||
out.extend_from_slice(&u32::from(p.depth).to_be_bytes());
|
||||
for v in rect {
|
||||
body.extend_from_slice(&v.to_be_bytes());
|
||||
out.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
body.extend_from_slice(&p.depth.to_be_bytes());
|
||||
body.push(comp);
|
||||
body.extend_from_slice(&data);
|
||||
out.extend_from_slice(&p.depth.to_be_bytes());
|
||||
out.push(comp);
|
||||
out.extend_from_slice(&data);
|
||||
Ok(())
|
||||
};
|
||||
let nc = p.channels.len().min(SLOTS as usize);
|
||||
for c in &p.channels[..nc] {
|
||||
array(c, &mut body)?;
|
||||
array(c, out)?;
|
||||
}
|
||||
for _ in nc..SLOTS as usize {
|
||||
body.extend_from_slice(&0u32.to_be_bytes());
|
||||
out.extend_from_slice(&0u32.to_be_bytes());
|
||||
}
|
||||
// User mask slot: unused; sheet mask slot: transparency, where Photoshop keeps it (photocraft#8054).
|
||||
body.extend_from_slice(&0u32.to_be_bytes());
|
||||
out.extend_from_slice(&0u32.to_be_bytes());
|
||||
match &p.alpha {
|
||||
Some(a) => array(a, &mut body)?,
|
||||
None => body.extend_from_slice(&0u32.to_be_bytes()),
|
||||
Some(a) => array(a, out)?,
|
||||
None => out.extend_from_slice(&0u32.to_be_bytes()),
|
||||
}
|
||||
out.extend_from_slice(&3u32.to_be_bytes());
|
||||
out.extend_from_slice(&(body.len() as u32).to_be_bytes());
|
||||
out.extend_from_slice(&body);
|
||||
out.end_len(at, false)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -578,11 +588,10 @@ fn pattern_has_user_mask(r: &mut Rd) -> Result<bool> {
|
||||
pub fn write_pattern_block(patterns: &[PsdPattern]) -> Result<Vec<u8>> {
|
||||
let mut out = Vec::new();
|
||||
for p in patterns {
|
||||
let mut one = Vec::new();
|
||||
write_pattern(p, &mut one)?;
|
||||
out.extend_from_slice(&(one.len() as u32).to_be_bytes());
|
||||
let pad = (4 - one.len() % 4) % 4;
|
||||
out.extend_from_slice(&one);
|
||||
let at = out.begin_len(false);
|
||||
write_pattern(p, &mut out)?;
|
||||
out.end_len(at, false)?;
|
||||
let pad = (4 - (out.len() - at - 4) % 4) % 4;
|
||||
out.extend(std::iter::repeat_n(0u8, pad));
|
||||
}
|
||||
Ok(out)
|
||||
|
||||
@@ -17,6 +17,56 @@ fn pat(depth: u16, alpha: bool) -> PsdPattern {
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare direct pattern serialization with staged bytes at supported depths.
|
||||
#[test]
|
||||
fn direct_writes_match_staged_bytes() {
|
||||
for depth in [1, 8, 16, 32] {
|
||||
for alpha in [false, true] {
|
||||
let mut p = pat(depth, alpha);
|
||||
if depth == 1 {
|
||||
p.channels = vec![vec![0xA8; 3]; 3];
|
||||
p.alpha = alpha.then(|| vec![0xF8; 3]);
|
||||
}
|
||||
p.id = "é".repeat(140);
|
||||
let mut indexed = p.clone();
|
||||
indexed.mode = 2;
|
||||
indexed.channels.truncate(1);
|
||||
indexed.palette = Some(vec![73; 17]);
|
||||
let patterns = [p, indexed];
|
||||
let mut block = Vec::new();
|
||||
let mut file = b"8BPT\0\x01\0\0\0\x02".to_vec();
|
||||
for p in &patterns {
|
||||
let mut one = Vec::new();
|
||||
staged_pattern(p, &mut one).unwrap();
|
||||
block.extend_from_slice(&(one.len() as u32).to_be_bytes());
|
||||
block.extend_from_slice(&one);
|
||||
block.resize(block.len().next_multiple_of(4), 0);
|
||||
file.extend_from_slice(&one);
|
||||
}
|
||||
assert_eq!(write_pattern_block(&patterns).unwrap(), block);
|
||||
assert_eq!(write_pat_file(&patterns).unwrap(), file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify borrowed raw pattern planes retain layout and length validation.
|
||||
#[test]
|
||||
fn raw_pattern_writes_still_validate_layout_and_plane_length() {
|
||||
for depth in [1, 16, 32, 7] {
|
||||
let p = pat(depth, false);
|
||||
assert!(write_pattern_block(&[PsdPattern { channels: vec![vec![0; 2]], ..p.clone() }]).is_err());
|
||||
assert!(write_pat_file(&[PsdPattern { channels: Vec::new(), alpha: Some(vec![0; 2]), ..p }]).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify matching plane lengths cannot bypass unsupported pattern-depth checks.
|
||||
#[test]
|
||||
fn writers_reject_unsupported_depth_with_matching_plane_lengths() {
|
||||
let p = PsdPattern { depth: 7, channels: vec![vec![0; 15]; 3], alpha: Some(vec![0; 15]), ..pat(8, false) };
|
||||
assert!(write_pattern_block(std::slice::from_ref(&p)).is_err());
|
||||
assert!(write_pat_file(&[p]).is_err());
|
||||
}
|
||||
|
||||
/// photocraft#9318 review: a name the reader refuses (more than 65,536 UTF-16 units with its NUL) is
|
||||
/// refused on write, so a saved library can't become a file that loads nothing.
|
||||
#[test]
|
||||
@@ -350,3 +400,74 @@ fn zero_size_pattern_rectangles_are_rejected() {
|
||||
set_i32(&mut block, vma + 8, 0);
|
||||
assert!(parse_pattern_block(&block).is_err());
|
||||
}
|
||||
|
||||
/// Serialize a pattern with the previous staged writer as a byte oracle.
|
||||
fn staged_pattern(p: &PsdPattern, out: &mut Vec<u8>) -> Result<()> {
|
||||
if p.width > MAX_EDGE || p.height > MAX_EDGE || p.width > i16::MAX as u32 || p.height > i16::MAX as u32 {
|
||||
return Err(PsdError::LimitExceeded("pattern size"));
|
||||
}
|
||||
out.extend_from_slice(&1u32.to_be_bytes());
|
||||
out.extend_from_slice(&p.mode.to_be_bytes());
|
||||
out.extend_from_slice(&(p.height as u16).to_be_bytes());
|
||||
out.extend_from_slice(&(p.width as u16).to_be_bytes());
|
||||
let units: Vec<u16> = p.name.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
// The reader refuses longer names (see `read_pattern`).
|
||||
if units.len() > 65_536 {
|
||||
return Err(PsdError::LimitExceeded("pattern name length"));
|
||||
}
|
||||
out.extend_from_slice(&(units.len() as u32).to_be_bytes());
|
||||
for u in units {
|
||||
out.extend_from_slice(&u.to_be_bytes());
|
||||
}
|
||||
let id = p.id.as_bytes();
|
||||
let idl = id.len().min(255);
|
||||
out.push(idl as u8);
|
||||
out.extend_from_slice(&id[..idl]);
|
||||
if p.mode == 2 {
|
||||
let mut pal = p.palette.clone().unwrap_or_default();
|
||||
pal.resize(768, 0);
|
||||
out.extend_from_slice(&pal);
|
||||
}
|
||||
let rect = [0i32, 0, p.height as i32, p.width as i32];
|
||||
let mut body = Vec::new();
|
||||
for v in rect {
|
||||
body.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
const SLOTS: u32 = 24;
|
||||
body.extend_from_slice(&SLOTS.to_be_bytes());
|
||||
let layout = PlaneLayout { planes: 1, width: p.width as usize, height: p.height as usize, depth: p.depth, version: Version::Psd };
|
||||
let array = |plane: &[u8], body: &mut Vec<u8>| -> Result<()> {
|
||||
let (comp, data) = if p.depth == 8 {
|
||||
(1u8, encode_planes(Compression::Rle, plane, &layout)?)
|
||||
} else {
|
||||
(0u8, encode_planes(Compression::Raw, plane, &layout)?)
|
||||
};
|
||||
body.extend_from_slice(&1u32.to_be_bytes());
|
||||
body.extend_from_slice(&((23 + data.len()) as u32).to_be_bytes());
|
||||
body.extend_from_slice(&u32::from(p.depth).to_be_bytes());
|
||||
for v in rect {
|
||||
body.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
body.extend_from_slice(&p.depth.to_be_bytes());
|
||||
body.push(comp);
|
||||
body.extend_from_slice(&data);
|
||||
Ok(())
|
||||
};
|
||||
let nc = p.channels.len().min(SLOTS as usize);
|
||||
for c in &p.channels[..nc] {
|
||||
array(c, &mut body)?;
|
||||
}
|
||||
for _ in nc..SLOTS as usize {
|
||||
body.extend_from_slice(&0u32.to_be_bytes());
|
||||
}
|
||||
// User mask slot: unused; sheet mask slot: transparency, where Photoshop keeps it (photocraft#8054).
|
||||
body.extend_from_slice(&0u32.to_be_bytes());
|
||||
match &p.alpha {
|
||||
Some(a) => array(a, &mut body)?,
|
||||
None => body.extend_from_slice(&0u32.to_be_bytes()),
|
||||
}
|
||||
out.extend_from_slice(&3u32.to_be_bytes());
|
||||
out.extend_from_slice(&(body.len() as u32).to_be_bytes());
|
||||
out.extend_from_slice(&body);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user