rewrite in rust

This commit is contained in:
Wim Pomp
2024-10-06 20:30:57 +02:00
parent 93d62c5345
commit 82931f7715
8 changed files with 1339 additions and 636 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@
/tiffwrite.egg-info/
/.pytest_cache/
/venv/
/target/

596
Cargo.lock generated Normal file
View File

@@ -0,0 +1,596 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "cc"
version = "1.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "crossbeam-deque"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "fraction"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7"
dependencies = [
"lazy_static",
"num",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indoc"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]]
name = "js-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "matrixmultiply"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a"
dependencies = [
"autocfg",
"rawpointer",
]
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "ndarray"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841"
dependencies = [
"matrixmultiply",
"num-complex",
"num-integer",
"num-traits",
"portable-atomic",
"portable-atomic-util",
"rawpointer",
]
[[package]]
name = "num"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "portable-atomic"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2"
[[package]]
name = "portable-atomic-util"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcdd8420072e66d54a407b3316991fe946ce3ab1083a7f575b2463866624704d"
dependencies = [
"portable-atomic",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pyo3"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15ee168e30649f7f234c3d49ef5a7a6cbf5134289bc46c29ff3155fa3221c225"
dependencies = [
"anyhow",
"cfg-if",
"indoc",
"libc",
"memoffset",
"once_cell",
"portable-atomic",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
"unindent",
]
[[package]]
name = "pyo3-build-config"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e61cef80755fe9e46bb8a0b8f20752ca7676dcc07a5277d8b7768c6172e529b3"
dependencies = [
"once_cell",
"python3-dll-a",
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67ce096073ec5405f5ee2b8b31f03a68e02aa10d5d4f565eca04acc41931fa1c"
dependencies = [
"libc",
"pyo3-build-config",
]
[[package]]
name = "pyo3-macros"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2440c6d12bc8f3ae39f1e775266fa5122fd0c8891ce7520fa6048e683ad3de28"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn",
]
[[package]]
name = "pyo3-macros-backend"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1be962f0e06da8f8465729ea2cb71a416d2257dff56cbe40a70d3e62a93ae5d1"
dependencies = [
"heck",
"proc-macro2",
"pyo3-build-config",
"quote",
"syn",
]
[[package]]
name = "python3-dll-a"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0b78171a90d808b319acfad166c4790d9e9759bbc14ac8273fe133673dd41b"
dependencies = [
"cc",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rawpointer"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "rayon"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "target-lexicon"
version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tiffwrite"
version = "2024.10.0"
dependencies = [
"anyhow",
"chrono",
"fraction",
"ndarray",
"num",
"pyo3",
"rayon",
]
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "unindent"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

21
Cargo.toml Normal file
View File

@@ -0,0 +1,21 @@
[package]
name = "tiffwrite"
version = "2024.10.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "tiffwrite"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.22.3", features = ["extension-module", "abi3-py310", "generate-import-lib", "anyhow"] }
anyhow = "1.0.89"
rayon = "1.10.0"
fraction = "0.15.3"
num = "0.4.3"
ndarray = "0.16.1"
chrono = "0.4.38"
[features]
nopython = []

View File

@@ -1,39 +1,21 @@
[tool.poetry]
[build-system]
requires = ["maturin>=1.5,<2.0"]
build-backend = "maturin"
[project]
name = "tiffwrite"
version = "2024.10.1"
description = "Parallel tiff writer compatible with ImageJ."
authors = ["Wim Pomp, Lenstra lab NKI <w.pomp@nki.nl>"]
license = "GPL-3.0-or-later"
readme = "README.md"
packages = [{include = "tiffwrite"}]
repository = "https://github.com/wimpomp/tiffwrite"
dynamic = ["version"]
authors = [{ name = "Wim Pomp", email = "w.pomp@nki.nl" }]
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
[tool.poetry.dependencies]
python = "^3.10"
tifffile = "*"
imagecodecs = "*"
numpy = "*"
tqdm = "*"
colorcet = "*"
matplotlib = "*"
parfor = ">=2024.9.2"
pytest = { version = "*", optional = true }
mypy = { version = "*", optional = true }
[tool.poetry.extras]
test = ["pytest", "mypy"]
[tool.pytest.ini_options]
filterwarnings = ["ignore:::(?!tiffwrite)"]
[tool.maturin]
features = ["pyo3/extension-module"]
module-name = "tiffwrite"
[tool.isort]
line_length = 119
[tool.mypy]
disable_error_code = ["import-untyped", "return"]
implicit_optional = true
exclude = ["build"]
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

515
src/lib.rs Normal file
View File

@@ -0,0 +1,515 @@
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use anyhow::Result;
use fraction::Fraction;
use num::{Complex, Zero};
use num::complex::ComplexFloat;
use ndarray::{s, Array2};
use num::traits::ToBytes;
use std::hash::{DefaultHasher, Hash, Hasher};
use chrono::Utc;
const TAG_SIZE: usize = 20;
const OFFSET_SIZE: usize = 8;
const OFFSET: u64 = 16;
#[derive(Clone, Debug)]
struct IFD {
tags: Vec<Tag>
}
impl IFD {
pub fn new() -> Self {
IFD { tags: Vec::new() }
}
fn push_tag(&mut self, tag: Tag) {
if !self.tags.contains(&tag) {
self.tags.push(tag);
}
}
fn extend_tags(&mut self, tags: Vec<Tag>) {
for tag in tags {
self.push_tag(tag);
}
}
fn write(&mut self, ijtifffile: &mut IJTiffFile, where_to_write_offset: u64) -> Result<u64> {
self.tags.sort();
ijtifffile.file.seek(SeekFrom::End(0))?;
if ijtifffile.file.stream_position()? % 2 == 1 {
ijtifffile.file.write(&[0])?;
}
let offset = ijtifffile.file.stream_position()?;
ijtifffile.file.write(&(self.tags.len() as u64).to_le_bytes())?;
for tag in self.tags.iter_mut() {
tag.write_tag(ijtifffile)?;
}
let where_to_write_next_ifd_offset = ijtifffile.file.stream_position()?;
ijtifffile.file.write(&vec![0u8; OFFSET_SIZE])?;
for tag in self.tags.iter() {
tag.write_data(ijtifffile)?;
}
ijtifffile.file.seek(SeekFrom::Start(where_to_write_offset))?;
ijtifffile.file.write(&offset.to_le_bytes())?;
Ok(where_to_write_next_ifd_offset)
}
}
#[derive(Clone, Debug, Eq)]
pub struct Tag {
code: u16,
bytes: Vec<u8>,
ttype: u16,
offset: u64
}
impl PartialOrd<Self> for Tag {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Tag {
fn cmp(&self, other: &Self) -> Ordering {
self.code.cmp(&other.code)
}
}
impl PartialEq for Tag {
fn eq(&self, other: &Self) -> bool {
self.code == other.code
}
}
impl Tag {
pub fn new(code: u16, bytes: Vec<u8>, ttype: u16) -> Self {
Tag { code, bytes, ttype, offset: 0 }
}
pub fn byte(code: u16, byte: Vec<u8>) -> Self {
Tag::new(code, byte, 1)
}
pub fn ascii(code: u16, ascii: &str) -> Self {
let mut bytes = ascii.as_bytes().to_vec();
bytes.push(0);
Tag::new(code, bytes, 2)
}
pub fn short(code: u16, short: Vec<u16>) -> Self {
Tag::new(code, short.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 3)
}
pub fn long(code: u16, long: Vec<u32>) -> Self {
Tag::new(code, long.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 4)
}
pub fn rational(code: u16, rational: Vec<Fraction>) -> Self {
Tag::new(code, rational.into_iter().map(|x|
u32::try_from(*x.denom().unwrap()).unwrap().to_le_bytes().into_iter().chain(
u32::try_from(*x.numer().unwrap()).unwrap().to_le_bytes()).collect::<Vec<_>>()
).flatten().collect(), 5)
}
pub fn sbyte(code: u16, sbyte: Vec<i8>) -> Self {
Tag::new(code, sbyte.iter().map(|x| x.to_le_bytes()).flatten().collect(), 6)
}
pub fn sshort(code: u16, sshort: Vec<i16>) -> Self {
Tag::new(code, sshort.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 8)
}
pub fn slong(code: u16, slong: Vec<i32>) -> Self {
Tag::new(code, slong.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 9)
}
pub fn srational(code: u16, srational: Vec<Fraction>) -> Self {
Tag::new(code, srational.into_iter().map(|x|
i32::try_from(*x.denom().unwrap()).unwrap().to_le_bytes().into_iter().chain(
i32::try_from(*x.numer().unwrap()).unwrap().to_le_bytes()).collect::<Vec<_>>()
).flatten().collect(), 10)
}
pub fn float(code: u16, float: Vec<f32>) -> Self {
Tag::new(code, float.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 11)
}
pub fn double(code: u16, double: Vec<f64>) -> Self {
Tag::new(code, double.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 12)
}
pub fn ifd(code: u16, ifd: Vec<u32>) -> Self {
Tag::new(code, ifd.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 13)
}
pub fn unicode(code: u16, unicode: &str) -> Self {
let mut bytes: Vec<u8> = unicode.encode_utf16().map(|x| x.to_le_bytes()).flatten().collect();
bytes.push(0);
Tag::new(code, bytes, 14)
}
pub fn complex(code: u16, complex: Vec<Complex<f32>>) -> Self {
Tag::new(code, complex.into_iter().map(|x|
x.re().to_le_bytes().into_iter().chain(x.im().to_le_bytes()).collect::<Vec<_>>()
).flatten().collect(), 15)
}
pub fn long8(code: u16, long8: Vec<u64>) -> Self {
Tag::new(code, long8.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 16)
}
pub fn slong8(code: u16, slong8: Vec<i64>) -> Self {
Tag::new(code, slong8.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 17)
}
pub fn ifd8(code: u16, ifd8: Vec<u64>) -> Self {
Tag::new(code, ifd8.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 18)
}
pub fn count(&self) -> u64 {
let c = match self.ttype {
1 => self.bytes.len(), // BYTE
2 => self.bytes.len(), // ASCII
3 => self.bytes.len() / 2, // SHORT
4 => self.bytes.len() / 4, // LONG
5 => self.bytes.len() / 8, // RATIONAL
6 => self.bytes.len(), // SBYTE
7 => self.bytes.len(), // UNDEFINED
8 => self.bytes.len() / 2, // SSHORT
9 => self.bytes.len() / 4, // SLONG
10 => self.bytes.len() / 8, // SRATIONAL
11 => self.bytes.len() / 4, // FLOAT
12 => self.bytes.len() / 8, // DOUBLE
13 => self.bytes.len() / 4, // IFD
14 => self.bytes.len() / 2, // UNICODE
15 => self.bytes.len() / 8, // COMPLEX
16 => self.bytes.len() / 8, // LONG8
17 => self.bytes.len() / 8, // SLONG8
18 => self.bytes.len() / 8, // IFD8
_ => self.bytes.len(),
};
c as u64
}
fn write_tag(&mut self, ijtifffile: &mut IJTiffFile) -> Result<()> {
self.offset = ijtifffile.file.stream_position()?;
ijtifffile.file.write(&self.code.to_le_bytes())?;
ijtifffile.file.write(&self.ttype.to_le_bytes())?;
ijtifffile.file.write(&self.count().to_le_bytes())?;
if self.bytes.len() <= OFFSET_SIZE {
ijtifffile.file.write(&self.bytes)?;
for _ in self.bytes.len()..OFFSET_SIZE {
ijtifffile.file.write(&[0])?;
}
} else {
ijtifffile.file.write(&vec![0u8; OFFSET_SIZE])?;
}
Ok(())
}
fn write_data(&self, ijtifffile: &mut IJTiffFile) -> Result<()> {
if self.bytes.len() > OFFSET_SIZE {
ijtifffile.file.seek(SeekFrom::End(0))?;
let offset = ijtifffile.write(&self.bytes)?;
ijtifffile.file.seek(SeekFrom::Start(
self.offset + (TAG_SIZE - OFFSET_SIZE) as u64))?;
ijtifffile.file.write(&offset.to_le_bytes())?;
if ijtifffile.file.stream_position()? % 2 == 1 {
ijtifffile.file.write(&[0u8])?;
}
}
Ok(())
}
}
#[derive(Clone, Debug)]
struct Frame {
tilebyteoffsets: Vec<u64>,
tilebytecounts: Vec<u64>,
image_width: u32,
image_length: u32,
bits_per_sample: u16,
compression: u16,
tile_width: u16,
tile_length: u16,
extra_tags: Vec<Tag>
}
impl Frame {
fn new(
tilebyteoffsets: Vec<u64>, tilebytecounts: Vec<u64>, image_width: u32,
image_length: u32, bits_per_sample: u16, compression: u16, tile_width: u16, tile_length: u16
) -> Self {
Frame {
tilebyteoffsets, tilebytecounts, image_width, image_length, bits_per_sample,
compression, tile_width, tile_length, extra_tags: Vec::new()
}
}
}
#[derive(Debug)]
pub struct IJTiffFile {
file: File,
frames: HashMap<(usize, u8), Frame>,
hashes: HashMap<u64, u64>,
pub shape: (usize, usize, usize),
pub n_frames: usize,
pub samples_per_pixel: u8,
pub colormap: Option<Vec<u16>>,
pub colors: Option<Vec<(u8, u8, u8)>>,
pub comment: Option<String>,
pub delta_z: Option<f64>,
pub timeinterval: Option<f64>,
pub extra_tags: Option<Vec<Tag>>
}
impl Drop for IJTiffFile {
fn drop(&mut self) {
if let Err(e) = self.close() {
println!("Error closing IJTiffFile: {:?}", e);
}
}
}
impl IJTiffFile {
pub fn new(path: &str, shape: (usize, usize, usize)) -> Result<Self> {
let mut file = OpenOptions::new().create(true).truncate(true)
.write(true).read(true).open(path)?;
file.write(b"II")?;
file.write(&43u16.to_le_bytes())?;
file.write(&8u16.to_le_bytes())?;
file.write(&0u16.to_le_bytes())?;
file.write(&OFFSET.to_le_bytes())?;
let colormap: Option<Vec<(u8, u8, u8)>> = None;
let (spp, n_frames) = if let None = &colormap {
(shape.0 as u8, shape.1 * shape.2)
} else {
(1, shape.0 * shape.1 * shape.2)
};
Ok(IJTiffFile { file, frames: HashMap::new(), hashes: HashMap::new(), shape, n_frames,
samples_per_pixel: spp, colormap: None, colors: None, comment: None, delta_z: None,
timeinterval: None, extra_tags: None } )
}
pub fn description(&self) -> String {
let mut desc: String = String::from("ImageJ=1.11a");
if let (None, None) = (self.colormap.as_ref(), self.colors.as_ref()) {
desc += &format!("\nimages={}", self.shape.0);
desc += &format!("\nslices={}", self.shape.1);
desc += &format!("\nframes={}", self.shape.2);
} else {
desc += &format!("\nimages={}", self.shape.0 * self.shape.1 * self.shape.2);
desc += &format!("\nchannels={}", self.shape.0);
desc += &format!("\nslices={}", self.shape.1);
desc += &format!("\nframes={}", self.shape.2);
};
if self.shape.0 == 1 {
desc += "\nmode=grayscale";
} else {
desc += "\nmode=composite";
}
desc += "\nhyperstack=true\nloop=false\nunit=micron";
if let Some(delta_z) = self.delta_z {
desc += &format!("\nspacing={}", delta_z);
}
if let Some(timeinterval) = self.timeinterval {
desc += &format!("\ninterval={}", timeinterval);
}
if let Some(comment) = &self.comment {
desc += &format!("\ncomment={}", comment);
}
desc
}
pub fn save(&mut self, frame: Array2<u16>, c: usize, z: usize, t: usize,
extra_tags: Option<Vec<Tag>>) -> Result<()> {
let mut compressed_frame = self.compress_frame(frame)?;
if let Some(tags) = extra_tags {
for tag in tags {
compressed_frame.extra_tags.push(tag);
}
}
self.frames.insert(self.get_frame_number(c, z, t), compressed_frame);
Ok(())
}
fn get_frame_number(&self, c: usize, z: usize, t: usize) -> (usize, u8) {
if let (None, None) = (self.colormap.as_ref(), self.colors.as_ref()) {
(z + t * self.shape.1, c as u8)
} else {
(c + z * self.shape.0 + t * self.shape.0 * self.shape.1, 0)
}
}
pub fn hash<T: Hash>(value: &T) -> u64 {
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}
fn hash_check(&mut self, bytes: &Vec<u8>, offset: u64) -> Result<bool> {
let current_offset = self.file.stream_position()?;
self.file.seek(SeekFrom::Start(offset))?;
let mut buffer = vec![0u8; bytes.len()];
self.file.read_exact(&mut buffer)?;
let same = bytes == &buffer;
self.file.seek(SeekFrom::Start(current_offset))?;
Ok(same)
}
fn write(&mut self, bytes: &Vec<u8>) -> Result<u64> {
let hash = IJTiffFile::hash(&bytes);
if self.hashes.contains_key(&hash) && self.hash_check(&bytes, *self.hashes.get(&hash).unwrap())? {
Ok(*self.hashes.get(&hash).unwrap())
} else {
if self.file.stream_position()? % 2 == 1 {
self.file.write(&[0])?;
}
let offset = self.file.stream_position()?;
self.hashes.insert(hash, offset);
self.file.write(&bytes)?;
Ok(offset)
}
}
fn compress_frame(&mut self, frame: Array2<u16>) -> Result<Frame> {
let image_width = frame.shape()[0] as u32;
let image_length = frame.shape()[1] as u32;
let mut tilebyteoffsets = Vec::new();
let mut tilebytecounts = Vec::new();
let tiles = IJTiffFile::tile(frame.reversed_axes(), 64);
for tile in tiles {
let bytes: Vec<u8> = tile.into_flat().into_iter().map(
|x| x.to_le_bytes()).into_iter().flatten().collect();
tilebytecounts.push(bytes.len() as u64);
tilebyteoffsets.push(self.write(&bytes)?);
}
Ok(Frame::new(tilebyteoffsets, tilebytecounts, image_width, image_length,
16, 1, 64, 64))
}
fn tile<T: Clone + Zero>(frame: Array2<T>, size: usize) -> Vec<Array2<T>> {
let shape = frame.shape();
let mut tiles = Vec::new();
let (n, m) = (shape[0] / size, shape[1] / size);
for i in 0..n {
for j in 0..m {
tiles.push(frame.slice(
s![i * size..(i + 1) * size, j * size..(j + 1) * size]).to_owned());
}
if shape[1] % size != 0 {
let mut tile = Array2::<T>::zeros((size, size));
tile.slice_mut(
s![.., ..shape[1] - m * size]
).assign(&frame.slice(s![i * size..(i + 1) * size, m * size..]));
tiles.push(tile);
}
}
if shape[0] % size != 0 {
for j in 0..m {
let mut tile = Array2::<T>::zeros((size, size));
tile.slice_mut(
s![..shape[0] - n * size, ..]
).assign(&frame.slice(s![n * size.., j * size..(j + 1) * size]));
tiles.push(tile);
}
if shape[1] % size != 0 {
let mut tile = Array2::<T>::zeros((size, size));
tile.slice_mut(
s![..shape[0] - n * size, ..shape[1] - m * size]
).assign(&frame.slice(s![n * size.., m * size..]));
tiles.push(tile);
}
}
tiles
}
fn get_colormap(&self, colormap: &Vec<u16>) -> Result<Vec<u16>> {
todo!();
Ok(Vec::new())
}
fn get_color(&self, colors: (u8, u8, u8)) -> Result<Vec<u16>> {
todo!();
Ok(Vec::new())
}
pub fn close(&mut self) -> Result<()> {
let mut where_to_write_next_ifd_offset = OFFSET - OFFSET_SIZE as u64;
let mut warn = false;
for frame_number in 0..self.n_frames {
if let Some(frame) = self.frames.get(&(frame_number, 0)) {
let mut tilebyteoffsets = Vec::new();
let mut tilebytecounts = Vec::new();
let mut frame_count = 0;
for channel in 0..self.samples_per_pixel {
if let Some(frame_n) = self.frames.get(&(frame_number, channel)) {
tilebyteoffsets.extend(frame_n.tilebyteoffsets.iter());
tilebytecounts.extend(frame_n.tilebytecounts.iter());
frame_count += 1;
} else {
warn = true;
}
}
let mut ifd = IFD::new();
ifd.push_tag(Tag::long(256, vec![frame.image_width]));
ifd.push_tag(Tag::long(257, vec![frame.image_length]));
ifd.push_tag(Tag::short(258, vec![frame.bits_per_sample; frame_count]));
ifd.push_tag(Tag::short(259, vec![1]));
ifd.push_tag(Tag::ascii(270, &self.description()));
ifd.push_tag(Tag::short(277, vec![frame_count as u16]));
ifd.push_tag(Tag::ascii(305, "tiffwrite_rs"));
ifd.push_tag(Tag::short(322, vec![frame.tile_width]));
ifd.push_tag(Tag::short(323, vec![frame.tile_length]));
ifd.push_tag(Tag::long8(324, tilebyteoffsets));
ifd.push_tag(Tag::long8(325, tilebytecounts));
if frame_number == 0 {
if let Some(colormap) = &self.colormap {
ifd.push_tag(Tag::short(320, self.get_colormap(colormap)?));
ifd.push_tag(Tag::short(262, vec![3])); // PhotometricInterpretation PHOTOMETRIC_PALETTE
} else if let None = self.colors {
ifd.push_tag(Tag::short(262, vec![1])); // PhotometricInterpretation PHOTOMETRIC_PALETTE
}
}
if frame_number < self.samples_per_pixel as usize {
if let Some(color) = &self.colors {
ifd.push_tag(Tag::short(320, self.get_color(color[frame_number])?));
ifd.push_tag(Tag::short(262, vec![3])); // PhotometricInterpretation PHOTOMETRIC_PALETTE
}
}
if let (None, None) = (&self.colormap, &self.colors) {
if self.shape.0 > 1 {
ifd.push_tag(Tag::short(284, vec![2]))
}
}
ifd.extend_tags(frame.extra_tags.to_owned());
if let Some(extra_tags) = &self.extra_tags {
ifd.extend_tags(extra_tags.to_owned());
}
ifd.push_tag(Tag::ascii(306, &format!("{}", Utc::now().format("%Y:%m:%d %H:%M:%S"))));
where_to_write_next_ifd_offset = ifd.write(self, where_to_write_next_ifd_offset)?;
} else {
warn = true;
}
if warn {
println!("Some frames were not added to the tif file, either you forgot them, \
or an error occurred and the tif file was closed prematurely.")
}
}
self.file.seek(SeekFrom::Start(where_to_write_next_ifd_offset))?;
self.file.write(&0u64.to_le_bytes())?;
Ok(())
}
}

28
src/main.rs Normal file
View File

@@ -0,0 +1,28 @@
#[cfg(not(feature = "nopython"))]
mod py;
mod lib;
use anyhow::Result;
use ndarray::{s, Array2};
use rayon::prelude::*;
use crate::lib::IJTiffFile;
fn main() -> Result<()> {
println!("Hello World!");
let mut f = IJTiffFile::new("foo.tif", (1, 2, 1))?;
let mut arr = Array2::<u16>::zeros((100, 100));
// for i in 0..arr.shape()[0] {
// for j in 0..arr.shape()[1] {
// arr[[i, j]] = i as u16;
// }
// }
f.save(arr.to_owned(), 0, 0, 0, None)?;
// let mut arr = Array2::<u16>::zeros((100, 100));
// arr.slice_mut(s![64.., ..64]).fill(1);
// arr.slice_mut(s![..64, 64..]).fill(2);
// arr.slice_mut(s![64.., 64..]).fill(3);
f.save(arr.to_owned(), 0, 1,0, None)?;
Ok(())
}

158
src/py.rs Normal file
View File

@@ -0,0 +1,158 @@
use pyo3::prelude::*;
use crate::lib::{IJTiffFile, Tag};
use std::time::Duration;
use pyo3::types::{PyInt, PyString};
use fraction::Fraction;
use num::Complex;
#[pyclass(subclass)]
#[pyo3(name = "Tag")]
#[derive(Clone, Debug)]
struct PyTag {
tag: Tag
}
#[pymethods]
impl PyTag {
#[staticmethod]
fn byte(code: u16, byte: Vec<u8>) -> Self {
PyTag { tag: Tag::byte(code, byte) }
}
#[staticmethod]
fn ascii(code: u16, ascii: &str) -> Self {
PyTag { tag: Tag::ascii(code, ascii) }
}
#[staticmethod]
fn short(code: u16, short: Vec<u16>) -> Self {
PyTag { tag: Tag::short(code, short) }
}
#[staticmethod]
fn long(code: u16, long: Vec<u32>) -> Self {
PyTag { tag: Tag::long(code, long) }
}
#[staticmethod]
fn rational(code: u16, rational: Vec<f64>) -> Self {
PyTag { tag: Tag::rational(code, rational.into_iter().map(|x| Fraction::from(x)).collect()) }
}
#[staticmethod]
fn sbyte(code: u16, sbyte: Vec<i8>) -> Self {
PyTag { tag: Tag::sbyte(code, sbyte) }
}
#[staticmethod]
fn sshort(code: u16, sshort: Vec<i16>) -> Self {
PyTag { tag: Tag::sshort(code, sshort) }
}
#[staticmethod]
fn slong(code: u16, slong: Vec<i32>) -> Self {
PyTag { tag: Tag::slong(code, slong) }
}
#[staticmethod]
fn srational(code: u16, srational: Vec<f64>) -> Self {
PyTag { tag: Tag::srational(code, srational.into_iter().map(|x| Fraction::from(x)).collect()) }
}
#[staticmethod]
fn float(code: u16, float: Vec<f32>) -> Self {
PyTag { tag: Tag::float(code, float) }
}
#[staticmethod]
fn double(code: u16, double: Vec<f64>) -> Self {
PyTag { tag: Tag::double(code, double) }
}
#[staticmethod]
fn ifd(code: u16, ifd: Vec<u32>) -> Self {
PyTag { tag: Tag::ifd(code, ifd) }
}
#[staticmethod]
fn unicode(code: u16, unicode: &str) -> Self {
PyTag { tag: Tag::unicode(code, unicode) }
}
#[staticmethod]
fn complex(code: u16, complex: Vec<(f32, f32)>) -> Self {
PyTag { tag: Tag::complex(code, complex.into_iter().map(|(x, y)| Complex { re: x, im: y }).collect()) }
}
#[staticmethod]
fn long8(code: u16, long8: Vec<u64>) -> Self {
PyTag { tag: Tag::long8(code, long8) }
}
#[staticmethod]
fn slong8(code: u16, slong8: Vec<i64>) -> Self {
PyTag { tag: Tag::slong8(code, slong8) }
}
#[staticmethod]
fn ifd8(code: u16, ifd8: Vec<u64>) -> Self {
PyTag { tag: Tag::ifd8(code, ifd8) }
}
}
#[pyclass(subclass)]
#[pyo3(name = "IJTiffFile")]
#[derive(Debug)]
struct PyIJTiffFile {
ijtifffile: IJTiffFile
}
#[pymethods]
impl PyIJTiffFile {
#[new]
fn new(path: &str, shape: (usize, usize, usize), dtype: &str) -> PyResult<Self> {
Ok(PyIJTiffFile { ijtifffile: IJTiffFile::new(path, shape)? } )
}
fn with_colors(&mut self, colors: (u8, u8, u8)) -> Self {
todo!()
}
fn with_colormap(&mut self, colormap: Vec<(u8, u8, u8)>) -> Self {
todo!()
}
fn with_px_size(&mut self, pxsize: f64) -> Self {
todo!()
}
fn with_delta_z(&mut self, delta_z: f64) -> Self {
todo!()
}
fn with_time_interval(&mut self, time_interval: f64) -> Self {
todo!()
}
fn with_comments(&mut self, comments: String) -> Self {
todo!()
}
fn append_extra_tag(&mut self, extra_tag: PyTag) {
if let Some(extra_tags) = self.ijtifffile.extra_tags.as_mut() {
extra_tags.push(extra_tag.tag);
} else {
self.ijtifffile.extra_tags = Some(vec![extra_tag.tag]);
}
}
}
#[pymodule]
fn tiffwrite(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyIJTiffFile>()?;
Ok(())
}

602
tiffwrite/__init__.py Executable file → Normal file
View File

@@ -1,603 +1,5 @@
from __future__ import annotations
from . import tiffwrite as rs
import struct
import warnings
from collections.abc import Iterable
from contextlib import contextmanager
from datetime import datetime
from fractions import Fraction
from functools import cached_property
from hashlib import sha1
from importlib.metadata import version
from io import BytesIO
from itertools import product
from pathlib import Path
from typing import Any, BinaryIO, Callable, Generator, Literal, Optional, Sequence
import colorcet
import numpy as np
import tifffile
from matplotlib import colors as mpl_colors
from numpy.typing import DTypeLike
from parfor import ParPool, PoolSingleton
from tqdm.auto import tqdm
__all__ = ["IJTiffFile", "Tag", "tiffwrite"]
try:
__version__ = version("tiffwrite")
except Exception: # noqa
__version__ = "unknown"
Strip = tuple[list[int], list[int]]
CZT = tuple[int, int, int]
def tiffwrite(file: str | Path, data: np.ndarray, axes: str = 'TZCXY', dtype: DTypeLike = None, bar: bool = False,
*args: Any, **kwargs: Any) -> None:
""" file: string; filename of the new tiff file
data: 2 to 5D numpy array
axes: string; order of dimensions in data, default: TZCXY for 5D, ZCXY for 4D, CXY for 3D, XY for 2D data
dtype: string; datatype to use when saving to tiff
bar: bool; whether to show a progress bar
other args: see IJTiffFile
"""
axes = axes[-np.ndim(data):].upper()
if not axes == 'CZTXY':
axes_shuffle = [axes.find(i) for i in 'CZTXY']
axes_add = [i for i, j in enumerate(axes_shuffle) if j < 0]
axes_shuffle = [i for i in axes_shuffle if i >= 0]
data = np.transpose(data, axes_shuffle)
for axis in axes_add:
data = np.expand_dims(data, axis)
shape = data.shape[:3]
with IJTiffFile(file, shape, data.dtype if dtype is None else dtype, *args, **kwargs) as f: # type: ignore
at_least_one = False
for n in tqdm(product(*[range(i) for i in shape]), total=np.prod(shape), desc='Saving tiff', disable=not bar):
if np.any(data[n]) or not at_least_one:
f.save(data[n], *n)
at_least_one = True
class Header:
def __init__(self, filehandle_or_byteorder: BinaryIO | Literal['>', '<'] | None = None,
bigtiff: bool = True) -> None:
if filehandle_or_byteorder is None or isinstance(filehandle_or_byteorder, str):
self.byteorder = filehandle_or_byteorder or '<'
self.bigtiff = bigtiff
if self.bigtiff:
self.tagsize = 20
self.tagnoformat = 'Q'
self.offsetsize = 8
self.offsetformat = 'Q'
self.offset = 16
else:
self.tagsize = 12
self.tagnoformat = 'H'
self.offsetsize = 4
self.offsetformat = 'I'
self.offset = 8
else:
fh = filehandle_or_byteorder
fh.seek(0)
self.byteorder = '>' if fh.read(2) == b'MM' else '<'
self.bigtiff = {42: False, 43: True}[struct.unpack(self.byteorder + 'H', fh.read(2))[0]]
if self.bigtiff:
self.tagsize = 20
self.tagnoformat = 'Q'
self.offsetsize = struct.unpack(self.byteorder + 'H', fh.read(2))[0]
self.offsetformat = {8: 'Q', 16: '2Q'}[self.offsetsize]
assert struct.unpack(self.byteorder + 'H', fh.read(2))[0] == 0, 'Not a TIFF-file'
self.offset = struct.unpack(self.byteorder + self.offsetformat, fh.read(self.offsetsize))[0]
else:
self.tagsize = 12
self.tagnoformat = 'H'
self.offsetformat = 'I'
self.offsetsize = 4
self.offset = struct.unpack(self.byteorder + self.offsetformat, fh.read(self.offsetsize))[0]
def write(self, fh: BinaryIO) -> None:
fh.write({'<': b'II', '>': b'MM'}[self.byteorder])
if self.bigtiff:
fh.write(struct.pack(self.byteorder + 'H', 43))
fh.write(struct.pack(self.byteorder + 'H', 8))
fh.write(struct.pack(self.byteorder + 'H', 0))
fh.write(struct.pack(self.byteorder + 'Q', self.offset))
else:
fh.write(struct.pack(self.byteorder + 'H', 42))
fh.write(struct.pack(self.byteorder + 'I', self.offset))
class Tag:
Value = bytes | str | float | Fraction | Sequence[bytes | str | float | Fraction]
tiff_tag_registry = tifffile.TiffTagRegistry({key: value.lower() for key, value in tifffile.TIFF.TAGS.items()})
@staticmethod
def from_dict(tags: dict[str | int, Value | Tag]) -> dict[int, Tag]:
return {(key if isinstance(key, int)
else (int(key[3:]) if key.lower().startswith('tag')
else Tag.tiff_tag_registry[key.lower()])): tag if isinstance(tag, Tag) else Tag(tag)
for key, tag in tags.items()}
@staticmethod
def fraction(numerator: float = 0, denominator: int = None) -> Fraction:
return Fraction(numerator, denominator).limit_denominator( # type: ignore
2 ** (31 if numerator < 0 or (denominator is not None and denominator < 0) else 32) - 1)
def __init__(self, ttype_or_value: str | Value, value: Value = None,
offset: int = None) -> None:
self._value: bytes | str | Sequence[bytes | str | float | Fraction]
self.fh: Optional[BinaryIO] = None
self.header: Optional[Header] = None
self.bytes_data: Optional[bytes] = None
if value is None:
self.value = ttype_or_value # type: ignore
if isinstance(self.value, (str, bytes)) or all([isinstance(value, (str, bytes)) for value in self.value]):
ttype = 'ascii'
elif all([isinstance(value, int) for value in self.value]):
min_value: int = np.min(self.value) # type: ignore
max_value: int = np.max(self.value) # type: ignore
type_map = {'uint8': 'byte', 'int8': 'sbyte', 'uint16': 'short', 'int16': 'sshort',
'uint32': 'long', 'int32': 'slong', 'uint64': 'long8', 'int64': 'slong8'}
for dtype, ttype in type_map.items():
if np.iinfo(dtype).min <= min_value and max_value <= np.iinfo(dtype).max:
break
else:
ttype = 'undefined'
elif all([isinstance(value, Fraction) for value in self.value]):
if all([value.numerator < 0 or value.denominator < 0 for value in self.value]): # type: ignore
ttype = 'srational'
else:
ttype = 'rational'
elif all([isinstance(value, (float, int)) for value in self.value]):
min_value = np.min(np.asarray(self.value)[np.isfinite(self.value)]) # type: ignore
max_value = np.max(np.asarray(self.value)[np.isfinite(self.value)]) # type: ignore
type_map = {'float32': 'float', 'float64': 'double'}
for dtype, ttype in type_map.items():
if np.finfo(dtype).min <= min_value and max_value <= np.finfo(dtype).max:
break
else:
ttype = 'undefined'
elif all([isinstance(value, complex) for value in self.value]):
ttype = 'complex'
else:
ttype = 'undefined'
self.ttype = tifffile.TIFF.DATATYPES[ttype.upper()] # noqa
else:
self.value = value # type: ignore
self.ttype = tifffile.TIFF.DATATYPES[ttype_or_value.upper()] if isinstance(ttype_or_value, str) \
else ttype_or_value # type: ignore
self.dtype = tifffile.TIFF.DATA_FORMATS[self.ttype]
self.offset = offset
self.type_check()
@property
def value(self) -> bytes | str | Sequence[bytes | str | float | Fraction]:
return self._value
@value.setter
def value(self, value: Value) -> None:
self._value = value if isinstance(value, Iterable) else (value,)
def __repr__(self) -> str:
if self.offset is None:
return f'{tifffile.TIFF.DATATYPES(self.ttype).name}: {self.value!r}'
else:
return f'{tifffile.TIFF.DATATYPES(self.ttype).name} @ {self.offset}: {self.value!r}'
def type_check(self) -> None:
try:
self.bytes_and_count(Header())
except Exception:
raise ValueError(f"tif tag type '{tifffile.TIFF.DATATYPES(self.ttype).name}' and "
f"data type '{type(self.value[0]).__name__}' do not correspond")
def bytes_and_count(self, header: Header) -> tuple[bytes, int]:
if isinstance(self.value, bytes):
return self.value, len(self.value) // struct.calcsize(self.dtype)
elif self.ttype in (2, 14):
if isinstance(self.value, str):
bytes_value = self.value.encode('ascii') + b'\x00'
else:
bytes_value = b'\x00'.join([value.encode('ascii') for value in self.value]) + b'\x00' # type: ignore
return bytes_value, len(bytes_value)
elif self.ttype in (5, 10):
return b''.join([struct.pack(header.byteorder + self.dtype, # type: ignore
*((value.denominator, value.numerator) if isinstance(value, Fraction)
else value)) for value in self.value]), len(self.value)
else:
return b''.join([struct.pack(header.byteorder + self.dtype, value) for value in self.value]), \
len(self.value)
def write_tag(self, fh: BinaryIO, key: int, header: Header, offset: int = None) -> None:
self.fh = fh
self.header = header
if offset is None:
self.offset = fh.tell()
else:
fh.seek(offset)
self.offset = offset
fh.write(struct.pack(header.byteorder + 'HH', key, self.ttype))
bytes_tag, count = self.bytes_and_count(header)
fh.write(struct.pack(header.byteorder + header.offsetformat, count))
len_bytes = len(bytes_tag)
if len_bytes <= header.offsetsize:
fh.write(bytes_tag)
self.bytes_data = None
empty_bytes = header.offsetsize - len_bytes
else:
self.bytes_data = bytes_tag
empty_bytes = header.offsetsize
if empty_bytes:
fh.write(empty_bytes * b'\x00')
def write_data(self, write: Callable[[BinaryIO, bytes], None] = None) -> None:
if self.bytes_data and self.fh is not None and self.header is not None and self.offset is not None:
self.fh.seek(0, 2)
if write is None:
offset = self.write(self.bytes_data)
else:
offset = write(self.fh, self.bytes_data)
self.fh.seek(self.offset + self.header.tagsize - self.header.offsetsize)
self.fh.write(struct.pack(self.header.byteorder + self.header.offsetformat, offset))
def write(self, bytes_value: bytes) -> Optional[int]:
if self.fh is not None:
if self.fh.tell() % 2:
self.fh.write(b'\x00')
offset = self.fh.tell()
self.fh.write(bytes_value)
return offset
def copy(self) -> Tag:
return self.__class__(self.ttype, self.value[:], self.offset)
class IFD(dict):
def __init__(self, fh: BinaryIO = None) -> None:
super().__init__()
self.fh = fh
self.header: Optional[Header] = None
self.offset: Optional[int] = None
self.where_to_write_next_ifd_offset: Optional[int] = None
if fh is not None:
header = Header(fh)
fh.seek(header.offset)
n_tags = struct.unpack(header.byteorder + header.tagnoformat,
fh.read(struct.calcsize(header.tagnoformat)))[0]
assert n_tags < 4096, 'Too many tags'
addr = []
addroffset = []
length = 8 if header.bigtiff else 2
length += n_tags * header.tagsize + header.offsetsize
for i in range(n_tags):
pos = header.offset + struct.calcsize(header.tagnoformat) + header.tagsize * i
fh.seek(pos)
code, ttype = struct.unpack(header.byteorder + 'HH', fh.read(4))
count = struct.unpack(header.byteorder + header.offsetformat, fh.read(header.offsetsize))[0]
dtype = tifffile.TIFF.DATA_FORMATS[ttype]
dtypelen = struct.calcsize(dtype)
toolong = struct.calcsize(dtype) * count > header.offsetsize
if toolong:
addr.append(fh.tell() - header.offset)
caddr = struct.unpack(header.byteorder + header.offsetformat, fh.read(header.offsetsize))[0]
addroffset.append(caddr - header.offset)
cp = fh.tell()
fh.seek(caddr)
if ttype == 1:
value: Tag.Value = fh.read(count)
elif ttype == 2:
value = fh.read(count).decode('ascii').rstrip('\x00')
elif ttype in (5, 10):
value = [struct.unpack(header.byteorder + dtype, fh.read(dtypelen)) # type: ignore
for _ in range(count)]
else:
value = [struct.unpack(header.byteorder + dtype, fh.read(dtypelen))[0] for _ in range(count)]
if toolong:
fh.seek(cp) # noqa
self[code] = Tag(ttype, value, pos)
fh.seek(header.offset)
def __setitem__(self, key: str | int, tag: str | float | Fraction | Tag) -> None:
super().__setitem__(Tag.tiff_tag_registry[key.lower()] if isinstance(key, str) else key,
tag if isinstance(tag, Tag) else Tag(tag))
def items(self) -> Generator[tuple[int, Tag], None, None]: # type: ignore[override]
return ((key, self[key]) for key in sorted(self))
def keys(self) -> Generator[int, None, None]: # type: ignore[override]
return (key for key in sorted(self))
def values(self) -> Generator[Tag, None, None]: # type: ignore[override]
return (self[key] for key in sorted(self))
def write(self, fh: BinaryIO, header: Header, write: Callable[[BinaryIO, bytes], None] = None) -> BinaryIO:
self.fh = fh
self.header = header
if fh.seek(0, 2) % 2:
fh.write(b'\x00')
self.offset = fh.tell()
fh.write(struct.pack(header.byteorder + header.tagnoformat, len(self)))
for key, tag in self.items():
tag.write_tag(fh, key, header)
self.where_to_write_next_ifd_offset = fh.tell()
fh.write(b'\x00' * header.offsetsize)
for tag in self.values():
tag.write_data(write)
return fh
def write_offset(self, where_to_write_offset: int) -> None:
if self.fh is not None and self.header is not None:
self.fh.seek(where_to_write_offset)
self.fh.write(struct.pack(self.header.byteorder + self.header.offsetformat, self.offset))
def copy(self) -> IFD:
new = self.__class__()
new.update({key: tag.copy() for key, tag in self.items()})
return new
FrameInfo = tuple[IFD, Strip, CZT]
class IJTiffFile:
""" Writes a tiff file in a format that the BioFormats reader in Fiji understands.
file: filename of the new tiff file
shape: shape (CZT) of the data to be written
dtype: datatype to use when saving to tiff
colors: a tuple with a color per channel, chosen from matplotlib.colors, html colors are also possible
colormap: name of a colormap from colorcet
pxsize: pixel size in um
deltaz: z slice interval in um
timeinterval: time between frames in seconds
extratags: other tags to be saved, example: Artist='John Doe', Tag4567=[400, 500]
or Copyright=Tag('ascii', 'Made by me'). See tiff_tag_registry.items().
wp@tl20200214
"""
def __init__(self, path: str | Path, shape: tuple[int, int, int], dtype: DTypeLike = 'uint16',
colors: Sequence[str] = None, colormap: str = None, pxsize: float = None,
deltaz: float = None, timeinterval: float = None,
compression: tuple[int, int] = (50000, 22), comment: str = None,
**extratags: Tag.Value | Tag) -> None:
assert len(shape) >= 3, 'please specify all c, z, t for the shape'
assert len(shape) <= 3, 'please specify only c, z, t for the shape'
assert np.dtype(dtype).char in 'BbHhf', 'datatype not supported'
assert colors is None or colormap is None, 'cannot have colors and colormap simultaneously'
self.path = Path(path)
self.shape = shape
self.dtype = np.dtype(dtype)
self.colors = colors
self.colormap = colormap
self.pxsize = pxsize
self.deltaz = deltaz
self.timeinterval = timeinterval
self.compression = compression
self.comment = comment
self.extratags = {} if extratags is None else Tag.from_dict(extratags) # type: ignore
if pxsize is not None:
pxsize_fraction = Tag.fraction(pxsize)
self.extratags.update({282: Tag(pxsize_fraction), 283: Tag(pxsize_fraction)})
self.header = Header()
self.spp = self.shape[0] if self.colormap is None and self.colors is None else 1 # samples/pixel
self.nframes = np.prod(self.shape[1:]) if self.colormap is None and self.colors is None else np.prod(self.shape)
self.frame_extra_tags: dict[tuple[int, int, int], dict[int, Tag]] = {}
self.fh = FileHandle(self.path)
self.pool = ParPool(self.compress_frame) # type: ignore
self.hashes = PoolSingleton().manager.dict()
self.main_process = True
with self.fh.lock() as fh: # noqa
self.header.write(fh)
def __setstate__(self, state: dict[str, Any]) -> None:
self.__dict__.update(state)
self.main_process = False
def __hash__(self) -> int:
return hash(self.path)
def get_frame_number(self, n: tuple[int, int, int]) -> tuple[int, int]:
if self.colormap is None and self.colors is None:
return n[1] + n[2] * self.shape[1], n[0]
else:
return n[0] + n[1] * self.shape[0] + n[2] * self.shape[0] * self.shape[1], 0
def ij_tiff_frame(self, frame: np.ndarray) -> bytes:
with BytesIO() as frame_bytes:
with tifffile.TiffWriter(frame_bytes, bigtiff=self.header.bigtiff,
byteorder=self.header.byteorder) as t: # type: ignore
# predictor=True might save a few bytes, but requires the package imagecodes to save floats
t.write(frame, compression=self.compression, contiguous=True, predictor=False) # type: ignore
return frame_bytes.getvalue()
def save(self, frame: np.ndarray | Any, c: int, z: int, t: int,
**extratags: Tag.Value | Tag) -> None:
""" save a 2d numpy array to the tiff at channel=c, slice=z, time=t, with optional extra tif tags
"""
assert (c, z, t) not in self.pool.tasks, f'frame {c} {z} {t} is added already'
assert all([0 <= i < s for i, s in zip((c, z, t), self.shape)]), \
'frame {} {} {} is outside shape {} {} {}'.format(c, z, t, *self.shape)
self.pool(frame.astype(self.dtype) if hasattr(frame, 'astype') else frame, handle=(c, z, t))
if extratags:
self.frame_extra_tags[(c, z, t)] = Tag.from_dict(extratags) # type: ignore
@property
def description(self) -> bytes:
desc = ['ImageJ=1.11a']
if self.colormap is None and self.colors is None:
desc.extend((f'images={np.prod(self.shape[:1])}', f'slices={self.shape[1]}', f'frames={self.shape[2]}'))
else:
desc.extend((f'images={np.prod(self.shape)}', f'channels={self.shape[0]}', f'slices={self.shape[1]}',
f'frames={self.shape[2]}'))
if self.shape[0] == 1:
desc.append('mode=grayscale')
else:
desc.append('mode=composite')
desc.extend(('hyperstack=true', 'loop=false', 'unit=micron'))
if self.deltaz is not None:
desc.append(f'spacing={self.deltaz}')
if self.timeinterval is not None:
desc.append(f'interval={self.timeinterval}')
desc_bytes = [bytes(d, 'ascii') for d in desc]
if self.comment is not None:
desc_bytes.append(b'')
if isinstance(self.comment, bytes):
desc_bytes.append(self.comment)
else:
desc_bytes.append(bytes(self.comment, 'ascii'))
return b'\n'.join(desc_bytes) + b'\0'
@cached_property
def colormap_bytes(self) -> Optional[bytes]:
if self.colormap:
colormap = getattr(colorcet, self.colormap)
colormap[0] = '#ffffff'
colormap[-1] = '#000000'
colormap = 65535 * np.array(
[[int(''.join(i), 16) for i in zip(*[iter(s[1:])] * 2)] for s in colormap]) // 255
if np.dtype(self.dtype).itemsize == 2:
colormap = np.tile(colormap, 256).reshape((-1, 3))
return b''.join([struct.pack(self.header.byteorder + 'H', c) for c in colormap.T.flatten()])
@cached_property
def colors_bytes(self) -> list[bytes]:
return [b''.join([struct.pack(self.header.byteorder + 'H', c)
for c in np.linspace(0, 65535 * np.array(mpl_colors.to_rgb(color)),
65536 if np.dtype(self.dtype).itemsize == 2 else 256,
dtype=int).T.flatten()]) for color in self.colors] if self.colors else []
def close(self) -> None:
if self.main_process:
ifds, strips = {}, {}
for n in list(self.pool.tasks):
for ifd, strip, delta in self.pool[n]:
framenr, channel = self.get_frame_number(tuple(i + j for i, j in zip(n, delta))) # type: ignore
ifds[framenr], strips[(framenr, channel)] = ifd, strip
self.pool.close()
with self.fh.lock() as fh: # noqa
for n, tags in self.frame_extra_tags.items():
framenr, _ = self.get_frame_number(n)
ifds[framenr].update(tags)
if 0 in ifds and self.colormap is not None:
ifds[0][320] = Tag('SHORT', self.colormap_bytes)
ifds[0][262] = Tag('SHORT', 3)
if self.colors is not None:
for c, color in enumerate(self.colors_bytes):
if c in ifds:
ifds[c][320] = Tag('SHORT', color)
ifds[c][262] = Tag('SHORT', 3)
if 0 in ifds and 306 not in ifds[0]:
ifds[0][306] = Tag('ASCII', datetime.now().strftime('%Y:%m:%d %H:%M:%S'))
wrn = False
for framenr in range(self.nframes):
if framenr in ifds and all([(framenr, channel) in strips for channel in range(self.spp)]):
stripbyteoffsets, stripbytecounts = zip(*[strips[(framenr, channel)]
for channel in range(self.spp)])
ifds[framenr][258].value = self.spp * ifds[framenr][258].value
ifds[framenr][270] = Tag('ASCII', self.description)
ifds[framenr][273] = Tag('LONG8', sum(stripbyteoffsets, []))
ifds[framenr][277] = Tag('SHORT', self.spp)
ifds[framenr][279] = Tag('LONG8', sum(stripbytecounts, []))
ifds[framenr][305] = Tag('ASCII', 'tiffwrite_tllab_NKI')
if self.extratags is not None:
ifds[framenr].update(self.extratags)
if self.colormap is None and self.colors is None and self.shape[0] > 1:
ifds[framenr][284] = Tag('SHORT', 2)
ifds[framenr].write(fh, self.header, self.write)
if framenr:
ifds[framenr].write_offset(ifds[framenr - 1].where_to_write_next_ifd_offset)
else:
ifds[framenr].write_offset(self.header.offset - self.header.offsetsize)
else:
wrn = True
if wrn:
warnings.warn('Some frames were not added to the tif file, either you forgot them, '
'or an error occurred and the tif file was closed prematurely.')
def __enter__(self) -> IJTiffFile:
return self
def __exit__(self, *args: Any, **kwargs: Any) -> None:
self.close()
@staticmethod
def hash_check(fh: BinaryIO, bvalue: bytes, offset: int) -> bool:
addr = fh.tell()
fh.seek(offset)
same = bvalue == fh.read(len(bvalue))
fh.seek(addr)
return same
def write(self, fh: BinaryIO, bvalue: bytes) -> int:
hash_value = sha1(bvalue).hexdigest() # hash uses a random seed making hashes different in different processes
if hash_value in self.hashes and self.hash_check(fh, bvalue, self.hashes[hash_value]):
return self.hashes[hash_value] # reuse previously saved data
else:
if fh.tell() % 2:
fh.write(b'\x00')
offset = fh.tell()
self.hashes[hash_value] = offset
fh.write(bvalue)
return offset
def compress_frame(self, frame: np.ndarray) -> Sequence[FrameInfo]:
""" This is run in a different process. Turns an image into bytes, writes them and returns the ifd, strip info
and czt delta. When subclassing IJTiffWrite this can be overridden to write one or more (using czt delta)
frames.
"""
stripbytecounts, ifd, chunks = self.get_chunks(self.ij_tiff_frame(frame))
stripbyteoffsets = []
with self.fh.lock() as fh: # noqa
for chunk in chunks:
stripbyteoffsets.append(self.write(fh, chunk))
return (ifd, (stripbyteoffsets, stripbytecounts), (0, 0, 0)),
@staticmethod
def get_chunks(frame: bytes) -> tuple[list[int], IFD, list[bytes]]:
with BytesIO(frame) as fh:
ifd = IFD(fh)
stripoffsets = ifd[273].value
stripbytecounts = ifd[279].value
chunks = []
for stripoffset, stripbytecount in zip(stripoffsets, stripbytecounts):
fh.seek(stripoffset)
chunks.append(fh.read(stripbytecount))
return stripbytecounts, ifd, chunks
class FileHandle:
""" Process safe file handle """
def __init__(self, path: Path) -> None:
manager = PoolSingleton().manager
if path.exists():
path.unlink()
with open(path, 'xb'):
class IJTiffFile(rs.IJTiffFile):
pass
self.path = path
self._lock = manager.RLock()
self._pos = manager.Value('i', 0)
@contextmanager
def lock(self) -> Generator[BinaryIO, None, None]:
with self._lock:
with open(self.path, 'rb+') as f:
try:
f.seek(self._pos.value)
yield f
finally:
self._pos.value = f.tell()