diff --git a/.gitignore b/.gitignore index 5a5a156..f5af85d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /tiffwrite.egg-info/ /.pytest_cache/ /venv/ +/target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..efdd80a --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8680bd6 --- /dev/null +++ b/Cargo.toml @@ -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 = [] diff --git a/pyproject.toml b/pyproject.toml index 2db2f95..484a62e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 "] -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" +line_length = 119 \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..13f946f --- /dev/null +++ b/src/lib.rs @@ -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 +} + +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) { + for tag in tags { + self.push_tag(tag); + } + } + + fn write(&mut self, ijtifffile: &mut IJTiffFile, where_to_write_offset: u64) -> Result { + 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, + ttype: u16, + offset: u64 +} + +impl PartialOrd for Tag { + fn partial_cmp(&self, other: &Self) -> Option { + 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, ttype: u16) -> Self { + Tag { code, bytes, ttype, offset: 0 } + } + + pub fn byte(code: u16, byte: Vec) -> 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) -> Self { + Tag::new(code, short.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 3) + } + + pub fn long(code: u16, long: Vec) -> Self { + Tag::new(code, long.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 4) + } + + pub fn rational(code: u16, rational: Vec) -> 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::>() + ).flatten().collect(), 5) + } + + pub fn sbyte(code: u16, sbyte: Vec) -> Self { + Tag::new(code, sbyte.iter().map(|x| x.to_le_bytes()).flatten().collect(), 6) + } + + pub fn sshort(code: u16, sshort: Vec) -> Self { + Tag::new(code, sshort.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 8) + } + + pub fn slong(code: u16, slong: Vec) -> Self { + Tag::new(code, slong.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 9) + } + + pub fn srational(code: u16, srational: Vec) -> 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::>() + ).flatten().collect(), 10) + } + + pub fn float(code: u16, float: Vec) -> Self { + Tag::new(code, float.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 11) + } + + pub fn double(code: u16, double: Vec) -> Self { + Tag::new(code, double.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 12) + } + + pub fn ifd(code: u16, ifd: Vec) -> 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 = 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>) -> Self { + Tag::new(code, complex.into_iter().map(|x| + x.re().to_le_bytes().into_iter().chain(x.im().to_le_bytes()).collect::>() + ).flatten().collect(), 15) + } + + pub fn long8(code: u16, long8: Vec) -> Self { + Tag::new(code, long8.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 16) + } + + pub fn slong8(code: u16, slong8: Vec) -> Self { + Tag::new(code, slong8.into_iter().map(|x| x.to_le_bytes()).flatten().collect(), 17) + } + + pub fn ifd8(code: u16, ifd8: Vec) -> 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, + tilebytecounts: Vec, + image_width: u32, + image_length: u32, + bits_per_sample: u16, + compression: u16, + tile_width: u16, + tile_length: u16, + extra_tags: Vec +} + +impl Frame { + fn new( + tilebyteoffsets: Vec, tilebytecounts: Vec, 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, + pub shape: (usize, usize, usize), + pub n_frames: usize, + pub samples_per_pixel: u8, + pub colormap: Option>, + pub colors: Option>, + pub comment: Option, + pub delta_z: Option, + pub timeinterval: Option, + pub extra_tags: Option> +} + +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 { + 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> = 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, c: usize, z: usize, t: usize, + extra_tags: Option>) -> 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(value: &T) -> u64 { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() + } + + fn hash_check(&mut self, bytes: &Vec, offset: u64) -> Result { + 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) -> Result { + 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) -> Result { + 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 = 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(frame: Array2, size: usize) -> Vec> { + 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::::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::::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::::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) -> Result> { + todo!(); + Ok(Vec::new()) + } + + fn get_color(&self, colors: (u8, u8, u8)) -> Result> { + 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(()) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f3fd0a5 --- /dev/null +++ b/src/main.rs @@ -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::::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::::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(()) +} \ No newline at end of file diff --git a/src/py.rs b/src/py.rs new file mode 100644 index 0000000..4ba2d78 --- /dev/null +++ b/src/py.rs @@ -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) -> 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) -> Self { + PyTag { tag: Tag::short(code, short) } + } + + #[staticmethod] + fn long(code: u16, long: Vec) -> Self { + PyTag { tag: Tag::long(code, long) } + } + + #[staticmethod] + fn rational(code: u16, rational: Vec) -> Self { + PyTag { tag: Tag::rational(code, rational.into_iter().map(|x| Fraction::from(x)).collect()) } + } + + #[staticmethod] + fn sbyte(code: u16, sbyte: Vec) -> Self { + PyTag { tag: Tag::sbyte(code, sbyte) } + } + + #[staticmethod] + fn sshort(code: u16, sshort: Vec) -> Self { + PyTag { tag: Tag::sshort(code, sshort) } + } + + #[staticmethod] + fn slong(code: u16, slong: Vec) -> Self { + PyTag { tag: Tag::slong(code, slong) } + } + + #[staticmethod] + fn srational(code: u16, srational: Vec) -> Self { + PyTag { tag: Tag::srational(code, srational.into_iter().map(|x| Fraction::from(x)).collect()) } + } + + #[staticmethod] + fn float(code: u16, float: Vec) -> Self { + PyTag { tag: Tag::float(code, float) } + } + + #[staticmethod] + fn double(code: u16, double: Vec) -> Self { + PyTag { tag: Tag::double(code, double) } + } + + #[staticmethod] + fn ifd(code: u16, ifd: Vec) -> 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) -> Self { + PyTag { tag: Tag::long8(code, long8) } + } + + #[staticmethod] + fn slong8(code: u16, slong8: Vec) -> Self { + PyTag { tag: Tag::slong8(code, slong8) } + } + + #[staticmethod] + fn ifd8(code: u16, ifd8: Vec) -> 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 { + 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::()?; + Ok(()) +} \ No newline at end of file diff --git a/tiffwrite/__init__.py b/tiffwrite/__init__.py old mode 100755 new mode 100644 index e4c0d1e..7f1ac1f --- a/tiffwrite/__init__.py +++ b/tiffwrite/__init__.py @@ -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'): - 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() +class IJTiffFile(rs.IJTiffFile): + pass