- add deflate compression

This commit is contained in:
Wim Pomp
2025-03-27 15:32:31 +01:00
parent 9ee8fc1029
commit 12d5d3355b
4 changed files with 161 additions and 57 deletions

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "tiffwrite" name = "tiffwrite"
version = "2025.2.0" version = "2025.3.0"
edition = "2021" edition = "2021"
authors = ["Wim Pomp <w.pomp@nki.nl>"] authors = ["Wim Pomp <w.pomp@nki.nl>"]
license = "MIT" license = "MIT"
@@ -16,16 +16,17 @@ name = "tiffwrite"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
anyhow = "1.0.95" anyhow = "1.0.97"
chrono = "0.4.39" chrono = "0.4.40"
flate2 = "1.1.0"
ndarray = "0.16.1" ndarray = "0.16.1"
num = "0.4.3" num = "0.4.3"
rayon = "1.10.0" rayon = "1.10.0"
zstd = "0.13.2" zstd = "0.13.3"
numpy = { version = "0.23.0", optional = true } numpy = { version = "0.24.0", optional = true }
[dependencies.pyo3] [dependencies.pyo3]
version = "0.23.4" version = "0.24.0"
features = ["extension-module", "abi3-py310", "generate-import-lib", "anyhow", "multiple-pymethods"] features = ["extension-module", "abi3-py310", "generate-import-lib", "anyhow", "multiple-pymethods"]
optional = true optional = true

View File

