use std::io::Write;
use flate2::{write::DeflateEncoder, Compress, Compression, FlushCompress};
pub const PAR_MIN_BLOCK: usize = 1 << 20;
pub fn block_count(filtered_len: usize, threads: usize) -> usize {
#[cfg(all(target_family = "wasm", not(target_feature = "atomics")))]
{
let _ = (filtered_len, threads);
return 1;
}
#[cfg(not(all(target_family = "wasm", not(target_feature = "atomics"))))]
{
if threads <= 1 || filtered_len < 2 * PAR_MIN_BLOCK {
return 1;
}
(filtered_len / PAR_MIN_BLOCK).min(threads).max(1)
}
}
fn adler32(data: &[u8]) -> u32 {
use simd_adler32::Adler32;
let mut h = Adler32::new();
h.write(data);
h.finish()
}
pub fn compress_parallel(
filtered: &[u8],
row_stride: usize,
level: u32,
threads: usize,
) -> std::io::Result<Vec<u8>> {
let rows = filtered.len() / row_stride;
let nblocks = block_count(filtered.len(), threads);
if nblocks <= 1 {
let mut e = DeflateEncoder::new(Vec::new(), Compression::new(level));
e.write_all(filtered)?;
let raw = e.finish()?;
return Ok(assemble(&raw, level, adler32(filtered)));
}
let rows_per = rows.div_ceil(nblocks);
let mut ranges = Vec::with_capacity(nblocks);
let mut r0 = 0usize;
while r0 < rows {
let r1 = (r0 + rows_per).min(rows);
ranges.push((r0 * row_stride, r1 * row_stride));
r0 = r1;
}
let last = ranges.len() - 1;
let mut parts: Vec<std::io::Result<Vec<u8>>> = Vec::new();
std::thread::scope(|s| {
let handles: Vec<_> = ranges
.iter()
.enumerate()
.map(|(i, &(a, b))| {
let chunk = &filtered[a..b];
s.spawn(move || compress_block(chunk, level, i == last))
})
.collect();
parts = handles.into_iter().map(|h| h.join().unwrap()).collect();
});
let mut raw = Vec::with_capacity(filtered.len() / 2 + 1024);
for p in parts {
raw.extend_from_slice(&p?);
}
Ok(assemble(&raw, level, adler32(filtered)))
}
fn compress_block(chunk: &[u8], level: u32, is_last: bool) -> std::io::Result<Vec<u8>> {
const SCRATCH: usize = 64 * 1024;
let mut c = Compress::new(Compression::new(level), false);
let mut out = Vec::with_capacity(chunk.len() / 2 + 64);
let mut buf = vec![0u8; SCRATCH];
let mut input = chunk;
while !input.is_empty() {
let (before_in, before_out) = (c.total_in(), c.total_out());
c.compress(input, &mut buf, FlushCompress::None)
.map_err(std::io::Error::other)?;
let produced = (c.total_out() - before_out) as usize;
let consumed = (c.total_in() - before_in) as usize;
out.extend_from_slice(&buf[..produced]);
input = &input[consumed..];
if consumed == 0 && produced == 0 {
break;
}
}
let flush = if is_last {
FlushCompress::Finish
} else {
FlushCompress::Full
};
loop {
let before_out = c.total_out();
let status = c
.compress(&[], &mut buf, flush)
.map_err(std::io::Error::other)?;
let produced = (c.total_out() - before_out) as usize;
out.extend_from_slice(&buf[..produced]);
match status {
flate2::Status::StreamEnd => break,
_ if produced == 0 => break,
_ => {}
}
}
Ok(out)
}
fn assemble(raw: &[u8], level: u32, adler: u32) -> Vec<u8> {
let cmf = 0x78u8;
let flevel = match level {
0..=1 => 0u8,
2..=5 => 1,
6 => 2,
_ => 3,
};
let mut flg = flevel << 6;
let rem = ((cmf as u16) << 8 | flg as u16) % 31;
if rem != 0 {
flg += (31 - rem) as u8;
}
let mut out = Vec::with_capacity(raw.len() + 6);
out.push(cmf);
out.push(flg);
out.extend_from_slice(raw);
out.extend_from_slice(&adler.to_be_bytes());
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
fn roundtrip(data: &[u8], stride: usize, threads: usize) {
let z = compress_parallel(data, stride, 6, threads).expect("compress");
let mut d = flate2::read::ZlibDecoder::new(&z[..]);
let mut back = Vec::new();
d.read_to_end(&mut back).expect("the stream must be valid zlib");
assert_eq!(back, data, "threads={threads}: round-trip mismatch");
}
#[test]
fn parallel_stream_is_valid_zlib_and_lossless() {
let stride = 4096;
let rows = 900; let mut data = Vec::with_capacity(stride * rows);
for r in 0..rows {
data.push(1u8);
for i in 1..stride {
data.push(((r * 31 + i * 7) % 251) as u8);
}
}
for threads in [1usize, 2, 3, 4, 8] {
roundtrip(&data, stride, threads);
}
}
#[test]
fn small_inputs_stay_serial() {
assert_eq!(block_count(1024, 8), 1);
assert_eq!(block_count(PAR_MIN_BLOCK, 8), 1, "one block's worth is not splittable");
assert_eq!(block_count(2 * PAR_MIN_BLOCK, 8), 2);
assert_eq!(block_count(64 * PAR_MIN_BLOCK, 8), 8, "capped by the thread budget");
assert_eq!(block_count(64 * PAR_MIN_BLOCK, 1), 1, "single thread never splits");
}
}
#[cfg(test)]
mod encoder_integration {
#[test]
fn parallel_encode_matches_serial_pixels() {
let (w, h, ch) = (900usize, 1400usize, 3usize);
let mut px = Vec::with_capacity(w * h * ch);
for y in 0..h {
for x in 0..w {
px.push(((x * 7 + y * 3) % 256) as u8);
px.push(((x ^ y) % 256) as u8);
px.push(((x * y) % 251) as u8);
}
}
let encode = |threads: usize| -> Vec<u8> {
let mut out = Vec::new();
{
let mut e = crate::Encoder::new(&mut out, w as u32, h as u32);
e.set_color(crate::ColorType::Rgb);
e.set_depth(crate::BitDepth::Eight);
e.set_compression(crate::Compression::Default);
e.set_filter(crate::FilterType::Up);
e.set_parallel(threads);
e.write_header().unwrap().write_image_data(&px).unwrap();
}
out
};
let decode = |bytes: &[u8]| -> Vec<u8> {
let d = crate::Decoder::new(std::io::Cursor::new(bytes.to_vec()));
let mut r = d.read_info().unwrap();
let mut buf = vec![0; r.output_buffer_size()];
let info = r.next_frame(&mut buf).unwrap();
buf.truncate(info.buffer_size());
buf
};
let serial = encode(1);
assert_eq!(decode(&serial), px, "serial path must be lossless");
for threads in [2usize, 4, 8] {
let par = encode(threads);
assert_eq!(
decode(&par),
px,
"threads={threads}: parallel encode lost pixels"
);
let blocks = super::block_count((w * ch + 1) * h, threads);
assert!(blocks > 1, "threads={threads}: expected a split, got {blocks}");
}
}
}