From 7fdfb7c9dc8ffdcbd8ac21329a4538814efbdd53 Mon Sep 17 00:00:00 2001 From: Wim Pomp Date: Mon, 23 Dec 2024 15:29:08 +0100 Subject: [PATCH] - bugfix: make sure all bytes are processed by the zstd encoder - bump dependencies --- .github/workflows/pytest.yml | 2 +- Cargo.toml | 10 ++++----- src/lib.rs | 42 ++++++++++++++++++++++++++++++------ src/py.rs | 2 ++ 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index aee5b1d..b066019 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,6 +1,6 @@ name: PyTest -on: workflow_call +on: [push, pull_request, workflow_call] jobs: pytest: diff --git a/Cargo.toml b/Cargo.toml index e8f4f7c..2525287 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tiffwrite" -version = "2024.11.0" +version = "2024.12.0" edition = "2021" authors = ["Wim Pomp "] license = "GPL-3.0-or-later" @@ -14,16 +14,16 @@ name = "tiffwrite" crate-type = ["cdylib", "rlib"] [dependencies] -anyhow = "1.0.89" -chrono = "0.4.38" +anyhow = "1.0.95" +chrono = "0.4.39" ndarray = "0.16.1" num = "0.4.3" rayon = "1.10.0" zstd = "0.13.2" -numpy = { version = "0.22.0", optional = true } +numpy = { version = "0.23.0", optional = true } [dependencies.pyo3] -version = "0.22.5" +version = "0.23.3" features = ["extension-module", "abi3-py310", "generate-import-lib", "anyhow", "multiple-pymethods"] optional = true diff --git a/src/lib.rs b/src/lib.rs index c3a1558..9a1c4cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,12 +23,14 @@ const OFFSET_SIZE: usize = 8; const OFFSET: u64 = 16; const COMPRESSION: u16 = 50000; +/// Image File Directory #[derive(Clone, Debug)] struct IFD { tags: HashSet, } impl IFD { + /// new IFD with empty set of tags pub fn new() -> Self { IFD { tags: HashSet::new(), @@ -61,6 +63,7 @@ impl IFD { } } +/// Tiff tag, use one of the constructors to get a tag of a specific type #[derive(Clone, Debug, Eq)] pub struct Tag { code: u16, @@ -317,6 +320,7 @@ impl Tag { } } + /// get the number of values in the tag pub fn count(&self) -> u64 { let c = match self.ttype { 1 => self.bytes.len(), // BYTE @@ -478,6 +482,16 @@ impl CompressedFrame { } } + /// loop until all bytes are encoded + fn write(encoder: &mut Encoder<&mut Vec>, buf: &[u8]) -> Result<()> { + let b = buf.len(); + let mut w = 0; + while w < b { + w += encoder.write(&buf[w..])?; + } + Ok(()) + } + fn compress_tile( frame: ArcArray2, slice: (usize, usize, usize, usize), @@ -495,7 +509,8 @@ impl CompressedFrame { encoder.set_pledged_src_size(Some((bytes_per_sample * tile_width * tile_length) as u64))?; let shape = (slice.1 - slice.0, slice.3 - slice.2); for i in 0..shape.0 { - encoder.write( + CompressedFrame::write( + &mut encoder, &frame .slice(s![slice.0..slice.1, slice.2..slice.3]) .slice(s![i, ..]) @@ -504,12 +519,15 @@ impl CompressedFrame { .flatten() .collect::>(), )?; - encoder.write(&vec![0u8; bytes_per_sample * (tile_width - shape.1)])?; + CompressedFrame::write( + &mut encoder, + &vec![0u8; bytes_per_sample * (tile_width - shape.1)], + )?; } - encoder.write(&vec![ - 0u8; - bytes_per_sample * tile_width * (tile_length - shape.0) - ])?; + CompressedFrame::write( + &mut encoder, + &vec![0u8; bytes_per_sample * tile_width * (tile_length - shape.0)], + )?; encoder.finish()?; Ok(dest) } @@ -551,6 +569,7 @@ impl Frame { } } +/// trait to convert numbers to bytes pub trait Bytes { const BITS_PER_SAMPLE: u16; const SAMPLE_FORMAT: u16; @@ -593,6 +612,7 @@ bytes_impl!(isize, 32, 2); bytes_impl!(f32, 32, 3); bytes_impl!(f64, 64, 3); +/// what colormap to save in the tiff; None, Colors: gradient from black to color, or full Colormap #[derive(Clone, Debug)] pub enum Colors { None, @@ -600,18 +620,24 @@ pub enum Colors { Colormap(Vec>), } +/// save 2d arrays in a tif file compatible with Fiji/ImageJ #[derive(Debug)] pub struct IJTiffFile { file: File, frames: HashMap<(usize, usize, usize), Frame>, hashes: HashMap, threads: HashMap<(usize, usize, usize), JoinHandle>, + /// zstd: -7 ..= 22 pub compression_level: i32, pub colors: Colors, pub comment: Option, + /// um per pixel pub px_size: Option, + /// um per slice pub delta_z: Option, + /// s per frame pub time_interval: Option, + /// extra tags; per frame: key = Some((c, z, t)), global: key = None pub extra_tags: HashMap, Vec>, } @@ -624,7 +650,8 @@ impl Drop for IJTiffFile { } impl IJTiffFile { - /// create new tifffile from path string + /// create new tifffile from path string, use it's save() method to save frames + /// the file is finalized when it goes out of scope pub fn new(path: &str) -> Result { let mut file = OpenOptions::new() .create(true) @@ -746,6 +773,7 @@ impl IJTiffFile { } } + /// save a 2d array to the tiff file at channel c, slice z, and time t pub fn save<'a, A, T>(&mut self, frame: A, c: usize, z: usize, t: usize) -> Result<()> where A: AsArray<'a, T, Ix2>, diff --git a/src/py.rs b/src/py.rs index 833b581..42d88fd 100644 --- a/src/py.rs +++ b/src/py.rs @@ -11,6 +11,7 @@ struct PyTag { tag: Tag, } +/// Tiff tag, use one of the constructors to get a tag of a specific type #[pymethods] impl PyTag { #[staticmethod] @@ -150,6 +151,7 @@ impl PyTag { } } + /// get the number of values in the tag fn count(&self) -> u64 { self.tag.count() }