@@ -48,7 +48,7 @@ class IJTiffFile(rs.IJTiffFile):
pxsize: pixel size in um pxsize: pixel size in um
deltaz: z slice interval in um deltaz: z slice interval in um
timeinterval: time between frames in seconds timeinterval: time between frames in seconds
compression: zstd compression level: -7 to 22. compression: ('zstd', level) for zstd with compression level: -7 to 22, 'deflate' for deflate compresion
comment: comment to be saved in tif comment: comment to be saved in tif
extratags: other tags to be saved, example: (Tag.ascii(315, 'John Doe'), Tag.bytes(4567, [400, 500]) extratags: other tags to be saved, example: (Tag.ascii(315, 'John Doe'), Tag.bytes(4567, [400, 500])
or (Tag.ascii(33432, 'Made by me'),). or (Tag.ascii(33432, 'Made by me'),).
@@ -58,14 +58,18 @@ class IJTiffFile(rs.IJTiffFile):
def __init__(self, path: str | Path, *, dtype: DTypeLike = 'uint16', def __init__(self, path: str | Path, *, dtype: DTypeLike = 'uint16',
colors: Sequence[str] = None, colormap: str = None, pxsize: float = None, colors: Sequence[str] = None, colormap: str = None, pxsize: float = None,
deltaz: float = None, timeinterval: float = None, compression: int = None, comment: str = None, deltaz: float = None, timeinterval: float = None,
compression: int | str | tuple[int, int] | tuple[str, int] = None, comment: str = None,
extratags: Sequence[Tag] = None) -> None: extratags: Sequence[Tag] = None) -> None:
codecs = {'z': 50000, 'd': 8, 8: 8, 50000: 50000}
self.path = Path(path) self.path = Path(path)
self.dtype = np.dtype(dtype) self.dtype = np.dtype(dtype)
if compression is not None: if compression is not None:
if isinstance(compression, Sequence): if isinstance(compression, tuple):
compression = compression[-1] compression = codecs.get(compression[0], 50000), (int(compression[1]) if len(compression) == 2 else 22)
self.set_compression_level(compression) else:
compression = codecs.get(compression, 50000), 22
self.set_compression(*compression)
if colors is not None: if colors is not None:
self.colors = np.array([get_color(color) for color in colors]) self.colors = np.array([get_color(color) for color in colors])
if colormap is not None: if colormap is not None:

View File

@@ -3,6 +3,7 @@ mod py;
use anyhow::Result; use anyhow::Result;
use chrono::Utc; use chrono::Utc;
use flate2::write::ZlibEncoder;
use ndarray::{s, ArcArray2, AsArray, Ix2}; use ndarray::{s, ArcArray2, AsArray, Ix2};
use num::{traits::ToBytes, Complex, FromPrimitive, Rational32}; use num::{traits::ToBytes, Complex, FromPrimitive, Rational32};
use rayon::prelude::*; use rayon::prelude::*;
@@ -16,12 +17,28 @@ use std::{
thread, thread,
thread::{sleep, JoinHandle}, thread::{sleep, JoinHandle},
}; };
use zstd::zstd_safe::CompressionLevel;
use zstd::{stream::Encoder, DEFAULT_COMPRESSION_LEVEL}; use zstd::{stream::Encoder, DEFAULT_COMPRESSION_LEVEL};
const TAG_SIZE: usize = 20; const TAG_SIZE: usize = 20;
const OFFSET_SIZE: usize = 8; const OFFSET_SIZE: usize = 8;
const OFFSET: u64 = 16; const OFFSET: u64 = 16;
const COMPRESSION: u16 = 50000;
/// Compression: deflate or zstd
#[derive(Clone, Debug)]
pub enum Compression {
Deflate,
Zstd(CompressionLevel),
}
impl Compression {
fn index(&self) -> u16 {
match self {
Compression::Deflate => 8,
Compression::Zstd(_) => 50000,
}
}
}
/// Image File Directory /// Image File Directory
#[allow(clippy::upper_case_acronyms)] #[allow(clippy::upper_case_acronyms)]
@@ -345,7 +362,7 @@ struct CompressedFrame {
} }
impl CompressedFrame { impl CompressedFrame {
fn new<T>(frame: ArcArray2<T>, compression_level: i32) -> CompressedFrame fn new<T>(frame: ArcArray2<T>, compression: Compression) -> CompressedFrame
where where
T: Bytes + Send + Sync, T: Bytes + Send + Sync,
{ {
@@ -391,16 +408,17 @@ impl CompressedFrame {
} }
} }
let bytes: Vec<_> = if slices.len() > 4 { let bytes: Vec<_> = match compression {
Compression::Deflate => {
if slices.len() > 4 {
slices slices
.into_par_iter() .into_par_iter()
.map(|slice| { .map(|slice| {
CompressedFrame::compress_tile( CompressedFrame::compress_tile_deflate(
frame.clone(), frame.clone(),
slice, slice,
tile_size, tile_size,
tile_size, tile_size,
compression_level,
) )
.unwrap() .unwrap()
}) })
@@ -409,16 +427,49 @@ impl CompressedFrame {
slices slices
.into_iter() .into_iter()
.map(|slice| { .map(|slice| {
CompressedFrame::compress_tile( CompressedFrame::compress_tile_deflate(
frame.clone(), frame.clone(),
slice, slice,
tile_size, tile_size,
tile_size, tile_size,
compression_level,
) )
.unwrap() .unwrap()
}) })
.collect() .collect()
}
}
Compression::Zstd(level) => {
if slices.len() > 4 {
slices
.into_par_iter()
.map(|slice| {
CompressedFrame::compress_tile_zstd(
frame.clone(),
slice,
tile_size,
tile_size,
level,
)
.unwrap()
})
.collect()
} else {
slices
.into_iter()
.map(|slice| {
CompressedFrame::compress_tile_zstd(
frame.clone(),
slice,
tile_size,
tile_size,
level,
)
.unwrap()
})
.collect()
}
}
}; };
CompressedFrame { CompressedFrame {
@@ -432,22 +483,18 @@ impl CompressedFrame {
} }
} }
fn compress_tile<T>( fn encode<W, T>(
mut encoder: W,
frame: ArcArray2<T>, frame: ArcArray2<T>,
slice: (usize, usize, usize, usize), slice: (usize, usize, usize, usize),
tile_width: usize, tile_width: usize,
tile_length: usize, tile_length: usize,
compression_level: i32, ) -> Result<W>
) -> Result<Vec<u8>>
where where
W: Write,
T: Bytes, T: Bytes,
{ {
let mut dest = Vec::new();
let mut encoder = Encoder::new(&mut dest, compression_level)?;
let bytes_per_sample = (T::BITS_PER_SAMPLE / 8) as usize; let bytes_per_sample = (T::BITS_PER_SAMPLE / 8) as usize;
encoder.include_contentsize(true)?;
encoder.set_pledged_src_size(Some((bytes_per_sample * tile_width * tile_length) as u64))?;
encoder.include_checksum(true)?;
let shape = (slice.1 - slice.0, slice.3 - slice.2); let shape = (slice.1 - slice.0, slice.3 - slice.2);
for i in 0..shape.0 { for i in 0..shape.0 {
encoder.write_all( encoder.write_all(
@@ -465,6 +512,40 @@ impl CompressedFrame {
0; 0;
bytes_per_sample * tile_width * (tile_length - shape.0) bytes_per_sample * tile_width * (tile_length - shape.0)
])?; ])?;
Ok(encoder)
}
fn compress_tile_deflate<T>(
frame: ArcArray2<T>,
slice: (usize, usize, usize, usize),
tile_width: usize,
tile_length: usize,
) -> Result<Vec<u8>>
where
T: Bytes,
{
let mut encoder = ZlibEncoder::new(Vec::new(), flate2::Compression::default());
encoder = CompressedFrame::encode(encoder, frame, slice, tile_width, tile_length)?;
Ok(encoder.finish()?)
}
fn compress_tile_zstd<T>(
frame: ArcArray2<T>,
slice: (usize, usize, usize, usize),
tile_width: usize,
tile_length: usize,
compression_level: i32,
) -> Result<Vec<u8>>
where
T: Bytes,
{
let mut dest = Vec::new();
let mut encoder = Encoder::new(&mut dest, compression_level)?;
let bytes_per_sample = (T::BITS_PER_SAMPLE / 8) as usize;
encoder.include_contentsize(true)?;
encoder.set_pledged_src_size(Some((bytes_per_sample * tile_width * tile_length) as u64))?;
encoder.include_checksum(true)?;
encoder = CompressedFrame::encode(encoder, frame, slice, tile_width, tile_length)?;
encoder.finish()?; encoder.finish()?;
Ok(dest) Ok(dest)
} }
@@ -569,7 +650,7 @@ pub struct IJTiffFile {
hashes: HashMap<u64, u64>, hashes: HashMap<u64, u64>,
threads: HashMap<(usize, usize, usize), JoinHandle<CompressedFrame>>, threads: HashMap<(usize, usize, usize), JoinHandle<CompressedFrame>>,
/// zstd: -7 ..= 22 /// zstd: -7 ..= 22
pub compression_level: i32, pub compression: Compression,
pub colors: Colors, pub colors: Colors,
pub comment: Option<String>, pub comment: Option<String>,
/// um per pixel /// um per pixel
@@ -610,7 +691,7 @@ impl IJTiffFile {
frames: HashMap::new(), frames: HashMap::new(),
hashes: HashMap::new(), hashes: HashMap::new(),
threads: HashMap::new(), threads: HashMap::new(),
compression_level: DEFAULT_COMPRESSION_LEVEL, compression: Compression::Zstd(DEFAULT_COMPRESSION_LEVEL),
colors: Colors::None, colors: Colors::None,
comment: None, comment: None,
px_size: None, px_size: None,
@@ -620,6 +701,11 @@ impl IJTiffFile {
}) })
} }
/// set compression: zstd(level) or deflate
pub fn set_compression(&mut self, compression: Compression) {
self.compression = compression;
}
/// to be saved in description tag (270) /// to be saved in description tag (270)
pub fn description(&self, c_size: usize, z_size: usize, t_size: usize) -> String { pub fn description(&self, c_size: usize, z_size: usize, t_size: usize) -> String {
let mut desc: String = String::from("ImageJ=1.11a"); let mut desc: String = String::from("ImageJ=1.11a");
@@ -727,11 +813,11 @@ impl IJTiffFile {
} }
sleep(Duration::from_millis(100)); sleep(Duration::from_millis(100));
} }
let compression_level = self.compression_level; let compression = self.compression.clone();
let frame = frame.into().to_shared(); let frame = frame.into().to_shared();
self.threads.insert( self.threads.insert(
(c, z, t), (c, z, t),
thread::spawn(move || CompressedFrame::new(frame, compression_level)), thread::spawn(move || CompressedFrame::new(frame, compression)),
); );
Ok(()) Ok(())
} }
@@ -833,7 +919,8 @@ impl IJTiffFile {
ifd.tags.insert(Tag::long(257, &[frame.image_length])); ifd.tags.insert(Tag::long(257, &[frame.image_length]));
ifd.tags ifd.tags
.insert(Tag::short(258, &vec![frame.bits_per_sample; frame_count])); .insert(Tag::short(258, &vec![frame.bits_per_sample; frame_count]));
ifd.tags.insert(Tag::short(259, &[COMPRESSION])); ifd.tags
.insert(Tag::short(259, &[self.compression.index()]));
ifd.tags ifd.tags
.insert(Tag::ascii(270, &self.description(c_size, z_size, t_size))); .insert(Tag::ascii(270, &self.description(c_size, z_size, t_size)));
ifd.tags.insert(Tag::short(277, &[frame_count as u16])); ifd.tags.insert(Tag::short(277, &[frame_count as u16]));

View File

@@ -1,7 +1,8 @@
use crate::{Colors, IJTiffFile, Tag}; use crate::{Colors, Compression, IJTiffFile, Tag};
use ndarray::s; use ndarray::s;
use num::{Complex, FromPrimitive, Rational32}; use num::{Complex, FromPrimitive, Rational32};
use numpy::{PyArrayMethods, PyReadonlyArray2}; use numpy::{PyArrayMethods, PyReadonlyArray2};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*; use pyo3::prelude::*;
#[pyclass(subclass)] #[pyclass(subclass)]
@@ -174,10 +175,21 @@ impl PyIJTiffFile {
} }
/// set zstd compression level: -7 ..= 22 /// set zstd compression level: -7 ..= 22
fn set_compression_level(&mut self, compression_level: i32) { fn set_compression(&mut self, compression: i32, level: i32) -> PyResult<()> {
if let Some(ref mut ijtifffile) = self.ijtifffile { let c = match compression {
ijtifffile.compression_level = compression_level.clamp(-7, 22); 50000 => Compression::Zstd(level.clamp(-7, 22)),
8 => Compression::Deflate,
_ => {
return Err(PyValueError::new_err(format!(
"Unknown compression {}",
compression
)))
} }
};
if let Some(ref mut ijtifffile) = self.ijtifffile {
ijtifffile.set_compression(c)
}
Ok(())
} }
#[getter] #[getter]