- work around a bioformats issue
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -9,3 +9,4 @@
|
||||
*.tif
|
||||
*.so
|
||||
__pycache__
|
||||
.agentbridge/
|
||||
|
||||
+4
-4
@@ -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"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[](https://git.wimpomp.nl/wim/tiffwrite/actions?workflow=pytest.yml)
|
||||
[](https://git.wimpomp.nl/wim/tiffwrite/actions?workflow=cargo_test.yml)
|
||||
[](https://git.pomppervova.nl/wim/tiffwrite/actions?workflow=pytest.yml)
|
||||
[](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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
@@ -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}")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user