├── .gitattributes ├── .gitignore ├── ACKNOWLEDGEMENTS.rst ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── _static │ └── custom.css ├── conf.py └── make.py ├── examples ├── earthbigdata.py └── issue125.py ├── pyproject.toml ├── setup.py ├── tests ├── conftest.py └── test_tifffile.py └── tifffile ├── __init__.py ├── __main__.py ├── _imagecodecs.py ├── geodb.py ├── lsm2bin.py ├── numcodecs.py ├── py.typed ├── tiff2fsspec.py ├── tiffcomment.py ├── tifffile.py └── zarr.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | *.ppm binary 4 | *.pbm binary 5 | *.pgm binary 6 | *.pnm binary 7 | *.pam binary 8 | *.container binary 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _*.c 2 | *.wpr 3 | *.wpu 4 | .idea 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | setup.cfg 34 | PKG-INFO 35 | mypy.ini 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | cover/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # UV 105 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 106 | # This is especially recommended for binary packages to ensure reproducibility, and is more 107 | # commonly ignored for libraries. 108 | uv.lock 109 | 110 | # poetry 111 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 112 | # This is especially recommended for binary packages to ensure reproducibility, and is more 113 | # commonly ignored for libraries. 114 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 115 | #poetry.lock 116 | 117 | # pdm 118 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 119 | #pdm.lock 120 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 121 | # in version control. 122 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 123 | .pdm.toml 124 | .pdm-python 125 | .pdm-build/ 126 | 127 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 128 | __pypackages__/ 129 | 130 | # Celery stuff 131 | celerybeat-schedule 132 | celerybeat.pid 133 | 134 | # SageMath parsed files 135 | *.sage.py 136 | 137 | # Environments 138 | .env 139 | .venv 140 | env/ 141 | venv/ 142 | ENV/ 143 | env.bak/ 144 | venv.bak/ 145 | 146 | # Spyder project settings 147 | .spyderproject 148 | .spyproject 149 | 150 | # Rope project settings 151 | .ropeproject 152 | 153 | # mkdocs documentation 154 | /site 155 | 156 | # mypy 157 | .mypy_cache/ 158 | .dmypy.json 159 | dmypy.json 160 | 161 | # Pyre type checker 162 | .pyre/ 163 | 164 | # pytype static type analyzer 165 | .pytype/ 166 | 167 | # Cython debug symbols 168 | cython_debug/ 169 | 170 | # PyCharm 171 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 172 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 173 | # and can be added to the global gitignore or merged into this file. For a more nuclear 174 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 175 | #.idea/ 176 | 177 | # PyPI configuration file 178 | .pypirc 179 | -------------------------------------------------------------------------------- /ACKNOWLEDGEMENTS.rst: -------------------------------------------------------------------------------- 1 | Acknowledgements 2 | ---------------- 3 | * Egor Zindy, for lsm_scan_info specifics. 4 | * Wim Lewis for a bug fix and some LSM functions. 5 | * Hadrien Mary for help on reading MicroManager files. 6 | * Christian Kliche for help writing tiled and color-mapped files. 7 | * Grzegorz Bokota, for reporting and fixing OME-XML handling issues. 8 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Revisions 2 | --------- 3 | 4 | 2025.6.1 5 | 6 | - Pass 5110 tests. 7 | - Add experimental option to write iterator of bytes and bytecounts (#301). 8 | 9 | 2025.5.26 10 | 11 | - Use threads in Zarr stores. 12 | 13 | 2025.5.24 14 | 15 | - Fix incorrect tags created by Philips DP v1.1 (#299). 16 | - Make Zarr stores partially listable. 17 | 18 | 2025.5.21 19 | 20 | - Move Zarr stores to tifffile.zarr namespace (breaking). 21 | - Require Zarr 3 for Zarr stores and remove support for Zarr 2 (breaking). 22 | - Drop support for Python 3.10. 23 | 24 | 2025.5.10 25 | 26 | - Raise ValueError when using zarr 3 (#296). 27 | - Fall back to compression.zstd on Python >= 3.14 if no imagecodecs. 28 | - Remove doctest command line option. 29 | - Support Python 3.14. 30 | 31 | 2025.3.30 32 | 33 | - Fix for imagecodecs 2025.3.30. 34 | 35 | 2025.3.13 36 | 37 | - Change bytes2str to decode only up to first NULL character (breaking). 38 | - Remove stripnull function calls to reduce overhead (#285). 39 | - Deprecate stripnull function. 40 | 41 | 2025.2.18 42 | 43 | - Fix julian_datetime milliseconds (#283). 44 | - Remove deprecated dtype arguments from imread and FileSequence (breaking). 45 | - Remove deprecated imsave and TiffWriter.save function/method (breaking). 46 | - Remove deprecated option to pass multiple values to compression (breaking). 47 | - Remove deprecated option to pass unit to resolution (breaking). 48 | - Remove deprecated enums from TIFF namespace (breaking). 49 | - Remove deprecated lazyattr and squeeze_axes functions (breaking). 50 | 51 | 2025.1.10 52 | 53 | - Improve type hints. 54 | - Deprecate Python 3.10. 55 | 56 | 2024.12.12 57 | 58 | - Read PlaneProperty from STK UIC1Tag (#280). 59 | - Allow 'None' as alias for COMPRESSION.NONE and PREDICTOR.NONE (#274). 60 | - Zarr 3 is not supported (#272). 61 | 62 | 2024.9.20 63 | 64 | - Fix writing colormap to ImageJ files (breaking). 65 | - Improve typing. 66 | - Drop support for Python 3.9. 67 | 68 | 2024.8.30 69 | 70 | - Support writing OME Dataset and some StructuredAnnotations elements. 71 | 72 | 2024.8.28 73 | 74 | - Fix LSM scan types and dimension orders (#269, breaking). 75 | - Use IO[bytes] instead of BinaryIO for typing (#268). 76 | 77 | 2024.8.24 78 | 79 | - Do not remove trailing length-1 dimension writing non-shaped file (breaking). 80 | - Fix writing OME-TIFF with certain modulo axes orders. 81 | - Make imshow NaN aware. 82 | 83 | 2024.8.10 84 | 85 | - Relax bitspersample check for JPEG, JPEG2K, and JPEGXL compression (#265). 86 | 87 | 2024.7.24 88 | 89 | - Fix reading contiguous multi-page series via Zarr store (#67). 90 | 91 | 2024.7.21 92 | 93 | - Fix integer overflow in product function caused by numpy types. 94 | - Allow tag reader functions to fail. 95 | 96 | 2024.7.2 97 | 98 | - Enable memmap to create empty files with non-native byte order. 99 | - Deprecate Python 3.9, support Python 3.13. 100 | 101 | 2024.6.18 102 | 103 | - Ensure TiffPage.nodata is castable to dtype (breaking, #260). 104 | - Support Argos AVS slides. 105 | 106 | 2024.5.22 107 | 108 | - Derive TiffPages, TiffPageSeries, FileSequence, StoredShape from Sequence. 109 | - Truncate circular IFD chain, do not raise TiffFileError (breaking). 110 | - Deprecate access to TiffPages.pages and FileSequence.files. 111 | - Enable DeprecationWarning for enums in TIFF namespace. 112 | - Remove some deprecated code (breaking). 113 | - Add iccprofile property to TiffPage and parameter to TiffWriter.write. 114 | - Do not detect VSI as SIS format. 115 | - Limit length of logged exception messages. 116 | - Fix docstring examples not correctly rendered on GitHub (#254, #255). 117 | 118 | 2024.5.10 119 | 120 | - Support reading JPEGXL compression in DNG 1.7. 121 | - Read invalid TIFF created by IDEAS software. 122 | 123 | 2024.5.3 124 | 125 | - Fix reading incompletely written LSM. 126 | - Fix reading Philips DP with extra rows of tiles (#253, breaking). 127 | 128 | 2024.4.24 129 | 130 | - Fix compatibility issue with numpy 2 (#252). 131 | 132 | 2024.4.18 133 | 134 | - Fix write_fsspec when last row of tiles is missing in Philips slide (#249). 135 | - Add option not to quote file names in write_fsspec. 136 | - Allow compressing bilevel images with deflate, LZMA, and Zstd. 137 | 138 | 2024.2.12 139 | 140 | - Deprecate dtype, add chunkdtype parameter in FileSequence.asarray. 141 | - Add imreadargs parameters passed to FileSequence.imread. 142 | 143 | 2024.1.30 144 | 145 | - Fix compatibility issue with numpy 2 (#238). 146 | - Enable DeprecationWarning for tuple compression argument. 147 | - Parse sequence of numbers in xml2dict. 148 | 149 | 2023.12.9 150 | 151 | - Read 32-bit Indica Labs TIFF as float32. 152 | - Fix UnboundLocalError reading big LSM files without time axis. 153 | - Use os.sched_getaffinity, if available, to get the number of CPUs (#231). 154 | - Limit the number of default worker threads to 32. 155 | 156 | 2023.9.26 157 | 158 | - Lazily convert dask array to ndarray when writing. 159 | - Allow to specify buffersize for reading and writing. 160 | - Fix IndexError reading some corrupted files with ZarrTiffStore (#227). 161 | 162 | 2023.9.18 163 | 164 | - Raise exception when writing non-volume data with volumetric tiles (#225). 165 | - Improve multi-threaded writing of compressed multi-page files. 166 | - Fix fsspec reference for big-endian files with predictors. 167 | 168 | 2023.8.30 169 | 170 | - Support exclusive file creation mode (#221, #223). 171 | 172 | 2023.8.25 173 | 174 | - Verify shaped metadata is compatible with page shape. 175 | - Support out parameter when returning selection from imread (#222). 176 | 177 | 2023.8.12 178 | 179 | - Support decompressing EER frames. 180 | - Facilitate filtering logged warnings (#216). 181 | - Read more tags from UIC1Tag (#217). 182 | - Fix premature closing of files in main (#218). 183 | - Don't force matplotlib backend to tkagg in main (#219). 184 | - Add py.typed marker. 185 | - Drop support for imagecodecs < 2023.3.16. 186 | 187 | 2023.7.18 188 | 189 | - Limit threading via TIFFFILE_NUM_THREADS environment variable (#215). 190 | - Remove maxworkers parameter from tiff2fsspec (breaking). 191 | 192 | 2023.7.10 193 | 194 | - Increase default strip size to 256 KB when writing with compression. 195 | - Fix ZarrTiffStore with non-default chunkmode. 196 | 197 | 2023.7.4 198 | 199 | - Add option to return selection from imread (#200). 200 | - Fix reading OME series with missing trailing frames (#199). 201 | - Fix fsspec reference for WebP compressed segments missing alpha channel. 202 | - Fix linting issues. 203 | - Detect files written by Agilent Technologies. 204 | - Drop support for Python 3.8 and numpy < 1.21 (NEP29). 205 | 206 | 2023.4.12 207 | 208 | - Do not write duplicate ImageDescription tags from extratags (breaking). 209 | - Support multifocal SVS files (#193). 210 | - Log warning when filtering out extratags. 211 | - Fix writing OME-TIFF with image description in extratags. 212 | - Ignore invalid predictor tag value if prediction is not used. 213 | - Raise KeyError if ZarrStore is missing requested chunk. 214 | 215 | 2023.3.21 216 | 217 | - Fix reading MMstack with missing data (#187). 218 | 219 | 2023.3.15 220 | 221 | - Fix corruption using tile generators with prediction/compression (#185). 222 | - Add parser for Micro-Manager MMStack series (breaking). 223 | - Return micromanager_metadata IndexMap as numpy array (breaking). 224 | - Revert optimizations for Micro-Manager OME series. 225 | - Do not use numcodecs zstd in write_fsspec (kerchunk issue 317). 226 | - More type annotations. 227 | 228 | 2023.2.28 229 | 230 | - Fix reading some Micro-Manager metadata from corrupted files. 231 | - Speed up reading Micro-Manager indexmap for creation of OME series. 232 | 233 | 2023.2.27 234 | 235 | - Use Micro-Manager indexmap offsets to create virtual TiffFrames. 236 | - Fixes for future imagecodecs. 237 | 238 | 2023.2.3 239 | 240 | - Fix overflow in calculation of databytecounts for large NDPI files. 241 | 242 | 2023.2.2 243 | 244 | - Fix regression reading layered NDPI files. 245 | - Add option to specify offset in FileHandle.read_array. 246 | 247 | 2023.1.23 248 | 249 | - Support reading NDTiffStorage. 250 | - Support reading PIXTIFF compression. 251 | - Support LERC with Zstd or Deflate compression. 252 | - Do not write duplicate and select extratags. 253 | - Allow to write uncompressed image data beyond 4 GB in classic TIFF. 254 | - Add option to specify chunkshape and dtype in FileSequence.asarray. 255 | - Add option for imread to write to output in FileSequence.asarray (#172). 256 | - Add function to read GDAL structural metadata. 257 | - Add function to read NDTiff.index files. 258 | - Fix IndexError accessing TiffFile.mdgel_metadata in non-MDGEL files. 259 | - Fix unclosed file ResourceWarning in TiffWriter. 260 | - Fix non-bool predictor arguments (#167). 261 | - Relax detection of OME-XML (#173). 262 | - Rename some TiffFrame parameters (breaking). 263 | - Deprecate squeeze_axes (will change signature). 264 | - Use defusexml in xml2dict. 265 | 266 | 2022.10.10 267 | 268 | - Fix RecursionError in peek_iterator. 269 | - Fix reading NDTiffv3 summary settings. 270 | - Fix svs_description_metadata parsing (#149). 271 | - Fix ImportError if Python was built without zlib or lzma. 272 | - Fix bool of COMPRESSION and PREDICTOR instances. 273 | - Deprecate non-sequence extrasamples arguments. 274 | - Parse SCIFIO metadata as ImageJ. 275 | 276 | 2022.8.12 277 | 278 | - Fix writing ImageJ format with hyperstack argument. 279 | - Fix writing description with metadata disabled. 280 | - Add option to disable writing shaped metadata in TiffWriter. 281 | 282 | 2022.8.8 283 | 284 | - Fix regression using imread out argument (#147). 285 | - Fix imshow show argument. 286 | - Support fsspec OpenFile. 287 | 288 | 2022.8.3 289 | 290 | - Fix regression writing default resolutionunit (#145). 291 | - Add strptime function parsing common datetime formats. 292 | 293 | 2022.7.31 294 | 295 | - Fix reading corrupted WebP compressed segments missing alpha channel (#122). 296 | - Fix regression reading compressed ImageJ files. 297 | 298 | 2022.7.28 299 | 300 | - Rename FileSequence.labels attribute to dims (breaking). 301 | - Rename tifffile_geodb module to geodb (breaking). 302 | - Rename TiffFile._astuple method to astuple (breaking). 303 | - Rename noplots command line argument to maxplots (breaking). 304 | - Fix reading ImageJ hyperstacks with non-TZC order. 305 | - Fix colorspace of JPEG segments encoded by Bio-Formats. 306 | - Fix fei_metadata for HELIOS FIB-SEM (#141, needs test). 307 | - Add xarray style properties to TiffPage (WIP). 308 | - Add option to specify OME-XML for TiffFile. 309 | - Add option to control multiscales in ZarrTiffStore. 310 | - Support writing to uncompressed ZarrTiffStore. 311 | - Support writing empty images with tiling. 312 | - Support overwriting some tag values in NDPI (#137). 313 | - Support Jetraw compression (experimental). 314 | - Standardize resolution parameter and property. 315 | - Deprecate third resolution argument on write (use resolutionunit). 316 | - Deprecate tuple type compression argument on write (use compressionargs). 317 | - Deprecate enums in TIFF namespace (use enums from module). 318 | - Improve default number of threads to write compressed segments (#139). 319 | - Parse metaseries time values as datetime objects (#143). 320 | - Increase internal read and write buffers to 256 MB. 321 | - Convert some warnings to debug messages. 322 | - Declare all classes final. 323 | - Add script to generate documentation via Sphinx. 324 | - Convert docstrings to Google style with Sphinx directives. 325 | 326 | 2022.5.4 327 | 328 | - Allow to write NewSubfileType=0 (#132). 329 | - Support writing iterators of strip or tile bytes. 330 | - Convert iterables (not iterators) to NumPy arrays when writing (breaking). 331 | - Explicitly specify optional keyword parameters for imread and imwrite. 332 | - Return number of written bytes from FileHandle write functions. 333 | 334 | 2022.4.28 335 | 336 | - Add option to specify fsspec version 1 URL template name (#131). 337 | - Ignore invalid dates in UIC tags (#129). 338 | - Fix zlib_encode and lzma_encode to work with non-contiguous arrays (#128). 339 | - Fix delta_encode to preserve byteorder of ndarrays. 340 | - Move Imagecodecs fallback functions to private module and add tests. 341 | 342 | 2022.4.26 343 | 344 | - Fix AttributeError in TiffFile.shaped_metadata (#127). 345 | - Fix TiffTag.overwrite with pre-packed binary value. 346 | - Write sparse TIFF if tile iterator contains None. 347 | - Raise ValueError when writing photometric mode with too few samples. 348 | - Improve test coverage. 349 | 350 | 2022.4.22 351 | 352 | - Add type hints for Python 3.10 (WIP). 353 | - Fix Mypy errors (breaking). 354 | - Mark many parameters positional-only or keyword-only (breaking). 355 | - Remove deprecated pages parameter from imread (breaking). 356 | - Remove deprecated compress and ijmetadata write parameters (breaking). 357 | - Remove deprecated fastij and movie parameters from TiffFile (breaking). 358 | - Remove deprecated multifile parameters from TiffFile (breaking). 359 | - Remove deprecated tif parameter from TiffTag.overwrite (breaking). 360 | - Remove deprecated file parameter from FileSequence.asarray (breaking). 361 | - Remove option to pass imread class to FileSequence (breaking). 362 | - Remove optional parameters from __str__ functions (breaking). 363 | - Rename TiffPageSeries.offset to dataoffset (breaking) 364 | - Change TiffPage.pages to None if no SubIFDs are present (breaking). 365 | - Change TiffPage.index to int (breaking). 366 | - Change TiffPage.is_contiguous, is_imagej, and is_shaped to bool (breaking). 367 | - Add TiffPage imagej_description and shaped_description properties. 368 | - Add TiffFormat abstract base class. 369 | - Deprecate lazyattr and use functools.cached_property instead (breaking). 370 | - Julian_datetime raises ValueError for dates before year 1 (breaking). 371 | - Regressed import time due to typing. 372 | 373 | 2022.4.8 374 | 375 | - Add _ARRAY_DIMENSIONS attributes to ZarrTiffStore. 376 | - Allow C instead of S axis when writing OME-TIFF. 377 | - Fix writing OME-TIFF with separate samples. 378 | - Fix reading unsqueezed pyramidal OME-TIFF series. 379 | 380 | 2022.3.25 381 | 382 | - Fix another ValueError using ZarrStore with zarr >= 2.11.0 (tiffslide #25). 383 | - Add parser for Hamamatsu streak metadata. 384 | - Improve hexdump. 385 | 386 | 2022.3.16 387 | 388 | - Use multi-threading to compress strips and tiles. 389 | - Raise TiffFileError when reading corrupted strips and tiles (#122). 390 | - Fix ScanImage single channel count (#121). 391 | - Add parser for AstroTIFF FITS metadata. 392 | 393 | 2022.2.9 394 | 395 | - Fix ValueError using multiscale ZarrStore with zarr >= 2.11.0. 396 | - Raise KeyError if ZarrStore does not contain key. 397 | - Limit number of warnings for missing files in multifile series. 398 | - Allow to save colormap to 32-bit ImageJ files (#115). 399 | 400 | 2022.2.2 401 | 402 | - Fix TypeError when second ImageDescription tag contains non-ASCII (#112). 403 | - Fix parsing IJMetadata with many IJMetadataByteCounts (#111). 404 | - Detect MicroManager NDTiffv2 header (not tested). 405 | - Remove cache from ZarrFileSequenceStore (use zarr.LRUStoreCache). 406 | - Raise limit on maximum number of pages. 407 | - Use J2K format when encoding JPEG2000 segments. 408 | - Formally deprecate imsave and TiffWriter.save. 409 | - Drop support for Python 3.7 and NumPy < 1.19 (NEP29). 410 | 411 | 2021.11.2 412 | 413 | - Lazy-load non-essential tag values (breaking). 414 | - Warn when reading from closed file. 415 | - Support ImageJ prop metadata type (#103). 416 | - Support writing indexed ImageJ format. 417 | - Fix multi-threaded access of multi-page Zarr stores with chunkmode 2. 418 | - Raise error if truncate is used with compression, packints, or tile. 419 | - Read STK metadata without UIC2tag. 420 | - Improve log and warning messages (WIP). 421 | - Improve string representation of large tag values. 422 | 423 | 2021.10.12 424 | 425 | - Revert renaming of file parameter in FileSequence.asarray (breaking). 426 | - Deprecate file parameter in FileSequence.asarray. 427 | 428 | 2021.10.10 429 | 430 | - Disallow letters as indices in FileSequence; use categories (breaking). 431 | - Do not warn of missing files in FileSequence; use files_missing property. 432 | - Support predictors in ZarrTiffStore.write_fsspec. 433 | - Add option to specify Zarr group name in write_fsspec. 434 | - Add option to specify categories for FileSequence patterns (#76). 435 | - Add option to specify chunk shape and dtype for ZarrFileSequenceStore. 436 | - Add option to tile ZarrFileSequenceStore and FileSequence.asarray. 437 | - Add option to pass additional zattrs to Zarr stores. 438 | - Detect Roche BIF files. 439 | 440 | 2021.8.30 441 | 442 | - Fix horizontal differencing with non-native byte order. 443 | - Fix multi-threaded access of memory-mappable, multi-page Zarr stores (#67). 444 | 445 | 2021.8.8 446 | 447 | - Fix tag offset and valueoffset for NDPI > 4 GB (#96). 448 | 449 | 2021.7.30 450 | 451 | - Deprecate first parameter to TiffTag.overwrite (no longer required). 452 | - TiffTag init API change (breaking). 453 | - Detect Ventana BIF series and warn that tiles are not stitched. 454 | - Enable reading PreviewImage from RAW formats (#93, #94). 455 | - Work around numpy.ndarray.tofile is very slow for non-contiguous arrays. 456 | - Fix issues with PackBits compression (requires imagecodecs 2021.7.30). 457 | 458 | 2021.7.2 459 | 460 | - Decode complex integer images found in SAR GeoTIFF. 461 | - Support reading NDPI with JPEG-XR compression. 462 | - Deprecate TiffWriter RGB auto-detection, except for RGB24/48 and RGBA32/64. 463 | 464 | 2021.6.14 465 | 466 | - Set stacklevel for deprecation warnings (#89). 467 | - Fix svs_description_metadata for SVS with double header (#88, breaking). 468 | - Fix reading JPEG compressed CMYK images. 469 | - Support ALT_JPEG and JPEG_2000_LOSSY compression found in Bio-Formats. 470 | - Log warning if TiffWriter auto-detects RGB mode (specify photometric). 471 | 472 | 2021.6.6 473 | 474 | - Fix TIFF.COMPESSOR typo (#85). 475 | - Round resolution numbers that do not fit in 64-bit rationals (#81). 476 | - Add support for JPEG XL compression. 477 | - Add Numcodecs compatible TIFF codec. 478 | - Rename ZarrFileStore to ZarrFileSequenceStore (breaking). 479 | - Add method to export fsspec ReferenceFileSystem from ZarrFileStore. 480 | - Fix fsspec ReferenceFileSystem v1 for multifile series. 481 | - Fix creating OME-TIFF with micron character in OME-XML. 482 | 483 | 2021.4.8 484 | 485 | - Fix reading OJPEG with wrong photometric or samplesperpixel tags (#75). 486 | - Fix fsspec ReferenceFileSystem v1 and JPEG compression. 487 | - Use TiffTagRegistry for NDPI_TAGS, EXIF_TAGS, GPS_TAGS, IOP_TAGS constants. 488 | - Make TIFF.GEO_KEYS an Enum (breaking). 489 | 490 | 2021.3.31 491 | 492 | - Use JPEG restart markers as tile offsets in NDPI. 493 | - Support version 1 and more codecs in fsspec ReferenceFileSystem (untested). 494 | 495 | 2021.3.17 496 | 497 | - Fix regression reading multi-file OME-TIFF with missing files (#72). 498 | - Fix fsspec ReferenceFileSystem with non-native byte order (#56). 499 | 500 | 2021.3.16 501 | 502 | - TIFF is no longer a defended trademark. 503 | - Add method to export fsspec ReferenceFileSystem from ZarrTiffStore (#56). 504 | 505 | 2021.3.5 506 | 507 | - Preliminary support for EER format (#68). 508 | - Do not warn about unknown compression (#68). 509 | 510 | 2021.3.4 511 | 512 | - Fix reading multi-file, multi-series OME-TIFF (#67). 513 | - Detect ScanImage 2021 files (#46). 514 | - Shape new version ScanImage series according to metadata (breaking). 515 | - Remove Description key from TiffFile.scanimage_metadata dict (breaking). 516 | - Also return ScanImage version from read_scanimage_metadata (breaking). 517 | - Fix docstrings. 518 | 519 | 2021.2.26 520 | 521 | - Squeeze axes of LSM series by default (breaking). 522 | - Add option to preserve single dimensions when reading from series (WIP). 523 | - Do not allow appending to OME-TIFF files. 524 | - Fix reading STK files without name attribute in metadata. 525 | - Make TIFF constants multi-thread safe and pickleable (#64). 526 | - Add detection of NDTiffStorage MajorVersion to read_micromanager_metadata. 527 | - Support ScanImage v4 files in read_scanimage_metadata. 528 | 529 | 2021.2.1 530 | 531 | - Fix multi-threaded access of ZarrTiffStores using same TiffFile instance. 532 | - Use fallback zlib and lzma codecs with imagecodecs lite builds. 533 | - Open Olympus and Panasonic RAW files for parsing, albeit not supported. 534 | - Support X2 and X4 differencing found in DNG. 535 | - Support reading JPEG_LOSSY compression found in DNG. 536 | 537 | 2021.1.14 538 | 539 | - Try ImageJ series if OME series fails (#54) 540 | - Add option to use pages as chunks in ZarrFileStore (experimental). 541 | - Fix reading from file objects with no readinto function. 542 | 543 | 2021.1.11 544 | 545 | - Fix test errors on PyPy. 546 | - Fix decoding bitorder with imagecodecs >= 2021.1.11. 547 | 548 | 2021.1.8 549 | 550 | - Decode float24 using imagecodecs >= 2021.1.8. 551 | - Consolidate reading of segments if possible. 552 | 553 | 2020.12.8 554 | 555 | - Fix corrupted ImageDescription in multi shaped series if buffer too small. 556 | - Fix libtiff warning that ImageDescription contains null byte in value. 557 | - Fix reading invalid files using JPEG compression with palette colorspace. 558 | 559 | 2020.12.4 560 | 561 | - Fix reading some JPEG compressed CFA images. 562 | - Make index of SubIFDs a tuple. 563 | - Pass through FileSequence.imread arguments in imread. 564 | - Do not apply regex flags to FileSequence axes patterns (breaking). 565 | 566 | 2020.11.26 567 | 568 | - Add option to pass axes metadata to ImageJ writer. 569 | - Pad incomplete tiles passed to TiffWriter.write (#38). 570 | - Split TiffTag constructor (breaking). 571 | - Change TiffTag.dtype to TIFF.DATATYPES (breaking). 572 | - Add TiffTag.overwrite method. 573 | - Add script to change ImageDescription in files. 574 | - Add TiffWriter.overwrite_description method (WIP). 575 | 576 | 2020.11.18 577 | 578 | - Support writing SEPARATED color space (#37). 579 | - Use imagecodecs.deflate codec if available. 580 | - Fix SCN and NDPI series with Z dimensions. 581 | - Add TiffReader alias for TiffFile. 582 | - TiffPage.is_volumetric returns True if ImageDepth > 1. 583 | - Zarr store getitem returns NumPy arrays instead of bytes. 584 | 585 | 2020.10.1 586 | 587 | - Formally deprecate unused TiffFile parameters (scikit-image #4996). 588 | 589 | 2020.9.30 590 | 591 | - Allow to pass additional arguments to compression codecs. 592 | - Deprecate TiffWriter.save method (use TiffWriter.write). 593 | - Deprecate TiffWriter.save compress parameter (use compression). 594 | - Remove multifile parameter from TiffFile (breaking). 595 | - Pass all is_flag arguments from imread to TiffFile. 596 | - Do not byte-swap JPEG2000, WEBP, PNG, JPEGXR segments in TiffPage.decode. 597 | 598 | 2020.9.29 599 | 600 | - Fix reading files produced by ScanImage > 2015 (#29). 601 | 602 | 2020.9.28 603 | 604 | - Derive ZarrStore from MutableMapping. 605 | - Support zero shape ZarrTiffStore. 606 | - Fix ZarrFileStore with non-TIFF files. 607 | - Fix ZarrFileStore with missing files. 608 | - Cache one chunk in ZarrFileStore. 609 | - Keep track of already opened files in FileCache. 610 | - Change parse_filenames function to return zero-based indices. 611 | - Remove reopen parameter from asarray (breaking). 612 | - Rename FileSequence.fromfile to imread (breaking). 613 | 614 | 2020.9.22 615 | 616 | - Add experimental Zarr storage interface (WIP). 617 | - Remove unused first dimension from TiffPage.shaped (breaking). 618 | - Move reading of STK planes to series interface (breaking). 619 | - Always use virtual frames for ScanImage files. 620 | - Use DimensionOrder to determine axes order in OmeXml. 621 | - Enable writing striped volumetric images. 622 | - Keep complete dataoffsets and databytecounts for TiffFrames. 623 | - Return full size tiles from Tiffpage.segments. 624 | - Rename TiffPage.is_sgi property to is_volumetric (breaking). 625 | - Rename TiffPageSeries.is_pyramid to is_pyramidal (breaking). 626 | - Fix TypeError when passing jpegtables to non-JPEG decode method (#25). 627 | 628 | 2020.9.3 629 | 630 | - Do not write contiguous series by default (breaking). 631 | - Allow to write to SubIFDs (WIP). 632 | - Fix writing F-contiguous NumPy arrays (#24). 633 | 634 | 2020.8.25 635 | 636 | - Do not convert EPICS timeStamp to datetime object. 637 | - Read incompletely written Micro-Manager image file stack header (#23). 638 | - Remove tag 51123 values from TiffFile.micromanager_metadata (breaking). 639 | 640 | 2020.8.13 641 | 642 | - Use tifffile metadata over OME and ImageJ for TiffFile.series (breaking). 643 | - Fix writing iterable of pages with compression (#20). 644 | - Expand error checking of TiffWriter data, dtype, shape, and tile arguments. 645 | 646 | 2020.7.24 647 | 648 | - Parse nested OmeXml metadata argument (WIP). 649 | - Do not lazy load TiffFrame JPEGTables. 650 | - Fix conditionally skipping some tests. 651 | 652 | 2020.7.22 653 | 654 | - Do not auto-enable OME-TIFF if description is passed to TiffWriter.save. 655 | - Raise error writing empty bilevel or tiled images. 656 | - Allow to write tiled bilevel images. 657 | - Allow to write multi-page TIFF from iterable of single page images (WIP). 658 | - Add function to validate OME-XML. 659 | - Correct Philips slide width and length. 660 | 661 | 2020.7.17 662 | 663 | - Initial support for writing OME-TIFF (WIP). 664 | - Return samples as separate dimension in OME series (breaking). 665 | - Fix modulo dimensions for multiple OME series. 666 | - Fix some test errors on big endian systems (#18). 667 | - Fix BytesWarning. 668 | - Allow to pass TIFF.PREDICTOR values to TiffWriter.save. 669 | 670 | 2020.7.4 671 | 672 | - Deprecate support for Python 3.6 (NEP 29). 673 | - Move pyramidal subresolution series to TiffPageSeries.levels (breaking). 674 | - Add parser for SVS, SCN, NDPI, and QPI pyramidal series. 675 | - Read single-file OME-TIFF pyramids. 676 | - Read NDPI files > 4 GB (#15). 677 | - Include SubIFDs in generic series. 678 | - Preliminary support for writing packed integer arrays (#11, WIP). 679 | - Read more LSM info subrecords. 680 | - Fix missing ReferenceBlackWhite tag for YCbCr photometrics. 681 | - Fix reading lossless JPEG compressed DNG files. 682 | 683 | 2020.6.3 684 | 685 | - Support os.PathLike file names (#9). 686 | 687 | 2020.5.30 688 | 689 | - Re-add pure Python PackBits decoder. 690 | 691 | 2020.5.25 692 | 693 | - Make imagecodecs an optional dependency again. 694 | - Disable multi-threaded decoding of small LZW compressed segments. 695 | - Fix caching of TiffPage.decode method. 696 | - Fix xml.etree.cElementTree ImportError on Python 3.9. 697 | - Fix tostring DeprecationWarning. 698 | 699 | 2020.5.11 700 | 701 | - Fix reading ImageJ grayscale mode RGB images (#6). 702 | - Remove napari reader plugin. 703 | 704 | 2020.5.7 705 | 706 | - Add napari reader plugin (tentative). 707 | - Fix writing single tiles larger than image data (#3). 708 | - Always store ExtraSamples values in tuple (breaking). 709 | 710 | 2020.5.5 711 | 712 | - Allow to write tiled TIFF from iterable of tiles (WIP). 713 | - Add method to iterate over decoded segments of TiffPage (WIP). 714 | - Pass chunks of segments to ThreadPoolExecutor.map to reduce memory usage. 715 | - Fix reading invalid files with too many strips. 716 | - Fix writing over-aligned image data. 717 | - Detect OME-XML without declaration (#2). 718 | - Support LERC compression (WIP). 719 | - Delay load imagecodecs functions. 720 | - Remove maxsize parameter from asarray (breaking). 721 | - Deprecate ijmetadata parameter from TiffWriter.save (use metadata). 722 | 723 | 2020.2.16 724 | 725 | - Add method to decode individual strips or tiles. 726 | - Read strips and tiles in order of their offsets. 727 | - Enable multi-threading when decompressing multiple strips. 728 | - Replace TiffPage.tags dictionary with TiffTags (breaking). 729 | - Replace TIFF.TAGS dictionary with TiffTagRegistry. 730 | - Remove TIFF.TAG_NAMES (breaking). 731 | - Improve handling of TiffSequence parameters in imread. 732 | - Match last uncommon parts of file paths to FileSequence pattern (breaking). 733 | - Allow letters in FileSequence pattern for indexing well plate rows. 734 | - Allow to reorder axes in FileSequence. 735 | - Allow to write > 4 GB arrays to plain TIFF when using compression. 736 | - Allow to write zero size NumPy arrays to nonconformant TIFF (tentative). 737 | - Fix xml2dict. 738 | - Require imagecodecs >= 2020.1.31. 739 | - Remove support for imagecodecs-lite (breaking). 740 | - Remove verify parameter to asarray method (breaking). 741 | - Remove deprecated lzw_decode functions (breaking). 742 | - Remove support for Python 2.7 and 3.5 (breaking). 743 | 744 | 2019.7.26 745 | 746 | - Fix infinite loop reading more than two tags of same code in IFD. 747 | - Delay import of logging module. 748 | 749 | 2019.7.20 750 | 751 | - Fix OME-XML detection for files created by Imaris. 752 | - Remove or replace assert statements. 753 | 754 | 2019.7.2 755 | 756 | - Do not write SampleFormat tag for unsigned data types. 757 | - Write ByteCount tag values as SHORT or LONG if possible. 758 | - Allow to specify axes in FileSequence pattern via group names. 759 | - Add option to concurrently read FileSequence using threads. 760 | - Derive TiffSequence from FileSequence. 761 | - Use str(datetime.timedelta) to format Timer duration. 762 | - Use perf_counter for Timer if possible. 763 | 764 | 2019.6.18 765 | 766 | - Fix reading planar RGB ImageJ files created by Bio-Formats. 767 | - Fix reading single-file, multi-image OME-TIFF without UUID. 768 | - Presume LSM stores uncompressed images contiguously per page. 769 | - Reformat some complex expressions. 770 | 771 | 2019.5.30 772 | 773 | - Ignore invalid frames in OME-TIFF. 774 | - Set default subsampling to (2, 2) for RGB JPEG compression. 775 | - Fix reading and writing planar RGB JPEG compression. 776 | - Replace buffered_read with FileHandle.read_segments. 777 | - Include page or frame numbers in exceptions and warnings. 778 | - Add Timer class. 779 | 780 | 2019.5.22 781 | 782 | - Add optional chroma subsampling for JPEG compression. 783 | - Enable writing PNG, JPEG, JPEGXR, and JPEG2K compression (WIP). 784 | - Fix writing tiled images with WebP compression. 785 | - Improve handling GeoTIFF sparse files. 786 | 787 | 2019.3.18 788 | 789 | - Fix regression decoding JPEG with RGB photometrics. 790 | - Fix reading OME-TIFF files with corrupted but unused pages. 791 | - Allow to load TiffFrame without specifying keyframe. 792 | - Calculate virtual TiffFrames for non-BigTIFF ScanImage files > 2GB. 793 | - Rename property is_chroma_subsampled to is_subsampled (breaking). 794 | - Make more attributes and methods private (WIP). 795 | 796 | 2019.3.8 797 | 798 | - Fix MemoryError when RowsPerStrip > ImageLength. 799 | - Fix SyntaxWarning on Python 3.8. 800 | - Fail to decode JPEG to planar RGB (tentative). 801 | - Separate public from private test files (WIP). 802 | - Allow testing without data files or imagecodecs. 803 | 804 | 2019.2.22 805 | 806 | - Use imagecodecs-lite as fallback for imagecodecs. 807 | - Simplify reading NumPy arrays from file. 808 | - Use TiffFrames when reading arrays from page sequences. 809 | - Support slices and iterators in TiffPageSeries sequence interface. 810 | - Auto-detect uniform series. 811 | - Use page hash to determine generic series. 812 | - Turn off TiffPages cache (tentative). 813 | - Pass through more parameters in imread. 814 | - Discontinue movie parameter in imread and TiffFile (breaking). 815 | - Discontinue bigsize parameter in imwrite (breaking). 816 | - Raise TiffFileError in case of issues with TIFF structure. 817 | - Return TiffFile.ome_metadata as XML (breaking). 818 | - Ignore OME series when last dimensions are not stored in TIFF pages. 819 | 820 | 2019.2.10 821 | 822 | - Assemble IFDs in memory to speed-up writing on some slow media. 823 | - Handle discontinued arguments fastij, multifile_close, and pages. 824 | 825 | 2019.1.30 826 | 827 | - Use black background in imshow. 828 | - Do not write datetime tag by default (breaking). 829 | - Fix OME-TIFF with SamplesPerPixel > 1. 830 | - Allow 64-bit IFD offsets for NDPI (files > 4GB still not supported). 831 | 832 | 2019.1.4 833 | 834 | - Fix decoding deflate without imagecodecs. 835 | 836 | 2019.1.1 837 | 838 | - Update copyright year. 839 | - Require imagecodecs >= 2018.12.16. 840 | - Do not use JPEG tables from keyframe. 841 | - Enable decoding large JPEG in NDPI. 842 | - Decode some old-style JPEG. 843 | - Reorder OME channel axis to match PlanarConfiguration storage. 844 | - Return tiled images as contiguous arrays. 845 | - Add decode_lzw proxy function for compatibility with old czifile module. 846 | - Use dedicated logger. 847 | 848 | 2018.11.28 849 | 850 | - Make SubIFDs accessible as TiffPage.pages. 851 | - Make parsing of TiffSequence axes pattern optional (breaking). 852 | - Limit parsing of TiffSequence axes pattern to file names, not path names. 853 | - Do not interpolate in imshow if image dimensions <= 512, else use bilinear. 854 | - Use logging.warning instead of warnings.warn in many cases. 855 | - Fix NumPy FutureWarning for out == 'memmap'. 856 | - Adjust ZSTD and WebP compression to libtiff-4.0.10 (WIP). 857 | - Decode old-style LZW with imagecodecs >= 2018.11.8. 858 | - Remove TiffFile.qptiff_metadata (QPI metadata are per page). 859 | - Do not use keyword arguments before variable positional arguments. 860 | - Make either all or none return statements in function return expression. 861 | - Use pytest parametrize to generate tests. 862 | - Replace test classes with functions. 863 | 864 | 2018.11.6 865 | 866 | - Rename imsave function to imwrite. 867 | - Re-add Python implementations of packints, delta, and bitorder codecs. 868 | - Fix TiffFrame.compression AttributeError. 869 | 870 | 2018.10.18 871 | 872 | - Rename tiffile package to tifffile. 873 | 874 | 2018.10.10 875 | 876 | - Read ZIF, the Zoomable Image Format (WIP). 877 | - Decode YCbCr JPEG as RGB (tentative). 878 | - Improve restoration of incomplete tiles. 879 | - Allow to write grayscale with extrasamples without specifying planarconfig. 880 | - Enable decoding of PNG and JXR via imagecodecs. 881 | - Deprecate 32-bit platforms (too many memory errors during tests). 882 | 883 | 2018.9.27 884 | 885 | - Read Olympus SIS (WIP). 886 | - Allow to write non-BigTIFF files up to ~4 GB (fix). 887 | - Fix parsing date and time fields in SEM metadata. 888 | - Detect some circular IFD references. 889 | - Enable WebP codecs via imagecodecs. 890 | - Add option to read TiffSequence from ZIP containers. 891 | - Remove TiffFile.isnative. 892 | - Move TIFF struct format constants out of TiffFile namespace. 893 | 894 | 2018.8.31 895 | 896 | - Fix wrong TiffTag.valueoffset. 897 | - Towards reading Hamamatsu NDPI (WIP). 898 | - Enable PackBits compression of byte and bool arrays. 899 | - Fix parsing NULL terminated CZ_SEM strings. 900 | 901 | 2018.8.24 902 | 903 | - Move tifffile.py and related modules into tiffile package. 904 | - Move usage examples to module docstring. 905 | - Enable multi-threading for compressed tiles and pages by default. 906 | - Add option to concurrently decode image tiles using threads. 907 | - Do not skip empty tiles (fix). 908 | - Read JPEG and J2K compressed strips and tiles. 909 | - Allow floating-point predictor on write. 910 | - Add option to specify subfiletype on write. 911 | - Depend on imagecodecs package instead of _tifffile, lzma, etc modules. 912 | - Remove reverse_bitorder, unpack_ints, and decode functions. 913 | - Use pytest instead of unittest. 914 | 915 | 2018.6.20 916 | 917 | - Save RGBA with unassociated extrasample by default (breaking). 918 | - Add option to specify ExtraSamples values. 919 | 920 | 2018.6.17 (included with 0.15.1) 921 | 922 | - Towards reading JPEG and other compressions via imagecodecs package (WIP). 923 | - Read SampleFormat VOID as UINT. 924 | - Add function to validate TIFF using `jhove -m TIFF-hul`. 925 | - Save bool arrays as bilevel TIFF. 926 | - Accept pathlib.Path as filenames. 927 | - Move software argument from TiffWriter __init__ to save. 928 | - Raise DOS limit to 16 TB. 929 | - Lazy load LZMA and ZSTD compressors and decompressors. 930 | - Add option to save IJMetadata tags. 931 | - Return correct number of pages for truncated series (fix). 932 | - Move EXIF tags to TIFF.TAG as per TIFF/EP standard. 933 | 934 | 2018.2.18 935 | 936 | - Always save RowsPerStrip and Resolution tags as required by TIFF standard. 937 | - Do not use badly typed ImageDescription. 938 | - Coerce bad ASCII string tags to bytes. 939 | - Tuning of __str__ functions. 940 | - Fix reading undefined tag values. 941 | - Read and write ZSTD compressed data. 942 | - Use hexdump to print bytes. 943 | - Determine TIFF byte order from data dtype in imsave. 944 | - Add option to specify RowsPerStrip for compressed strips. 945 | - Allow memory-map of arrays with non-native byte order. 946 | - Attempt to handle ScanImage <= 5.1 files. 947 | - Restore TiffPageSeries.pages sequence interface. 948 | - Use numpy.frombuffer instead of fromstring to read from binary data. 949 | - Parse GeoTIFF metadata. 950 | - Add option to apply horizontal differencing before compression. 951 | - Towards reading PerkinElmer QPI (QPTIFF, no test files). 952 | - Do not index out of bounds data in tifffile.c unpackbits and decodelzw. 953 | 954 | 2017.9.29 955 | 956 | - Many backward incompatible changes improving speed and resource usage: 957 | - Add detail argument to __str__ function. Remove info functions. 958 | - Fix potential issue correcting offsets of large LSM files with positions. 959 | - Remove TiffFile sequence interface; use TiffFile.pages instead. 960 | - Do not make tag values available as TiffPage attributes. 961 | - Use str (not bytes) type for tag and metadata strings (WIP). 962 | - Use documented standard tag and value names (WIP). 963 | - Use enums for some documented TIFF tag values. 964 | - Remove memmap and tmpfile options; use out='memmap' instead. 965 | - Add option to specify output in asarray functions. 966 | - Add option to concurrently decode pages using threads. 967 | - Add TiffPage.asrgb function (WIP). 968 | - Do not apply colormap in asarray. 969 | - Remove colormapped, rgbonly, and scale_mdgel options from asarray. 970 | - Consolidate metadata in TiffFile _metadata functions. 971 | - Remove non-tag metadata properties from TiffPage. 972 | - Add function to convert LSM to tiled BIN files. 973 | - Align image data in file. 974 | - Make TiffPage.dtype a numpy.dtype. 975 | - Add ndim and size properties to TiffPage and TiffPageSeries. 976 | - Allow imsave to write non-BigTIFF files up to ~4 GB. 977 | - Only read one page for shaped series if possible. 978 | - Add memmap function to create memory-mapped array stored in TIFF file. 979 | - Add option to save empty arrays to TIFF files. 980 | - Add option to save truncated TIFF files. 981 | - Allow single tile images to be saved contiguously. 982 | - Add optional movie mode for files with uniform pages. 983 | - Lazy load pages. 984 | - Use lightweight TiffFrame for IFDs sharing properties with key TiffPage. 985 | - Move module constants to TIFF namespace (speed up module import). 986 | - Remove fastij option from TiffFile. 987 | - Remove pages parameter from TiffFile. 988 | - Remove TIFFfile alias. 989 | - Deprecate Python 2. 990 | - Require enum34 and futures packages on Python 2.7. 991 | - Remove Record class and return all metadata as dict instead. 992 | - Add functions to parse STK, MetaSeries, ScanImage, SVS, Pilatus metadata. 993 | - Read tags from EXIF and GPS IFDs. 994 | - Use pformat for tag and metadata values. 995 | - Fix reading some UIC tags. 996 | - Do not modify input array in imshow (fix). 997 | - Fix Python implementation of unpack_ints. 998 | 999 | 2017.5.23 1000 | 1001 | - Write correct number of SampleFormat values (fix). 1002 | - Use Adobe deflate code to write ZIP compressed files. 1003 | - Add option to pass tag values as packed binary data for writing. 1004 | - Defer tag validation to attribute access. 1005 | - Use property instead of lazyattr decorator for simple expressions. 1006 | 1007 | 2017.3.17 1008 | 1009 | - Write IFDs and tag values on word boundaries. 1010 | - Read ScanImage metadata. 1011 | - Remove is_rgb and is_indexed attributes from TiffFile. 1012 | - Create files used by doctests. 1013 | 1014 | 2017.1.12 (included with scikit-image 0.14.x) 1015 | 1016 | - Read Zeiss SEM metadata. 1017 | - Read OME-TIFF with invalid references to external files. 1018 | - Rewrite C LZW decoder (5x faster). 1019 | - Read corrupted LSM files missing EOI code in LZW stream. 1020 | 1021 | 2017.1.1 1022 | 1023 | - Add option to append images to existing TIFF files. 1024 | - Read files without pages. 1025 | - Read S-FEG and Helios NanoLab tags created by FEI software. 1026 | - Allow saving Color Filter Array (CFA) images. 1027 | - Add info functions returning more information about TiffFile and TiffPage. 1028 | - Add option to read specific pages only. 1029 | - Remove maxpages argument (breaking). 1030 | - Remove test_tifffile function. 1031 | 1032 | 2016.10.28 1033 | 1034 | - Improve detection of ImageJ hyperstacks. 1035 | - Read TVIPS metadata created by EM-MENU (by Marco Oster). 1036 | - Add option to disable using OME-XML metadata. 1037 | - Allow non-integer range attributes in modulo tags (by Stuart Berg). 1038 | 1039 | 2016.6.21 1040 | 1041 | - Do not always memmap contiguous data in page series. 1042 | 1043 | 2016.5.13 1044 | 1045 | - Add option to specify resolution unit. 1046 | - Write grayscale images with extra samples when planarconfig is specified. 1047 | - Do not write RGB color images with 2 samples. 1048 | - Reorder TiffWriter.save keyword arguments (breaking). 1049 | 1050 | 2016.4.18 1051 | 1052 | - TiffWriter, imread, and imsave accept open binary file streams. 1053 | 1054 | 2016.04.13 1055 | 1056 | - Fix reversed fill order in 2 and 4 bps images. 1057 | - Implement reverse_bitorder in C. 1058 | 1059 | 2016.03.18 1060 | 1061 | - Fix saving additional ImageJ metadata. 1062 | 1063 | 2016.2.22 1064 | 1065 | - Write 8 bytes double tag values using offset if necessary (bug fix). 1066 | - Add option to disable writing second image description tag. 1067 | - Detect tags with incorrect counts. 1068 | - Disable color mapping for LSM. 1069 | 1070 | 2015.11.13 1071 | 1072 | - Read LSM 6 mosaics. 1073 | - Add option to specify directory of memory-mapped files. 1074 | - Add command line options to specify vmin and vmax values for colormapping. 1075 | 1076 | 2015.10.06 1077 | 1078 | - New helper function to apply colormaps. 1079 | - Renamed is_palette attributes to is_indexed (breaking). 1080 | - Color-mapped samples are now contiguous (breaking). 1081 | - Do not color-map ImageJ hyperstacks (breaking). 1082 | - Towards reading Leica SCN. 1083 | 1084 | 2015.9.25 1085 | 1086 | - Read images with reversed bit order (FillOrder is LSB2MSB). 1087 | 1088 | 2015.9.21 1089 | 1090 | - Read RGB OME-TIFF. 1091 | - Warn about malformed OME-XML. 1092 | 1093 | 2015.9.16 1094 | 1095 | - Detect some corrupted ImageJ metadata. 1096 | - Better axes labels for shaped files. 1097 | - Do not create TiffTag for default values. 1098 | - Chroma subsampling is not supported. 1099 | - Memory-map data in TiffPageSeries if possible (optional). 1100 | 1101 | 2015.8.17 1102 | 1103 | - Write ImageJ hyperstacks (optional). 1104 | - Read and write LZMA compressed data. 1105 | - Specify datetime when saving (optional). 1106 | - Save tiled and color-mapped images (optional). 1107 | - Ignore void bytecounts and offsets if possible. 1108 | - Ignore bogus image_depth tag created by ISS Vista software. 1109 | - Decode floating-point horizontal differencing (not tiled). 1110 | - Save image data contiguously if possible. 1111 | - Only read first IFD from ImageJ files if possible. 1112 | - Read ImageJ raw format (files larger than 4 GB). 1113 | - TiffPageSeries class for pages with compatible shape and data type. 1114 | - Try to read incomplete tiles. 1115 | - Open file dialog if no filename is passed on command line. 1116 | - Ignore errors when decoding OME-XML. 1117 | - Rename decoder functions (breaking). 1118 | 1119 | 2014.8.24 1120 | 1121 | - TiffWriter class for incremental writing images. 1122 | - Simplify examples. 1123 | 1124 | 2014.8.19 1125 | 1126 | - Add memmap function to FileHandle. 1127 | - Add function to determine if image data in TiffPage is memory-mappable. 1128 | - Do not close files if multifile_close parameter is False. 1129 | 1130 | 2014.8.10 1131 | 1132 | - Return all extrasamples by default (breaking). 1133 | - Read data from series of pages into memory-mapped array (optional). 1134 | - Squeeze OME dimensions (breaking). 1135 | - Workaround missing EOI code in strips. 1136 | - Support image and tile depth tags (SGI extension). 1137 | - Better handling of STK/UIC tags (breaking). 1138 | - Disable color mapping for STK. 1139 | - Julian to datetime converter. 1140 | - TIFF ASCII type may be NULL separated. 1141 | - Unwrap strip offsets for LSM files greater than 4 GB. 1142 | - Correct strip byte counts in compressed LSM files. 1143 | - Skip missing files in OME series. 1144 | - Read embedded TIFF files. 1145 | 1146 | 2014.2.05 1147 | 1148 | - Save rational numbers as type 5 (bug fix). 1149 | 1150 | 2013.12.20 1151 | 1152 | - Keep other files in OME multi-file series closed. 1153 | - FileHandle class to abstract binary file handle. 1154 | - Disable color mapping for bad OME-TIFF produced by bio-formats. 1155 | - Read bad OME-XML produced by ImageJ when cropping. 1156 | 1157 | 2013.11.3 1158 | 1159 | - Allow zlib compress data in imsave function (optional). 1160 | - Memory-map contiguous image data (optional). 1161 | 1162 | 2013.10.28 1163 | 1164 | - Read MicroManager metadata and little-endian ImageJ tag. 1165 | - Save extra tags in imsave function. 1166 | - Save tags in ascending order by code (bug fix). 1167 | 1168 | 2012.10.18 1169 | 1170 | - Accept file like objects (read from OIB files). 1171 | 1172 | 2012.8.21 1173 | 1174 | - Rename TIFFfile to TiffFile and TIFFpage to TiffPage. 1175 | - TiffSequence class for reading sequence of TIFF files. 1176 | - Read UltraQuant tags. 1177 | - Allow float numbers as resolution in imsave function. 1178 | 1179 | 2012.8.3 1180 | 1181 | - Read MD GEL tags and NIH Image header. 1182 | 1183 | 2012.7.25 1184 | 1185 | - Read ImageJ tags. 1186 | - … 1187 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD-3-Clause license 2 | 3 | Copyright (c) 2008-2025, Christoph Gohlke 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, 10 | this list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include CHANGES.rst 4 | include ACKNOWLEDGEMENTS.rst 5 | include pyproject.toml 6 | 7 | include tifffile/py.typed 8 | 9 | exclude *.cmd 10 | recursive-exclude doc * 11 | recursive-exclude test * 12 | recursive-exclude tests * 13 | 14 | recursive-exclude * __pycache__ 15 | recursive-exclude * *.py[co] 16 | recursive-exclude * *- 17 | recursive-exclude test/data * 18 | recursive-exclude test/_tmp * 19 | 20 | include tests/conftest.py 21 | include tests/test_tifffile.py 22 | 23 | include examples/earthbigdata.py 24 | include examples/issue125.py 25 | 26 | include docs/conf.py 27 | include docs/make.py 28 | include docs/_static/custom.css 29 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. 2 | This file is generated by setup.py 3 | 4 | Read and write TIFF files 5 | ========================= 6 | 7 | Tifffile is a Python library to 8 | 9 | (1) store NumPy arrays in TIFF (Tagged Image File Format) files, and 10 | (2) read image and metadata from TIFF-like files used in bioimaging. 11 | 12 | Image and metadata can be read from TIFF, BigTIFF, OME-TIFF, GeoTIFF, 13 | Adobe DNG, ZIF (Zoomable Image File Format), MetaMorph STK, Zeiss LSM, 14 | ImageJ hyperstack, Micro-Manager MMStack and NDTiff, SGI, NIHImage, 15 | Olympus FluoView and SIS, ScanImage, Molecular Dynamics GEL, 16 | Aperio SVS, Leica SCN, Roche BIF, PerkinElmer QPTIFF (QPI, PKI), 17 | Hamamatsu NDPI, Argos AVS, and Philips DP formatted files. 18 | 19 | Image data can be read as NumPy arrays or Zarr arrays/groups from strips, 20 | tiles, pages (IFDs), SubIFDs, higher-order series, and pyramidal levels. 21 | 22 | Image data can be written to TIFF, BigTIFF, OME-TIFF, and ImageJ hyperstack 23 | compatible files in multi-page, volumetric, pyramidal, memory-mappable, 24 | tiled, predicted, or compressed form. 25 | 26 | Many compression and predictor schemes are supported via the imagecodecs 27 | library, including LZW, PackBits, Deflate, PIXTIFF, LZMA, LERC, Zstd, 28 | JPEG (8 and 12-bit, lossless), JPEG 2000, JPEG XR, JPEG XL, WebP, PNG, EER, 29 | Jetraw, 24-bit floating-point, and horizontal differencing. 30 | 31 | Tifffile can also be used to inspect TIFF structures, read image data from 32 | multi-dimensional file sequences, write fsspec ReferenceFileSystem for 33 | TIFF files and image file sequences, patch TIFF tag values, and parse 34 | many proprietary metadata formats. 35 | 36 | :Author: `Christoph Gohlke `_ 37 | :License: BSD-3-Clause 38 | :Version: 2025.6.1 39 | :DOI: `10.5281/zenodo.6795860 `_ 40 | 41 | Quickstart 42 | ---------- 43 | 44 | Install the tifffile package and all dependencies from the 45 | `Python Package Index `_:: 46 | 47 | python -m pip install -U tifffile[all] 48 | 49 | Tifffile is also available in other package repositories such as Anaconda, 50 | Debian, and MSYS2. 51 | 52 | The tifffile library is type annotated and documented via docstrings:: 53 | 54 | python -c "import tifffile; help(tifffile)" 55 | 56 | Tifffile can be used as a console script to inspect and preview TIFF files:: 57 | 58 | python -m tifffile --help 59 | 60 | See `Examples`_ for using the programming interface. 61 | 62 | Source code and support are available on 63 | `GitHub `_. 64 | 65 | Support is also provided on the 66 | `image.sc `_ forum. 67 | 68 | Requirements 69 | ------------ 70 | 71 | This revision was tested with the following requirements and dependencies 72 | (other versions may work): 73 | 74 | - `CPython `_ 3.11.9, 3.12.10, 3.13.3 3.14.0b2 64-bit 75 | - `NumPy `_ 2.2.6 76 | - `Imagecodecs `_ 2025.3.30 77 | (required for encoding or decoding LZW, JPEG, etc. compressed segments) 78 | - `Matplotlib `_ 3.10.3 79 | (required for plotting) 80 | - `Lxml `_ 5.4.0 81 | (required only for validating and printing XML) 82 | - `Zarr `_ 3.0.8 83 | (required only for using Zarr stores; Zarr 2 is not compatible) 84 | - `Kerchunk `_ 0.2.8 85 | (required only for opening ReferenceFileSystem files) 86 | 87 | Revisions 88 | --------- 89 | 90 | 2025.6.1 91 | 92 | - Pass 5110 tests. 93 | - Add experimental option to write iterator of bytes and bytecounts (#301). 94 | 95 | 2025.5.26 96 | 97 | - Use threads in Zarr stores. 98 | 99 | 2025.5.24 100 | 101 | - Fix incorrect tags created by Philips DP v1.1 (#299). 102 | - Make Zarr stores partially listable. 103 | 104 | 2025.5.21 105 | 106 | - Move Zarr stores to tifffile.zarr namespace (breaking). 107 | - Require Zarr 3 for Zarr stores and remove support for Zarr 2 (breaking). 108 | - Drop support for Python 3.10. 109 | 110 | 2025.5.10 111 | 112 | - Raise ValueError when using zarr 3 (#296). 113 | - Fall back to compression.zstd on Python >= 3.14 if no imagecodecs. 114 | - Remove doctest command line option. 115 | - Support Python 3.14. 116 | 117 | 2025.3.30 118 | 119 | - Fix for imagecodecs 2025.3.30. 120 | 121 | 2025.3.13 122 | 123 | - Change bytes2str to decode only up to first NULL character (breaking). 124 | - Remove stripnull function calls to reduce overhead (#285). 125 | - Deprecate stripnull function. 126 | 127 | 2025.2.18 128 | 129 | - Fix julian_datetime milliseconds (#283). 130 | - Remove deprecated dtype arguments from imread and FileSequence (breaking). 131 | - Remove deprecated imsave and TiffWriter.save function/method (breaking). 132 | - Remove deprecated option to pass multiple values to compression (breaking). 133 | - Remove deprecated option to pass unit to resolution (breaking). 134 | - Remove deprecated enums from TIFF namespace (breaking). 135 | - Remove deprecated lazyattr and squeeze_axes functions (breaking). 136 | 137 | 2025.1.10 138 | 139 | - Improve type hints. 140 | - Deprecate Python 3.10. 141 | 142 | 2024.12.12 143 | 144 | - Read PlaneProperty from STK UIC1Tag (#280). 145 | - Allow 'None' as alias for COMPRESSION.NONE and PREDICTOR.NONE (#274). 146 | - Zarr 3 is not supported (#272). 147 | 148 | 2024.9.20 149 | 150 | - Fix writing colormap to ImageJ files (breaking). 151 | - Improve typing. 152 | - Drop support for Python 3.9. 153 | 154 | 2024.8.30 155 | 156 | - Support writing OME Dataset and some StructuredAnnotations elements. 157 | 158 | 2024.8.28 159 | 160 | - Fix LSM scan types and dimension orders (#269, breaking). 161 | - Use IO[bytes] instead of BinaryIO for typing (#268). 162 | 163 | 2024.8.24 164 | 165 | - Do not remove trailing length-1 dimension writing non-shaped file (breaking). 166 | - Fix writing OME-TIFF with certain modulo axes orders. 167 | - Make imshow NaN aware. 168 | 169 | 2024.8.10 170 | 171 | - Relax bitspersample check for JPEG, JPEG2K, and JPEGXL compression (#265). 172 | 173 | 2024.7.24 174 | 175 | - Fix reading contiguous multi-page series via Zarr store (#67). 176 | 177 | 2024.7.21 178 | 179 | - Fix integer overflow in product function caused by numpy types. 180 | - Allow tag reader functions to fail. 181 | 182 | 2024.7.2 183 | 184 | - Enable memmap to create empty files with non-native byte order. 185 | - Deprecate Python 3.9, support Python 3.13. 186 | 187 | 2024.6.18 188 | 189 | - Ensure TiffPage.nodata is castable to dtype (breaking, #260). 190 | - Support Argos AVS slides. 191 | 192 | 2024.5.22 193 | 194 | - Derive TiffPages, TiffPageSeries, FileSequence, StoredShape from Sequence. 195 | - Truncate circular IFD chain, do not raise TiffFileError (breaking). 196 | - Deprecate access to TiffPages.pages and FileSequence.files. 197 | - Enable DeprecationWarning for enums in TIFF namespace. 198 | - Remove some deprecated code (breaking). 199 | - Add iccprofile property to TiffPage and parameter to TiffWriter.write. 200 | - Do not detect VSI as SIS format. 201 | - Limit length of logged exception messages. 202 | - Fix docstring examples not correctly rendered on GitHub (#254, #255). 203 | 204 | - … 205 | 206 | Refer to the CHANGES file for older revisions. 207 | 208 | Notes 209 | ----- 210 | 211 | TIFF, the Tagged Image File Format, was created by the Aldus Corporation and 212 | Adobe Systems Incorporated. 213 | 214 | Tifffile supports a subset of the TIFF6 specification, mainly 8, 16, 32, and 215 | 64-bit integer, 16, 32, and 64-bit float, grayscale and multi-sample images. 216 | Specifically, CCITT and OJPEG compression, chroma subsampling without JPEG 217 | compression, color space transformations, samples with differing types, or 218 | IPTC, ICC, and XMP metadata are not implemented. 219 | 220 | Besides classic TIFF, tifffile supports several TIFF-like formats that do not 221 | strictly adhere to the TIFF6 specification. Some formats allow file and data 222 | sizes to exceed the 4 GB limit of the classic TIFF: 223 | 224 | - **BigTIFF** is identified by version number 43 and uses different file 225 | header, IFD, and tag structures with 64-bit offsets. The format also adds 226 | 64-bit data types. Tifffile can read and write BigTIFF files. 227 | - **ImageJ hyperstacks** store all image data, which may exceed 4 GB, 228 | contiguously after the first IFD. Files > 4 GB contain one IFD only. 229 | The size and shape of the up to 6-dimensional image data can be determined 230 | from the ImageDescription tag of the first IFD, which is Latin-1 encoded. 231 | Tifffile can read and write ImageJ hyperstacks. 232 | - **OME-TIFF** files store up to 8-dimensional image data in one or multiple 233 | TIFF or BigTIFF files. The UTF-8 encoded OME-XML metadata found in the 234 | ImageDescription tag of the first IFD defines the position of TIFF IFDs in 235 | the high-dimensional image data. Tifffile can read OME-TIFF files (except 236 | multi-file pyramidal) and write NumPy arrays to single-file OME-TIFF. 237 | - **Micro-Manager NDTiff** stores multi-dimensional image data in one 238 | or more classic TIFF files. Metadata contained in a separate NDTiff.index 239 | binary file defines the position of the TIFF IFDs in the image array. 240 | Each TIFF file also contains metadata in a non-TIFF binary structure at 241 | offset 8. Downsampled image data of pyramidal datasets are stored in 242 | separate folders. Tifffile can read NDTiff files. Version 0 and 1 series, 243 | tiling, stitching, and multi-resolution pyramids are not supported. 244 | - **Micro-Manager MMStack** stores 6-dimensional image data in one or more 245 | classic TIFF files. Metadata contained in non-TIFF binary structures and 246 | JSON strings define the image stack dimensions and the position of the image 247 | frame data in the file and the image stack. The TIFF structures and metadata 248 | are often corrupted or wrong. Tifffile can read MMStack files. 249 | - **Carl Zeiss LSM** files store all IFDs below 4 GB and wrap around 32-bit 250 | StripOffsets pointing to image data above 4 GB. The StripOffsets of each 251 | series and position require separate unwrapping. The StripByteCounts tag 252 | contains the number of bytes for the uncompressed data. Tifffile can read 253 | LSM files of any size. 254 | - **MetaMorph Stack, STK** files contain additional image planes stored 255 | contiguously after the image data of the first page. The total number of 256 | planes is equal to the count of the UIC2tag. Tifffile can read STK files. 257 | - **ZIF**, the Zoomable Image File format, is a subspecification of BigTIFF 258 | with SGI's ImageDepth extension and additional compression schemes. 259 | Only little-endian, tiled, interleaved, 8-bit per sample images with 260 | JPEG, PNG, JPEG XR, and JPEG 2000 compression are allowed. Tifffile can 261 | read and write ZIF files. 262 | - **Hamamatsu NDPI** files use some 64-bit offsets in the file header, IFD, 263 | and tag structures. Single, LONG typed tag values can exceed 32-bit. 264 | The high bytes of 64-bit tag values and offsets are stored after IFD 265 | structures. Tifffile can read NDPI files > 4 GB. 266 | JPEG compressed segments with dimensions >65530 or missing restart markers 267 | cannot be decoded with common JPEG libraries. Tifffile works around this 268 | limitation by separately decoding the MCUs between restart markers, which 269 | performs poorly. BitsPerSample, SamplesPerPixel, and 270 | PhotometricInterpretation tags may contain wrong values, which can be 271 | corrected using the value of tag 65441. 272 | - **Philips TIFF** slides store padded ImageWidth and ImageLength tag values 273 | for tiled pages. The values can be corrected using the DICOM_PIXEL_SPACING 274 | attributes of the XML formatted description of the first page. Tile offsets 275 | and byte counts may be 0. Tifffile can read Philips slides. 276 | - **Ventana/Roche BIF** slides store tiles and metadata in a BigTIFF container. 277 | Tiles may overlap and require stitching based on the TileJointInfo elements 278 | in the XMP tag. Volumetric scans are stored using the ImageDepth extension. 279 | Tifffile can read BIF and decode individual tiles but does not perform 280 | stitching. 281 | - **ScanImage** optionally allows corrupted non-BigTIFF files > 2 GB. 282 | The values of StripOffsets and StripByteCounts can be recovered using the 283 | constant differences of the offsets of IFD and tag values throughout the 284 | file. Tifffile can read such files if the image data are stored contiguously 285 | in each page. 286 | - **GeoTIFF sparse** files allow strip or tile offsets and byte counts to be 0. 287 | Such segments are implicitly set to 0 or the NODATA value on reading. 288 | Tifffile can read GeoTIFF sparse files. 289 | - **Tifffile shaped** files store the array shape and user-provided metadata 290 | of multi-dimensional image series in JSON format in the ImageDescription tag 291 | of the first page of the series. The format allows multiple series, 292 | SubIFDs, sparse segments with zero offset and byte count, and truncated 293 | series, where only the first page of a series is present, and the image data 294 | are stored contiguously. No other software besides Tifffile supports the 295 | truncated format. 296 | 297 | Other libraries for reading, writing, inspecting, or manipulating scientific 298 | TIFF files from Python are 299 | `aicsimageio `_, 300 | `apeer-ometiff-library 301 | `_, 302 | `bigtiff `_, 303 | `fabio.TiffIO `_, 304 | `GDAL `_, 305 | `imread `_, 306 | `large_image `_, 307 | `openslide-python `_, 308 | `opentile `_, 309 | `pylibtiff `_, 310 | `pylsm `_, 311 | `pymimage `_, 312 | `python-bioformats `_, 313 | `pytiff `_, 314 | `scanimagetiffreader-python 315 | `_, 316 | `SimpleITK `_, 317 | `slideio `_, 318 | `tiffslide `_, 319 | `tifftools `_, 320 | `tyf `_, 321 | `xtiff `_, and 322 | `ndtiff `_. 323 | 324 | References 325 | ---------- 326 | 327 | - TIFF 6.0 Specification and Supplements. Adobe Systems Incorporated. 328 | https://www.adobe.io/open/standards/TIFF.html 329 | https://download.osgeo.org/libtiff/doc/ 330 | - TIFF File Format FAQ. https://www.awaresystems.be/imaging/tiff/faq.html 331 | - The BigTIFF File Format. 332 | https://www.awaresystems.be/imaging/tiff/bigtiff.html 333 | - MetaMorph Stack (STK) Image File Format. 334 | http://mdc.custhelp.com/app/answers/detail/a_id/18862 335 | - Image File Format Description LSM 5/7 Release 6.0 (ZEN 2010). 336 | Carl Zeiss MicroImaging GmbH. BioSciences. May 10, 2011 337 | - The OME-TIFF format. 338 | https://docs.openmicroscopy.org/ome-model/latest/ 339 | - UltraQuant(r) Version 6.0 for Windows Start-Up Guide. 340 | http://www.ultralum.com/images%20ultralum/pdf/UQStart%20Up%20Guide.pdf 341 | - Micro-Manager File Formats. 342 | https://micro-manager.org/wiki/Micro-Manager_File_Formats 343 | - ScanImage BigTiff Specification. 344 | https://docs.scanimage.org/Appendix/ScanImage+BigTiff+Specification.html 345 | - ZIF, the Zoomable Image File format. https://zif.photo/ 346 | - GeoTIFF File Format https://gdal.org/drivers/raster/gtiff.html 347 | - Cloud optimized GeoTIFF. 348 | https://github.com/cogeotiff/cog-spec/blob/master/spec.md 349 | - Tags for TIFF and Related Specifications. Digital Preservation. 350 | https://www.loc.gov/preservation/digital/formats/content/tiff_tags.shtml 351 | - CIPA DC-008-2016: Exchangeable image file format for digital still cameras: 352 | Exif Version 2.31. 353 | http://www.cipa.jp/std/documents/e/DC-008-Translation-2016-E.pdf 354 | - The EER (Electron Event Representation) file format. 355 | https://github.com/fei-company/EerReaderLib 356 | - Digital Negative (DNG) Specification. Version 1.7.1.0, September 2023. 357 | https://helpx.adobe.com/content/dam/help/en/photoshop/pdf/DNG_Spec_1_7_1_0.pdf 358 | - Roche Digital Pathology. BIF image file format for digital pathology. 359 | https://diagnostics.roche.com/content/dam/diagnostics/Blueprint/en/pdf/rmd/Roche-Digital-Pathology-BIF-Whitepaper.pdf 360 | - Astro-TIFF specification. https://astro-tiff.sourceforge.io/ 361 | - Aperio Technologies, Inc. Digital Slides and Third-Party Data Interchange. 362 | Aperio_Digital_Slides_and_Third-party_data_interchange.pdf 363 | - PerkinElmer image format. 364 | https://downloads.openmicroscopy.org/images/Vectra-QPTIFF/perkinelmer/PKI_Image%20Format.docx 365 | - NDTiffStorage. https://github.com/micro-manager/NDTiffStorage 366 | - Argos AVS File Format. 367 | https://github.com/user-attachments/files/15580286/ARGOS.AVS.File.Format.pdf 368 | 369 | Examples 370 | -------- 371 | 372 | Write a NumPy array to a single-page RGB TIFF file: 373 | 374 | .. code-block:: python 375 | 376 | >>> import numpy 377 | >>> data = numpy.random.randint(0, 255, (256, 256, 3), 'uint8') 378 | >>> imwrite('temp.tif', data, photometric='rgb') 379 | 380 | Read the image from the TIFF file as NumPy array: 381 | 382 | .. code-block:: python 383 | 384 | >>> image = imread('temp.tif') 385 | >>> image.shape 386 | (256, 256, 3) 387 | 388 | Use the `photometric` and `planarconfig` arguments to write a 3x3x3 NumPy 389 | array to an interleaved RGB, a planar RGB, or a 3-page grayscale TIFF: 390 | 391 | .. code-block:: python 392 | 393 | >>> data = numpy.random.randint(0, 255, (3, 3, 3), 'uint8') 394 | >>> imwrite('temp.tif', data, photometric='rgb') 395 | >>> imwrite('temp.tif', data, photometric='rgb', planarconfig='separate') 396 | >>> imwrite('temp.tif', data, photometric='minisblack') 397 | 398 | Use the `extrasamples` argument to specify how extra components are 399 | interpreted, for example, for an RGBA image with unassociated alpha channel: 400 | 401 | .. code-block:: python 402 | 403 | >>> data = numpy.random.randint(0, 255, (256, 256, 4), 'uint8') 404 | >>> imwrite('temp.tif', data, photometric='rgb', extrasamples=['unassalpha']) 405 | 406 | Write a 3-dimensional NumPy array to a multi-page, 16-bit grayscale TIFF file: 407 | 408 | .. code-block:: python 409 | 410 | >>> data = numpy.random.randint(0, 2**12, (64, 301, 219), 'uint16') 411 | >>> imwrite('temp.tif', data, photometric='minisblack') 412 | 413 | Read the whole image stack from the multi-page TIFF file as NumPy array: 414 | 415 | .. code-block:: python 416 | 417 | >>> image_stack = imread('temp.tif') 418 | >>> image_stack.shape 419 | (64, 301, 219) 420 | >>> image_stack.dtype 421 | dtype('uint16') 422 | 423 | Read the image from the first page in the TIFF file as NumPy array: 424 | 425 | .. code-block:: python 426 | 427 | >>> image = imread('temp.tif', key=0) 428 | >>> image.shape 429 | (301, 219) 430 | 431 | Read images from a selected range of pages: 432 | 433 | .. code-block:: python 434 | 435 | >>> images = imread('temp.tif', key=range(4, 40, 2)) 436 | >>> images.shape 437 | (18, 301, 219) 438 | 439 | Iterate over all pages in the TIFF file and successively read images: 440 | 441 | .. code-block:: python 442 | 443 | >>> with TiffFile('temp.tif') as tif: 444 | ... for page in tif.pages: 445 | ... image = page.asarray() 446 | ... 447 | 448 | Get information about the image stack in the TIFF file without reading 449 | any image data: 450 | 451 | .. code-block:: python 452 | 453 | >>> tif = TiffFile('temp.tif') 454 | >>> len(tif.pages) # number of pages in the file 455 | 64 456 | >>> page = tif.pages[0] # get shape and dtype of image in first page 457 | >>> page.shape 458 | (301, 219) 459 | >>> page.dtype 460 | dtype('uint16') 461 | >>> page.axes 462 | 'YX' 463 | >>> series = tif.series[0] # get shape and dtype of first image series 464 | >>> series.shape 465 | (64, 301, 219) 466 | >>> series.dtype 467 | dtype('uint16') 468 | >>> series.axes 469 | 'QYX' 470 | >>> tif.close() 471 | 472 | Inspect the "XResolution" tag from the first page in the TIFF file: 473 | 474 | .. code-block:: python 475 | 476 | >>> with TiffFile('temp.tif') as tif: 477 | ... tag = tif.pages[0].tags['XResolution'] 478 | ... 479 | >>> tag.value 480 | (1, 1) 481 | >>> tag.name 482 | 'XResolution' 483 | >>> tag.code 484 | 282 485 | >>> tag.count 486 | 1 487 | >>> tag.dtype 488 | 489 | 490 | Iterate over all tags in the TIFF file: 491 | 492 | .. code-block:: python 493 | 494 | >>> with TiffFile('temp.tif') as tif: 495 | ... for page in tif.pages: 496 | ... for tag in page.tags: 497 | ... tag_name, tag_value = tag.name, tag.value 498 | ... 499 | 500 | Overwrite the value of an existing tag, for example, XResolution: 501 | 502 | .. code-block:: python 503 | 504 | >>> with TiffFile('temp.tif', mode='r+') as tif: 505 | ... _ = tif.pages[0].tags['XResolution'].overwrite((96000, 1000)) 506 | ... 507 | 508 | Write a 5-dimensional floating-point array using BigTIFF format, separate 509 | color components, tiling, Zlib compression level 8, horizontal differencing 510 | predictor, and additional metadata: 511 | 512 | .. code-block:: python 513 | 514 | >>> data = numpy.random.rand(2, 5, 3, 301, 219).astype('float32') 515 | >>> imwrite( 516 | ... 'temp.tif', 517 | ... data, 518 | ... bigtiff=True, 519 | ... photometric='rgb', 520 | ... planarconfig='separate', 521 | ... tile=(32, 32), 522 | ... compression='zlib', 523 | ... compressionargs={'level': 8}, 524 | ... predictor=True, 525 | ... metadata={'axes': 'TZCYX'}, 526 | ... ) 527 | 528 | Write a 10 fps time series of volumes with xyz voxel size 2.6755x2.6755x3.9474 529 | micron^3 to an ImageJ hyperstack formatted TIFF file: 530 | 531 | .. code-block:: python 532 | 533 | >>> volume = numpy.random.randn(6, 57, 256, 256).astype('float32') 534 | >>> image_labels = [f'{i}' for i in range(volume.shape[0] * volume.shape[1])] 535 | >>> imwrite( 536 | ... 'temp.tif', 537 | ... volume, 538 | ... imagej=True, 539 | ... resolution=(1.0 / 2.6755, 1.0 / 2.6755), 540 | ... metadata={ 541 | ... 'spacing': 3.947368, 542 | ... 'unit': 'um', 543 | ... 'finterval': 1 / 10, 544 | ... 'fps': 10.0, 545 | ... 'axes': 'TZYX', 546 | ... 'Labels': image_labels, 547 | ... }, 548 | ... ) 549 | 550 | Read the volume and metadata from the ImageJ hyperstack file: 551 | 552 | .. code-block:: python 553 | 554 | >>> with TiffFile('temp.tif') as tif: 555 | ... volume = tif.asarray() 556 | ... axes = tif.series[0].axes 557 | ... imagej_metadata = tif.imagej_metadata 558 | ... 559 | >>> volume.shape 560 | (6, 57, 256, 256) 561 | >>> axes 562 | 'TZYX' 563 | >>> imagej_metadata['slices'] 564 | 57 565 | >>> imagej_metadata['frames'] 566 | 6 567 | 568 | Memory-map the contiguous image data in the ImageJ hyperstack file: 569 | 570 | .. code-block:: python 571 | 572 | >>> memmap_volume = memmap('temp.tif') 573 | >>> memmap_volume.shape 574 | (6, 57, 256, 256) 575 | >>> del memmap_volume 576 | 577 | Create a TIFF file containing an empty image and write to the memory-mapped 578 | NumPy array (note: this does not work with compression or tiling): 579 | 580 | .. code-block:: python 581 | 582 | >>> memmap_image = memmap( 583 | ... 'temp.tif', shape=(256, 256, 3), dtype='float32', photometric='rgb' 584 | ... ) 585 | >>> type(memmap_image) 586 | 587 | >>> memmap_image[255, 255, 1] = 1.0 588 | >>> memmap_image.flush() 589 | >>> del memmap_image 590 | 591 | Write two NumPy arrays to a multi-series TIFF file (note: other TIFF readers 592 | will not recognize the two series; use the OME-TIFF format for better 593 | interoperability): 594 | 595 | .. code-block:: python 596 | 597 | >>> series0 = numpy.random.randint(0, 255, (32, 32, 3), 'uint8') 598 | >>> series1 = numpy.random.randint(0, 255, (4, 256, 256), 'uint16') 599 | >>> with TiffWriter('temp.tif') as tif: 600 | ... tif.write(series0, photometric='rgb') 601 | ... tif.write(series1, photometric='minisblack') 602 | ... 603 | 604 | Read the second image series from the TIFF file: 605 | 606 | .. code-block:: python 607 | 608 | >>> series1 = imread('temp.tif', series=1) 609 | >>> series1.shape 610 | (4, 256, 256) 611 | 612 | Successively write the frames of one contiguous series to a TIFF file: 613 | 614 | .. code-block:: python 615 | 616 | >>> data = numpy.random.randint(0, 255, (30, 301, 219), 'uint8') 617 | >>> with TiffWriter('temp.tif') as tif: 618 | ... for frame in data: 619 | ... tif.write(frame, contiguous=True) 620 | ... 621 | 622 | Append an image series to the existing TIFF file (note: this does not work 623 | with ImageJ hyperstack or OME-TIFF files): 624 | 625 | .. code-block:: python 626 | 627 | >>> data = numpy.random.randint(0, 255, (301, 219, 3), 'uint8') 628 | >>> imwrite('temp.tif', data, photometric='rgb', append=True) 629 | 630 | Create a TIFF file from a generator of tiles: 631 | 632 | .. code-block:: python 633 | 634 | >>> data = numpy.random.randint(0, 2**12, (31, 33, 3), 'uint16') 635 | >>> def tiles(data, tileshape): 636 | ... for y in range(0, data.shape[0], tileshape[0]): 637 | ... for x in range(0, data.shape[1], tileshape[1]): 638 | ... yield data[y : y + tileshape[0], x : x + tileshape[1]] 639 | ... 640 | >>> imwrite( 641 | ... 'temp.tif', 642 | ... tiles(data, (16, 16)), 643 | ... tile=(16, 16), 644 | ... shape=data.shape, 645 | ... dtype=data.dtype, 646 | ... photometric='rgb', 647 | ... ) 648 | 649 | Write a multi-dimensional, multi-resolution (pyramidal), multi-series OME-TIFF 650 | file with optional metadata. Sub-resolution images are written to SubIFDs. 651 | Limit parallel encoding to 2 threads. Write a thumbnail image as a separate 652 | image series: 653 | 654 | .. code-block:: python 655 | 656 | >>> data = numpy.random.randint(0, 255, (8, 2, 512, 512, 3), 'uint8') 657 | >>> subresolutions = 2 658 | >>> pixelsize = 0.29 # micrometer 659 | >>> with TiffWriter('temp.ome.tif', bigtiff=True) as tif: 660 | ... metadata = { 661 | ... 'axes': 'TCYXS', 662 | ... 'SignificantBits': 8, 663 | ... 'TimeIncrement': 0.1, 664 | ... 'TimeIncrementUnit': 's', 665 | ... 'PhysicalSizeX': pixelsize, 666 | ... 'PhysicalSizeXUnit': 'µm', 667 | ... 'PhysicalSizeY': pixelsize, 668 | ... 'PhysicalSizeYUnit': 'µm', 669 | ... 'Channel': {'Name': ['Channel 1', 'Channel 2']}, 670 | ... 'Plane': {'PositionX': [0.0] * 16, 'PositionXUnit': ['µm'] * 16}, 671 | ... 'Description': 'A multi-dimensional, multi-resolution image', 672 | ... 'MapAnnotation': { # for OMERO 673 | ... 'Namespace': 'openmicroscopy.org/PyramidResolution', 674 | ... '1': '256 256', 675 | ... '2': '128 128', 676 | ... }, 677 | ... } 678 | ... options = dict( 679 | ... photometric='rgb', 680 | ... tile=(128, 128), 681 | ... compression='jpeg', 682 | ... resolutionunit='CENTIMETER', 683 | ... maxworkers=2, 684 | ... ) 685 | ... tif.write( 686 | ... data, 687 | ... subifds=subresolutions, 688 | ... resolution=(1e4 / pixelsize, 1e4 / pixelsize), 689 | ... metadata=metadata, 690 | ... **options, 691 | ... ) 692 | ... # write pyramid levels to the two subifds 693 | ... # in production use resampling to generate sub-resolution images 694 | ... for level in range(subresolutions): 695 | ... mag = 2 ** (level + 1) 696 | ... tif.write( 697 | ... data[..., ::mag, ::mag, :], 698 | ... subfiletype=1, 699 | ... resolution=(1e4 / mag / pixelsize, 1e4 / mag / pixelsize), 700 | ... **options, 701 | ... ) 702 | ... # add a thumbnail image as a separate series 703 | ... # it is recognized by QuPath as an associated image 704 | ... thumbnail = (data[0, 0, ::8, ::8] >> 2).astype('uint8') 705 | ... tif.write(thumbnail, metadata={'Name': 'thumbnail'}) 706 | ... 707 | 708 | Access the image levels in the pyramidal OME-TIFF file: 709 | 710 | .. code-block:: python 711 | 712 | >>> baseimage = imread('temp.ome.tif') 713 | >>> second_level = imread('temp.ome.tif', series=0, level=1) 714 | >>> with TiffFile('temp.ome.tif') as tif: 715 | ... baseimage = tif.series[0].asarray() 716 | ... second_level = tif.series[0].levels[1].asarray() 717 | ... number_levels = len(tif.series[0].levels) # includes base level 718 | ... 719 | 720 | Iterate over and decode single JPEG compressed tiles in the TIFF file: 721 | 722 | .. code-block:: python 723 | 724 | >>> with TiffFile('temp.ome.tif') as tif: 725 | ... fh = tif.filehandle 726 | ... for page in tif.pages: 727 | ... for index, (offset, bytecount) in enumerate( 728 | ... zip(page.dataoffsets, page.databytecounts) 729 | ... ): 730 | ... _ = fh.seek(offset) 731 | ... data = fh.read(bytecount) 732 | ... tile, indices, shape = page.decode( 733 | ... data, index, jpegtables=page.jpegtables 734 | ... ) 735 | ... 736 | 737 | Use Zarr to read parts of the tiled, pyramidal images in the TIFF file: 738 | 739 | .. code-block:: python 740 | 741 | >>> import zarr 742 | >>> store = imread('temp.ome.tif', aszarr=True) 743 | >>> z = zarr.open(store, mode='r') 744 | >>> z 745 | 746 | >>> z['0'] # base layer 747 | 748 | >>> z['0'][2, 0, 128:384, 256:].shape # read a tile from the base layer 749 | (256, 256, 3) 750 | >>> store.close() 751 | 752 | Load the base layer from the Zarr store as a dask array: 753 | 754 | .. code-block:: python 755 | 756 | >>> import dask.array 757 | >>> store = imread('temp.ome.tif', aszarr=True) 758 | >>> dask.array.from_zarr(store, '0', zarr_format=2) 759 | dask.array<...shape=(8, 2, 512, 512, 3)...chunksize=(1, 1, 128, 128, 3)... 760 | >>> store.close() 761 | 762 | Write the Zarr store to a fsspec ReferenceFileSystem in JSON format: 763 | 764 | .. code-block:: python 765 | 766 | >>> store = imread('temp.ome.tif', aszarr=True) 767 | >>> store.write_fsspec('temp.ome.tif.json', url='file://') 768 | >>> store.close() 769 | 770 | Open the fsspec ReferenceFileSystem as a Zarr group: 771 | 772 | .. code-block:: python 773 | 774 | >>> from kerchunk.utils import refs_as_store 775 | >>> import imagecodecs.numcodecs 776 | >>> imagecodecs.numcodecs.register_codecs(verbose=False) 777 | >>> z = zarr.open(refs_as_store('temp.ome.tif.json'), mode='r') 778 | >>> z 779 | > 780 | 781 | Create an OME-TIFF file containing an empty, tiled image series and write 782 | to it via the Zarr interface (note: this does not work with compression): 783 | 784 | .. code-block:: python 785 | 786 | >>> imwrite( 787 | ... 'temp2.ome.tif', 788 | ... shape=(8, 800, 600), 789 | ... dtype='uint16', 790 | ... photometric='minisblack', 791 | ... tile=(128, 128), 792 | ... metadata={'axes': 'CYX'}, 793 | ... ) 794 | >>> store = imread('temp2.ome.tif', mode='r+', aszarr=True) 795 | >>> z = zarr.open(store, mode='r+') 796 | >>> z 797 | 798 | >>> z[3, 100:200, 200:300:2] = 1024 799 | >>> store.close() 800 | 801 | Read images from a sequence of TIFF files as NumPy array using two I/O worker 802 | threads: 803 | 804 | .. code-block:: python 805 | 806 | >>> imwrite('temp_C001T001.tif', numpy.random.rand(64, 64)) 807 | >>> imwrite('temp_C001T002.tif', numpy.random.rand(64, 64)) 808 | >>> image_sequence = imread( 809 | ... ['temp_C001T001.tif', 'temp_C001T002.tif'], ioworkers=2, maxworkers=1 810 | ... ) 811 | >>> image_sequence.shape 812 | (2, 64, 64) 813 | >>> image_sequence.dtype 814 | dtype('float64') 815 | 816 | Read an image stack from a series of TIFF files with a file name pattern 817 | as NumPy or Zarr arrays: 818 | 819 | .. code-block:: python 820 | 821 | >>> image_sequence = TiffSequence('temp_C0*.tif', pattern=r'_(C)(\d+)(T)(\d+)') 822 | >>> image_sequence.shape 823 | (1, 2) 824 | >>> image_sequence.axes 825 | 'CT' 826 | >>> data = image_sequence.asarray() 827 | >>> data.shape 828 | (1, 2, 64, 64) 829 | >>> store = image_sequence.aszarr() 830 | >>> zarr.open(store, mode='r', ioworkers=2, maxworkers=1) 831 | 832 | >>> image_sequence.close() 833 | 834 | Write the Zarr store to a fsspec ReferenceFileSystem in JSON format: 835 | 836 | .. code-block:: python 837 | 838 | >>> store = image_sequence.aszarr() 839 | >>> store.write_fsspec('temp.json', url='file://') 840 | 841 | Open the fsspec ReferenceFileSystem as a Zarr array: 842 | 843 | .. code-block:: python 844 | 845 | >>> from kerchunk.utils import refs_as_store 846 | >>> import tifffile.numcodecs 847 | >>> tifffile.numcodecs.register_codec() 848 | >>> zarr.open(refs_as_store('temp.json'), mode='r') 849 | shape=(1, 2, 64, 64) ...> 850 | 851 | Inspect the TIFF file from the command line:: 852 | 853 | $ python -m tifffile temp.ome.tif -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | dl { 2 | margin: 0; 3 | margin-top: 1em; 4 | margin-right: 0px; 5 | margin-bottom: 0px; 6 | margin-left: 0px; 7 | padding: 0; 8 | } -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # tifffile/docs/conf.py 2 | 3 | import os 4 | import sys 5 | 6 | here = os.path.dirname(__file__) 7 | sys.path.insert(0, os.path.split(here)[0]) 8 | 9 | import tifffile 10 | 11 | project = 'tifffile' 12 | copyright = '2008-2025, Christoph Gohlke' 13 | author = 'Christoph Gohlke' 14 | version = tifffile.__version__ 15 | 16 | extensions = [ 17 | 'sphinx.ext.napoleon', 18 | 'sphinx.ext.autodoc', 19 | 'sphinx.ext.autosummary', 20 | 'sphinx.ext.doctest', 21 | # 'sphinxcontrib.spelling', 22 | # 'sphinx.ext.viewcode', 23 | # 'sphinx.ext.autosectionlabel', 24 | # 'numpydoc', 25 | # 'sphinx_issues', 26 | ] 27 | 28 | templates_path = ['_templates'] 29 | 30 | exclude_patterns = [] 31 | 32 | html_theme = 'alabaster' 33 | 34 | html_static_path = ['_static'] 35 | html_css_files = ['custom.css'] 36 | html_show_sourcelink = False 37 | 38 | autodoc_member_order = 'bysource' # bysource, groupwise 39 | autodoc_default_flags = ['members'] 40 | autodoc_typehints = 'description' 41 | autodoc_type_aliases = {'ArrayLike': 'numpy.ArrayLike'} 42 | autoclass_content = 'class' 43 | autosectionlabel_prefix_document = True 44 | autosummary_generate = True 45 | 46 | napoleon_google_docstring = True 47 | napoleon_numpy_docstring = False 48 | -------------------------------------------------------------------------------- /docs/make.py: -------------------------------------------------------------------------------- 1 | # tifffile/docs/make.py 2 | 3 | """Make documentation for tifffile package using Sphinx.""" 4 | 5 | import os 6 | import sys 7 | 8 | from sphinx.cmd.build import main 9 | 10 | here = os.path.dirname(__file__) 11 | sys.path.insert(0, os.path.split(here)[0]) 12 | path = os.environ.get('PATH') 13 | if path: 14 | os.environ['PATH'] = os.path.join(sys.exec_prefix, 'Scripts') + ';' + path 15 | 16 | import tifffile # noqa 17 | 18 | members = [ 19 | 'imread', 20 | 'imwrite', 21 | 'memmap', 22 | 'TiffWriter', 23 | 'TiffFile', 24 | # 'TiffFileError', 25 | 'TiffFormat', 26 | 'TiffPage', 27 | 'TiffFrame', 28 | 'TiffPages', 29 | 'TiffTag', 30 | 'TiffTags', 31 | 'TiffTagRegistry', 32 | 'TiffPageSeries', 33 | 'TiffSequence', 34 | 'FileSequence', 35 | 'zarr.ZarrStore', 36 | 'zarr.ZarrTiffStore', 37 | 'zarr.ZarrFileSequenceStore', 38 | # Constants 39 | 'DATATYPE', 40 | 'SAMPLEFORMAT', 41 | 'PLANARCONFIG', 42 | 'COMPRESSION', 43 | 'PREDICTOR', 44 | 'EXTRASAMPLE', 45 | 'FILETYPE', 46 | 'PHOTOMETRIC', 47 | 'RESUNIT', 48 | 'CHUNKMODE', 49 | 'TIFF', 50 | # classes 51 | 'FileHandle', 52 | 'OmeXml', 53 | # 'OmeXmlError', 54 | 'Timer', 55 | 'NullContext', 56 | 'StoredShape', 57 | 'TiledSequence', 58 | # functions 59 | 'logger', 60 | 'repeat_nd', 61 | 'natural_sorted', 62 | 'parse_filenames', 63 | 'matlabstr2py', 64 | 'strptime', 65 | 'imagej_metadata_tag', 66 | 'imagej_description', 67 | # 'read_scanimage_metadata', 68 | 'read_micromanager_metadata', 69 | 'read_ndtiff_index', 70 | 'create_output', 71 | 'hexdump', 72 | 'xml2dict', 73 | 'tiffcomment', 74 | 'tiff2fsspec', 75 | 'lsm2bin', 76 | 'validate_jhove', 77 | 'imshow', 78 | '.geodb', 79 | ] 80 | 81 | title = f'tifffile {tifffile.__version__}' 82 | underline = '=' * len(title) 83 | members_ = [] 84 | for name in members: 85 | if not name: 86 | continue 87 | if '.' in name[1:]: 88 | name = name.rsplit('.', 1)[-1] 89 | members_.append(name.replace('.', '').lower()) 90 | memberlist = '\n '.join(members_) 91 | 92 | with open(here + '/index.rst', 'w') as fh: 93 | fh.write( 94 | f""".. tifffile documentation 95 | 96 | .. currentmodule:: tifffile 97 | 98 | {title} 99 | {underline} 100 | 101 | .. automodule:: tifffile 102 | 103 | .. toctree:: 104 | :hidden: 105 | :maxdepth: 2 106 | 107 | genindex 108 | license 109 | revisions 110 | examples 111 | 112 | 113 | .. toctree:: 114 | :hidden: 115 | :maxdepth: 2 116 | 117 | {memberlist} 118 | 119 | 120 | """ 121 | ) 122 | 123 | 124 | with open(here + '/genindex.rst', 'w') as fh: 125 | fh.write( 126 | """ 127 | Index 128 | ===== 129 | 130 | """ 131 | ) 132 | 133 | with open(here + '/license.rst', 'w') as fh: 134 | fh.write( 135 | """ 136 | License 137 | ======= 138 | 139 | .. include:: ../LICENSE 140 | """ 141 | ) 142 | 143 | 144 | with open(here + '/examples.rst', 'w') as fh: 145 | fh.write( 146 | """ 147 | Examples 148 | ======== 149 | 150 | See `#examples `_. 151 | """ 152 | ) 153 | 154 | 155 | with open(here + '/revisions.rst', 'w') as fh: 156 | fh.write(""".. include:: ../CHANGES.rst""") 157 | 158 | 159 | with open('tiff.rst', 'w') as fh: 160 | fh.write( 161 | """ 162 | .. currentmodule:: tifffile 163 | 164 | TIFF 165 | ==== 166 | 167 | .. autoclass:: tifffile.TIFF 168 | :members: 169 | 170 | .. autoclass:: tifffile._TIFF 171 | :members: 172 | """ 173 | ) 174 | 175 | 176 | automodule = """.. currentmodule:: {module} 177 | 178 | {name} 179 | {size} 180 | 181 | .. automodule:: {module}.{name} 182 | :members: 183 | 184 | """ 185 | 186 | autoclass = """.. currentmodule:: {module} 187 | 188 | {name} 189 | {size} 190 | 191 | .. autoclass:: {module}.{name} 192 | :members: 193 | 194 | """ 195 | 196 | automethod = """.. currentmodule:: {module} 197 | 198 | {name} 199 | {size} 200 | 201 | .. autofunction:: {name} 202 | 203 | """ 204 | 205 | for name in members: 206 | if not name or name == 'TIFF': 207 | continue 208 | 209 | if '.' in name[1:]: 210 | module, name = name.rsplit('.', 1) 211 | module = f'tifffile.{module}' 212 | else: 213 | module = 'tifffile' 214 | 215 | if name[0] == '.': 216 | template = automodule 217 | name = name[1:] 218 | elif name[0].isupper(): 219 | template = autoclass 220 | else: 221 | template = automethod 222 | size = '=' * len(name) 223 | 224 | with open(f'{here}/{name.lower()}.rst', 'w') as fh: 225 | fh.write(template.format(module=module, name=name, size=size)) 226 | 227 | main(['-b', 'html', here, here + '/html']) 228 | 229 | os.system('start html/index.html') 230 | -------------------------------------------------------------------------------- /examples/earthbigdata.py: -------------------------------------------------------------------------------- 1 | # tifffile/examples/earthbigdata.py 2 | 3 | # Copyright (c) 2021-2025, Christoph Gohlke 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, 10 | # this list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of the copyright holder nor the names of its 17 | # contributors may be used to endorse or promote products derived from 18 | # this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | # POSSIBILITY OF SUCH DAMAGE. 31 | 32 | # This file uses VSCode Jupyter-like code cells 33 | # https://code.visualstudio.com/docs/python/jupyter-support-py 34 | 35 | # %% [markdown] 36 | """ 37 | # Create an fsspec ReferenceFileSystem for a large set of remote GeoTIFF files 38 | 39 | by [Christoph Gohlke](https://www.cgohlke.com) 40 | 41 | Published Oct 9, 2021. Last updated May 21, 2025. 42 | 43 | This Python script uses the [tifffile](https://github.com/cgohlke/tifffile) and 44 | [imagecodecs](https://github.com/cgohlke/imagecodecs) packages to create a 45 | [fsspec ReferenceFileSystem](https://github.com/fsspec/kerchunk) file in 46 | JSON format for the [Earthbigdata]( 47 | http://sentinel-1-global-coherence-earthbigdata.s3-website-us-west-2.amazonaws.com 48 | ) set, which consists of 1,033,422 GeoTIFF files stored on AWS. 49 | The ReferenceFileSystem is used to create a multi-dimensional Xarray dataset. 50 | 51 | Refer to the discussion at [kerchunk/issues/78]( 52 | https://github.com/fsspec/kerchunk/issues/78). 53 | 54 | Source code is available in the [tifffile repository]( 55 | https://github.com/cgohlke/tifffile/blob/master/examples/earthbigdata.py). 56 | """ 57 | 58 | # %% 59 | import base64 60 | import os 61 | 62 | import imagecodecs.numcodecs 63 | import kerchunk.utils 64 | import matplotlib.pyplot 65 | import tifffile 66 | import tifffile.zarr 67 | import xarray 68 | import zarr # >= 3 69 | 70 | # %% [markdown] 71 | """ 72 | ## Get a list of all remote TIFF files 73 | 74 | Call the AWS command line app to recursively list all files in the Earthbigdata 75 | set. Cache the output in a local file. Filter the list for TIFF files and 76 | remove the common path. 77 | """ 78 | 79 | # %% 80 | if not os.path.exists('earthbigdata.txt'): 81 | os.system( 82 | 'aws s3 ls sentinel-1-global-coherence-earthbigdata/data/tiles' 83 | ' --recursive > earthbigdata.txt' 84 | ) 85 | 86 | with open('earthbigdata.txt', encoding='utf-8') as fh: 87 | tiff_files = [ 88 | line.split()[-1][11:] for line in fh.readlines() if '.tif' in line 89 | ] 90 | print('Number of TIFF files:', len(tiff_files)) 91 | 92 | 93 | # %% [markdown] 94 | """ 95 | ## Define metadata to describe the dataset 96 | 97 | Define labels, coordinate arrays, file name regular expression patterns, and 98 | categories for all dimensions in the Earthbigdata set. 99 | """ 100 | 101 | # %% 102 | baseurl = ( 103 | 'https://' 104 | 'sentinel-1-global-coherence-earthbigdata.s3.us-west-2.amazonaws.com' 105 | '/data/tiles/' 106 | ) 107 | 108 | chunkshape = (1200, 1200) 109 | fillvalue = 0 110 | 111 | latitude_label = 'latitude' 112 | latitude_pattern = rf'(?P<{latitude_label}>[NS]\d+)' 113 | latitude_coordinates = [ 114 | (j * -0.00083333333 - 0.000416666665 + i) 115 | for i in range(82, -79, -1) 116 | for j in range(1200) 117 | ] 118 | latitude_category = {} 119 | i = 0 120 | for j in range(82, -1, -1): 121 | latitude_category[f'N{j:-02}'] = i 122 | i += 1 123 | for j in range(1, 79): 124 | latitude_category[f'S{j:-02}'] = i 125 | i += 1 126 | 127 | longitude_label = 'longitude' 128 | longitude_pattern = rf'(?P<{longitude_label}>[EW]\d+)' 129 | longitude_coordinates = [ 130 | (j * 0.00083333333 + 0.000416666665 + i) 131 | for i in range(-180, 180) 132 | for j in range(1200) 133 | ] 134 | longitude_category = {} 135 | i = 0 136 | for j in range(180, 0, -1): 137 | longitude_category[f'W{j:-03}'] = i 138 | i += 1 139 | for j in range(180): 140 | longitude_category[f'E{j:-03}'] = i 141 | i += 1 142 | 143 | season_label = 'season' 144 | season_category = {'winter': 0, 'spring': 1, 'summer': 2, 'fall': 3} 145 | season_coordinates = list(season_category.keys()) 146 | season_pattern = rf'_(?P<{season_label}>{"|".join(season_category)})' 147 | 148 | polarization_label = 'polarization' 149 | polarization_category = {'vv': 0, 'vh': 1, 'hv': 2, 'hh': 3} 150 | polarization_coordinates = list(polarization_category.keys()) 151 | polarization_pattern = ( 152 | rf'_(?P<{polarization_label}>{"|".join(polarization_category)})' 153 | ) 154 | 155 | coherence_label = 'coherence' 156 | coherence_category = { 157 | '06': 0, 158 | '12': 1, 159 | '18': 2, 160 | '24': 3, 161 | '36': 4, 162 | '48': 5, 163 | } 164 | coherence_coordinates = list(int(i) for i in coherence_category.keys()) 165 | coherence_pattern = ( 166 | rf'_COH(?P<{coherence_label}>{"|".join(coherence_category)})' 167 | ) 168 | 169 | orbit_label = 'orbit' 170 | orbit_coordinates = list(range(1, 176)) 171 | orbit_pattern = rf'_(?P<{orbit_label}>\d+)' 172 | 173 | flightdirection_label = 'flightdirection' 174 | flightdirection_category = {'A': 0, 'D': 1} 175 | flightdirection_coordinates = list(flightdirection_category.keys()) 176 | flightdirection_pattern = ( 177 | rf'(?P<{flightdirection_label}>[{"|".join(flightdirection_category)}])_' 178 | ) 179 | 180 | 181 | # %% [markdown] 182 | """ 183 | ## Open a file for writing the fsspec ReferenceFileSystem in JSON format 184 | """ 185 | 186 | # %% 187 | jsonfile = open('earthbigdata.json', 'w', encoding='utf-8', newline='\n') 188 | 189 | # %% [markdown] 190 | """ 191 | ## Write the coordinate arrays 192 | 193 | Add the coordinate arrays to a Zarr group, convert it to a fsspec 194 | ReferenceFileSystem JSON string, and write it to the open file. 195 | """ 196 | 197 | # %% 198 | coordinates = {} # type: ignore[var-annotated] 199 | zarrgroup = zarr.open_group(coordinates, use_consolidated=False, zarr_format=2) 200 | zararray = zarrgroup.create_array( 201 | longitude_label, 202 | shape=[len(longitude_coordinates)], 203 | dtype='float32', 204 | # compressor='zlib', 205 | ) 206 | zararray[:] = longitude_coordinates 207 | zararray.attrs['_ARRAY_DIMENSIONS'] = [longitude_label] 208 | 209 | zararray = zarrgroup.create_array( 210 | latitude_label, 211 | shape=[len(latitude_coordinates)], 212 | dtype='float32', 213 | # compressor='zlib', 214 | ) 215 | zararray[:] = latitude_coordinates 216 | zararray.attrs['_ARRAY_DIMENSIONS'] = [latitude_label] 217 | 218 | zararray = zarrgroup.create_array( 219 | season_label, 220 | shape=[len(season_coordinates)], 221 | dtype='|U6', 222 | compressor=None, 223 | ) 224 | zararray[:] = season_coordinates 225 | zararray.attrs['_ARRAY_DIMENSIONS'] = [season_label] 226 | 227 | zararray = zarrgroup.create_array( 228 | polarization_label, 229 | shape=[len(polarization_coordinates)], 230 | dtype='|U2', 231 | compressor=None, 232 | ) 233 | zararray[:] = polarization_coordinates 234 | zararray.attrs['_ARRAY_DIMENSIONS'] = [polarization_label] 235 | 236 | zararray = zarrgroup.create_array( 237 | coherence_label, 238 | shape=[len(coherence_coordinates)], 239 | dtype='uint8', 240 | compressor=None, 241 | ) 242 | zararray[:] = coherence_coordinates 243 | zararray.attrs['_ARRAY_DIMENSIONS'] = [coherence_label] 244 | 245 | zararray = zarrgroup.create_array( 246 | orbit_label, shape=[len(orbit_coordinates)], dtype='int32' 247 | ) 248 | zararray[:] = orbit_coordinates 249 | zararray.attrs['_ARRAY_DIMENSIONS'] = [orbit_label] 250 | 251 | zararray = zarrgroup.create_array( 252 | flightdirection_label, 253 | shape=[len(flightdirection_coordinates)], 254 | dtype='|U1', 255 | compressor=None, 256 | ) 257 | zararray[:] = flightdirection_coordinates 258 | zararray.attrs['_ARRAY_DIMENSIONS'] = [flightdirection_label] 259 | 260 | # base64 encode any values containing non-ascii characters 261 | for k, v in coordinates.items(): 262 | v = v.to_bytes() 263 | try: 264 | coordinates[k] = v.decode() 265 | except UnicodeDecodeError: 266 | coordinates[k] = 'base64:' + base64.b64encode(v).decode() 267 | 268 | coordinates_json = tifffile.zarr._json_dumps(coordinates).decode() 269 | 270 | jsonfile.write(coordinates_json[:-2]) # skip the last newline and brace 271 | 272 | # %% [markdown] 273 | """ 274 | ## Create a TiffSequence from a list of file names 275 | 276 | Filter the list of GeoTIFF files for files containing coherence 'COH' data. 277 | The regular expression pattern and categories are used to parse the file names 278 | for chunk indices. 279 | 280 | Note: the created TiffSequence cannot be used to access any files. The file 281 | names do not refer to existing files. The `baseurl` is later used to get 282 | the real location of the files. 283 | """ 284 | 285 | # %% 286 | mode = 'COH' 287 | fileseq = tifffile.TiffSequence( 288 | [file for file in tiff_files if '_' + mode in file], 289 | pattern=( 290 | latitude_pattern 291 | + longitude_pattern 292 | + season_pattern 293 | + polarization_pattern 294 | + coherence_pattern 295 | ), 296 | categories={ 297 | latitude_label: latitude_category, 298 | longitude_label: longitude_category, 299 | season_label: season_category, 300 | polarization_label: polarization_category, 301 | coherence_label: coherence_category, 302 | }, 303 | ) 304 | assert len(fileseq) == 444821 305 | assert fileseq.files_missing == 5119339 306 | assert fileseq.shape == (161, 360, 4, 4, 6) 307 | assert fileseq.dims == ( 308 | 'latitude', 309 | 'longitude', 310 | 'season', 311 | 'polarization', 312 | 'coherence', 313 | ) 314 | print(fileseq) 315 | 316 | 317 | # %% [markdown] 318 | """ 319 | ## Create a ZarrTiffStore from the TiffSequence 320 | 321 | Define `axestiled` to tile the latitude and longitude dimensions of the 322 | TiffSequence with the first and second image/chunk dimensions. 323 | Define extra `zattrs` to create an Xarray compatible store. 324 | """ 325 | 326 | # %% 327 | store = fileseq.aszarr( 328 | chunkdtype='uint8', 329 | chunkshape=chunkshape, 330 | fillvalue=fillvalue, 331 | axestiled={0: 0, 1: 1}, 332 | zattrs={ 333 | '_ARRAY_DIMENSIONS': [ 334 | season_label, 335 | polarization_label, 336 | coherence_label, 337 | latitude_label, 338 | longitude_label, 339 | ] 340 | }, 341 | ) 342 | print(store) 343 | 344 | # %% [markdown] 345 | """ 346 | ## Append the ZarrTiffStore to the open ReferenceFileSystem file 347 | 348 | Use the mode name to create a Zarr subgroup. 349 | Use the `imagecodecs_tiff` Numcodecs compatible codec for decoding TIFF files. 350 | """ 351 | 352 | # %% 353 | store.write_fsspec( 354 | jsonfile, 355 | baseurl, 356 | groupname=mode, 357 | codec_id='imagecodecs_tiff', 358 | _append=True, 359 | _close=False, 360 | ) 361 | 362 | # %% [markdown] 363 | """ 364 | ## Repeat for the other modes 365 | 366 | Repeat the `TiffSequence->aszarr->write_fsspec` workflow for the other modes. 367 | """ 368 | 369 | # %% 370 | for mode in ( 371 | 'AMP', 372 | 'tau', 373 | 'rmse', 374 | 'rho', 375 | ): 376 | fileseq = tifffile.TiffSequence( 377 | [file for file in tiff_files if '_' + mode in file], 378 | pattern=( 379 | latitude_pattern 380 | + longitude_pattern 381 | + season_pattern 382 | + polarization_pattern 383 | ), 384 | categories={ 385 | latitude_label: latitude_category, 386 | longitude_label: longitude_category, 387 | season_label: season_category, 388 | polarization_label: polarization_category, 389 | }, 390 | ) 391 | print(fileseq) 392 | with fileseq.aszarr( 393 | chunkdtype='uint16', 394 | chunkshape=chunkshape, 395 | fillvalue=fillvalue, 396 | axestiled={0: 0, 1: 1}, 397 | zattrs={ 398 | '_ARRAY_DIMENSIONS': [ 399 | season_label, 400 | polarization_label, 401 | latitude_label, 402 | longitude_label, 403 | ] 404 | }, 405 | ) as store: 406 | print(store) 407 | store.write_fsspec( 408 | jsonfile, 409 | baseurl, 410 | groupname=mode, 411 | codec_id='imagecodecs_tiff', 412 | _append=True, 413 | _close=False, 414 | ) 415 | 416 | 417 | for mode in ('inc', 'lsmap'): 418 | fileseq = tifffile.TiffSequence( 419 | [file for file in tiff_files if '_' + mode in file], 420 | pattern=( 421 | latitude_pattern 422 | + longitude_pattern 423 | + orbit_pattern 424 | + flightdirection_pattern 425 | ), 426 | categories={ 427 | latitude_label: latitude_category, 428 | longitude_label: longitude_category, 429 | # orbit has no category 430 | flightdirection_label: flightdirection_category, 431 | }, 432 | ) 433 | print(fileseq) 434 | with fileseq.aszarr( 435 | chunkdtype='uint8', 436 | chunkshape=chunkshape, 437 | fillvalue=fillvalue, 438 | axestiled={0: 0, 1: 1}, 439 | zattrs={ 440 | '_ARRAY_DIMENSIONS': [ 441 | orbit_label, 442 | flightdirection_label, 443 | latitude_label, 444 | longitude_label, 445 | ] 446 | }, 447 | ) as store: 448 | print(store) 449 | store.write_fsspec( 450 | jsonfile, 451 | baseurl, 452 | groupname=mode, 453 | codec_id='imagecodecs_tiff', 454 | _append=True, 455 | _close=mode == 'lsmap', # close after last store 456 | ) 457 | 458 | # %% [markdown] 459 | """ 460 | ## Close the JSON file 461 | """ 462 | 463 | # %% 464 | jsonfile.close() 465 | 466 | # %% [markdown] 467 | """ 468 | ## Use the fsspec ReferenceFileSystem file to create an Xarray dataset 469 | 470 | Register `imagecodecs.numcodecs` before using the ReferenceFileSystem. 471 | """ 472 | 473 | # %% 474 | imagecodecs.numcodecs.register_codecs() 475 | 476 | # %% [markdown] 477 | """ 478 | ### Create an Xarray dataset from the ReferenceFileSystem file 479 | 480 | Use `mask_and_scale` to disable conversion to floating point. 481 | """ 482 | 483 | # %% 484 | store = kerchunk.utils.refs_as_store('earthbigdata.json') 485 | dataset = xarray.open_zarr(store, consolidated=False, mask_and_scale=False) 486 | print(dataset) 487 | 488 | # %% [markdown] 489 | """ 490 | ### Select the Southern California region in the dataset 491 | """ 492 | 493 | # %% 494 | socal = dataset.sel(latitude=slice(36, 32.5), longitude=slice(-121, -115)) 495 | print(socal) 496 | 497 | # %% [markdown] 498 | """ 499 | ### Plot a selection of the dataset 500 | 501 | The few GeoTIFF files comprising the selection are transparently downloaded, 502 | decoded, and stitched to an in-memory NumPy array and plotted using Matplotlib. 503 | """ 504 | 505 | # %% 506 | image = socal['COH'].loc['winter', 'vv', 12] 507 | assert image[100, 100] == 53 508 | xarray.plot.imshow(image, size=6, aspect=1.8) 509 | matplotlib.pyplot.show() 510 | 511 | # %% [markdown] 512 | """ 513 | ## System information 514 | 515 | Print information about the software used to run this script. 516 | """ 517 | 518 | 519 | # %% 520 | def system_info() -> str: 521 | """Print information about Python environment.""" 522 | import datetime 523 | import sys 524 | 525 | import fsspec 526 | import imagecodecs 527 | import kerchunk 528 | import matplotlib 529 | import numcodecs 530 | import numpy 531 | import tifffile 532 | import xarray 533 | import zarr 534 | 535 | return '\n'.join( 536 | ( 537 | sys.executable, 538 | f'Python {sys.version}', 539 | '', 540 | f'numpy {numpy.__version__}', 541 | f'matplotlib {matplotlib.__version__}', 542 | f'tifffile {tifffile.__version__}', 543 | f'imagecodecs {imagecodecs.__version__}', 544 | f'numcodecs {numcodecs.__version__}', 545 | f'fsspec {fsspec.__version__}', 546 | f'kerchunk {kerchunk.__version__}', 547 | f'xarray {xarray.__version__}', 548 | f'zarr {zarr.__version__}', 549 | '', 550 | str(datetime.datetime.now()), 551 | ) 552 | ) 553 | 554 | 555 | print(system_info()) 556 | -------------------------------------------------------------------------------- /examples/issue125.py: -------------------------------------------------------------------------------- 1 | # tifffile/examples/issues125.py 2 | 3 | """Create a Fsspec ReferenceFileSystem for a sequence of TIFF files on S3. 4 | 5 | This Python script uses the Tifffile and Fsspec libraries to create a 6 | multiscale ReferenceFileSystem JSON file for a sequence of cloud optimized 7 | GeoTIFF (COG) files stored on S3. The tiles of the COG files are used as 8 | chunks. No additional Numcodecs codec needs to be registered since the COG 9 | files use Zlib compression. An Xarray dataset is created from the 10 | ReferenceFileSystem file and a subset of the dataset is plotted. 11 | 12 | See https://github.com/cgohlke/tifffile/issues/125 13 | 14 | """ 15 | 16 | import fsspec 17 | import kerchunk.utils 18 | import tifffile 19 | import xarray 20 | from matplotlib import pyplot 21 | 22 | # get a list of cloud optimized GeoTIFF files stored on S3 23 | remote_options = { 24 | 'anon': True, 25 | 'client_kwargs': {'endpoint_url': 'https://mghp.osn.xsede.org'}, 26 | } 27 | fs = fsspec.filesystem('s3', **remote_options) 28 | files = [f's3://{f}' for f in fs.ls('/rsignellbucket1/lcmap/cog')] 29 | 30 | # write the ReferenceFileSystem of each file to a JSON file 31 | with open('issue125.json', 'w', encoding='utf-8', newline='\n') as jsonfile: 32 | for i, filename in enumerate(tifffile.natural_sorted(files)): 33 | url, name = filename.rsplit('/', 1) 34 | with fs.open(filename) as fh: 35 | with tifffile.TiffFile(fh, name=name) as tif: 36 | print(tif.geotiff_metadata) 37 | with tif.series[0].aszarr() as store: 38 | store.write_fsspec( 39 | jsonfile, 40 | url=url, 41 | # using an experimental API: 42 | _shape=[len(files)], # shape of file sequence 43 | _axes='T', # axes of file sequence 44 | _index=[i], # index of this file in sequence 45 | _append=i != 0, # if True, only write index keys+value 46 | _close=i == len(files) - 1, # if True, no more appends 47 | # groupname='0', # required for non-pyramidal series 48 | ) 49 | 50 | 51 | # create a xarray dataset from the ReferenceFileSystem file 52 | 53 | store = kerchunk.utils.refs_as_store('issue125.json') 54 | dataset = xarray.open_zarr(store, consolidated=False, mask_and_scale=False) 55 | print(dataset) 56 | 57 | # plot a slice of the 5th pyramidal level of the dataset 58 | xarray.plot.imshow(dataset['5'][0, 32:-32, 32:-32]) 59 | pyplot.show() 60 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # tifffile/setup.py 2 | 3 | """Tifffile package Setuptools script.""" 4 | 5 | import re 6 | import sys 7 | 8 | from setuptools import setup 9 | 10 | buildnumber = '' 11 | 12 | 13 | def search(pattern: str, string: str, flags: int = 0) -> str: 14 | """Return first match of pattern in string.""" 15 | match = re.search(pattern, string, flags) 16 | if match is None: 17 | raise ValueError(f'{pattern!r} not found') 18 | return match.groups()[0] 19 | 20 | 21 | def fix_docstring_examples(docstring: str) -> str: 22 | """Return docstring with examples fixed for GitHub.""" 23 | start = True 24 | indent = False 25 | lines = ['..', ' This file is generated by setup.py', ''] 26 | for line in docstring.splitlines(): 27 | if not line.strip(): 28 | start = True 29 | indent = False 30 | if line.startswith('>>> '): 31 | indent = True 32 | if start: 33 | lines.extend(['.. code-block:: python', '']) 34 | start = False 35 | lines.append((' ' if indent else '') + line) 36 | return '\n'.join(lines) 37 | 38 | 39 | with open('tifffile/tifffile.py', encoding='utf-8') as fh: 40 | code = fh.read().replace('\r\n', '\n').replace('\r', '\n') 41 | 42 | version = search(r"__version__ = '(.*?)'", code).replace('.x.x', '.dev0') 43 | version += ('.' + buildnumber) if buildnumber else '' 44 | 45 | description = search(r'"""(.*)\.(?:\r\n|\r|\n)', code) 46 | 47 | readme = search( 48 | r'(?:\r\n|\r|\n){2}r"""(.*)"""(?:\r\n|\r|\n){2}from __future__', 49 | code, 50 | re.MULTILINE | re.DOTALL, 51 | ) 52 | readme = '\n'.join( 53 | [description, '=' * len(description)] + readme.splitlines()[1:] 54 | ) 55 | 56 | if 'sdist' in sys.argv: 57 | # update README, LICENSE, and CHANGES files 58 | 59 | with open('README.rst', 'w', encoding='utf-8') as fh: 60 | fh.write(fix_docstring_examples(readme)) 61 | 62 | license = search( 63 | r'(# Copyright.*?(?:\r\n|\r|\n))(?:\r\n|\r|\n)+r""', 64 | code, 65 | re.MULTILINE | re.DOTALL, 66 | ) 67 | license = license.replace('# ', '').replace('#', '') 68 | 69 | with open('LICENSE', 'w', encoding='utf-8') as fh: 70 | fh.write('BSD-3-Clause license\n\n') 71 | fh.write(license) 72 | 73 | revisions = search( 74 | r'(?:\r\n|\r|\n){2}(Revisions.*)- …', 75 | readme, 76 | re.MULTILINE | re.DOTALL, 77 | ).strip() 78 | 79 | with open('CHANGES.rst', encoding='utf-8') as fh: 80 | old = fh.read() 81 | 82 | old = old.split(revisions.splitlines()[-1])[-1] 83 | with open('CHANGES.rst', 'w', encoding='utf-8') as fh: 84 | fh.write(revisions.strip()) 85 | fh.write(old) 86 | 87 | setup( 88 | name='tifffile', 89 | version=version, 90 | license='BSD-3-Clause', 91 | description=description, 92 | long_description=readme, 93 | long_description_content_type='text/x-rst', 94 | author='Christoph Gohlke', 95 | author_email='cgohlke@cgohlke.com', 96 | url='https://www.cgohlke.com', 97 | project_urls={ 98 | 'Bug Tracker': 'https://github.com/cgohlke/tifffile/issues', 99 | 'Source Code': 'https://github.com/cgohlke/tifffile', 100 | # 'Documentation': 'https://', 101 | }, 102 | packages=['tifffile'], 103 | package_data={'tifffile': ['py.typed']}, 104 | python_requires='>=3.11', 105 | install_requires=[ 106 | 'numpy', 107 | # 'imagecodecs>=2023.8.12', 108 | ], 109 | extras_require={ 110 | 'codecs': ['imagecodecs>=2024.12.30'], 111 | 'xml': ['defusedxml', 'lxml'], 112 | 'zarr': ['zarr>=3', 'fsspec', 'kerchunk'], 113 | 'plot': ['matplotlib'], 114 | 'all': [ 115 | 'imagecodecs>=2024.12.30', 116 | 'matplotlib', 117 | 'defusedxml', 118 | 'lxml', 119 | 'zarr>=3', 120 | 'fsspec', 121 | 'kerchunk', 122 | ], 123 | 'test': [ 124 | 'cmapfile', 125 | 'czifile', 126 | 'dask', 127 | 'defusedxml', 128 | 'fsspec', 129 | 'imagecodecs', 130 | 'kerchunk', 131 | 'lfdfiles', 132 | 'lxml', 133 | 'ndtiff', 134 | 'oiffile', 135 | 'psdtags', 136 | 'pytest', 137 | 'requests', 138 | 'roifile', 139 | 'xarray', 140 | 'zarr>=3', 141 | ], 142 | }, 143 | entry_points={ 144 | 'console_scripts': [ 145 | 'tifffile = tifffile:main', 146 | 'tiffcomment = tifffile.tiffcomment:main', 147 | 'tiff2fsspec = tifffile.tiff2fsspec:main', 148 | 'lsm2bin = tifffile.lsm2bin:main', 149 | ], 150 | # 'napari.plugin': ['tifffile = tifffile.napari_tifffile'], 151 | }, 152 | platforms=['any'], 153 | classifiers=[ 154 | 'Development Status :: 4 - Beta', 155 | 'Intended Audience :: Science/Research', 156 | 'Intended Audience :: Developers', 157 | 'Operating System :: OS Independent', 158 | 'Programming Language :: Python :: 3 :: Only', 159 | 'Programming Language :: Python :: 3.11', 160 | 'Programming Language :: Python :: 3.12', 161 | 'Programming Language :: Python :: 3.13', 162 | 'Programming Language :: Python :: 3.14', 163 | ], 164 | ) 165 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # tifffile/tests/conftest.py 2 | 3 | from __future__ import annotations 4 | 5 | import os 6 | import sys 7 | from typing import Any 8 | 9 | if os.environ.get('VSCODE_CWD'): 10 | # work around pytest not using PYTHONPATH in VSCode 11 | sys.path.insert( 12 | 0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 13 | ) 14 | 15 | if os.environ.get('SKIP_CODECS', None): 16 | sys.modules['imagecodecs'] = None # type: ignore[assignment] 17 | 18 | 19 | def pytest_report_header(config: Any, start_path: Any) -> Any: 20 | try: 21 | from numpy import __version__ as numpy 22 | from test_tifffile import config 23 | from tifffile import __version__ as tifffile 24 | 25 | try: 26 | from imagecodecs import __version__ as imagecodecs 27 | except ImportError: 28 | imagecodecs = 'N/A' 29 | try: 30 | from zarr import __version__ as zarr 31 | except ImportError: 32 | zarr = 'N/A' 33 | try: 34 | from dask import __version__ as dask 35 | except ImportError: 36 | dask = 'N/A' 37 | try: 38 | from xarray import __version__ as xarray 39 | except ImportError: 40 | xarray = 'N/A' 41 | try: 42 | from fsspec import __version__ as fsspec 43 | except ImportError: 44 | fsspec = 'N/A' 45 | return ( 46 | f'versions: tifffile-{tifffile}, ' 47 | f'imagecodecs-{imagecodecs}, ' 48 | f'numpy-{numpy}, ' 49 | f'zarr-{zarr}, ' 50 | f'dask-{dask}, ' 51 | f'xarray-{xarray}, ' 52 | f'fsspec-{fsspec}\n' 53 | f'test config: {config()}' 54 | ) 55 | except Exception: 56 | pass 57 | 58 | 59 | collect_ignore = ['_tmp', 'data', 'data-'] 60 | -------------------------------------------------------------------------------- /tifffile/__init__.py: -------------------------------------------------------------------------------- 1 | # tifffile/__init__.py 2 | 3 | from .tifffile import * 4 | from .tifffile import __all__, __doc__, __version__, main 5 | 6 | 7 | def _set_module() -> None: 8 | """Set __module__ attribute for all public objects.""" 9 | globs = globals() 10 | for item in __all__: 11 | obj = globs[item] 12 | if hasattr(obj, '__module__'): 13 | obj.__module__ = 'tifffile' 14 | main.__module__ = 'tifffile' 15 | 16 | 17 | _set_module() 18 | -------------------------------------------------------------------------------- /tifffile/__main__.py: -------------------------------------------------------------------------------- 1 | # tifffile/__main__.py 2 | 3 | """Tifffile package command line script.""" 4 | 5 | import sys 6 | 7 | from .tifffile import main 8 | 9 | sys.exit(main()) 10 | -------------------------------------------------------------------------------- /tifffile/_imagecodecs.py: -------------------------------------------------------------------------------- 1 | # tifffile/_imagecodecs.py 2 | 3 | # Copyright (c) 2008-2024, Christoph Gohlke 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, 10 | # this list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of the copyright holder nor the names of its 17 | # contributors may be used to endorse or promote products derived from 18 | # this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | # POSSIBILITY OF SUCH DAMAGE. 31 | 32 | """Fallback imagecodecs codecs. 33 | 34 | This module provides alternative, pure Python and NumPy implementations of 35 | some functions of the `imagecodecs`_ package. The functions may raise 36 | `NotImplementedError`. 37 | 38 | .. _imagecodecs: https://github.com/cgohlke/imagecodecs 39 | 40 | """ 41 | 42 | from __future__ import annotations 43 | 44 | __all__ = [ 45 | 'bitorder_decode', 46 | 'delta_decode', 47 | 'delta_encode', 48 | 'float24_decode', 49 | 'lzma_decode', 50 | 'lzma_encode', 51 | 'packbits_decode', 52 | 'packints_decode', 53 | 'packints_encode', 54 | 'zlib_decode', 55 | 'zlib_encode', 56 | 'zstd_decode', 57 | 'zstd_encode', 58 | ] 59 | 60 | from typing import TYPE_CHECKING, overload 61 | 62 | import numpy 63 | 64 | if TYPE_CHECKING: 65 | from typing import Any, Literal 66 | 67 | from numpy.typing import DTypeLike, NDArray 68 | 69 | try: 70 | import lzma 71 | 72 | def lzma_encode( 73 | data: bytes | NDArray[Any], 74 | /, 75 | level: int | None = None, 76 | *, 77 | out: Any = None, 78 | ) -> bytes: 79 | """Compress LZMA.""" 80 | if isinstance(data, numpy.ndarray): 81 | data = data.tobytes() 82 | return lzma.compress(data) 83 | 84 | def lzma_decode(data: bytes, /, *, out: Any = None) -> bytes: 85 | """Decompress LZMA.""" 86 | return lzma.decompress(data) 87 | 88 | except ImportError: 89 | # Python was built without lzma 90 | def lzma_encode( 91 | data: bytes | NDArray[Any], 92 | /, 93 | level: int | None = None, 94 | *, 95 | out: Any = None, 96 | ) -> bytes: 97 | """Raise ImportError.""" 98 | import lzma # noqa 99 | 100 | return b'' 101 | 102 | def lzma_decode(data: bytes, /, *, out: Any = None) -> bytes: 103 | """Raise ImportError.""" 104 | import lzma # noqa 105 | 106 | return b'' 107 | 108 | 109 | try: 110 | import zlib 111 | 112 | def zlib_encode( 113 | data: bytes | NDArray[Any], 114 | /, 115 | level: int | None = None, 116 | *, 117 | out: Any = None, 118 | ) -> bytes: 119 | """Compress Zlib DEFLATE.""" 120 | if isinstance(data, numpy.ndarray): 121 | data = data.tobytes() 122 | return zlib.compress(data, 6 if level is None else level) 123 | 124 | def zlib_decode(data: bytes, /, *, out: Any = None) -> bytes: 125 | """Decompress Zlib DEFLATE.""" 126 | return zlib.decompress(data) 127 | 128 | except ImportError: 129 | # Python was built without zlib 130 | 131 | def zlib_encode( 132 | data: bytes | NDArray[Any], 133 | /, 134 | level: int | None = None, 135 | *, 136 | out: Any = None, 137 | ) -> bytes: 138 | """Raise ImportError.""" 139 | import zlib # noqa 140 | 141 | return b'' 142 | 143 | def zlib_decode(data: bytes, /, *, out: Any = None) -> bytes: 144 | """Raise ImportError.""" 145 | import zlib # noqa 146 | 147 | return b'' 148 | 149 | 150 | try: 151 | from compression import zstd 152 | 153 | def zstd_encode( 154 | data: bytes | NDArray[Any], 155 | /, 156 | level: int | None = None, 157 | *, 158 | out: Any = None, 159 | ) -> bytes: 160 | """Compress ZSTD.""" 161 | if isinstance(data, numpy.ndarray): 162 | data = data.tobytes() 163 | return zstd.compress(data, level=level) 164 | 165 | def zstd_decode(data: bytes, /, *, out: Any = None) -> bytes: 166 | """Decompress ZSTD.""" 167 | return zstd.decompress(data) 168 | 169 | except ImportError: 170 | # Python was built without zstd 171 | def zstd_encode( 172 | data: bytes | NDArray[Any], 173 | /, 174 | level: int | None = None, 175 | *, 176 | out: Any = None, 177 | ) -> bytes: 178 | """Raise ImportError.""" 179 | from compression import zstd # noqa 180 | 181 | return b'' 182 | 183 | def zstd_decode(data: bytes, /, *, out: Any = None) -> bytes: 184 | """Raise ImportError.""" 185 | from compression import zstd # noqa 186 | 187 | return b'' 188 | 189 | 190 | def packbits_decode(encoded: bytes, /, *, out: Any = None) -> bytes: 191 | r"""Decompress PackBits encoded byte string. 192 | 193 | >>> packbits_decode(b'\x80\x80') # NOP 194 | b'' 195 | >>> packbits_decode(b'\x02123') 196 | b'123' 197 | >>> packbits_decode( 198 | ... b'\xfe\xaa\x02\x80\x00\x2a\xfd\xaa\x03\x80\x00\x2a\x22\xf7\xaa' 199 | ... )[:-5] 200 | b'\xaa\xaa\xaa\x80\x00*\xaa\xaa\xaa\xaa\x80\x00*"\xaa\xaa\xaa\xaa\xaa' 201 | 202 | """ 203 | out = [] 204 | out_extend = out.extend 205 | i = 0 206 | try: 207 | while True: 208 | n = ord(encoded[i : i + 1]) + 1 209 | i += 1 210 | if n > 129: 211 | # replicate 212 | out_extend(encoded[i : i + 1] * (258 - n)) 213 | i += 1 214 | elif n < 129: 215 | # literal 216 | out_extend(encoded[i : i + n]) 217 | i += n 218 | except TypeError: 219 | pass 220 | return bytes(out) 221 | 222 | 223 | @overload 224 | def delta_encode( 225 | data: bytes | bytearray, 226 | /, 227 | axis: int = -1, 228 | dist: int = 1, 229 | *, 230 | out: Any = None, 231 | ) -> bytes: ... 232 | 233 | 234 | @overload 235 | def delta_encode( 236 | data: NDArray[Any], /, axis: int = -1, dist: int = 1, *, out: Any = None 237 | ) -> NDArray[Any]: ... 238 | 239 | 240 | def delta_encode( 241 | data: bytes | bytearray | NDArray[Any], 242 | /, 243 | axis: int = -1, 244 | dist: int = 1, 245 | *, 246 | out: Any = None, 247 | ) -> bytes | NDArray[Any]: 248 | """Encode Delta.""" 249 | if dist != 1: 250 | raise NotImplementedError( 251 | f"delta_encode with {dist=} requires the 'imagecodecs' package" 252 | ) 253 | if isinstance(data, (bytes, bytearray)): 254 | data = numpy.frombuffer(data, dtype=numpy.uint8) 255 | diff = numpy.diff(data, axis=0) 256 | return numpy.insert(diff, 0, data[0]).tobytes() 257 | 258 | dtype = data.dtype 259 | if dtype.kind == 'f': 260 | data = data.view(f'{dtype.byteorder}u{dtype.itemsize}') 261 | diff = numpy.diff(data, axis=axis) 262 | key: list[int | slice] = [slice(None)] * data.ndim 263 | key[axis] = 0 264 | diff = numpy.insert(diff, 0, data[tuple(key)], axis=axis) 265 | if not data.dtype.isnative: 266 | diff = diff.byteswap(True) 267 | diff = diff.view(diff.dtype.newbyteorder()) 268 | if dtype.kind == 'f': 269 | return diff.view(dtype) 270 | return diff 271 | 272 | 273 | @overload 274 | def delta_decode( 275 | data: bytes | bytearray, /, axis: int, dist: int, *, out: Any 276 | ) -> bytes: ... 277 | 278 | 279 | @overload 280 | def delta_decode( 281 | data: NDArray[Any], /, axis: int, dist: int, *, out: Any 282 | ) -> NDArray[Any]: ... 283 | 284 | 285 | def delta_decode( 286 | data: bytes | bytearray | NDArray[Any], 287 | /, 288 | axis: int = -1, 289 | dist: int = 1, 290 | *, 291 | out: Any = None, 292 | ) -> bytes | NDArray[Any]: 293 | """Decode Delta.""" 294 | if dist != 1: 295 | raise NotImplementedError( 296 | f"delta_decode with {dist=} requires the 'imagecodecs' package" 297 | ) 298 | if out is not None and not out.flags.writeable: 299 | out = None 300 | if isinstance(data, (bytes, bytearray)): 301 | data = numpy.frombuffer(data, dtype=numpy.uint8) 302 | return numpy.cumsum( # type: ignore[no-any-return] 303 | data, axis=0, dtype=numpy.uint8, out=out 304 | ).tobytes() 305 | if data.dtype.kind == 'f': 306 | if not data.dtype.isnative: 307 | raise NotImplementedError( 308 | f'delta_decode with {data.dtype!r} ' 309 | "requires the 'imagecodecs' package" 310 | ) 311 | view = data.view(f'{data.dtype.byteorder}u{data.dtype.itemsize}') 312 | view = numpy.cumsum(view, axis=axis, dtype=view.dtype) 313 | return view.view(data.dtype) 314 | return numpy.cumsum( # type: ignore[no-any-return] 315 | data, axis=axis, dtype=data.dtype, out=out 316 | ) 317 | 318 | 319 | @overload 320 | def bitorder_decode( 321 | data: bytes | bytearray, /, *, out: Any = None, _bitorder: list[Any] = [] 322 | ) -> bytes: ... 323 | 324 | 325 | @overload 326 | def bitorder_decode( 327 | data: NDArray[Any], /, *, out: Any = None, _bitorder: list[Any] = [] 328 | ) -> NDArray[Any]: ... 329 | 330 | 331 | def bitorder_decode( 332 | data: bytes | bytearray | NDArray[Any], 333 | /, 334 | *, 335 | out: Any = None, 336 | _bitorder: list[Any] = [], 337 | ) -> bytes | NDArray[Any]: 338 | r"""Reverse bits in each byte of bytes or numpy array. 339 | 340 | Decode data where pixels with lower column values are stored in the 341 | lower-order bits of the bytes (TIFF FillOrder is LSB2MSB). 342 | 343 | Parameters: 344 | data: 345 | Data to bit-reversed. If bytes type, a new bit-reversed 346 | bytes is returned. NumPy arrays are bit-reversed in-place. 347 | 348 | Examples: 349 | >>> bitorder_decode(b'\x01\x64') 350 | b'\x80&' 351 | >>> data = numpy.array([1, 666], dtype='uint16') 352 | >>> _ = bitorder_decode(data) 353 | >>> data 354 | array([ 128, 16473], dtype=uint16) 355 | 356 | """ 357 | if not _bitorder: 358 | _bitorder.append( 359 | b'\x00\x80@\xc0 \xa0`\xe0\x10\x90P\xd00\xb0p\xf0\x08\x88H' 360 | b'\xc8(\xa8h\xe8\x18\x98X\xd88\xb8x\xf8\x04\x84D\xc4$\xa4d' 361 | b'\xe4\x14\x94T\xd44\xb4t\xf4\x0c\x8cL\xcc,\xacl\xec\x1c\x9c' 362 | b'\\\xdc<\xbc|\xfc\x02\x82B\xc2"\xa2b\xe2\x12\x92R\xd22' 363 | b'\xb2r\xf2\n\x8aJ\xca*\xaaj\xea\x1a\x9aZ\xda:\xbaz\xfa' 364 | b'\x06\x86F\xc6&\xa6f\xe6\x16\x96V\xd66\xb6v\xf6\x0e\x8eN' 365 | b'\xce.\xaen\xee\x1e\x9e^\xde>\xbe~\xfe\x01\x81A\xc1!\xa1a' 366 | b'\xe1\x11\x91Q\xd11\xb1q\xf1\t\x89I\xc9)\xa9i\xe9\x19' 367 | b'\x99Y\xd99\xb9y\xf9\x05\x85E\xc5%\xa5e\xe5\x15\x95U\xd55' 368 | b'\xb5u\xf5\r\x8dM\xcd-\xadm\xed\x1d\x9d]\xdd=\xbd}\xfd' 369 | b'\x03\x83C\xc3#\xa3c\xe3\x13\x93S\xd33\xb3s\xf3\x0b\x8bK' 370 | b'\xcb+\xabk\xeb\x1b\x9b[\xdb;\xbb{\xfb\x07\x87G\xc7\'\xa7g' 371 | b'\xe7\x17\x97W\xd77\xb7w\xf7\x0f\x8fO\xcf/\xafo\xef\x1f\x9f_' 372 | b'\xdf?\xbf\x7f\xff' 373 | ) 374 | _bitorder.append(numpy.frombuffer(_bitorder[0], dtype=numpy.uint8)) 375 | if isinstance(data, (bytes, bytearray)): 376 | return data.translate(_bitorder[0]) 377 | try: 378 | view = data.view('uint8') 379 | numpy.take(_bitorder[1], view, out=view) 380 | return data 381 | except ValueError as exc: 382 | raise NotImplementedError( 383 | "bitorder_decode of slices requires the 'imagecodecs' package" 384 | ) from exc 385 | return None # type: ignore[unreachable] 386 | 387 | 388 | def packints_decode( 389 | data: bytes, 390 | /, 391 | dtype: DTypeLike, 392 | bitspersample: int, 393 | runlen: int = 0, 394 | *, 395 | out: Any = None, 396 | ) -> NDArray[Any]: 397 | """Decompress bytes to array of integers. 398 | 399 | This implementation only handles itemsizes 1, 8, 16, 32, and 64 bits. 400 | Install the Imagecodecs package for decoding other integer sizes. 401 | 402 | Parameters: 403 | data: 404 | Data to decompress. 405 | dtype: 406 | Numpy boolean or integer type. 407 | bitspersample: 408 | Number of bits per integer. 409 | runlen: 410 | Number of consecutive integers after which to start at next byte. 411 | 412 | Examples: 413 | >>> packints_decode(b'a', 'B', 1) 414 | array([0, 1, 1, 0, 0, 0, 0, 1], dtype=uint8) 415 | 416 | """ 417 | if bitspersample == 1: # bitarray 418 | data_array = numpy.frombuffer(data, '|B') 419 | data_array = numpy.unpackbits(data_array) 420 | if runlen % 8: 421 | data_array = data_array.reshape(-1, runlen + (8 - runlen % 8)) 422 | data_array = data_array[:, :runlen].reshape(-1) 423 | return data_array.astype(dtype) 424 | if bitspersample in (8, 16, 32, 64): 425 | return numpy.frombuffer(data, dtype) 426 | raise NotImplementedError( 427 | f'packints_decode of {bitspersample}-bit integers ' 428 | "requires the 'imagecodecs' package" 429 | ) 430 | 431 | 432 | def packints_encode( 433 | data: NDArray[Any], 434 | /, 435 | bitspersample: int, 436 | axis: int = -1, 437 | *, 438 | out: Any = None, 439 | ) -> bytes: 440 | """Tightly pack integers.""" 441 | raise NotImplementedError( 442 | "packints_encode requires the 'imagecodecs' package" 443 | ) 444 | 445 | 446 | def float24_decode( 447 | data: bytes, /, byteorder: Literal['>', '<'] 448 | ) -> NDArray[Any]: 449 | """Return float32 array from float24.""" 450 | raise NotImplementedError( 451 | "float24_decode requires the 'imagecodecs' package" 452 | ) 453 | -------------------------------------------------------------------------------- /tifffile/lsm2bin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # tifffile/lsm2bin.py 3 | 4 | """Convert TZCYX LSM file to series of BIN files. 5 | 6 | Usage: ``lsm2bin lsm_filename [bin_filename]`` 7 | 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | import sys 13 | 14 | try: 15 | from .tifffile import lsm2bin 16 | except ImportError: 17 | try: 18 | from tifffile.tifffile import lsm2bin 19 | except ImportError: 20 | from tifffile import lsm2bin 21 | 22 | 23 | def main(argv: list[str] | None = None) -> int: 24 | """Lsm2bin command line usage main function.""" 25 | if argv is None: 26 | argv = sys.argv 27 | if len(argv) > 1: 28 | lsm2bin(argv[1], argv[2] if len(argv) > 2 else None) 29 | else: 30 | print() 31 | print(__doc__.strip()) 32 | return 0 33 | 34 | 35 | if __name__ == '__main__': 36 | sys.exit(main()) 37 | -------------------------------------------------------------------------------- /tifffile/numcodecs.py: -------------------------------------------------------------------------------- 1 | # tifffile/numcodecs.py 2 | 3 | # Copyright (c) 2021-2025, Christoph Gohlke 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, 10 | # this list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of the copyright holder nor the names of its 17 | # contributors may be used to endorse or promote products derived from 18 | # this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | # POSSIBILITY OF SUCH DAMAGE. 31 | 32 | """TIFF codec for the Numcodecs package.""" 33 | 34 | from __future__ import annotations 35 | 36 | __all__ = ['register_codec', 'Tiff'] 37 | 38 | from io import BytesIO 39 | from typing import TYPE_CHECKING 40 | 41 | from numcodecs import registry 42 | from numcodecs.abc import Codec 43 | 44 | from .tifffile import TiffFile, TiffWriter 45 | 46 | if TYPE_CHECKING: 47 | from collections.abc import Iterable, Sequence 48 | from typing import Any 49 | 50 | from .tifffile import ( 51 | COMPRESSION, 52 | EXTRASAMPLE, 53 | PHOTOMETRIC, 54 | PLANARCONFIG, 55 | PREDICTOR, 56 | ByteOrder, 57 | TagTuple, 58 | ) 59 | 60 | 61 | class Tiff(Codec): # type: ignore[misc] 62 | """TIFF codec for Numcodecs.""" 63 | 64 | codec_id = 'tifffile' 65 | 66 | def __init__( 67 | self, 68 | # TiffFile.asarray 69 | key: int | slice | Iterable[int] | None = None, 70 | series: int | None = None, 71 | level: int | None = None, 72 | # TiffWriter 73 | bigtiff: bool = False, 74 | byteorder: ByteOrder | None = None, 75 | imagej: bool = False, 76 | ome: bool | None = None, 77 | # TiffWriter.write 78 | photometric: PHOTOMETRIC | int | str | None = None, 79 | planarconfig: PLANARCONFIG | int | str | None = None, 80 | extrasamples: Sequence[EXTRASAMPLE | int | str] | None = None, 81 | volumetric: bool = False, 82 | tile: Sequence[int] | None = None, 83 | rowsperstrip: int | None = None, 84 | compression: COMPRESSION | int | str | None = None, 85 | compressionargs: dict[str, Any] | None = None, 86 | predictor: PREDICTOR | int | str | bool | None = None, 87 | subsampling: tuple[int, int] | None = None, 88 | metadata: dict[str, Any] | None = {}, 89 | extratags: Sequence[TagTuple] | None = None, 90 | truncate: bool = False, 91 | maxworkers: int | None = None, 92 | ) -> None: 93 | self.key = key 94 | self.series = series 95 | self.level = level 96 | self.bigtiff = bigtiff 97 | self.byteorder = byteorder 98 | self.imagej = imagej 99 | self.ome = ome 100 | self.photometric = photometric 101 | self.planarconfig = planarconfig 102 | self.extrasamples = extrasamples 103 | self.volumetric = volumetric 104 | self.tile = tile 105 | self.rowsperstrip = rowsperstrip 106 | self.compression = compression 107 | self.compressionargs = compressionargs 108 | self.predictor = predictor 109 | self.subsampling = subsampling 110 | self.metadata = metadata 111 | self.extratags = extratags 112 | self.truncate = truncate 113 | self.maxworkers = maxworkers 114 | 115 | def encode(self, buf: Any) -> bytes: 116 | """Return TIFF file as bytes.""" 117 | with BytesIO() as fh: 118 | with TiffWriter( 119 | fh, 120 | bigtiff=self.bigtiff, 121 | byteorder=self.byteorder, 122 | imagej=self.imagej, 123 | ome=self.ome, 124 | ) as tif: 125 | tif.write( 126 | buf, 127 | photometric=self.photometric, 128 | planarconfig=self.planarconfig, 129 | extrasamples=self.extrasamples, 130 | volumetric=self.volumetric, 131 | tile=self.tile, 132 | rowsperstrip=self.rowsperstrip, 133 | compression=self.compression, 134 | compressionargs=self.compressionargs, 135 | predictor=self.predictor, 136 | subsampling=self.subsampling, 137 | metadata=self.metadata, 138 | extratags=self.extratags, 139 | truncate=self.truncate, 140 | maxworkers=self.maxworkers, 141 | ) 142 | result = fh.getvalue() 143 | return result 144 | 145 | def decode(self, buf: Any, out: Any = None) -> Any: 146 | """Return decoded image as NumPy array.""" 147 | with BytesIO(buf) as fh: 148 | with TiffFile(fh) as tif: 149 | result = tif.asarray( 150 | key=self.key, 151 | series=self.series, 152 | level=self.level, 153 | maxworkers=self.maxworkers, 154 | out=out, 155 | ) 156 | return result 157 | 158 | 159 | def register_codec(cls: Codec = Tiff, codec_id: str | None = None) -> None: 160 | """Register :py:class:`Tiff` codec with Numcodecs.""" 161 | registry.register_codec(cls, codec_id=codec_id) 162 | -------------------------------------------------------------------------------- /tifffile/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cgohlke/tifffile/80e63893f30fcba701a6fc6ba110ab8a079371cd/tifffile/py.typed -------------------------------------------------------------------------------- /tifffile/tiff2fsspec.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # tifffile/tiff2fsspec.py 3 | 4 | """Write fsspec ReferenceFileSystem for TIFF file. 5 | 6 | positional arguments: 7 | tifffile path to the local TIFF input file 8 | url remote URL of TIFF file without file name 9 | 10 | optional arguments: 11 | -h, --help show this help message and exit 12 | --out OUT path to the JSON output file 13 | --series SERIES index of series in file 14 | --level LEVEL index of level in series 15 | --key KEY index of page in file or series 16 | --chunkmode CHUNKMODE 17 | mode used for chunking {None, pages} 18 | 19 | For example: 20 | 21 | ``tiff2fsspec ./test.ome.tif https://server.com/path/`` 22 | 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | import argparse 28 | 29 | try: 30 | from .tifffile import tiff2fsspec 31 | except ImportError: 32 | try: 33 | from tifffile.tifffile import tiff2fsspec 34 | except ImportError: 35 | from tifffile import tiff2fsspec 36 | 37 | 38 | def main() -> int: 39 | """Tiff2fsspec command line usage main function.""" 40 | parser = argparse.ArgumentParser( 41 | 'tiff2fsspec', 42 | description='Write fsspec ReferenceFileSystem for TIFF file.', 43 | ) 44 | parser.add_argument( 45 | 'tifffile', type=str, help='path to the local TIFF input file' 46 | ) 47 | parser.add_argument( 48 | 'url', type=str, help='remote URL of TIFF file without file name' 49 | ) 50 | parser.add_argument( 51 | '--out', type=str, default=None, help='path to the JSON output file' 52 | ) 53 | parser.add_argument( 54 | '--series', type=int, default=None, help='index of series in file' 55 | ) 56 | parser.add_argument( 57 | '--level', type=int, default=None, help='index of level in series' 58 | ) 59 | parser.add_argument( 60 | '--key', type=int, default=None, help='index of page in file or series' 61 | ) 62 | parser.add_argument( 63 | '--chunkmode', 64 | type=int, 65 | default=None, 66 | help='mode used for chunking {None, pages}', 67 | ) 68 | parser.add_argument( 69 | '--ver', type=int, default=None, help='version of ReferenceFileSystem' 70 | ) 71 | args = parser.parse_args() 72 | 73 | tiff2fsspec( 74 | args.tifffile, 75 | args.url, 76 | out=args.out, 77 | key=args.key, 78 | series=args.series, 79 | level=args.level, 80 | chunkmode=args.chunkmode, 81 | version=args.ver, 82 | ) 83 | return 0 84 | 85 | 86 | if __name__ == '__main__': 87 | import sys 88 | 89 | sys.exit(main()) 90 | -------------------------------------------------------------------------------- /tifffile/tiffcomment.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # tifffile/tiffcomment.py 3 | 4 | """Print or replace ImageDescription in first page of TIFF file. 5 | 6 | Usage: ``tiffcomment [--set comment] file`` 7 | 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | import os 13 | import sys 14 | 15 | try: 16 | from .tifffile import tiffcomment 17 | except ImportError: 18 | try: 19 | from tifffile.tifffile import tiffcomment 20 | except ImportError: 21 | from tifffile import tiffcomment 22 | 23 | 24 | def main(argv: list[str] | None = None) -> int: 25 | """Tiffcomment command line usage main function.""" 26 | comment: str | bytes | None 27 | 28 | if argv is None: 29 | argv = sys.argv 30 | 31 | if len(argv) > 2 and argv[1] in '--set': 32 | comment = argv[2] 33 | files = argv[3:] 34 | else: 35 | comment = None 36 | files = argv[1:] 37 | 38 | if len(files) == 0 or any(f.startswith('-') for f in files): 39 | print() 40 | print(__doc__.strip()) 41 | return 0 42 | 43 | if comment is None: 44 | pass 45 | elif os.path.exists(comment): 46 | with open(comment, 'rb') as fh: 47 | comment = fh.read() 48 | else: 49 | try: 50 | comment = comment.encode('ascii') 51 | except UnicodeEncodeError as exc: 52 | print(f'{exc}') 53 | assert isinstance(comment, str) 54 | comment = comment.encode() 55 | 56 | for file in files: 57 | try: 58 | result = tiffcomment(file, comment) 59 | except Exception as exc: 60 | print(f'{file}: {exc}') 61 | else: 62 | if result: 63 | print(result) 64 | return 0 65 | 66 | 67 | if __name__ == '__main__': 68 | sys.exit(main()) 69 | -------------------------------------------------------------------------------- /tifffile/zarr.py: -------------------------------------------------------------------------------- 1 | # tifffile/zarr.py 2 | 3 | # Copyright (c) 2008-2025, Christoph Gohlke 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # 1. Redistributions of source code must retain the above copyright notice, 10 | # this list of conditions and the following disclaimer. 11 | # 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # 3. Neither the name of the copyright holder nor the names of its 17 | # contributors may be used to endorse or promote products derived from 18 | # this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | # POSSIBILITY OF SUCH DAMAGE. 31 | 32 | """Zarr 3 TIFF and file sequence stores.""" 33 | 34 | from __future__ import annotations 35 | 36 | __all__ = ['ZarrStore', 'ZarrTiffStore', 'ZarrFileSequenceStore'] 37 | 38 | import asyncio 39 | import json 40 | import os 41 | import sys 42 | import threading 43 | from typing import TYPE_CHECKING 44 | 45 | import numpy 46 | import zarr 47 | 48 | try: 49 | from zarr.abc.store import ByteRequest, Store 50 | from zarr.core.buffer.cpu import NDBuffer 51 | from zarr.core.chunk_grids import RegularChunkGrid 52 | except ImportError as exc: 53 | raise ValueError(f'zarr {zarr.__version__} < 3 is not supported') from exc 54 | 55 | from .tifffile import ( 56 | CHUNKMODE, 57 | COMPRESSION, 58 | FileCache, 59 | FileSequence, 60 | NullContext, 61 | TiffFrame, 62 | TiffPage, 63 | TiffPageSeries, 64 | TiledSequence, 65 | create_output, 66 | enumarg, 67 | imread, 68 | jpeg_decode_colorspace, 69 | product, 70 | ) 71 | 72 | if TYPE_CHECKING: 73 | from collections.abc import ( 74 | AsyncIterator, 75 | Callable, 76 | Iterable, 77 | Iterator, 78 | Sequence, 79 | ) 80 | from typing import Any, TextIO 81 | 82 | from numpy.typing import DTypeLike, NDArray 83 | from zarr.core.buffer import Buffer, BufferPrototype 84 | from zarr.core.common import BytesLike 85 | from zarr.core.indexing import BasicSelection 86 | 87 | from .tifffile import ByteOrder, OutputType 88 | 89 | 90 | class ZarrStore(Store): 91 | """Zarr 3 store base class. 92 | 93 | Parameters: 94 | fillvalue: 95 | Value to use for missing chunks of Zarr store. 96 | The default is 0. 97 | chunkmode: 98 | Specifies how to chunk data. 99 | read_only: 100 | Passed to zarr `Store` constructor. 101 | 102 | References: 103 | 1. https://zarr.readthedocs.io/en/stable/api/zarr/abc/store/ 104 | 3. https://zarr.readthedocs.io/en/stable/spec/v2.html 105 | 3. https://forum.image.sc/t/multiscale-arrays-v0-1/37930 106 | 107 | """ 108 | 109 | _read_only: bool 110 | _store: dict[str, Any] 111 | _fillvalue: int | float 112 | _chunkmode: int 113 | 114 | def __init__( 115 | self, 116 | /, 117 | *, 118 | fillvalue: int | float | None = None, 119 | chunkmode: CHUNKMODE | int | str | None = None, 120 | read_only: bool = True, 121 | ) -> None: 122 | super().__init__(read_only=read_only) 123 | 124 | self._store = {} 125 | self._fillvalue = 0 if fillvalue is None else fillvalue 126 | if chunkmode is None: 127 | self._chunkmode = CHUNKMODE(0) 128 | else: 129 | self._chunkmode = enumarg(CHUNKMODE, chunkmode) 130 | 131 | def __eq__(self, other: object) -> bool: 132 | """Return if objects are equal.""" 133 | return ( 134 | isinstance(other, type(self)) 135 | and self._store == other._store 136 | and self._fillvalue == other._fillvalue 137 | and self._chunkmode == other._chunkmode 138 | ) 139 | 140 | async def get_partial_values( 141 | self, 142 | prototype: BufferPrototype, 143 | key_ranges: Iterable[tuple[str, ByteRequest | None]], 144 | ) -> list[Buffer | None]: 145 | """Return possibly partial values from given key_ranges.""" 146 | # print(f'get_partial_values({key_ranges=})') 147 | return [ 148 | await self.get(key, prototype, byte_range) 149 | for key, byte_range in key_ranges 150 | ] 151 | 152 | @property 153 | def supports_writes(self) -> bool: 154 | """Store supports writes.""" 155 | return not self._read_only 156 | 157 | def _set(self, key: str, value: Buffer, /) -> None: 158 | """Store (key, value) pair.""" 159 | raise NotImplementedError 160 | 161 | async def set(self, key: str, value: Buffer) -> None: 162 | """Store (key, value) pair.""" 163 | self._set(key, value) 164 | 165 | @property 166 | def supports_deletes(self) -> bool: 167 | """Store supports deletes.""" 168 | return False 169 | 170 | async def delete(self, key: str) -> None: 171 | """Remove key from store.""" 172 | raise PermissionError('ZarrStore does not support deletes') 173 | 174 | @property 175 | def supports_partial_writes(self) -> bool: 176 | """Store support partial writes.""" 177 | return False 178 | 179 | async def set_partial_values( 180 | self, key_start_values: Iterable[tuple[str, int, BytesLike]] 181 | ) -> None: 182 | """Store values at key, starting at byte range_start.""" 183 | raise PermissionError('ZarrStore does not support partial writes') 184 | 185 | @property 186 | def supports_listing(self) -> bool: 187 | """Store supports listing.""" 188 | return True 189 | 190 | async def list(self) -> AsyncIterator[str]: 191 | """Return all keys in store.""" 192 | for key in self._store: 193 | yield key 194 | 195 | async def list_prefix(self, prefix: str) -> AsyncIterator[str]: 196 | """Return all keys in store that begin with prefix. 197 | 198 | Keys are returned relative to the root of the store. 199 | 200 | """ 201 | for key in list(self._store): 202 | if key.startswith(prefix): 203 | yield key 204 | 205 | async def list_dir(self, prefix: str) -> AsyncIterator[str]: 206 | """Return all keys and prefixes with prefix. 207 | 208 | Keys and prefixes do not contain the character "/" after the given 209 | prefix. 210 | 211 | """ 212 | prefix = prefix.rstrip('/') 213 | if prefix == '': 214 | keys_unique = {k.split('/')[0] for k in self._store} 215 | else: 216 | keys_unique = { 217 | key.removeprefix(prefix + '/').split('/')[0] 218 | for key in self._store 219 | if key.startswith(prefix + '/') and key != prefix 220 | } 221 | for key in keys_unique: 222 | yield key 223 | 224 | @property 225 | def is_multiscales(self) -> bool: 226 | """Return if ZarrStore is multi-scales.""" 227 | return b'multiscales' in self._store['.zattrs'] 228 | 229 | def __repr__(self) -> str: 230 | return f'{self.__class__.__name__}' 231 | 232 | # async def _get_many( 233 | # self, 234 | # requests: Iterable[tuple[str, BufferPrototype, ByteRequest | None]] 235 | # ) -> AsyncGenerator[tuple[str, Buffer | None], None]: 236 | # print(f'_get_many({requests=})') 237 | # return super()._get_many(requests) 238 | 239 | # async def getsize_prefix(self, prefix: str) -> int: 240 | # print(f'getsize_prefix({prefix=})') 241 | # return super().getsize_prefix(prefix) 242 | 243 | 244 | class ZarrTiffStore(ZarrStore): 245 | """Zarr 3 store interface to image array in TiffPage or TiffPageSeries. 246 | 247 | ZarrTiffStore is using a TiffFile instance for reading and decoding chunks. 248 | Therefore, ZarrTiffStore instances cannot be pickled. 249 | 250 | For writing, image data must be stored in uncompressed, unpredicted, 251 | and unpacked form. Sparse strips and tiles are not written. 252 | 253 | Parameters: 254 | arg: 255 | TIFF page or series to wrap as Zarr store. 256 | level: 257 | Pyramidal level to wrap. The default is 0. 258 | chunkmode: 259 | Use strips or tiles (0) or whole page data (2) as chunks. 260 | The default is 0. 261 | fillvalue: 262 | Value to use for missing chunks. The default is 0. 263 | zattrs: 264 | Additional attributes to store in `.zattrs`. 265 | multiscales: 266 | Create a multiscales compatible Zarr group store. 267 | By default, create a Zarr array store for pages and non-pyramidal 268 | series. 269 | lock: 270 | Reentrant lock to synchronize seeks and reads from file. 271 | By default, the lock of the parent's file handle is used. 272 | squeeze: 273 | Remove length-1 dimensions from shape of TiffPageSeries. 274 | maxworkers: 275 | If `chunkmode=0`, asynchronously run chunk decode function 276 | in separate thread if greater than 1. 277 | If `chunkmode=2`, maximum number of threads to concurrently decode 278 | strips or tiles. 279 | If *None* or *0*, use up to :py:attr:`_TIFF.MAXWORKERS` or 280 | asyncio assigned threads. 281 | buffersize: 282 | Approximate number of bytes to read from file in one pass 283 | if `chunkmode=2`. The default is :py:attr:`_TIFF.BUFFERSIZE`. 284 | read_only: 285 | Passed to zarr `Store` constructor. 286 | _openfiles: 287 | Internal API. 288 | 289 | """ 290 | 291 | _data: list[TiffPageSeries] 292 | _filecache: FileCache 293 | _transform: Callable[[NDArray[Any]], NDArray[Any]] | None 294 | _maxworkers: int 295 | _buffersize: int | None 296 | _squeeze: bool | None 297 | _multiscales: bool 298 | 299 | def __init__( 300 | self, 301 | arg: TiffPage | TiffFrame | TiffPageSeries, 302 | /, 303 | *, 304 | level: int | None = None, 305 | chunkmode: CHUNKMODE | int | str | None = None, 306 | fillvalue: int | float | None = None, 307 | zattrs: dict[str, Any] | None = None, 308 | multiscales: bool | None = None, 309 | lock: threading.RLock | NullContext | None = None, 310 | squeeze: bool | None = None, 311 | maxworkers: int | None = None, 312 | buffersize: int | None = None, 313 | read_only: bool | None = None, 314 | _openfiles: int | None = None, 315 | **kwargs: Any, 316 | ) -> None: 317 | if chunkmode is None: 318 | self._chunkmode = CHUNKMODE(0) 319 | else: 320 | self._chunkmode = enumarg(CHUNKMODE, chunkmode) 321 | 322 | if self._chunkmode not in {0, 2}: 323 | raise NotImplementedError(f'{self._chunkmode!r} not implemented') 324 | 325 | self._squeeze = None if squeeze is None else bool(squeeze) 326 | self._buffersize = buffersize 327 | 328 | if isinstance(arg, TiffPageSeries): 329 | self._data = arg.levels 330 | self._transform = arg.transform 331 | if multiscales is not None and not multiscales: 332 | level = 0 333 | if level is not None: 334 | self._data = [self._data[level]] 335 | name = arg.name 336 | else: 337 | self._data = [TiffPageSeries([arg])] 338 | self._transform = None 339 | name = 'Unnamed' 340 | 341 | if not maxworkers: 342 | maxworkers = self._data[0].keyframe.maxworkers 343 | if maxworkers < 3 and self._chunkmode == 0: 344 | maxworkers = 1 345 | self._maxworkers = maxworkers 346 | 347 | fh = self._data[0].keyframe.parent._parent.filehandle 348 | 349 | if read_only is None: 350 | read_only = not fh.writable() or self._chunkmode != 0 351 | 352 | super().__init__( 353 | fillvalue=fillvalue, chunkmode=self._chunkmode, read_only=read_only 354 | ) 355 | 356 | if lock is None: 357 | fh.set_lock(True) 358 | lock = fh.lock 359 | self._filecache = FileCache(size=_openfiles, lock=lock) 360 | 361 | zattrs = {} if zattrs is None else dict(zattrs) 362 | # TODO: Zarr Encoding Specification 363 | # https://xarray.pydata.org/en/stable/internals/zarr-encoding-spec.html 364 | 365 | if multiscales or len(self._data) > 1: 366 | # multiscales 367 | self._multiscales = True 368 | if '_ARRAY_DIMENSIONS' in zattrs: 369 | array_dimensions = zattrs.pop('_ARRAY_DIMENSIONS') 370 | else: 371 | array_dimensions = list(self._data[0].get_axes(squeeze)) 372 | self._store['.zgroup'] = _json_dumps({'zarr_format': 2}) 373 | self._store['.zattrs'] = _json_dumps( 374 | { 375 | # TODO: use https://ngff.openmicroscopy.org/latest/ 376 | 'multiscales': [ 377 | { 378 | 'version': '0.1', 379 | 'name': name, 380 | 'datasets': [ 381 | {'path': str(i)} 382 | for i in range(len(self._data)) 383 | ], 384 | # 'axes': [...] 385 | # 'type': 'unknown', 386 | 'metadata': {}, 387 | } 388 | ], 389 | **zattrs, 390 | } 391 | ) 392 | shape0 = self._data[0].get_shape(squeeze) 393 | for level, series in enumerate(self._data): 394 | keyframe = series.keyframe 395 | keyframe.decode # cache decode function 396 | shape = series.get_shape(squeeze) 397 | dtype = series.dtype 398 | if fillvalue is None: 399 | self._fillvalue = fillvalue = keyframe.nodata 400 | if self._chunkmode: 401 | chunks = keyframe.shape 402 | else: 403 | chunks = keyframe.chunks 404 | self._store[f'{level}/.zattrs'] = _json_dumps( 405 | { 406 | '_ARRAY_DIMENSIONS': [ 407 | (f'{ax}{level}' if i != j else ax) 408 | for ax, i, j in zip( 409 | array_dimensions, shape, shape0 410 | ) 411 | ] 412 | } 413 | ) 414 | self._store[f'{level}/.zarray'] = _json_dumps( 415 | { 416 | 'zarr_format': 2, 417 | 'shape': shape, 418 | 'chunks': _chunks(chunks, shape), 419 | 'dtype': _dtype_str(dtype), 420 | 'compressor': None, 421 | 'fill_value': _json_value(fillvalue, dtype), 422 | 'order': 'C', 423 | 'filters': None, 424 | } 425 | ) 426 | if not self._read_only: 427 | self._read_only = not _is_writable(keyframe) 428 | else: 429 | self._multiscales = False 430 | series = self._data[0] 431 | keyframe = series.keyframe 432 | keyframe.decode # cache decode function 433 | shape = series.get_shape(squeeze) 434 | dtype = series.dtype 435 | if fillvalue is None: 436 | self._fillvalue = fillvalue = keyframe.nodata 437 | if self._chunkmode: 438 | chunks = keyframe.shape 439 | else: 440 | chunks = keyframe.chunks 441 | if '_ARRAY_DIMENSIONS' not in zattrs: 442 | zattrs['_ARRAY_DIMENSIONS'] = list(series.get_axes(squeeze)) 443 | self._store['.zattrs'] = _json_dumps(zattrs) 444 | self._store['.zarray'] = _json_dumps( 445 | { 446 | 'zarr_format': 2, 447 | 'shape': shape, 448 | 'chunks': _chunks(chunks, shape), 449 | 'dtype': _dtype_str(dtype), 450 | 'compressor': None, 451 | 'fill_value': _json_value(fillvalue, dtype), 452 | 'order': 'C', 453 | 'filters': None, 454 | } 455 | ) 456 | if not self._read_only: 457 | self._read_only = not _is_writable(keyframe) 458 | 459 | def close(self) -> None: 460 | """Close store.""" 461 | super().close() 462 | self._filecache.clear() 463 | 464 | def write_fsspec( 465 | self, 466 | jsonfile: str | os.PathLike[Any] | TextIO, 467 | /, 468 | url: str | None, 469 | *, 470 | groupname: str | None = None, 471 | templatename: str | None = None, 472 | compressors: dict[COMPRESSION | int, str | None] | None = None, 473 | version: int | None = None, 474 | _shape: Sequence[int] | None = None, 475 | _axes: Sequence[str] | None = None, 476 | _index: Sequence[int] | None = None, 477 | _append: bool = False, 478 | _close: bool = True, 479 | ) -> None: 480 | """Write fsspec ReferenceFileSystem as JSON to file. 481 | 482 | Parameters: 483 | jsonfile: 484 | Name or open file handle of output JSON file. 485 | url: 486 | Remote location of TIFF file(s) without file name(s). 487 | groupname: 488 | Zarr group name. 489 | templatename: 490 | Version 1 URL template name. The default is 'u'. 491 | compressors: 492 | Mapping of :py:class:`COMPRESSION` codes to Numcodecs codec 493 | names. 494 | version: 495 | Version of fsspec file to write. The default is 0. 496 | _shape: 497 | Shape of file sequence (experimental). 498 | _axes: 499 | Axes of file sequence (experimental). 500 | _index 501 | Index of file in sequence (experimental). 502 | _append: 503 | If *True*, only write index keys and values (experimental). 504 | _close: 505 | If *True*, no more appends (experimental). 506 | 507 | Raises: 508 | ValueError: 509 | ZarrTiffStore cannot be represented as ReferenceFileSystem 510 | due to features that are not supported by Zarr, Numcodecs, 511 | or Imagecodecs: 512 | 513 | - compressors, such as CCITT 514 | - filters, such as bitorder reversal, packed integers 515 | - dtypes, such as float24, complex integers 516 | - JPEGTables in multi-page series 517 | - incomplete chunks, such as `imagelength % rowsperstrip != 0` 518 | 519 | Files containing incomplete tiles may fail at runtime. 520 | 521 | Notes: 522 | Parameters `_shape`, `_axes`, `_index`, `_append`, and `_close` 523 | are an experimental API for joining the ReferenceFileSystems of 524 | multiple files of a TiffSequence. 525 | 526 | References: 527 | - `fsspec ReferenceFileSystem format 528 | `_ 529 | 530 | """ 531 | compressors = { 532 | 1: None, 533 | 8: 'zlib', 534 | 32946: 'zlib', 535 | 34925: 'lzma', 536 | 50013: 'zlib', # pixtiff 537 | 5: 'imagecodecs_lzw', 538 | 7: 'imagecodecs_jpeg', 539 | 22610: 'imagecodecs_jpegxr', 540 | 32773: 'imagecodecs_packbits', 541 | 33003: 'imagecodecs_jpeg2k', 542 | 33004: 'imagecodecs_jpeg2k', 543 | 33005: 'imagecodecs_jpeg2k', 544 | 33007: 'imagecodecs_jpeg', 545 | 34712: 'imagecodecs_jpeg2k', 546 | 34887: 'imagecodecs_lerc', 547 | 34892: 'imagecodecs_jpeg', 548 | 34933: 'imagecodecs_png', 549 | 34934: 'imagecodecs_jpegxr', 550 | 48124: 'imagecodecs_jetraw', 551 | 50000: 'imagecodecs_zstd', # numcodecs.zstd fails w/ unknown sizes 552 | 50001: 'imagecodecs_webp', 553 | 50002: 'imagecodecs_jpegxl', 554 | 52546: 'imagecodecs_jpegxl', 555 | **({} if compressors is None else compressors), 556 | } 557 | 558 | for series in self._data: 559 | errormsg = ' not supported by the fsspec ReferenceFileSystem' 560 | keyframe = series.keyframe 561 | if ( 562 | keyframe.compression in {65000, 65001, 65002} 563 | and keyframe.parent.is_eer 564 | ): 565 | compressors[keyframe.compression] = 'imagecodecs_eer' 566 | if keyframe.compression not in compressors: 567 | raise ValueError(f'{keyframe.compression!r} is' + errormsg) 568 | if keyframe.fillorder != 1: 569 | raise ValueError(f'{keyframe.fillorder!r} is' + errormsg) 570 | if keyframe.sampleformat not in {1, 2, 3, 6}: 571 | # TODO: support float24 and cint via filters? 572 | raise ValueError(f'{keyframe.sampleformat!r} is' + errormsg) 573 | if ( 574 | keyframe.bitspersample 575 | not in { 576 | 8, 577 | 16, 578 | 32, 579 | 64, 580 | 128, 581 | } 582 | and keyframe.compression 583 | not in { 584 | # JPEG 585 | 7, 586 | 33007, 587 | 34892, 588 | } 589 | and compressors[keyframe.compression] != 'imagecodecs_eer' 590 | ): 591 | raise ValueError( 592 | f'BitsPerSample {keyframe.bitspersample} is' + errormsg 593 | ) 594 | if ( 595 | not self._chunkmode 596 | and not keyframe.is_tiled 597 | and keyframe.imagelength % keyframe.rowsperstrip 598 | ): 599 | raise ValueError('incomplete chunks are' + errormsg) 600 | if self._chunkmode and not keyframe.is_final: 601 | raise ValueError(f'{self._chunkmode!r} is' + errormsg) 602 | if keyframe.jpegtables is not None and len(series.pages) > 1: 603 | raise ValueError( 604 | 'JPEGTables in multi-page files are' + errormsg 605 | ) 606 | 607 | if url is None: 608 | url = '' 609 | elif url and url[-1] != '/': 610 | url += '/' 611 | url = url.replace('\\', '/') 612 | 613 | if groupname is None: 614 | groupname = '' 615 | elif groupname and groupname[-1] != '/': 616 | groupname += '/' 617 | 618 | byteorder: ByteOrder | None = '<' if sys.byteorder == 'big' else '>' 619 | if ( 620 | self._data[0].keyframe.parent.byteorder != byteorder 621 | or self._data[0].keyframe.dtype is None 622 | or self._data[0].keyframe.dtype.itemsize == 1 623 | ): 624 | byteorder = None 625 | 626 | index: str 627 | _shape = [] if _shape is None else list(_shape) 628 | _axes = [] if _axes is None else list(_axes) 629 | if len(_shape) != len(_axes): 630 | raise ValueError('len(_shape) != len(_axes)') 631 | if _index is None: 632 | index = '' 633 | elif len(_shape) != len(_index): 634 | raise ValueError('len(_shape) != len(_index)') 635 | elif _index: 636 | index = '.'.join(str(i) for i in _index) 637 | index += '.' 638 | 639 | refs: dict[str, Any] = {} 640 | refzarr: dict[str, Any] 641 | if version == 1: 642 | if _append: 643 | raise ValueError('cannot append to version 1') 644 | if templatename is None: 645 | templatename = 'u' 646 | refs['version'] = 1 647 | refs['templates'] = {} 648 | refs['gen'] = [] 649 | templates = {} 650 | if self._data[0].is_multifile: 651 | i = 0 652 | for page in self._data[0].pages: 653 | if page is None or page.keyframe is None: 654 | continue 655 | fname = page.keyframe.parent.filehandle.name 656 | if fname in templates: 657 | continue 658 | key = f'{templatename}{i}' 659 | templates[fname] = f'{{{{{key}}}}}' 660 | refs['templates'][key] = url + fname 661 | i += 1 662 | else: 663 | fname = self._data[0].keyframe.parent.filehandle.name 664 | key = f'{templatename}' 665 | templates[fname] = f'{{{{{key}}}}}' 666 | refs['templates'][key] = url + fname 667 | 668 | refs['refs'] = refzarr = {} 669 | else: 670 | refzarr = refs 671 | 672 | if not _append: 673 | if groupname: 674 | # TODO: support nested groups 675 | refzarr['.zgroup'] = _json_dumps({'zarr_format': 2}).decode() 676 | 677 | for key, value in self._store.items(): 678 | if '.zattrs' in key and _axes: 679 | value = json.loads(value) 680 | if '_ARRAY_DIMENSIONS' in value: 681 | value['_ARRAY_DIMENSIONS'] = ( 682 | _axes + value['_ARRAY_DIMENSIONS'] 683 | ) 684 | value = _json_dumps(value) 685 | elif '.zarray' in key: 686 | level = int(key.split('/')[0]) if '/' in key else 0 687 | keyframe = self._data[level].keyframe 688 | value = json.loads(value) 689 | if _shape: 690 | value['shape'] = _shape + value['shape'] 691 | value['chunks'] = [1] * len(_shape) + value['chunks'] 692 | codec_id = compressors[keyframe.compression] 693 | if codec_id == 'imagecodecs_jpeg': 694 | # TODO: handle JPEG color spaces 695 | jpegtables = keyframe.jpegtables 696 | if jpegtables is None: 697 | tables = None 698 | else: 699 | import base64 700 | 701 | tables = base64.b64encode(jpegtables).decode() 702 | jpegheader = keyframe.jpegheader 703 | if jpegheader is None: 704 | header = None 705 | else: 706 | import base64 707 | 708 | header = base64.b64encode(jpegheader).decode() 709 | ( 710 | colorspace_jpeg, 711 | colorspace_data, 712 | ) = jpeg_decode_colorspace( 713 | keyframe.photometric, 714 | keyframe.planarconfig, 715 | keyframe.extrasamples, 716 | keyframe.is_jfif, 717 | ) 718 | value['compressor'] = { 719 | 'id': codec_id, 720 | 'tables': tables, 721 | 'header': header, 722 | 'bitspersample': keyframe.bitspersample, 723 | 'colorspace_jpeg': colorspace_jpeg, 724 | 'colorspace_data': colorspace_data, 725 | } 726 | elif ( 727 | codec_id == 'imagecodecs_webp' 728 | and keyframe.samplesperpixel == 4 729 | ): 730 | value['compressor'] = { 731 | 'id': codec_id, 732 | 'hasalpha': True, 733 | } 734 | elif codec_id == 'imagecodecs_eer': 735 | if keyframe.compression == 65002: 736 | rlebits = int(keyframe.tags.valueof(65007, 7)) 737 | horzbits = int(keyframe.tags.valueof(65008, 2)) 738 | vertbits = int(keyframe.tags.valueof(65009, 2)) 739 | elif keyframe.compression == 65001: 740 | rlebits = 7 741 | horzbits = 2 742 | vertbits = 2 743 | else: 744 | rlebits = 8 745 | horzbits = 2 746 | vertbits = 2 747 | value['compressor'] = { 748 | 'id': codec_id, 749 | 'shape': keyframe.chunks, 750 | 'rlebits': rlebits, 751 | 'horzbits': horzbits, 752 | 'vertbits': vertbits, 753 | } 754 | elif codec_id is not None: 755 | value['compressor'] = {'id': codec_id} 756 | if byteorder is not None: 757 | value['dtype'] = byteorder + value['dtype'][1:] 758 | if keyframe.predictor > 1: 759 | # predictors need access to chunk shape and dtype 760 | # requires imagecodecs > 2021.8.26 to read 761 | if keyframe.predictor in {2, 34892, 34893}: 762 | filter_id = 'imagecodecs_delta' 763 | else: 764 | filter_id = 'imagecodecs_floatpred' 765 | if keyframe.predictor <= 3: 766 | dist = 1 767 | elif keyframe.predictor in {34892, 34894}: 768 | dist = 2 769 | else: 770 | dist = 4 771 | if ( 772 | keyframe.planarconfig == 1 773 | and keyframe.samplesperpixel > 1 774 | ): 775 | axis = -2 776 | else: 777 | axis = -1 778 | value['filters'] = [ 779 | { 780 | 'id': filter_id, 781 | 'axis': axis, 782 | 'dist': dist, 783 | 'shape': value['chunks'], 784 | 'dtype': value['dtype'], 785 | } 786 | ] 787 | value = _json_dumps(value) 788 | 789 | refzarr[groupname + key] = value.decode() 790 | 791 | fh: TextIO 792 | if hasattr(jsonfile, 'write'): 793 | fh = jsonfile # type: ignore[assignment] 794 | else: 795 | fh = open(jsonfile, 'w', encoding='utf-8') 796 | 797 | if version == 1: 798 | fh.write(json.dumps(refs, indent=1).rsplit('}"', 1)[0] + '}"') 799 | indent = ' ' 800 | elif _append: 801 | indent = ' ' 802 | else: 803 | fh.write(json.dumps(refs, indent=1)[:-2]) 804 | indent = ' ' 805 | 806 | offset: int | None 807 | for key, value in self._store.items(): 808 | if '.zarray' in key: 809 | value = json.loads(value) 810 | shape = value['shape'] 811 | chunks = value['chunks'] 812 | levelstr = (key.split('/')[0] + '/') if '/' in key else '' 813 | for chunkindex in _ndindex(shape, chunks): 814 | key = levelstr + chunkindex 815 | keyframe, page, _, offset, bytecount = self._parse_key(key) 816 | if page and self._chunkmode and offset is None: 817 | offset = page.dataoffsets[0] 818 | bytecount = keyframe.nbytes 819 | if offset and bytecount: 820 | fname = keyframe.parent.filehandle.name 821 | if version == 1: 822 | fname = templates[fname] 823 | else: 824 | fname = f'{url}{fname}' 825 | fh.write( 826 | f',\n{indent}"{groupname}{key}": ' 827 | f'["{fname}", {offset}, {bytecount}]' 828 | ) 829 | 830 | # TODO: support nested groups 831 | if version == 1: 832 | fh.write('\n }\n}') 833 | elif _close: 834 | fh.write('\n}') 835 | 836 | if not hasattr(jsonfile, 'write'): 837 | fh.close() 838 | 839 | async def get( 840 | self, 841 | key: str, 842 | prototype: BufferPrototype, 843 | byte_range: ByteRequest | None = None, 844 | ) -> Buffer | None: 845 | """Return value associated with key.""" 846 | # print(f'get({key=}, {byte_range=})') 847 | if byte_range is not None: 848 | raise NotImplementedError(f'{byte_range=!r} not supported') 849 | 850 | if key in self._store: 851 | return prototype.buffer.from_bytes(self._store[key]) 852 | 853 | if ( 854 | key == 'zarr.json' 855 | or key[-10:] == '.zmetadata' 856 | or key[-7:] == '.zarray' 857 | or key[-7:] == '.zgroup' 858 | ): 859 | # catch '.zarray' and 'attribute/.zarray' 860 | return None 861 | 862 | keyframe, page, chunkindex, offset, bytecount = self._parse_key(key) 863 | 864 | if page is None or offset == 0 or bytecount == 0: 865 | return None 866 | 867 | fh = page.parent.filehandle 868 | 869 | if self._chunkmode: 870 | if offset is not None: 871 | # contiguous image data in page or series 872 | # create virtual frame instead of loading page from file 873 | assert bytecount is not None 874 | page = TiffFrame( 875 | page.parent, 876 | index=0, 877 | keyframe=keyframe, 878 | dataoffsets=(offset,), 879 | databytecounts=(bytecount,), 880 | ) 881 | # TODO: use asyncio.to_thread ? 882 | self._filecache.open(fh) 883 | chunk = page.asarray( 884 | lock=self._filecache.lock, 885 | maxworkers=self._maxworkers, 886 | buffersize=self._buffersize, 887 | ) 888 | self._filecache.close(fh) 889 | if self._transform is not None: 890 | chunk = self._transform(chunk) 891 | return prototype.buffer( 892 | chunk.reshape(-1).view('B') # type: ignore[arg-type] 893 | ) 894 | 895 | assert offset is not None and bytecount is not None 896 | chunk_bytes = self._filecache.read(fh, offset, bytecount) 897 | 898 | decodeargs: dict[str, Any] = {'_fullsize': True} 899 | if page.jpegtables is not None: 900 | decodeargs['jpegtables'] = page.jpegtables 901 | if keyframe.jpegheader is not None: 902 | decodeargs['jpegheader'] = keyframe.jpegheader 903 | 904 | assert chunkindex is not None 905 | keyframe.decode # cache decode function 906 | if self._maxworkers > 1: 907 | decoded = await asyncio.to_thread( 908 | keyframe.decode, chunk_bytes, chunkindex, **decodeargs 909 | ) 910 | else: 911 | decoded = keyframe.decode(chunk_bytes, chunkindex, **decodeargs) 912 | chunk = decoded[0] # type: ignore[assignment] 913 | del decoded 914 | assert chunk is not None 915 | if self._transform is not None: 916 | chunk = self._transform(chunk) 917 | 918 | if self._chunkmode: 919 | chunks = keyframe.shape # type: ignore[unreachable] 920 | else: 921 | chunks = keyframe.chunks 922 | if chunk.size != product(chunks): 923 | raise RuntimeError(f'{chunk.size} != {product(chunks)}') 924 | return prototype.buffer( 925 | chunk.reshape(-1).view('B') # type: ignore[arg-type] 926 | ) 927 | 928 | async def exists(self, key: str) -> bool: 929 | """Return if key exists in store.""" 930 | # print(f'exists({key=})') 931 | if key in self._store: 932 | return True 933 | assert isinstance(key, str) 934 | try: 935 | _, page, _, offset, bytecount = self._parse_key(key) 936 | except (KeyError, IndexError): 937 | return False 938 | if self._chunkmode and offset is None: 939 | return True 940 | return ( 941 | page is not None 942 | and offset is not None 943 | and bytecount is not None 944 | and offset > 0 945 | and bytecount > 0 946 | ) 947 | 948 | async def set(self, key: str, value: Buffer) -> None: 949 | """Store (key, value) pair.""" 950 | if self._read_only: 951 | raise PermissionError('ZarrTiffStore is read-only') 952 | 953 | if ( 954 | key in self._store 955 | or key == 'zarr.json' 956 | or key[-10:] == '.zmetadata' 957 | or key[-7:] == '.zarray' 958 | or key[-7:] == '.zgroup' 959 | ): 960 | # catch '.zarray' and 'attribute/.zarray' 961 | return None 962 | 963 | keyframe, page, chunkindex, offset, bytecount = self._parse_key(key) 964 | if ( 965 | page is None 966 | or offset is None 967 | or offset == 0 968 | or bytecount is None 969 | or bytecount == 0 970 | ): 971 | return 972 | data = value.to_bytes() 973 | if bytecount < len(data): 974 | data = data[:bytecount] 975 | self._filecache.write(page.parent.filehandle, offset, data) 976 | 977 | def _parse_key(self, key: str, /) -> tuple[ 978 | TiffPage, 979 | TiffPage | TiffFrame | None, 980 | int | None, 981 | int | None, 982 | int | None, 983 | ]: 984 | """Return keyframe, page, index, offset, and bytecount from key. 985 | 986 | Raise KeyError if key is not valid. 987 | 988 | """ 989 | if self._multiscales: 990 | try: 991 | level, key = key.split('/') 992 | series = self._data[int(level)] 993 | except (ValueError, IndexError) as exc: 994 | raise KeyError(key) from exc 995 | else: 996 | series = self._data[0] 997 | keyframe = series.keyframe 998 | pageindex, chunkindex = self._indices(key, series) 999 | if series.dataoffset is not None: 1000 | # contiguous or truncated 1001 | page = series[0] 1002 | if page is None or page.dtype is None or page.keyframe is None: 1003 | return keyframe, None, chunkindex, 0, 0 1004 | offset = pageindex * page.size * page.dtype.itemsize 1005 | try: 1006 | offset += page.dataoffsets[chunkindex] 1007 | except IndexError as exc: 1008 | raise KeyError(key) from exc 1009 | if self._chunkmode: 1010 | bytecount = page.size * page.dtype.itemsize 1011 | return page.keyframe, page, chunkindex, offset, bytecount 1012 | elif self._chunkmode: 1013 | with self._filecache.lock: 1014 | page = series[pageindex] 1015 | if page is None or page.keyframe is None: 1016 | return keyframe, None, None, 0, 0 1017 | return page.keyframe, page, None, None, None 1018 | else: 1019 | with self._filecache.lock: 1020 | page = series[pageindex] 1021 | if page is None or page.keyframe is None: 1022 | return keyframe, None, chunkindex, 0, 0 1023 | try: 1024 | offset = page.dataoffsets[chunkindex] 1025 | except IndexError: 1026 | # raise KeyError(key) from exc 1027 | # issue #249: Philips may be missing last row of tiles 1028 | return page.keyframe, page, chunkindex, 0, 0 1029 | try: 1030 | bytecount = page.databytecounts[chunkindex] 1031 | except IndexError as exc: 1032 | raise KeyError(key) from exc 1033 | return page.keyframe, page, chunkindex, offset, bytecount 1034 | 1035 | def _indices(self, key: str, series: TiffPageSeries, /) -> tuple[int, int]: 1036 | """Return page and strile indices from Zarr chunk index.""" 1037 | keyframe = series.keyframe 1038 | shape = series.get_shape(self._squeeze) 1039 | try: 1040 | indices = [int(i) for i in key.split('.')] 1041 | except ValueError as exc: 1042 | raise KeyError(key) from exc 1043 | assert len(indices) == len(shape) 1044 | if self._chunkmode: 1045 | chunked = (1,) * len(keyframe.shape) 1046 | else: 1047 | chunked = keyframe.chunked 1048 | p = 1 1049 | for i, s in enumerate(shape[::-1]): 1050 | p *= s 1051 | if p == keyframe.size: 1052 | i = len(indices) - i - 1 1053 | frames_indices = indices[:i] 1054 | strile_indices = indices[i:] 1055 | frames_chunked = shape[:i] 1056 | strile_chunked = list(shape[i:]) # updated later 1057 | break 1058 | else: 1059 | raise RuntimeError 1060 | if len(strile_chunked) == len(keyframe.shape): 1061 | strile_chunked = list(chunked) 1062 | else: 1063 | # get strile_chunked including singleton dimensions 1064 | i = len(strile_indices) - 1 1065 | j = len(keyframe.shape) - 1 1066 | while True: 1067 | if strile_chunked[i] == keyframe.shape[j]: 1068 | strile_chunked[i] = chunked[j] 1069 | i -= 1 1070 | j -= 1 1071 | elif strile_chunked[i] == 1: 1072 | i -= 1 1073 | else: 1074 | raise RuntimeError('shape does not match page shape') 1075 | if i < 0 or j < 0: 1076 | break 1077 | assert product(strile_chunked) == product(chunked) 1078 | if len(frames_indices) > 0: 1079 | frameindex = int( 1080 | numpy.ravel_multi_index(frames_indices, frames_chunked) 1081 | ) 1082 | else: 1083 | frameindex = 0 1084 | if len(strile_indices) > 0: 1085 | strileindex = int( 1086 | numpy.ravel_multi_index(strile_indices, strile_chunked) 1087 | ) 1088 | else: 1089 | strileindex = 0 1090 | return frameindex, strileindex 1091 | 1092 | 1093 | class ZarrFileSequenceStore(ZarrStore): 1094 | """Zarr 3 store interface to image array in FileSequence. 1095 | 1096 | Parameters: 1097 | filesequence: 1098 | FileSequence instance to wrap as Zarr store. 1099 | Files in containers are not supported. 1100 | fillvalue: 1101 | Value to use for missing chunks. The default is 0. 1102 | chunkmode: 1103 | Currently only one chunk per file is supported. 1104 | chunkshape: 1105 | Shape of chunk in each file. 1106 | Must match ``FileSequence.imread(file, **imreadargs).shape``. 1107 | chunkdtype: 1108 | Data type of chunk in each file. 1109 | Must match ``FileSequence.imread(file, **imreadargs).dtype``. 1110 | axestiled: 1111 | Axes to be tiled. Map stacked sequence axis to chunk axis. 1112 | zattrs: 1113 | Additional attributes to store in `.zattrs`. 1114 | ioworkers: 1115 | If not 1, asynchronously run `imread` function in separate thread. 1116 | If enabled, internal threading for the `imread` function 1117 | should be disabled. 1118 | read_only: 1119 | Passed to zarr `Store` constructor. 1120 | imreadargs: 1121 | Arguments passed to :py:attr:`FileSequence.imread`. 1122 | **kwargs: 1123 | Arguments passed to :py:attr:`FileSequence.imread`in addition 1124 | to `imreadargs`. 1125 | 1126 | Notes: 1127 | If `chunkshape` or `chunkdtype` are *None* (default), their values 1128 | are determined by reading the first file with 1129 | ``FileSequence.imread(arg.files[0], **imreadargs)``. 1130 | 1131 | """ 1132 | 1133 | imread: Callable[..., NDArray[Any]] 1134 | """Function to read image array from single file.""" 1135 | 1136 | _lookup: dict[tuple[int, ...], str] 1137 | _chunks: tuple[int, ...] 1138 | _dtype: numpy.dtype[Any] 1139 | _tiled: TiledSequence 1140 | _commonpath: str 1141 | _ioworkers: int 1142 | _kwargs: dict[str, Any] 1143 | 1144 | def __init__( 1145 | self, 1146 | filesequence: FileSequence, 1147 | /, 1148 | *, 1149 | fillvalue: int | float | None = None, 1150 | chunkmode: CHUNKMODE | int | str | None = None, 1151 | chunkshape: Sequence[int] | None = None, 1152 | chunkdtype: DTypeLike | None = None, 1153 | axestiled: dict[int, int] | Sequence[tuple[int, int]] | None = None, 1154 | zattrs: dict[str, Any] | None = None, 1155 | ioworkers: int | None = 1, 1156 | imreadargs: dict[str, Any] | None = None, 1157 | read_only: bool = True, 1158 | **kwargs: Any, 1159 | ) -> None: 1160 | super().__init__( 1161 | fillvalue=fillvalue, chunkmode=chunkmode, read_only=read_only 1162 | ) 1163 | 1164 | if self._chunkmode not in {0, 3}: 1165 | raise ValueError(f'invalid chunkmode {self._chunkmode!r}') 1166 | 1167 | if not isinstance(filesequence, FileSequence): 1168 | raise TypeError('not a FileSequence') 1169 | 1170 | if filesequence._container: 1171 | raise NotImplementedError('cannot open container as Zarr store') 1172 | 1173 | # TODO: deprecate kwargs? 1174 | if imreadargs is not None: 1175 | kwargs |= imreadargs 1176 | 1177 | self._ioworkers = 1 if ioworkers is None else ioworkers 1178 | 1179 | self._kwargs = kwargs 1180 | self._imread = filesequence.imread 1181 | self._commonpath = filesequence.commonpath() 1182 | 1183 | if chunkshape is None or chunkdtype is None: 1184 | chunk = filesequence.imread(filesequence[0], **kwargs) 1185 | self._chunks = chunk.shape 1186 | self._dtype = chunk.dtype 1187 | else: 1188 | self._chunks = tuple(chunkshape) 1189 | self._dtype = numpy.dtype(chunkdtype) 1190 | chunk = None 1191 | 1192 | self._tiled = TiledSequence( 1193 | filesequence.shape, self._chunks, axestiled=axestiled 1194 | ) 1195 | self._lookup = dict( 1196 | zip(self._tiled.indices(filesequence.indices), filesequence) 1197 | ) 1198 | 1199 | zattrs = {} if zattrs is None else dict(zattrs) 1200 | # TODO: add _ARRAY_DIMENSIONS to ZarrFileSequenceStore 1201 | # if '_ARRAY_DIMENSIONS' not in zattrs: 1202 | # zattrs['_ARRAY_DIMENSIONS'] = list(...) 1203 | 1204 | self._store['.zattrs'] = _json_dumps(zattrs) 1205 | self._store['.zarray'] = _json_dumps( 1206 | { 1207 | 'zarr_format': 2, 1208 | 'shape': self._tiled.shape, 1209 | 'chunks': self._tiled.chunks, 1210 | 'dtype': _dtype_str(self._dtype), 1211 | 'compressor': None, 1212 | 'fill_value': _json_value(fillvalue, self._dtype), 1213 | 'order': 'C', 1214 | 'filters': None, 1215 | } 1216 | ) 1217 | 1218 | async def exists(self, key: str) -> bool: 1219 | """Return if key exists in store.""" 1220 | # print(f'exists({key=})') 1221 | if key in self._store: 1222 | return True 1223 | assert isinstance(key, str) 1224 | try: 1225 | indices = tuple(int(i) for i in key.split('.')) 1226 | except Exception: 1227 | return False 1228 | return indices in self._lookup 1229 | 1230 | async def get( 1231 | self, 1232 | key: str, 1233 | prototype: BufferPrototype, 1234 | byte_range: ByteRequest | None = None, 1235 | ) -> Buffer | None: 1236 | """Return value associated with key.""" 1237 | if byte_range is not None: 1238 | raise NotImplementedError(f'{byte_range=!r} not supported') 1239 | 1240 | if key in self._store: 1241 | return prototype.buffer.from_bytes(self._store[key]) 1242 | 1243 | if ( 1244 | key == 'zarr.json' 1245 | or key[-10:] == '.zmetadata' 1246 | or key[-7:] == '.zarray' 1247 | or key[-7:] == '.zgroup' 1248 | ): 1249 | # catch '.zarray' and 'attribute/.zarray' 1250 | return None 1251 | 1252 | indices = tuple(int(i) for i in key.split('.')) 1253 | filename = self._lookup.get(indices, None) 1254 | if filename is None: 1255 | return None 1256 | if self._ioworkers != 1: 1257 | chunk = await asyncio.to_thread( 1258 | self._imread, filename, **self._kwargs 1259 | ) 1260 | else: 1261 | chunk = self._imread(filename, **self._kwargs) 1262 | return prototype.buffer( 1263 | chunk.reshape(-1).view('B') # type: ignore[arg-type] 1264 | ) 1265 | 1266 | def write_fsspec( 1267 | self, 1268 | jsonfile: str | os.PathLike[Any] | TextIO, 1269 | /, 1270 | url: str | None, 1271 | *, 1272 | quote: bool | None = None, 1273 | groupname: str | None = None, 1274 | templatename: str | None = None, 1275 | codec_id: str | None = None, 1276 | version: int | None = None, 1277 | _append: bool = False, 1278 | _close: bool = True, 1279 | ) -> None: 1280 | """Write fsspec ReferenceFileSystem as JSON to file. 1281 | 1282 | Parameters: 1283 | jsonfile: 1284 | Name or open file handle of output JSON file. 1285 | url: 1286 | Remote location of TIFF file(s) without file name(s). 1287 | quote: 1288 | Quote file names, that is, replace ' ' with '%20'. 1289 | The default is True. 1290 | groupname: 1291 | Zarr group name. 1292 | templatename: 1293 | Version 1 URL template name. The default is 'u'. 1294 | codec_id: 1295 | Name of Numcodecs codec to decode files or chunks. 1296 | version: 1297 | Version of fsspec file to write. The default is 0. 1298 | _append, _close: 1299 | Experimental API. 1300 | 1301 | References: 1302 | - `fsspec ReferenceFileSystem format 1303 | `_ 1304 | 1305 | """ 1306 | from urllib.parse import quote as quote_ 1307 | 1308 | kwargs = self._kwargs.copy() 1309 | 1310 | if codec_id is not None: 1311 | pass 1312 | elif self._imread is imread: 1313 | codec_id = 'tifffile' 1314 | elif 'imagecodecs' in self._imread.__module__: 1315 | if ( 1316 | self._imread.__name__ != 'imread' 1317 | or 'codec' not in self._kwargs 1318 | ): 1319 | raise ValueError('cannot determine codec_id') 1320 | codec = kwargs.pop('codec') 1321 | if isinstance(codec, (list, tuple)): 1322 | codec = codec[0] 1323 | if callable(codec): 1324 | codec = codec.__name__.split('_')[0] 1325 | codec_id = { 1326 | 'apng': 'imagecodecs_apng', 1327 | 'avif': 'imagecodecs_avif', 1328 | 'gif': 'imagecodecs_gif', 1329 | 'heif': 'imagecodecs_heif', 1330 | 'jpeg': 'imagecodecs_jpeg', 1331 | 'jpeg8': 'imagecodecs_jpeg', 1332 | 'jpeg12': 'imagecodecs_jpeg', 1333 | 'jpeg2k': 'imagecodecs_jpeg2k', 1334 | 'jpegls': 'imagecodecs_jpegls', 1335 | 'jpegxl': 'imagecodecs_jpegxl', 1336 | 'jpegxr': 'imagecodecs_jpegxr', 1337 | 'ljpeg': 'imagecodecs_ljpeg', 1338 | 'lerc': 'imagecodecs_lerc', 1339 | # 'npy': 'imagecodecs_npy', 1340 | 'png': 'imagecodecs_png', 1341 | 'qoi': 'imagecodecs_qoi', 1342 | 'tiff': 'imagecodecs_tiff', 1343 | 'webp': 'imagecodecs_webp', 1344 | 'zfp': 'imagecodecs_zfp', 1345 | }[codec] 1346 | else: 1347 | # TODO: choose codec from filename 1348 | raise ValueError('cannot determine codec_id') 1349 | 1350 | if url is None: 1351 | url = '' 1352 | elif url and url[-1] != '/': 1353 | url += '/' 1354 | 1355 | if groupname is None: 1356 | groupname = '' 1357 | elif groupname and groupname[-1] != '/': 1358 | groupname += '/' 1359 | 1360 | refs: dict[str, Any] = {} 1361 | if version == 1: 1362 | if _append: 1363 | raise ValueError('cannot append to version 1 files') 1364 | if templatename is None: 1365 | templatename = 'u' 1366 | refs['version'] = 1 1367 | refs['templates'] = {templatename: url} 1368 | refs['gen'] = [] 1369 | refs['refs'] = refzarr = {} 1370 | url = f'{{{{{templatename}}}}}' 1371 | else: 1372 | refzarr = refs 1373 | 1374 | if groupname and not _append: 1375 | refzarr['.zgroup'] = _json_dumps({'zarr_format': 2}).decode() 1376 | 1377 | for key, value in self._store.items(): 1378 | if '.zarray' in key: 1379 | value = json.loads(value) 1380 | # TODO: make kwargs serializable 1381 | value['compressor'] = {'id': codec_id, **kwargs} 1382 | value = _json_dumps(value) 1383 | refzarr[groupname + key] = value.decode() 1384 | 1385 | fh: TextIO 1386 | if hasattr(jsonfile, 'write'): 1387 | fh = jsonfile # type: ignore[assignment] 1388 | else: 1389 | fh = open(jsonfile, 'w', encoding='utf-8') 1390 | 1391 | if version == 1: 1392 | fh.write(json.dumps(refs, indent=1).rsplit('}"', 1)[0] + '}"') 1393 | indent = ' ' 1394 | elif _append: 1395 | fh.write(',\n') 1396 | fh.write(json.dumps(refs, indent=1)[2:-2]) 1397 | indent = ' ' 1398 | else: 1399 | fh.write(json.dumps(refs, indent=1)[:-2]) 1400 | indent = ' ' 1401 | 1402 | prefix = len(self._commonpath) 1403 | 1404 | for key, value in self._store.items(): 1405 | if '.zarray' in key: 1406 | value = json.loads(value) 1407 | for index, filename in sorted( 1408 | self._lookup.items(), key=lambda x: x[0] 1409 | ): 1410 | filename = filename[prefix:].replace('\\', '/') 1411 | if quote is None or quote: 1412 | filename = quote_(filename) 1413 | if filename[0] == '/': 1414 | filename = filename[1:] 1415 | indexstr = '.'.join(str(i) for i in index) 1416 | fh.write( 1417 | f',\n{indent}"{groupname}{indexstr}": ' 1418 | f'["{url}{filename}"]' 1419 | ) 1420 | 1421 | if version == 1: 1422 | fh.write('\n }\n}') 1423 | elif _close: 1424 | fh.write('\n}') 1425 | 1426 | if not hasattr(jsonfile, 'write'): 1427 | fh.close() 1428 | 1429 | 1430 | def zarr_selection( 1431 | store: ZarrStore, 1432 | selection: BasicSelection, 1433 | /, 1434 | *, 1435 | groupindex: str | None = None, 1436 | close: bool = True, 1437 | out: OutputType = None, 1438 | ) -> NDArray[Any]: 1439 | """Return selection from Zarr store. 1440 | 1441 | Parameters: 1442 | store: 1443 | ZarrStore instance to read selection from. 1444 | selection: 1445 | Subset of image to be extracted and returned. 1446 | Refer to the Zarr documentation for valid selections. 1447 | groupindex: 1448 | Index of array if store is Zarr group. 1449 | close: 1450 | Close store before returning. 1451 | out: 1452 | Specifies how image array is returned. 1453 | By default, create a new array. 1454 | If a *numpy.ndarray*, a writable array to which the images 1455 | are copied. 1456 | If *'memmap'*, create a memory-mapped array in a temporary 1457 | file. 1458 | If a *string* or *open file*, the file used to create a 1459 | memory-mapped array. 1460 | 1461 | """ 1462 | import zarr 1463 | from zarr.core.indexing import BasicIndexer 1464 | 1465 | zarray: zarr.Array 1466 | 1467 | z = zarr.open(store, mode='r', zarr_format=2) 1468 | try: 1469 | if isinstance(z, zarr.Group): 1470 | if groupindex is None: 1471 | groupindex = '0' 1472 | zarray = z[groupindex] # type: ignore[assignment] 1473 | else: 1474 | zarray = z 1475 | if out is not None: 1476 | shape = BasicIndexer( 1477 | selection, 1478 | shape=zarray.shape, 1479 | chunk_grid=RegularChunkGrid(chunk_shape=zarray.chunks), 1480 | ).shape 1481 | ndbuffer = NDBuffer.from_numpy_array( 1482 | create_output(out, shape, zarray.dtype) 1483 | ) 1484 | else: 1485 | ndbuffer = None 1486 | result = zarray.get_basic_selection(selection, out=ndbuffer) 1487 | finally: 1488 | if close: 1489 | store.close() 1490 | return result # type: ignore[return-value] 1491 | 1492 | 1493 | def _empty_chunk( 1494 | shape: tuple[int, ...], 1495 | dtype: DTypeLike, 1496 | fillvalue: int | float | None, 1497 | /, 1498 | ) -> NDArray[Any]: 1499 | """Return empty chunk.""" 1500 | if fillvalue is None or fillvalue == 0: 1501 | # return bytes(product(shape) * dtype.itemsize) 1502 | return numpy.zeros(shape, dtype) 1503 | chunk = numpy.empty(shape, dtype) 1504 | chunk[:] = fillvalue 1505 | return chunk # .tobytes() 1506 | 1507 | 1508 | def _dtype_str(dtype: numpy.dtype[Any], /) -> str: 1509 | """Return dtype as string with native byte order.""" 1510 | if dtype.itemsize == 1: 1511 | byteorder = '|' 1512 | else: 1513 | byteorder = {'big': '>', 'little': '<'}[sys.byteorder] 1514 | return byteorder + dtype.str[1:] 1515 | 1516 | 1517 | def _json_dumps(obj: Any, /) -> bytes: 1518 | """Serialize object to JSON formatted string.""" 1519 | return json.dumps( 1520 | obj, 1521 | indent=1, 1522 | sort_keys=True, 1523 | ensure_ascii=True, 1524 | separators=(',', ': '), 1525 | ).encode('ascii') 1526 | 1527 | 1528 | def _json_value(value: Any, dtype: numpy.dtype[Any], /) -> Any: 1529 | """Return value which is serializable to JSON.""" 1530 | if value is None: 1531 | return value 1532 | if dtype.kind == 'b': 1533 | return bool(value) 1534 | if dtype.kind in 'ui': 1535 | return int(value) 1536 | if dtype.kind == 'f': 1537 | if numpy.isnan(value): 1538 | return 'NaN' 1539 | if numpy.isposinf(value): 1540 | return 'Infinity' 1541 | if numpy.isneginf(value): 1542 | return '-Infinity' 1543 | return float(value) 1544 | if dtype.kind == 'c': 1545 | value = numpy.array(value, dtype) 1546 | return ( 1547 | _json_value(value.real, dtype.type().real.dtype), 1548 | _json_value(value.imag, dtype.type().imag.dtype), 1549 | ) 1550 | return value 1551 | 1552 | 1553 | def _ndindex( 1554 | shape: tuple[int, ...], chunks: tuple[int, ...], / 1555 | ) -> Iterator[str]: 1556 | """Return iterator over all chunk index strings.""" 1557 | assert len(shape) == len(chunks) 1558 | chunked = tuple( 1559 | i // j + (1 if i % j else 0) for i, j in zip(shape, chunks) 1560 | ) 1561 | for indices in numpy.ndindex(chunked): 1562 | yield '.'.join(str(index) for index in indices) 1563 | 1564 | 1565 | def _is_writable(keyframe: TiffPage) -> bool: 1566 | """Return True if chunks are writable.""" 1567 | return ( 1568 | keyframe.compression == 1 1569 | and keyframe.fillorder == 1 1570 | and keyframe.sampleformat in {1, 2, 3, 6} 1571 | and keyframe.bitspersample in {8, 16, 32, 64, 128} 1572 | # and ( 1573 | # keyframe.rowsperstrip == 0 1574 | # or keyframe.imagelength % keyframe.rowsperstrip == 0 1575 | # ) 1576 | ) 1577 | 1578 | 1579 | def _chunks( 1580 | chunks: tuple[int, ...], shape: tuple[int, ...], / 1581 | ) -> tuple[int, ...]: 1582 | """Return chunks with same length as shape.""" 1583 | ndim = len(shape) 1584 | if ndim == 0: 1585 | return () # empty array 1586 | if 0 in shape: 1587 | return (1,) * ndim 1588 | newchunks = [] 1589 | i = ndim - 1 1590 | j = len(chunks) - 1 1591 | while True: 1592 | if j < 0: 1593 | newchunks.append(1) 1594 | i -= 1 1595 | elif shape[i] > 1 and chunks[j] > 1: 1596 | newchunks.append(chunks[j]) 1597 | i -= 1 1598 | j -= 1 1599 | elif shape[i] == chunks[j]: # both 1 1600 | newchunks.append(1) 1601 | i -= 1 1602 | j -= 1 1603 | elif shape[i] == 1: 1604 | newchunks.append(1) 1605 | i -= 1 1606 | elif chunks[j] == 1: 1607 | newchunks.append(1) 1608 | j -= 1 1609 | else: 1610 | raise RuntimeError 1611 | if i < 0 or ndim == len(newchunks): 1612 | break 1613 | # assert ndim == len(newchunks) 1614 | return tuple(newchunks[::-1]) 1615 | --------------------------------------------------------------------------------