rewrite in rust
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@
|
|||||||
/tiffwrite.egg-info/
|
/tiffwrite.egg-info/
|
||||||
/.pytest_cache/
|
/.pytest_cache/
|
||||||
/venv/
|
/venv/
|
||||||
|
/target/
|
||||||
|
|||||||
596
Cargo.lock
generated
Normal file
596
Cargo.lock
generated
Normal 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
21
Cargo.toml
Normal 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 = []
|
||||||
@@ -1,39 +1,21 @@
|
|||||||
[tool.poetry]
|
[build-system]
|
||||||
|
requires = ["maturin>=1.5,<2.0"]
|
||||||
|
build-backend = "maturin"
|
||||||
|
|
||||||
|
[project]
|
||||||
name = "tiffwrite"
|
name = "tiffwrite"
|
||||||
version = "2024.10.1"
|
dynamic = ["version"]
|
||||||
description = "Parallel tiff writer compatible with ImageJ."
|
authors = [{ name = "Wim Pomp", email = "w.pomp@nki.nl" }]
|
||||||
authors = ["Wim Pomp, Lenstra lab NKI <w.pomp@nki.nl>"]
|
requires-python = ">=3.10"
|
||||||
license = "GPL-3.0-or-later"
|
classifiers = [
|
||||||
readme = "README.md"
|
"Programming Language :: Rust",
|
||||||
packages = [{include = "tiffwrite"}]
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
repository = "https://github.com/wimpomp/tiffwrite"
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.maturin]
|
||||||
python = "^3.10"
|
features = ["pyo3/extension-module"]
|
||||||
tifffile = "*"
|
module-name = "tiffwrite"
|
||||||
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.isort]
|
[tool.isort]
|
||||||
line_length = 119
|
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
515
src/lib.rs
Normal 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
28
src/main.rs
Normal 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
158
src/py.rs
Normal 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
602
tiffwrite/__init__.py
Executable file → Normal 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
|
class IJTiffFile(rs.IJTiffFile):
|
||||||
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
|
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()
|
|
||||||
|
|||||||
Reference in New Issue
Block a user