Integration: level 3 instead of fast() under zlib-rs, io test into tests/it (#7689)

zlib-rs maps level 1 to deflate_quick, which writes static Huffman blocks
only, so every Compression::fast() caller (PDF export and print, brush
presets, PNG Fast) wrote 5-32% bigger streams than miniz's level 1 did.
Level 3 is the lowest level that is never bigger than miniz level 1 on the
docs/images photos, raw and Up-filtered (level 2 was +0.9% on one); it is
0-16% smaller and takes 1.2-3.4 times as long. The new test exports a
noisy-gradient RGB document as PDF and checks the image stream against
miniz level 1 of the same samples: 603007 vs 547315 bytes with fast(),
passes with level 3. The PR's doc no longer says the levels are unchanged.

Main moved the io integration tests into crates/io/tests/it/, so the new
deflate_compatibility.rs moves there and uses crate::common.

Co-authored-by: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed 2026-10-27 02:12:13 +01:00
1 parent cd485b6c2c
commit 053f0b7198
7 files changed
+66 -11

No files matched your search

+9 -4
View File
@@ -419,7 +419,7 @@ fn adam7_idat(data: &[u8], w: usize, h: usize, bpp: usize, level: PngCompression
}
let lvl = match level {
PngCompression::None => flate2::Compression::none(),
PngCompression::Fast => flate2::Compression::fast(),
PngCompression::Fast => FAST,
PngCompression::Default => flate2::Compression::default(),
PngCompression::Best => flate2::Compression::best(),
};
@@ -428,6 +428,11 @@ fn adam7_idat(data: &[u8], w: usize, h: usize, bpp: usize, level: PngCompression
z.finish().map_err(|e| CodecError::encode(F, e))
}
/// The deflate level of [`PngCompression::Fast`]: zlib-rs level 1 writes static Huffman blocks
/// only (20-30% bigger on filtered photos); level 3 is the lowest that is never bigger than
/// miniz's level 1 was, at about twice its time.
const FAST: flate2::Compression = flate2::Compression::new(3);
/// Images at least this large (bytes) are filtered and deflated on all cores.
const PARALLEL_MIN_BYTES: usize = 4 << 20;
@@ -456,7 +461,7 @@ fn banded_idat(data: &[u8], w: usize, h: usize, bpp: usize, level: PngCompressio
let band_rows = (BAND_BYTES / stride.saturating_add(1)).max(1);
let bands: Vec<(usize, usize)> = (0..h).step_by(band_rows).map(|y| (y, (y + band_rows).min(h))).collect();
let lvl = match level {
PngCompression::Fast => flate2::Compression::fast(),
PngCompression::Fast => FAST,
PngCompression::Best => flate2::Compression::best(),
_ => flate2::Compression::default(),
};
@@ -494,7 +499,7 @@ fn banded_idat(data: &[u8], w: usize, h: usize, bpp: usize, level: PngCompressio
let mut out = Vec::with_capacity(parts.iter().map(|p| p.1.len()).sum::<usize>() + 6);
// zlib header: deflate, 32 K window; FLEVEL only advertises the effort.
out.extend_from_slice(match level {
PngCompression::Fast => &[0x78, 0x01],
PngCompression::Fast => &[0x78, 0x5E],
PngCompression::Best => &[0x78, 0xDA],
_ => &[0x78, 0x9C],
});
@@ -509,7 +514,7 @@ fn banded_idat(data: &[u8], w: usize, h: usize, bpp: usize, level: PngCompressio
/// Raw deflate of one band: ends on a sync flush, or finishes the stream for the `last` band.
/// The output buffer is sized past deflate's worst case so that one call takes the whole band
/// and completes the flush: when a sync flush fills the buffer, miniz_oxide (flate2's Rust
/// and completes the flush: when a sync flush fills the buffer, miniz_oxide (flate2's former Rust
/// backend) can return before compressing the last lookahead bytes, and the next call only
/// drains the pending output, so a resumed flush silently drops data. `None` (the caller then
/// uses the serial encoder) if the single call did not finish.
+2 -1
View File
@@ -254,7 +254,8 @@ pub fn encode_tip(t: &GrayTile) -> Vec<u8> {
out.push(if eight { 8 } else { 16 });
out.extend_from_slice(&t.width.to_le_bytes());
out.extend_from_slice(&t.height.to_le_bytes());
let mut z = flate2::write::ZlibEncoder::new(out, flate2::Compression::fast());
// Level 3: zlib-rs level 1 writes static Huffman blocks only, 13-30% bigger than miniz's level 1.
let mut z = flate2::write::ZlibEncoder::new(out, flate2::Compression::new(3));
let mut buf = Vec::with_capacity(8192);
let mut ok = true;
for chunk in t.data.chunks(4096) {
+4 -2
View File
@@ -341,8 +341,10 @@ fn pdf_string(s: &str) -> String {
}
fn deflate(data: &[u8]) -> Vec<u8> {
// Fast: print PDFs are transient and large; level 1 is ~4× quicker than the default.
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
// Fast: print PDFs are transient and large. Level 3, not 1: zlib-rs level 1 writes static
// Huffman blocks only (13-30% bigger on photos); level 3 is the lowest that is never bigger
// than miniz's level 1 was, at about twice its time.
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::new(3));
let _ = e.write_all(data);
e.finish().unwrap_or_default()
}
+4 -2
View File
@@ -59,8 +59,10 @@ fn pdf_string(s: &str) -> String {
}
fn deflate(data: &[u8]) -> Vec<u8> {
// Fast: print PDFs are transient and large; level 1 is ~4× quicker than the default.
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
// Fast: print PDFs are transient and large. Level 3, not 1: zlib-rs level 1 writes static
// Huffman blocks only (13-30% bigger on photos); level 3 is the lowest that is never bigger
// than miniz's level 1 was, at about twice its time.
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::new(3));
let _ = e.write_all(data);
e.finish().unwrap_or_default()
}
@@ -1,5 +1,5 @@
//! Backend compatibility, including native-format tile data rather than ZIP assembly alone.
mod common;
use crate::common;
use std::io::Read;
@@ -102,3 +102,44 @@ fn png_psd_and_pcraft_decode_identically() {
check_png(&read_thumbnail(&bytes).unwrap().unwrap(), &image);
}
}
/// PDF export writes its image at the old backend's size or smaller: zlib-rs level 1
/// (`Compression::fast()`) writes static Huffman blocks only, which made PDFs of photos
/// 5-30% bigger than miniz's level 1 did.
#[test]
fn pdf_image_stream_is_no_bigger_than_miniz_fast() {
let (w, h) = (512u32, 384u32);
let mut doc = Document::new("photo", Size::new(w, h), ColorMode::Rgb, SampleType::U8);
// A noisy gradient: smooth like a photo, with sensor-like grain.
let mut x32 = 0x9E37_79B9u32;
let mut samples = Vec::with_capacity((w * h * 3) as usize);
let mut vals = Vec::with_capacity((w * h * 4) as usize);
for y in 0..h {
for x in 0..w {
for c in 0..3 {
x32 ^= x32 << 13;
x32 ^= x32 >> 17;
x32 ^= x32 << 5;
let base = (x * 160 / w + y * 80 / h + c * 20) as i32;
let v = (base + (x32 % 9) as i32 - 4).clamp(0, 255) as u8;
samples.push(v);
vals.push(common::g(v));
}
vals.push(1.0);
}
}
let mut surface = photocraft_raster::Surface::new(doc.pixel_format());
surface.write_region(doc.bounds(), &vals);
doc.layers.push(Layer::new("photo", LayerContent::Raster(surface)));
let pdf = photocraft_io::export(&doc, "photo.pdf", &Default::default()).unwrap().bytes;
let at = pdf.windows(15).position(|s| s == b"/Subtype /Image").unwrap();
let dict = &pdf[at..];
let len_at = dict.windows(8).position(|s| s == b"/Length ").unwrap() + 8;
let digits = dict[len_at..].iter().take_while(|b| b.is_ascii_digit()).count();
let len: usize = std::str::from_utf8(&dict[len_at..len_at + digits]).unwrap().parse().unwrap();
let start = dict.windows(7).position(|s| s == b"stream\n").unwrap() + 7;
let stream = &dict[start..start + len];
assert_eq!(miniz_oxide::inflate::decompress_to_vec_zlib(stream).unwrap(), samples);
let old = miniz_oxide::deflate::compress_to_vec_zlib(&samples, 1).len();
assert!(len <= old, "PDF image stream {len} bytes, miniz level 1 wrote {old}");
}
+1
View File
@@ -20,6 +20,7 @@ mod comps_artboards;
mod corpus;
mod corpus_regressions;
mod cylinder_warp;
mod deflate_compatibility;
mod effects_multi;
mod exif_resolution;
mod export_progress;
+4 -1
View File
@@ -5,7 +5,10 @@ Replace the workspace flate2 Rust backend with zlib-rs and runtime CPU detection
Size, import-speed and dependency tradeoffs remain maintainer decisions.
Some PSD ZIP files grow by up to 0.71%. The macOS 16-bit city PSD import is about 6% slower in confirmation.
The small web-layout PNG grows by 598 bytes (+1.40%). One .pcraft preview adds 25 bytes.
Real photo PNGs become smaller and encode faster. Compression levels are unchanged.
Real photo PNGs become smaller and encode faster. The default and best levels are unchanged; callers that
used level 1 (PDF export and print, brush tips, PNG Fast) now use level 3, because zlib-rs level 1 writes
static Huffman blocks only (13-32% bigger on photos). Level 3 is never bigger than miniz's level 1 on the
docs/images photos (0-16% smaller) and takes 1.2-3.4 times as long.
There is no native-format tile speedup from deflate.
The timings below measure only the backend switch. They exclude the later ZIP allocation, validation and slice decoder changes.