- work around a bioformats issue
Cargo Test / Cargo Test (push) Successful in 2m25s
PyTest / pytest (3.10) (push) Successful in 1m18s
PyTest / pytest (3.12) (push) Successful in 56s
PyTest / pytest (3.14) (push) Successful in 57s

This commit is contained in:
w.pomp
2026-06-16 14:25:26 +02:00
parent 998b24e7af
commit b3078dd915
9 changed files with 56 additions and 50 deletions
+3 -2
View File
@@ -1,9 +1,10 @@
name: PyTest
name: Cargo Test
on: [push, pull_request, workflow_call]
jobs:
pytest:
cargo_test:
name: Cargo Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
+1
View File
@@ -9,3 +9,4 @@
*.tif
*.so
__pycache__
.agentbridge/
+4 -4
View File
@@ -1,13 +1,13 @@
[package]
name = "tiffwrite"
version = "2026.5.1"
version = "2026.6.0"
edition = "2024"
rust-version = "1.88.0"
authors = ["Wim Pomp <w.pomp@nki.nl>"]
license = "MIT OR Apache-2.0"
description = "Write BioFormats/ImageJ compatible tiffs with zstd compression in parallel."
homepage = "https://git.wimpomp.nl/wim/tiffwrite"
repository = "https://git.wimpomp.nl/wim/tiffwrite"
homepage = "https://git.pomppervova.nl/wim/tiffwrite"
repository = "https://git.pomppervova.nl/wim/tiffwrite"
documentation = "https://docs.rs/tiffwrite"
readme = "README.md"
keywords = ["bioformats", "tiff", "ndarray", "zstd", "fiji"]
@@ -28,7 +28,7 @@ lazy_static = "1"
ndarray = "0.17"
num = "0.4"
numpy = { version = "0.28", optional = true }
pyo3 = { version = "0.28", features = ["abi3-py310", "eyre", "generate-import-lib", "multiple-pymethods"], optional = true }
pyo3 = { version = "0.28", features = ["abi3-py310", "eyre", "multiple-pymethods"], optional = true }
pyo3-stub-gen = { version = "0.22", optional = true }
rayon = "1"
thiserror = "2"
+3 -3
View File
@@ -1,5 +1,5 @@
[![pytest](https://git.wimpomp.nl/wim/tiffwrite/actions/workflows/pytest.yml/badge.svg)](https://git.wimpomp.nl/wim/tiffwrite/actions?workflow=pytest.yml)
[![cargo test](https://git.wimpomp.nl/wim/tiffwrite/actions/workflows/cargo_test.yml/badge.svg)](https://git.wimpomp.nl/wim/tiffwrite/actions?workflow=cargo_test.yml)
[![pytest](https://git.pomppervova.nl/wim/tiffwrite/actions/workflows/pytest.yml/badge.svg)](https://git.pomppervova.nl/wim/tiffwrite/actions?workflow=pytest.yml)
[![cargo test](https://git.pomppervova.nl/wim/tiffwrite/actions/workflows/cargo_test.yml/badge.svg)](https://git.pomppervova.nl/wim/tiffwrite/actions?workflow=cargo_test.yml)
# Tiffwrite
Write BioFormats/ImageJ compatible tiffs with zstd compression in parallel using Rust.
@@ -22,7 +22,7 @@ makes that very hard anyway.
or
- install [rust](https://rustup.rs/)
- ``` pip install tiffwrite@git+https://git.wimpomp.nl/wim/tiffwrite ```
- ``` pip install tiffwrite@git+https://git.pomppervova.nl/wim/tiffwrite ```
## Usage
### Write an image stack
+2 -3
View File
@@ -57,7 +57,7 @@ class IJTiffFile(rs.IJTiffFile):
def __init__(
self,
path: str | Path,
path: str | Path, # noqa
*,
dtype: DTypeLike = "uint16",
colors: Sequence[str] = None,
@@ -79,7 +79,6 @@ class IJTiffFile(rs.IJTiffFile):
if colors is not None and colormap is not None:
warn("Cannot have colors and colormap simultaneously.", TiffWriteWarning, stacklevel=2)
self.path = Path(path)
self.dtype = np.dtype(dtype)
if compression is not None:
if isinstance(compression, tuple):
@@ -140,7 +139,7 @@ class IJTiffFile(rs.IJTiffFile):
case np.float64:
self.save_f64(frame, c, z, t)
case _:
raise TypeError(f"Cannot save type {self.dtype}")
raise TypeError(f"Cannot save type {self.dtype}") # noqa
if extratags is not None:
for extra_tag in extratags:
self.append_extra_tag(extra_tag, (c, z, t))
+10 -7
View File
@@ -2,6 +2,7 @@
# ruff: noqa: E501, F401, F403, F405
import builtins
import pathlib
import typing
import numpy
@@ -13,6 +14,8 @@ __all__ = [
]
class IJTiffFile:
@property
def path(self) -> typing.Optional[pathlib.Path]: ...
@property
def colors(self) -> typing.Optional[builtins.list[builtins.list[builtins.int]]]: ...
@colors.setter
@@ -49,16 +52,16 @@ class IJTiffFile:
self, czt: typing.Optional[tuple[builtins.int, builtins.int, builtins.int]] = None
) -> builtins.list[Tag]: ...
def close(self) -> None: ...
def save_f64(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
def save_u32(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
def save_u16(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
def save_i64(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
def save_f32(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
def save_f64(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
def save_i64(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
def save_u16(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
def save_u32(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
def save_u8(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
def save_i32(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
def save_u64(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
def save_i16(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
def save_i8(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
def save_u64(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
def save_i32(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
def save_i16(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ...
class Tag:
@staticmethod
+1 -1
View File
@@ -34,7 +34,7 @@ tiffwrite_generate_stub = "tiffwrite:tiffwrite_generate_stub"
[tool.maturin]
python-source = "py"
features = ["pyo3/extension-module", "python"]
features = ["python"]
module-name = "tiffwrite.tiffwrite_rs"
[tool.isort]
+20 -5
View File
@@ -16,7 +16,7 @@ use std::collections::HashSet;
use std::fs::{File, OpenOptions};
use std::hash::{DefaultHasher, Hash, Hasher};
use std::io::{BufWriter, Read, Seek, SeekFrom, Write};
use std::path::Path;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::thread::available_parallelism;
use std::{cmp::Ordering, collections::HashMap};
@@ -356,9 +356,6 @@ impl Tag {
self.offset + (TAG_SIZE - OFFSET_SIZE) as u64,
))?;
file.write_all(&offset.to_le_bytes())?;
if file.stream_position()? % 2 == 1 {
file.write_all(&[0])?;
}
}
Ok(())
}
@@ -560,9 +557,20 @@ impl Frame {
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 = Frame::encode(encoder, frame, slice, tile_width, tile_length)?;
encoder.finish()?;
// work around https://github.com/ome/bioformats/issues/4442
if dest.len() == frame.len() {
dest.clear();
let mut encoder = Encoder::new(&mut dest, compression_level)?;
encoder.include_contentsize(true)?;
encoder
.set_pledged_src_size(Some((bytes_per_sample * tile_width * tile_length) as u64))?;
encoder.include_checksum(false)?;
encoder = Frame::encode(encoder, frame, slice, tile_width, tile_length)?;
encoder.finish()?;
}
Ok(dest)
}
}
@@ -669,6 +677,7 @@ pub struct IJTiffFile {
frames: HashMap<(usize, usize, usize), Frame>,
hashes: Arc<Mutex<HashMap<u64, u64>>>,
threads: HashMap<(usize, usize, usize), JoinHandle<Result<Frame, Error>>>,
path: PathBuf,
/// zstd: -7 ..= 22
pub compression: Compression,
pub colors: Colors,
@@ -695,6 +704,7 @@ impl IJTiffFile {
/// create new tifffile from path, use its save() method to save frames
/// the file is finalized when it goes out of scope
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let path = path.as_ref();
let mut file = OpenOptions::new()
.create(true)
.truncate(true)
@@ -711,6 +721,7 @@ impl IJTiffFile {
frames: HashMap::new(),
hashes: Arc::new(Mutex::new(HashMap::new())),
threads: HashMap::new(),
path: path.to_owned(),
compression: Compression::Zstd(DEFAULT_COMPRESSION_LEVEL),
colors: Colors::None,
comment: None,
@@ -854,6 +865,7 @@ impl IJTiffFile {
}
fn hash_check(f: &mut BufWriter<File>, bytes: &Vec<u8>, offset: u64) -> Result<bool, Error> {
f.flush()?;
let current_offset = f.stream_position()?;
f.seek(SeekFrom::Start(offset))?;
let mut buffer = vec![0; bytes.len()];
@@ -1054,7 +1066,10 @@ impl IJTiffFile {
warn.push((frame_number, 0));
}
if !warn.is_empty() {
println!("The following frames were not added to the tif file:");
println!(
"The following frames were not added to the tif file: {}",
self.path.display()
);
for (frame_number, channel) in &warn {
let (c, z, t) = self.get_czt(*frame_number, *channel, c_size, z_size);
println!("c: {c}, z: {z}, t: {t}")
+11 -24
View File
@@ -184,6 +184,11 @@ impl PyIJTiffFile {
})
}
#[getter]
fn get_path(&self) -> PyResult<Option<PathBuf>> {
Ok(self.ijtifffile.as_ref().map(|f| f.path.clone()))
}
/// set zstd compression level: -7 ..= 22
fn set_compression(&mut self, compression: i32, level: i32) -> PyResult<()> {
let c = match compression {
@@ -240,11 +245,7 @@ impl PyIJTiffFile {
#[getter]
fn get_px_size(&self) -> PyResult<Option<f64>> {
if let Some(ijtifffile) = &self.ijtifffile {
Ok(ijtifffile.px_size)
} else {
Ok(None)
}
Ok(self.ijtifffile.as_ref().and_then(|f| f.px_size))
}
#[setter]
@@ -257,11 +258,7 @@ impl PyIJTiffFile {
#[getter]
fn get_delta_z(&self) -> PyResult<Option<f64>> {
if let Some(ijtifffile) = &self.ijtifffile {
Ok(ijtifffile.delta_z)
} else {
Ok(None)
}
Ok(self.ijtifffile.as_ref().and_then(|f| f.delta_z))
}
#[setter]
@@ -274,11 +271,7 @@ impl PyIJTiffFile {
#[getter]
fn get_time_interval(&self) -> PyResult<Option<f64>> {
if let Some(ijtifffile) = &self.ijtifffile {
Ok(ijtifffile.time_interval)
} else {
Ok(None)
}
Ok(self.ijtifffile.as_ref().and_then(|f| f.time_interval))
}
#[setter]
@@ -291,11 +284,7 @@ impl PyIJTiffFile {
#[getter]
fn get_comment(&self) -> PyResult<Option<String>> {
if let Some(ijtifffile) = &self.ijtifffile {
Ok(ijtifffile.comment.clone())
} else {
Ok(None)
}
Ok(self.ijtifffile.as_ref().and_then(|f| f.comment.clone()))
}
#[setter]
@@ -308,10 +297,8 @@ impl PyIJTiffFile {
#[pyo3(signature = (tag, czt=None))]
fn append_extra_tag(&mut self, tag: PyTag, czt: Option<(usize, usize, usize)>) {
if let Some(ijtifffile) = self.ijtifffile.as_mut()
&& let Some(extra_tags) = ijtifffile.extra_tags.get_mut(&czt)
{
extra_tags.push(tag.tag)
if let Some(ijtifffile) = self.ijtifffile.as_mut() {
ijtifffile.extra_tags.entry(czt).or_default().push(tag.tag);
}
}