mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-11 00:11:46 +00:00
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:
1 parent
cd485b6c2c
commit
053f0b7198
7 files changed
+66
-11
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
+42
-1
@@ -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}");
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user