1
2
3#! /usr/bin/env python3
4# -*- coding: utf-8 -*-
5# tifffile.py
6
7# Copyright (c) 2008-2018, Christoph Gohlke
8# Copyright (c) 2008-2018, The Regents of the University of California
9# Produced at the Laboratory for Fluorescence Dynamics
10# All rights reserved.
11#
12# Redistribution and use in source and binary forms, with or without
13# modification, are permitted provided that the following conditions are met:
14#
15# * Redistributions of source code must retain the above copyright
16#   notice, this list of conditions and the following disclaimer.
17# * Redistributions in binary form must reproduce the above copyright
18#   notice, this list of conditions and the following disclaimer in the
19#   documentation and/or other materials provided with the distribution.
20# * Neither the name of the copyright holders nor the names of any
21#   contributors may be used to endorse or promote products derived
22#   from this software without specific prior written permission.
23#
24# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
28# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
29# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
30# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
31# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
32# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34# POSSIBILITY OF SUCH DAMAGE.
35
36"""Read image and meta data from (bio) TIFF(R) files. Save numpy arrays as TIFF.
37
38Image and metadata can be read from TIFF, BigTIFF, OME-TIFF, STK, LSM, NIH,
39SGI, ImageJ, MicroManager, FluoView, ScanImage, SEQ, GEL, and GeoTIFF files.
40
41Tifffile is not a general-purpose TIFF library.
42Only a subset of the TIFF specification is supported, mainly uncompressed and
43losslessly compressed 1, 8, 16, 32 and 64 bit integer, 16, 32 and 64-bit float,
44grayscale and RGB(A) images, which are commonly used in scientific imaging.
45Specifically, reading slices of image data, image trees defined via SubIFDs,
46CCITT and OJPEG compression, chroma subsampling without JPEG compression,
47or IPTC and XMP metadata are not implemented.
48
49TIFF(R), the tagged Image File Format, is a trademark and under control of
50Adobe Systems Incorporated. BigTIFF allows for files greater than 4 GB.
51STK, LSM, FluoView, SGI, SEQ, GEL, and OME-TIFF, are custom extensions
52defined by Molecular Devices (Universal Imaging Corporation), Carl Zeiss
53MicroImaging, Olympus, Silicon Graphics International, Media Cybernetics,
54Molecular Dynamics, and the Open Microscopy Environment consortium
55respectively.
56
57For command line usage run C{python -m tifffile --help}
58
59:Author:
60  `Christoph Gohlke <https://www.lfd.uci.edu/~gohlke/>`_
61
62:Organization:
63  Laboratory for Fluorescence Dynamics, University of California, Irvine
64
65:Version: 2018.06.15
66
67Requirements
68------------
69* `CPython 3.6 64-bit <https://www.python.org>`_
70* `Numpy 1.14 <http://www.numpy.org>`_
71* `Matplotlib 2.2 <https://www.matplotlib.org>`_ (optional for plotting)
72* `Tifffile.c 2018.02.10 <https://www.lfd.uci.edu/~gohlke/>`_
73  (recommended for faster decoding of PackBits and LZW encoded strings)
74* `Tifffile_geodb.py 2018.02.10 <https://www.lfd.uci.edu/~gohlke/>`_
75  (optional enums for GeoTIFF metadata)
76* Python 2 requires 'futures', 'enum34', 'pathlib'.
77
78Revisions
79---------
802018.06.15
81    Pass 2680 tests.
82    Towards reading JPEG and other compressions via imagecodecs package (WIP).
83    Add function to validate TIFF using 'jhove -m TIFF-hul'.
84    Save bool arrays as bilevel TIFF.
85    Accept pathlib.Path as filenames.
86    Move 'software' argument from TiffWriter __init__ to save.
87    Raise DOS limit to 16 TB.
88    Lazy load lzma and zstd compressors and decompressors.
89    Add option to save IJMetadata tags.
90    Return correct number of pages for truncated series (bug fix).
91    Move EXIF tags to TIFF.TAG as per TIFF/EP standard.
922018.02.18
93    Pass 2293 tests.
94    Always save RowsPerStrip and Resolution tags as required by TIFF standard.
95    Do not use badly typed ImageDescription.
96    Coherce bad ASCII string tags to bytes.
97    Tuning of __str__ functions.
98    Fix reading 'undefined' tag values (bug fix).
99    Read and write ZSTD compressed data.
100    Use hexdump to print byte strings.
101    Determine TIFF byte order from data dtype in imsave.
102    Add option to specify RowsPerStrip for compressed strips.
103    Allow memory map of arrays with non-native byte order.
104    Attempt to handle ScanImage <= 5.1 files.
105    Restore TiffPageSeries.pages sequence interface.
106    Use numpy.frombuffer instead of fromstring to read from binary data.
107    Parse GeoTIFF metadata.
108    Add option to apply horizontal differencing before compression.
109    Towards reading PerkinElmer QPTIFF (no test files).
110    Do not index out of bounds data in tifffile.c unpackbits and decodelzw.
1112017.09.29 (tentative)
112    Many backwards incompatible changes improving speed and resource usage:
113    Pass 2268 tests.
114    Add detail argument to __str__ function. Remove info functions.
115    Fix potential issue correcting offsets of large LSM files with positions.
116    Remove TiffFile sequence interface; use TiffFile.pages instead.
117    Do not make tag values available as TiffPage attributes.
118    Use str (not bytes) type for tag and metadata strings (WIP).
119    Use documented standard tag and value names (WIP).
120    Use enums for some documented TIFF tag values.
121    Remove 'memmap' and 'tmpfile' options; use out='memmap' instead.
122    Add option to specify output in asarray functions.
123    Add option to concurrently decode image strips or tiles using threads.
124    Add TiffPage.asrgb function (WIP).
125    Do not apply colormap in asarray.
126    Remove 'colormapped', 'rgbonly', and 'scale_mdgel' options from asarray.
127    Consolidate metadata in TiffFile _metadata functions.
128    Remove non-tag metadata properties from TiffPage.
129    Add function to convert LSM to tiled BIN files.
130    Align image data in file.
131    Make TiffPage.dtype a numpy.dtype.
132    Add 'ndim' and 'size' properties to TiffPage and TiffPageSeries.
133    Allow imsave to write non-BigTIFF files up to ~4 GB.
134    Only read one page for shaped series if possible.
135    Add memmap function to create memory-mapped array stored in TIFF file.
136    Add option to save empty arrays to TIFF files.
137    Add option to save truncated TIFF files.
138    Allow single tile images to be saved contiguously.
139    Add optional movie mode for files with uniform pages.
140    Lazy load pages.
141    Use lightweight TiffFrame for IFDs sharing properties with key TiffPage.
142    Move module constants to 'TIFF' namespace (speed up module import).
143    Remove 'fastij' option from TiffFile.
144    Remove 'pages' parameter from TiffFile.
145    Remove TIFFfile alias.
146    Deprecate Python 2.
147    Require enum34 and futures packages on Python 2.7.
148    Remove Record class and return all metadata as dict instead.
149    Add functions to parse STK, MetaSeries, ScanImage, SVS, Pilatus metadata.
150    Read tags from EXIF and GPS IFDs.
151    Use pformat for tag and metadata values.
152    Fix reading some UIC tags (bug fix).
153    Do not modify input array in imshow (bug fix).
154    Fix Python implementation of unpack_ints.
1552017.05.23
156    Pass 1961 tests.
157    Write correct number of SampleFormat values (bug fix).
158    Use Adobe deflate code to write ZIP compressed files.
159    Add option to pass tag values as packed binary data for writing.
160    Defer tag validation to attribute access.
161    Use property instead of lazyattr decorator for simple expressions.
1622017.03.17
163    Write IFDs and tag values on word boundaries.
164    Read ScanImage metadata.
165    Remove is_rgb and is_indexed attributes from TiffFile.
166    Create files used by doctests.
1672017.01.12
168    Read Zeiss SEM metadata.
169    Read OME-TIFF with invalid references to external files.
170    Rewrite C LZW decoder (5x faster).
171    Read corrupted LSM files missing EOI code in LZW stream.
1722017.01.01
173    Add option to append images to existing TIFF files.
174    Read files without pages.
175    Read S-FEG and Helios NanoLab tags created by FEI software.
176    Allow saving Color Filter Array (CFA) images.
177    Add info functions returning more information about TiffFile and TiffPage.
178    Add option to read specific pages only.
179    Remove maxpages argument (backwards incompatible).
180    Remove test_tifffile function.
1812016.10.28
182    Pass 1944 tests.
183    Improve detection of ImageJ hyperstacks.
184    Read TVIPS metadata created by EM-MENU (by Marco Oster).
185    Add option to disable using OME-XML metadata.
186    Allow non-integer range attributes in modulo tags (by Stuart Berg).
1872016.06.21
188    Do not always memmap contiguous data in page series.
1892016.05.13
190    Add option to specify resolution unit.
191    Write grayscale images with extra samples when planarconfig is specified.
192    Do not write RGB color images with 2 samples.
193    Reorder TiffWriter.save keyword arguments (backwards incompatible).
1942016.04.18
195    Pass 1932 tests.
196    TiffWriter, imread, and imsave accept open binary file streams.
1972016.04.13
198    Correctly handle reversed fill order in 2 and 4 bps images (bug fix).
199    Implement reverse_bitorder in C.
2002016.03.18
201    Fix saving additional ImageJ metadata.
2022016.02.22
203    Pass 1920 tests.
204    Write 8 bytes double tag values using offset if necessary (bug fix).
205    Add option to disable writing second image description tag.
206    Detect tags with incorrect counts.
207    Disable color mapping for LSM.
2082015.11.13
209    Read LSM 6 mosaics.
210    Add option to specify directory of memory-mapped files.
211    Add command line options to specify vmin and vmax values for colormapping.
2122015.10.06
213    New helper function to apply colormaps.
214    Renamed is_palette attributes to is_indexed (backwards incompatible).
215    Color-mapped samples are now contiguous (backwards incompatible).
216    Do not color-map ImageJ hyperstacks (backwards incompatible).
217    Towards reading Leica SCN.
2182015.09.25
219    Read images with reversed bit order (FillOrder is LSB2MSB).
2202015.09.21
221    Read RGB OME-TIFF.
222    Warn about malformed OME-XML.
2232015.09.16
224    Detect some corrupted ImageJ metadata.
225    Better axes labels for 'shaped' files.
226    Do not create TiffTag for default values.
227    Chroma subsampling is not supported.
228    Memory-map data in TiffPageSeries if possible (optional).
2292015.08.17
230    Pass 1906 tests.
231    Write ImageJ hyperstacks (optional).
232    Read and write LZMA compressed data.
233    Specify datetime when saving (optional).
234    Save tiled and color-mapped images (optional).
235    Ignore void bytecounts and offsets if possible.
236    Ignore bogus image_depth tag created by ISS Vista software.
237    Decode floating point horizontal differencing (not tiled).
238    Save image data contiguously if possible.
239    Only read first IFD from ImageJ files if possible.
240    Read ImageJ 'raw' format (files larger than 4 GB).
241    TiffPageSeries class for pages with compatible shape and data type.
242    Try to read incomplete tiles.
243    Open file dialog if no filename is passed on command line.
244    Ignore errors when decoding OME-XML.
245    Rename decoder functions (backwards incompatible).
2462014.08.24
247    TiffWriter class for incremental writing images.
248    Simplify examples.
2492014.08.19
250    Add memmap function to FileHandle.
251    Add function to determine if image data in TiffPage is memory-mappable.
252    Do not close files if multifile_close parameter is False.
2532014.08.10
254    Pass 1730 tests.
255    Return all extrasamples by default (backwards incompatible).
256    Read data from series of pages into memory-mapped array (optional).
257    Squeeze OME dimensions (backwards incompatible).
258    Workaround missing EOI code in strips.
259    Support image and tile depth tags (SGI extension).
260    Better handling of STK/UIC tags (backwards incompatible).
261    Disable color mapping for STK.
262    Julian to datetime converter.
263    TIFF ASCII type may be NULL separated.
264    Unwrap strip offsets for LSM files greater than 4 GB.
265    Correct strip byte counts in compressed LSM files.
266    Skip missing files in OME series.
267    Read embedded TIFF files.
2682014.02.05
269    Save rational numbers as type 5 (bug fix).
2702013.12.20
271    Keep other files in OME multi-file series closed.
272    FileHandle class to abstract binary file handle.
273    Disable color mapping for bad OME-TIFF produced by bio-formats.
274    Read bad OME-XML produced by ImageJ when cropping.
2752013.11.03
276    Allow zlib compress data in imsave function (optional).
277    Memory-map contiguous image data (optional).
2782013.10.28
279    Read MicroManager metadata and little-endian ImageJ tag.
280    Save extra tags in imsave function.
281    Save tags in ascending order by code (bug fix).
2822012.10.18
283    Accept file like objects (read from OIB files).
2842012.08.21
285    Rename TIFFfile to TiffFile and TIFFpage to TiffPage.
286    TiffSequence class for reading sequence of TIFF files.
287    Read UltraQuant tags.
288    Allow float numbers as resolution in imsave function.
2892012.08.03
290    Read MD GEL tags and NIH Image header.
2912012.07.25
292    Read ImageJ tags.
293    ...
294
295Notes
296-----
297The API is not stable yet and might change between revisions.
298
299Tested on little-endian platforms only.
300
301Other Python packages and modules for reading (bio) scientific TIFF files:
302
303*  `python-bioformats <https://github.com/CellProfiler/python-bioformats>`_
304*  `Imread <https://github.com/luispedro/imread>`_
305*  `PyLibTiff <https://github.com/pearu/pylibtiff>`_
306*  `ITK <https://www.itk.org>`_
307*  `PyLSM <https://launchpad.net/pylsm>`_
308*  `PyMca.TiffIO.py <https://github.com/vasole/pymca>`_ (same as fabio.TiffIO)
309*  `BioImageXD.Readers <http://www.bioimagexd.net/>`_
310*  `Cellcognition.io <http://cellcognition.org/>`_
311*  `pymimage <https://github.com/ardoi/pymimage>`_
312*  `pytiff <https://github.com/FZJ-INM1-BDA/pytiff>`_
313
314Acknowledgements
315----------------
316*   Egor Zindy, University of Manchester, for lsm_scan_info specifics.
317*   Wim Lewis for a bug fix and some LSM functions.
318*   Hadrien Mary for help on reading MicroManager files.
319*   Christian Kliche for help writing tiled and color-mapped files.
320
321References
322----------
3231)  TIFF 6.0 Specification and Supplements. Adobe Systems Incorporated.
324    http://partners.adobe.com/public/developer/tiff/
3252)  TIFF File Format FAQ. http://www.awaresystems.be/imaging/tiff/faq.html
3263)  MetaMorph Stack (STK) Image File Format.
327    http://support.meta.moleculardevices.com/docs/t10243.pdf
3284)  Image File Format Description LSM 5/7 Release 6.0 (ZEN 2010).
329    Carl Zeiss MicroImaging GmbH. BioSciences. May 10, 2011
3305)  The OME-TIFF format.
331    http://www.openmicroscopy.org/site/support/file-formats/ome-tiff
3326)  UltraQuant(r) Version 6.0 for Windows Start-Up Guide.
333    http://www.ultralum.com/images%20ultralum/pdf/UQStart%20Up%20Guide.pdf
3347)  Micro-Manager File Formats.
335    http://www.micro-manager.org/wiki/Micro-Manager_File_Formats
3368)  Tags for TIFF and Related Specifications. Digital Preservation.
337    http://www.digitalpreservation.gov/formats/content/tiff_tags.shtml
3389)  ScanImage BigTiff Specification - ScanImage 2016.
339    http://scanimage.vidriotechnologies.com/display/SI2016/
340    ScanImage+BigTiff+Specification
34110) CIPA DC-008-2016: Exchangeable image file format for digital still cameras:
342    Exif Version 2.31.
343    http://www.cipa.jp/std/documents/e/DC-008-Translation-2016-E.pdf
344
345Examples
346--------
347>>> # write numpy array to TIFF file
348>>> data = numpy.random.rand(4, 301, 219)
349>>> imsave('temp.tif', data, photometric='minisblack')
350
351>>> # read numpy array from TIFF file
352>>> image = imread('temp.tif')
353>>> numpy.testing.assert_array_equal(image, data)
354
355>>> # iterate over pages and tags in TIFF file
356>>> with TiffFile('temp.tif') as tif:
357...     images = tif.asarray()
358...     for page in tif.pages:
359...         for tag in page.tags.values():
360...             _ = tag.name, tag.value
361...         image = page.asarray()
362
363"""
364
365from __future__ import division, print_function
366
367import sys
368import os
369import io
370import re
371import glob
372import math
373import zlib
374import time
375import json
376import enum
377import struct
378import pathlib
379import warnings
380import binascii
381import tempfile
382import datetime
383import threading
384import collections
385import multiprocessing
386import concurrent.futures
387
388import numpy
389
390# delay imports: mmap, pprint, fractions, xml, tkinter, matplotlib, lzma, zstd,
391#                subprocess
392
393__version__ = '2018.06.15'
394__docformat__ = 'restructuredtext en'
395__all__ = (
396    'imsave', 'imread', 'imshow', 'memmap',
397    'TiffFile', 'TiffWriter', 'TiffSequence',
398    # utility functions used by oiffile or czifile
399    'FileHandle', 'lazyattr', 'natural_sorted', 'decode_lzw', 'stripnull',
400    'create_output', 'repeat_nd', 'format_size', 'product', 'xml2dict')
401
402
403def imread(files, **kwargs):
404    """Return image data from TIFF file(s) as numpy array.
405
406    Refer to the TiffFile class and member functions for documentation.
407
408    Parameters
409    ----------
410    files : str, binary stream, or sequence
411        File name, seekable binary stream, glob pattern, or sequence of
412        file names.
413    kwargs : dict
414        Parameters 'multifile' and 'is_ome' are passed to the TiffFile class.
415        The 'pattern' parameter is passed to the TiffSequence class.
416        Other parameters are passed to the asarray functions.
417        The first image series is returned if no arguments are provided.
418
419    Examples
420    --------
421    >>> # get image from first page
422    >>> imsave('temp.tif', numpy.random.rand(3, 4, 301, 219))
423    >>> im = imread('temp.tif', key=0)
424    >>> im.shape
425    (4, 301, 219)
426
427    >>> # get images from sequence of files
428    >>> ims = imread(['temp.tif', 'temp.tif'])
429    >>> ims.shape
430    (2, 3, 4, 301, 219)
431
432    """
433    kwargs_file = parse_kwargs(kwargs, 'multifile', 'is_ome')
434    kwargs_seq = parse_kwargs(kwargs, 'pattern')
435
436    if isinstance(files, basestring) and any(i in files for i in '?*'):
437        files = glob.glob(files)
438    if not files:
439        raise ValueError('no files found')
440    if not hasattr(files, 'seek') and len(files) == 1:
441        files = files[0]
442
443    if isinstance(files, basestring) or hasattr(files, 'seek'):
444        with TiffFile(files, **kwargs_file) as tif:
445            return tif.asarray(**kwargs)
446    else:
447        with TiffSequence(files, **kwargs_seq) as imseq:
448            return imseq.asarray(**kwargs)
449
450
451def imsave(file, data=None, shape=None, dtype=None, bigsize=2**32-2**25,
452           **kwargs):
453    """Write numpy array to TIFF file.
454
455    Refer to the TiffWriter class and member functions for documentation.
456
457    Parameters
458    ----------
459    file : str or binary stream
460        File name or writable binary stream, such as an open file or BytesIO.
461    data : array_like
462        Input image. The last dimensions are assumed to be image depth,
463        height, width, and samples.
464        If None, an empty array of the specified shape and dtype is
465        saved to file.
466        Unless 'byteorder' is specified in 'kwargs', the TIFF file byte order
467        is determined from the data's dtype or the dtype argument.
468    shape : tuple
469        If 'data' is None, shape of an empty array to save to the file.
470    dtype : numpy.dtype
471        If 'data' is None, data-type of an empty array to save to the file.
472    bigsize : int
473        Create a BigTIFF file if the size of data in bytes is larger than
474        this threshold and 'imagej' or 'truncate' are not enabled.
475        By default, the threshold is 4 GB minus 32 MB reserved for metadata.
476        Use the 'bigtiff' parameter to explicitly specify the type of
477        file created.
478    kwargs : dict
479        Parameters 'append', 'byteorder', 'bigtiff', and 'imagej', are passed
480        to TiffWriter(). Other parameters are passed to TiffWriter.save().
481
482    Returns
483    -------
484    If the image data are written contiguously, return offset and bytecount
485    of image data in the file.
486
487    Examples
488    --------
489    >>> # save a RGB image
490    >>> data = numpy.random.randint(0, 255, (256, 256, 3), 'uint8')
491    >>> imsave('temp.tif', data, photometric='rgb')
492
493    >>> # save a random array and metadata, using compression
494    >>> data = numpy.random.rand(2, 5, 3, 301, 219)
495    >>> imsave('temp.tif', data, compress=6, metadata={'axes': 'TZCYX'})
496
497    """
498    tifargs = parse_kwargs(kwargs, 'append', 'bigtiff', 'byteorder', 'imagej')
499    if data is None:
500        size = product(shape) * numpy.dtype(dtype).itemsize
501        byteorder = numpy.dtype(dtype).byteorder
502    else:
503        try:
504            size = data.nbytes
505            byteorder = data.dtype.byteorder
506        except Exception:
507            size = 0
508            byteorder = None
509    if size > bigsize and 'bigtiff' not in tifargs and not (
510            tifargs.get('imagej', False) or tifargs.get('truncate', False)):
511        tifargs['bigtiff'] = True
512    if 'byteorder' not in tifargs:
513        tifargs['byteorder'] = byteorder
514
515    with TiffWriter(file, **tifargs) as tif:
516        return tif.save(data, shape, dtype, **kwargs)
517
518
519def memmap(filename, shape=None, dtype=None, page=None, series=0, mode='r+',
520           **kwargs):
521    """Return memory-mapped numpy array stored in TIFF file.
522
523    Memory-mapping requires data stored in native byte order, without tiling,
524    compression, predictors, etc.
525    If 'shape' and 'dtype' are provided, existing files will be overwritten or
526    appended to depending on the 'append' parameter.
527    Otherwise the image data of a specified page or series in an existing
528    file will be memory-mapped. By default, the image data of the first page
529    series is memory-mapped.
530    Call flush() to write any changes in the array to the file.
531    Raise ValueError if the image data in the file is not memory-mappable.
532
533    Parameters
534    ----------
535    filename : str
536        Name of the TIFF file which stores the array.
537    shape : tuple
538        Shape of the empty array.
539    dtype : numpy.dtype
540        Data-type of the empty array.
541    page : int
542        Index of the page which image data to memory-map.
543    series : int
544        Index of the page series which image data to memory-map.
545    mode : {'r+', 'r', 'c'}, optional
546        The file open mode. Default is to open existing file for reading and
547        writing ('r+').
548    kwargs : dict
549        Additional parameters passed to imsave() or TiffFile().
550
551    Examples
552    --------
553    >>> # create an empty TIFF file and write to memory-mapped image
554    >>> im = memmap('temp.tif', shape=(256, 256), dtype='float32')
555    >>> im[255, 255] = 1.0
556    >>> im.flush()
557    >>> im.shape, im.dtype
558    ((256, 256), dtype('float32'))
559    >>> del im
560
561    >>> # memory-map image data in a TIFF file
562    >>> im = memmap('temp.tif', page=0)
563    >>> im[255, 255]
564    1.0
565
566    """
567    if shape is not None and dtype is not None:
568        # create a new, empty array
569        kwargs.update(data=None, shape=shape, dtype=dtype, returnoffset=True,
570                      align=TIFF.ALLOCATIONGRANULARITY)
571        result = imsave(filename, **kwargs)
572        if result is None:
573            # TODO: fail before creating file or writing data
574            raise ValueError('image data are not memory-mappable')
575        offset = result[0]
576    else:
577        # use existing file
578        with TiffFile(filename, **kwargs) as tif:
579            if page is not None:
580                page = tif.pages[page]
581                if not page.is_memmappable:
582                    raise ValueError('image data are not memory-mappable')
583                offset, _ = page.is_contiguous
584                shape = page.shape
585                dtype = page.dtype
586            else:
587                series = tif.series[series]
588                if series.offset is None:
589                    raise ValueError('image data are not memory-mappable')
590                shape = series.shape
591                dtype = series.dtype
592                offset = series.offset
593            dtype = tif.byteorder + dtype.char
594    return numpy.memmap(filename, dtype, mode, offset, shape, 'C')
595
596
597class lazyattr(object):
598    """Attribute whose value is computed on first access."""
599    # TODO: help() doesn't work
600    __slots__ = ('func',)
601
602    def __init__(self, func):
603        self.func = func
604        # self.__name__ = func.__name__
605        # self.__doc__ = func.__doc__
606        # self.lock = threading.RLock()
607
608    def __get__(self, instance, owner):
609        # with self.lock:
610        if instance is None:
611            return self
612        try:
613            value = self.func(instance)
614        except AttributeError as e:
615            raise RuntimeError(e)
616        if value is NotImplemented:
617            return getattr(super(owner, instance), self.func.__name__)
618        setattr(instance, self.func.__name__, value)
619        return value
620
621
622class TiffWriter(object):
623    """Write numpy arrays to TIFF file.
624
625    TiffWriter instances must be closed using the 'close' method, which is
626    automatically called when using the 'with' context manager.
627
628    TiffWriter's main purpose is saving nD numpy array's as TIFF,
629    not to create any possible TIFF format. Specifically, JPEG compression,
630    SubIFDs, ExifIFD, or GPSIFD tags are not supported.
631
632    Examples
633    --------
634    >>> # successively append images to BigTIFF file
635    >>> data = numpy.random.rand(2, 5, 3, 301, 219)
636    >>> with TiffWriter('temp.tif', bigtiff=True) as tif:
637    ...     for i in range(data.shape[0]):
638    ...         tif.save(data[i], compress=6, photometric='minisblack')
639
640    """
641    def __init__(self, file, bigtiff=False, byteorder=None, append=False,
642                 imagej=False):
643        """Open a TIFF file for writing.
644
645        An empty TIFF file is created if the file does not exist, else the
646        file is overwritten with an empty TIFF file unless 'append'
647        is true. Use bigtiff=True when creating files larger than 4 GB.
648
649        Parameters
650        ----------
651        file : str, binary stream, or FileHandle
652            File name or writable binary stream, such as an open file
653            or BytesIO.
654        bigtiff : bool
655            If True, the BigTIFF format is used.
656        byteorder : {'<', '>', '=', '|'}
657            The endianness of the data in the file.
658            By default, this is the system's native byte order.
659        append : bool
660            If True and 'file' is an existing standard TIFF file, image data
661            and tags are appended to the file.
662            Appending data may corrupt specifically formatted TIFF files
663            such as LSM, STK, ImageJ, NIH, or FluoView.
664        imagej : bool
665            If True, write an ImageJ hyperstack compatible file.
666            This format can handle data types uint8, uint16, or float32 and
667            data shapes up to 6 dimensions in TZCYXS order.
668            RGB images (S=3 or S=4) must be uint8.
669            ImageJ's default byte order is big-endian but this implementation
670            uses the system's native byte order by default.
671            ImageJ does not support BigTIFF format or LZMA compression.
672            The ImageJ file format is undocumented.
673
674        """
675        if append:
676            # determine if file is an existing TIFF file that can be extended
677            try:
678                with FileHandle(file, mode='rb', size=0) as fh:
679                    pos = fh.tell()
680                    try:
681                        with TiffFile(fh) as tif:
682                            if (append != 'force' and
683                                    any(getattr(tif, 'is_'+a) for a in (
684                                        'lsm', 'stk', 'imagej', 'nih',
685                                        'fluoview', 'micromanager'))):
686                                raise ValueError('file contains metadata')
687                            byteorder = tif.byteorder
688                            bigtiff = tif.is_bigtiff
689                            self._ifdoffset = tif.pages.next_page_offset
690                    except Exception as e:
691                        raise ValueError('cannot append to file: %s' % str(e))
692                    finally:
693                        fh.seek(pos)
694            except (IOError, FileNotFoundError):
695                append = False
696
697        if byteorder in (None, '=', '|'):
698            byteorder = '<' if sys.byteorder == 'little' else '>'
699        elif byteorder not in ('<', '>'):
700            raise ValueError('invalid byteorder %s' % byteorder)
701        if imagej and bigtiff:
702            warnings.warn('writing incompatible BigTIFF ImageJ')
703
704        self._byteorder = byteorder
705        self._imagej = bool(imagej)
706        self._truncate = False
707        self._metadata = None
708        self._colormap = None
709
710        self._descriptionoffset = 0
711        self._descriptionlen = 0
712        self._descriptionlenoffset = 0
713        self._tags = None
714        self._shape = None  # normalized shape of data in consecutive pages
715        self._datashape = None  # shape of data in consecutive pages
716        self._datadtype = None  # data type
717        self._dataoffset = None  # offset to data
718        self._databytecounts = None  # byte counts per plane
719        self._tagoffsets = None  # strip or tile offset tag code
720
721        if bigtiff:
722            self._bigtiff = True
723            self._offsetsize = 8
724            self._tagsize = 20
725            self._tagnoformat = 'Q'
726            self._offsetformat = 'Q'
727            self._valueformat = '8s'
728        else:
729            self._bigtiff = False
730            self._offsetsize = 4
731            self._tagsize = 12
732            self._tagnoformat = 'H'
733            self._offsetformat = 'I'
734            self._valueformat = '4s'
735
736        if append:
737            self._fh = FileHandle(file, mode='r+b', size=0)
738            self._fh.seek(0, 2)
739        else:
740            self._fh = FileHandle(file, mode='wb', size=0)
741            self._fh.write({'<': b'II', '>': b'MM'}[byteorder])
742            if bigtiff:
743                self._fh.write(struct.pack(byteorder+'HHH', 43, 8, 0))
744            else:
745                self._fh.write(struct.pack(byteorder+'H', 42))
746            # first IFD
747            self._ifdoffset = self._fh.tell()
748            self._fh.write(struct.pack(byteorder+self._offsetformat, 0))
749
750    def save(self, data=None, shape=None, dtype=None, returnoffset=False,
751             photometric=None, planarconfig=None, tile=None, contiguous=True,
752             align=16, truncate=False, compress=0, rowsperstrip=None,
753             predictor=False, colormap=None, description=None,
754             datetime=None, resolution=None, software='tifffile.py',
755             metadata={}, ijmetadata=None, extratags=()):
756        """Write numpy array and tags to TIFF file.
757
758        The data shape's last dimensions are assumed to be image depth,
759        height (length), width, and samples.
760        If a colormap is provided, the data's dtype must be uint8 or uint16
761        and the data values are indices into the last dimension of the
762        colormap.
763        If 'shape' and 'dtype' are specified, an empty array is saved.
764        This option cannot be used with compression or multiple tiles.
765        Image data are written uncompressed in one strip per plane by default.
766        Dimensions larger than 2 to 4 (depending on photometric mode, planar
767        configuration, and SGI mode) are flattened and saved as separate pages.
768        The SampleFormat and BitsPerSample tags are derived from the data type.
769
770        Parameters
771        ----------
772        data : numpy.ndarray or None
773            Input image array.
774        shape : tuple or None
775            Shape of the empty array to save. Used only if 'data' is None.
776        dtype : numpy.dtype or None
777            Data-type of the empty array to save. Used only if 'data' is None.
778        returnoffset : bool
779            If True and the image data in the file is memory-mappable, return
780            the offset and number of bytes of the image data in the file.
781        photometric : {'MINISBLACK', 'MINISWHITE', 'RGB', 'PALETTE', 'CFA'}
782            The color space of the image data.
783            By default, this setting is inferred from the data shape and the
784            value of colormap.
785            For CFA images, DNG tags must be specified in 'extratags'.
786        planarconfig : {'CONTIG', 'SEPARATE'}
787            Specifies if samples are stored contiguous or in separate planes.
788            By default, this setting is inferred from the data shape.
789            If this parameter is set, extra samples are used to store grayscale
790            images.
791            'CONTIG': last dimension contains samples.
792            'SEPARATE': third last dimension contains samples.
793        tile : tuple of int
794            The shape (depth, length, width) of image tiles to write.
795            If None (default), image data are written in strips.
796            The tile length and width must be a multiple of 16.
797            If the tile depth is provided, the SGI ImageDepth and TileDepth
798            tags are used to save volume data.
799            Unless a single tile is used, tiles cannot be used to write
800            contiguous files.
801            Few software can read the SGI format, e.g. MeVisLab.
802        contiguous : bool
803            If True (default) and the data and parameters are compatible with
804            previous ones, if any, the image data are stored contiguously after
805            the previous one. Parameters 'photometric' and 'planarconfig'
806            are ignored. Parameters 'description', datetime', and 'extratags'
807            are written to the first page of a contiguous series only.
808        align : int
809            Byte boundary on which to align the image data in the file.
810            Default 16. Use mmap.ALLOCATIONGRANULARITY for memory-mapped data.
811            Following contiguous writes are not aligned.
812        truncate : bool
813            If True, only write the first page including shape metadata if
814            possible (uncompressed, contiguous, not tiled).
815            Other TIFF readers will only be able to read part of the data.
816        compress : int or 'LZMA', 'ZSTD'
817            Values from 0 to 9 controlling the level of zlib compression.
818            If 0 (default), data are written uncompressed.
819            Compression cannot be used to write contiguous files.
820            If 'LZMA' or 'ZSTD', LZMA or ZSTD compression is used, which is
821            not available on all platforms.
822        rowsperstrip : int
823            The number of rows per strip used for compression.
824            Uncompressed data are written in one strip per plane.
825        predictor : bool
826            If True, apply horizontal differencing to integer type images
827            before compression.
828        colormap : numpy.ndarray
829            RGB color values for the corresponding data value.
830            Must be of shape (3, 2**(data.itemsize*8)) and dtype uint16.
831        description : str
832            The subject of the image. Must be 7-bit ASCII. Cannot be used with
833            the ImageJ format. Saved with the first page only.
834        datetime : datetime
835            Date and time of image creation in '%Y:%m:%d %H:%M:%S' format.
836            If None (default), the current date and time is used.
837            Saved with the first page only.
838        resolution : (float, float[, str]) or ((int, int), (int, int)[, str])
839            X and Y resolutions in pixels per resolution unit as float or
840            rational numbers. A third, optional parameter specifies the
841            resolution unit, which must be None (default for ImageJ),
842            'INCH' (default), or 'CENTIMETER'.
843        software : str
844            Name of the software used to create the file. Must be 7-bit ASCII.
845            Saved with the first page only.
846        metadata : dict
847            Additional meta data to be saved along with shape information
848            in JSON or ImageJ formats in an ImageDescription tag.
849            If None, do not write a second ImageDescription tag.
850            Strings must be 7-bit ASCII. Saved with the first page only.
851        ijmetadata : dict
852            Additional meta data to be saved in application specific
853            IJMetadata and IJMetadataByteCounts tags. Refer to the
854            imagej_metadata_tags function for valid keys and values.
855            Saved with the first page only.
856        extratags : sequence of tuples
857            Additional tags as [(code, dtype, count, value, writeonce)].
858
859            code : int
860                The TIFF tag Id.
861            dtype : str
862                Data type of items in 'value' in Python struct format.
863                One of B, s, H, I, 2I, b, h, i, 2i, f, d, Q, or q.
864            count : int
865                Number of data values. Not used for string or byte string
866                values.
867            value : sequence
868                'Count' values compatible with 'dtype'.
869                Byte strings must contain count values of dtype packed as
870                binary data.
871            writeonce : bool
872                If True, the tag is written to the first page only.
873
874        """
875        # TODO: refactor this function
876        fh = self._fh
877        byteorder = self._byteorder
878
879        if data is None:
880            if compress:
881                raise ValueError('cannot save compressed empty file')
882            datashape = shape
883            datadtype = numpy.dtype(dtype).newbyteorder(byteorder)
884            datadtypechar = datadtype.char
885        else:
886            data = numpy.asarray(data, byteorder+data.dtype.char, 'C')
887            if data.size == 0:
888                raise ValueError('cannot save empty array')
889            datashape = data.shape
890            datadtype = data.dtype
891            datadtypechar = data.dtype.char
892
893        returnoffset = returnoffset and datadtype.isnative
894        bilevel = datadtypechar == '?'
895        if bilevel:
896            index = -1 if datashape[-1] > 1 else -2
897            datasize = product(datashape[:index])
898            if datashape[index] % 8:
899                datasize *= datashape[index] // 8 + 1
900            else:
901                datasize *= datashape[index] // 8
902        else:
903            datasize = product(datashape) * datadtype.itemsize
904
905        # just append contiguous data if possible
906        self._truncate = bool(truncate)
907        if self._datashape:
908            if (not contiguous
909                    or self._datashape[1:] != datashape
910                    or self._datadtype != datadtype
911                    or (compress and self._tags)
912                    or tile
913                    or not numpy.array_equal(colormap, self._colormap)):
914                # incompatible shape, dtype, compression mode, or colormap
915                self._write_remaining_pages()
916                self._write_image_description()
917                self._truncate = False
918                self._descriptionoffset = 0
919                self._descriptionlenoffset = 0
920                self._datashape = None
921                self._colormap = None
922                if self._imagej:
923                    raise ValueError(
924                        'ImageJ does not support non-contiguous data')
925            else:
926                # consecutive mode
927                self._datashape = (self._datashape[0] + 1,) + datashape
928                if not compress:
929                    # write contiguous data, write IFDs/tags later
930                    offset = fh.tell()
931                    if data is None:
932                        fh.write_empty(datasize)
933                    else:
934                        fh.write_array(data)
935                    if returnoffset:
936                        return offset, datasize
937                    return
938
939        input_shape = datashape
940        tagnoformat = self._tagnoformat
941        valueformat = self._valueformat
942        offsetformat = self._offsetformat
943        offsetsize = self._offsetsize
944        tagsize = self._tagsize
945
946        MINISBLACK = TIFF.PHOTOMETRIC.MINISBLACK
947        RGB = TIFF.PHOTOMETRIC.RGB
948        CFA = TIFF.PHOTOMETRIC.CFA
949        PALETTE = TIFF.PHOTOMETRIC.PALETTE
950        CONTIG = TIFF.PLANARCONFIG.CONTIG
951        SEPARATE = TIFF.PLANARCONFIG.SEPARATE
952
953        # parse input
954        if photometric is not None:
955            photometric = enumarg(TIFF.PHOTOMETRIC, photometric)
956        if planarconfig:
957            planarconfig = enumarg(TIFF.PLANARCONFIG, planarconfig)
958        if not compress:
959            compress = False
960            compresstag = 1
961            predictor = False
962        else:
963            if isinstance(compress, (tuple, list)):
964                compress, compresslevel = compress
965            elif isinstance(compress, int):
966                compress, compresslevel = 'ADOBE_DEFLATE', int(compress)
967                if not 0 <= compresslevel <= 9:
968                    raise ValueError('invalid compression level %s' % compress)
969            else:
970                compresslevel = None
971            compress = compress.upper()
972            compresstag = enumarg(TIFF.COMPRESSION, compress)
973
974        # prepare ImageJ format
975        if self._imagej:
976            if compress in ('LZMA', 'ZSTD'):
977                raise ValueError(
978                    'ImageJ cannot handle LZMA or ZSTD compression')
979            if description:
980                warnings.warn('not writing description to ImageJ file')
981                description = None
982            volume = False
983            if datadtypechar not in 'BHhf':
984                raise ValueError(
985                    'ImageJ does not support data type %s' % datadtypechar)
986            ijrgb = photometric == RGB if photometric else None
987            if datadtypechar not in 'B':
988                ijrgb = False
989            ijshape = imagej_shape(datashape, ijrgb)
990            if ijshape[-1] in (3, 4):
991                photometric = RGB
992                if datadtypechar not in 'B':
993                    raise ValueError('ImageJ does not support data type %s '
994                                     'for RGB' % datadtypechar)
995            elif photometric is None:
996                photometric = MINISBLACK
997                planarconfig = None
998            if planarconfig == SEPARATE:
999                raise ValueError('ImageJ does not support planar images')
1000            else:
1001                planarconfig = CONTIG if ijrgb else None
1002
1003        # define compress function
1004        if compress:
1005            if compresslevel is None:
1006                compressor, compresslevel = TIFF.COMPESSORS[compresstag]
1007            else:
1008                compressor, _ = TIFF.COMPESSORS[compresstag]
1009                compresslevel = int(compresslevel)
1010            if predictor:
1011                if datadtype.kind not in 'iu':
1012                    raise ValueError(
1013                        'prediction not implemented for %s' % datadtype)
1014
1015                def compress(data, level=compresslevel):
1016                    # horizontal differencing
1017                    diff = numpy.diff(data, axis=-2)
1018                    data = numpy.insert(diff, 0, data[..., 0, :], axis=-2)
1019                    return compressor(data, level)
1020            else:
1021                def compress(data, level=compresslevel):
1022                    return compressor(data, level)
1023
1024        # verify colormap and indices
1025        if colormap is not None:
1026            if datadtypechar not in 'BH':
1027                raise ValueError('invalid data dtype for palette mode')
1028            colormap = numpy.asarray(colormap, dtype=byteorder+'H')
1029            if colormap.shape != (3, 2**(datadtype.itemsize * 8)):
1030                raise ValueError('invalid color map shape')
1031            self._colormap = colormap
1032
1033        # verify tile shape
1034        if tile:
1035            tile = tuple(int(i) for i in tile[:3])
1036            volume = len(tile) == 3
1037            if (len(tile) < 2 or tile[-1] % 16 or tile[-2] % 16 or
1038                    any(i < 1 for i in tile)):
1039                raise ValueError('invalid tile shape')
1040        else:
1041            tile = ()
1042            volume = False
1043
1044        # normalize data shape to 5D or 6D, depending on volume:
1045        #   (pages, planar_samples, [depth,] height, width, contig_samples)
1046        datashape = reshape_nd(datashape, 3 if photometric == RGB else 2)
1047        shape = datashape
1048        ndim = len(datashape)
1049
1050        samplesperpixel = 1
1051        extrasamples = 0
1052        if volume and ndim < 3:
1053            volume = False
1054        if colormap is not None:
1055            photometric = PALETTE
1056            planarconfig = None
1057        if photometric is None:
1058            photometric = MINISBLACK
1059            if bilevel:
1060                photometric = TIFF.PHOTOMETRIC.MINISWHITE
1061            elif planarconfig == CONTIG:
1062                if ndim > 2 and shape[-1] in (3, 4):
1063                    photometric = RGB
1064            elif planarconfig == SEPARATE:
1065                if volume and ndim > 3 and shape[-4] in (3, 4):
1066                    photometric = RGB
1067                elif ndim > 2 and shape[-3] in (3, 4):
1068                    photometric = RGB
1069            elif ndim > 2 and shape[-1] in (3, 4):
1070                photometric = RGB
1071            elif self._imagej:
1072                photometric = MINISBLACK
1073            elif volume and ndim > 3 and shape[-4] in (3, 4):
1074                photometric = RGB
1075            elif ndim > 2 and shape[-3] in (3, 4):
1076                photometric = RGB
1077        if planarconfig and len(shape) <= (3 if volume else 2):
1078            planarconfig = None
1079            photometric = MINISBLACK
1080        if photometric == RGB:
1081            if len(shape) < 3:
1082                raise ValueError('not a RGB(A) image')
1083            if len(shape) < 4:
1084                volume = False
1085            if planarconfig is None:
1086                if shape[-1] in (3, 4):
1087                    planarconfig = CONTIG
1088                elif shape[-4 if volume else -3] in (3, 4):
1089                    planarconfig = SEPARATE
1090                elif shape[-1] > shape[-4 if volume else -3]:
1091                    planarconfig = SEPARATE
1092                else:
1093                    planarconfig = CONTIG
1094            if planarconfig == CONTIG:
1095                datashape = (-1, 1) + shape[(-4 if volume else -3):]
1096                samplesperpixel = datashape[-1]
1097            else:
1098                datashape = (-1,) + shape[(-4 if volume else -3):] + (1,)
1099                samplesperpixel = datashape[1]
1100            if samplesperpixel > 3:
1101                extrasamples = samplesperpixel - 3
1102        elif photometric == CFA:
1103            if len(shape) != 2:
1104                raise ValueError('invalid CFA image')
1105            volume = False
1106            planarconfig = None
1107            datashape = (-1, 1) + shape[-2:] + (1,)
1108            if 50706 not in (et[0] for et in extratags):
1109                raise ValueError('must specify DNG tags for CFA image')
1110        elif planarconfig and len(shape) > (3 if volume else 2):
1111            if planarconfig == CONTIG:
1112                datashape = (-1, 1) + shape[(-4 if volume else -3):]
1113                samplesperpixel = datashape[-1]
1114            else:
1115                datashape = (-1,) + shape[(-4 if volume else -3):] + (1,)
1116                samplesperpixel = datashape[1]
1117            extrasamples = samplesperpixel - 1
1118        else:
1119            planarconfig = None
1120            # remove trailing 1s
1121            while len(shape) > 2 and shape[-1] == 1:
1122                shape = shape[:-1]
1123            if len(shape) < 3:
1124                volume = False
1125            datashape = (-1, 1) + shape[(-3 if volume else -2):] + (1,)
1126
1127        # normalize shape to 6D
1128        assert len(datashape) in (5, 6)
1129        if len(datashape) == 5:
1130            datashape = datashape[:2] + (1,) + datashape[2:]
1131        if datashape[0] == -1:
1132            s0 = product(input_shape) // product(datashape[1:])
1133            datashape = (s0,) + datashape[1:]
1134        shape = datashape
1135        if data is not None:
1136            data = data.reshape(shape)
1137
1138        if tile and not volume:
1139            tile = (1, tile[-2], tile[-1])
1140
1141        if photometric == PALETTE:
1142            if (samplesperpixel != 1 or extrasamples or
1143                    shape[1] != 1 or shape[-1] != 1):
1144                raise ValueError('invalid data shape for palette mode')
1145
1146        if photometric == RGB and samplesperpixel == 2:
1147            raise ValueError('not a RGB image (samplesperpixel=2)')
1148
1149        if bilevel:
1150            if compress:
1151                raise ValueError('cannot save compressed bilevel image')
1152            if tile:
1153                raise ValueError('cannot save tiled bilevel image')
1154            if photometric not in (0, 1):
1155                raise ValueError('cannot save bilevel image as %s' %
1156                                 str(photometric))
1157            datashape = list(datashape)
1158            if datashape[-2] % 8:
1159                datashape[-2] = datashape[-2] // 8 + 1
1160            else:
1161                datashape[-2] = datashape[-2] // 8
1162            datashape = tuple(datashape)
1163            assert datasize == product(datashape)
1164            if data is not None:
1165                data = numpy.packbits(data, axis=-2)
1166                assert datashape[-2] == data.shape[-2]
1167
1168        bytestr = bytes if sys.version[0] == '2' else (
1169            lambda x: bytes(x, 'ascii') if isinstance(x, str) else x)
1170        tags = []  # list of (code, ifdentry, ifdvalue, writeonce)
1171
1172        strip_or_tile = 'Tile' if tile else 'Strip'
1173        tagbytecounts = TIFF.TAG_NAMES[strip_or_tile + 'ByteCounts']
1174        tag_offsets = TIFF.TAG_NAMES[strip_or_tile + 'Offsets']
1175        self._tagoffsets = tag_offsets
1176
1177        def pack(fmt, *val):
1178            return struct.pack(byteorder+fmt, *val)
1179
1180        def addtag(code, dtype, count, value, writeonce=False):
1181            # Compute ifdentry & ifdvalue bytes from code, dtype, count, value
1182            # Append (code, ifdentry, ifdvalue, writeonce) to tags list
1183            code = int(TIFF.TAG_NAMES.get(code, code))
1184            try:
1185                tifftype = TIFF.DATA_DTYPES[dtype]
1186            except KeyError:
1187                raise ValueError('unknown dtype %s' % dtype)
1188            rawcount = count
1189
1190            if dtype == 's':
1191                # strings
1192                value = bytestr(value) + b'\0'
1193                count = rawcount = len(value)
1194                rawcount = value.find(b'\0\0')
1195                if rawcount < 0:
1196                    rawcount = count
1197                else:
1198                    rawcount += 1  # length of string without buffer
1199                value = (value,)
1200            elif isinstance(value, bytes):
1201                # packed binary data
1202                dtsize = struct.calcsize(dtype)
1203                if len(value) % dtsize:
1204                    raise ValueError('invalid packed binary data')
1205                count = len(value) // dtsize
1206            if len(dtype) > 1:
1207                count *= int(dtype[:-1])
1208                dtype = dtype[-1]
1209            ifdentry = [pack('HH', code, tifftype),
1210                        pack(offsetformat, rawcount)]
1211            ifdvalue = None
1212            if struct.calcsize(dtype) * count <= offsetsize:
1213                # value(s) can be written directly
1214                if isinstance(value, bytes):
1215                    ifdentry.append(pack(valueformat, value))
1216                elif count == 1:
1217                    if isinstance(value, (tuple, list, numpy.ndarray)):
1218                        value = value[0]
1219                    ifdentry.append(pack(valueformat, pack(dtype, value)))
1220                else:
1221                    ifdentry.append(pack(valueformat,
1222                                         pack(str(count)+dtype, *value)))
1223            else:
1224                # use offset to value(s)
1225                ifdentry.append(pack(offsetformat, 0))
1226                if isinstance(value, bytes):
1227                    ifdvalue = value
1228                elif isinstance(value, numpy.ndarray):
1229                    assert value.size == count
1230                    assert value.dtype.char == dtype
1231                    ifdvalue = value.tostring()
1232                elif isinstance(value, (tuple, list)):
1233                    ifdvalue = pack(str(count)+dtype, *value)
1234                else:
1235                    ifdvalue = pack(dtype, value)
1236            tags.append((code, b''.join(ifdentry), ifdvalue, writeonce))
1237
1238        def rational(arg, max_denominator=1000000):
1239            """"Return nominator and denominator from float or two integers."""
1240            from fractions import Fraction  # delayed import
1241            try:
1242                f = Fraction.from_float(arg)
1243            except TypeError:
1244                f = Fraction(arg[0], arg[1])
1245            f = f.limit_denominator(max_denominator)
1246            return f.numerator, f.denominator
1247
1248        if description:
1249            # user provided description
1250            addtag('ImageDescription', 's', 0, description, writeonce=True)
1251
1252        # write shape and metadata to ImageDescription
1253        self._metadata = {} if not metadata else metadata.copy()
1254        if self._imagej:
1255            description = imagej_description(
1256                input_shape, shape[-1] in (3, 4), self._colormap is not None,
1257                **self._metadata)
1258        elif metadata or metadata == {}:
1259            if self._truncate:
1260                self._metadata.update(truncated=True)
1261            description = json_description(input_shape, **self._metadata)
1262        else:
1263            description = None
1264        if description:
1265            # add 64 bytes buffer
1266            # the image description might be updated later with the final shape
1267            description = str2bytes(description, 'ascii')
1268            description += b'\0'*64
1269            self._descriptionlen = len(description)
1270            addtag('ImageDescription', 's', 0, description, writeonce=True)
1271
1272        if software:
1273            addtag('Software', 's', 0, software, writeonce=True)
1274        if datetime is None:
1275            datetime = self._now()
1276        addtag('DateTime', 's', 0, datetime.strftime('%Y:%m:%d %H:%M:%S'),
1277               writeonce=True)
1278        addtag('Compression', 'H', 1, compresstag)
1279        if predictor:
1280            addtag('Predictor', 'H', 1, 2)
1281        addtag('ImageWidth', 'I', 1, shape[-2])
1282        addtag('ImageLength', 'I', 1, shape[-3])
1283        if tile:
1284            addtag('TileWidth', 'I', 1, tile[-1])
1285            addtag('TileLength', 'I', 1, tile[-2])
1286            if tile[0] > 1:
1287                addtag('ImageDepth', 'I', 1, shape[-4])
1288                addtag('TileDepth', 'I', 1, tile[0])
1289        addtag('NewSubfileType', 'I', 1, 0)
1290        if not bilevel:
1291            sampleformat = {'u': 1, 'i': 2, 'f': 3, 'c': 6}[datadtype.kind]
1292            addtag('SampleFormat', 'H', samplesperpixel,
1293                   (sampleformat,) * samplesperpixel)
1294        addtag('PhotometricInterpretation', 'H', 1, photometric.value)
1295        if colormap is not None:
1296            addtag('ColorMap', 'H', colormap.size, colormap)
1297        addtag('SamplesPerPixel', 'H', 1, samplesperpixel)
1298        if bilevel:
1299            pass
1300        elif planarconfig and samplesperpixel > 1:
1301            addtag('PlanarConfiguration', 'H', 1, planarconfig.value)
1302            addtag('BitsPerSample', 'H', samplesperpixel,
1303                   (datadtype.itemsize * 8,) * samplesperpixel)
1304        else:
1305            addtag('BitsPerSample', 'H', 1, datadtype.itemsize * 8)
1306        if extrasamples:
1307            if photometric == RGB and extrasamples == 1:
1308                addtag('ExtraSamples', 'H', 1, 1)  # associated alpha channel
1309            else:
1310                addtag('ExtraSamples', 'H', extrasamples, (0,) * extrasamples)
1311        if resolution is not None:
1312            addtag('XResolution', '2I', 1, rational(resolution[0]))
1313            addtag('YResolution', '2I', 1, rational(resolution[1]))
1314            if len(resolution) > 2:
1315                unit = resolution[2]
1316                unit = 1 if unit is None else enumarg(TIFF.RESUNIT, unit)
1317            elif self._imagej:
1318                unit = 1
1319            else:
1320                unit = 2
1321            addtag('ResolutionUnit', 'H', 1, unit)
1322        elif not self._imagej:
1323            addtag('XResolution', '2I', 1, (1, 1))
1324            addtag('YResolution', '2I', 1, (1, 1))
1325            addtag('ResolutionUnit', 'H', 1, 1)
1326        if ijmetadata:
1327            for t in imagej_metadata_tags(ijmetadata, byteorder):
1328                addtag(*t)
1329
1330        contiguous = not compress
1331        if tile:
1332            # one chunk per tile per plane
1333            tiles = ((shape[2] + tile[0] - 1) // tile[0],
1334                     (shape[3] + tile[1] - 1) // tile[1],
1335                     (shape[4] + tile[2] - 1) // tile[2])
1336            numtiles = product(tiles) * shape[1]
1337            stripbytecounts = [
1338                product(tile) * shape[-1] * datadtype.itemsize] * numtiles
1339            addtag(tagbytecounts, offsetformat, numtiles, stripbytecounts)
1340            addtag(tag_offsets, offsetformat, numtiles, [0] * numtiles)
1341            contiguous = contiguous and product(tiles) == 1
1342            if not contiguous:
1343                # allocate tile buffer
1344                chunk = numpy.empty(tile + (shape[-1],), dtype=datadtype)
1345        elif contiguous:
1346            # one strip per plane
1347            if bilevel:
1348                stripbytecounts = [product(datashape[2:])] * shape[1]
1349            else:
1350                stripbytecounts = [
1351                    product(datashape[2:]) * datadtype.itemsize] * shape[1]
1352            addtag(tagbytecounts, offsetformat, shape[1], stripbytecounts)
1353            addtag(tag_offsets, offsetformat, shape[1], [0] * shape[1])
1354            addtag('RowsPerStrip', 'I', 1, shape[-3])
1355        else:
1356            # compress rowsperstrip or ~64 KB chunks
1357            rowsize = product(shape[-2:]) * datadtype.itemsize
1358            if rowsperstrip is None:
1359                rowsperstrip = 65536 // rowsize
1360            if rowsperstrip < 1:
1361                rowsperstrip = 1
1362            elif rowsperstrip > shape[-3]:
1363                rowsperstrip = shape[-3]
1364            addtag('RowsPerStrip', 'I', 1, rowsperstrip)
1365
1366            numstrips = (shape[-3] + rowsperstrip - 1) // rowsperstrip
1367            numstrips *= shape[1]
1368            stripbytecounts = [0] * numstrips
1369            addtag(tagbytecounts, offsetformat, numstrips, [0] * numstrips)
1370            addtag(tag_offsets, offsetformat, numstrips, [0] * numstrips)
1371
1372        if data is None and not contiguous:
1373            raise ValueError('cannot write non-contiguous empty file')
1374
1375        # add extra tags from user
1376        for t in extratags:
1377            addtag(*t)
1378
1379        # TODO: check TIFFReadDirectoryCheckOrder warning in files containing
1380        #   multiple tags of same code
1381        # the entries in an IFD must be sorted in ascending order by tag code
1382        tags = sorted(tags, key=lambda x: x[0])
1383
1384        if not (self._bigtiff or self._imagej) and (
1385                fh.tell() + datasize > 2**31-1):
1386            raise ValueError('data too large for standard TIFF file')
1387
1388        # if not compressed or multi-tiled, write the first IFD and then
1389        # all data contiguously; else, write all IFDs and data interleaved
1390        for pageindex in range(1 if contiguous else shape[0]):
1391            # update pointer at ifd_offset
1392            pos = fh.tell()
1393            if pos % 2:
1394                # location of IFD must begin on a word boundary
1395                fh.write(b'\0')
1396                pos += 1
1397            fh.seek(self._ifdoffset)
1398            fh.write(pack(offsetformat, pos))
1399            fh.seek(pos)
1400
1401            # write ifdentries
1402            fh.write(pack(tagnoformat, len(tags)))
1403            tag_offset = fh.tell()
1404            fh.write(b''.join(t[1] for t in tags))
1405            self._ifdoffset = fh.tell()
1406            fh.write(pack(offsetformat, 0))  # offset to next IFD
1407
1408            # write tag values and patch offsets in ifdentries, if necessary
1409            for tagindex, tag in enumerate(tags):
1410                if tag[2]:
1411                    pos = fh.tell()
1412                    if pos % 2:
1413                        # tag value is expected to begin on word boundary
1414                        fh.write(b'\0')
1415                        pos += 1
1416                    fh.seek(tag_offset + tagindex*tagsize + offsetsize + 4)
1417                    fh.write(pack(offsetformat, pos))
1418                    fh.seek(pos)
1419                    if tag[0] == tag_offsets:
1420                        stripoffsetsoffset = pos
1421                    elif tag[0] == tagbytecounts:
1422                        strip_bytecounts_offset = pos
1423                    elif tag[0] == 270 and tag[2].endswith(b'\0\0\0\0'):
1424                        # image description buffer
1425                        self._descriptionoffset = pos
1426                        self._descriptionlenoffset = (
1427                            tag_offset + tagindex * tagsize + 4)
1428                    fh.write(tag[2])
1429
1430            # write image data
1431            data_offset = fh.tell()
1432            skip = align - data_offset % align
1433            fh.seek(skip, 1)
1434            data_offset += skip
1435            if contiguous:
1436                if data is None:
1437                    fh.write_empty(datasize)
1438                else:
1439                    fh.write_array(data)
1440            elif tile:
1441                if data is None:
1442                    fh.write_empty(numtiles * stripbytecounts[0])
1443                else:
1444                    stripindex = 0
1445                    for plane in data[pageindex]:
1446                        for tz in range(tiles[0]):
1447                            for ty in range(tiles[1]):
1448                                for tx in range(tiles[2]):
1449                                    c0 = min(tile[0], shape[2] - tz*tile[0])
1450                                    c1 = min(tile[1], shape[3] - ty*tile[1])
1451                                    c2 = min(tile[2], shape[4] - tx*tile[2])
1452                                    chunk[c0:, c1:, c2:] = 0
1453                                    chunk[:c0, :c1, :c2] = plane[
1454                                        tz*tile[0]:tz*tile[0]+c0,
1455                                        ty*tile[1]:ty*tile[1]+c1,
1456                                        tx*tile[2]:tx*tile[2]+c2]
1457                                    if compress:
1458                                        t = compress(chunk)
1459                                        fh.write(t)
1460                                        stripbytecounts[stripindex] = len(t)
1461                                        stripindex += 1
1462                                    else:
1463                                        fh.write_array(chunk)
1464                                        fh.flush()
1465            elif compress:
1466                # write one strip per rowsperstrip
1467                assert data.shape[2] == 1  # not handling depth
1468                numstrips = (shape[-3] + rowsperstrip - 1) // rowsperstrip
1469                stripindex = 0
1470                for plane in data[pageindex]:
1471                    for i in range(numstrips):
1472                        strip = plane[0, i*rowsperstrip: (i+1)*rowsperstrip]
1473                        strip = compress(strip)
1474                        fh.write(strip)
1475                        stripbytecounts[stripindex] = len(strip)
1476                        stripindex += 1
1477
1478            # update strip/tile offsets and bytecounts if necessary
1479            pos = fh.tell()
1480            for tagindex, tag in enumerate(tags):
1481                if tag[0] == tag_offsets:  # strip/tile offsets
1482                    if tag[2]:
1483                        fh.seek(stripoffsetsoffset)
1484                        strip_offset = data_offset
1485                        for size in stripbytecounts:
1486                            fh.write(pack(offsetformat, strip_offset))
1487                            strip_offset += size
1488                    else:
1489                        fh.seek(tag_offset + tagindex*tagsize + offsetsize + 4)
1490                        fh.write(pack(offsetformat, data_offset))
1491                elif tag[0] == tagbytecounts:  # strip/tile bytecounts
1492                    if compress:
1493                        if tag[2]:
1494                            fh.seek(strip_bytecounts_offset)
1495                            for size in stripbytecounts:
1496                                fh.write(pack(offsetformat, size))
1497                        else:
1498                            fh.seek(tag_offset + tagindex*tagsize +
1499                                    offsetsize + 4)
1500                            fh.write(pack(offsetformat, stripbytecounts[0]))
1501                    break
1502            fh.seek(pos)
1503            fh.flush()
1504
1505            # remove tags that should be written only once
1506            if pageindex == 0:
1507                tags = [tag for tag in tags if not tag[-1]]
1508
1509        self._shape = shape
1510        self._datashape = (1,) + input_shape
1511        self._datadtype = datadtype
1512        self._dataoffset = data_offset
1513        self._databytecounts = stripbytecounts
1514
1515        if contiguous:
1516            # write remaining IFDs/tags later
1517            self._tags = tags
1518            # return offset and size of image data
1519            if returnoffset:
1520                return data_offset, sum(stripbytecounts)
1521
1522    def _write_remaining_pages(self):
1523        """Write outstanding IFDs and tags to file."""
1524        if not self._tags or self._truncate:
1525            return
1526
1527        fh = self._fh
1528        fhpos = fh.tell()
1529        if fhpos % 2:
1530            fh.write(b'\0')
1531            fhpos += 1
1532        byteorder = self._byteorder
1533        offsetformat = self._offsetformat
1534        offsetsize = self._offsetsize
1535        tagnoformat = self._tagnoformat
1536        tagsize = self._tagsize
1537        dataoffset = self._dataoffset
1538        pagedatasize = sum(self._databytecounts)
1539        pageno = self._shape[0] * self._datashape[0] - 1
1540
1541        def pack(fmt, *val):
1542            return struct.pack(byteorder+fmt, *val)
1543
1544        # construct template IFD in memory
1545        # need to patch offsets to next IFD and data before writing to disk
1546        ifd = io.BytesIO()
1547        ifd.write(pack(tagnoformat, len(self._tags)))
1548        tagoffset = ifd.tell()
1549        ifd.write(b''.join(t[1] for t in self._tags))
1550        ifdoffset = ifd.tell()
1551        ifd.write(pack(offsetformat, 0))  # offset to next IFD
1552        # tag values
1553        for tagindex, tag in enumerate(self._tags):
1554            offset2value = tagoffset + tagindex*tagsize + offsetsize + 4
1555            if tag[2]:
1556                pos = ifd.tell()
1557                if pos % 2:  # tag value is expected to begin on word boundary
1558                    ifd.write(b'\0')
1559                    pos += 1
1560                ifd.seek(offset2value)
1561                try:
1562                    ifd.write(pack(offsetformat, pos + fhpos))
1563                except Exception:  # struct.error
1564                    if self._imagej:
1565                        warnings.warn('truncating ImageJ file')
1566                        self._truncate = True
1567                        return
1568                    raise ValueError('data too large for non-BigTIFF file')
1569                ifd.seek(pos)
1570                ifd.write(tag[2])
1571                if tag[0] == self._tagoffsets:
1572                    # save strip/tile offsets for later updates
1573                    stripoffset2offset = offset2value
1574                    stripoffset2value = pos
1575            elif tag[0] == self._tagoffsets:
1576                # save strip/tile offsets for later updates
1577                stripoffset2offset = None
1578                stripoffset2value = offset2value
1579        # size to word boundary
1580        if ifd.tell() % 2:
1581            ifd.write(b'\0')
1582
1583        # check if all IFDs fit in file
1584        pos = fh.tell()
1585        if not self._bigtiff and pos + ifd.tell() * pageno > 2**32 - 256:
1586            if self._imagej:
1587                warnings.warn('truncating ImageJ file')
1588                self._truncate = True
1589                return
1590            raise ValueError('data too large for non-BigTIFF file')
1591
1592        # TODO: assemble IFD chain in memory
1593        for _ in range(pageno):
1594            # update pointer at IFD offset
1595            pos = fh.tell()
1596            fh.seek(self._ifdoffset)
1597            fh.write(pack(offsetformat, pos))
1598            fh.seek(pos)
1599            self._ifdoffset = pos + ifdoffset
1600            # update strip/tile offsets in IFD
1601            dataoffset += pagedatasize  # offset to image data
1602            if stripoffset2offset is None:
1603                ifd.seek(stripoffset2value)
1604                ifd.write(pack(offsetformat, dataoffset))
1605            else:
1606                ifd.seek(stripoffset2offset)
1607                ifd.write(pack(offsetformat, pos + stripoffset2value))
1608                ifd.seek(stripoffset2value)
1609                stripoffset = dataoffset
1610                for size in self._databytecounts:
1611                    ifd.write(pack(offsetformat, stripoffset))
1612                    stripoffset += size
1613            # write IFD entry
1614            fh.write(ifd.getvalue())
1615
1616        self._tags = None
1617        self._datadtype = None
1618        self._dataoffset = None
1619        self._databytecounts = None
1620        # do not reset _shape or _data_shape
1621
1622    def _write_image_description(self):
1623        """Write meta data to ImageDescription tag."""
1624        if (not self._datashape or self._datashape[0] == 1 or
1625                self._descriptionoffset <= 0):
1626            return
1627
1628        colormapped = self._colormap is not None
1629        if self._imagej:
1630            isrgb = self._shape[-1] in (3, 4)
1631            description = imagej_description(
1632                self._datashape, isrgb, colormapped, **self._metadata)
1633        else:
1634            description = json_description(self._datashape, **self._metadata)
1635
1636        # rewrite description and its length to file
1637        description = description.encode('utf-8')
1638        description = description[:self._descriptionlen-1]
1639        pos = self._fh.tell()
1640        self._fh.seek(self._descriptionoffset)
1641        self._fh.write(description)
1642        self._fh.seek(self._descriptionlenoffset)
1643        self._fh.write(struct.pack(self._byteorder+self._offsetformat,
1644                                   len(description)+1))
1645        self._fh.seek(pos)
1646
1647        self._descriptionoffset = 0
1648        self._descriptionlenoffset = 0
1649        self._descriptionlen = 0
1650
1651    def _now(self):
1652        """Return current date and time."""
1653        return datetime.datetime.now()
1654
1655    def close(self):
1656        """Write remaining pages and close file handle."""
1657        if not self._truncate:
1658            self._write_remaining_pages()
1659        self._write_image_description()
1660        self._fh.close()
1661
1662    def __enter__(self):
1663        return self
1664
1665    def __exit__(self, exc_type, exc_value, traceback):
1666        self.close()
1667
1668
1669class TiffFile(object):
1670    """Read image and metadata from TIFF file.
1671
1672    TiffFile instances must be closed using the 'close' method, which is
1673    automatically called when using the 'with' context manager.
1674
1675    Attributes
1676    ----------
1677    pages : TiffPages
1678        Sequence of TIFF pages in file.
1679    series : list of TiffPageSeries
1680        Sequences of closely related TIFF pages. These are computed
1681        from OME, LSM, ImageJ, etc. metadata or based on similarity
1682        of page properties such as shape, dtype, and compression.
1683    byteorder : '>', '<'
1684        The endianness of data in the file.
1685        '>': big-endian (Motorola).
1686        '>': little-endian (Intel).
1687    is_flag : bool
1688        If True, file is of a certain format.
1689        Flags are: bigtiff, movie, shaped, ome, imagej, stk, lsm, fluoview,
1690        nih, vista, 'micromanager, metaseries, mdgel, mediacy, tvips, fei,
1691        sem, scn, svs, scanimage, andor, epics, pilatus, qptiff.
1692
1693    All attributes are read-only.
1694
1695    Examples
1696    --------
1697    >>> # read image array from TIFF file
1698    >>> imsave('temp.tif', numpy.random.rand(5, 301, 219))
1699    >>> with TiffFile('temp.tif') as tif:
1700    ...     data = tif.asarray()
1701    >>> data.shape
1702    (5, 301, 219)
1703
1704    """
1705    def __init__(self, arg, name=None, offset=None, size=None,
1706                 multifile=True, movie=None, **kwargs):
1707        """Initialize instance from file.
1708
1709        Parameters
1710        ----------
1711        arg : str or open file
1712            Name of file or open file object.
1713            The file objects are closed in TiffFile.close().
1714        name : str
1715            Optional name of file in case 'arg' is a file handle.
1716        offset : int
1717            Optional start position of embedded file. By default, this is
1718            the current file position.
1719        size : int
1720            Optional size of embedded file. By default, this is the number
1721            of bytes from the 'offset' to the end of the file.
1722        multifile : bool
1723            If True (default), series may include pages from multiple files.
1724            Currently applies to OME-TIFF only.
1725        movie : bool
1726            If True, assume that later pages differ from first page only by
1727            data offsets and byte counts. Significantly increases speed and
1728            reduces memory usage when reading movies with thousands of pages.
1729            Enabling this for non-movie files will result in data corruption
1730            or crashes. Python 3 only.
1731        kwargs : bool
1732            'is_ome': If False, disable processing of OME-XML metadata.
1733
1734        """
1735        if 'fastij' in kwargs:
1736            del kwargs['fastij']
1737            raise DeprecationWarning('the fastij option will be removed')
1738        for key, value in kwargs.items():
1739            if key[:3] == 'is_' and key[3:] in TIFF.FILE_FLAGS:
1740                if value is not None and not value:
1741                    setattr(self, key, bool(value))
1742            else:
1743                raise TypeError('unexpected keyword argument: %s' % key)
1744
1745        fh = FileHandle(arg, mode='rb', name=name, offset=offset, size=size)
1746        self._fh = fh
1747        self._multifile = bool(multifile)
1748        self._files = {fh.name: self}  # cache of TiffFiles
1749        try:
1750            fh.seek(0)
1751            try:
1752                byteorder = {b'II': '<', b'MM': '>'}[fh.read(2)]
1753            except KeyError:
1754                raise ValueError('not a TIFF file')
1755            sys_byteorder = {'big': '>', 'little': '<'}[sys.byteorder]
1756            self.isnative = byteorder == sys_byteorder
1757
1758            version = struct.unpack(byteorder+'H', fh.read(2))[0]
1759            if version == 43:
1760                # BigTiff
1761                self.is_bigtiff = True
1762                offsetsize, zero = struct.unpack(byteorder+'HH', fh.read(4))
1763                if zero or offsetsize != 8:
1764                    raise ValueError('invalid BigTIFF file')
1765                self.byteorder = byteorder
1766                self.offsetsize = 8
1767                self.offsetformat = byteorder+'Q'
1768                self.tagnosize = 8
1769                self.tagnoformat = byteorder+'Q'
1770                self.tagsize = 20
1771                self.tagformat1 = byteorder+'HH'
1772                self.tagformat2 = byteorder+'Q8s'
1773            elif version == 42:
1774                self.is_bigtiff = False
1775                self.byteorder = byteorder
1776                self.offsetsize = 4
1777                self.offsetformat = byteorder+'I'
1778                self.tagnosize = 2
1779                self.tagnoformat = byteorder+'H'
1780                self.tagsize = 12
1781                self.tagformat1 = byteorder+'HH'
1782                self.tagformat2 = byteorder+'I4s'
1783            else:
1784                raise ValueError('invalid TIFF file')
1785
1786            # file handle is at offset to offset to first page
1787            self.pages = TiffPages(self)
1788
1789            if self.is_lsm and (self.filehandle.size >= 2**32 or
1790                                self.pages[0].compression != 1 or
1791                                self.pages[1].compression != 1):
1792                self._lsm_load_pages()
1793                self._lsm_fix_strip_offsets()
1794                self._lsm_fix_strip_bytecounts()
1795            elif movie:
1796                self.pages.useframes = True
1797
1798        except Exception:
1799            fh.close()
1800            raise
1801
1802    @property
1803    def filehandle(self):
1804        """Return file handle."""
1805        return self._fh
1806
1807    @property
1808    def filename(self):
1809        """Return name of file handle."""
1810        return self._fh.name
1811
1812    @lazyattr
1813    def fstat(self):
1814        """Return status of file handle as stat_result object."""
1815        try:
1816            return os.fstat(self._fh.fileno())
1817        except Exception:  # io.UnsupportedOperation
1818            return None
1819
1820    def close(self):
1821        """Close open file handle(s)."""
1822        for tif in self._files.values():
1823            tif.filehandle.close()
1824        self._files = {}
1825
1826    def asarray(self, key=None, series=None, out=None, validate=True,
1827                maxworkers=1):
1828        """Return image data from multiple TIFF pages as numpy array.
1829
1830        By default, the data from the first series is returned.
1831
1832        Parameters
1833        ----------
1834        key : int, slice, or sequence of page indices
1835            Defines which pages to return as array.
1836        series : int or TiffPageSeries
1837            Defines which series of pages to return as array.
1838        out : numpy.ndarray, str, or file-like object; optional
1839            Buffer where image data will be saved.
1840            If None (default), a new array will be created.
1841            If numpy.ndarray, a writable array of compatible dtype and shape.
1842            If 'memmap', directly memory-map the image data in the TIFF file
1843            if possible; else create a memory-mapped array in a temporary file.
1844            If str or open file, the file name or file object used to
1845            create a memory-map to an array stored in a binary file on disk.
1846        validate : bool
1847            If True (default), validate various tags.
1848            Passed to TiffPage.asarray().
1849        maxworkers : int
1850            Maximum number of threads to concurrently get data from pages.
1851            Default is 1. If None, up to half the CPU cores are used.
1852            Reading data from file is limited to a single thread.
1853            Using multiple threads can significantly speed up this function
1854            if the bottleneck is decoding compressed data, e.g. in case of
1855            large LZW compressed LSM files.
1856            If the bottleneck is I/O or pure Python code, using multiple
1857            threads might be detrimental.
1858
1859        """
1860        if not self.pages:
1861            return numpy.array([])
1862        if key is None and series is None:
1863            series = 0
1864        if series is not None:
1865            try:
1866                series = self.series[series]
1867            except (KeyError, TypeError):
1868                pass
1869            pages = series._pages
1870        else:
1871            pages = self.pages
1872
1873        if key is None:
1874            pass
1875        elif isinstance(key, inttypes):
1876            pages = [pages[key]]
1877        elif isinstance(key, slice):
1878            pages = pages[key]
1879        elif isinstance(key, collections.Iterable):
1880            pages = [pages[k] for k in key]
1881        else:
1882            raise TypeError('key must be an int, slice, or sequence')
1883
1884        if not pages:
1885            raise ValueError('no pages selected')
1886
1887        if self.is_nih:
1888            result = stack_pages(pages, out=out, maxworkers=maxworkers,
1889                                 squeeze=False)
1890        elif key is None and series and series.offset:
1891            typecode = self.byteorder + series.dtype.char
1892            if out == 'memmap' and pages[0].is_memmappable:
1893                result = self.filehandle.memmap_array(
1894                    typecode, series.shape, series.offset)
1895            else:
1896                if out is not None:
1897                    out = create_output(out, series.shape, series.dtype)
1898                self.filehandle.seek(series.offset)
1899                result = self.filehandle.read_array(
1900                    typecode, product(series.shape), out=out, native=True)
1901        elif len(pages) == 1:
1902            result = pages[0].asarray(out=out, validate=validate)
1903        else:
1904            result = stack_pages(pages, out=out, maxworkers=maxworkers)
1905
1906        if result is None:
1907            return
1908
1909        if key is None:
1910            try:
1911                result.shape = series.shape
1912            except ValueError:
1913                try:
1914                    warnings.warn('failed to reshape %s to %s' % (
1915                        result.shape, series.shape))
1916                    # try series of expected shapes
1917                    result.shape = (-1,) + series.shape
1918                except ValueError:
1919                    # revert to generic shape
1920                    result.shape = (-1,) + pages[0].shape
1921        elif len(pages) == 1:
1922            result.shape = pages[0].shape
1923        else:
1924            result.shape = (-1,) + pages[0].shape
1925        return result
1926
1927    @lazyattr
1928    def series(self):
1929        """Return related pages as TiffPageSeries.
1930
1931        Side effect: after calling this function, TiffFile.pages might contain
1932        TiffPage and TiffFrame instances.
1933
1934        """
1935        if not self.pages:
1936            return []
1937
1938        useframes = self.pages.useframes
1939        keyframe = self.pages.keyframe
1940        series = []
1941        for name in 'ome imagej lsm fluoview nih mdgel shaped'.split():
1942            if getattr(self, 'is_' + name, False):
1943                series = getattr(self, '_%s_series' % name)()
1944                break
1945        self.pages.useframes = useframes
1946        self.pages.keyframe = keyframe
1947        if not series:
1948            series = self._generic_series()
1949
1950        # remove empty series, e.g. in MD Gel files
1951        series = [s for s in series if sum(s.shape) > 0]
1952
1953        for i, s in enumerate(series):
1954            s.index = i
1955        return series
1956
1957    def _generic_series(self):
1958        """Return image series in file."""
1959        if self.pages.useframes:
1960            # movie mode
1961            page = self.pages[0]
1962            shape = page.shape
1963            axes = page.axes
1964            if len(self.pages) > 1:
1965                shape = (len(self.pages),) + shape
1966                axes = 'I' + axes
1967            return [TiffPageSeries(self.pages[:], shape, page.dtype, axes,
1968                                   stype='movie')]
1969
1970        self.pages.clear(False)
1971        self.pages.load()
1972        result = []
1973        keys = []
1974        series = {}
1975        compressions = TIFF.DECOMPESSORS
1976        for page in self.pages:
1977            if not page.shape:
1978                continue
1979            key = page.shape + (page.axes, page.compression in compressions)
1980            if key in series:
1981                series[key].append(page)
1982            else:
1983                keys.append(key)
1984                series[key] = [page]
1985        for key in keys:
1986            pages = series[key]
1987            page = pages[0]
1988            shape = page.shape
1989            axes = page.axes
1990            if len(pages) > 1:
1991                shape = (len(pages),) + shape
1992                axes = 'I' + axes
1993            result.append(TiffPageSeries(pages, shape, page.dtype, axes,
1994                                         stype='Generic'))
1995
1996        return result
1997
1998    def _shaped_series(self):
1999        """Return image series in "shaped" file."""
2000        pages = self.pages
2001        pages.useframes = True
2002        lenpages = len(pages)
2003
2004        def append_series(series, pages, axes, shape, reshape, name,
2005                          truncated):
2006            page = pages[0]
2007            if not axes:
2008                shape = page.shape
2009                axes = page.axes
2010                if len(pages) > 1:
2011                    shape = (len(pages),) + shape
2012                    axes = 'Q' + axes
2013            size = product(shape)
2014            resize = product(reshape)
2015            if page.is_contiguous and resize > size and resize % size == 0:
2016                if truncated is None:
2017                    truncated = True
2018                axes = 'Q' + axes
2019                shape = (resize // size,) + shape
2020            try:
2021                axes = reshape_axes(axes, shape, reshape)
2022                shape = reshape
2023            except ValueError as e:
2024                warnings.warn(str(e))
2025            series.append(
2026                TiffPageSeries(pages, shape, page.dtype, axes, name=name,
2027                               stype='Shaped', truncated=truncated))
2028
2029        keyframe = axes = shape = reshape = name = None
2030        series = []
2031        index = 0
2032        while True:
2033            if index >= lenpages:
2034                break
2035            # new keyframe; start of new series
2036            pages.keyframe = index
2037            keyframe = pages[index]
2038            if not keyframe.is_shaped:
2039                warnings.warn('invalid shape metadata or corrupted file')
2040                return
2041            # read metadata
2042            axes = None
2043            shape = None
2044            metadata = json_description_metadata(keyframe.is_shaped)
2045            name = metadata.get('name', '')
2046            reshape = metadata['shape']
2047            truncated = metadata.get('truncated', None)
2048            if 'axes' in metadata:
2049                axes = metadata['axes']
2050                if len(axes) == len(reshape):
2051                    shape = reshape
2052                else:
2053                    axes = ''
2054                    warnings.warn('axes do not match shape')
2055            # skip pages if possible
2056            spages = [keyframe]
2057            size = product(reshape)
2058            npages, mod = divmod(size, product(keyframe.shape))
2059            if mod:
2060                warnings.warn('series shape does not match page shape')
2061                return
2062            if 1 < npages <= lenpages - index:
2063                size *= keyframe._dtype.itemsize
2064                if truncated:
2065                    npages = 1
2066                elif (keyframe.is_final and
2067                      keyframe.offset + size < pages[index+1].offset):
2068                    truncated = False
2069                else:
2070                    # need to read all pages for series
2071                    truncated = False
2072                    for j in range(index+1, index+npages):
2073                        page = pages[j]
2074                        page.keyframe = keyframe
2075                        spages.append(page)
2076            append_series(series, spages, axes, shape, reshape, name,
2077                          truncated)
2078            index += npages
2079
2080        return series
2081
2082    def _imagej_series(self):
2083        """Return image series in ImageJ file."""
2084        # ImageJ's dimension order is always TZCYXS
2085        # TODO: fix loading of color, composite, or palette images
2086        self.pages.useframes = True
2087        self.pages.keyframe = 0
2088
2089        ij = self.imagej_metadata
2090        pages = self.pages
2091        page = pages[0]
2092
2093        def is_hyperstack():
2094            # ImageJ hyperstack store all image metadata in the first page and
2095            # image data are stored contiguously before the second page, if any
2096            if not page.is_final:
2097                return False
2098            images = ij.get('images', 0)
2099            if images <= 1:
2100                return False
2101            offset, count = page.is_contiguous
2102            if (count != product(page.shape) * page.bitspersample // 8
2103                    or offset + count*images > self.filehandle.size):
2104                raise ValueError()
2105            # check that next page is stored after data
2106            if len(pages) > 1 and offset + count*images > pages[1].offset:
2107                return False
2108            return True
2109
2110        try:
2111            hyperstack = is_hyperstack()
2112        except ValueError:
2113            warnings.warn('invalid ImageJ metadata or corrupted file')
2114            return
2115        if hyperstack:
2116            # no need to read other pages
2117            pages = [page]
2118        else:
2119            self.pages.load()
2120
2121        shape = []
2122        axes = []
2123        if 'frames' in ij:
2124            shape.append(ij['frames'])
2125            axes.append('T')
2126        if 'slices' in ij:
2127            shape.append(ij['slices'])
2128            axes.append('Z')
2129        if 'channels' in ij and not (page.photometric == 2 and not
2130                                     ij.get('hyperstack', False)):
2131            shape.append(ij['channels'])
2132            axes.append('C')
2133        remain = ij.get('images', len(pages))//(product(shape) if shape else 1)
2134        if remain > 1:
2135            shape.append(remain)
2136            axes.append('I')
2137        if page.axes[0] == 'I':
2138            # contiguous multiple images
2139            shape.extend(page.shape[1:])
2140            axes.extend(page.axes[1:])
2141        elif page.axes[:2] == 'SI':
2142            # color-mapped contiguous multiple images
2143            shape = page.shape[0:1] + tuple(shape) + page.shape[2:]
2144            axes = list(page.axes[0]) + axes + list(page.axes[2:])
2145        else:
2146            shape.extend(page.shape)
2147            axes.extend(page.axes)
2148
2149        truncated = (
2150            hyperstack and len(self.pages) == 1 and
2151            page.is_contiguous[1] != product(shape) * page.bitspersample // 8)
2152
2153        return [TiffPageSeries(pages, shape, page.dtype, axes, stype='ImageJ',
2154                               truncated=truncated)]
2155
2156    def _fluoview_series(self):
2157        """Return image series in FluoView file."""
2158        self.pages.useframes = True
2159        self.pages.keyframe = 0
2160        self.pages.load()
2161        mm = self.fluoview_metadata
2162        mmhd = list(reversed(mm['Dimensions']))
2163        axes = ''.join(TIFF.MM_DIMENSIONS.get(i[0].upper(), 'Q')
2164                       for i in mmhd if i[1] > 1)
2165        shape = tuple(int(i[1]) for i in mmhd if i[1] > 1)
2166        return [TiffPageSeries(self.pages, shape, self.pages[0].dtype, axes,
2167                               name=mm['ImageName'], stype='FluoView')]
2168
2169    def _mdgel_series(self):
2170        """Return image series in MD Gel file."""
2171        # only a single page, scaled according to metadata in second page
2172        self.pages.useframes = False
2173        self.pages.keyframe = 0
2174        self.pages.load()
2175        md = self.mdgel_metadata
2176        if md['FileTag'] in (2, 128):
2177            dtype = numpy.dtype('float32')
2178            scale = md['ScalePixel']
2179            scale = scale[0] / scale[1]  # rational
2180            if md['FileTag'] == 2:
2181                # squary root data format
2182                def transform(a):
2183                    return a.astype('float32')**2 * scale
2184            else:
2185                def transform(a):
2186                    return a.astype('float32') * scale
2187        else:
2188            transform = None
2189        page = self.pages[0]
2190        return [TiffPageSeries([page], page.shape, dtype, page.axes,
2191                               transform=transform, stype='MDGel')]
2192
2193    def _nih_series(self):
2194        """Return image series in NIH file."""
2195        self.pages.useframes = True
2196        self.pages.keyframe = 0
2197        self.pages.load()
2198        page0 = self.pages[0]
2199        if len(self.pages) == 1:
2200            shape = page0.shape
2201            axes = page0.axes
2202        else:
2203            shape = (len(self.pages),) + page0.shape
2204            axes = 'I' + page0.axes
2205        return [
2206            TiffPageSeries(self.pages, shape, page0.dtype, axes, stype='NIH')]
2207
2208    def _ome_series(self):
2209        """Return image series in OME-TIFF file(s)."""
2210        from xml.etree import cElementTree as etree  # delayed import
2211        omexml = self.pages[0].description
2212        try:
2213            root = etree.fromstring(omexml)
2214        except etree.ParseError as e:
2215            # TODO: test badly encoded OME-XML
2216            warnings.warn('ome-xml: %s' % e)
2217            try:
2218                # might work on Python 2
2219                omexml = omexml.decode('utf-8', 'ignore').encode('utf-8')
2220                root = etree.fromstring(omexml)
2221            except Exception:
2222                return
2223
2224        self.pages.useframes = True
2225        self.pages.keyframe = 0
2226        self.pages.load()
2227
2228        uuid = root.attrib.get('UUID', None)
2229        self._files = {uuid: self}
2230        dirname = self._fh.dirname
2231        modulo = {}
2232        series = []
2233        for element in root:
2234            if element.tag.endswith('BinaryOnly'):
2235                # TODO: load OME-XML from master or companion file
2236                warnings.warn('ome-xml: not an ome-tiff master file')
2237                break
2238            if element.tag.endswith('StructuredAnnotations'):
2239                for annot in element:
2240                    if not annot.attrib.get('Namespace',
2241                                            '').endswith('modulo'):
2242                        continue
2243                    for value in annot:
2244                        for modul in value:
2245                            for along in modul:
2246                                if not along.tag[:-1].endswith('Along'):
2247                                    continue
2248                                axis = along.tag[-1]
2249                                newaxis = along.attrib.get('Type', 'other')
2250                                newaxis = TIFF.AXES_LABELS[newaxis]
2251                                if 'Start' in along.attrib:
2252                                    step = float(along.attrib.get('Step', 1))
2253                                    start = float(along.attrib['Start'])
2254                                    stop = float(along.attrib['End']) + step
2255                                    labels = numpy.arange(start, stop, step)
2256                                else:
2257                                    labels = [label.text for label in along
2258                                              if label.tag.endswith('Label')]
2259                                modulo[axis] = (newaxis, labels)
2260
2261            if not element.tag.endswith('Image'):
2262                continue
2263
2264            attr = element.attrib
2265            name = attr.get('Name', None)
2266
2267            for pixels in element:
2268                if not pixels.tag.endswith('Pixels'):
2269                    continue
2270                attr = pixels.attrib
2271                dtype = attr.get('PixelType', None)
2272                axes = ''.join(reversed(attr['DimensionOrder']))
2273                shape = list(int(attr['Size'+ax]) for ax in axes)
2274                size = product(shape[:-2])
2275                ifds = None
2276                spp = 1  # samples per pixel
2277                # FIXME: this implementation assumes the last two
2278                # dimensions are stored in tiff pages (shape[:-2]).
2279                # Apparently that is not always the case.
2280                for data in pixels:
2281                    if data.tag.endswith('Channel'):
2282                        attr = data.attrib
2283                        if ifds is None:
2284                            spp = int(attr.get('SamplesPerPixel', spp))
2285                            ifds = [None] * (size // spp)
2286                        elif int(attr.get('SamplesPerPixel', 1)) != spp:
2287                            raise ValueError(
2288                                "cannot handle differing SamplesPerPixel")
2289                        continue
2290                    if ifds is None:
2291                        ifds = [None] * (size // spp)
2292                    if not data.tag.endswith('TiffData'):
2293                        continue
2294                    attr = data.attrib
2295                    ifd = int(attr.get('IFD', 0))
2296                    num = int(attr.get('NumPlanes', 1 if 'IFD' in attr else 0))
2297                    num = int(attr.get('PlaneCount', num))
2298                    idx = [int(attr.get('First'+ax, 0)) for ax in axes[:-2]]
2299                    try:
2300                        idx = numpy.ravel_multi_index(idx, shape[:-2])
2301                    except ValueError:
2302                        # ImageJ produces invalid ome-xml when cropping
2303                        warnings.warn('ome-xml: invalid TiffData index')
2304                        continue
2305                    for uuid in data:
2306                        if not uuid.tag.endswith('UUID'):
2307                            continue
2308                        if uuid.text not in self._files:
2309                            if not self._multifile:
2310                                # abort reading multifile OME series
2311                                # and fall back to generic series
2312                                return []
2313                            fname = uuid.attrib['FileName']
2314                            try:
2315                                tif = TiffFile(os.path.join(dirname, fname))
2316                                tif.pages.useframes = True
2317                                tif.pages.keyframe = 0
2318                                tif.pages.load()
2319                            except (IOError, FileNotFoundError, ValueError):
2320                                warnings.warn(
2321                                    "ome-xml: failed to read '%s'" % fname)
2322                                break
2323                            self._files[uuid.text] = tif
2324                            tif.close()
2325                        pages = self._files[uuid.text].pages
2326                        try:
2327                            for i in range(num if num else len(pages)):
2328                                ifds[idx + i] = pages[ifd + i]
2329                        except IndexError:
2330                            warnings.warn('ome-xml: index out of range')
2331                        # only process first UUID
2332                        break
2333                    else:
2334                        pages = self.pages
2335                        try:
2336                            for i in range(num if num else len(pages)):
2337                                ifds[idx + i] = pages[ifd + i]
2338                        except IndexError:
2339                            warnings.warn('ome-xml: index out of range')
2340
2341                if all(i is None for i in ifds):
2342                    # skip images without data
2343                    continue
2344
2345                # set a keyframe on all IFDs
2346                keyframe = None
2347                for i in ifds:
2348                    # try find a TiffPage
2349                    if i and i == i.keyframe:
2350                        keyframe = i
2351                        break
2352                if not keyframe:
2353                    # reload a TiffPage from file
2354                    for i, keyframe in enumerate(ifds):
2355                        if keyframe:
2356                            keyframe.parent.pages.keyframe = keyframe.index
2357                            keyframe = keyframe.parent.pages[keyframe.index]
2358                            ifds[i] = keyframe
2359                            break
2360                for i in ifds:
2361                    if i is not None:
2362                        i.keyframe = keyframe
2363
2364                dtype = keyframe.dtype
2365                series.append(
2366                    TiffPageSeries(ifds, shape, dtype, axes, parent=self,
2367                                   name=name, stype='OME'))
2368        for serie in series:
2369            shape = list(serie.shape)
2370            for axis, (newaxis, labels) in modulo.items():
2371                i = serie.axes.index(axis)
2372                size = len(labels)
2373                if shape[i] == size:
2374                    serie.axes = serie.axes.replace(axis, newaxis, 1)
2375                else:
2376                    shape[i] //= size
2377                    shape.insert(i+1, size)
2378                    serie.axes = serie.axes.replace(axis, axis+newaxis, 1)
2379            serie.shape = tuple(shape)
2380        # squeeze dimensions
2381        for serie in series:
2382            serie.shape, serie.axes = squeeze_axes(serie.shape, serie.axes)
2383        return series
2384
2385    def _lsm_series(self):
2386        """Return main image series in LSM file. Skip thumbnails."""
2387        lsmi = self.lsm_metadata
2388        axes = TIFF.CZ_LSMINFO_SCANTYPE[lsmi['ScanType']]
2389        if self.pages[0].photometric == 2:  # RGB; more than one channel
2390            axes = axes.replace('C', '').replace('XY', 'XYC')
2391        if lsmi.get('DimensionP', 0) > 1:
2392            axes += 'P'
2393        if lsmi.get('DimensionM', 0) > 1:
2394            axes += 'M'
2395        axes = axes[::-1]
2396        shape = tuple(int(lsmi[TIFF.CZ_LSMINFO_DIMENSIONS[i]]) for i in axes)
2397        name = lsmi.get('Name', '')
2398        self.pages.keyframe = 0
2399        pages = self.pages[::2]
2400        dtype = pages[0].dtype
2401        series = [TiffPageSeries(pages, shape, dtype, axes, name=name,
2402                                 stype='LSM')]
2403
2404        if self.pages[1].is_reduced:
2405            self.pages.keyframe = 1
2406            pages = self.pages[1::2]
2407            dtype = pages[0].dtype
2408            cp, i = 1, 0
2409            while cp < len(pages) and i < len(shape)-2:
2410                cp *= shape[i]
2411                i += 1
2412            shape = shape[:i] + pages[0].shape
2413            axes = axes[:i] + 'CYX'
2414            series.append(TiffPageSeries(pages, shape, dtype, axes, name=name,
2415                                         stype='LSMreduced'))
2416
2417        return series
2418
2419    def _lsm_load_pages(self):
2420        """Load all pages from LSM file."""
2421        self.pages.cache = True
2422        self.pages.useframes = True
2423        # second series: thumbnails
2424        self.pages.keyframe = 1
2425        keyframe = self.pages[1]
2426        for page in self.pages[1::2]:
2427            page.keyframe = keyframe
2428        # first series: data
2429        self.pages.keyframe = 0
2430        keyframe = self.pages[0]
2431        for page in self.pages[::2]:
2432            page.keyframe = keyframe
2433
2434    def _lsm_fix_strip_offsets(self):
2435        """Unwrap strip offsets for LSM files greater than 4 GB.
2436
2437        Each series and position require separate unwrapping (undocumented).
2438
2439        """
2440        if self.filehandle.size < 2**32:
2441            return
2442
2443        pages = self.pages
2444        npages = len(pages)
2445        series = self.series[0]
2446        axes = series.axes
2447
2448        # find positions
2449        positions = 1
2450        for i in 0, 1:
2451            if series.axes[i] in 'PM':
2452                positions *= series.shape[i]
2453
2454        # make time axis first
2455        if positions > 1:
2456            ntimes = 0
2457            for i in 1, 2:
2458                if axes[i] == 'T':
2459                    ntimes = series.shape[i]
2460                    break
2461            if ntimes:
2462                div, mod = divmod(npages, 2*positions*ntimes)
2463                assert mod == 0
2464                shape = (positions, ntimes, div, 2)
2465                indices = numpy.arange(product(shape)).reshape(shape)
2466                indices = numpy.moveaxis(indices, 1, 0)
2467        else:
2468            indices = numpy.arange(npages).reshape(-1, 2)
2469
2470        # images of reduced page might be stored first
2471        if pages[0].dataoffsets[0] > pages[1].dataoffsets[0]:
2472            indices = indices[..., ::-1]
2473
2474        # unwrap offsets
2475        wrap = 0
2476        previousoffset = 0
2477        for i in indices.flat:
2478            page = pages[i]
2479            dataoffsets = []
2480            for currentoffset in page.dataoffsets:
2481                if currentoffset < previousoffset:
2482                    wrap += 2**32
2483                dataoffsets.append(currentoffset + wrap)
2484                previousoffset = currentoffset
2485            page.dataoffsets = tuple(dataoffsets)
2486
2487    def _lsm_fix_strip_bytecounts(self):
2488        """Set databytecounts to size of compressed data.
2489
2490        The StripByteCounts tag in LSM files contains the number of bytes
2491        for the uncompressed data.
2492
2493        """
2494        pages = self.pages
2495        if pages[0].compression == 1:
2496            return
2497        # sort pages by first strip offset
2498        pages = sorted(pages, key=lambda p: p.dataoffsets[0])
2499        npages = len(pages) - 1
2500        for i, page in enumerate(pages):
2501            if page.index % 2:
2502                continue
2503            offsets = page.dataoffsets
2504            bytecounts = page.databytecounts
2505            if i < npages:
2506                lastoffset = pages[i+1].dataoffsets[0]
2507            else:
2508                # LZW compressed strips might be longer than uncompressed
2509                lastoffset = min(offsets[-1] + 2*bytecounts[-1], self._fh.size)
2510            offsets = offsets + (lastoffset,)
2511            page.databytecounts = tuple(offsets[j+1] - offsets[j]
2512                                        for j in range(len(bytecounts)))
2513
2514    def __getattr__(self, name):
2515        """Return 'is_flag' attributes from first page."""
2516        if name[3:] in TIFF.FILE_FLAGS:
2517            if not self.pages:
2518                return False
2519            value = bool(getattr(self.pages[0], name))
2520            setattr(self, name, value)
2521            return value
2522        raise AttributeError("'%s' object has no attribute '%s'" %
2523                             (self.__class__.__name__, name))
2524
2525    def __enter__(self):
2526        return self
2527
2528    def __exit__(self, exc_type, exc_value, traceback):
2529        self.close()
2530
2531    def __str__(self, detail=0, width=79):
2532        """Return string containing information about file.
2533
2534        The detail parameter specifies the level of detail returned:
2535
2536        0: file only.
2537        1: all series, first page of series and its tags.
2538        2: large tag values and file metadata.
2539        3: all pages.
2540
2541        """
2542        info = [
2543            "TiffFile '%s'",
2544            format_size(self._fh.size),
2545            {'<': 'LittleEndian', '>': 'BigEndian'}[self.byteorder]]
2546        if self.is_bigtiff:
2547            info.append('BigTiff')
2548        info.append('|'.join(f.upper() for f in self.flags))
2549        if len(self.pages) > 1:
2550            info.append('%i Pages' % len(self.pages))
2551        if len(self.series) > 1:
2552            info.append('%i Series' % len(self.series))
2553        if len(self._files) > 1:
2554            info.append('%i Files' % (len(self._files)))
2555        info = '  '.join(info)
2556        info = info.replace('    ', '  ').replace('   ', '  ')
2557        info = info % snipstr(self._fh.name, max(12, width+2-len(info)))
2558        if detail <= 0:
2559            return info
2560        info = [info]
2561        info.append('\n'.join(str(s) for s in self.series))
2562        if detail >= 3:
2563            info.extend((TiffPage.__str__(p, detail=detail, width=width)
2564                         for p in self.pages
2565                         if p is not None))
2566        else:
2567            info.extend((TiffPage.__str__(s.pages[0], detail=detail,
2568                                          width=width)
2569                         for s in self.series
2570                         if s.pages[0] is not None))
2571        if detail >= 2:
2572            for name in sorted(self.flags):
2573                if hasattr(self, name + '_metadata'):
2574                    m = getattr(self, name + '_metadata')
2575                    if m:
2576                        info.append(
2577                            '%s_METADATA\n%s' % (name.upper(),
2578                                                 pformat(m, width=width,
2579                                                         height=detail*12)))
2580        return '\n\n'.join(info).replace('\n\n\n', '\n\n')
2581
2582    @lazyattr
2583    def flags(self):
2584        """Return set of file flags."""
2585        return set(name.lower() for name in sorted(TIFF.FILE_FLAGS)
2586                   if getattr(self, 'is_' + name))
2587
2588    @lazyattr
2589    def is_mdgel(self):
2590        """File has MD Gel format."""
2591        try:
2592            return self.pages[0].is_mdgel or self.pages[1].is_mdgel
2593        except IndexError:
2594            return False
2595
2596    @property
2597    def is_movie(self):
2598        """Return if file is a movie."""
2599        return self.pages.useframes
2600
2601    @lazyattr
2602    def shaped_metadata(self):
2603        """Return Tifffile metadata from JSON descriptions as dicts."""
2604        if not self.is_shaped:
2605            return
2606        return tuple(json_description_metadata(s.pages[0].is_shaped)
2607                     for s in self.series if s.stype.lower() == 'shaped')
2608
2609    @lazyattr
2610    def ome_metadata(self):
2611        """Return OME XML as dict."""
2612        # TODO: remove this or return XML?
2613        if not self.is_ome:
2614            return
2615        return xml2dict(self.pages[0].description)['OME']
2616
2617    @lazyattr
2618    def qptiff_metadata(self):
2619        """Return PerkinElmer-QPI-ImageDescription XML element as dict."""
2620        if not self.is_qptiff:
2621            return
2622        root = 'PerkinElmer-QPI-ImageDescription'
2623        xml = self.pages[0].description.replace(' ' + root + ' ', root)
2624        return xml2dict(xml)[root]
2625
2626    @lazyattr
2627    def lsm_metadata(self):
2628        """Return LSM metadata from CZ_LSMINFO tag as dict."""
2629        if not self.is_lsm:
2630            return
2631        return self.pages[0].tags['CZ_LSMINFO'].value
2632
2633    @lazyattr
2634    def stk_metadata(self):
2635        """Return STK metadata from UIC tags as dict."""
2636        if not self.is_stk:
2637            return
2638        page = self.pages[0]
2639        tags = page.tags
2640        result = {}
2641        result['NumberPlanes'] = tags['UIC2tag'].count
2642        if page.description:
2643            result['PlaneDescriptions'] = page.description.split('\0')
2644            # result['plane_descriptions'] = stk_description_metadata(
2645            #    page.image_description)
2646        if 'UIC1tag' in tags:
2647            result.update(tags['UIC1tag'].value)
2648        if 'UIC3tag' in tags:
2649            result.update(tags['UIC3tag'].value)  # wavelengths
2650        if 'UIC4tag' in tags:
2651            result.update(tags['UIC4tag'].value)  # override uic1 tags
2652        uic2tag = tags['UIC2tag'].value
2653        result['ZDistance'] = uic2tag['ZDistance']
2654        result['TimeCreated'] = uic2tag['TimeCreated']
2655        result['TimeModified'] = uic2tag['TimeModified']
2656        try:
2657            result['DatetimeCreated'] = numpy.array(
2658                [julian_datetime(*dt) for dt in
2659                 zip(uic2tag['DateCreated'], uic2tag['TimeCreated'])],
2660                dtype='datetime64[ns]')
2661            result['DatetimeModified'] = numpy.array(
2662                [julian_datetime(*dt) for dt in
2663                 zip(uic2tag['DateModified'], uic2tag['TimeModified'])],
2664                dtype='datetime64[ns]')
2665        except ValueError as e:
2666            warnings.warn('stk_metadata: %s' % e)
2667        return result
2668
2669    @lazyattr
2670    def imagej_metadata(self):
2671        """Return consolidated ImageJ metadata as dict."""
2672        if not self.is_imagej:
2673            return
2674        page = self.pages[0]
2675        result = imagej_description_metadata(page.is_imagej)
2676        if 'IJMetadata' in page.tags:
2677            try:
2678                result.update(page.tags['IJMetadata'].value)
2679            except Exception:
2680                pass
2681        return result
2682
2683    @lazyattr
2684    def fluoview_metadata(self):
2685        """Return consolidated FluoView metadata as dict."""
2686        if not self.is_fluoview:
2687            return
2688        result = {}
2689        page = self.pages[0]
2690        result.update(page.tags['MM_Header'].value)
2691        # TODO: read stamps from all pages
2692        result['Stamp'] = page.tags['MM_Stamp'].value
2693        # skip parsing image description; not reliable
2694        # try:
2695        #     t = fluoview_description_metadata(page.image_description)
2696        #     if t is not None:
2697        #         result['ImageDescription'] = t
2698        # except Exception as e:
2699        #     warnings.warn(
2700        #         "failed to read FluoView image description: %s" % e)
2701        return result
2702
2703    @lazyattr
2704    def nih_metadata(self):
2705        """Return NIH Image metadata from NIHImageHeader tag as dict."""
2706        if not self.is_nih:
2707            return
2708        return self.pages[0].tags['NIHImageHeader'].value
2709
2710    @lazyattr
2711    def fei_metadata(self):
2712        """Return FEI metadata from SFEG or HELIOS tags as dict."""
2713        if not self.is_fei:
2714            return
2715        tags = self.pages[0].tags
2716        if 'FEI_SFEG' in tags:
2717            return tags['FEI_SFEG'].value
2718        if 'FEI_HELIOS' in tags:
2719            return tags['FEI_HELIOS'].value
2720
2721    @lazyattr
2722    def sem_metadata(self):
2723        """Return SEM metadata from CZ_SEM tag as dict."""
2724        if not self.is_sem:
2725            return
2726        return self.pages[0].tags['CZ_SEM'].value
2727
2728    @lazyattr
2729    def mdgel_metadata(self):
2730        """Return consolidated metadata from MD GEL tags as dict."""
2731        for page in self.pages[:2]:
2732            if 'MDFileTag' in page.tags:
2733                tags = page.tags
2734                break
2735        else:
2736            return
2737        result = {}
2738        for code in range(33445, 33453):
2739            name = TIFF.TAGS[code]
2740            if name not in tags:
2741                continue
2742            result[name[2:]] = tags[name].value
2743        return result
2744
2745    @lazyattr
2746    def andor_metadata(self):
2747        """Return Andor tags as dict."""
2748        return self.pages[0].andor_tags
2749
2750    @lazyattr
2751    def epics_metadata(self):
2752        """Return EPICS areaDetector tags as dict."""
2753        return self.pages[0].epics_tags
2754
2755    @lazyattr
2756    def tvips_metadata(self):
2757        """Return TVIPS tag as dict."""
2758        if not self.is_tvips:
2759            return
2760        return self.pages[0].tags['TVIPS'].value
2761
2762    @lazyattr
2763    def metaseries_metadata(self):
2764        """Return MetaSeries metadata from image description as dict."""
2765        if not self.is_metaseries:
2766            return
2767        return metaseries_description_metadata(self.pages[0].description)
2768
2769    @lazyattr
2770    def pilatus_metadata(self):
2771        """Return Pilatus metadata from image description as dict."""
2772        if not self.is_pilatus:
2773            return
2774        return pilatus_description_metadata(self.pages[0].description)
2775
2776    @lazyattr
2777    def micromanager_metadata(self):
2778        """Return consolidated MicroManager metadata as dict."""
2779        if not self.is_micromanager:
2780            return
2781        # from file header
2782        result = read_micromanager_metadata(self._fh)
2783        # from tag
2784        result.update(self.pages[0].tags['MicroManagerMetadata'].value)
2785        return result
2786
2787    @lazyattr
2788    def scanimage_metadata(self):
2789        """Return ScanImage non-varying frame and ROI metadata as dict."""
2790        if not self.is_scanimage:
2791            return
2792        result = {}
2793        try:
2794            framedata, roidata = read_scanimage_metadata(self._fh)
2795            result['FrameData'] = framedata
2796            result.update(roidata)
2797        except ValueError:
2798            pass
2799        # TODO: scanimage_artist_metadata
2800        try:
2801            result['Description'] = scanimage_description_metadata(
2802                self.pages[0].description)
2803        except Exception as e:
2804            warnings.warn('scanimage_description_metadata failed: %s' % e)
2805        return result
2806
2807    @property
2808    def geotiff_metadata(self):
2809        """Return GeoTIFF metadata from first page as dict."""
2810        if not self.is_geotiff:
2811            return
2812        return self.pages[0].geotiff_tags
2813
2814
2815class TiffPages(object):
2816    """Sequence of TIFF image file directories."""
2817    def __init__(self, parent):
2818        """Initialize instance from file. Read first TiffPage from file.
2819
2820        The file position must be at an offset to an offset to a TiffPage.
2821
2822        """
2823        self.parent = parent
2824        self.pages = []  # cache of TiffPages, TiffFrames, or their offsets
2825        self.complete = False  # True if offsets to all pages were read
2826        self._tiffpage = TiffPage  # class for reading tiff pages
2827        self._keyframe = None
2828        self._cache = True
2829
2830        # read offset to first page
2831        fh = parent.filehandle
2832        self._nextpageoffset = fh.tell()
2833        offset = struct.unpack(parent.offsetformat,
2834                               fh.read(parent.offsetsize))[0]
2835
2836        if offset == 0:
2837            # warnings.warn('file contains no pages')
2838            self.complete = True
2839            return
2840        if offset >= fh.size:
2841            warnings.warn('invalid page offset (%i)' % offset)
2842            self.complete = True
2843            return
2844
2845        # always read and cache first page
2846        fh.seek(offset)
2847        page = TiffPage(parent, index=0)
2848        self.pages.append(page)
2849        self._keyframe = page
2850
2851    @property
2852    def cache(self):
2853        """Return if pages/frames are currenly being cached."""
2854        return self._cache
2855
2856    @cache.setter
2857    def cache(self, value):
2858        """Enable or disable caching of pages/frames. Clear cache if False."""
2859        value = bool(value)
2860        if self._cache and not value:
2861            self.clear()
2862        self._cache = value
2863
2864    @property
2865    def useframes(self):
2866        """Return if currently using TiffFrame (True) or TiffPage (False)."""
2867        return self._tiffpage == TiffFrame and TiffFrame is not TiffPage
2868
2869    @useframes.setter
2870    def useframes(self, value):
2871        """Set to use TiffFrame (True) or TiffPage (False)."""
2872        self._tiffpage = TiffFrame if value else TiffPage
2873
2874    @property
2875    def keyframe(self):
2876        """Return index of current keyframe."""
2877        return self._keyframe.index
2878
2879    @keyframe.setter
2880    def keyframe(self, index):
2881        """Set current keyframe. Load TiffPage from file if necessary."""
2882        if self._keyframe.index == index:
2883            return
2884        if self.complete or 0 <= index < len(self.pages):
2885            page = self.pages[index]
2886            if isinstance(page, TiffPage):
2887                self._keyframe = page
2888                return
2889            elif isinstance(page, TiffFrame):
2890                # remove existing frame
2891                self.pages[index] = page.offset
2892        # load TiffPage from file
2893        useframes = self.useframes
2894        self._tiffpage = TiffPage
2895        self._keyframe = self[index]
2896        self.useframes = useframes
2897
2898    @property
2899    def next_page_offset(self):
2900        """Return offset where offset to a new page can be stored."""
2901        if not self.complete:
2902            self._seek(-1)
2903        return self._nextpageoffset
2904
2905    def load(self):
2906        """Read all remaining pages from file."""
2907        fh = self.parent.filehandle
2908        keyframe = self._keyframe
2909        pages = self.pages
2910        if not self.complete:
2911            self._seek(-1)
2912        for i, page in enumerate(pages):
2913            if isinstance(page, inttypes):
2914                fh.seek(page)
2915                page = self._tiffpage(self.parent, index=i, keyframe=keyframe)
2916                pages[i] = page
2917
2918    def clear(self, fully=True):
2919        """Delete all but first page from cache. Set keyframe to first page."""
2920        pages = self.pages
2921        if not self._cache or len(pages) < 1:
2922            return
2923        self._keyframe = pages[0]
2924        if fully:
2925            # delete all but first TiffPage/TiffFrame
2926            for i, page in enumerate(pages[1:]):
2927                if not isinstance(page, inttypes):
2928                    pages[i+1] = page.offset
2929        elif TiffFrame is not TiffPage:
2930            # delete only TiffFrames
2931            for i, page in enumerate(pages):
2932                if isinstance(page, TiffFrame):
2933                    pages[i] = page.offset
2934
2935    def _seek(self, index, maxpages=2**22):
2936        """Seek file to offset of specified page."""
2937        pages = self.pages
2938        if not pages:
2939            return
2940
2941        fh = self.parent.filehandle
2942        if fh.closed:
2943            raise RuntimeError('FileHandle is closed')
2944
2945        if self.complete or 0 <= index < len(pages):
2946            page = pages[index]
2947            offset = page if isinstance(page, inttypes) else page.offset
2948            fh.seek(offset)
2949            return
2950
2951        offsetformat = self.parent.offsetformat
2952        offsetsize = self.parent.offsetsize
2953        tagnoformat = self.parent.tagnoformat
2954        tagnosize = self.parent.tagnosize
2955        tagsize = self.parent.tagsize
2956        unpack = struct.unpack
2957
2958        page = pages[-1]
2959        offset = page if isinstance(page, inttypes) else page.offset
2960
2961        while len(pages) < maxpages:
2962            # read offsets to pages from file until index is reached
2963            fh.seek(offset)
2964            # skip tags
2965            try:
2966                tagno = unpack(tagnoformat, fh.read(tagnosize))[0]
2967                if tagno > 4096:
2968                    raise ValueError('suspicious number of tags')
2969            except Exception:
2970                warnings.warn('corrupted tag list at offset %i' % offset)
2971                del pages[-1]
2972                self.complete = True
2973                break
2974            self._nextpageoffset = offset + tagnosize + tagno * tagsize
2975            fh.seek(self._nextpageoffset)
2976
2977            # read offset to next page
2978            offset = unpack(offsetformat, fh.read(offsetsize))[0]
2979            if offset == 0:
2980                self.complete = True
2981                break
2982            if offset >= fh.size:
2983                warnings.warn('invalid page offset (%i)' % offset)
2984                self.complete = True
2985                break
2986
2987            pages.append(offset)
2988            if 0 <= index < len(pages):
2989                break
2990
2991        if index >= len(pages):
2992            raise IndexError('list index out of range')
2993
2994        page = pages[index]
2995        fh.seek(page if isinstance(page, inttypes) else page.offset)
2996
2997    def __bool__(self):
2998        """Return True if file contains any pages."""
2999        return len(self.pages) > 0
3000
3001    def __len__(self):
3002        """Return number of pages in file."""
3003        if not self.complete:
3004            self._seek(-1)
3005        return len(self.pages)
3006
3007    def __getitem__(self, key):
3008        """Return specified page(s) from cache or file."""
3009        pages = self.pages
3010        if not pages:
3011            raise IndexError('list index out of range')
3012        if key == 0:
3013            return pages[key]
3014
3015        if isinstance(key, slice):
3016            start, stop, _ = key.indices(2**31-1)
3017            if not self.complete and max(stop, start) > len(pages):
3018                self._seek(-1)
3019            return [self[i] for i in range(*key.indices(len(pages)))]
3020
3021        if self.complete and key >= len(pages):
3022            raise IndexError('list index out of range')
3023
3024        try:
3025            page = pages[key]
3026        except IndexError:
3027            page = 0
3028        if not isinstance(page, inttypes):
3029            return page
3030
3031        self._seek(key)
3032        page = self._tiffpage(self.parent, index=key, keyframe=self._keyframe)
3033        if self._cache:
3034            pages[key] = page
3035        return page
3036
3037    def __iter__(self):
3038        """Return iterator over all pages."""
3039        i = 0
3040        while True:
3041            try:
3042                yield self[i]
3043                i += 1
3044            except IndexError:
3045                break
3046
3047
3048class TiffPage(object):
3049    """TIFF image file directory (IFD).
3050
3051    Attributes
3052    ----------
3053    index : int
3054        Index of page in file.
3055    dtype : numpy.dtype or None
3056        Data type (native byte order) of the image in IFD.
3057    shape : tuple
3058        Dimensions of the image in IFD.
3059    axes : str
3060        Axes label codes:
3061        'X' width, 'Y' height, 'S' sample, 'I' image series|page|plane,
3062        'Z' depth, 'C' color|em-wavelength|channel, 'E' ex-wavelength|lambda,
3063        'T' time, 'R' region|tile, 'A' angle, 'P' phase, 'H' lifetime,
3064        'L' exposure, 'V' event, 'Q' unknown, '_' missing
3065    tags : dict
3066        Dictionary of tags in IFD. {tag.name: TiffTag}
3067    colormap : numpy.ndarray
3068        Color look up table, if exists.
3069
3070    All attributes are read-only.
3071
3072    Notes
3073    -----
3074    The internal, normalized '_shape' attribute is 6 dimensional:
3075
3076    0 : number planes/images  (stk, ij).
3077    1 : planar samplesperpixel.
3078    2 : imagedepth Z  (sgi).
3079    3 : imagelength Y.
3080    4 : imagewidth X.
3081    5 : contig samplesperpixel.
3082
3083    """
3084    # default properties; will be updated from tags
3085    imagewidth = 0
3086    imagelength = 0
3087    imagedepth = 1
3088    tilewidth = 0
3089    tilelength = 0
3090    tiledepth = 1
3091    bitspersample = 1
3092    samplesperpixel = 1
3093    sampleformat = 1
3094    rowsperstrip = 2**32-1
3095    compression = 1
3096    planarconfig = 1
3097    fillorder = 1
3098    photometric = 0
3099    predictor = 1
3100    extrasamples = 1
3101    colormap = None
3102    software = ''
3103    description = ''
3104    description1 = ''
3105
3106    def __init__(self, parent, index, keyframe=None):
3107        """Initialize instance from file.
3108
3109        The file handle position must be at offset to a valid IFD.
3110
3111        """
3112        self.parent = parent
3113        self.index = index
3114        self.shape = ()
3115        self._shape = ()
3116        self.dtype = None
3117        self._dtype = None
3118        self.axes = ''
3119        self.tags = {}
3120
3121        self.dataoffsets = ()
3122        self.databytecounts = ()
3123
3124        # read TIFF IFD structure and its tags from file
3125        fh = parent.filehandle
3126        self.offset = fh.tell()  # offset to this IFD
3127        try:
3128            tagno = struct.unpack(parent.tagnoformat,
3129                                  fh.read(parent.tagnosize))[0]
3130            if tagno > 4096:
3131                raise ValueError('suspicious number of tags')
3132        except Exception:
3133            raise ValueError('corrupted tag list at offset %i' % self.offset)
3134
3135        tagsize = parent.tagsize
3136        data = fh.read(tagsize * tagno)
3137        tags = self.tags
3138        index = -tagsize
3139        for _ in range(tagno):
3140            index += tagsize
3141            try:
3142                tag = TiffTag(self.parent, data[index:index+tagsize])
3143            except TiffTag.Error as e:
3144                warnings.warn(str(e))
3145                continue
3146            tagname = tag.name
3147            if tagname not in tags:
3148                name = tagname
3149                tags[name] = tag
3150            else:
3151                # some files contain multiple tags with same code
3152                # e.g. MicroManager files contain two ImageDescription tags
3153                i = 1
3154                while True:
3155                    name = '%s%i' % (tagname, i)
3156                    if name not in tags:
3157                        tags[name] = tag
3158                        break
3159            name = TIFF.TAG_ATTRIBUTES.get(name, '')
3160            if name:
3161                if (name[:3] in 'sof des' and not isinstance(tag.value, str)):
3162                    pass  # wrong string type for software, description
3163                else:
3164                    setattr(self, name, tag.value)
3165
3166        if not tags:
3167            return  # found in FIBICS
3168
3169        # consolidate private tags; remove them from self.tags
3170        if self.is_andor:
3171            self.andor_tags
3172        elif self.is_epics:
3173            self.epics_tags
3174
3175        if self.is_lsm or (self.index and self.parent.is_lsm):
3176            # correct non standard LSM bitspersample tags
3177            self.tags['BitsPerSample']._fix_lsm_bitspersample(self)
3178
3179        if self.is_vista or (self.index and self.parent.is_vista):
3180            # ISS Vista writes wrong ImageDepth tag
3181            self.imagedepth = 1
3182
3183        if self.is_stk and 'UIC1tag' in tags and not tags['UIC1tag'].value:
3184            # read UIC1tag now that plane count is known
3185            uic1tag = tags['UIC1tag']
3186            fh.seek(uic1tag.valueoffset)
3187            tags['UIC1tag'].value = read_uic1tag(
3188                fh, self.parent.byteorder, uic1tag.dtype,
3189                uic1tag.count, None, tags['UIC2tag'].count)
3190
3191        if 'IJMetadata' in tags:
3192            # decode IJMetadata tag
3193            try:
3194                tags['IJMetadata'].value = imagej_metadata(
3195                    tags['IJMetadata'].value,
3196                    tags['IJMetadataByteCounts'].value,
3197                    self.parent.byteorder)
3198            except Exception as e:
3199                warnings.warn(str(e))
3200
3201        if 'BitsPerSample' in tags:
3202            tag = tags['BitsPerSample']
3203            if tag.count == 1:
3204                self.bitspersample = tag.value
3205            else:
3206                # LSM might list more items than samplesperpixel
3207                value = tag.value[:self.samplesperpixel]
3208                if any((v-value[0] for v in value)):
3209                    self.bitspersample = value
3210                else:
3211                    self.bitspersample = value[0]
3212
3213        if 'SampleFormat' in tags:
3214            tag = tags['SampleFormat']
3215            if tag.count == 1:
3216                self.sampleformat = tag.value
3217            else:
3218                value = tag.value[:self.samplesperpixel]
3219                if any((v-value[0] for v in value)):
3220                    self.sampleformat = value
3221                else:
3222                    self.sampleformat = value[0]
3223
3224        if 'ImageLength' in tags:
3225            if 'RowsPerStrip' not in tags or tags['RowsPerStrip'].count > 1:
3226                self.rowsperstrip = self.imagelength
3227            # self.stripsperimage = int(math.floor(
3228            #    float(self.imagelength + self.rowsperstrip - 1) /
3229            #    self.rowsperstrip))
3230
3231        # determine dtype
3232        dtype = self.sampleformat, self.bitspersample
3233        dtype = TIFF.SAMPLE_DTYPES.get(dtype, None)
3234        if dtype is not None:
3235            dtype = numpy.dtype(dtype)
3236        self.dtype = self._dtype = dtype
3237
3238        # determine shape of data
3239        imagelength = self.imagelength
3240        imagewidth = self.imagewidth
3241        imagedepth = self.imagedepth
3242        samplesperpixel = self.samplesperpixel
3243
3244        if self.is_stk:
3245            assert self.imagedepth == 1
3246            uictag = tags['UIC2tag'].value
3247            planes = tags['UIC2tag'].count
3248            if self.planarconfig == 1:
3249                self._shape = (
3250                    planes, 1, 1, imagelength, imagewidth, samplesperpixel)
3251                if samplesperpixel == 1:
3252                    self.shape = (planes, imagelength, imagewidth)
3253                    self.axes = 'YX'
3254                else:
3255                    self.shape = (
3256                        planes, imagelength, imagewidth, samplesperpixel)
3257                    self.axes = 'YXS'
3258            else:
3259                self._shape = (
3260                    planes, samplesperpixel, 1, imagelength, imagewidth, 1)
3261                if samplesperpixel == 1:
3262                    self.shape = (planes, imagelength, imagewidth)
3263                    self.axes = 'YX'
3264                else:
3265                    self.shape = (
3266                        planes, samplesperpixel, imagelength, imagewidth)
3267                    self.axes = 'SYX'
3268            # detect type of series
3269            if planes == 1:
3270                self.shape = self.shape[1:]
3271            elif numpy.all(uictag['ZDistance'] != 0):
3272                self.axes = 'Z' + self.axes
3273            elif numpy.all(numpy.diff(uictag['TimeCreated']) != 0):
3274                self.axes = 'T' + self.axes
3275            else:
3276                self.axes = 'I' + self.axes
3277        elif self.photometric == 2 or samplesperpixel > 1:  # PHOTOMETRIC.RGB
3278            if self.planarconfig == 1:
3279                self._shape = (
3280                    1, 1, imagedepth, imagelength, imagewidth, samplesperpixel)
3281                if imagedepth == 1:
3282                    self.shape = (imagelength, imagewidth, samplesperpixel)
3283                    self.axes = 'YXS'
3284                else:
3285                    self.shape = (
3286                        imagedepth, imagelength, imagewidth, samplesperpixel)
3287                    self.axes = 'ZYXS'
3288            else:
3289                self._shape = (1, samplesperpixel, imagedepth,
3290                               imagelength, imagewidth, 1)
3291                if imagedepth == 1:
3292                    self.shape = (samplesperpixel, imagelength, imagewidth)
3293                    self.axes = 'SYX'
3294                else:
3295                    self.shape = (
3296                        samplesperpixel, imagedepth, imagelength, imagewidth)
3297                    self.axes = 'SZYX'
3298        else:
3299            self._shape = (1, 1, imagedepth, imagelength, imagewidth, 1)
3300            if imagedepth == 1:
3301                self.shape = (imagelength, imagewidth)
3302                self.axes = 'YX'
3303            else:
3304                self.shape = (imagedepth, imagelength, imagewidth)
3305                self.axes = 'ZYX'
3306
3307        # dataoffsets and databytecounts
3308        if 'TileOffsets' in tags:
3309            self.dataoffsets = tags['TileOffsets'].value
3310        elif 'StripOffsets' in tags:
3311            self.dataoffsets = tags['StripOffsets'].value
3312        else:
3313            self.dataoffsets = (0,)
3314
3315        if 'TileByteCounts' in tags:
3316            self.databytecounts = tags['TileByteCounts'].value
3317        elif 'StripByteCounts' in tags:
3318            self.databytecounts = tags['StripByteCounts'].value
3319        else:
3320            self.databytecounts = (
3321                product(self.shape) * (self.bitspersample // 8),)
3322            if self.compression != 1:
3323                warnings.warn('required ByteCounts tag is missing')
3324
3325        assert len(self.shape) == len(self.axes)
3326
3327    def asarray(self, out=None, squeeze=True, lock=None, reopen=True,
3328                maxsize=2**44, validate=True):
3329        """Read image data from file and return as numpy array.
3330
3331        Raise ValueError if format is unsupported.
3332
3333        Parameters
3334        ----------
3335        out : numpy.ndarray, str, or file-like object; optional
3336            Buffer where image data will be saved.
3337            If None (default), a new array will be created.
3338            If numpy.ndarray, a writable array of compatible dtype and shape.
3339            If 'memmap', directly memory-map the image data in the TIFF file
3340            if possible; else create a memory-mapped array in a temporary file.
3341            If str or open file, the file name or file object used to
3342            create a memory-map to an array stored in a binary file on disk.
3343        squeeze : bool
3344            If True, all length-1 dimensions (except X and Y) are
3345            squeezed out from the array.
3346            If False, the shape of the returned array might be different from
3347            the page.shape.
3348        lock : {RLock, NullContext}
3349            A reentrant lock used to syncronize reads from file.
3350            If None (default), the lock of the parent's filehandle is used.
3351        reopen : bool
3352            If True (default) and the parent file handle is closed, the file
3353            is temporarily re-opened and closed if no exception occurs.
3354        maxsize: int or None
3355            Maximum size of data before a ValueError is raised.
3356            Can be used to catch DOS. Default: 16 TB.
3357        validate : bool
3358            If True (default), validate various parameters.
3359            If None, only validate parameters and return None.
3360
3361        """
3362        self_ = self
3363        self = self.keyframe  # self or keyframe
3364
3365        if not self._shape or product(self._shape) == 0:
3366            return
3367
3368        tags = self.tags
3369
3370        if validate or validate is None:
3371            if maxsize and product(self._shape) > maxsize:
3372                raise ValueError('data are too large %s' % str(self._shape))
3373            if self.dtype is None:
3374                raise ValueError('data type not supported: %s%i' % (
3375                    self.sampleformat, self.bitspersample))
3376            if self.compression not in TIFF.DECOMPESSORS:
3377                raise ValueError(
3378                    'cannot decompress %s' % self.compression.name)
3379            if 'SampleFormat' in tags:
3380                tag = tags['SampleFormat']
3381                if tag.count != 1 and any((i-tag.value[0] for i in tag.value)):
3382                    raise ValueError(
3383                        'sample formats do not match %s' % tag.value)
3384            if self.is_chroma_subsampled and (self.compression != 7 or
3385                                              self.planarconfig == 2):
3386                raise NotImplementedError('chroma subsampling not supported')
3387            if validate is None:
3388                return
3389
3390        fh = self_.parent.filehandle
3391        lock = fh.lock if lock is None else lock
3392        with lock:
3393            closed = fh.closed
3394            if closed:
3395                if reopen:
3396                    fh.open()
3397                else:
3398                    raise IOError('file handle is closed')
3399
3400        dtype = self._dtype
3401        shape = self._shape
3402        imagewidth = self.imagewidth
3403        imagelength = self.imagelength
3404        imagedepth = self.imagedepth
3405        bitspersample = self.bitspersample
3406        typecode = self.parent.byteorder + dtype.char
3407        lsb2msb = self.fillorder == 2
3408        offsets, bytecounts = self_.offsets_bytecounts
3409        istiled = self.is_tiled
3410
3411        if istiled:
3412            tilewidth = self.tilewidth
3413            tilelength = self.tilelength
3414            tiledepth = self.tiledepth
3415            tw = (imagewidth + tilewidth - 1) // tilewidth
3416            tl = (imagelength + tilelength - 1) // tilelength
3417            td = (imagedepth + tiledepth - 1) // tiledepth
3418            shape = (shape[0], shape[1],
3419                     td*tiledepth, tl*tilelength, tw*tilewidth, shape[-1])
3420            tileshape = (tiledepth, tilelength, tilewidth, shape[-1])
3421            runlen = tilewidth
3422        else:
3423            runlen = imagewidth
3424
3425        if self.planarconfig == 1:
3426            runlen *= self.samplesperpixel
3427
3428        if out == 'memmap' and self.is_memmappable:
3429            with lock:
3430                result = fh.memmap_array(typecode, shape, offset=offsets[0])
3431        elif self.is_contiguous:
3432            if out is not None:
3433                out = create_output(out, shape, dtype)
3434            with lock:
3435                fh.seek(offsets[0])
3436                result = fh.read_array(typecode, product(shape), out=out)
3437            if out is None and not result.dtype.isnative:
3438                # swap byte order and dtype without copy
3439                result.byteswap(True)
3440                result = result.newbyteorder()
3441            if lsb2msb:
3442                reverse_bitorder(result)
3443        else:
3444            result = create_output(out, shape, dtype)
3445
3446            decompress = TIFF.DECOMPESSORS[self.compression]
3447
3448            if self.compression == 7:  # COMPRESSION.JPEG
3449                if bitspersample not in (8, 12):
3450                    raise ValueError(
3451                        'unsupported JPEG precision %i' % bitspersample)
3452                if 'JPEGTables' in tags:
3453                    table = tags['JPEGTables'].value
3454                else:
3455                    table = b''
3456                unpack = identityfunc
3457                colorspace = TIFF.PHOTOMETRIC(self.photometric).name
3458
3459                def decompress(x, func=decompress, table=table,
3460                               bitspersample=bitspersample,
3461                               colorspace=colorspace):
3462                    return func(x, table, bitspersample,
3463                                colorspace).reshape(-1)
3464
3465            elif bitspersample in (8, 16, 32, 64, 128):
3466                if (bitspersample * runlen) % 8:
3467                    raise ValueError('data and sample size mismatch')
3468
3469                def unpack(x, typecode=typecode):
3470                    if self.predictor == 3:  # PREDICTOR.FLOATINGPOINT
3471                        # the floating point horizontal differencing decoder
3472                        # needs the raw byte order
3473                        typecode = dtype.char
3474                    try:
3475                        # read only numpy array
3476                        return numpy.frombuffer(x, typecode)
3477                    except ValueError:
3478                        # strips may be missing EOI
3479                        # warnings.warn('unpack: %s' % e)
3480                        xlen = ((len(x) // (bitspersample // 8)) *
3481                                (bitspersample // 8))
3482                        return numpy.frombuffer(x[:xlen], typecode)
3483
3484            elif isinstance(bitspersample, tuple):
3485                def unpack(x, typecode=typecode, bitspersample=bitspersample):
3486                    return unpack_rgb(x, typecode, bitspersample)
3487            else:
3488                def unpack(x, typecode=typecode, bitspersample=bitspersample,
3489                           runlen=runlen):
3490                    return unpack_ints(x, typecode, bitspersample, runlen)
3491
3492            if istiled:
3493                writable = None
3494                tw, tl, td, pl = 0, 0, 0, 0
3495                for tile in buffered_read(fh, lock, offsets, bytecounts):
3496                    if lsb2msb:
3497                        tile = reverse_bitorder(tile)
3498                    tile = decompress(tile)
3499                    tile = unpack(tile)
3500                    try:
3501                        tile.shape = tileshape
3502                    except ValueError:
3503                        # incomplete tiles; see gdal issue #1179
3504                        warnings.warn('invalid tile data')
3505                        t = numpy.zeros(tileshape, dtype).reshape(-1)
3506                        s = min(tile.size, t.size)
3507                        t[:s] = tile[:s]
3508                        tile = t.reshape(tileshape)
3509                    if self.predictor == 2:  # PREDICTOR.HORIZONTAL
3510                        if writable is None:
3511                            writable = tile.flags['WRITEABLE']
3512                        if writable:
3513                            numpy.cumsum(tile, axis=-2, dtype=dtype, out=tile)
3514                        else:
3515                            tile = numpy.cumsum(tile, axis=-2, dtype=dtype)
3516                    elif self.predictor == 3:  # PREDICTOR.FLOATINGPOINT
3517                        raise NotImplementedError()
3518                    result[0, pl, td:td+tiledepth,
3519                           tl:tl+tilelength, tw:tw+tilewidth, :] = tile
3520                    del tile
3521                    tw += tilewidth
3522                    if tw >= shape[4]:
3523                        tw, tl = 0, tl + tilelength
3524                        if tl >= shape[3]:
3525                            tl, td = 0, td + tiledepth
3526                            if td >= shape[2]:
3527                                td, pl = 0, pl + 1
3528                result = result[..., :imagedepth, :imagelength, :imagewidth, :]
3529            else:
3530                strip_size = self.rowsperstrip * self.imagewidth
3531                if self.planarconfig == 1:
3532                    strip_size *= self.samplesperpixel
3533                result = result.reshape(-1)
3534                index = 0
3535                for strip in buffered_read(fh, lock, offsets, bytecounts):
3536                    if lsb2msb:
3537                        strip = reverse_bitorder(strip)
3538                    strip = decompress(strip)
3539                    strip = unpack(strip)
3540                    size = min(result.size, strip.size, strip_size,
3541                               result.size - index)
3542                    result[index:index+size] = strip[:size]
3543                    del strip
3544                    index += size
3545
3546        result.shape = self._shape
3547
3548        if self.predictor != 1 and not (istiled and not self.is_contiguous):
3549            if self.parent.is_lsm and self.compression == 1:
3550                pass  # work around bug in LSM510 software
3551            elif self.predictor == 2:  # PREDICTOR.HORIZONTAL
3552                numpy.cumsum(result, axis=-2, dtype=dtype, out=result)
3553            elif self.predictor == 3:  # PREDICTOR.FLOATINGPOINT
3554                result = decode_floats(result)
3555
3556        if squeeze:
3557            try:
3558                result.shape = self.shape
3559            except ValueError:
3560                warnings.warn('failed to reshape from %s to %s' % (
3561                    str(result.shape), str(self.shape)))
3562
3563        if closed:
3564            # TODO: file should remain open if an exception occurred above
3565            fh.close()
3566        return result
3567
3568    def asrgb(self, uint8=False, alpha=None, colormap=None,
3569              dmin=None, dmax=None, *args, **kwargs):
3570        """Return image data as RGB(A).
3571
3572        Work in progress.
3573
3574        """
3575        data = self.asarray(*args, **kwargs)
3576        self = self.keyframe  # self or keyframe
3577        photometric = self.photometric
3578        PHOTOMETRIC = TIFF.PHOTOMETRIC
3579
3580        if photometric == PHOTOMETRIC.PALETTE:
3581            colormap = self.colormap
3582            if (colormap.shape[1] < 2**self.bitspersample or
3583                    self.dtype.char not in 'BH'):
3584                raise ValueError('cannot apply colormap')
3585            if uint8:
3586                if colormap.max() > 255:
3587                    colormap >>= 8
3588                colormap = colormap.astype('uint8')
3589            if 'S' in self.axes:
3590                data = data[..., 0] if self.planarconfig == 1 else data[0]
3591            data = apply_colormap(data, colormap)
3592
3593        elif photometric == PHOTOMETRIC.RGB:
3594            if 'ExtraSamples' in self.tags:
3595                if alpha is None:
3596                    alpha = TIFF.EXTRASAMPLE
3597                extrasamples = self.extrasamples
3598                if self.tags['ExtraSamples'].count == 1:
3599                    extrasamples = (extrasamples,)
3600                for i, exs in enumerate(extrasamples):
3601                    if exs in alpha:
3602                        if self.planarconfig == 1:
3603                            data = data[..., [0, 1, 2, 3+i]]
3604                        else:
3605                            data = data[:, [0, 1, 2, 3+i]]
3606                        break
3607            else:
3608                if self.planarconfig == 1:
3609                    data = data[..., :3]
3610                else:
3611                    data = data[:, :3]
3612            # TODO: convert to uint8?
3613
3614        elif photometric == PHOTOMETRIC.MINISBLACK:
3615            raise NotImplementedError()
3616        elif photometric == PHOTOMETRIC.MINISWHITE:
3617            raise NotImplementedError()
3618        elif photometric == PHOTOMETRIC.SEPARATED:
3619            raise NotImplementedError()
3620        else:
3621            raise NotImplementedError()
3622        return data
3623
3624    def aspage(self):
3625        return self
3626
3627    @property
3628    def keyframe(self):
3629        return self
3630
3631    @keyframe.setter
3632    def keyframe(self, index):
3633        return
3634
3635    @lazyattr
3636    def offsets_bytecounts(self):
3637        """Return simplified offsets and bytecounts."""
3638        if self.is_contiguous:
3639            offset, byte_count = self.is_contiguous
3640            return [offset], [byte_count]
3641        return clean_offsets_counts(self.dataoffsets, self.databytecounts)
3642
3643    @lazyattr
3644    def is_contiguous(self):
3645        """Return offset and size of contiguous data, else None.
3646
3647        Excludes prediction and fill_order.
3648
3649        """
3650        if (self.compression != 1
3651                or self.bitspersample not in (8, 16, 32, 64)):
3652            return
3653        if 'TileWidth' in self.tags:
3654            if (self.imagewidth != self.tilewidth or
3655                    self.imagelength % self.tilelength or
3656                    self.tilewidth % 16 or self.tilelength % 16):
3657                return
3658            if ('ImageDepth' in self.tags and 'TileDepth' in self.tags and
3659                    (self.imagelength != self.tilelength or
3660                     self.imagedepth % self.tiledepth)):
3661                return
3662
3663        offsets = self.dataoffsets
3664        bytecounts = self.databytecounts
3665        if len(offsets) == 1:
3666            return offsets[0], bytecounts[0]
3667        if self.is_stk or all((offsets[i] + bytecounts[i] == offsets[i+1] or
3668                               bytecounts[i+1] == 0)  # no data/ignore offset
3669                              for i in range(len(offsets)-1)):
3670            return offsets[0], sum(bytecounts)
3671
3672    @lazyattr
3673    def is_final(self):
3674        """Return if page's image data are stored in final form.
3675
3676        Excludes byte-swapping.
3677
3678        """
3679        return (self.is_contiguous and self.fillorder == 1 and
3680                self.predictor == 1 and not self.is_chroma_subsampled)
3681
3682    @lazyattr
3683    def is_memmappable(self):
3684        """Return if page's image data in file can be memory-mapped."""
3685        return (self.parent.filehandle.is_file and self.is_final and
3686                # (self.bitspersample == 8 or self.parent.isnative) and
3687                self.is_contiguous[0] % self.dtype.itemsize == 0)  # aligned?
3688
3689    def __str__(self, detail=0, width=79):
3690        """Return string containing information about page."""
3691        if self.keyframe != self:
3692            return TiffFrame.__str__(self, detail)
3693        attr = ''
3694        for name in ('memmappable', 'final', 'contiguous'):
3695            attr = getattr(self, 'is_'+name)
3696            if attr:
3697                attr = name.upper()
3698                break
3699        info = '  '.join(s for s in (
3700            'x'.join(str(i) for i in self.shape),
3701            '%s%s' % (TIFF.SAMPLEFORMAT(self.sampleformat).name,
3702                      self.bitspersample),
3703            '|'.join(i for i in (
3704                TIFF.PHOTOMETRIC(self.photometric).name,
3705                'TILED' if self.is_tiled else '',
3706                self.compression.name if self.compression != 1 else '',
3707                self.planarconfig.name if self.planarconfig != 1 else '',
3708                self.predictor.name if self.predictor != 1 else '',
3709                self.fillorder.name if self.fillorder != 1 else '')
3710                     if i),
3711            attr,
3712            '|'.join((f.upper() for f in self.flags))
3713            ) if s)
3714        info = 'TiffPage %i @%i  %s' % (self.index, self.offset, info)
3715        if detail <= 0:
3716            return info
3717        info = [info]
3718        tags = self.tags
3719        tlines = []
3720        vlines = []
3721        for tag in sorted(tags.values(), key=lambda x: x.code):
3722            value = tag.__str__(width=width+1)
3723            tlines.append(value[:width].strip())
3724            if detail > 1 and len(value) > width:
3725                name = tag.name.upper()
3726                if detail <= 2 and ('COUNTS' in name or 'OFFSETS' in name):
3727                    value = pformat(tag.value, width=width, height=detail*4)
3728                else:
3729                    value = pformat(tag.value, width=width, height=detail*12)
3730                vlines.append('%s\n%s' % (tag.name, value))
3731        info.append('\n'.join(tlines))
3732        if detail > 1:
3733            info.append('\n\n'.join(vlines))
3734        if detail > 3:
3735            try:
3736                info.append('DATA\n%s' % pformat(
3737                    self.asarray(), width=width, height=detail*8))
3738            except Exception:
3739                pass
3740        return '\n\n'.join(info)
3741
3742    @lazyattr
3743    def flags(self):
3744        """Return set of flags."""
3745        return set((name.lower() for name in sorted(TIFF.FILE_FLAGS)
3746                    if getattr(self, 'is_' + name)))
3747
3748    @property
3749    def ndim(self):
3750        """Return number of array dimensions."""
3751        return len(self.shape)
3752
3753    @property
3754    def size(self):
3755        """Return number of elements in array."""
3756        return product(self.shape)
3757
3758    @lazyattr
3759    def andor_tags(self):
3760        """Return consolidated metadata from Andor tags as dict.
3761
3762        Remove Andor tags from self.tags.
3763
3764        """
3765        if not self.is_andor:
3766            return
3767        tags = self.tags
3768        result = {'Id': tags['AndorId'].value}
3769        for tag in list(self.tags.values()):
3770            code = tag.code
3771            if not 4864 < code < 5031:
3772                continue
3773            value = tag.value
3774            name = tag.name[5:] if len(tag.name) > 5 else tag.name
3775            result[name] = value
3776            del tags[tag.name]
3777        return result
3778
3779    @lazyattr
3780    def epics_tags(self):
3781        """Return consolidated metadata from EPICS areaDetector tags as dict.
3782
3783        Remove areaDetector tags from self.tags.
3784
3785        """
3786        if not self.is_epics:
3787            return
3788        result = {}
3789        tags = self.tags
3790        for tag in list(self.tags.values()):
3791            code = tag.code
3792            if not 65000 <= code < 65500:
3793                continue
3794            value = tag.value
3795            if code == 65000:
3796                result['timeStamp'] = datetime.datetime.fromtimestamp(
3797                    float(value))
3798            elif code == 65001:
3799                result['uniqueID'] = int(value)
3800            elif code == 65002:
3801                result['epicsTSSec'] = int(value)
3802            elif code == 65003:
3803                result['epicsTSNsec'] = int(value)
3804            else:
3805                key, value = value.split(':', 1)
3806                result[key] = astype(value)
3807            del tags[tag.name]
3808        return result
3809
3810    @lazyattr
3811    def geotiff_tags(self):
3812        """Return consolidated metadata from GeoTIFF tags as dict."""
3813        if not self.is_geotiff:
3814            return
3815        tags = self.tags
3816
3817        gkd = tags['GeoKeyDirectoryTag'].value
3818        if gkd[0] != 1:
3819            warnings.warn('invalid GeoKeyDirectoryTag')
3820            return {}
3821
3822        result = {
3823            'KeyDirectoryVersion': gkd[0],
3824            'KeyRevision': gkd[1],
3825            'KeyRevisionMinor': gkd[2],
3826            # 'NumberOfKeys': gkd[3],
3827        }
3828        # deltags = ['GeoKeyDirectoryTag']
3829        geokeys = TIFF.GEO_KEYS
3830        geocodes = TIFF.GEO_CODES
3831        for index in range(gkd[3]):
3832            keyid, tagid, count, offset = gkd[4 + index * 4: index * 4 + 8]
3833            keyid = geokeys.get(keyid, keyid)
3834            if tagid == 0:
3835                value = offset
3836            else:
3837                tagname = TIFF.TAGS[tagid]
3838                # deltags.append(tagname)
3839                value = tags[tagname].value[offset: offset + count]
3840                if tagid == 34737 and count > 1 and value[-1] == '|':
3841                    value = value[:-1]
3842                value = value if count > 1 else value[0]
3843            if keyid in geocodes:
3844                try:
3845                    value = geocodes[keyid](value)
3846                except Exception:
3847                    pass
3848            result[keyid] = value
3849
3850        if 'IntergraphMatrixTag' in tags:
3851            value = tags['IntergraphMatrixTag'].value
3852            value = numpy.array(value)
3853            if len(value) == 16:
3854                value = value.reshape((4, 4)).tolist()
3855            result['IntergraphMatrix'] = value
3856        if 'ModelPixelScaleTag' in tags:
3857            value = numpy.array(tags['ModelPixelScaleTag'].value).tolist()
3858            result['ModelPixelScale'] = value
3859        if 'ModelTiepointTag' in tags:
3860            value = tags['ModelTiepointTag'].value
3861            value = numpy.array(value).reshape((-1, 6)).squeeze().tolist()
3862            result['ModelTiepoint'] = value
3863        if 'ModelTransformationTag' in tags:
3864            value = tags['ModelTransformationTag'].value
3865            value = numpy.array(value).reshape((4, 4)).tolist()
3866            result['ModelTransformation'] = value
3867        elif False:
3868            # if 'ModelPixelScaleTag' in tags and 'ModelTiepointTag' in tags:
3869            sx, sy, sz = tags['ModelPixelScaleTag'].value
3870            tiepoints = tags['ModelTiepointTag'].value
3871            transforms = []
3872            for tp in range(0, len(tiepoints), 6):
3873                i, j, k, x, y, z = tiepoints[tp:tp+6]
3874                transforms.append([
3875                    [sx,  0.0, 0.0, x - i * sx],
3876                    [0.0, -sy, 0.0, y + j * sy],
3877                    [0.0, 0.0,  sz, z - k * sz],
3878                    [0.0, 0.0, 0.0, 1.0]])
3879            if len(tiepoints) == 6:
3880                transforms = transforms[0]
3881            result['ModelTransformation'] = transforms
3882
3883        if 'RPCCoefficientTag' in tags:
3884            rpcc = tags['RPCCoefficientTag'].value
3885            result['RPCCoefficient'] = {
3886                'ERR_BIAS': rpcc[0],
3887                'ERR_RAND': rpcc[1],
3888                'LINE_OFF': rpcc[2],
3889                'SAMP_OFF': rpcc[3],
3890                'LAT_OFF': rpcc[4],
3891                'LONG_OFF': rpcc[5],
3892                'HEIGHT_OFF': rpcc[6],
3893                'LINE_SCALE': rpcc[7],
3894                'SAMP_SCALE': rpcc[8],
3895                'LAT_SCALE': rpcc[9],
3896                'LONG_SCALE': rpcc[10],
3897                'HEIGHT_SCALE': rpcc[11],
3898                'LINE_NUM_COEFF': rpcc[12:33],
3899                'LINE_DEN_COEFF ': rpcc[33:53],
3900                'SAMP_NUM_COEFF': rpcc[53:73],
3901                'SAMP_DEN_COEFF': rpcc[73:]}
3902
3903        return result
3904
3905    @property
3906    def is_tiled(self):
3907        """Page contains tiled image."""
3908        return 'TileWidth' in self.tags
3909
3910    @property
3911    def is_reduced(self):
3912        """Page is reduced image of another image."""
3913        return ('NewSubfileType' in self.tags and
3914                self.tags['NewSubfileType'].value & 1)
3915
3916    @property
3917    def is_chroma_subsampled(self):
3918        """Page contains chroma subsampled image."""
3919        return ('YCbCrSubSampling' in self.tags and
3920                self.tags['YCbCrSubSampling'].value != (1, 1))
3921
3922    @lazyattr
3923    def is_imagej(self):
3924        """Return ImageJ description if exists, else None."""
3925        for description in (self.description, self.description1):
3926            if not description:
3927                return
3928            if description[:7] == 'ImageJ=':
3929                return description
3930
3931    @lazyattr
3932    def is_shaped(self):
3933        """Return description containing array shape if exists, else None."""
3934        for description in (self.description, self.description1):
3935            if not description:
3936                return
3937            if description[:1] == '{' and '"shape":' in description:
3938                return description
3939            if description[:6] == 'shape=':
3940                return description
3941
3942    @property
3943    def is_mdgel(self):
3944        """Page contains MDFileTag tag."""
3945        return 'MDFileTag' in self.tags
3946
3947    @property
3948    def is_mediacy(self):
3949        """Page contains Media Cybernetics Id tag."""
3950        return ('MC_Id' in self.tags and
3951                self.tags['MC_Id'].value[:7] == b'MC TIFF')
3952
3953    @property
3954    def is_stk(self):
3955        """Page contains UIC2Tag tag."""
3956        return 'UIC2tag' in self.tags
3957
3958    @property
3959    def is_lsm(self):
3960        """Page contains CZ_LSMINFO tag."""
3961        return 'CZ_LSMINFO' in self.tags
3962
3963    @property
3964    def is_fluoview(self):
3965        """Page contains FluoView MM_STAMP tag."""
3966        return 'MM_Stamp' in self.tags
3967
3968    @property
3969    def is_nih(self):
3970        """Page contains NIH image header."""
3971        return 'NIHImageHeader' in self.tags
3972
3973    @property
3974    def is_sgi(self):
3975        """Page contains SGI image and tile depth tags."""
3976        return 'ImageDepth' in self.tags and 'TileDepth' in self.tags
3977
3978    @property
3979    def is_vista(self):
3980        """Software tag is 'ISS Vista'."""
3981        return self.software == 'ISS Vista'
3982
3983    @property
3984    def is_metaseries(self):
3985        """Page contains MDS MetaSeries metadata in ImageDescription tag."""
3986        if self.index > 1 or self.software != 'MetaSeries':
3987            return False
3988        d = self.description
3989        return d.startswith('<MetaData>') and d.endswith('</MetaData>')
3990
3991    @property
3992    def is_ome(self):
3993        """Page contains OME-XML in ImageDescription tag."""
3994        if self.index > 1 or not self.description:
3995            return False
3996        d = self.description
3997        return d[:14] == '<?xml version=' and d[-6:] == '</OME>'
3998
3999    @property
4000    def is_scn(self):
4001        """Page contains Leica SCN XML in ImageDescription tag."""
4002        if self.index > 1 or not self.description:
4003            return False
4004        d = self.description
4005        return d[:14] == '<?xml version=' and d[-6:] == '</scn>'
4006
4007    @property
4008    def is_micromanager(self):
4009        """Page contains Micro-Manager metadata."""
4010        return 'MicroManagerMetadata' in self.tags
4011
4012    @property
4013    def is_andor(self):
4014        """Page contains Andor Technology tags."""
4015        return 'AndorId' in self.tags
4016
4017    @property
4018    def is_pilatus(self):
4019        """Page contains Pilatus tags."""
4020        return (self.software[:8] == 'TVX TIFF' and
4021                self.description[:2] == '# ')
4022
4023    @property
4024    def is_epics(self):
4025        """Page contains EPICS areaDetector tags."""
4026        return (self.description == 'EPICS areaDetector' or
4027                self.software == 'EPICS areaDetector')
4028
4029    @property
4030    def is_tvips(self):
4031        """Page contains TVIPS metadata."""
4032        return 'TVIPS' in self.tags
4033
4034    @property
4035    def is_fei(self):
4036        """Page contains SFEG or HELIOS metadata."""
4037        return 'FEI_SFEG' in self.tags or 'FEI_HELIOS' in self.tags
4038
4039    @property
4040    def is_sem(self):
4041        """Page contains Zeiss SEM metadata."""
4042        return 'CZ_SEM' in self.tags
4043
4044    @property
4045    def is_svs(self):
4046        """Page contains Aperio metadata."""
4047        return self.description[:20] == 'Aperio Image Library'
4048
4049    @property
4050    def is_scanimage(self):
4051        """Page contains ScanImage metadata."""
4052        return (self.description[:12] == 'state.config' or
4053                self.software[:22] == 'SI.LINE_FORMAT_VERSION' or
4054                'scanimage.SI.' in self.description[-256:])
4055
4056    @property
4057    def is_qptiff(self):
4058        """Page contains PerkinElmer tissue images metadata."""
4059        # The ImageDescription tag contains XML with a top-level
4060        # <PerkinElmer-QPI-ImageDescription> element
4061        return self.software[:15] == 'PerkinElmer-QPI'
4062
4063    @property
4064    def is_geotiff(self):
4065        """Page contains GeoTIFF metadata."""
4066        return 'GeoKeyDirectoryTag' in self.tags
4067
4068
4069class TiffFrame(object):
4070    """Lightweight TIFF image file directory (IFD).
4071
4072    Only a limited number of tag values are read from file, e.g. StripOffsets,
4073    and StripByteCounts. Other tag values are assumed to be identical with a
4074    specified TiffPage instance, the keyframe.
4075
4076    TiffFrame is intended to reduce resource usage and speed up reading data
4077    from file, not for introspection of metadata.
4078
4079    Not compatible with Python 2.
4080
4081    """
4082    __slots__ = ('keyframe', 'parent', 'index', 'offset',
4083                 'dataoffsets', 'databytecounts')
4084
4085    is_mdgel = False
4086    tags = {}
4087
4088    def __init__(self, parent, index, keyframe):
4089        """Read specified tags from file.
4090
4091        The file handle position must be at the offset to a valid IFD.
4092
4093        """
4094        self.keyframe = keyframe
4095        self.parent = parent
4096        self.index = index
4097        self.dataoffsets = None
4098        self.databytecounts = None
4099
4100        unpack = struct.unpack
4101        fh = parent.filehandle
4102        self.offset = fh.tell()
4103        try:
4104            tagno = unpack(parent.tagnoformat, fh.read(parent.tagnosize))[0]
4105            if tagno > 4096:
4106                raise ValueError('suspicious number of tags')
4107        except Exception:
4108            raise ValueError('corrupted page list at offset %i' % self.offset)
4109
4110        # tags = {}
4111        tagcodes = {273, 279, 324, 325}  # TIFF.FRAME_TAGS
4112        tagsize = parent.tagsize
4113        codeformat = parent.tagformat1[:2]
4114
4115        data = fh.read(tagsize * tagno)
4116        index = -tagsize
4117        for _ in range(tagno):
4118            index += tagsize
4119            code = unpack(codeformat, data[index:index+2])[0]
4120            if code not in tagcodes:
4121                continue
4122            try:
4123                tag = TiffTag(parent, data[index:index+tagsize])
4124            except TiffTag.Error as e:
4125                warnings.warn(str(e))
4126                continue
4127            if code == 273 or code == 324:
4128                setattr(self, 'dataoffsets', tag.value)
4129            elif code == 279 or code == 325:
4130                setattr(self, 'databytecounts', tag.value)
4131            # elif code == 270:
4132            #     tagname = tag.name
4133            #     if tagname not in tags:
4134            #         tags[tagname] = bytes2str(tag.value)
4135            #     elif 'ImageDescription1' not in tags:
4136            #         tags['ImageDescription1'] = bytes2str(tag.value)
4137            # else:
4138            #     tags[tag.name] = tag.value
4139
4140    def aspage(self):
4141        """Return TiffPage from file."""
4142        self.parent.filehandle.seek(self.offset)
4143        return TiffPage(self.parent, index=self.index, keyframe=None)
4144
4145    def asarray(self, *args, **kwargs):
4146        """Read image data from file and return as numpy array."""
4147        # TODO: fix TypeError on Python 2
4148        #   "TypeError: unbound method asarray() must be called with TiffPage
4149        #   instance as first argument (got TiffFrame instance instead)"
4150        kwargs['validate'] = False
4151        return TiffPage.asarray(self, *args, **kwargs)
4152
4153    def asrgb(self, *args, **kwargs):
4154        """Read image data from file and return RGB image as numpy array."""
4155        kwargs['validate'] = False
4156        return TiffPage.asrgb(self, *args, **kwargs)
4157
4158    @property
4159    def offsets_bytecounts(self):
4160        """Return simplified offsets and bytecounts."""
4161        if self.keyframe.is_contiguous:
4162            return self.dataoffsets[:1], self.keyframe.is_contiguous[1:]
4163        return clean_offsets_counts(self.dataoffsets, self.databytecounts)
4164
4165    @property
4166    def is_contiguous(self):
4167        """Return offset and size of contiguous data, else None."""
4168        if self.keyframe.is_contiguous:
4169            return self.dataoffsets[0], self.keyframe.is_contiguous[1]
4170
4171    @property
4172    def is_memmappable(self):
4173        """Return if page's image data in file can be memory-mapped."""
4174        return self.keyframe.is_memmappable
4175
4176    def __getattr__(self, name):
4177        """Return attribute from keyframe."""
4178        if name in TIFF.FRAME_ATTRS:
4179            return getattr(self.keyframe, name)
4180        # this error could be raised because an AttributeError was
4181        # raised inside a @property function
4182        raise AttributeError("'%s' object has no attribute '%s'" %
4183                             (self.__class__.__name__, name))
4184
4185    def __str__(self, detail=0):
4186        """Return string containing information about frame."""
4187        info = '  '.join(s for s in (
4188            'x'.join(str(i) for i in self.shape),
4189            str(self.dtype)))
4190        return 'TiffFrame %i @%i  %s' % (self.index, self.offset, info)
4191
4192
4193class TiffTag(object):
4194    """TIFF tag structure.
4195
4196    Attributes
4197    ----------
4198    name : string
4199        Name of tag.
4200    code : int
4201        Decimal code of tag.
4202    dtype : str
4203        Datatype of tag data. One of TIFF DATA_FORMATS.
4204    count : int
4205        Number of values.
4206    value : various types
4207        Tag data as Python object.
4208    ImageSourceData : int
4209        Location of value in file.
4210
4211    All attributes are read-only.
4212
4213    """
4214    __slots__ = ('code', 'count', 'dtype', 'value', 'valueoffset')
4215
4216    class Error(Exception):
4217        pass
4218
4219    def __init__(self, parent, tagheader, **kwargs):
4220        """Initialize instance from tag header."""
4221        fh = parent.filehandle
4222        byteorder = parent.byteorder
4223        unpack = struct.unpack
4224        offsetsize = parent.offsetsize
4225
4226        self.valueoffset = fh.tell() + offsetsize + 4
4227        code, type_ = unpack(parent.tagformat1, tagheader[:4])
4228        count, value = unpack(parent.tagformat2, tagheader[4:])
4229
4230        try:
4231            dtype = TIFF.DATA_FORMATS[type_]
4232        except KeyError:
4233            raise TiffTag.Error('unknown tag data type %i' % type_)
4234
4235        fmt = '%s%i%s' % (byteorder, count * int(dtype[0]), dtype[1])
4236        size = struct.calcsize(fmt)
4237        if size > offsetsize or code in TIFF.TAG_READERS:
4238            self.valueoffset = offset = unpack(parent.offsetformat, value)[0]
4239            if offset < 8 or offset > fh.size - size:
4240                raise TiffTag.Error('invalid tag value offset')
4241            # if offset % 2:
4242            #     warnings.warn('tag value does not begin on word boundary')
4243            fh.seek(offset)
4244            if code in TIFF.TAG_READERS:
4245                readfunc = TIFF.TAG_READERS[code]
4246                value = readfunc(fh, byteorder, dtype, count, offsetsize)
4247            elif type_ == 7 or (count > 1 and dtype[-1] == 'B'):
4248                value = read_bytes(fh, byteorder, dtype, count, offsetsize)
4249            elif code in TIFF.TAGS or dtype[-1] == 's':
4250                value = unpack(fmt, fh.read(size))
4251            else:
4252                value = read_numpy(fh, byteorder, dtype, count, offsetsize)
4253        elif dtype[-1] == 'B' or type_ == 7:
4254            value = value[:size]
4255        else:
4256            value = unpack(fmt, value[:size])
4257
4258        process = (code not in TIFF.TAG_READERS and code not in TIFF.TAG_TUPLE
4259                   and type_ != 7)
4260        if process and dtype[-1] == 's' and isinstance(value[0], bytes):
4261            # TIFF ASCII fields can contain multiple strings,
4262            #   each terminated with a NUL
4263            value = value[0]
4264            try:
4265                value = bytes2str(stripascii(value).strip())
4266            except UnicodeDecodeError:
4267                warnings.warn('tag %i: coercing invalid ASCII to bytes' % code)
4268                dtype = '1B'
4269        else:
4270            if code in TIFF.TAG_ENUM:
4271                t = TIFF.TAG_ENUM[code]
4272                try:
4273                    value = tuple(t(v) for v in value)
4274                except ValueError as e:
4275                    warnings.warn(str(e))
4276            if process:
4277                if len(value) == 1:
4278                    value = value[0]
4279
4280        self.code = code
4281        self.dtype = dtype
4282        self.count = count
4283        self.value = value
4284
4285    @property
4286    def name(self):
4287        return TIFF.TAGS.get(self.code, str(self.code))
4288
4289    def _fix_lsm_bitspersample(self, parent):
4290        """Correct LSM bitspersample tag.
4291
4292        Old LSM writers may use a separate region for two 16-bit values,
4293        although they fit into the tag value element of the tag.
4294
4295        """
4296        if self.code == 258 and self.count == 2:
4297            # TODO: test this case; need example file
4298            warnings.warn('correcting LSM bitspersample tag')
4299            tof = parent.offsetformat[parent.offsetsize]
4300            self.valueoffset = struct.unpack(tof, self._value)[0]
4301            parent.filehandle.seek(self.valueoffset)
4302            self.value = struct.unpack('<HH', parent.filehandle.read(4))
4303
4304    def __str__(self, detail=0, width=79):
4305        """Return string containing information about tag."""
4306        height = 1 if detail <= 0 else 8 * detail
4307        tcode = '%i%s' % (self.count * int(self.dtype[0]), self.dtype[1])
4308        line = 'TiffTag %i %s  %s @%i  ' % (
4309            self.code, self.name, tcode, self.valueoffset)[:width]
4310
4311        if self.code in TIFF.TAG_ENUM:
4312            if self.count == 1:
4313                value = TIFF.TAG_ENUM[self.code](self.value).name
4314            else:
4315                value = pformat(tuple(v.name for v in self.value))
4316        else:
4317            value = pformat(self.value, width=width, height=height)
4318
4319        if detail <= 0:
4320            line += value
4321            line = line[:width]
4322        else:
4323            line += '\n' + value
4324        return line
4325
4326
4327class TiffPageSeries(object):
4328    """Series of TIFF pages with compatible shape and data type.
4329
4330    Attributes
4331    ----------
4332    pages : list of TiffPage
4333        Sequence of TiffPages in series.
4334    dtype : numpy.dtype
4335        Data type (native byte order) of the image array in series.
4336    shape : tuple
4337        Dimensions of the image array in series.
4338    axes : str
4339        Labels of axes in shape. See TiffPage.axes.
4340    offset : int or None
4341        Position of image data in file if memory-mappable, else None.
4342
4343    """
4344    def __init__(self, pages, shape, dtype, axes, parent=None, name=None,
4345                 transform=None, stype=None, truncated=False):
4346        """Initialize instance."""
4347        self.index = 0
4348        self._pages = pages  # might contain only first of contiguous pages
4349        self.shape = tuple(shape)
4350        self.axes = ''.join(axes)
4351        self.dtype = numpy.dtype(dtype)
4352        self.stype = stype if stype else ''
4353        self.name = name if name else ''
4354        self.transform = transform
4355        if parent:
4356            self.parent = parent
4357        elif pages:
4358            self.parent = pages[0].parent
4359        else:
4360            self.parent = None
4361        if len(pages) == 1 and not truncated:
4362            self._len = int(product(self.shape) // product(pages[0].shape))
4363        else:
4364            self._len = len(pages)
4365
4366    def asarray(self, out=None):
4367        """Return image data from series of TIFF pages as numpy array."""
4368        if self.parent:
4369            result = self.parent.asarray(series=self, out=out)
4370            if self.transform is not None:
4371                result = self.transform(result)
4372            return result
4373
4374    @lazyattr
4375    def offset(self):
4376        """Return offset to series data in file, if any."""
4377        if not self._pages:
4378            return
4379
4380        pos = 0
4381        for page in self._pages:
4382            if page is None:
4383                return
4384            if not page.is_final:
4385                return
4386            if not pos:
4387                pos = page.is_contiguous[0] + page.is_contiguous[1]
4388                continue
4389            if pos != page.is_contiguous[0]:
4390                return
4391            pos += page.is_contiguous[1]
4392
4393        page = self._pages[0]
4394        offset = page.is_contiguous[0]
4395        if (page.is_imagej or page.is_shaped) and len(self._pages) == 1:
4396            # truncated files
4397            return offset
4398        if pos == offset + product(self.shape) * self.dtype.itemsize:
4399            return offset
4400
4401    @property
4402    def ndim(self):
4403        """Return number of array dimensions."""
4404        return len(self.shape)
4405
4406    @property
4407    def size(self):
4408        """Return number of elements in array."""
4409        return int(product(self.shape))
4410
4411    @property
4412    def pages(self):
4413        """Return sequence of all pages in series."""
4414        # a workaround to keep the old interface working
4415        return self
4416
4417    def __len__(self):
4418        """Return number of TiffPages in series."""
4419        return self._len
4420
4421    def __getitem__(self, key):
4422        """Return specified TiffPage."""
4423        if len(self._pages) == 1 and 0 < key < self._len:
4424            index = self._pages[0].index
4425            return self.parent.pages[index + key]
4426        return self._pages[key]
4427
4428    def __iter__(self):
4429        """Return iterator over TiffPages in series."""
4430        if len(self._pages) == self._len:
4431            for page in self._pages:
4432                yield page
4433        else:
4434            pages = self.parent.pages
4435            index = self._pages[0].index
4436            for i in range(self._len):
4437                yield pages[index + i]
4438
4439    def __str__(self):
4440        """Return string with information about series."""
4441        s = '  '.join(s for s in (
4442            snipstr("'%s'" % self.name, 20) if self.name else '',
4443            'x'.join(str(i) for i in self.shape),
4444            str(self.dtype),
4445            self.axes,
4446            self.stype,
4447            '%i Pages' % len(self.pages),
4448            ('Offset=%i' % self.offset) if self.offset else '') if s)
4449        return 'TiffPageSeries %i  %s' % (self.index, s)
4450
4451
4452class TiffSequence(object):
4453    """Sequence of TIFF files.
4454
4455    The image data in all files must match shape, dtype, etc.
4456
4457    Attributes
4458    ----------
4459    files : list
4460        List of file names.
4461    shape : tuple
4462        Shape of image sequence. Excludes shape of image array.
4463    axes : str
4464        Labels of axes in shape.
4465
4466    Examples
4467    --------
4468    >>> # read image stack from sequence of TIFF files
4469    >>> imsave('temp_C001T001.tif', numpy.random.rand(64, 64))
4470    >>> imsave('temp_C001T002.tif', numpy.random.rand(64, 64))
4471    >>> tifs = TiffSequence('temp_C001*.tif')
4472    >>> tifs.shape
4473    (1, 2)
4474    >>> tifs.axes
4475    'CT'
4476    >>> data = tifs.asarray()
4477    >>> data.shape
4478    (1, 2, 64, 64)
4479
4480    """
4481    _patterns = {
4482        'axes': r"""
4483            # matches Olympus OIF and Leica TIFF series
4484            _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))
4485            _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
4486            _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
4487            _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
4488            _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
4489            _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
4490            _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
4491            """}
4492
4493    class ParseError(Exception):
4494        pass
4495
4496    def __init__(self, files, imread=TiffFile, pattern='axes',
4497                 *args, **kwargs):
4498        """Initialize instance from multiple files.
4499
4500        Parameters
4501        ----------
4502        files : str, pathlib.Path, or sequence thereof
4503            Glob pattern or sequence of file names.
4504            Binary streams are not supported.
4505        imread : function or class
4506            Image read function or class with asarray function returning numpy
4507            array from single file.
4508        pattern : str
4509            Regular expression pattern that matches axes names and sequence
4510            indices in file names.
4511            By default, the pattern matches Olympus OIF and Leica TIFF series.
4512
4513        """
4514        if isinstance(files, pathlib.Path):
4515            files = str(files)
4516        if isinstance(files, basestring):
4517            files = natural_sorted(glob.glob(files))
4518        files = list(files)
4519        if not files:
4520            raise ValueError('no files found')
4521        if isinstance(files[0], pathlib.Path):
4522            files = [str(pathlib.Path(f)) for f in files]
4523        elif not isinstance(files[0], basestring):
4524            raise ValueError('not a file name')
4525        self.files = files
4526
4527        if hasattr(imread, 'asarray'):
4528            # redefine imread
4529            _imread = imread
4530
4531            def imread(fname, *args, **kwargs):
4532                with _imread(fname) as im:
4533                    return im.asarray(*args, **kwargs)
4534
4535        self.imread = imread
4536
4537        self.pattern = self._patterns.get(pattern, pattern)
4538        try:
4539            self._parse()
4540            if not self.axes:
4541                self.axes = 'I'
4542        except self.ParseError:
4543            self.axes = 'I'
4544            self.shape = (len(files),)
4545            self._startindex = (0,)
4546            self._indices = tuple((i,) for i in range(len(files)))
4547
4548    def __str__(self):
4549        """Return string with information about image sequence."""
4550        return '\n'.join([
4551            self.files[0],
4552            ' size: %i' % len(self.files),
4553            ' axes: %s' % self.axes,
4554            ' shape: %s' % str(self.shape)])
4555
4556    def __len__(self):
4557        return len(self.files)
4558
4559    def __enter__(self):
4560        return self
4561
4562    def __exit__(self, exc_type, exc_value, traceback):
4563        self.close()
4564
4565    def close(self):
4566        pass
4567
4568    def asarray(self, out=None, *args, **kwargs):
4569        """Read image data from all files and return as numpy array.
4570
4571        The args and kwargs parameters are passed to the imread function.
4572
4573        Raise IndexError or ValueError if image shapes do not match.
4574
4575        """
4576        im = self.imread(self.files[0], *args, **kwargs)
4577        shape = self.shape + im.shape
4578        result = create_output(out, shape, dtype=im.dtype)
4579        result = result.reshape(-1, *im.shape)
4580        for index, fname in zip(self._indices, self.files):
4581            index = [i-j for i, j in zip(index, self._startindex)]
4582            index = numpy.ravel_multi_index(index, self.shape)
4583            im = self.imread(fname, *args, **kwargs)
4584            result[index] = im
4585        result.shape = shape
4586        return result
4587
4588    def _parse(self):
4589        """Get axes and shape from file names."""
4590        if not self.pattern:
4591            raise self.ParseError('invalid pattern')
4592        pattern = re.compile(self.pattern, re.IGNORECASE | re.VERBOSE)
4593        matches = pattern.findall(self.files[0])
4594        if not matches:
4595            raise self.ParseError('pattern does not match file names')
4596        matches = matches[-1]
4597        if len(matches) % 2:
4598            raise self.ParseError('pattern does not match axis name and index')
4599        axes = ''.join(m for m in matches[::2] if m)
4600        if not axes:
4601            raise self.ParseError('pattern does not match file names')
4602
4603        indices = []
4604        for fname in self.files:
4605            matches = pattern.findall(fname)[-1]
4606            if axes != ''.join(m for m in matches[::2] if m):
4607                raise ValueError('axes do not match within the image sequence')
4608            indices.append([int(m) for m in matches[1::2] if m])
4609        shape = tuple(numpy.max(indices, axis=0))
4610        startindex = tuple(numpy.min(indices, axis=0))
4611        shape = tuple(i-j+1 for i, j in zip(shape, startindex))
4612        if product(shape) != len(self.files):
4613            warnings.warn('files are missing. Missing data are zeroed')
4614
4615        self.axes = axes.upper()
4616        self.shape = shape
4617        self._indices = indices
4618        self._startindex = startindex
4619
4620
4621class FileHandle(object):
4622    """Binary file handle.
4623
4624    A limited, special purpose file handler that can:
4625
4626    * handle embedded files (for CZI within CZI files)
4627    * re-open closed files (for multi-file formats, such as OME-TIFF)
4628    * read and write numpy arrays and records from file like objects
4629
4630    Only 'rb' and 'wb' modes are supported. Concurrently reading and writing
4631    of the same stream is untested.
4632
4633    When initialized from another file handle, do not use it unless this
4634    FileHandle is closed.
4635
4636    Attributes
4637    ----------
4638    name : str
4639        Name of the file.
4640    path : str
4641        Absolute path to file.
4642    size : int
4643        Size of file in bytes.
4644    is_file : bool
4645        If True, file has a filno and can be memory-mapped.
4646
4647    All attributes are read-only.
4648
4649    """
4650    __slots__ = ('_fh', '_file', '_mode', '_name', '_dir', '_lock',
4651                 '_offset', '_size', '_close', 'is_file')
4652
4653    def __init__(self, file, mode='rb', name=None, offset=None, size=None):
4654        """Initialize file handle from file name or another file handle.
4655
4656        Parameters
4657        ----------
4658        file : str, pathlib.Path, binary stream, or FileHandle
4659            File name or seekable binary stream, such as an open file
4660            or BytesIO.
4661        mode : str
4662            File open mode in case 'file' is a file name. Must be 'rb' or 'wb'.
4663        name : str
4664            Optional name of file in case 'file' is a binary stream.
4665        offset : int
4666            Optional start position of embedded file. By default, this is
4667            the current file position.
4668        size : int
4669            Optional size of embedded file. By default, this is the number
4670            of bytes from the 'offset' to the end of the file.
4671
4672        """
4673        self._file = file
4674        self._fh = None
4675        self._mode = mode
4676        self._name = name
4677        self._dir = ''
4678        self._offset = offset
4679        self._size = size
4680        self._close = True
4681        self.is_file = False
4682        self._lock = NullContext()
4683        self.open()
4684
4685    def open(self):
4686        """Open or re-open file."""
4687        if self._fh:
4688            return  # file is open
4689
4690        if isinstance(self._file, pathlib.Path):
4691            self._file = str(self._file)
4692        if isinstance(self._file, basestring):
4693            # file name
4694            self._file = os.path.realpath(self._file)
4695            self._dir, self._name = os.path.split(self._file)
4696            self._fh = open(self._file, self._mode)
4697            self._close = True
4698            if self._offset is None:
4699                self._offset = 0
4700        elif isinstance(self._file, FileHandle):
4701            # FileHandle
4702            self._fh = self._file._fh
4703            if self._offset is None:
4704                self._offset = 0
4705            self._offset += self._file._offset
4706            self._close = False
4707            if not self._name:
4708                if self._offset:
4709                    name, ext = os.path.splitext(self._file._name)
4710                    self._name = '%s@%i%s' % (name, self._offset, ext)
4711                else:
4712                    self._name = self._file._name
4713            if self._mode and self._mode != self._file._mode:
4714                raise ValueError('FileHandle has wrong mode')
4715            self._mode = self._file._mode
4716            self._dir = self._file._dir
4717        elif hasattr(self._file, 'seek'):
4718            # binary stream: open file, BytesIO
4719            try:
4720                self._file.tell()
4721            except Exception:
4722                raise ValueError('binary stream is not seekable')
4723            self._fh = self._file
4724            if self._offset is None:
4725                self._offset = self._file.tell()
4726            self._close = False
4727            if not self._name:
4728                try:
4729                    self._dir, self._name = os.path.split(self._fh.name)
4730                except AttributeError:
4731                    self._name = 'Unnamed binary stream'
4732            try:
4733                self._mode = self._fh.mode
4734            except AttributeError:
4735                pass
4736        else:
4737            raise ValueError('The first parameter must be a file name, '
4738                             'seekable binary stream, or FileHandle')
4739
4740        if self._offset:
4741            self._fh.seek(self._offset)
4742
4743        if self._size is None:
4744            pos = self._fh.tell()
4745            self._fh.seek(self._offset, 2)
4746            self._size = self._fh.tell()
4747            self._fh.seek(pos)
4748
4749        try:
4750            self._fh.fileno()
4751            self.is_file = True
4752        except Exception:
4753            self.is_file = False
4754
4755    def read(self, size=-1):
4756        """Read 'size' bytes from file, or until EOF is reached."""
4757        if size < 0 and self._offset:
4758            size = self._size
4759        return self._fh.read(size)
4760
4761    def write(self, bytestring):
4762        """Write bytestring to file."""
4763        return self._fh.write(bytestring)
4764
4765    def flush(self):
4766        """Flush write buffers if applicable."""
4767        return self._fh.flush()
4768
4769    def memmap_array(self, dtype, shape, offset=0, mode='r', order='C'):
4770        """Return numpy.memmap of data stored in file."""
4771        if not self.is_file:
4772            raise ValueError('Cannot memory-map file without fileno')
4773        return numpy.memmap(self._fh, dtype=dtype, mode=mode,
4774                            offset=self._offset + offset,
4775                            shape=shape, order=order)
4776
4777    def read_array(self, dtype, count=-1, sep='', chunksize=2**25, out=None,
4778                   native=False):
4779        """Return numpy array from file.
4780
4781        Work around numpy issue #2230, "numpy.fromfile does not accept
4782        StringIO object" https://github.com/numpy/numpy/issues/2230.
4783
4784        """
4785        fh = self._fh
4786        dtype = numpy.dtype(dtype)
4787        size = self._size if count < 0 else count * dtype.itemsize
4788
4789        if out is None:
4790            try:
4791                result = numpy.fromfile(fh, dtype, count, sep)
4792            except IOError:
4793                # ByteIO
4794                data = fh.read(size)
4795                result = numpy.frombuffer(data, dtype, count).copy()
4796            if native and not result.dtype.isnative:
4797                # swap byte order and dtype without copy
4798                result.byteswap(True)
4799                result = result.newbyteorder()
4800            return result
4801
4802        # Read data from file in chunks and copy to output array
4803        shape = out.shape
4804        size = min(out.nbytes, size)
4805        out = out.reshape(-1)
4806        index = 0
4807        while size > 0:
4808            data = fh.read(min(chunksize, size))
4809            datasize = len(data)
4810            if datasize == 0:
4811                break
4812            size -= datasize
4813            data = numpy.frombuffer(data, dtype)
4814            out[index:index+data.size] = data
4815            index += data.size
4816
4817        if hasattr(out, 'flush'):
4818            out.flush()
4819        return out.reshape(shape)
4820
4821    def read_record(self, dtype, shape=1, byteorder=None):
4822        """Return numpy record from file."""
4823        rec = numpy.rec
4824        try:
4825            record = rec.fromfile(self._fh, dtype, shape, byteorder=byteorder)
4826        except Exception:
4827            dtype = numpy.dtype(dtype)
4828            if shape is None:
4829                shape = self._size // dtype.itemsize
4830            size = product(sequence(shape)) * dtype.itemsize
4831            data = self._fh.read(size)
4832            record = rec.fromstring(data, dtype, shape, byteorder=byteorder)
4833        return record[0] if shape == 1 else record
4834
4835    def write_empty(self, size):
4836        """Append size bytes to file. Position must be at end of file."""
4837        if size < 1:
4838            return
4839        self._fh.seek(size-1, 1)
4840        self._fh.write(b'\x00')
4841
4842    def write_array(self, data):
4843        """Write numpy array to binary file."""
4844        try:
4845            data.tofile(self._fh)
4846        except Exception:
4847            # BytesIO
4848            self._fh.write(data.tostring())
4849
4850    def tell(self):
4851        """Return file's current position."""
4852        return self._fh.tell() - self._offset
4853
4854    def seek(self, offset, whence=0):
4855        """Set file's current position."""
4856        if self._offset:
4857            if whence == 0:
4858                self._fh.seek(self._offset + offset, whence)
4859                return
4860            elif whence == 2 and self._size > 0:
4861                self._fh.seek(self._offset + self._size + offset, 0)
4862                return
4863        self._fh.seek(offset, whence)
4864
4865    def close(self):
4866        """Close file."""
4867        if self._close and self._fh:
4868            self._fh.close()
4869            self._fh = None
4870
4871    def __enter__(self):
4872        return self
4873
4874    def __exit__(self, exc_type, exc_value, traceback):
4875        self.close()
4876
4877    def __getattr__(self, name):
4878        """Return attribute from underlying file object."""
4879        if self._offset:
4880            warnings.warn(
4881                "FileHandle: '%s' not implemented for embedded files" % name)
4882        return getattr(self._fh, name)
4883
4884    @property
4885    def name(self):
4886        return self._name
4887
4888    @property
4889    def dirname(self):
4890        return self._dir
4891
4892    @property
4893    def path(self):
4894        return os.path.join(self._dir, self._name)
4895
4896    @property
4897    def size(self):
4898        return self._size
4899
4900    @property
4901    def closed(self):
4902        return self._fh is None
4903
4904    @property
4905    def lock(self):
4906        return self._lock
4907
4908    @lock.setter
4909    def lock(self, value):
4910        self._lock = threading.RLock() if value else NullContext()
4911
4912
4913class NullContext(object):
4914    """Null context manager.
4915
4916    >>> with NullContext():
4917    ...     pass
4918
4919    """
4920    def __enter__(self):
4921        return self
4922
4923    def __exit__(self, exc_type, exc_value, traceback):
4924        pass
4925
4926
4927class OpenFileCache(object):
4928    """Keep files open."""
4929
4930    __slots__ = ('files', 'past', 'lock', 'size')
4931
4932    def __init__(self, size, lock=None):
4933        """Initialize open file cache."""
4934        self.past = []  # FIFO of opened files
4935        self.files = {}  # refcounts of opened files
4936        self.lock = NullContext() if lock is None else lock
4937        self.size = int(size)
4938
4939    def open(self, filehandle):
4940        """Re-open file if necessary."""
4941        with self.lock:
4942            if filehandle in self.files:
4943                self.files[filehandle] += 1
4944            elif filehandle.closed:
4945                filehandle.open()
4946                self.files[filehandle] = 1
4947                self.past.append(filehandle)
4948
4949    def close(self, filehandle):
4950        """Close openend file if no longer used."""
4951        with self.lock:
4952            if filehandle in self.files:
4953                self.files[filehandle] -= 1
4954                # trim the file cache
4955                index = 0
4956                size = len(self.past)
4957                while size > self.size and index < size:
4958                    filehandle = self.past[index]
4959                    if self.files[filehandle] == 0:
4960                        filehandle.close()
4961                        del self.files[filehandle]
4962                        del self.past[index]
4963                        size -= 1
4964                    else:
4965                        index += 1
4966
4967    def clear(self):
4968        """Close all opened files if not in use."""
4969        with self.lock:
4970            for filehandle, refcount in list(self.files.items()):
4971                if refcount == 0:
4972                    filehandle.close()
4973                    del self.files[filehandle]
4974                    del self.past[self.past.index(filehandle)]
4975
4976
4977class LazyConst(object):
4978    """Class whose attributes are computed on first access from its methods."""
4979    def __init__(self, cls):
4980        self._cls = cls
4981        self.__doc__ = getattr(cls, '__doc__')
4982
4983    def __getattr__(self, name):
4984        func = getattr(self._cls, name)
4985        if not callable(func):
4986            return func
4987        try:
4988            value = func()
4989        except TypeError:
4990            # Python 2 unbound method
4991            value = func.__func__()
4992        setattr(self, name, value)
4993        return value
4994
4995
4996@LazyConst
4997class TIFF(object):
4998    """Namespace for module constants."""
4999
5000    def TAGS():
5001        # TIFF tag codes and names from TIFF6, TIFF/EP, EXIF, and other specs
5002        return {
5003            11: 'ProcessingSoftware',
5004            254: 'NewSubfileType',
5005            255: 'SubfileType',
5006            256: 'ImageWidth',
5007            257: 'ImageLength',
5008            258: 'BitsPerSample',
5009            259: 'Compression',
5010            262: 'PhotometricInterpretation',
5011            263: 'Thresholding',
5012            264: 'CellWidth',
5013            265: 'CellLength',
5014            266: 'FillOrder',
5015            269: 'DocumentName',
5016            270: 'ImageDescription',
5017            271: 'Make',
5018            272: 'Model',
5019            273: 'StripOffsets',
5020            274: 'Orientation',
5021            277: 'SamplesPerPixel',
5022            278: 'RowsPerStrip',
5023            279: 'StripByteCounts',
5024            280: 'MinSampleValue',
5025            281: 'MaxSampleValue',
5026            282: 'XResolution',
5027            283: 'YResolution',
5028            284: 'PlanarConfiguration',
5029            285: 'PageName',
5030            286: 'XPosition',
5031            287: 'YPosition',
5032            288: 'FreeOffsets',
5033            289: 'FreeByteCounts',
5034            290: 'GrayResponseUnit',
5035            291: 'GrayResponseCurve',
5036            292: 'T4Options',
5037            293: 'T6Options',
5038            296: 'ResolutionUnit',
5039            297: 'PageNumber',
5040            300: 'ColorResponseUnit',
5041            301: 'TransferFunction',
5042            305: 'Software',
5043            306: 'DateTime',
5044            315: 'Artist',
5045            316: 'HostComputer',
5046            317: 'Predictor',
5047            318: 'WhitePoint',
5048            319: 'PrimaryChromaticities',
5049            320: 'ColorMap',
5050            321: 'HalftoneHints',
5051            322: 'TileWidth',
5052            323: 'TileLength',
5053            324: 'TileOffsets',
5054            325: 'TileByteCounts',
5055            326: 'BadFaxLines',
5056            327: 'CleanFaxData',
5057            328: 'ConsecutiveBadFaxLines',
5058            330: 'SubIFDs',
5059            332: 'InkSet',
5060            333: 'InkNames',
5061            334: 'NumberOfInks',
5062            336: 'DotRange',
5063            337: 'TargetPrinter',
5064            338: 'ExtraSamples',
5065            339: 'SampleFormat',
5066            340: 'SMinSampleValue',
5067            341: 'SMaxSampleValue',
5068            342: 'TransferRange',
5069            343: 'ClipPath',
5070            344: 'XClipPathUnits',
5071            345: 'YClipPathUnits',
5072            346: 'Indexed',
5073            347: 'JPEGTables',
5074            351: 'OPIProxy',
5075            400: 'GlobalParametersIFD',
5076            401: 'ProfileType',
5077            402: 'FaxProfile',
5078            403: 'CodingMethods',
5079            404: 'VersionYear',
5080            405: 'ModeNumber',
5081            433: 'Decode',
5082            434: 'DefaultImageColor',
5083            435: 'T82Options',
5084            437: 'JPEGTables_',  # 347
5085            512: 'JPEGProc',
5086            513: 'JPEGInterchangeFormat',
5087            514: 'JPEGInterchangeFormatLength',
5088            515: 'JPEGRestartInterval',
5089            517: 'JPEGLosslessPredictors',
5090            518: 'JPEGPointTransforms',
5091            519: 'JPEGQTables',
5092            520: 'JPEGDCTables',
5093            521: 'JPEGACTables',
5094            529: 'YCbCrCoefficients',
5095            530: 'YCbCrSubSampling',
5096            531: 'YCbCrPositioning',
5097            532: 'ReferenceBlackWhite',
5098            559: 'StripRowCounts',
5099            700: 'XMP',  # XMLPacket
5100            769: 'GDIGamma',  # GDI+
5101            770: 'ICCProfileDescriptor',  # GDI+
5102            771: 'SRGBRenderingIntent',  # GDI+
5103            800: 'ImageTitle',  # GDI+
5104            999: 'USPTO_Miscellaneous',
5105            4864: 'AndorId',  # TODO: Andor Technology 4864 - 5030
5106            4869: 'AndorTemperature',
5107            4876: 'AndorExposureTime',
5108            4878: 'AndorKineticCycleTime',
5109            4879: 'AndorAccumulations',
5110            4881: 'AndorAcquisitionCycleTime',
5111            4882: 'AndorReadoutTime',
5112            4884: 'AndorPhotonCounting',
5113            4885: 'AndorEmDacLevel',
5114            4890: 'AndorFrames',
5115            4896: 'AndorHorizontalFlip',
5116            4897: 'AndorVerticalFlip',
5117            4898: 'AndorClockwise',
5118            4899: 'AndorCounterClockwise',
5119            4904: 'AndorVerticalClockVoltage',
5120            4905: 'AndorVerticalShiftSpeed',
5121            4907: 'AndorPreAmpSetting',
5122            4908: 'AndorCameraSerial',
5123            4911: 'AndorActualTemperature',
5124            4912: 'AndorBaselineClamp',
5125            4913: 'AndorPrescans',
5126            4914: 'AndorModel',
5127            4915: 'AndorChipSizeX',
5128            4916: 'AndorChipSizeY',
5129            4944: 'AndorBaselineOffset',
5130            4966: 'AndorSoftwareVersion',
5131            18246: 'Rating',
5132            18247: 'XP_DIP_XML',
5133            18248: 'StitchInfo',
5134            18249: 'RatingPercent',
5135            20481: 'ResolutionXUnit',  # GDI+
5136            20482: 'ResolutionYUnit',  # GDI+
5137            20483: 'ResolutionXLengthUnit',  # GDI+
5138            20484: 'ResolutionYLengthUnit',  # GDI+
5139            20485: 'PrintFlags',  # GDI+
5140            20486: 'PrintFlagsVersion',  # GDI+
5141            20487: 'PrintFlagsCrop',  # GDI+
5142            20488: 'PrintFlagsBleedWidth',  # GDI+
5143            20489: 'PrintFlagsBleedWidthScale',  # GDI+
5144            20490: 'HalftoneLPI',  # GDI+
5145            20491: 'HalftoneLPIUnit',  # GDI+
5146            20492: 'HalftoneDegree',  # GDI+
5147            20493: 'HalftoneShape',  # GDI+
5148            20494: 'HalftoneMisc',  # GDI+
5149            20495: 'HalftoneScreen',  # GDI+
5150            20496: 'JPEGQuality',  # GDI+
5151            20497: 'GridSize',  # GDI+
5152            20498: 'ThumbnailFormat',  # GDI+
5153            20499: 'ThumbnailWidth',  # GDI+
5154            20500: 'ThumbnailHeight',  # GDI+
5155            20501: 'ThumbnailColorDepth',  # GDI+
5156            20502: 'ThumbnailPlanes',  # GDI+
5157            20503: 'ThumbnailRawBytes',  # GDI+
5158            20504: 'ThumbnailSize',  # GDI+
5159            20505: 'ThumbnailCompressedSize',  # GDI+
5160            20506: 'ColorTransferFunction',  # GDI+
5161            20507: 'ThumbnailData',
5162            20512: 'ThumbnailImageWidth',  # GDI+
5163            20513: 'ThumbnailImageHeight',  # GDI+
5164            20514: 'ThumbnailBitsPerSample',  # GDI+
5165            20515: 'ThumbnailCompression',
5166            20516: 'ThumbnailPhotometricInterp',  # GDI+
5167            20517: 'ThumbnailImageDescription',  # GDI+
5168            20518: 'ThumbnailEquipMake',  # GDI+
5169            20519: 'ThumbnailEquipModel',  # GDI+
5170            20520: 'ThumbnailStripOffsets',  # GDI+
5171            20521: 'ThumbnailOrientation',  # GDI+
5172            20522: 'ThumbnailSamplesPerPixel',  # GDI+
5173            20523: 'ThumbnailRowsPerStrip',  # GDI+
5174            20524: 'ThumbnailStripBytesCount',  # GDI+
5175            20525: 'ThumbnailResolutionX',
5176            20526: 'ThumbnailResolutionY',
5177            20527: 'ThumbnailPlanarConfig',  # GDI+
5178            20528: 'ThumbnailResolutionUnit',
5179            20529: 'ThumbnailTransferFunction',
5180            20530: 'ThumbnailSoftwareUsed',  # GDI+
5181            20531: 'ThumbnailDateTime',  # GDI+
5182            20532: 'ThumbnailArtist',  # GDI+
5183            20533: 'ThumbnailWhitePoint',  # GDI+
5184            20534: 'ThumbnailPrimaryChromaticities',  # GDI+
5185            20535: 'ThumbnailYCbCrCoefficients',  # GDI+
5186            20536: 'ThumbnailYCbCrSubsampling',  # GDI+
5187            20537: 'ThumbnailYCbCrPositioning',
5188            20538: 'ThumbnailRefBlackWhite',  # GDI+
5189            20539: 'ThumbnailCopyRight',  # GDI+
5190            20545: 'InteroperabilityIndex',
5191            20546: 'InteroperabilityVersion',
5192            20624: 'LuminanceTable',
5193            20625: 'ChrominanceTable',
5194            20736: 'FrameDelay',  # GDI+
5195            20737: 'LoopCount',  # GDI+
5196            20738: 'GlobalPalette',  # GDI+
5197            20739: 'IndexBackground',  # GDI+
5198            20740: 'IndexTransparent',  # GDI+
5199            20752: 'PixelUnit',  # GDI+
5200            20753: 'PixelPerUnitX',  # GDI+
5201            20754: 'PixelPerUnitY',  # GDI+
5202            20755: 'PaletteHistogram',  # GDI+
5203            28672: 'SonyRawFileType',  # Sony ARW
5204            28722: 'VignettingCorrParams',  # Sony ARW
5205            28725: 'ChromaticAberrationCorrParams',  # Sony ARW
5206            28727: 'DistortionCorrParams',  # Sony ARW
5207            # Private tags >= 32768
5208            32781: 'ImageID',
5209            32931: 'WangTag1',
5210            32932: 'WangAnnotation',
5211            32933: 'WangTag3',
5212            32934: 'WangTag4',
5213            32953: 'ImageReferencePoints',
5214            32954: 'RegionXformTackPoint',
5215            32955: 'WarpQuadrilateral',
5216            32956: 'AffineTransformMat',
5217            32995: 'Matteing',
5218            32996: 'DataType',
5219            32997: 'ImageDepth',
5220            32998: 'TileDepth',
5221            33300: 'ImageFullWidth',
5222            33301: 'ImageFullLength',
5223            33302: 'TextureFormat',
5224            33303: 'TextureWrapModes',
5225            33304: 'FieldOfViewCotangent',
5226            33305: 'MatrixWorldToScreen',
5227            33306: 'MatrixWorldToCamera',
5228            33405: 'Model2',
5229            33421: 'CFARepeatPatternDim',
5230            33422: 'CFAPattern',
5231            33423: 'BatteryLevel',
5232            33424: 'KodakIFD',
5233            33434: 'ExposureTime',
5234            33437: 'FNumber',
5235            33432: 'Copyright',
5236            33445: 'MDFileTag',
5237            33446: 'MDScalePixel',
5238            33447: 'MDColorTable',
5239            33448: 'MDLabName',
5240            33449: 'MDSampleInfo',
5241            33450: 'MDPrepDate',
5242            33451: 'MDPrepTime',
5243            33452: 'MDFileUnits',
5244            33550: 'ModelPixelScaleTag',
5245            33589: 'AdventScale',
5246            33590: 'AdventRevision',
5247            33628: 'UIC1tag',  # Metamorph  Universal Imaging Corp STK
5248            33629: 'UIC2tag',
5249            33630: 'UIC3tag',
5250            33631: 'UIC4tag',
5251            33723: 'IPTCNAA',
5252            33858: 'ExtendedTagsOffset',  # DEFF points IFD with private tags
5253            33918: 'IntergraphPacketData',  # INGRPacketDataTag
5254            33919: 'IntergraphFlagRegisters',  # INGRFlagRegisters
5255            33920: 'IntergraphMatrixTag',  # IrasBTransformationMatrix
5256            33921: 'INGRReserved',
5257            33922: 'ModelTiepointTag',
5258            33923: 'LeicaMagic',
5259            34016: 'Site',
5260            34017: 'ColorSequence',
5261            34018: 'IT8Header',
5262            34019: 'RasterPadding',
5263            34020: 'BitsPerRunLength',
5264            34021: 'BitsPerExtendedRunLength',
5265            34022: 'ColorTable',
5266            34023: 'ImageColorIndicator',
5267            34024: 'BackgroundColorIndicator',
5268            34025: 'ImageColorValue',
5269            34026: 'BackgroundColorValue',
5270            34027: 'PixelIntensityRange',
5271            34028: 'TransparencyIndicator',
5272            34029: 'ColorCharacterization',
5273            34030: 'HCUsage',
5274            34031: 'TrapIndicator',
5275            34032: 'CMYKEquivalent',
5276            34118: 'CZ_SEM',  # Zeiss SEM
5277            34152: 'AFCP_IPTC',
5278            34232: 'PixelMagicJBIGOptions',
5279            34263: 'JPLCartoIFD',
5280            34122: 'IPLAB',  # number of images
5281            34264: 'ModelTransformationTag',
5282            34306: 'WB_GRGBLevels',  # Leaf MOS
5283            34310: 'LeafData',
5284            34361: 'MM_Header',
5285            34362: 'MM_Stamp',
5286            34363: 'MM_Unknown',
5287            34377: 'ImageResources',  # Photoshop
5288            34386: 'MM_UserBlock',
5289            34412: 'CZ_LSMINFO',
5290            34665: 'ExifTag',
5291            34675: 'InterColorProfile',  # ICCProfile
5292            34680: 'FEI_SFEG',  #
5293            34682: 'FEI_HELIOS',  #
5294            34683: 'FEI_TITAN',  #
5295            34687: 'FXExtensions',
5296            34688: 'MultiProfiles',
5297            34689: 'SharedData',
5298            34690: 'T88Options',
5299            34710: 'MarCCD',  # offset to MarCCD header
5300            34732: 'ImageLayer',
5301            34735: 'GeoKeyDirectoryTag',
5302            34736: 'GeoDoubleParamsTag',
5303            34737: 'GeoAsciiParamsTag',
5304            34750: 'JBIGOptions',
5305            34821: 'PIXTIFF',  # ? Pixel Translations Inc
5306            34850: 'ExposureProgram',
5307            34852: 'SpectralSensitivity',
5308            34853: 'GPSTag',  # GPSIFD
5309            34855: 'ISOSpeedRatings',
5310            34856: 'OECF',
5311            34857: 'Interlace',
5312            34858: 'TimeZoneOffset',
5313            34859: 'SelfTimerMode',
5314            34864: 'SensitivityType',
5315            34865: 'StandardOutputSensitivity',
5316            34866: 'RecommendedExposureIndex',
5317            34867: 'ISOSpeed',
5318            34868: 'ISOSpeedLatitudeyyy',
5319            34869: 'ISOSpeedLatitudezzz',
5320            34908: 'HylaFAXFaxRecvParams',
5321            34909: 'HylaFAXFaxSubAddress',
5322            34910: 'HylaFAXFaxRecvTime',
5323            34911: 'FaxDcs',
5324            34929: 'FedexEDR',
5325            34954: 'LeafSubIFD',
5326            34959: 'Aphelion1',
5327            34960: 'Aphelion2',
5328            34961: 'AphelionInternal',  # ADCIS
5329            36864: 'ExifVersion',
5330            36867: 'DateTimeOriginal',
5331            36868: 'DateTimeDigitized',
5332            36873: 'GooglePlusUploadCode',
5333            36880: 'OffsetTime',
5334            36881: 'OffsetTimeOriginal',
5335            36882: 'OffsetTimeDigitized',
5336            # TODO: Pilatus/CHESS/TV6 36864..37120 conflicting with Exif tags
5337            # 36864: 'TVX ?',
5338            # 36865: 'TVX_NumExposure',
5339            # 36866: 'TVX_NumBackground',
5340            # 36867: 'TVX_ExposureTime',
5341            # 36868: 'TVX_BackgroundTime',
5342            # 36870: 'TVX ?',
5343            # 36873: 'TVX_SubBpp',
5344            # 36874: 'TVX_SubWide',
5345            # 36875: 'TVX_SubHigh',
5346            # 36876: 'TVX_BlackLevel',
5347            # 36877: 'TVX_DarkCurrent',
5348            # 36878: 'TVX_ReadNoise',
5349            # 36879: 'TVX_DarkCurrentNoise',
5350            # 36880: 'TVX_BeamMonitor',
5351            # 37120: 'TVX_UserVariables',  # A/D values
5352            37121: 'ComponentsConfiguration',
5353            37122: 'CompressedBitsPerPixel',
5354            37377: 'ShutterSpeedValue',
5355            37378: 'ApertureValue',
5356            37379: 'BrightnessValue',
5357            37380: 'ExposureBiasValue',
5358            37381: 'MaxApertureValue',
5359            37382: 'SubjectDistance',
5360            37383: 'MeteringMode',
5361            37384: 'LightSource',
5362            37385: 'Flash',
5363            37386: 'FocalLength',
5364            37387: 'FlashEnergy_',  # 37387
5365            37388: 'SpatialFrequencyResponse_',  # 37388
5366            37389: 'Noise',
5367            37390: 'FocalPlaneXResolution',
5368            37391: 'FocalPlaneYResolution',
5369            37392: 'FocalPlaneResolutionUnit',
5370            37393: 'ImageNumber',
5371            37394: 'SecurityClassification',
5372            37395: 'ImageHistory',
5373            37396: 'SubjectLocation',
5374            37397: 'ExposureIndex',
5375            37398: 'TIFFEPStandardID',
5376            37399: 'SensingMethod',
5377            37434: 'CIP3DataFile',
5378            37435: 'CIP3Sheet',
5379            37436: 'CIP3Side',
5380            37439: 'StoNits',
5381            37500: 'MakerNote',
5382            37510: 'UserComment',
5383            37520: 'SubsecTime',
5384            37521: 'SubsecTimeOriginal',
5385            37522: 'SubsecTimeDigitized',
5386            37679: 'MODIText',  # Microsoft Office Document Imaging
5387            37680: 'MODIOLEPropertySetStorage',
5388            37681: 'MODIPositioning',
5389            37706: 'TVIPS',  # offset to TemData structure
5390            37707: 'TVIPS1',
5391            37708: 'TVIPS2',  # same TemData structure as undefined
5392            37724: 'ImageSourceData',  # Photoshop
5393            37888: 'Temperature',
5394            37889: 'Humidity',
5395            37890: 'Pressure',
5396            37891: 'WaterDepth',
5397            37892: 'Acceleration',
5398            37893: 'CameraElevationAngle',
5399            40001: 'MC_IpWinScal',  # Media Cybernetics
5400            40100: 'MC_IdOld',
5401            40965: 'InteroperabilityTag',  # InteropOffset
5402            40091: 'XPTitle',
5403            40092: 'XPComment',
5404            40093: 'XPAuthor',
5405            40094: 'XPKeywords',
5406            40095: 'XPSubject',
5407            40960: 'FlashpixVersion',
5408            40961: 'ColorSpace',
5409            40962: 'PixelXDimension',
5410            40963: 'PixelYDimension',
5411            40964: 'RelatedSoundFile',
5412            40976: 'SamsungRawPointersOffset',
5413            40977: 'SamsungRawPointersLength',
5414            41217: 'SamsungRawByteOrder',
5415            41218: 'SamsungRawUnknown',
5416            41483: 'FlashEnergy',
5417            41484: 'SpatialFrequencyResponse',
5418            41485: 'Noise_',  # 37389
5419            41486: 'FocalPlaneXResolution_',  # 37390
5420            41487: 'FocalPlaneYResolution_',  # 37391
5421            41488: 'FocalPlaneResolutionUnit_',  # 37392
5422            41489: 'ImageNumber_',  # 37393
5423            41490: 'SecurityClassification_',  # 37394
5424            41491: 'ImageHistory_',  # 37395
5425            41492: 'SubjectLocation_',  # 37395
5426            41493: 'ExposureIndex_ ',  # 37397
5427            41494: 'TIFF-EPStandardID',
5428            41495: 'SensingMethod_',  # 37399
5429            41728: 'FileSource',
5430            41729: 'SceneType',
5431            41730: 'CFAPattern_',  # 33422
5432            41985: 'CustomRendered',
5433            41986: 'ExposureMode',
5434            41987: 'WhiteBalance',
5435            41988: 'DigitalZoomRatio',
5436            41989: 'FocalLengthIn35mmFilm',
5437            41990: 'SceneCaptureType',
5438            41991: 'GainControl',
5439            41992: 'Contrast',
5440            41993: 'Saturation',
5441            41994: 'Sharpness',
5442            41995: 'DeviceSettingDescription',
5443            41996: 'SubjectDistanceRange',
5444            42016: 'ImageUniqueID',
5445            42032: 'CameraOwnerName',
5446            42033: 'BodySerialNumber',
5447            42034: 'LensSpecification',
5448            42035: 'LensMake',
5449            42036: 'LensModel',
5450            42037: 'LensSerialNumber',
5451            42112: 'GDAL_METADATA',
5452            42113: 'GDAL_NODATA',
5453            42240: 'Gamma',
5454            43314: 'NIHImageHeader',
5455            44992: 'ExpandSoftware',
5456            44993: 'ExpandLens',
5457            44994: 'ExpandFilm',
5458            44995: 'ExpandFilterLens',
5459            44996: 'ExpandScanner',
5460            44997: 'ExpandFlashLamp',
5461            48129: 'PixelFormat',  # HDP and WDP
5462            48130: 'Transformation',
5463            48131: 'Uncompressed',
5464            48132: 'ImageType',
5465            48256: 'ImageWidth_',  # 256
5466            48257: 'ImageHeight_',
5467            48258: 'WidthResolution',
5468            48259: 'HeightResolution',
5469            48320: 'ImageOffset',
5470            48321: 'ImageByteCount',
5471            48322: 'AlphaOffset',
5472            48323: 'AlphaByteCount',
5473            48324: 'ImageDataDiscard',
5474            48325: 'AlphaDataDiscard',
5475            50215: 'OceScanjobDescription',
5476            50216: 'OceApplicationSelector',
5477            50217: 'OceIdentificationNumber',
5478            50218: 'OceImageLogicCharacteristics',
5479            50255: 'Annotations',
5480            50288: 'MC_Id',  # Media Cybernetics
5481            50289: 'MC_XYPosition',
5482            50290: 'MC_ZPosition',
5483            50291: 'MC_XYCalibration',
5484            50292: 'MC_LensCharacteristics',
5485            50293: 'MC_ChannelName',
5486            50294: 'MC_ExcitationWavelength',
5487            50295: 'MC_TimeStamp',
5488            50296: 'MC_FrameProperties',
5489            50341: 'PrintImageMatching',
5490            50495: 'PCO_RAW',  # TODO: PCO CamWare
5491            50547: 'OriginalFileName',
5492            50560: 'USPTO_OriginalContentType',  # US Patent Office
5493            50561: 'USPTO_RotationCode',
5494            50656: 'CR2CFAPattern',
5495            50706: 'DNGVersion',  # DNG 50706 .. 51112
5496            50707: 'DNGBackwardVersion',
5497            50708: 'UniqueCameraModel',
5498            50709: 'LocalizedCameraModel',
5499            50710: 'CFAPlaneColor',
5500            50711: 'CFALayout',
5501            50712: 'LinearizationTable',
5502            50713: 'BlackLevelRepeatDim',
5503            50714: 'BlackLevel',
5504            50715: 'BlackLevelDeltaH',
5505            50716: 'BlackLevelDeltaV',
5506            50717: 'WhiteLevel',
5507            50718: 'DefaultScale',
5508            50719: 'DefaultCropOrigin',
5509            50720: 'DefaultCropSize',
5510            50721: 'ColorMatrix1',
5511            50722: 'ColorMatrix2',
5512            50723: 'CameraCalibration1',
5513            50724: 'CameraCalibration2',
5514            50725: 'ReductionMatrix1',
5515            50726: 'ReductionMatrix2',
5516            50727: 'AnalogBalance',
5517            50728: 'AsShotNeutral',
5518            50729: 'AsShotWhiteXY',
5519            50730: 'BaselineExposure',
5520            50731: 'BaselineNoise',
5521            50732: 'BaselineSharpness',
5522            50733: 'BayerGreenSplit',
5523            50734: 'LinearResponseLimit',
5524            50735: 'CameraSerialNumber',
5525            50736: 'LensInfo',
5526            50737: 'ChromaBlurRadius',
5527            50738: 'AntiAliasStrength',
5528            50739: 'ShadowScale',
5529            50740: 'DNGPrivateData',
5530            50741: 'MakerNoteSafety',
5531            50752: 'RawImageSegmentation',
5532            50778: 'CalibrationIlluminant1',
5533            50779: 'CalibrationIlluminant2',
5534            50780: 'BestQualityScale',
5535            50781: 'RawDataUniqueID',
5536            50784: 'AliasLayerMetadata',
5537            50827: 'OriginalRawFileName',
5538            50828: 'OriginalRawFileData',
5539            50829: 'ActiveArea',
5540            50830: 'MaskedAreas',
5541            50831: 'AsShotICCProfile',
5542            50832: 'AsShotPreProfileMatrix',
5543            50833: 'CurrentICCProfile',
5544            50834: 'CurrentPreProfileMatrix',
5545            50838: 'IJMetadataByteCounts',
5546            50839: 'IJMetadata',
5547            50844: 'RPCCoefficientTag',
5548            50879: 'ColorimetricReference',
5549            50885: 'SRawType',
5550            50898: 'PanasonicTitle',
5551            50899: 'PanasonicTitle2',
5552            50931: 'CameraCalibrationSignature',
5553            50932: 'ProfileCalibrationSignature',
5554            50933: 'ProfileIFD',
5555            50934: 'AsShotProfileName',
5556            50935: 'NoiseReductionApplied',
5557            50936: 'ProfileName',
5558            50937: 'ProfileHueSatMapDims',
5559            50938: 'ProfileHueSatMapData1',
5560            50939: 'ProfileHueSatMapData2',
5561            50940: 'ProfileToneCurve',
5562            50941: 'ProfileEmbedPolicy',
5563            50942: 'ProfileCopyright',
5564            50964: 'ForwardMatrix1',
5565            50965: 'ForwardMatrix2',
5566            50966: 'PreviewApplicationName',
5567            50967: 'PreviewApplicationVersion',
5568            50968: 'PreviewSettingsName',
5569            50969: 'PreviewSettingsDigest',
5570            50970: 'PreviewColorSpace',
5571            50971: 'PreviewDateTime',
5572            50972: 'RawImageDigest',
5573            50973: 'OriginalRawFileDigest',
5574            50974: 'SubTileBlockSize',
5575            50975: 'RowInterleaveFactor',
5576            50981: 'ProfileLookTableDims',
5577            50982: 'ProfileLookTableData',
5578            51008: 'OpcodeList1',
5579            51009: 'OpcodeList2',
5580            51022: 'OpcodeList3',
5581            51023: 'FibicsXML',  #
5582            51041: 'NoiseProfile',
5583            51043: 'TimeCodes',
5584            51044: 'FrameRate',
5585            51058: 'TStop',
5586            51081: 'ReelName',
5587            51089: 'OriginalDefaultFinalSize',
5588            51090: 'OriginalBestQualitySize',
5589            51091: 'OriginalDefaultCropSize',
5590            51105: 'CameraLabel',
5591            51107: 'ProfileHueSatMapEncoding',
5592            51108: 'ProfileLookTableEncoding',
5593            51109: 'BaselineExposureOffset',
5594            51110: 'DefaultBlackRender',
5595            51111: 'NewRawImageDigest',
5596            51112: 'RawToPreviewGain',
5597            51125: 'DefaultUserCrop',
5598            51123: 'MicroManagerMetadata',
5599            59932: 'Padding',
5600            59933: 'OffsetSchema',
5601            # Reusable Tags 65000-65535
5602            # 65000:  Dimap_Document XML
5603            # 65000-65112:  Photoshop Camera RAW EXIF tags
5604            # 65000: 'OwnerName',
5605            # 65001: 'SerialNumber',
5606            # 65002: 'Lens',
5607            # 65024: 'KDC_IFD',
5608            # 65100: 'RawFile',
5609            # 65101: 'Converter',
5610            # 65102: 'WhiteBalance',
5611            # 65105: 'Exposure',
5612            # 65106: 'Shadows',
5613            # 65107: 'Brightness',
5614            # 65108: 'Contrast',
5615            # 65109: 'Saturation',
5616            # 65110: 'Sharpness',
5617            # 65111: 'Smoothness',
5618            # 65112: 'MoireFilter',
5619            65200: 'FlexXML',  #
5620            65563: 'PerSample',
5621        }
5622
5623    def TAG_NAMES():
5624        return {v: c for c, v in TIFF.TAGS.items()}
5625
5626    def TAG_READERS():
5627        # Map TIFF tag codes to import functions
5628        return {
5629            320: read_colormap,
5630            # 700: read_bytes,  # read_utf8,
5631            # 34377: read_bytes,
5632            33723: read_bytes,
5633            # 34675: read_bytes,
5634            33628: read_uic1tag,  # Universal Imaging Corp STK
5635            33629: read_uic2tag,
5636            33630: read_uic3tag,
5637            33631: read_uic4tag,
5638            34118: read_cz_sem,  # Carl Zeiss SEM
5639            34361: read_mm_header,  # Olympus FluoView
5640            34362: read_mm_stamp,
5641            34363: read_numpy,  # MM_Unknown
5642            34386: read_numpy,  # MM_UserBlock
5643            34412: read_cz_lsminfo,  # Carl Zeiss LSM
5644            34680: read_fei_metadata,  # S-FEG
5645            34682: read_fei_metadata,  # Helios NanoLab
5646            37706: read_tvips_header,  # TVIPS EMMENU
5647            37724: read_bytes,  # ImageSourceData
5648            33923: read_bytes,  # read_leica_magic
5649            43314: read_nih_image_header,
5650            # 40001: read_bytes,
5651            40100: read_bytes,
5652            50288: read_bytes,
5653            50296: read_bytes,
5654            50839: read_bytes,
5655            51123: read_json,
5656            34665: read_exif_ifd,
5657            34853: read_gps_ifd,
5658            40965: read_interoperability_ifd,
5659        }
5660
5661    def TAG_TUPLE():
5662        # Tags whose values must be stored as tuples
5663        return frozenset((273, 279, 324, 325, 530, 531, 34736))
5664
5665    def TAG_ATTRIBUTES():
5666        #  Map tag codes to TiffPage attribute names
5667        return {
5668            'ImageWidth': 'imagewidth',
5669            'ImageLength': 'imagelength',
5670            'BitsPerSample': 'bitspersample',
5671            'Compression': 'compression',
5672            'PlanarConfiguration': 'planarconfig',
5673            'FillOrder': 'fillorder',
5674            'PhotometricInterpretation': 'photometric',
5675            'ColorMap': 'colormap',
5676            'ImageDescription': 'description',
5677            'ImageDescription1': 'description1',
5678            'SamplesPerPixel': 'samplesperpixel',
5679            'RowsPerStrip': 'rowsperstrip',
5680            'Software': 'software',
5681            'Predictor': 'predictor',
5682            'TileWidth': 'tilewidth',
5683            'TileLength': 'tilelength',
5684            'ExtraSamples': 'extrasamples',
5685            'SampleFormat': 'sampleformat',
5686            'ImageDepth': 'imagedepth',
5687            'TileDepth': 'tiledepth',
5688        }
5689
5690    def TAG_ENUM():
5691        return {
5692            # 254: TIFF.FILETYPE,
5693            255: TIFF.OFILETYPE,
5694            259: TIFF.COMPRESSION,
5695            262: TIFF.PHOTOMETRIC,
5696            263: TIFF.THRESHHOLD,
5697            266: TIFF.FILLORDER,
5698            274: TIFF.ORIENTATION,
5699            284: TIFF.PLANARCONFIG,
5700            290: TIFF.GRAYRESPONSEUNIT,
5701            # 292: TIFF.GROUP3OPT,
5702            # 293: TIFF.GROUP4OPT,
5703            296: TIFF.RESUNIT,
5704            300: TIFF.COLORRESPONSEUNIT,
5705            317: TIFF.PREDICTOR,
5706            338: TIFF.EXTRASAMPLE,
5707            339: TIFF.SAMPLEFORMAT,
5708            # 512: TIFF.JPEGPROC,
5709            # 531: TIFF.YCBCRPOSITION,
5710        }
5711
5712    def FILETYPE():
5713        class FILETYPE(enum.IntFlag):
5714            # Python 3.6 only
5715            UNDEFINED = 0
5716            REDUCEDIMAGE = 1
5717            PAGE = 2
5718            MASK = 4
5719        return FILETYPE
5720
5721    def OFILETYPE():
5722        class OFILETYPE(enum.IntEnum):
5723            UNDEFINED = 0
5724            IMAGE = 1
5725            REDUCEDIMAGE = 2
5726            PAGE = 3
5727        return OFILETYPE
5728
5729    def COMPRESSION():
5730        class COMPRESSION(enum.IntEnum):
5731            NONE = 1  # Uncompressed
5732            CCITTRLE = 2  # CCITT 1D
5733            CCITT_T4 = 3  # 'T4/Group 3 Fax',
5734            CCITT_T6 = 4  # 'T6/Group 4 Fax',
5735            LZW = 5
5736            OJPEG = 6  # old-style JPEG
5737            JPEG = 7
5738            ADOBE_DEFLATE = 8
5739            JBIG_BW = 9
5740            JBIG_COLOR = 10
5741            JPEG_99 = 99
5742            KODAK_262 = 262
5743            NEXT = 32766
5744            SONY_ARW = 32767
5745            PACKED_RAW = 32769
5746            SAMSUNG_SRW = 32770
5747            CCIRLEW = 32771
5748            SAMSUNG_SRW2 = 32772
5749            PACKBITS = 32773
5750            THUNDERSCAN = 32809
5751            IT8CTPAD = 32895
5752            IT8LW = 32896
5753            IT8MP = 32897
5754            IT8BL = 32898
5755            PIXARFILM = 32908
5756            PIXARLOG = 32909
5757            DEFLATE = 32946
5758            DCS = 32947
5759            APERIO_JP2000_YCBC = 33003  # Leica Aperio
5760            APERIO_JP2000_RGB = 33005  # Leica Aperio
5761            JBIG = 34661
5762            SGILOG = 34676
5763            SGILOG24 = 34677
5764            JPEG2000 = 34712
5765            NIKON_NEF = 34713
5766            JBIG2 = 34715
5767            MDI_BINARY = 34718  # 'Microsoft Document Imaging
5768            MDI_PROGRESSIVE = 34719  # 'Microsoft Document Imaging
5769            MDI_VECTOR = 34720  # 'Microsoft Document Imaging
5770            JPEG_LOSSY = 34892
5771            LZMA = 34925
5772            ZSTD = 34926
5773            OPS_PNG = 34933  # Objective Pathology Services
5774            OPS_JPEGXR = 34934  # Objective Pathology Services
5775            PIXTIFF = 50013
5776            KODAK_DCR = 65000
5777            PENTAX_PEF = 65535
5778            # def __bool__(self): return self != 1  # Python 3.6 only
5779        return COMPRESSION
5780
5781    def PHOTOMETRIC():
5782        class PHOTOMETRIC(enum.IntEnum):
5783            MINISWHITE = 0
5784            MINISBLACK = 1
5785            RGB = 2
5786            PALETTE = 3
5787            MASK = 4
5788            SEPARATED = 5  # CMYK
5789            YCBCR = 6
5790            CIELAB = 8
5791            ICCLAB = 9
5792            ITULAB = 10
5793            CFA = 32803  # Color Filter Array
5794            LOGL = 32844
5795            LOGLUV = 32845
5796            LINEAR_RAW = 34892
5797        return PHOTOMETRIC
5798
5799    def THRESHHOLD():
5800        class THRESHHOLD(enum.IntEnum):
5801            BILEVEL = 1
5802            HALFTONE = 2
5803            ERRORDIFFUSE = 3
5804        return THRESHHOLD
5805
5806    def FILLORDER():
5807        class FILLORDER(enum.IntEnum):
5808            MSB2LSB = 1
5809            LSB2MSB = 2
5810        return FILLORDER
5811
5812    def ORIENTATION():
5813        class ORIENTATION(enum.IntEnum):
5814            TOPLEFT = 1
5815            TOPRIGHT = 2
5816            BOTRIGHT = 3
5817            BOTLEFT = 4
5818            LEFTTOP = 5
5819            RIGHTTOP = 6
5820            RIGHTBOT = 7
5821            LEFTBOT = 8
5822        return ORIENTATION
5823
5824    def PLANARCONFIG():
5825        class PLANARCONFIG(enum.IntEnum):
5826            CONTIG = 1
5827            SEPARATE = 2
5828        return PLANARCONFIG
5829
5830    def GRAYRESPONSEUNIT():
5831        class GRAYRESPONSEUNIT(enum.IntEnum):
5832            _10S = 1
5833            _100S = 2
5834            _1000S = 3
5835            _10000S = 4
5836            _100000S = 5
5837        return GRAYRESPONSEUNIT
5838
5839    def GROUP4OPT():
5840        class GROUP4OPT(enum.IntEnum):
5841            UNCOMPRESSED = 2
5842        return GROUP4OPT
5843
5844    def RESUNIT():
5845        class RESUNIT(enum.IntEnum):
5846            NONE = 1
5847            INCH = 2
5848            CENTIMETER = 3
5849            # def __bool__(self): return self != 1  # Python 3.6 only
5850        return RESUNIT
5851
5852    def COLORRESPONSEUNIT():
5853        class COLORRESPONSEUNIT(enum.IntEnum):
5854            _10S = 1
5855            _100S = 2
5856            _1000S = 3
5857            _10000S = 4
5858            _100000S = 5
5859        return COLORRESPONSEUNIT
5860
5861    def PREDICTOR():
5862        class PREDICTOR(enum.IntEnum):
5863            NONE = 1
5864            HORIZONTAL = 2
5865            FLOATINGPOINT = 3
5866            # def __bool__(self): return self != 1  # Python 3.6 only
5867        return PREDICTOR
5868
5869    def EXTRASAMPLE():
5870        class EXTRASAMPLE(enum.IntEnum):
5871            UNSPECIFIED = 0
5872            ASSOCALPHA = 1
5873            UNASSALPHA = 2
5874        return EXTRASAMPLE
5875
5876    def SAMPLEFORMAT():
5877        class SAMPLEFORMAT(enum.IntEnum):
5878            UINT = 1
5879            INT = 2
5880            IEEEFP = 3
5881            VOID = 4
5882            COMPLEXINT = 5
5883            COMPLEXIEEEFP = 6
5884        return SAMPLEFORMAT
5885
5886    def DATATYPES():
5887        class DATATYPES(enum.IntEnum):
5888            NOTYPE = 0
5889            BYTE = 1
5890            ASCII = 2
5891            SHORT = 3
5892            LONG = 4
5893            RATIONAL = 5
5894            SBYTE = 6
5895            UNDEFINED = 7
5896            SSHORT = 8
5897            SLONG = 9
5898            SRATIONAL = 10
5899            FLOAT = 11
5900            DOUBLE = 12
5901            IFD = 13
5902            UNICODE = 14
5903            COMPLEX = 15
5904            LONG8 = 16
5905            SLONG8 = 17
5906            IFD8 = 18
5907        return DATATYPES
5908
5909    def DATA_FORMATS():
5910        # Map TIFF DATATYPES to Python struct formats
5911        return {
5912            1: '1B',   # BYTE 8-bit unsigned integer.
5913            2: '1s',   # ASCII 8-bit byte that contains a 7-bit ASCII code;
5914                       #   the last byte must be NULL (binary zero).
5915            3: '1H',   # SHORT 16-bit (2-byte) unsigned integer
5916            4: '1I',   # LONG 32-bit (4-byte) unsigned integer.
5917            5: '2I',   # RATIONAL Two LONGs: the first represents the numerator
5918                       #   of a fraction; the second, the denominator.
5919            6: '1b',   # SBYTE An 8-bit signed (twos-complement) integer.
5920            7: '1B',   # UNDEFINED An 8-bit byte that may contain anything,
5921                       #   depending on the definition of the field.
5922            8: '1h',   # SSHORT A 16-bit (2-byte) signed (twos-complement)
5923                       #   integer.
5924            9: '1i',   # SLONG A 32-bit (4-byte) signed (twos-complement)
5925                       #   integer.
5926            10: '2i',  # SRATIONAL Two SLONGs: the first represents the
5927                       #   numerator of a fraction, the second the denominator.
5928            11: '1f',  # FLOAT Single precision (4-byte) IEEE format.
5929            12: '1d',  # DOUBLE Double precision (8-byte) IEEE format.
5930            13: '1I',  # IFD unsigned 4 byte IFD offset.
5931            # 14: '',  # UNICODE
5932            # 15: '',  # COMPLEX
5933            16: '1Q',  # LONG8 unsigned 8 byte integer (BigTiff)
5934            17: '1q',  # SLONG8 signed 8 byte integer (BigTiff)
5935            18: '1Q',  # IFD8 unsigned 8 byte IFD offset (BigTiff)
5936        }
5937
5938    def DATA_DTYPES():
5939        # Map numpy dtypes to TIFF DATATYPES
5940        return {'B': 1, 's': 2, 'H': 3, 'I': 4, '2I': 5, 'b': 6,
5941                'h': 8, 'i': 9, '2i': 10, 'f': 11, 'd': 12, 'Q': 16, 'q': 17}
5942
5943    def SAMPLE_DTYPES():
5944        # Map TIFF SampleFormats and BitsPerSample to numpy dtype
5945        return {
5946            (1, 1): '?',  # bitmap
5947            (1, 2): 'B',
5948            (1, 3): 'B',
5949            (1, 4): 'B',
5950            (1, 5): 'B',
5951            (1, 6): 'B',
5952            (1, 7): 'B',
5953            (1, 8): 'B',
5954            (1, 9): 'H',
5955            (1, 10): 'H',
5956            (1, 11): 'H',
5957            (1, 12): 'H',
5958            (1, 13): 'H',
5959            (1, 14): 'H',
5960            (1, 15): 'H',
5961            (1, 16): 'H',
5962            (1, 17): 'I',
5963            (1, 18): 'I',
5964            (1, 19): 'I',
5965            (1, 20): 'I',
5966            (1, 21): 'I',
5967            (1, 22): 'I',
5968            (1, 23): 'I',
5969            (1, 24): 'I',
5970            (1, 25): 'I',
5971            (1, 26): 'I',
5972            (1, 27): 'I',
5973            (1, 28): 'I',
5974            (1, 29): 'I',
5975            (1, 30): 'I',
5976            (1, 31): 'I',
5977            (1, 32): 'I',
5978            (1, 64): 'Q',
5979            (2, 8): 'b',
5980            (2, 16): 'h',
5981            (2, 32): 'i',
5982            (2, 64): 'q',
5983            (3, 16): 'e',
5984            (3, 32): 'f',
5985            (3, 64): 'd',
5986            (6, 64): 'F',
5987            (6, 128): 'D',
5988            (1, (5, 6, 5)): 'B',
5989        }
5990
5991    def COMPESSORS():
5992        # Map COMPRESSION to compress functions and default compression levels
5993
5994        class Compressors(object):
5995            """Delay import compressor functions."""
5996            def __init__(self):
5997                self._compressors = {8: (zlib.compress, 6),
5998                                     32946: (zlib.compress, 6)}
5999
6000            def __getitem__(self, key):
6001                if key in self._compressors:
6002                    return self._compressors[key]
6003
6004                if key == 34925:
6005                    try:
6006                        import lzma  # delayed import
6007                    except ImportError:
6008                        try:
6009                            import backports.lzma as lzma  # delayed import
6010                        except ImportError:
6011                            raise KeyError
6012
6013                    def lzma_compress(x, level):
6014                        return lzma.compress(x)
6015
6016                    self._compressors[key] = lzma_compress, 0
6017                    return lzma_compress, 0
6018
6019                if key == 34926:
6020                    try:
6021                        import zstd  # delayed import
6022                    except ImportError:
6023                        raise KeyError
6024                    self._compressors[key] = zstd.compress, 9
6025                    return zstd.compress, 9
6026
6027                raise KeyError
6028
6029            def __contains__(self, key):
6030                try:
6031                    self[key]
6032                    return True
6033                except KeyError:
6034                    return False
6035
6036        return Compressors()
6037
6038    def DECOMPESSORS():
6039        # Map COMPRESSION to decompress functions
6040
6041        class Decompressors(object):
6042            """Delay import decompressor functions."""
6043            def __init__(self):
6044                self._decompressors = {None: identityfunc,
6045                                       1: identityfunc,
6046                                       5: decode_lzw,
6047                                       8: zlib.decompress,
6048                                       32773: decode_packbits,
6049                                       32946: zlib.decompress}
6050
6051            def __getitem__(self, key):
6052                if key in self._decompressors:
6053                    return self._decompressors[key]
6054
6055                if key == 7:
6056                    try:
6057                        from imagecodecs import jpeg, jpeg_12
6058                    except ImportError:
6059                        raise KeyError
6060
6061                    def decode_jpeg(x, table, bps, colorspace=None):
6062                        if bps == 8:
6063                            return jpeg.decode_jpeg(x, table, colorspace)
6064                        elif bps == 12:
6065                            return jpeg_12.decode_jpeg_12(x, table, colorspace)
6066                        else:
6067                            raise ValueError('bitspersample not supported')
6068
6069                    self._decompressors[key] = decode_jpeg
6070                    return decode_jpeg
6071
6072                if key == 34925:
6073                    try:
6074                        import lzma  # delayed import
6075                    except ImportError:
6076                        try:
6077                            import backports.lzma as lzma  # delayed import
6078                        except ImportError:
6079                            raise KeyError
6080                    self._decompressors[key] = lzma.decompress
6081                    return lzma.decompress
6082
6083                if key == 34926:
6084                    try:
6085                        import zstd  # delayed import
6086                    except ImportError:
6087                        raise KeyError
6088                    self._decompressors[key] = zstd.decompress
6089                    return zstd.decompress
6090                raise KeyError
6091
6092            def __contains__(self, item):
6093                try:
6094                    self[item]
6095                    return True
6096                except KeyError:
6097                    return False
6098
6099        return Decompressors()
6100
6101    def FRAME_ATTRS():
6102        # Attributes that a TiffFrame shares with its keyframe
6103        return set('shape ndim size dtype axes is_final'.split())
6104
6105    def FILE_FLAGS():
6106        # TiffFile and TiffPage 'is_\*' attributes
6107        exclude = set('reduced final memmappable contiguous tiled '
6108                      'chroma_subsampled'.split())
6109        return set(a[3:] for a in dir(TiffPage)
6110                   if a[:3] == 'is_' and a[3:] not in exclude)
6111
6112    def FILE_EXTENSIONS():
6113        # TIFF file extensions
6114        return tuple('tif tiff ome.tif lsm stk qptiff pcoraw '
6115                     'gel seq svs bif tf8 tf2 btf'.split())
6116
6117    def FILEOPEN_FILTER():
6118        # String for use in Windows File Open box
6119        return [('%s files' % ext.upper(), '*.%s' % ext)
6120                for ext in TIFF.FILE_EXTENSIONS] + [('allfiles', '*')]
6121
6122    def AXES_LABELS():
6123        # TODO: is there a standard for character axes labels?
6124        axes = {
6125            'X': 'width',
6126            'Y': 'height',
6127            'Z': 'depth',
6128            'S': 'sample',  # rgb(a)
6129            'I': 'series',  # general sequence, plane, page, IFD
6130            'T': 'time',
6131            'C': 'channel',  # color, emission wavelength
6132            'A': 'angle',
6133            'P': 'phase',  # formerly F    # P is Position in LSM!
6134            'R': 'tile',  # region, point, mosaic
6135            'H': 'lifetime',  # histogram
6136            'E': 'lambda',  # excitation wavelength
6137            'L': 'exposure',  # lux
6138            'V': 'event',
6139            'Q': 'other',
6140            'M': 'mosaic',  # LSM 6
6141        }
6142        axes.update(dict((v, k) for k, v in axes.items()))
6143        return axes
6144
6145    def ANDOR_TAGS():
6146        # Andor Technology tags #4864 - 5030
6147        return set(range(4864, 5030))
6148
6149    def EXIF_TAGS():
6150        tags = {
6151            # 65000 - 65112  Photoshop Camera RAW EXIF tags
6152            65000: 'OwnerName',
6153            65001: 'SerialNumber',
6154            65002: 'Lens',
6155            65100: 'RawFile',
6156            65101: 'Converter',
6157            65102: 'WhiteBalance',
6158            65105: 'Exposure',
6159            65106: 'Shadows',
6160            65107: 'Brightness',
6161            65108: 'Contrast',
6162            65109: 'Saturation',
6163            65110: 'Sharpness',
6164            65111: 'Smoothness',
6165            65112: 'MoireFilter',
6166        }
6167        tags.update(TIFF.TAGS)
6168        return tags
6169
6170    def GPS_TAGS():
6171        return {
6172            0: 'GPSVersionID',
6173            1: 'GPSLatitudeRef',
6174            2: 'GPSLatitude',
6175            3: 'GPSLongitudeRef',
6176            4: 'GPSLongitude',
6177            5: 'GPSAltitudeRef',
6178            6: 'GPSAltitude',
6179            7: 'GPSTimeStamp',
6180            8: 'GPSSatellites',
6181            9: 'GPSStatus',
6182            10: 'GPSMeasureMode',
6183            11: 'GPSDOP',
6184            12: 'GPSSpeedRef',
6185            13: 'GPSSpeed',
6186            14: 'GPSTrackRef',
6187            15: 'GPSTrack',
6188            16: 'GPSImgDirectionRef',
6189            17: 'GPSImgDirection',
6190            18: 'GPSMapDatum',
6191            19: 'GPSDestLatitudeRef',
6192            20: 'GPSDestLatitude',
6193            21: 'GPSDestLongitudeRef',
6194            22: 'GPSDestLongitude',
6195            23: 'GPSDestBearingRef',
6196            24: 'GPSDestBearing',
6197            25: 'GPSDestDistanceRef',
6198            26: 'GPSDestDistance',
6199            27: 'GPSProcessingMethod',
6200            28: 'GPSAreaInformation',
6201            29: 'GPSDateStamp',
6202            30: 'GPSDifferential',
6203            31: 'GPSHPositioningError',
6204        }
6205
6206    def IOP_TAGS():
6207        return {
6208            1: 'InteroperabilityIndex',
6209            2: 'InteroperabilityVersion',
6210            4096: 'RelatedImageFileFormat',
6211            4097: 'RelatedImageWidth',
6212            4098: 'RelatedImageLength',
6213        }
6214
6215    def GEO_KEYS():
6216        return {
6217            1024: 'GTModelTypeGeoKey',
6218            1025: 'GTRasterTypeGeoKey',
6219            1026: 'GTCitationGeoKey',
6220            2048: 'GeographicTypeGeoKey',
6221            2049: 'GeogCitationGeoKey',
6222            2050: 'GeogGeodeticDatumGeoKey',
6223            2051: 'GeogPrimeMeridianGeoKey',
6224            2052: 'GeogLinearUnitsGeoKey',
6225            2053: 'GeogLinearUnitSizeGeoKey',
6226            2054: 'GeogAngularUnitsGeoKey',
6227            2055: 'GeogAngularUnitsSizeGeoKey',
6228            2056: 'GeogEllipsoidGeoKey',
6229            2057: 'GeogSemiMajorAxisGeoKey',
6230            2058: 'GeogSemiMinorAxisGeoKey',
6231            2059: 'GeogInvFlatteningGeoKey',
6232            2060: 'GeogAzimuthUnitsGeoKey',
6233            2061: 'GeogPrimeMeridianLongGeoKey',
6234            2062: 'GeogTOWGS84GeoKey',
6235            3059: 'ProjLinearUnitsInterpCorrectGeoKey',  # GDAL
6236            3072: 'ProjectedCSTypeGeoKey',
6237            3073: 'PCSCitationGeoKey',
6238            3074: 'ProjectionGeoKey',
6239            3075: 'ProjCoordTransGeoKey',
6240            3076: 'ProjLinearUnitsGeoKey',
6241            3077: 'ProjLinearUnitSizeGeoKey',
6242            3078: 'ProjStdParallel1GeoKey',
6243            3079: 'ProjStdParallel2GeoKey',
6244            3080: 'ProjNatOriginLongGeoKey',
6245            3081: 'ProjNatOriginLatGeoKey',
6246            3082: 'ProjFalseEastingGeoKey',
6247            3083: 'ProjFalseNorthingGeoKey',
6248            3084: 'ProjFalseOriginLongGeoKey',
6249            3085: 'ProjFalseOriginLatGeoKey',
6250            3086: 'ProjFalseOriginEastingGeoKey',
6251            3087: 'ProjFalseOriginNorthingGeoKey',
6252            3088: 'ProjCenterLongGeoKey',
6253            3089: 'ProjCenterLatGeoKey',
6254            3090: 'ProjCenterEastingGeoKey',
6255            3091: 'ProjFalseOriginNorthingGeoKey',
6256            3092: 'ProjScaleAtNatOriginGeoKey',
6257            3093: 'ProjScaleAtCenterGeoKey',
6258            3094: 'ProjAzimuthAngleGeoKey',
6259            3095: 'ProjStraightVertPoleLongGeoKey',
6260            3096: 'ProjRectifiedGridAngleGeoKey',
6261            4096: 'VerticalCSTypeGeoKey',
6262            4097: 'VerticalCitationGeoKey',
6263            4098: 'VerticalDatumGeoKey',
6264            4099: 'VerticalUnitsGeoKey',
6265        }
6266
6267    def GEO_CODES():
6268        try:
6269            from .tifffile_geodb import GEO_CODES  # delayed import
6270        except (ImportError, ValueError):
6271            try:
6272                from tifffile_geodb import GEO_CODES  # delayed import
6273            except (ImportError, ValueError):
6274                GEO_CODES = {}
6275        return GEO_CODES
6276
6277    def CZ_LSMINFO():
6278        return [
6279            ('MagicNumber', 'u4'),
6280            ('StructureSize', 'i4'),
6281            ('DimensionX', 'i4'),
6282            ('DimensionY', 'i4'),
6283            ('DimensionZ', 'i4'),
6284            ('DimensionChannels', 'i4'),
6285            ('DimensionTime', 'i4'),
6286            ('DataType', 'i4'),  # DATATYPES
6287            ('ThumbnailX', 'i4'),
6288            ('ThumbnailY', 'i4'),
6289            ('VoxelSizeX', 'f8'),
6290            ('VoxelSizeY', 'f8'),
6291            ('VoxelSizeZ', 'f8'),
6292            ('OriginX', 'f8'),
6293            ('OriginY', 'f8'),
6294            ('OriginZ', 'f8'),
6295            ('ScanType', 'u2'),
6296            ('SpectralScan', 'u2'),
6297            ('TypeOfData', 'u4'),  # TYPEOFDATA
6298            ('OffsetVectorOverlay', 'u4'),
6299            ('OffsetInputLut', 'u4'),
6300            ('OffsetOutputLut', 'u4'),
6301            ('OffsetChannelColors', 'u4'),
6302            ('TimeIntervall', 'f8'),
6303            ('OffsetChannelDataTypes', 'u4'),
6304            ('OffsetScanInformation', 'u4'),  # SCANINFO
6305            ('OffsetKsData', 'u4'),
6306            ('OffsetTimeStamps', 'u4'),
6307            ('OffsetEventList', 'u4'),
6308            ('OffsetRoi', 'u4'),
6309            ('OffsetBleachRoi', 'u4'),
6310            ('OffsetNextRecording', 'u4'),
6311            # LSM 2.0 ends here
6312            ('DisplayAspectX', 'f8'),
6313            ('DisplayAspectY', 'f8'),
6314            ('DisplayAspectZ', 'f8'),
6315            ('DisplayAspectTime', 'f8'),
6316            ('OffsetMeanOfRoisOverlay', 'u4'),
6317            ('OffsetTopoIsolineOverlay', 'u4'),
6318            ('OffsetTopoProfileOverlay', 'u4'),
6319            ('OffsetLinescanOverlay', 'u4'),
6320            ('ToolbarFlags', 'u4'),
6321            ('OffsetChannelWavelength', 'u4'),
6322            ('OffsetChannelFactors', 'u4'),
6323            ('ObjectiveSphereCorrection', 'f8'),
6324            ('OffsetUnmixParameters', 'u4'),
6325            # LSM 3.2, 4.0 end here
6326            ('OffsetAcquisitionParameters', 'u4'),
6327            ('OffsetCharacteristics', 'u4'),
6328            ('OffsetPalette', 'u4'),
6329            ('TimeDifferenceX', 'f8'),
6330            ('TimeDifferenceY', 'f8'),
6331            ('TimeDifferenceZ', 'f8'),
6332            ('InternalUse1', 'u4'),
6333            ('DimensionP', 'i4'),
6334            ('DimensionM', 'i4'),
6335            ('DimensionsReserved', '16i4'),
6336            ('OffsetTilePositions', 'u4'),
6337            ('', '9u4'),  # Reserved
6338            ('OffsetPositions', 'u4'),
6339            # ('', '21u4'),  # must be 0
6340        ]
6341
6342    def CZ_LSMINFO_READERS():
6343        # Import functions for CZ_LSMINFO sub-records
6344        # TODO: read more CZ_LSMINFO sub-records
6345        return {
6346            'ScanInformation': read_lsm_scaninfo,
6347            'TimeStamps': read_lsm_timestamps,
6348            'EventList': read_lsm_eventlist,
6349            'ChannelColors': read_lsm_channelcolors,
6350            'Positions': read_lsm_floatpairs,
6351            'TilePositions': read_lsm_floatpairs,
6352            'VectorOverlay': None,
6353            'InputLut': None,
6354            'OutputLut': None,
6355            'TimeIntervall': None,
6356            'ChannelDataTypes': None,
6357            'KsData': None,
6358            'Roi': None,
6359            'BleachRoi': None,
6360            'NextRecording': None,
6361            'MeanOfRoisOverlay': None,
6362            'TopoIsolineOverlay': None,
6363            'TopoProfileOverlay': None,
6364            'ChannelWavelength': None,
6365            'SphereCorrection': None,
6366            'ChannelFactors': None,
6367            'UnmixParameters': None,
6368            'AcquisitionParameters': None,
6369            'Characteristics': None,
6370        }
6371
6372    def CZ_LSMINFO_SCANTYPE():
6373        # Map CZ_LSMINFO.ScanType to dimension order
6374        return {
6375            0: 'XYZCT',  # 'Stack' normal x-y-z-scan
6376            1: 'XYZCT',  # 'Z-Scan' x-z-plane Y=1
6377            2: 'XYZCT',  # 'Line'
6378            3: 'XYTCZ',  # 'Time Series Plane' time series x-y  XYCTZ ? Z=1
6379            4: 'XYZTC',  # 'Time Series z-Scan' time series x-z
6380            5: 'XYTCZ',  # 'Time Series Mean-of-ROIs'
6381            6: 'XYZTC',  # 'Time Series Stack' time series x-y-z
6382            7: 'XYCTZ',  # Spline Scan
6383            8: 'XYCZT',  # Spline Plane x-z
6384            9: 'XYTCZ',  # Time Series Spline Plane x-z
6385            10: 'XYZCT',  # 'Time Series Point' point mode
6386        }
6387
6388    def CZ_LSMINFO_DIMENSIONS():
6389        # Map dimension codes to CZ_LSMINFO attribute
6390        return {
6391            'X': 'DimensionX',
6392            'Y': 'DimensionY',
6393            'Z': 'DimensionZ',
6394            'C': 'DimensionChannels',
6395            'T': 'DimensionTime',
6396            'P': 'DimensionP',
6397            'M': 'DimensionM',
6398        }
6399
6400    def CZ_LSMINFO_DATATYPES():
6401        # Description of CZ_LSMINFO.DataType
6402        return {
6403            0: 'varying data types',
6404            1: '8 bit unsigned integer',
6405            2: '12 bit unsigned integer',
6406            5: '32 bit float',
6407        }
6408
6409    def CZ_LSMINFO_TYPEOFDATA():
6410        # Description of CZ_LSMINFO.TypeOfData
6411        return {
6412            0: 'Original scan data',
6413            1: 'Calculated data',
6414            2: '3D reconstruction',
6415            3: 'Topography height map',
6416        }
6417
6418    def CZ_LSMINFO_SCANINFO_ARRAYS():
6419        return {
6420            0x20000000: 'Tracks',
6421            0x30000000: 'Lasers',
6422            0x60000000: 'DetectionChannels',
6423            0x80000000: 'IlluminationChannels',
6424            0xa0000000: 'BeamSplitters',
6425            0xc0000000: 'DataChannels',
6426            0x11000000: 'Timers',
6427            0x13000000: 'Markers',
6428        }
6429
6430    def CZ_LSMINFO_SCANINFO_STRUCTS():
6431        return {
6432            # 0x10000000: 'Recording',
6433            0x40000000: 'Track',
6434            0x50000000: 'Laser',
6435            0x70000000: 'DetectionChannel',
6436            0x90000000: 'IlluminationChannel',
6437            0xb0000000: 'BeamSplitter',
6438            0xd0000000: 'DataChannel',
6439            0x12000000: 'Timer',
6440            0x14000000: 'Marker',
6441        }
6442
6443    def CZ_LSMINFO_SCANINFO_ATTRIBUTES():
6444        return {
6445            # Recording
6446            0x10000001: 'Name',
6447            0x10000002: 'Description',
6448            0x10000003: 'Notes',
6449            0x10000004: 'Objective',
6450            0x10000005: 'ProcessingSummary',
6451            0x10000006: 'SpecialScanMode',
6452            0x10000007: 'ScanType',
6453            0x10000008: 'ScanMode',
6454            0x10000009: 'NumberOfStacks',
6455            0x1000000a: 'LinesPerPlane',
6456            0x1000000b: 'SamplesPerLine',
6457            0x1000000c: 'PlanesPerVolume',
6458            0x1000000d: 'ImagesWidth',
6459            0x1000000e: 'ImagesHeight',
6460            0x1000000f: 'ImagesNumberPlanes',
6461            0x10000010: 'ImagesNumberStacks',
6462            0x10000011: 'ImagesNumberChannels',
6463            0x10000012: 'LinscanXySize',
6464            0x10000013: 'ScanDirection',
6465            0x10000014: 'TimeSeries',
6466            0x10000015: 'OriginalScanData',
6467            0x10000016: 'ZoomX',
6468            0x10000017: 'ZoomY',
6469            0x10000018: 'ZoomZ',
6470            0x10000019: 'Sample0X',
6471            0x1000001a: 'Sample0Y',
6472            0x1000001b: 'Sample0Z',
6473            0x1000001c: 'SampleSpacing',
6474            0x1000001d: 'LineSpacing',
6475            0x1000001e: 'PlaneSpacing',
6476            0x1000001f: 'PlaneWidth',
6477            0x10000020: 'PlaneHeight',
6478            0x10000021: 'VolumeDepth',
6479            0x10000023: 'Nutation',
6480            0x10000034: 'Rotation',
6481            0x10000035: 'Precession',
6482            0x10000036: 'Sample0time',
6483            0x10000037: 'StartScanTriggerIn',
6484            0x10000038: 'StartScanTriggerOut',
6485            0x10000039: 'StartScanEvent',
6486            0x10000040: 'StartScanTime',
6487            0x10000041: 'StopScanTriggerIn',
6488            0x10000042: 'StopScanTriggerOut',
6489            0x10000043: 'StopScanEvent',
6490            0x10000044: 'StopScanTime',
6491            0x10000045: 'UseRois',
6492            0x10000046: 'UseReducedMemoryRois',
6493            0x10000047: 'User',
6494            0x10000048: 'UseBcCorrection',
6495            0x10000049: 'PositionBcCorrection1',
6496            0x10000050: 'PositionBcCorrection2',
6497            0x10000051: 'InterpolationY',
6498            0x10000052: 'CameraBinning',
6499            0x10000053: 'CameraSupersampling',
6500            0x10000054: 'CameraFrameWidth',
6501            0x10000055: 'CameraFrameHeight',
6502            0x10000056: 'CameraOffsetX',
6503            0x10000057: 'CameraOffsetY',
6504            0x10000059: 'RtBinning',
6505            0x1000005a: 'RtFrameWidth',
6506            0x1000005b: 'RtFrameHeight',
6507            0x1000005c: 'RtRegionWidth',
6508            0x1000005d: 'RtRegionHeight',
6509            0x1000005e: 'RtOffsetX',
6510            0x1000005f: 'RtOffsetY',
6511            0x10000060: 'RtZoom',
6512            0x10000061: 'RtLinePeriod',
6513            0x10000062: 'Prescan',
6514            0x10000063: 'ScanDirectionZ',
6515            # Track
6516            0x40000001: 'MultiplexType',  # 0 After Line; 1 After Frame
6517            0x40000002: 'MultiplexOrder',
6518            0x40000003: 'SamplingMode',  # 0 Sample; 1 Line Avg; 2 Frame Avg
6519            0x40000004: 'SamplingMethod',  # 1 Mean; 2 Sum
6520            0x40000005: 'SamplingNumber',
6521            0x40000006: 'Acquire',
6522            0x40000007: 'SampleObservationTime',
6523            0x4000000b: 'TimeBetweenStacks',
6524            0x4000000c: 'Name',
6525            0x4000000d: 'Collimator1Name',
6526            0x4000000e: 'Collimator1Position',
6527            0x4000000f: 'Collimator2Name',
6528            0x40000010: 'Collimator2Position',
6529            0x40000011: 'IsBleachTrack',
6530            0x40000012: 'IsBleachAfterScanNumber',
6531            0x40000013: 'BleachScanNumber',
6532            0x40000014: 'TriggerIn',
6533            0x40000015: 'TriggerOut',
6534            0x40000016: 'IsRatioTrack',
6535            0x40000017: 'BleachCount',
6536            0x40000018: 'SpiCenterWavelength',
6537            0x40000019: 'PixelTime',
6538            0x40000021: 'CondensorFrontlens',
6539            0x40000023: 'FieldStopValue',
6540            0x40000024: 'IdCondensorAperture',
6541            0x40000025: 'CondensorAperture',
6542            0x40000026: 'IdCondensorRevolver',
6543            0x40000027: 'CondensorFilter',
6544            0x40000028: 'IdTransmissionFilter1',
6545            0x40000029: 'IdTransmission1',
6546            0x40000030: 'IdTransmissionFilter2',
6547            0x40000031: 'IdTransmission2',
6548            0x40000032: 'RepeatBleach',
6549            0x40000033: 'EnableSpotBleachPos',
6550            0x40000034: 'SpotBleachPosx',
6551            0x40000035: 'SpotBleachPosy',
6552            0x40000036: 'SpotBleachPosz',
6553            0x40000037: 'IdTubelens',
6554            0x40000038: 'IdTubelensPosition',
6555            0x40000039: 'TransmittedLight',
6556            0x4000003a: 'ReflectedLight',
6557            0x4000003b: 'SimultanGrabAndBleach',
6558            0x4000003c: 'BleachPixelTime',
6559            # Laser
6560            0x50000001: 'Name',
6561            0x50000002: 'Acquire',
6562            0x50000003: 'Power',
6563            # DetectionChannel
6564            0x70000001: 'IntegrationMode',
6565            0x70000002: 'SpecialMode',
6566            0x70000003: 'DetectorGainFirst',
6567            0x70000004: 'DetectorGainLast',
6568            0x70000005: 'AmplifierGainFirst',
6569            0x70000006: 'AmplifierGainLast',
6570            0x70000007: 'AmplifierOffsFirst',
6571            0x70000008: 'AmplifierOffsLast',
6572            0x70000009: 'PinholeDiameter',
6573            0x7000000a: 'CountingTrigger',
6574            0x7000000b: 'Acquire',
6575            0x7000000c: 'PointDetectorName',
6576            0x7000000d: 'AmplifierName',
6577            0x7000000e: 'PinholeName',
6578            0x7000000f: 'FilterSetName',
6579            0x70000010: 'FilterName',
6580            0x70000013: 'IntegratorName',
6581            0x70000014: 'ChannelName',
6582            0x70000015: 'DetectorGainBc1',
6583            0x70000016: 'DetectorGainBc2',
6584            0x70000017: 'AmplifierGainBc1',
6585            0x70000018: 'AmplifierGainBc2',
6586            0x70000019: 'AmplifierOffsetBc1',
6587            0x70000020: 'AmplifierOffsetBc2',
6588            0x70000021: 'SpectralScanChannels',
6589            0x70000022: 'SpiWavelengthStart',
6590            0x70000023: 'SpiWavelengthStop',
6591            0x70000026: 'DyeName',
6592            0x70000027: 'DyeFolder',
6593            # IlluminationChannel
6594            0x90000001: 'Name',
6595            0x90000002: 'Power',
6596            0x90000003: 'Wavelength',
6597            0x90000004: 'Aquire',
6598            0x90000005: 'DetchannelName',
6599            0x90000006: 'PowerBc1',
6600            0x90000007: 'PowerBc2',
6601            # BeamSplitter
6602            0xb0000001: 'FilterSet',
6603            0xb0000002: 'Filter',
6604            0xb0000003: 'Name',
6605            # DataChannel
6606            0xd0000001: 'Name',
6607            0xd0000003: 'Acquire',
6608            0xd0000004: 'Color',
6609            0xd0000005: 'SampleType',
6610            0xd0000006: 'BitsPerSample',
6611            0xd0000007: 'RatioType',
6612            0xd0000008: 'RatioTrack1',
6613            0xd0000009: 'RatioTrack2',
6614            0xd000000a: 'RatioChannel1',
6615            0xd000000b: 'RatioChannel2',
6616            0xd000000c: 'RatioConst1',
6617            0xd000000d: 'RatioConst2',
6618            0xd000000e: 'RatioConst3',
6619            0xd000000f: 'RatioConst4',
6620            0xd0000010: 'RatioConst5',
6621            0xd0000011: 'RatioConst6',
6622            0xd0000012: 'RatioFirstImages1',
6623            0xd0000013: 'RatioFirstImages2',
6624            0xd0000014: 'DyeName',
6625            0xd0000015: 'DyeFolder',
6626            0xd0000016: 'Spectrum',
6627            0xd0000017: 'Acquire',
6628            # Timer
6629            0x12000001: 'Name',
6630            0x12000002: 'Description',
6631            0x12000003: 'Interval',
6632            0x12000004: 'TriggerIn',
6633            0x12000005: 'TriggerOut',
6634            0x12000006: 'ActivationTime',
6635            0x12000007: 'ActivationNumber',
6636            # Marker
6637            0x14000001: 'Name',
6638            0x14000002: 'Description',
6639            0x14000003: 'TriggerIn',
6640            0x14000004: 'TriggerOut',
6641        }
6642
6643    def NIH_IMAGE_HEADER():
6644        return [
6645            ('FileID', 'a8'),
6646            ('nLines', 'i2'),
6647            ('PixelsPerLine', 'i2'),
6648            ('Version', 'i2'),
6649            ('OldLutMode', 'i2'),
6650            ('OldnColors', 'i2'),
6651            ('Colors', 'u1', (3, 32)),
6652            ('OldColorStart', 'i2'),
6653            ('ColorWidth', 'i2'),
6654            ('ExtraColors', 'u2', (6, 3)),
6655            ('nExtraColors', 'i2'),
6656            ('ForegroundIndex', 'i2'),
6657            ('BackgroundIndex', 'i2'),
6658            ('XScale', 'f8'),
6659            ('Unused2', 'i2'),
6660            ('Unused3', 'i2'),
6661            ('UnitsID', 'i2'),  # NIH_UNITS_TYPE
6662            ('p1', [('x', 'i2'), ('y', 'i2')]),
6663            ('p2', [('x', 'i2'), ('y', 'i2')]),
6664            ('CurveFitType', 'i2'),  # NIH_CURVEFIT_TYPE
6665            ('nCoefficients', 'i2'),
6666            ('Coeff', 'f8', 6),
6667            ('UMsize', 'u1'),
6668            ('UM', 'a15'),
6669            ('UnusedBoolean', 'u1'),
6670            ('BinaryPic', 'b1'),
6671            ('SliceStart', 'i2'),
6672            ('SliceEnd', 'i2'),
6673            ('ScaleMagnification', 'f4'),
6674            ('nSlices', 'i2'),
6675            ('SliceSpacing', 'f4'),
6676            ('CurrentSlice', 'i2'),
6677            ('FrameInterval', 'f4'),
6678            ('PixelAspectRatio', 'f4'),
6679            ('ColorStart', 'i2'),
6680            ('ColorEnd', 'i2'),
6681            ('nColors', 'i2'),
6682            ('Fill1', '3u2'),
6683            ('Fill2', '3u2'),
6684            ('Table', 'u1'),  # NIH_COLORTABLE_TYPE
6685            ('LutMode', 'u1'),  # NIH_LUTMODE_TYPE
6686            ('InvertedTable', 'b1'),
6687            ('ZeroClip', 'b1'),
6688            ('XUnitSize', 'u1'),
6689            ('XUnit', 'a11'),
6690            ('StackType', 'i2'),  # NIH_STACKTYPE_TYPE
6691            # ('UnusedBytes', 'u1', 200)
6692        ]
6693
6694    def NIH_COLORTABLE_TYPE():
6695        return ('CustomTable', 'AppleDefault', 'Pseudo20', 'Pseudo32',
6696                'Rainbow', 'Fire1', 'Fire2', 'Ice', 'Grays', 'Spectrum')
6697
6698    def NIH_LUTMODE_TYPE():
6699        return ('PseudoColor', 'OldAppleDefault', 'OldSpectrum', 'GrayScale',
6700                'ColorLut', 'CustomGrayscale')
6701
6702    def NIH_CURVEFIT_TYPE():
6703        return ('StraightLine', 'Poly2', 'Poly3', 'Poly4', 'Poly5', 'ExpoFit',
6704                'PowerFit', 'LogFit', 'RodbardFit', 'SpareFit1',
6705                'Uncalibrated', 'UncalibratedOD')
6706
6707    def NIH_UNITS_TYPE():
6708        return ('Nanometers', 'Micrometers', 'Millimeters', 'Centimeters',
6709                'Meters', 'Kilometers', 'Inches', 'Feet', 'Miles', 'Pixels',
6710                'OtherUnits')
6711
6712    def NIH_STACKTYPE_TYPE():
6713        return ('VolumeStack', 'RGBStack', 'MovieStack', 'HSVStack')
6714
6715    def TVIPS_HEADER_V1():
6716        # TVIPS TemData structure from EMMENU Help file
6717        return [
6718            ('Version', 'i4'),
6719            ('CommentV1', 'a80'),
6720            ('HighTension', 'i4'),
6721            ('SphericalAberration', 'i4'),
6722            ('IlluminationAperture', 'i4'),
6723            ('Magnification', 'i4'),
6724            ('PostMagnification', 'i4'),
6725            ('FocalLength', 'i4'),
6726            ('Defocus', 'i4'),
6727            ('Astigmatism', 'i4'),
6728            ('AstigmatismDirection', 'i4'),
6729            ('BiprismVoltage', 'i4'),
6730            ('SpecimenTiltAngle', 'i4'),
6731            ('SpecimenTiltDirection', 'i4'),
6732            ('IlluminationTiltDirection', 'i4'),
6733            ('IlluminationTiltAngle', 'i4'),
6734            ('ImageMode', 'i4'),
6735            ('EnergySpread', 'i4'),
6736            ('ChromaticAberration', 'i4'),
6737            ('ShutterType', 'i4'),
6738            ('DefocusSpread', 'i4'),
6739            ('CcdNumber', 'i4'),
6740            ('CcdSize', 'i4'),
6741            ('OffsetXV1', 'i4'),
6742            ('OffsetYV1', 'i4'),
6743            ('PhysicalPixelSize', 'i4'),
6744            ('Binning', 'i4'),
6745            ('ReadoutSpeed', 'i4'),
6746            ('GainV1', 'i4'),
6747            ('SensitivityV1', 'i4'),
6748            ('ExposureTimeV1', 'i4'),
6749            ('FlatCorrected', 'i4'),
6750            ('DeadPxCorrected', 'i4'),
6751            ('ImageMean', 'i4'),
6752            ('ImageStd', 'i4'),
6753            ('DisplacementX', 'i4'),
6754            ('DisplacementY', 'i4'),
6755            ('DateV1', 'i4'),
6756            ('TimeV1', 'i4'),
6757            ('ImageMin', 'i4'),
6758            ('ImageMax', 'i4'),
6759            ('ImageStatisticsQuality', 'i4'),
6760        ]
6761
6762    def TVIPS_HEADER_V2():
6763        return [
6764            ('ImageName', 'V160'),  # utf16
6765            ('ImageFolder', 'V160'),
6766            ('ImageSizeX', 'i4'),
6767            ('ImageSizeY', 'i4'),
6768            ('ImageSizeZ', 'i4'),
6769            ('ImageSizeE', 'i4'),
6770            ('ImageDataType', 'i4'),
6771            ('Date', 'i4'),
6772            ('Time', 'i4'),
6773            ('Comment', 'V1024'),
6774            ('ImageHistory', 'V1024'),
6775            ('Scaling', '16f4'),
6776            ('ImageStatistics', '16c16'),
6777            ('ImageType', 'i4'),
6778            ('ImageDisplaType', 'i4'),
6779            ('PixelSizeX', 'f4'),  # distance between two px in x, [nm]
6780            ('PixelSizeY', 'f4'),  # distance between two px in y, [nm]
6781            ('ImageDistanceZ', 'f4'),
6782            ('ImageDistanceE', 'f4'),
6783            ('ImageMisc', '32f4'),
6784            ('TemType', 'V160'),
6785            ('TemHighTension', 'f4'),
6786            ('TemAberrations', '32f4'),
6787            ('TemEnergy', '32f4'),
6788            ('TemMode', 'i4'),
6789            ('TemMagnification', 'f4'),
6790            ('TemMagnificationCorrection', 'f4'),
6791            ('PostMagnification', 'f4'),
6792            ('TemStageType', 'i4'),
6793            ('TemStagePosition', '5f4'),  # x, y, z, a, b
6794            ('TemImageShift', '2f4'),
6795            ('TemBeamShift', '2f4'),
6796            ('TemBeamTilt', '2f4'),
6797            ('TilingParameters', '7f4'),  # 0: tiling? 1:x 2:y 3: max x
6798                                          # 4: max y 5: overlap x 6: overlap y
6799            ('TemIllumination', '3f4'),  # 0: spotsize 1: intensity
6800            ('TemShutter', 'i4'),
6801            ('TemMisc', '32f4'),
6802            ('CameraType', 'V160'),
6803            ('PhysicalPixelSizeX', 'f4'),
6804            ('PhysicalPixelSizeY', 'f4'),
6805            ('OffsetX', 'i4'),
6806            ('OffsetY', 'i4'),
6807            ('BinningX', 'i4'),
6808            ('BinningY', 'i4'),
6809            ('ExposureTime', 'f4'),
6810            ('Gain', 'f4'),
6811            ('ReadoutRate', 'f4'),
6812            ('FlatfieldDescription', 'V160'),
6813            ('Sensitivity', 'f4'),
6814            ('Dose', 'f4'),
6815            ('CamMisc', '32f4'),
6816            ('FeiMicroscopeInformation', 'V1024'),
6817            ('FeiSpecimenInformation', 'V1024'),
6818            ('Magic', 'u4'),
6819        ]
6820
6821    def MM_HEADER():
6822        # Olympus FluoView MM_Header
6823        MM_DIMENSION = [
6824            ('Name', 'a16'),
6825            ('Size', 'i4'),
6826            ('Origin', 'f8'),
6827            ('Resolution', 'f8'),
6828            ('Unit', 'a64')]
6829        return [
6830            ('HeaderFlag', 'i2'),
6831            ('ImageType', 'u1'),
6832            ('ImageName', 'a257'),
6833            ('OffsetData', 'u4'),
6834            ('PaletteSize', 'i4'),
6835            ('OffsetPalette0', 'u4'),
6836            ('OffsetPalette1', 'u4'),
6837            ('CommentSize', 'i4'),
6838            ('OffsetComment', 'u4'),
6839            ('Dimensions', MM_DIMENSION, 10),
6840            ('OffsetPosition', 'u4'),
6841            ('MapType', 'i2'),
6842            ('MapMin', 'f8'),
6843            ('MapMax', 'f8'),
6844            ('MinValue', 'f8'),
6845            ('MaxValue', 'f8'),
6846            ('OffsetMap', 'u4'),
6847            ('Gamma', 'f8'),
6848            ('Offset', 'f8'),
6849            ('GrayChannel', MM_DIMENSION),
6850            ('OffsetThumbnail', 'u4'),
6851            ('VoiceField', 'i4'),
6852            ('OffsetVoiceField', 'u4'),
6853        ]
6854
6855    def MM_DIMENSIONS():
6856        # Map FluoView MM_Header.Dimensions to axes characters
6857        return {
6858            'X': 'X',
6859            'Y': 'Y',
6860            'Z': 'Z',
6861            'T': 'T',
6862            'CH': 'C',
6863            'WAVELENGTH': 'C',
6864            'TIME': 'T',
6865            'XY': 'R',
6866            'EVENT': 'V',
6867            'EXPOSURE': 'L',
6868        }
6869
6870    def UIC_TAGS():
6871        # Map Universal Imaging Corporation MetaMorph internal tag ids to
6872        # name and type
6873        from fractions import Fraction  # delayed import
6874
6875        return [
6876            ('AutoScale', int),
6877            ('MinScale', int),
6878            ('MaxScale', int),
6879            ('SpatialCalibration', int),
6880            ('XCalibration', Fraction),
6881            ('YCalibration', Fraction),
6882            ('CalibrationUnits', str),
6883            ('Name', str),
6884            ('ThreshState', int),
6885            ('ThreshStateRed', int),
6886            ('tagid_10', None),  # undefined
6887            ('ThreshStateGreen', int),
6888            ('ThreshStateBlue', int),
6889            ('ThreshStateLo', int),
6890            ('ThreshStateHi', int),
6891            ('Zoom', int),
6892            ('CreateTime', julian_datetime),
6893            ('LastSavedTime', julian_datetime),
6894            ('currentBuffer', int),
6895            ('grayFit', None),
6896            ('grayPointCount', None),
6897            ('grayX', Fraction),
6898            ('grayY', Fraction),
6899            ('grayMin', Fraction),
6900            ('grayMax', Fraction),
6901            ('grayUnitName', str),
6902            ('StandardLUT', int),
6903            ('wavelength', int),
6904            ('StagePosition', '(%i,2,2)u4'),  # N xy positions as fract
6905            ('CameraChipOffset', '(%i,2,2)u4'),  # N xy offsets as fract
6906            ('OverlayMask', None),
6907            ('OverlayCompress', None),
6908            ('Overlay', None),
6909            ('SpecialOverlayMask', None),
6910            ('SpecialOverlayCompress', None),
6911            ('SpecialOverlay', None),
6912            ('ImageProperty', read_uic_image_property),
6913            ('StageLabel', '%ip'),  # N str
6914            ('AutoScaleLoInfo', Fraction),
6915            ('AutoScaleHiInfo', Fraction),
6916            ('AbsoluteZ', '(%i,2)u4'),  # N fractions
6917            ('AbsoluteZValid', '(%i,)u4'),  # N long
6918            ('Gamma', 'I'),  # 'I' uses offset
6919            ('GammaRed', 'I'),
6920            ('GammaGreen', 'I'),
6921            ('GammaBlue', 'I'),
6922            ('CameraBin', '2I'),
6923            ('NewLUT', int),
6924            ('ImagePropertyEx', None),
6925            ('PlaneProperty', int),
6926            ('UserLutTable', '(256,3)u1'),
6927            ('RedAutoScaleInfo', int),
6928            ('RedAutoScaleLoInfo', Fraction),
6929            ('RedAutoScaleHiInfo', Fraction),
6930            ('RedMinScaleInfo', int),
6931            ('RedMaxScaleInfo', int),
6932            ('GreenAutoScaleInfo', int),
6933            ('GreenAutoScaleLoInfo', Fraction),
6934            ('GreenAutoScaleHiInfo', Fraction),
6935            ('GreenMinScaleInfo', int),
6936            ('GreenMaxScaleInfo', int),
6937            ('BlueAutoScaleInfo', int),
6938            ('BlueAutoScaleLoInfo', Fraction),
6939            ('BlueAutoScaleHiInfo', Fraction),
6940            ('BlueMinScaleInfo', int),
6941            ('BlueMaxScaleInfo', int),
6942            # ('OverlayPlaneColor', read_uic_overlay_plane_color),
6943        ]
6944
6945    def PILATUS_HEADER():
6946        # PILATUS CBF Header Specification, Version 1.4
6947        # Map key to [value_indices], type
6948        return {
6949            'Detector': ([slice(1, None)], str),
6950            'Pixel_size': ([1, 4], float),
6951            'Silicon': ([3], float),
6952            'Exposure_time': ([1], float),
6953            'Exposure_period': ([1], float),
6954            'Tau': ([1], float),
6955            'Count_cutoff': ([1], int),
6956            'Threshold_setting': ([1], float),
6957            'Gain_setting': ([1, 2], str),
6958            'N_excluded_pixels': ([1], int),
6959            'Excluded_pixels': ([1], str),
6960            'Flat_field': ([1], str),
6961            'Trim_file': ([1], str),
6962            'Image_path': ([1], str),
6963            # optional
6964            'Wavelength': ([1], float),
6965            'Energy_range': ([1, 2], float),
6966            'Detector_distance': ([1], float),
6967            'Detector_Voffset': ([1], float),
6968            'Beam_xy': ([1, 2], float),
6969            'Flux': ([1], str),
6970            'Filter_transmission': ([1], float),
6971            'Start_angle': ([1], float),
6972            'Angle_increment': ([1], float),
6973            'Detector_2theta': ([1], float),
6974            'Polarization': ([1], float),
6975            'Alpha': ([1], float),
6976            'Kappa': ([1], float),
6977            'Phi': ([1], float),
6978            'Phi_increment': ([1], float),
6979            'Chi': ([1], float),
6980            'Chi_increment': ([1], float),
6981            'Oscillation_axis': ([slice(1, None)], str),
6982            'N_oscillations': ([1], int),
6983            'Start_position': ([1], float),
6984            'Position_increment': ([1], float),
6985            'Shutter_time': ([1], float),
6986            'Omega': ([1], float),
6987            'Omega_increment': ([1], float)
6988        }
6989
6990    def REVERSE_BITORDER_BYTES():
6991        # Bytes with reversed bitorder
6992        return (
6993            b'\x00\x80@\xc0 \xa0`\xe0\x10\x90P\xd00\xb0p\xf0\x08\x88H\xc8('
6994            b'\xa8h\xe8\x18\x98X\xd88\xb8x\xf8\x04\x84D\xc4$\xa4d\xe4\x14'
6995            b'\x94T\xd44\xb4t\xf4\x0c\x8cL\xcc,\xacl\xec\x1c\x9c\\\xdc<\xbc|'
6996            b'\xfc\x02\x82B\xc2"\xa2b\xe2\x12\x92R\xd22\xb2r\xf2\n\x8aJ\xca*'
6997            b'\xaaj\xea\x1a\x9aZ\xda:\xbaz\xfa\x06\x86F\xc6&\xa6f\xe6\x16'
6998            b'\x96V\xd66\xb6v\xf6\x0e\x8eN\xce.\xaen\xee\x1e\x9e^\xde>\xbe~'
6999            b'\xfe\x01\x81A\xc1!\xa1a\xe1\x11\x91Q\xd11\xb1q\xf1\t\x89I\xc9)'
7000            b'\xa9i\xe9\x19\x99Y\xd99\xb9y\xf9\x05\x85E\xc5%\xa5e\xe5\x15'
7001            b'\x95U\xd55\xb5u\xf5\r\x8dM\xcd-\xadm\xed\x1d\x9d]\xdd=\xbd}'
7002            b'\xfd\x03\x83C\xc3#\xa3c\xe3\x13\x93S\xd33\xb3s\xf3\x0b\x8bK'
7003            b'\xcb+\xabk\xeb\x1b\x9b[\xdb;\xbb{\xfb\x07\x87G\xc7\'\xa7g\xe7'
7004            b'\x17\x97W\xd77\xb7w\xf7\x0f\x8fO\xcf/\xafo\xef\x1f\x9f_'
7005            b'\xdf?\xbf\x7f\xff')
7006
7007    def REVERSE_BITORDER_ARRAY():
7008        # Numpy array of bytes with reversed bitorder
7009        return numpy.frombuffer(TIFF.REVERSE_BITORDER_BYTES, dtype='uint8')
7010
7011    def ALLOCATIONGRANULARITY():
7012        # alignment for writing contiguous data to TIFF
7013        import mmap  # delayed import
7014        return mmap.ALLOCATIONGRANULARITY
7015
7016
7017def read_tags(fh, byteorder, offsetsize, tagnames,
7018              customtags=None, maxifds=None):
7019    """Read tags from chain of IFDs and return as list of dicts.
7020
7021    The file handle position must be at a valid IFD header.
7022
7023    """
7024    if offsetsize == 4:
7025        offsetformat = byteorder+'I'
7026        tagnosize = 2
7027        tagnoformat = byteorder+'H'
7028        tagsize = 12
7029        tagformat1 = byteorder+'HH'
7030        tagformat2 = byteorder+'I4s'
7031    elif offsetsize == 8:
7032        offsetformat = byteorder+'Q'
7033        tagnosize = 8
7034        tagnoformat = byteorder+'Q'
7035        tagsize = 20
7036        tagformat1 = byteorder+'HH'
7037        tagformat2 = byteorder+'Q8s'
7038    else:
7039        raise ValueError('invalid offset size')
7040
7041    if customtags is None:
7042        customtags = {}
7043    if maxifds is None:
7044        maxifds = 2**32
7045
7046    result = []
7047    unpack = struct.unpack
7048    offset = fh.tell()
7049    while len(result) < maxifds:
7050        # loop over IFDs
7051        try:
7052            tagno = unpack(tagnoformat, fh.read(tagnosize))[0]
7053            if tagno > 4096:
7054                raise ValueError('suspicious number of tags')
7055        except Exception:
7056            warnings.warn('corrupted tag list at offset %i' % offset)
7057            break
7058
7059        tags = {}
7060        data = fh.read(tagsize * tagno)
7061        pos = fh.tell()
7062        index = 0
7063        for _ in range(tagno):
7064            code, type_ = unpack(tagformat1, data[index:index+4])
7065            count, value = unpack(tagformat2, data[index+4:index+tagsize])
7066            index += tagsize
7067            name = tagnames.get(code, str(code))
7068            try:
7069                dtype = TIFF.DATA_FORMATS[type_]
7070            except KeyError:
7071                raise TiffTag.Error('unknown tag data type %i' % type_)
7072
7073            fmt = '%s%i%s' % (byteorder, count * int(dtype[0]), dtype[1])
7074            size = struct.calcsize(fmt)
7075            if size > offsetsize or code in customtags:
7076                offset = unpack(offsetformat, value)[0]
7077                if offset < 8 or offset > fh.size - size:
7078                    raise TiffTag.Error('invalid tag value offset %i' % offset)
7079                fh.seek(offset)
7080                if code in customtags:
7081                    readfunc = customtags[code][1]
7082                    value = readfunc(fh, byteorder, dtype, count, offsetsize)
7083                elif type_ == 7 or (count > 1 and dtype[-1] == 'B'):
7084                    value = read_bytes(fh, byteorder, dtype, count, offsetsize)
7085                elif code in tagnames or dtype[-1] == 's':
7086                    value = unpack(fmt, fh.read(size))
7087                else:
7088                    value = read_numpy(fh, byteorder, dtype, count, offsetsize)
7089            elif dtype[-1] == 'B' or type_ == 7:
7090                value = value[:size]
7091            else:
7092                value = unpack(fmt, value[:size])
7093
7094            if code not in customtags and code not in TIFF.TAG_TUPLE:
7095                if len(value) == 1:
7096                    value = value[0]
7097            if type_ != 7 and dtype[-1] == 's' and isinstance(value, bytes):
7098                # TIFF ASCII fields can contain multiple strings,
7099                #   each terminated with a NUL
7100                try:
7101                    value = bytes2str(stripascii(value).strip())
7102                except UnicodeDecodeError:
7103                    warnings.warn(
7104                        'tag %i: coercing invalid ASCII to bytes' % code)
7105
7106            tags[name] = value
7107
7108        result.append(tags)
7109        # read offset to next page
7110        fh.seek(pos)
7111        offset = unpack(offsetformat, fh.read(offsetsize))[0]
7112        if offset == 0:
7113            break
7114        if offset >= fh.size:
7115            warnings.warn('invalid page offset %i' % offset)
7116            break
7117        fh.seek(offset)
7118
7119    if result and maxifds == 1:
7120        result = result[0]
7121    return result
7122
7123
7124def read_exif_ifd(fh, byteorder, dtype, count, offsetsize):
7125    """Read EXIF tags from file and return as dict."""
7126    exif = read_tags(fh, byteorder, offsetsize, TIFF.EXIF_TAGS, maxifds=1)
7127    for name in ('ExifVersion', 'FlashpixVersion'):
7128        try:
7129            exif[name] = bytes2str(exif[name])
7130        except Exception:
7131            pass
7132    if 'UserComment' in exif:
7133        idcode = exif['UserComment'][:8]
7134        try:
7135            if idcode == b'ASCII\x00\x00\x00':
7136                exif['UserComment'] = bytes2str(exif['UserComment'][8:])
7137            elif idcode == b'UNICODE\x00':
7138                exif['UserComment'] = exif['UserComment'][8:].decode('utf-16')
7139        except Exception:
7140            pass
7141    return exif
7142
7143
7144def read_gps_ifd(fh, byteorder, dtype, count, offsetsize):
7145    """Read GPS tags from file and return as dict."""
7146    return read_tags(fh, byteorder, offsetsize, TIFF.GPS_TAGS, maxifds=1)
7147
7148
7149def read_interoperability_ifd(fh, byteorder, dtype, count, offsetsize):
7150    """Read Interoperability tags from file and return as dict."""
7151    tag_names = {1: 'InteroperabilityIndex'}
7152    return read_tags(fh, byteorder, offsetsize, tag_names, maxifds=1)
7153
7154
7155def read_bytes(fh, byteorder, dtype, count, offsetsize):
7156    """Read tag data from file and return as byte string."""
7157    dtype = 'B' if dtype[-1] == 's' else byteorder+dtype[-1]
7158    count *= numpy.dtype(dtype).itemsize
7159    data = fh.read(count)
7160    if len(data) != count:
7161        warnings.warn('failed to read all bytes: %i, %i' % (len(data), count))
7162    return data
7163
7164
7165def read_utf8(fh, byteorder, dtype, count, offsetsize):
7166    """Read tag data from file and return as unicode string."""
7167    return fh.read(count).decode('utf-8')
7168
7169
7170def read_numpy(fh, byteorder, dtype, count, offsetsize):
7171    """Read tag data from file and return as numpy array."""
7172    dtype = 'b' if dtype[-1] == 's' else byteorder+dtype[-1]
7173    return fh.read_array(dtype, count)
7174
7175
7176def read_colormap(fh, byteorder, dtype, count, offsetsize):
7177    """Read ColorMap data from file and return as numpy array."""
7178    cmap = fh.read_array(byteorder+dtype[-1], count)
7179    cmap.shape = (3, -1)
7180    return cmap
7181
7182
7183def read_json(fh, byteorder, dtype, count, offsetsize):
7184    """Read JSON tag data from file and return as object."""
7185    data = fh.read(count)
7186    try:
7187        return json.loads(unicode(stripnull(data), 'utf-8'))
7188    except ValueError:
7189        warnings.warn("invalid JSON '%s'" % data)
7190
7191
7192def read_mm_header(fh, byteorder, dtype, count, offsetsize):
7193    """Read FluoView mm_header tag from file and return as dict."""
7194    mmh = fh.read_record(TIFF.MM_HEADER, byteorder=byteorder)
7195    mmh = recarray2dict(mmh)
7196    mmh['Dimensions'] = [
7197        (bytes2str(d[0]).strip(), d[1], d[2], d[3], bytes2str(d[4]).strip())
7198        for d in mmh['Dimensions']]
7199    d = mmh['GrayChannel']
7200    mmh['GrayChannel'] = (
7201        bytes2str(d[0]).strip(), d[1], d[2], d[3], bytes2str(d[4]).strip())
7202    return mmh
7203
7204
7205def read_mm_stamp(fh, byteorder, dtype, count, offsetsize):
7206    """Read FluoView mm_stamp tag from file and return as numpy.ndarray."""
7207    return fh.read_array(byteorder+'f8', 8)
7208
7209
7210def read_uic1tag(fh, byteorder, dtype, count, offsetsize, planecount=None):
7211    """Read MetaMorph STK UIC1Tag from file and return as dict.
7212
7213    Return empty dictionary if planecount is unknown.
7214
7215    """
7216    assert dtype in ('2I', '1I') and byteorder == '<'
7217    result = {}
7218    if dtype == '2I':
7219        # pre MetaMorph 2.5 (not tested)
7220        values = fh.read_array('<u4', 2*count).reshape(count, 2)
7221        result = {'ZDistance': values[:, 0] / values[:, 1]}
7222    elif planecount:
7223        for _ in range(count):
7224            tagid = struct.unpack('<I', fh.read(4))[0]
7225            if tagid in (28, 29, 37, 40, 41):
7226                # silently skip unexpected tags
7227                fh.read(4)
7228                continue
7229            name, value = read_uic_tag(fh, tagid, planecount, offset=True)
7230            result[name] = value
7231    return result
7232
7233
7234def read_uic2tag(fh, byteorder, dtype, planecount, offsetsize):
7235    """Read MetaMorph STK UIC2Tag from file and return as dict."""
7236    assert dtype == '2I' and byteorder == '<'
7237    values = fh.read_array('<u4', 6*planecount).reshape(planecount, 6)
7238    return {
7239        'ZDistance': values[:, 0] / values[:, 1],
7240        'DateCreated': values[:, 2],  # julian days
7241        'TimeCreated': values[:, 3],  # milliseconds
7242        'DateModified': values[:, 4],  # julian days
7243        'TimeModified': values[:, 5]}  # milliseconds
7244
7245
7246def read_uic3tag(fh, byteorder, dtype, planecount, offsetsize):
7247    """Read MetaMorph STK UIC3Tag from file and return as dict."""
7248    assert dtype == '2I' and byteorder == '<'
7249    values = fh.read_array('<u4', 2*planecount).reshape(planecount, 2)
7250    return {'Wavelengths': values[:, 0] / values[:, 1]}
7251
7252
7253def read_uic4tag(fh, byteorder, dtype, planecount, offsetsize):
7254    """Read MetaMorph STK UIC4Tag from file and return as dict."""
7255    assert dtype == '1I' and byteorder == '<'
7256    result = {}
7257    while True:
7258        tagid = struct.unpack('<H', fh.read(2))[0]
7259        if tagid == 0:
7260            break
7261        name, value = read_uic_tag(fh, tagid, planecount, offset=False)
7262        result[name] = value
7263    return result
7264
7265
7266def read_uic_tag(fh, tagid, planecount, offset):
7267    """Read a single UIC tag value from file and return tag name and value.
7268
7269    UIC1Tags use an offset.
7270
7271    """
7272    def read_int(count=1):
7273        value = struct.unpack('<%iI' % count, fh.read(4*count))
7274        return value[0] if count == 1 else value
7275
7276    try:
7277        name, dtype = TIFF.UIC_TAGS[tagid]
7278    except IndexError:
7279        # unknown tag
7280        return '_TagId%i' % tagid, read_int()
7281
7282    Fraction = TIFF.UIC_TAGS[4][1]
7283
7284    if offset:
7285        pos = fh.tell()
7286        if dtype not in (int, None):
7287            off = read_int()
7288            if off < 8:
7289                if dtype is str:
7290                    return name, ''
7291                warnings.warn("invalid offset for uic tag '%s': %i" %
7292                              (name, off))
7293                return name, off
7294            fh.seek(off)
7295
7296    if dtype is None:
7297        # skip
7298        name = '_' + name
7299        value = read_int()
7300    elif dtype is int:
7301        # int
7302        value = read_int()
7303    elif dtype is Fraction:
7304        # fraction
7305        value = read_int(2)
7306        value = value[0] / value[1]
7307    elif dtype is julian_datetime:
7308        # datetime
7309        value = julian_datetime(*read_int(2))
7310    elif dtype is read_uic_image_property:
7311        # ImagePropertyEx
7312        value = read_uic_image_property(fh)
7313    elif dtype is str:
7314        # pascal string
7315        size = read_int()
7316        if 0 <= size < 2**10:
7317            value = struct.unpack('%is' % size, fh.read(size))[0][:-1]
7318            value = bytes2str(stripnull(value))
7319        elif offset:
7320            value = ''
7321            warnings.warn("corrupt string in uic tag '%s'" % name)
7322        else:
7323            raise ValueError('invalid string size: %i' % size)
7324    elif dtype == '%ip':
7325        # sequence of pascal strings
7326        value = []
7327        for _ in range(planecount):
7328            size = read_int()
7329            if 0 <= size < 2**10:
7330                string = struct.unpack('%is' % size, fh.read(size))[0][:-1]
7331                string = bytes2str(stripnull(string))
7332                value.append(string)
7333            elif offset:
7334                warnings.warn("corrupt string in uic tag '%s'" % name)
7335            else:
7336                raise ValueError('invalid string size: %i' % size)
7337    else:
7338        # struct or numpy type
7339        dtype = '<' + dtype
7340        if '%i' in dtype:
7341            dtype = dtype % planecount
7342        if '(' in dtype:
7343            # numpy type
7344            value = fh.read_array(dtype, 1)[0]
7345            if value.shape[-1] == 2:
7346                # assume fractions
7347                value = value[..., 0] / value[..., 1]
7348        else:
7349            # struct format
7350            value = struct.unpack(dtype, fh.read(struct.calcsize(dtype)))
7351            if len(value) == 1:
7352                value = value[0]
7353
7354    if offset:
7355        fh.seek(pos + 4)
7356
7357    return name, value
7358
7359
7360def read_uic_image_property(fh):
7361    """Read UIC ImagePropertyEx tag from file and return as dict."""
7362    # TODO: test this
7363    size = struct.unpack('B', fh.read(1))[0]
7364    name = struct.unpack('%is' % size, fh.read(size))[0][:-1]
7365    flags, prop = struct.unpack('<IB', fh.read(5))
7366    if prop == 1:
7367        value = struct.unpack('II', fh.read(8))
7368        value = value[0] / value[1]
7369    else:
7370        size = struct.unpack('B', fh.read(1))[0]
7371        value = struct.unpack('%is' % size, fh.read(size))[0]
7372    return dict(name=name, flags=flags, value=value)
7373
7374
7375def read_cz_lsminfo(fh, byteorder, dtype, count, offsetsize):
7376    """Read CZ_LSMINFO tag from file and return as dict."""
7377    assert byteorder == '<'
7378    magic_number, structure_size = struct.unpack('<II', fh.read(8))
7379    if magic_number not in (50350412, 67127628):
7380        raise ValueError('invalid CZ_LSMINFO structure')
7381    fh.seek(-8, 1)
7382
7383    if structure_size < numpy.dtype(TIFF.CZ_LSMINFO).itemsize:
7384        # adjust structure according to structure_size
7385        lsminfo = []
7386        size = 0
7387        for name, dtype in TIFF.CZ_LSMINFO:
7388            size += numpy.dtype(dtype).itemsize
7389            if size > structure_size:
7390                break
7391            lsminfo.append((name, dtype))
7392    else:
7393        lsminfo = TIFF.CZ_LSMINFO
7394
7395    lsminfo = fh.read_record(lsminfo, byteorder=byteorder)
7396    lsminfo = recarray2dict(lsminfo)
7397
7398    # read LSM info subrecords at offsets
7399    for name, reader in TIFF.CZ_LSMINFO_READERS.items():
7400        if reader is None:
7401            continue
7402        offset = lsminfo.get('Offset' + name, 0)
7403        if offset < 8:
7404            continue
7405        fh.seek(offset)
7406        try:
7407            lsminfo[name] = reader(fh)
7408        except ValueError:
7409            pass
7410    return lsminfo
7411
7412
7413def read_lsm_floatpairs(fh):
7414    """Read LSM sequence of float pairs from file and return as list."""
7415    size = struct.unpack('<i', fh.read(4))[0]
7416    return fh.read_array('<2f8', count=size)
7417
7418
7419def read_lsm_positions(fh):
7420    """Read LSM positions from file and return as list."""
7421    size = struct.unpack('<I', fh.read(4))[0]
7422    return fh.read_array('<2f8', count=size)
7423
7424
7425def read_lsm_timestamps(fh):
7426    """Read LSM time stamps from file and return as list."""
7427    size, count = struct.unpack('<ii', fh.read(8))
7428    if size != (8 + 8 * count):
7429        warnings.warn('invalid LSM TimeStamps block')
7430        return []
7431    # return struct.unpack('<%dd' % count, fh.read(8*count))
7432    return fh.read_array('<f8', count=count)
7433
7434
7435def read_lsm_eventlist(fh):
7436    """Read LSM events from file and return as list of (time, type, text)."""
7437    count = struct.unpack('<II', fh.read(8))[1]
7438    events = []
7439    while count > 0:
7440        esize, etime, etype = struct.unpack('<IdI', fh.read(16))
7441        etext = bytes2str(stripnull(fh.read(esize - 16)))
7442        events.append((etime, etype, etext))
7443        count -= 1
7444    return events
7445
7446
7447def read_lsm_channelcolors(fh):
7448    """Read LSM ChannelColors structure from file and return as dict."""
7449    result = {'Mono': False, 'Colors': [], 'ColorNames': []}
7450    pos = fh.tell()
7451    (size, ncolors, nnames,
7452     coffset, noffset, mono) = struct.unpack('<IIIIII', fh.read(24))
7453    if ncolors != nnames:
7454        warnings.warn('invalid LSM ChannelColors structure')
7455        return result
7456    result['Mono'] = bool(mono)
7457    # Colors
7458    fh.seek(pos + coffset)
7459    colors = fh.read_array('uint8', count=ncolors*4).reshape((ncolors, 4))
7460    result['Colors'] = colors.tolist()
7461    # ColorNames
7462    fh.seek(pos + noffset)
7463    buffer = fh.read(size - noffset)
7464    names = []
7465    while len(buffer) > 4:
7466        size = struct.unpack('<I', buffer[:4])[0]
7467        names.append(bytes2str(buffer[4:3+size]))
7468        buffer = buffer[4+size:]
7469    result['ColorNames'] = names
7470    return result
7471
7472
7473def read_lsm_scaninfo(fh):
7474    """Read LSM ScanInfo structure from file and return as dict."""
7475    block = {}
7476    blocks = [block]
7477    unpack = struct.unpack
7478    if struct.unpack('<I', fh.read(4))[0] != 0x10000000:
7479        # not a Recording sub block
7480        warnings.warn('invalid LSM ScanInfo structure')
7481        return block
7482    fh.read(8)
7483    while True:
7484        entry, dtype, size = unpack('<III', fh.read(12))
7485        if dtype == 2:
7486            # ascii
7487            value = bytes2str(stripnull(fh.read(size)))
7488        elif dtype == 4:
7489            # long
7490            value = unpack('<i', fh.read(4))[0]
7491        elif dtype == 5:
7492            # rational
7493            value = unpack('<d', fh.read(8))[0]
7494        else:
7495            value = 0
7496        if entry in TIFF.CZ_LSMINFO_SCANINFO_ARRAYS:
7497            blocks.append(block)
7498            name = TIFF.CZ_LSMINFO_SCANINFO_ARRAYS[entry]
7499            newobj = []
7500            block[name] = newobj
7501            block = newobj
7502        elif entry in TIFF.CZ_LSMINFO_SCANINFO_STRUCTS:
7503            blocks.append(block)
7504            newobj = {}
7505            block.append(newobj)
7506            block = newobj
7507        elif entry in TIFF.CZ_LSMINFO_SCANINFO_ATTRIBUTES:
7508            name = TIFF.CZ_LSMINFO_SCANINFO_ATTRIBUTES[entry]
7509            block[name] = value
7510        elif entry == 0xffffffff:
7511            # end sub block
7512            block = blocks.pop()
7513        else:
7514            # unknown entry
7515            block['Entry0x%x' % entry] = value
7516        if not blocks:
7517            break
7518    return block
7519
7520
7521def read_tvips_header(fh, byteorder, dtype, count, offsetsize):
7522    """Read TVIPS EM-MENU headers and return as dict."""
7523    result = {}
7524    header = fh.read_record(TIFF.TVIPS_HEADER_V1, byteorder=byteorder)
7525    for name, typestr in TIFF.TVIPS_HEADER_V1:
7526        result[name] = header[name].tolist()
7527    if header['Version'] == 2:
7528        header = fh.read_record(TIFF.TVIPS_HEADER_V2, byteorder=byteorder)
7529        if header['Magic'] != int(0xaaaaaaaa):
7530            warnings.warn('invalid TVIPS v2 magic number')
7531            return {}
7532        # decode utf16 strings
7533        for name, typestr in TIFF.TVIPS_HEADER_V2:
7534            if typestr.startswith('V'):
7535                s = header[name].tostring().decode('utf16', errors='ignore')
7536                result[name] = stripnull(s, null='\0')
7537            else:
7538                result[name] = header[name].tolist()
7539        # convert nm to m
7540        for axis in 'XY':
7541            header['PhysicalPixelSize' + axis] /= 1e9
7542            header['PixelSize' + axis] /= 1e9
7543    elif header.version != 1:
7544        warnings.warn('unknown TVIPS header version')
7545        return {}
7546    return result
7547
7548
7549def read_fei_metadata(fh, byteorder, dtype, count, offsetsize):
7550    """Read FEI SFEG/HELIOS headers and return as dict."""
7551    result = {}
7552    section = {}
7553    data = bytes2str(fh.read(count))
7554    for line in data.splitlines():
7555        line = line.strip()
7556        if line.startswith('['):
7557            section = {}
7558            result[line[1:-1]] = section
7559            continue
7560        try:
7561            key, value = line.split('=')
7562        except ValueError:
7563            continue
7564        section[key] = astype(value)
7565    return result
7566
7567
7568def read_cz_sem(fh, byteorder, dtype, count, offsetsize):
7569    """Read Zeiss SEM tag and return as dict."""
7570    result = {'': ()}
7571    key = None
7572    data = bytes2str(fh.read(count))
7573    for line in data.splitlines():
7574        if line.isupper():
7575            key = line.lower()
7576        elif key:
7577            try:
7578                name, value = line.split('=')
7579            except ValueError:
7580                continue
7581            value = value.strip()
7582            unit = ''
7583            try:
7584                v, u = value.split()
7585                number = astype(v, (int, float))
7586                if number != v:
7587                    value = number
7588                    unit = u
7589            except Exception:
7590                number = astype(value, (int, float))
7591                if number != value:
7592                    value = number
7593                if value in ('No', 'Off'):
7594                    value = False
7595                elif value in ('Yes', 'On'):
7596                    value = True
7597            result[key] = (name.strip(), value)
7598            if unit:
7599                result[key] += (unit,)
7600            key = None
7601        else:
7602            result[''] += (astype(line, (int, float)),)
7603    return result
7604
7605
7606def read_nih_image_header(fh, byteorder, dtype, count, offsetsize):
7607    """Read NIH_IMAGE_HEADER tag from file and return as dict."""
7608    a = fh.read_record(TIFF.NIH_IMAGE_HEADER, byteorder=byteorder)
7609    a = a.newbyteorder(byteorder)
7610    a = recarray2dict(a)
7611    a['XUnit'] = a['XUnit'][:a['XUnitSize']]
7612    a['UM'] = a['UM'][:a['UMsize']]
7613    return a
7614
7615
7616def read_scanimage_metadata(fh):
7617    """Read ScanImage BigTIFF v3 static and ROI metadata from open file.
7618
7619    Return non-varying frame data as dict and ROI group data as JSON.
7620
7621    The settings can be used to read image data and metadata without parsing
7622    the TIFF file.
7623
7624    Raise ValueError if file does not contain valid ScanImage v3 metadata.
7625
7626    """
7627    fh.seek(0)
7628    try:
7629        byteorder, version = struct.unpack('<2sH', fh.read(4))
7630        if byteorder != b'II' or version != 43:
7631            raise Exception
7632        fh.seek(16)
7633        magic, version, size0, size1 = struct.unpack('<IIII', fh.read(16))
7634        if magic != 117637889 or version != 3:
7635            raise Exception
7636    except Exception:
7637        raise ValueError('not a ScanImage BigTIFF v3 file')
7638
7639    frame_data = matlabstr2py(bytes2str(fh.read(size0)[:-1]))
7640    roi_data = read_json(fh, '<', None, size1, None) if size1 > 1 else {}
7641    return frame_data, roi_data
7642
7643
7644def read_micromanager_metadata(fh):
7645    """Read MicroManager non-TIFF settings from open file and return as dict.
7646
7647    The settings can be used to read image data without parsing the TIFF file.
7648
7649    Raise ValueError if the file does not contain valid MicroManager metadata.
7650
7651    """
7652    fh.seek(0)
7653    try:
7654        byteorder = {b'II': '<', b'MM': '>'}[fh.read(2)]
7655    except IndexError:
7656        raise ValueError('not a MicroManager TIFF file')
7657
7658    result = {}
7659    fh.seek(8)
7660    (index_header, index_offset, display_header, display_offset,
7661     comments_header, comments_offset, summary_header, summary_length
7662     ) = struct.unpack(byteorder + 'IIIIIIII', fh.read(32))
7663
7664    if summary_header != 2355492:
7665        raise ValueError('invalid MicroManager summary header')
7666    result['Summary'] = read_json(fh, byteorder, None, summary_length, None)
7667
7668    if index_header != 54773648:
7669        raise ValueError('invalid MicroManager index header')
7670    fh.seek(index_offset)
7671    header, count = struct.unpack(byteorder + 'II', fh.read(8))
7672    if header != 3453623:
7673        raise ValueError('invalid MicroManager index header')
7674    data = struct.unpack(byteorder + 'IIIII'*count, fh.read(20*count))
7675    result['IndexMap'] = {'Channel': data[::5],
7676                          'Slice': data[1::5],
7677                          'Frame': data[2::5],
7678                          'Position': data[3::5],
7679                          'Offset': data[4::5]}
7680
7681    if display_header != 483765892:
7682        raise ValueError('invalid MicroManager display header')
7683    fh.seek(display_offset)
7684    header, count = struct.unpack(byteorder + 'II', fh.read(8))
7685    if header != 347834724:
7686        raise ValueError('invalid MicroManager display header')
7687    result['DisplaySettings'] = read_json(fh, byteorder, None, count, None)
7688
7689    if comments_header != 99384722:
7690        raise ValueError('invalid MicroManager comments header')
7691    fh.seek(comments_offset)
7692    header, count = struct.unpack(byteorder + 'II', fh.read(8))
7693    if header != 84720485:
7694        raise ValueError('invalid MicroManager comments header')
7695    result['Comments'] = read_json(fh, byteorder, None, count, None)
7696
7697    return result
7698
7699
7700def read_metaseries_catalog(fh):
7701    """Read MetaSeries non-TIFF hint catalog from file.
7702
7703    Raise ValueError if the file does not contain a valid hint catalog.
7704
7705    """
7706    # TODO: implement read_metaseries_catalog
7707    raise NotImplementedError()
7708
7709
7710def imagej_metadata_tags(metadata, byteorder):
7711    """Return IJMetadata and IJMetadataByteCounts tags from metadata dict.
7712
7713    The tags can be passed to the TiffWriter.save function as extratags.
7714
7715    The metadata dict may contain the following keys and values:
7716
7717        Info : str
7718            Human-readable information as string.
7719        Labels : sequence of str
7720            Human-readable labels for each channel.
7721        Ranges : sequence of doubles
7722            Lower and upper values for each channel.
7723        LUTs : sequence of (3, 256) uint8 ndarrays
7724            Color palettes for each channel.
7725        Plot : bytes
7726            Undocumented ImageJ internal format.
7727        ROI: bytes
7728            Undocumented ImageJ internal region of interest format.
7729        Overlays : bytes
7730            Undocumented ImageJ internal format.
7731
7732    """
7733    header = [{'>': b'IJIJ', '<': b'JIJI'}[byteorder]]
7734    bytecounts = [0]
7735    body = []
7736
7737    def _string(data, byteorder):
7738        return data.encode('utf-16' + {'>': 'be', '<': 'le'}[byteorder])
7739
7740    def _doubles(data, byteorder):
7741        return struct.pack(byteorder+('d' * len(data)), *data)
7742
7743    def _ndarray(data, byteorder):
7744        return data.tobytes()
7745
7746    def _bytes(data, byteorder):
7747        return data
7748
7749    metadata_types = (
7750        ('Info', b'info', 1, _string),
7751        ('Labels', b'labl', None, _string),
7752        ('Ranges', b'rang', 1, _doubles),
7753        ('LUTs', b'luts', None, _ndarray),
7754        ('Plot', b'plot', 1, _bytes),
7755        ('ROI', b'roi ', 1, _bytes),
7756        ('Overlays', b'over', None, _bytes))
7757
7758    for key, mtype, count, func in metadata_types:
7759        if key.lower() in metadata:
7760            key = key.lower()
7761        elif key not in metadata:
7762            continue
7763        if byteorder == '<':
7764            mtype = mtype[::-1]
7765        values = metadata[key]
7766        if count is None:
7767            count = len(values)
7768        else:
7769            values = [values]
7770        header.append(mtype + struct.pack(byteorder+'I', count))
7771        for value in values:
7772            data = func(value, byteorder)
7773            body.append(data)
7774            bytecounts.append(len(data))
7775
7776    if not body:
7777        return ()
7778    body = b''.join(body)
7779    header = b''.join(header)
7780    data = header + body
7781    bytecounts[0] = len(header)
7782    bytecounts = struct.pack(byteorder+('I' * len(bytecounts)), *bytecounts)
7783    return ((50839, 'B', len(data), data, True),
7784            (50838, 'I', len(bytecounts)//4, bytecounts, True))
7785
7786
7787def imagej_metadata(data, bytecounts, byteorder):
7788    """Return IJMetadata tag value as dict.
7789
7790    The 'Info' string can have multiple formats, e.g. OIF or ScanImage,
7791    that might be parsed into dicts using the matlabstr2py or
7792    oiffile.SettingsFile functions.
7793
7794    """
7795    def _string(data, byteorder):
7796        return data.decode('utf-16' + {'>': 'be', '<': 'le'}[byteorder])
7797
7798    def _doubles(data, byteorder):
7799        return struct.unpack(byteorder+('d' * (len(data) // 8)), data)
7800
7801    def _lut(data, byteorder):
7802        return numpy.frombuffer(data, 'uint8').reshape(-1, 256)
7803
7804    def _bytes(data, byteorder):
7805        return data
7806
7807    metadata_types = {  # big-endian
7808        b'info': ('Info', _string),
7809        b'labl': ('Labels', _string),
7810        b'rang': ('Ranges', _doubles),
7811        b'luts': ('LUTs', _lut),
7812        b'plot': ('Plots', _bytes),
7813        b'roi ': ('ROI', _bytes),
7814        b'over': ('Overlays', _bytes)}
7815    metadata_types.update(  # little-endian
7816        dict((k[::-1], v) for k, v in metadata_types.items()))
7817
7818    if not bytecounts:
7819        raise ValueError('no ImageJ metadata')
7820
7821    if data[:4] not in (b'IJIJ', b'JIJI'):
7822        raise ValueError('invalid ImageJ metadata')
7823
7824    header_size = bytecounts[0]
7825    if header_size < 12 or header_size > 804:
7826        raise ValueError('invalid ImageJ metadata header size')
7827
7828    ntypes = (header_size - 4) // 8
7829    header = struct.unpack(byteorder+'4sI'*ntypes, data[4:4+ntypes*8])
7830    pos = 4 + ntypes * 8
7831    counter = 0
7832    result = {}
7833    for mtype, count in zip(header[::2], header[1::2]):
7834        values = []
7835        name, func = metadata_types.get(mtype, (bytes2str(mtype), read_bytes))
7836        for _ in range(count):
7837            counter += 1
7838            pos1 = pos + bytecounts[counter]
7839            values.append(func(data[pos:pos1], byteorder))
7840            pos = pos1
7841        result[name.strip()] = values[0] if count == 1 else values
7842    return result
7843
7844
7845def imagej_description_metadata(description):
7846    """Return metatata from ImageJ image description as dict.
7847
7848    Raise ValueError if not a valid ImageJ description.
7849
7850    >>> description = 'ImageJ=1.11a\\nimages=510\\nhyperstack=true\\n'
7851    >>> imagej_description_metadata(description)  # doctest: +SKIP
7852    {'ImageJ': '1.11a', 'images': 510, 'hyperstack': True}
7853
7854    """
7855    def _bool(val):
7856        return {'true': True, 'false': False}[val.lower()]
7857
7858    result = {}
7859    for line in description.splitlines():
7860        try:
7861            key, val = line.split('=')
7862        except Exception:
7863            continue
7864        key = key.strip()
7865        val = val.strip()
7866        for dtype in (int, float, _bool):
7867            try:
7868                val = dtype(val)
7869                break
7870            except Exception:
7871                pass
7872        result[key] = val
7873
7874    if 'ImageJ' not in result:
7875        raise ValueError('not a ImageJ image description')
7876    return result
7877
7878
7879def imagej_description(shape, rgb=None, colormaped=False, version='1.11a',
7880                       hyperstack=None, mode=None, loop=None, **kwargs):
7881    """Return ImageJ image description from data shape.
7882
7883    ImageJ can handle up to 6 dimensions in order TZCYXS.
7884
7885    >>> imagej_description((51, 5, 2, 196, 171))  # doctest: +SKIP
7886    ImageJ=1.11a
7887    images=510
7888    channels=2
7889    slices=5
7890    frames=51
7891    hyperstack=true
7892    mode=grayscale
7893    loop=false
7894
7895    """
7896    if colormaped:
7897        raise NotImplementedError('ImageJ colormapping not supported')
7898    shape = imagej_shape(shape, rgb=rgb)
7899    rgb = shape[-1] in (3, 4)
7900
7901    result = ['ImageJ=%s' % version]
7902    append = []
7903    result.append('images=%i' % product(shape[:-3]))
7904    if hyperstack is None:
7905        hyperstack = True
7906        append.append('hyperstack=true')
7907    else:
7908        append.append('hyperstack=%s' % bool(hyperstack))
7909    if shape[2] > 1:
7910        result.append('channels=%i' % shape[2])
7911    if mode is None and not rgb:
7912        mode = 'grayscale'
7913    if hyperstack and mode:
7914        append.append('mode=%s' % mode)
7915    if shape[1] > 1:
7916        result.append('slices=%i' % shape[1])
7917    if shape[0] > 1:
7918        result.append('frames=%i' % shape[0])
7919        if loop is None:
7920            append.append('loop=false')
7921    if loop is not None:
7922        append.append('loop=%s' % bool(loop))
7923    for key, value in kwargs.items():
7924        append.append('%s=%s' % (key.lower(), value))
7925
7926    return '\n'.join(result + append + [''])
7927
7928
7929def imagej_shape(shape, rgb=None):
7930    """Return shape normalized to 6D ImageJ hyperstack TZCYXS.
7931
7932    Raise ValueError if not a valid ImageJ hyperstack shape.
7933
7934    >>> imagej_shape((2, 3, 4, 5, 3), False)
7935    (2, 3, 4, 5, 3, 1)
7936
7937    """
7938    shape = tuple(int(i) for i in shape)
7939    ndim = len(shape)
7940    if 1 > ndim > 6:
7941        raise ValueError('invalid ImageJ hyperstack: not 2 to 6 dimensional')
7942    if rgb is None:
7943        rgb = shape[-1] in (3, 4) and ndim > 2
7944    if rgb and shape[-1] not in (3, 4):
7945        raise ValueError('invalid ImageJ hyperstack: not a RGB image')
7946    if not rgb and ndim == 6 and shape[-1] != 1:
7947        raise ValueError('invalid ImageJ hyperstack: not a non-RGB image')
7948    if rgb or shape[-1] == 1:
7949        return (1, ) * (6 - ndim) + shape
7950    return (1, ) * (5 - ndim) + shape + (1,)
7951
7952
7953def json_description(shape, **metadata):
7954    """Return JSON image description from data shape and other meta data.
7955
7956    Return UTF-8 encoded JSON.
7957
7958    >>> json_description((256, 256, 3), axes='YXS')  # doctest: +SKIP
7959    b'{"shape": [256, 256, 3], "axes": "YXS"}'
7960
7961    """
7962    metadata.update(shape=shape)
7963    return json.dumps(metadata)  # .encode('utf-8')
7964
7965
7966def json_description_metadata(description):
7967    """Return metatata from JSON formated image description as dict.
7968
7969    Raise ValuError if description is of unknown format.
7970
7971    >>> description = '{"shape": [256, 256, 3], "axes": "YXS"}'
7972    >>> json_description_metadata(description)  # doctest: +SKIP
7973    {'shape': [256, 256, 3], 'axes': 'YXS'}
7974    >>> json_description_metadata('shape=(256, 256, 3)')
7975    {'shape': (256, 256, 3)}
7976
7977    """
7978    if description[:6] == 'shape=':
7979        # old style 'shaped' description; not JSON
7980        shape = tuple(int(i) for i in description[7:-1].split(','))
7981        return dict(shape=shape)
7982    if description[:1] == '{' and description[-1:] == '}':
7983        # JSON description
7984        return json.loads(description)
7985    raise ValueError('invalid JSON image description', description)
7986
7987
7988def fluoview_description_metadata(description, ignoresections=None):
7989    """Return metatata from FluoView image description as dict.
7990
7991    The FluoView image description format is unspecified. Expect failures.
7992
7993    >>> descr = ('[Intensity Mapping]\\nMap Ch0: Range=00000 to 02047\\n'
7994    ...          '[Intensity Mapping End]')
7995    >>> fluoview_description_metadata(descr)
7996    {'Intensity Mapping': {'Map Ch0: Range': '00000 to 02047'}}
7997
7998    """
7999    if not description.startswith('['):
8000        raise ValueError('invalid FluoView image description')
8001    if ignoresections is None:
8002        ignoresections = {'Region Info (Fields)', 'Protocol Description'}
8003
8004    result = {}
8005    sections = [result]
8006    comment = False
8007    for line in description.splitlines():
8008        if not comment:
8009            line = line.strip()
8010        if not line:
8011            continue
8012        if line[0] == '[':
8013            if line[-5:] == ' End]':
8014                # close section
8015                del sections[-1]
8016                section = sections[-1]
8017                name = line[1:-5]
8018                if comment:
8019                    section[name] = '\n'.join(section[name])
8020                if name[:4] == 'LUT ':
8021                    a = numpy.array(section[name], dtype='uint8')
8022                    a.shape = -1, 3
8023                    section[name] = a
8024                continue
8025            # new section
8026            comment = False
8027            name = line[1:-1]
8028            if name[:4] == 'LUT ':
8029                section = []
8030            elif name in ignoresections:
8031                section = []
8032                comment = True
8033            else:
8034                section = {}
8035            sections.append(section)
8036            result[name] = section
8037            continue
8038        # add entry
8039        if comment:
8040            section.append(line)
8041            continue
8042        line = line.split('=', 1)
8043        if len(line) == 1:
8044            section[line[0].strip()] = None
8045            continue
8046        key, value = line
8047        if key[:4] == 'RGB ':
8048            section.extend(int(rgb) for rgb in value.split())
8049        else:
8050            section[key.strip()] = astype(value.strip())
8051    return result
8052
8053
8054def pilatus_description_metadata(description):
8055    """Return metatata from Pilatus image description as dict.
8056
8057    Return metadata from Pilatus pixel array detectors by Dectris, created
8058    by camserver or TVX software.
8059
8060    >>> pilatus_description_metadata('# Pixel_size 172e-6 m x 172e-6 m')
8061    {'Pixel_size': (0.000172, 0.000172)}
8062
8063    """
8064    result = {}
8065    if not description.startswith('# '):
8066        return result
8067    for c in '#:=,()':
8068        description = description.replace(c, ' ')
8069    for line in description.split('\n'):
8070        if line[:2] != '  ':
8071            continue
8072        line = line.split()
8073        name = line[0]
8074        if line[0] not in TIFF.PILATUS_HEADER:
8075            try:
8076                result['DateTime'] = datetime.datetime.strptime(
8077                    ' '.join(line), '%Y-%m-%dT%H %M %S.%f')
8078            except Exception:
8079                result[name] = ' '.join(line[1:])
8080            continue
8081        indices, dtype = TIFF.PILATUS_HEADER[line[0]]
8082        if isinstance(indices[0], slice):
8083            # assumes one slice
8084            values = line[indices[0]]
8085        else:
8086            values = [line[i] for i in indices]
8087        if dtype is float and values[0] == 'not':
8088            values = ['NaN']
8089        values = tuple(dtype(v) for v in values)
8090        if dtype == str:
8091            values = ' '.join(values)
8092        elif len(values) == 1:
8093            values = values[0]
8094        result[name] = values
8095    return result
8096
8097
8098def svs_description_metadata(description):
8099    """Return metatata from Aperio image description as dict.
8100
8101    The Aperio image description format is unspecified. Expect failures.
8102
8103    >>> svs_description_metadata('Aperio Image Library v1.0')
8104    {'Aperio Image Library': 'v1.0'}
8105
8106    """
8107    if not description.startswith('Aperio Image Library '):
8108        raise ValueError('invalid Aperio image description')
8109    result = {}
8110    lines = description.split('\n')
8111    key, value = lines[0].strip().rsplit(None, 1)  # 'Aperio Image Library'
8112    result[key.strip()] = value.strip()
8113    if len(lines) == 1:
8114        return result
8115    items = lines[1].split('|')
8116    result[''] = items[0].strip()  # TODO: parse this?
8117    for item in items[1:]:
8118        key, value = item.split(' = ')
8119        result[key.strip()] = astype(value.strip())
8120    return result
8121
8122
8123def stk_description_metadata(description):
8124    """Return metadata from MetaMorph image description as list of dict.
8125
8126    The MetaMorph image description format is unspecified. Expect failures.
8127
8128    """
8129    description = description.strip()
8130    if not description:
8131        return []
8132    try:
8133        description = bytes2str(description)
8134    except UnicodeDecodeError:
8135        warnings.warn('failed to parse MetaMorph image description')
8136        return []
8137    result = []
8138    for plane in description.split('\x00'):
8139        d = {}
8140        for line in plane.split('\r\n'):
8141            line = line.split(':', 1)
8142            if len(line) > 1:
8143                name, value = line
8144                d[name.strip()] = astype(value.strip())
8145            else:
8146                value = line[0].strip()
8147                if value:
8148                    if '' in d:
8149                        d[''].append(value)
8150                    else:
8151                        d[''] = [value]
8152        result.append(d)
8153    return result
8154
8155
8156def metaseries_description_metadata(description):
8157    """Return metatata from MetaSeries image description as dict."""
8158    if not description.startswith('<MetaData>'):
8159        raise ValueError('invalid MetaSeries image description')
8160
8161    from xml.etree import cElementTree as etree  # delayed import
8162    root = etree.fromstring(description)
8163    types = {'float': float, 'int': int,
8164             'bool': lambda x: asbool(x, 'on', 'off')}
8165
8166    def parse(root, result):
8167        # recursive
8168        for child in root:
8169            attrib = child.attrib
8170            if not attrib:
8171                result[child.tag] = parse(child, {})
8172                continue
8173            if 'id' in attrib:
8174                i = attrib['id']
8175                t = attrib['type']
8176                v = attrib['value']
8177                if t in types:
8178                    result[i] = types[t](v)
8179                else:
8180                    result[i] = v
8181        return result
8182
8183    adict = parse(root, {})
8184    if 'Description' in adict:
8185        adict['Description'] = adict['Description'].replace('&#13;&#10;', '\n')
8186    return adict
8187
8188
8189def scanimage_description_metadata(description):
8190    """Return metatata from ScanImage image description as dict."""
8191    return matlabstr2py(description)
8192
8193
8194def scanimage_artist_metadata(artist):
8195    """Return metatata from ScanImage artist tag as dict."""
8196    try:
8197        return json.loads(artist)
8198    except ValueError:
8199        warnings.warn("invalid JSON '%s'" % artist)
8200
8201
8202def _replace_by(module_function, package=__package__, warn=None, prefix='_'):
8203    """Try replace decorated function by module.function."""
8204    return lambda f: f  # imageio: just use what's in here
8205    def _warn(e, warn):
8206        if warn is None:
8207            warn = '\n  Functionality might be degraded or be slow.\n'
8208        elif warn is True:
8209            warn = ''
8210        elif not warn:
8211            return
8212        warnings.warn('%s%s' % (e, warn))
8213
8214    try:
8215        from importlib import import_module
8216    except ImportError as e:
8217        _warn(e, warn)
8218        return identityfunc
8219
8220    def decorate(func, module_function=module_function, warn=warn):
8221        module, function = module_function.split('.')
8222        try:
8223            if package:
8224                module = import_module('.' + module, package=package)
8225            else:
8226                module = import_module(module)
8227        except Exception as e:
8228            _warn(e, warn)
8229            return func
8230        try:
8231            func, oldfunc = getattr(module, function), func
8232        except Exception as e:
8233            _warn(e, warn)
8234            return func
8235        globals()[prefix + func.__name__] = oldfunc
8236        return func
8237
8238    return decorate
8239
8240
8241def decode_floats(data):
8242    """Decode floating point horizontal differencing.
8243
8244    The TIFF predictor type 3 reorders the bytes of the image values and
8245    applies horizontal byte differencing to improve compression of floating
8246    point images. The ordering of interleaved color channels is preserved.
8247
8248    Parameters
8249    ----------
8250    data : numpy.ndarray
8251        The image to be decoded. The dtype must be a floating point.
8252        The shape must include the number of contiguous samples per pixel
8253        even if 1.
8254
8255    """
8256    shape = data.shape
8257    dtype = data.dtype
8258    if len(shape) < 3:
8259        raise ValueError('invalid data shape')
8260    if dtype.char not in 'dfe':
8261        raise ValueError('not a floating point image')
8262    littleendian = data.dtype.byteorder == '<' or (
8263        sys.byteorder == 'little' and data.dtype.byteorder == '=')
8264    # undo horizontal byte differencing
8265    data = data.view('uint8')
8266    data.shape = shape[:-2] + (-1,) + shape[-1:]
8267    numpy.cumsum(data, axis=-2, dtype='uint8', out=data)
8268    # reorder bytes
8269    if littleendian:
8270        data.shape = shape[:-2] + (-1,) + shape[-2:]
8271    data = numpy.swapaxes(data, -3, -2)
8272    data = numpy.swapaxes(data, -2, -1)
8273    data = data[..., ::-1]
8274    # back to float
8275    data = numpy.ascontiguousarray(data)
8276    data = data.view(dtype)
8277    data.shape = shape
8278    return data
8279
8280
8281@_replace_by('_tifffile.decode_packbits')
8282def decode_packbits(encoded):
8283    """Decompress PackBits encoded byte string.
8284
8285    PackBits is a simple byte-oriented run-length compression scheme.
8286
8287    """
8288    func = ord if sys.version[0] == '2' else identityfunc
8289    result = []
8290    result_extend = result.extend
8291    i = 0
8292    try:
8293        while True:
8294            n = func(encoded[i]) + 1
8295            i += 1
8296            if n < 129:
8297                result_extend(encoded[i:i+n])
8298                i += n
8299            elif n > 129:
8300                result_extend(encoded[i:i+1] * (258-n))
8301                i += 1
8302    except IndexError:
8303        pass
8304    return b''.join(result) if sys.version[0] == '2' else bytes(result)
8305
8306
8307@_replace_by('_tifffile.decode_lzw')
8308def decode_lzw(encoded):
8309    """Decompress LZW (Lempel-Ziv-Welch) encoded TIFF strip (byte string).
8310
8311    The strip must begin with a CLEAR code and end with an EOI code.
8312
8313    This implementation of the LZW decoding algorithm is described in (1) and
8314    is not compatible with old style LZW compressed files like quad-lzw.tif.
8315
8316    """
8317    len_encoded = len(encoded)
8318    bitcount_max = len_encoded * 8
8319    unpack = struct.unpack
8320
8321    if sys.version[0] == '2':
8322        newtable = [chr(i) for i in range(256)]
8323    else:
8324        newtable = [bytes([i]) for i in range(256)]
8325    newtable.extend((0, 0))
8326
8327    def next_code():
8328        """Return integer of 'bitw' bits at 'bitcount' position in encoded."""
8329        start = bitcount // 8
8330        s = encoded[start:start+4]
8331        try:
8332            code = unpack('>I', s)[0]
8333        except Exception:
8334            code = unpack('>I', s + b'\x00'*(4-len(s)))[0]
8335        code <<= bitcount % 8
8336        code &= mask
8337        return code >> shr
8338
8339    switchbitch = {  # code: bit-width, shr-bits, bit-mask
8340        255: (9, 23, int(9*'1'+'0'*23, 2)),
8341        511: (10, 22, int(10*'1'+'0'*22, 2)),
8342        1023: (11, 21, int(11*'1'+'0'*21, 2)),
8343        2047: (12, 20, int(12*'1'+'0'*20, 2)), }
8344    bitw, shr, mask = switchbitch[255]
8345    bitcount = 0
8346
8347    if len_encoded < 4:
8348        raise ValueError('strip must be at least 4 characters long')
8349
8350    if next_code() != 256:
8351        raise ValueError('strip must begin with CLEAR code')
8352
8353    code = 0
8354    oldcode = 0
8355    result = []
8356    result_append = result.append
8357    while True:
8358        code = next_code()  # ~5% faster when inlining this function
8359        bitcount += bitw
8360        if code == 257 or bitcount >= bitcount_max:  # EOI
8361            break
8362        if code == 256:  # CLEAR
8363            table = newtable[:]
8364            table_append = table.append
8365            lentable = 258
8366            bitw, shr, mask = switchbitch[255]
8367            code = next_code()
8368            bitcount += bitw
8369            if code == 257:  # EOI
8370                break
8371            result_append(table[code])
8372        else:
8373            if code < lentable:
8374                decoded = table[code]
8375                newcode = table[oldcode] + decoded[:1]
8376            else:
8377                newcode = table[oldcode]
8378                newcode += newcode[:1]
8379                decoded = newcode
8380            result_append(decoded)
8381            table_append(newcode)
8382            lentable += 1
8383        oldcode = code
8384        if lentable in switchbitch:
8385            bitw, shr, mask = switchbitch[lentable]
8386
8387    if code != 257:
8388        warnings.warn('unexpected end of LZW stream (code %i)' % code)
8389
8390    return b''.join(result)
8391
8392
8393@_replace_by('_tifffile.unpack_ints')
8394def unpack_ints(data, dtype, itemsize, runlen=0):
8395    """Decompress byte string to array of integers of any bit size <= 32.
8396
8397    This Python implementation is slow and only handles itemsizes 1, 2, 4, 8,
8398    16, 32, and 64.
8399
8400    Parameters
8401    ----------
8402    data : byte str
8403        Data to decompress.
8404    dtype : numpy.dtype or str
8405        A numpy boolean or integer type.
8406    itemsize : int
8407        Number of bits per integer.
8408    runlen : int
8409        Number of consecutive integers, after which to start at next byte.
8410
8411    Examples
8412    --------
8413    >>> unpack_ints(b'a', 'B', 1)
8414    array([0, 1, 1, 0, 0, 0, 0, 1], dtype=uint8)
8415    >>> unpack_ints(b'ab', 'B', 2)
8416    array([1, 2, 0, 1, 1, 2, 0, 2], dtype=uint8)
8417
8418    """
8419    if itemsize == 1:  # bitarray
8420        data = numpy.frombuffer(data, '|B')
8421        data = numpy.unpackbits(data)
8422        if runlen % 8:
8423            data = data.reshape(-1, runlen + (8 - runlen % 8))
8424            data = data[:, :runlen].reshape(-1)
8425        return data.astype(dtype)
8426
8427    dtype = numpy.dtype(dtype)
8428    if itemsize in (8, 16, 32, 64):
8429        return numpy.frombuffer(data, dtype)
8430    if itemsize not in (1, 2, 4, 8, 16, 32):
8431        raise ValueError('itemsize not supported: %i' % itemsize)
8432    if dtype.kind not in 'biu':
8433        raise ValueError('invalid dtype')
8434
8435    itembytes = next(i for i in (1, 2, 4, 8) if 8 * i >= itemsize)
8436    if itembytes != dtype.itemsize:
8437        raise ValueError('dtype.itemsize too small')
8438    if runlen == 0:
8439        runlen = (8 * len(data)) // itemsize
8440    skipbits = runlen * itemsize % 8
8441    if skipbits:
8442        skipbits = 8 - skipbits
8443    shrbits = itembytes*8 - itemsize
8444    bitmask = int(itemsize*'1'+'0'*shrbits, 2)
8445    dtypestr = '>' + dtype.char  # dtype always big-endian?
8446
8447    unpack = struct.unpack
8448    size = runlen * (len(data)*8 // (runlen*itemsize + skipbits))
8449    result = numpy.empty((size,), dtype)
8450    bitcount = 0
8451    for i in range(size):
8452        start = bitcount // 8
8453        s = data[start:start+itembytes]
8454        try:
8455            code = unpack(dtypestr, s)[0]
8456        except Exception:
8457            code = unpack(dtypestr, s + b'\x00'*(itembytes-len(s)))[0]
8458        code <<= bitcount % 8
8459        code &= bitmask
8460        result[i] = code >> shrbits
8461        bitcount += itemsize
8462        if (i+1) % runlen == 0:
8463            bitcount += skipbits
8464    return result
8465
8466
8467def unpack_rgb(data, dtype='<B', bitspersample=(5, 6, 5), rescale=True):
8468    """Return array from byte string containing packed samples.
8469
8470    Use to unpack RGB565 or RGB555 to RGB888 format.
8471
8472    Parameters
8473    ----------
8474    data : byte str
8475        The data to be decoded. Samples in each pixel are stored consecutively.
8476        Pixels are aligned to 8, 16, or 32 bit boundaries.
8477    dtype : numpy.dtype
8478        The sample data type. The byteorder applies also to the data stream.
8479    bitspersample : tuple
8480        Number of bits for each sample in a pixel.
8481    rescale : bool
8482        Upscale samples to the number of bits in dtype.
8483
8484    Returns
8485    -------
8486    result : ndarray
8487        Flattened array of unpacked samples of native dtype.
8488
8489    Examples
8490    --------
8491    >>> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff)
8492    >>> print(unpack_rgb(data, '<B', (5, 6, 5), False))
8493    [ 1  1  1 31 63 31]
8494    >>> print(unpack_rgb(data, '<B', (5, 6, 5)))
8495    [  8   4   8 255 255 255]
8496    >>> print(unpack_rgb(data, '<B', (5, 5, 5)))
8497    [ 16   8   8 255 255 255]
8498
8499    """
8500    dtype = numpy.dtype(dtype)
8501    bits = int(numpy.sum(bitspersample))
8502    if not (bits <= 32 and all(i <= dtype.itemsize*8 for i in bitspersample)):
8503        raise ValueError('sample size not supported: %s' % str(bitspersample))
8504    dt = next(i for i in 'BHI' if numpy.dtype(i).itemsize*8 >= bits)
8505    data = numpy.frombuffer(data, dtype.byteorder+dt)
8506    result = numpy.empty((data.size, len(bitspersample)), dtype.char)
8507    for i, bps in enumerate(bitspersample):
8508        t = data >> int(numpy.sum(bitspersample[i+1:]))
8509        t &= int('0b'+'1'*bps, 2)
8510        if rescale:
8511            o = ((dtype.itemsize * 8) // bps + 1) * bps
8512            if o > data.dtype.itemsize * 8:
8513                t = t.astype('I')
8514            t *= (2**o - 1) // (2**bps - 1)
8515            t //= 2**(o - (dtype.itemsize * 8))
8516        result[:, i] = t
8517    return result.reshape(-1)
8518
8519
8520@_replace_by('_tifffile.reverse_bitorder')
8521def reverse_bitorder(data):
8522    """Reverse bits in each byte of byte string or numpy array.
8523
8524    Decode data where pixels with lower column values are stored in the
8525    lower-order bits of the bytes (FillOrder is LSB2MSB).
8526
8527    Parameters
8528    ----------
8529    data : byte string or ndarray
8530        The data to be bit reversed. If byte string, a new bit-reversed byte
8531        string is returned. Numpy arrays are bit-reversed in-place.
8532
8533    Examples
8534    --------
8535    >>> reverse_bitorder(b'\\x01\\x64')
8536    b'\\x80&'
8537    >>> data = numpy.array([1, 666], dtype='uint16')
8538    >>> reverse_bitorder(data)
8539    >>> data
8540    array([  128, 16473], dtype=uint16)
8541
8542    """
8543    try:
8544        view = data.view('uint8')
8545        numpy.take(TIFF.REVERSE_BITORDER_ARRAY, view, out=view)
8546    except AttributeError:
8547        return data.translate(TIFF.REVERSE_BITORDER_BYTES)
8548    except ValueError:
8549        raise NotImplementedError('slices of arrays not supported')
8550
8551
8552def apply_colormap(image, colormap, contig=True):
8553    """Return palette-colored image.
8554
8555    The image values are used to index the colormap on axis 1. The returned
8556    image is of shape image.shape+colormap.shape[0] and dtype colormap.dtype.
8557
8558    Parameters
8559    ----------
8560    image : numpy.ndarray
8561        Indexes into the colormap.
8562    colormap : numpy.ndarray
8563        RGB lookup table aka palette of shape (3, 2**bits_per_sample).
8564    contig : bool
8565        If True, return a contiguous array.
8566
8567    Examples
8568    --------
8569    >>> image = numpy.arange(256, dtype='uint8')
8570    >>> colormap = numpy.vstack([image, image, image]).astype('uint16') * 256
8571    >>> apply_colormap(image, colormap)[-1]
8572    array([65280, 65280, 65280], dtype=uint16)
8573
8574    """
8575    image = numpy.take(colormap, image, axis=1)
8576    image = numpy.rollaxis(image, 0, image.ndim)
8577    if contig:
8578        image = numpy.ascontiguousarray(image)
8579    return image
8580
8581
8582def reorient(image, orientation):
8583    """Return reoriented view of image array.
8584
8585    Parameters
8586    ----------
8587    image : numpy.ndarray
8588        Non-squeezed output of asarray() functions.
8589        Axes -3 and -2 must be image length and width respectively.
8590    orientation : int or str
8591        One of TIFF.ORIENTATION names or values.
8592
8593    """
8594    ORIENTATION = TIFF.ORIENTATION
8595    orientation = enumarg(ORIENTATION, orientation)
8596
8597    if orientation == ORIENTATION.TOPLEFT:
8598        return image
8599    elif orientation == ORIENTATION.TOPRIGHT:
8600        return image[..., ::-1, :]
8601    elif orientation == ORIENTATION.BOTLEFT:
8602        return image[..., ::-1, :, :]
8603    elif orientation == ORIENTATION.BOTRIGHT:
8604        return image[..., ::-1, ::-1, :]
8605    elif orientation == ORIENTATION.LEFTTOP:
8606        return numpy.swapaxes(image, -3, -2)
8607    elif orientation == ORIENTATION.RIGHTTOP:
8608        return numpy.swapaxes(image, -3, -2)[..., ::-1, :]
8609    elif orientation == ORIENTATION.RIGHTBOT:
8610        return numpy.swapaxes(image, -3, -2)[..., ::-1, :, :]
8611    elif orientation == ORIENTATION.LEFTBOT:
8612        return numpy.swapaxes(image, -3, -2)[..., ::-1, ::-1, :]
8613
8614
8615def repeat_nd(a, repeats):
8616    """Return read-only view into input array with elements repeated.
8617
8618    Zoom nD image by integer factors using nearest neighbor interpolation
8619    (box filter).
8620
8621    Parameters
8622    ----------
8623    a : array_like
8624        Input array.
8625    repeats : sequence of int
8626        The number of repetitions to apply along each dimension of input array.
8627
8628    Example
8629    -------
8630    >>> repeat_nd([[1, 2], [3, 4]], (2, 2))
8631    array([[1, 1, 2, 2],
8632           [1, 1, 2, 2],
8633           [3, 3, 4, 4],
8634           [3, 3, 4, 4]])
8635
8636    """
8637    a = numpy.asarray(a)
8638    reshape = []
8639    shape = []
8640    strides = []
8641    for i, j, k in zip(a.strides, a.shape, repeats):
8642        shape.extend((j, k))
8643        strides.extend((i, 0))
8644        reshape.append(j * k)
8645    return numpy.lib.stride_tricks.as_strided(
8646        a, shape, strides, writeable=False).reshape(reshape)
8647
8648
8649def reshape_nd(data_or_shape, ndim):
8650    """Return image array or shape with at least ndim dimensions.
8651
8652    Prepend 1s to image shape as necessary.
8653
8654    >>> reshape_nd(numpy.empty(0), 1).shape
8655    (0,)
8656    >>> reshape_nd(numpy.empty(1), 2).shape
8657    (1, 1)
8658    >>> reshape_nd(numpy.empty((2, 3)), 3).shape
8659    (1, 2, 3)
8660    >>> reshape_nd(numpy.empty((3, 4, 5)), 3).shape
8661    (3, 4, 5)
8662    >>> reshape_nd((2, 3), 3)
8663    (1, 2, 3)
8664
8665    """
8666    is_shape = isinstance(data_or_shape, tuple)
8667    shape = data_or_shape if is_shape else data_or_shape.shape
8668    if len(shape) >= ndim:
8669        return data_or_shape
8670    shape = (1,) * (ndim - len(shape)) + shape
8671    return shape if is_shape else data_or_shape.reshape(shape)
8672
8673
8674def squeeze_axes(shape, axes, skip='XY'):
8675    """Return shape and axes with single-dimensional entries removed.
8676
8677    Remove unused dimensions unless their axes are listed in 'skip'.
8678
8679    >>> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC')
8680    ((5, 2, 1), 'TYX')
8681
8682    """
8683    if len(shape) != len(axes):
8684        raise ValueError('dimensions of axes and shape do not match')
8685    shape, axes = zip(*(i for i in zip(shape, axes)
8686                        if i[0] > 1 or i[1] in skip))
8687    return tuple(shape), ''.join(axes)
8688
8689
8690def transpose_axes(image, axes, asaxes='CTZYX'):
8691    """Return image with its axes permuted to match specified axes.
8692
8693    A view is returned if possible.
8694
8695    >>> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape
8696    (5, 2, 1, 3, 4)
8697
8698    """
8699    for ax in axes:
8700        if ax not in asaxes:
8701            raise ValueError('unknown axis %s' % ax)
8702    # add missing axes to image
8703    shape = image.shape
8704    for ax in reversed(asaxes):
8705        if ax not in axes:
8706            axes = ax + axes
8707            shape = (1,) + shape
8708    image = image.reshape(shape)
8709    # transpose axes
8710    image = image.transpose([axes.index(ax) for ax in asaxes])
8711    return image
8712
8713
8714def reshape_axes(axes, shape, newshape, unknown='Q'):
8715    """Return axes matching new shape.
8716
8717    Unknown dimensions are labelled 'Q'.
8718
8719    >>> reshape_axes('YXS', (219, 301, 1), (219, 301))
8720    'YX'
8721    >>> reshape_axes('IYX', (12, 219, 301), (3, 4, 219, 1, 301, 1))
8722    'QQYQXQ'
8723
8724    """
8725    shape = tuple(shape)
8726    newshape = tuple(newshape)
8727    if len(axes) != len(shape):
8728        raise ValueError('axes do not match shape')
8729
8730    size = product(shape)
8731    newsize = product(newshape)
8732    if size != newsize:
8733        raise ValueError('cannot reshape %s to %s' % (shape, newshape))
8734    if not axes or not newshape:
8735        return ''
8736
8737    lendiff = max(0, len(shape) - len(newshape))
8738    if lendiff:
8739        newshape = newshape + (1,) * lendiff
8740
8741    i = len(shape)-1
8742    prodns = 1
8743    prods = 1
8744    result = []
8745    for ns in newshape[::-1]:
8746        prodns *= ns
8747        while i > 0 and shape[i] == 1 and ns != 1:
8748            i -= 1
8749        if ns == shape[i] and prodns == prods*shape[i]:
8750            prods *= shape[i]
8751            result.append(axes[i])
8752            i -= 1
8753        else:
8754            result.append(unknown)
8755
8756    return ''.join(reversed(result[lendiff:]))
8757
8758
8759def stack_pages(pages, out=None, maxworkers=1, *args, **kwargs):
8760    """Read data from sequence of TiffPage and stack them vertically.
8761
8762    Additional parameters are passsed to the TiffPage.asarray function.
8763
8764    """
8765    npages = len(pages)
8766    if npages == 0:
8767        raise ValueError('no pages')
8768
8769    if npages == 1:
8770        return pages[0].asarray(out=out, *args, **kwargs)
8771
8772    page0 = next(p for p in pages if p is not None)
8773    page0.asarray(validate=None)  # ThreadPoolExecutor swallows exceptions
8774    shape = (npages,) + page0.keyframe.shape
8775    dtype = page0.keyframe.dtype
8776    out = create_output(out, shape, dtype)
8777
8778    if maxworkers is None:
8779        maxworkers = multiprocessing.cpu_count() // 2
8780    page0.parent.filehandle.lock = maxworkers > 1
8781
8782    filecache = OpenFileCache(size=max(4, maxworkers),
8783                              lock=page0.parent.filehandle.lock)
8784
8785    def func(page, index, out=out, filecache=filecache,
8786             args=args, kwargs=kwargs):
8787        """Read, decode, and copy page data."""
8788        if page is not None:
8789            filecache.open(page.parent.filehandle)
8790            out[index] = page.asarray(lock=filecache.lock, reopen=False,
8791                                      validate=False, *args, **kwargs)
8792            filecache.close(page.parent.filehandle)
8793
8794    if maxworkers < 2:
8795        for i, page in enumerate(pages):
8796            func(page, i)
8797    else:
8798        with concurrent.futures.ThreadPoolExecutor(maxworkers) as executor:
8799            executor.map(func, pages, range(npages))
8800
8801    filecache.clear()
8802    page0.parent.filehandle.lock = None
8803
8804    return out
8805
8806
8807def clean_offsets_counts(offsets, counts):
8808    """Return cleaned offsets and byte counts.
8809
8810    Remove zero offsets and counts. Use to sanitize _offsets and _bytecounts
8811    tag values for strips or tiles.
8812
8813    """
8814    offsets = list(offsets)
8815    counts = list(counts)
8816    assert len(offsets) == len(counts)
8817    j = 0
8818    for i, (o, b) in enumerate(zip(offsets, counts)):
8819        if o > 0 and b > 0:
8820            if i > j:
8821                offsets[j] = o
8822                counts[j] = b
8823            j += 1
8824        elif b > 0 and o <= 0:
8825            raise ValueError('invalid offset')
8826        else:
8827            warnings.warn('empty byte count')
8828    if j == 0:
8829        j = 1
8830    return offsets[:j], counts[:j]
8831
8832
8833def buffered_read(fh, lock, offsets, bytecounts, buffersize=2**26):
8834    """Return iterator over blocks read from file."""
8835    length = len(offsets)
8836    i = 0
8837    while i < length:
8838        data = []
8839        with lock:
8840            size = 0
8841            while size < buffersize and i < length:
8842                fh.seek(offsets[i])
8843                bytecount = bytecounts[i]
8844                data.append(fh.read(bytecount))
8845                size += bytecount
8846                i += 1
8847        for block in data:
8848            yield block
8849
8850
8851def create_output(out, shape, dtype, mode='w+', suffix='.memmap'):
8852    """Return numpy array where image data of shape and dtype can be copied.
8853
8854    The 'out' parameter may have the following values or types:
8855
8856    None
8857        An empty array of shape and dtype is created and returned.
8858    numpy.ndarray
8859        An existing writable array of compatible dtype and shape. A view of
8860        the same array is returned after verification.
8861    'memmap' or 'memmap:tempdir'
8862        A memory-map to an array stored in a temporary binary file on disk
8863        is created and returned.
8864    str or open file
8865        The file name or file object used to create a memory-map to an array
8866        stored in a binary file on disk. The created memory-mapped array is
8867        returned.
8868
8869    """
8870    if out is None:
8871        return numpy.zeros(shape, dtype)
8872    if isinstance(out, str) and out[:6] == 'memmap':
8873        tempdir = out[7:] if len(out) > 7 else None
8874        with tempfile.NamedTemporaryFile(dir=tempdir, suffix=suffix) as fh:
8875            return numpy.memmap(fh, shape=shape, dtype=dtype, mode=mode)
8876    if isinstance(out, numpy.ndarray):
8877        if product(shape) != product(out.shape):
8878            raise ValueError('incompatible output shape')
8879        if not numpy.can_cast(dtype, out.dtype):
8880            raise ValueError('incompatible output dtype')
8881        return out.reshape(shape)
8882    if isinstance(out, pathlib.Path):
8883        out = str(out)
8884    return numpy.memmap(out, shape=shape, dtype=dtype, mode=mode)
8885
8886
8887def matlabstr2py(string):
8888    """Return Python object from Matlab string representation.
8889
8890    Return str, bool, int, float, list (Matlab arrays or cells), or
8891    dict (Matlab structures) types.
8892
8893    Use to access ScanImage metadata.
8894
8895    >>> matlabstr2py('1')
8896    1
8897    >>> matlabstr2py("['x y z' true false; 1 2.0 -3e4; NaN Inf @class]")
8898    [['x y z', True, False], [1, 2.0, -30000.0], [nan, inf, '@class']]
8899    >>> d = matlabstr2py("SI.hChannels.channelType = {'stripe' 'stripe'}\\n"
8900    ...                  "SI.hChannels.channelsActive = 2")
8901    >>> d['SI.hChannels.channelType']
8902    ['stripe', 'stripe']
8903
8904    """
8905    # TODO: handle invalid input
8906    # TODO: review unboxing of multidimensional arrays
8907
8908    def lex(s):
8909        # return sequence of tokens from matlab string representation
8910        tokens = ['[']
8911        while True:
8912            t, i = next_token(s)
8913            if t is None:
8914                break
8915            if t == ';':
8916                tokens.extend((']', '['))
8917            elif t == '[':
8918                tokens.extend(('[', '['))
8919            elif t == ']':
8920                tokens.extend((']', ']'))
8921            else:
8922                tokens.append(t)
8923            s = s[i:]
8924        tokens.append(']')
8925        return tokens
8926
8927    def next_token(s):
8928        # return next token in matlab string
8929        length = len(s)
8930        if length == 0:
8931            return None, 0
8932        i = 0
8933        while i < length and s[i] == ' ':
8934            i += 1
8935        if i == length:
8936            return None, i
8937        if s[i] in '{[;]}':
8938            return s[i], i + 1
8939        if s[i] == "'":
8940            j = i + 1
8941            while j < length and s[j] != "'":
8942                j += 1
8943            return s[i: j+1], j + 1
8944        if s[i] == '<':
8945            j = i + 1
8946            while j < length and s[j] != '>':
8947                j += 1
8948            return s[i: j+1], j + 1
8949        j = i
8950        while j < length and s[j] not in ' {[;]}':
8951            j += 1
8952        return s[i:j], j
8953
8954    def value(s, fail=False):
8955        # return Python value of token
8956        s = s.strip()
8957        if not s:
8958            return s
8959        if len(s) == 1:
8960            try:
8961                return int(s)
8962            except Exception:
8963                if fail:
8964                    raise ValueError()
8965                return s
8966        if s[0] == "'":
8967            if fail and s[-1] != "'" or "'" in s[1:-1]:
8968                raise ValueError()
8969            return s[1:-1]
8970        if s[0] == '<':
8971            if fail and s[-1] != '>' or '<' in s[1:-1]:
8972                raise ValueError()
8973            return s
8974        if fail and any(i in s for i in " ';[]{}"):
8975            raise ValueError()
8976        if s[0] == '@':
8977            return s
8978        if s in ('true', 'True'):
8979            return True
8980        if s in ('false', 'False'):
8981            return False
8982        if s[:6] == 'zeros(':
8983            return numpy.zeros([int(i) for i in s[6:-1].split(',')]).tolist()
8984        if s[:5] == 'ones(':
8985            return numpy.ones([int(i) for i in s[5:-1].split(',')]).tolist()
8986        if '.' in s or 'e' in s:
8987            try:
8988                return float(s)
8989            except Exception:
8990                pass
8991        try:
8992            return int(s)
8993        except Exception:
8994            pass
8995        try:
8996            return float(s)  # nan, inf
8997        except Exception:
8998            if fail:
8999                raise ValueError()
9000        return s
9001
9002    def parse(s):
9003        # return Python value from string representation of Matlab value
9004        s = s.strip()
9005        try:
9006            return value(s, fail=True)
9007        except ValueError:
9008            pass
9009        result = add2 = []
9010        levels = [add2]
9011        for t in lex(s):
9012            if t in '[{':
9013                add2 = []
9014                levels.append(add2)
9015            elif t in ']}':
9016                x = levels.pop()
9017                if len(x) == 1 and isinstance(x[0], (list, str)):
9018                    x = x[0]
9019                add2 = levels[-1]
9020                add2.append(x)
9021            else:
9022                add2.append(value(t))
9023        if len(result) == 1 and isinstance(result[0], (list, str)):
9024            result = result[0]
9025        return result
9026
9027    if '\r' in string or '\n' in string:
9028        # structure
9029        d = {}
9030        for line in string.splitlines():
9031            line = line.strip()
9032            if not line or line[0] == '%':
9033                continue
9034            k, v = line.split('=', 1)
9035            k = k.strip()
9036            if any(c in k for c in " ';[]{}<>"):
9037                continue
9038            d[k] = parse(v)
9039        return d
9040    return parse(string)
9041
9042
9043def stripnull(string, null=b'\x00'):
9044    """Return string truncated at first null character.
9045
9046    Clean NULL terminated C strings. For unicode strings use null='\\0'.
9047
9048    >>> stripnull(b'string\\x00')
9049    b'string'
9050    >>> stripnull('string\\x00', null='\\0')
9051    'string'
9052
9053    """
9054    i = string.find(null)
9055    return string if (i < 0) else string[:i]
9056
9057
9058def stripascii(string):
9059    """Return string truncated at last byte that is 7-bit ASCII.
9060
9061    Clean NULL separated and terminated TIFF strings.
9062
9063    >>> stripascii(b'string\\x00string\\n\\x01\\x00')
9064    b'string\\x00string\\n'
9065    >>> stripascii(b'\\x00')
9066    b''
9067
9068    """
9069    # TODO: pythonize this
9070    i = len(string)
9071    while i:
9072        i -= 1
9073        if 8 < byte2int(string[i]) < 127:
9074            break
9075    else:
9076        i = -1
9077    return string[:i+1]
9078
9079
9080def asbool(value, true=(b'true', u'true'), false=(b'false', u'false')):
9081    """Return string as bool if possible, else raise TypeError.
9082
9083    >>> asbool(b' False ')
9084    False
9085
9086    """
9087    value = value.strip().lower()
9088    if value in true:  # might raise UnicodeWarning/BytesWarning
9089        return True
9090    if value in false:
9091        return False
9092    raise TypeError()
9093
9094
9095def astype(value, types=None):
9096    """Return argument as one of types if possible.
9097
9098    >>> astype('42')
9099    42
9100    >>> astype('3.14')
9101    3.14
9102    >>> astype('True')
9103    True
9104    >>> astype(b'Neee-Wom')
9105    'Neee-Wom'
9106
9107    """
9108    if types is None:
9109        types = int, float, asbool, bytes2str
9110    for typ in types:
9111        try:
9112            return typ(value)
9113        except (ValueError, AttributeError, TypeError, UnicodeEncodeError):
9114            pass
9115    return value
9116
9117
9118def format_size(size, threshold=1536):
9119    """Return file size as string from byte size.
9120
9121    >>> format_size(1234)
9122    '1234 B'
9123    >>> format_size(12345678901)
9124    '11.50 GiB'
9125
9126    """
9127    if size < threshold:
9128        return "%i B" % size
9129    for unit in ('KiB', 'MiB', 'GiB', 'TiB', 'PiB'):
9130        size /= 1024.0
9131        if size < threshold:
9132            return "%.2f %s" % (size, unit)
9133
9134
9135def identityfunc(arg):
9136    """Single argument identity function.
9137
9138    >>> identityfunc('arg')
9139    'arg'
9140
9141    """
9142    return arg
9143
9144
9145def nullfunc(*args, **kwargs):
9146    """Null function.
9147
9148    >>> nullfunc('arg', kwarg='kwarg')
9149
9150    """
9151    return
9152
9153
9154def sequence(value):
9155    """Return tuple containing value if value is not a sequence.
9156
9157    >>> sequence(1)
9158    (1,)
9159    >>> sequence([1])
9160    [1]
9161
9162    """
9163    try:
9164        len(value)
9165        return value
9166    except TypeError:
9167        return (value,)
9168
9169
9170def product(iterable):
9171    """Return product of sequence of numbers.
9172
9173    Equivalent of functools.reduce(operator.mul, iterable, 1).
9174    Multiplying numpy integers might overflow.
9175
9176    >>> product([2**8, 2**30])
9177    274877906944
9178    >>> product([])
9179    1
9180
9181    """
9182    prod = 1
9183    for i in iterable:
9184        prod *= i
9185    return prod
9186
9187
9188def natural_sorted(iterable):
9189    """Return human sorted list of strings.
9190
9191    E.g. for sorting file names.
9192
9193    >>> natural_sorted(['f1', 'f2', 'f10'])
9194    ['f1', 'f2', 'f10']
9195
9196    """
9197    def sortkey(x):
9198        return [(int(c) if c.isdigit() else c) for c in re.split(numbers, x)]
9199
9200    numbers = re.compile(r'(\d+)')
9201    return sorted(iterable, key=sortkey)
9202
9203
9204def excel_datetime(timestamp, epoch=datetime.datetime.fromordinal(693594)):
9205    """Return datetime object from timestamp in Excel serial format.
9206
9207    Convert LSM time stamps.
9208
9209    >>> excel_datetime(40237.029999999795)
9210    datetime.datetime(2010, 2, 28, 0, 43, 11, 999982)
9211
9212    """
9213    return epoch + datetime.timedelta(timestamp)
9214
9215
9216def julian_datetime(julianday, milisecond=0):
9217    """Return datetime from days since 1/1/4713 BC and ms since midnight.
9218
9219    Convert Julian dates according to MetaMorph.
9220
9221    >>> julian_datetime(2451576, 54362783)
9222    datetime.datetime(2000, 2, 2, 15, 6, 2, 783)
9223
9224    """
9225    if julianday <= 1721423:
9226        # no datetime before year 1
9227        return None
9228
9229    a = julianday + 1
9230    if a > 2299160:
9231        alpha = math.trunc((a - 1867216.25) / 36524.25)
9232        a += 1 + alpha - alpha // 4
9233    b = a + (1524 if a > 1721423 else 1158)
9234    c = math.trunc((b - 122.1) / 365.25)
9235    d = math.trunc(365.25 * c)
9236    e = math.trunc((b - d) / 30.6001)
9237
9238    day = b - d - math.trunc(30.6001 * e)
9239    month = e - (1 if e < 13.5 else 13)
9240    year = c - (4716 if month > 2.5 else 4715)
9241
9242    hour, milisecond = divmod(milisecond, 1000 * 60 * 60)
9243    minute, milisecond = divmod(milisecond, 1000 * 60)
9244    second, milisecond = divmod(milisecond, 1000)
9245
9246    return datetime.datetime(year, month, day,
9247                             hour, minute, second, milisecond)
9248
9249
9250def byteorder_isnative(byteorder):
9251    """Return if byteorder matches the system's byteorder.
9252
9253    >>> byteorder_isnative('=')
9254    True
9255
9256    """
9257    if byteorder == '=' or byteorder == sys.byteorder:
9258        return True
9259    keys = {'big': '>', 'little': '<'}
9260    return keys.get(byteorder, byteorder) == keys[sys.byteorder]
9261
9262
9263def recarray2dict(recarray):
9264    """Return numpy.recarray as dict."""
9265    # TODO: subarrays
9266    result = {}
9267    for descr, value in zip(recarray.dtype.descr, recarray):
9268        name, dtype = descr[:2]
9269        if dtype[1] == 'S':
9270            value = bytes2str(stripnull(value))
9271        elif value.ndim < 2:
9272            value = value.tolist()
9273        result[name] = value
9274    return result
9275
9276
9277def xml2dict(xml, sanitize=True, prefix=None):
9278    """Return XML as dict.
9279
9280    >>> xml2dict('<?xml version="1.0" ?><root attr="name"><key>1</key></root>')
9281    {'root': {'key': 1, 'attr': 'name'}}
9282
9283    """
9284    from xml.etree import cElementTree as etree  # delayed import
9285
9286    at = tx = ''
9287    if prefix:
9288        at, tx = prefix
9289
9290    def astype(value):
9291        # return value as int, float, bool, or str
9292        for t in (int, float, asbool):
9293            try:
9294                return t(value)
9295            except Exception:
9296                pass
9297        return value
9298
9299    def etree2dict(t):
9300        # adapted from https://stackoverflow.com/a/10077069/453463
9301        key = t.tag
9302        if sanitize:
9303            key = key.rsplit('}', 1)[-1]
9304        d = {key: {} if t.attrib else None}
9305        children = list(t)
9306        if children:
9307            dd = collections.defaultdict(list)
9308            for dc in map(etree2dict, children):
9309                for k, v in dc.items():
9310                    dd[k].append(astype(v))
9311            d = {key: {k: astype(v[0]) if len(v) == 1 else astype(v)
9312                       for k, v in dd.items()}}
9313        if t.attrib:
9314            d[key].update((at + k, astype(v)) for k, v in t.attrib.items())
9315        if t.text:
9316            text = t.text.strip()
9317            if children or t.attrib:
9318                if text:
9319                    d[key][tx + 'value'] = astype(text)
9320            else:
9321                d[key] = astype(text)
9322        return d
9323
9324    return etree2dict(etree.fromstring(xml))
9325
9326
9327def hexdump(bytestr, width=75, height=24, snipat=-2, modulo=2, ellipsis='...'):
9328    """Return hexdump representation of byte string.
9329
9330    >>> hexdump(binascii.unhexlify('49492a00080000000e00fe0004000100'))
9331    '49 49 2a 00 08 00 00 00 0e 00 fe 00 04 00 01 00 II*.............'
9332
9333    """
9334    size = len(bytestr)
9335    if size < 1 or width < 2 or height < 1:
9336        return ''
9337    if height == 1:
9338        addr = b''
9339        bytesperline = min(modulo * (((width - len(addr)) // 4) // modulo),
9340                           size)
9341        if bytesperline < 1:
9342            return ''
9343        nlines = 1
9344    else:
9345        addr = b'%%0%ix: ' % len(b'%x' % size)
9346        bytesperline = min(modulo * (((width - len(addr % 1)) // 4) // modulo),
9347                           size)
9348        if bytesperline < 1:
9349            return ''
9350        width = 3*bytesperline + len(addr % 1)
9351        nlines = (size - 1) // bytesperline + 1
9352
9353    if snipat is None or snipat == 1:
9354        snipat = height
9355    elif 0 < abs(snipat) < 1:
9356        snipat = int(math.floor(height * snipat))
9357    if snipat < 0:
9358        snipat += height
9359
9360    if height == 1 or nlines == 1:
9361        blocks = [(0, bytestr[:bytesperline])]
9362        addr = b''
9363        height = 1
9364        width = 3 * bytesperline
9365    elif height is None or nlines <= height:
9366        blocks = [(0, bytestr)]
9367    elif snipat <= 0:
9368        start = bytesperline * (nlines - height)
9369        blocks = [(start, bytestr[start:])]  # (start, None)
9370    elif snipat >= height or height < 3:
9371        end = bytesperline * height
9372        blocks = [(0, bytestr[:end])]  # (end, None)
9373    else:
9374        end1 = bytesperline * snipat
9375        end2 = bytesperline * (height - snipat - 1)
9376        blocks = [(0, bytestr[:end1]),
9377                  (size-end1-end2, None),
9378                  (size-end2, bytestr[size-end2:])]
9379
9380    ellipsis = str2bytes(ellipsis)
9381    result = []
9382    for start, bytestr in blocks:
9383        if bytestr is None:
9384            result.append(ellipsis)  # 'skip %i bytes' % start)
9385            continue
9386        hexstr = binascii.hexlify(bytestr)
9387        strstr = re.sub(br'[^\x20-\x7f]', b'.', bytestr)
9388        for i in range(0, len(bytestr), bytesperline):
9389            h = hexstr[2*i:2*i+bytesperline*2]
9390            r = (addr % (i + start)) if height > 1 else addr
9391            r += b' '.join(h[i:i+2] for i in range(0, 2*bytesperline, 2))
9392            r += b' ' * (width - len(r))
9393            r += strstr[i:i+bytesperline]
9394            result.append(r)
9395    result = b'\n'.join(result)
9396    if sys.version_info[0] == 3:
9397        result = result.decode('ascii')
9398    return result
9399
9400
9401def isprintable(string):
9402    """Return if all characters in string are printable.
9403
9404    >>> isprintable('abc')
9405    True
9406    >>> isprintable(b'\01')
9407    False
9408
9409    """
9410    string = string.strip()
9411    if len(string) < 1:
9412        return True
9413    if sys.version_info[0] == 3:
9414        try:
9415            return string.isprintable()
9416        except Exception:
9417            pass
9418        try:
9419            return string.decode('utf-8').isprintable()
9420        except Exception:
9421            pass
9422    else:
9423        if string.isalnum():
9424            return True
9425        printable = ('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRST'
9426                     'UVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c')
9427        return all(c in printable for c in string)
9428
9429
9430def clean_whitespace(string, compact=False):
9431    """Return string with compressed whitespace."""
9432    for a, b in (('\r\n', '\n'), ('\r', '\n'), ('\n\n', '\n'),
9433                 ('\t', ' '), ('  ', ' ')):
9434        string = string.replace(a, b)
9435    if compact:
9436        for a, b in (('\n', ' '), ('[ ', '['),
9437                     ('  ', ' '), ('  ', ' '), ('  ', ' ')):
9438            string = string.replace(a, b)
9439    return string.strip()
9440
9441
9442def pformat_xml(xml):
9443    """Return pretty formatted XML."""
9444    try:
9445        import lxml.etree as etree  # delayed import
9446        if not isinstance(xml, bytes):
9447            xml = xml.encode('utf-8')
9448        xml = etree.parse(io.BytesIO(xml))
9449        xml = etree.tostring(xml, pretty_print=True, xml_declaration=True,
9450                             encoding=xml.docinfo.encoding)
9451        xml = bytes2str(xml)
9452    except Exception:
9453        if isinstance(xml, bytes):
9454            xml = bytes2str(xml)
9455        xml = xml.replace('><', '>\n<')
9456    return xml.replace('  ', ' ').replace('\t', ' ')
9457
9458
9459def pformat(arg, width=79, height=24, compact=True):
9460    """Return pretty formatted representation of object as string.
9461
9462    Whitespace might be altered.
9463
9464    """
9465    if height is None or height < 1:
9466        height = 1024
9467    if width is None or width < 1:
9468        width = 256
9469
9470    npopt = numpy.get_printoptions()
9471    numpy.set_printoptions(threshold=100, linewidth=width)
9472
9473    if isinstance(arg, basestring):
9474        if arg[:5].lower() in ('<?xml', b'<?xml'):
9475            if height == 1:
9476                arg = arg[:4*width]
9477            else:
9478                arg = pformat_xml(arg)
9479        elif isinstance(arg, bytes):
9480            if isprintable(arg):
9481                arg = bytes2str(arg)
9482                arg = clean_whitespace(arg)
9483            else:
9484                numpy.set_printoptions(**npopt)
9485                return hexdump(arg, width=width, height=height, modulo=1)
9486        arg = arg.rstrip()
9487    elif isinstance(arg, numpy.record):
9488        arg = arg.pprint()
9489    else:
9490        import pprint  # delayed import
9491        compact = {} if sys.version_info[0] == 2 else dict(compact=compact)
9492        arg = pprint.pformat(arg, width=width, **compact)
9493
9494    numpy.set_printoptions(**npopt)
9495
9496    if height == 1:
9497        arg = clean_whitespace(arg, compact=True)
9498        return arg[:width]
9499
9500    argl = list(arg.splitlines())
9501    if len(argl) > height:
9502        arg = '\n'.join(argl[:height//2] + ['...'] + argl[-height//2:])
9503    return arg
9504
9505
9506def snipstr(string, width=79, snipat=0.5, ellipsis='...'):
9507    """Return string cut to specified length.
9508
9509    >>> snipstr('abcdefghijklmnop', 8)
9510    'abc...op'
9511
9512    """
9513    if ellipsis is None:
9514        if isinstance(string, bytes):
9515            ellipsis = b'...'
9516        else:
9517            ellipsis = u'\u2026'  # does not print on win-py3.5
9518    esize = len(ellipsis)
9519
9520    splitlines = string.splitlines()
9521    # TODO: finish and test multiline snip
9522
9523    result = []
9524    for line in splitlines:
9525        if line is None:
9526            result.append(ellipsis)
9527            continue
9528        linelen = len(line)
9529        if linelen <= width:
9530            result.append(string)
9531            continue
9532
9533        split = snipat
9534        if split is None or split == 1:
9535            split = linelen
9536        elif 0 < abs(split) < 1:
9537            split = int(math.floor(linelen * split))
9538        if split < 0:
9539            split += linelen
9540            if split < 0:
9541                split = 0
9542
9543        if esize == 0 or width < esize + 1:
9544            if split <= 0:
9545                result.append(string[-width:])
9546            else:
9547                result.append(string[:width])
9548        elif split <= 0:
9549            result.append(ellipsis + string[esize-width:])
9550        elif split >= linelen or width < esize + 4:
9551            result.append(string[:width-esize] + ellipsis)
9552        else:
9553            splitlen = linelen - width + esize
9554            end1 = split - splitlen // 2
9555            end2 = end1 + splitlen
9556            result.append(string[:end1] + ellipsis + string[end2:])
9557
9558    if isinstance(string, bytes):
9559        return b'\n'.join(result)
9560    else:
9561        return '\n'.join(result)
9562
9563
9564def enumarg(enum, arg):
9565    """Return enum member from its name or value.
9566
9567    >>> enumarg(TIFF.PHOTOMETRIC, 2)
9568    <PHOTOMETRIC.RGB: 2>
9569    >>> enumarg(TIFF.PHOTOMETRIC, 'RGB')
9570    <PHOTOMETRIC.RGB: 2>
9571
9572    """
9573    try:
9574        return enum(arg)
9575    except Exception:
9576        try:
9577            return enum[arg.upper()]
9578        except Exception:
9579            raise ValueError('invalid argument %s' % arg)
9580
9581
9582def parse_kwargs(kwargs, *keys, **keyvalues):
9583    """Return dict with keys from keys|keyvals and values from kwargs|keyvals.
9584
9585    Existing keys are deleted from kwargs.
9586
9587    >>> kwargs = {'one': 1, 'two': 2, 'four': 4}
9588    >>> kwargs2 = parse_kwargs(kwargs, 'two', 'three', four=None, five=5)
9589    >>> kwargs == {'one': 1}
9590    True
9591    >>> kwargs2 == {'two': 2, 'four': 4, 'five': 5}
9592    True
9593
9594    """
9595    result = {}
9596    for key in keys:
9597        if key in kwargs:
9598            result[key] = kwargs[key]
9599            del kwargs[key]
9600    for key, value in keyvalues.items():
9601        if key in kwargs:
9602            result[key] = kwargs[key]
9603            del kwargs[key]
9604        else:
9605            result[key] = value
9606    return result
9607
9608
9609def update_kwargs(kwargs, **keyvalues):
9610    """Update dict with keys and values if keys do not already exist.
9611
9612    >>> kwargs = {'one': 1, }
9613    >>> update_kwargs(kwargs, one=None, two=2)
9614    >>> kwargs == {'one': 1, 'two': 2}
9615    True
9616
9617    """
9618    for key, value in keyvalues.items():
9619        if key not in kwargs:
9620            kwargs[key] = value
9621
9622
9623def validate_jhove(filename, jhove='jhove', ignore=('More than 50 IFDs',)):
9624    """Validate TIFF file using jhove -m TIFF-hul.
9625
9626    Raise ValueError if jhove outputs an error message unless the message
9627    contains one of the strings in 'ignore'.
9628
9629    JHOVE does not support bigtiff or more than 50 IFDs.
9630
9631    See `JHOVE TIFF-hul Module <http://jhove.sourceforge.net/tiff-hul.html>`_
9632
9633    """
9634    import subprocess  # noqa: delayed import
9635    out = subprocess.check_output([jhove, filename, '-m', 'TIFF-hul'])
9636    if b'ErrorMessage: ' in out:
9637        for line in out.splitlines():
9638            line = line.strip()
9639            if line.startswith(b'ErrorMessage: '):
9640                error = line[14:].decode('utf8')
9641                for i in ignore:
9642                    if i in error:
9643                        break
9644                else:
9645                    raise ValueError(error)
9646                break
9647
9648
9649def lsm2bin(lsmfile, binfile=None, tile=(256, 256), verbose=True):
9650    """Convert [MP]TZCYX LSM file to series of BIN files.
9651
9652    One BIN file containing 'ZCYX' data are created for each position, time,
9653    and tile. The position, time, and tile indices are encoded at the end
9654    of the filenames.
9655
9656    """
9657    verbose = print_ if verbose else nullfunc
9658
9659    if binfile is None:
9660        binfile = lsmfile
9661    elif binfile.lower() == 'none':
9662        binfile = None
9663    if binfile:
9664        binfile += '_(z%ic%iy%ix%i)_m%%ip%%it%%03iy%%ix%%i.bin'
9665
9666    verbose('\nOpening LSM file... ', end='', flush=True)
9667    start_time = time.time()
9668
9669    with TiffFile(lsmfile) as lsm:
9670        if not lsm.is_lsm:
9671            verbose('\n', lsm, flush=True)
9672            raise ValueError('not a LSM file')
9673        series = lsm.series[0]  # first series contains the image data
9674        shape = series.shape
9675        axes = series.axes
9676        dtype = series.dtype
9677        size = product(shape) * dtype.itemsize
9678
9679        verbose('%.3f s' % (time.time() - start_time))
9680        # verbose(lsm, flush=True)
9681        verbose('Image\n  axes:  %s\n  shape: %s\n  dtype: %s\n  size:  %s'
9682                % (axes, shape, dtype, format_size(size)), flush=True)
9683        if not series.axes.endswith('TZCYX'):
9684            raise ValueError('not a *TZCYX LSM file')
9685
9686        verbose('Copying image from LSM to BIN files', end='', flush=True)
9687        start_time = time.time()
9688        tiles = shape[-2] // tile[-2], shape[-1] // tile[-1]
9689        if binfile:
9690            binfile = binfile % (shape[-4], shape[-3], tile[0], tile[1])
9691        shape = (1,) * (7-len(shape)) + shape
9692        # cache for ZCYX stacks and output files
9693        data = numpy.empty(shape[3:], dtype=dtype)
9694        out = numpy.empty((shape[-4], shape[-3], tile[0], tile[1]),
9695                          dtype=dtype)
9696        # iterate over Tiff pages containing data
9697        pages = iter(series.pages)
9698        for m in range(shape[0]):  # mosaic axis
9699            for p in range(shape[1]):  # position axis
9700                for t in range(shape[2]):  # time axis
9701                    for z in range(shape[3]):  # z slices
9702                        data[z] = next(pages).asarray()
9703                    for y in range(tiles[0]):  # tile y
9704                        for x in range(tiles[1]):  # tile x
9705                            out[:] = data[...,
9706                                          y*tile[0]:(y+1)*tile[0],
9707                                          x*tile[1]:(x+1)*tile[1]]
9708                            if binfile:
9709                                out.tofile(binfile % (m, p, t, y, x))
9710                            verbose('.', end='', flush=True)
9711        verbose(' %.3f s' % (time.time() - start_time))
9712
9713
9714def imshow(data, title=None, vmin=0, vmax=None, cmap=None, bitspersample=None,
9715           photometric='RGB', interpolation=None, dpi=96, figure=None,
9716           subplot=111, maxdim=32768, **kwargs):
9717    """Plot n-dimensional images using matplotlib.pyplot.
9718
9719    Return figure, subplot and plot axis.
9720    Requires pyplot already imported C{from matplotlib import pyplot}.
9721
9722    Parameters
9723    ----------
9724    bitspersample : int or None
9725        Number of bits per channel in integer RGB images.
9726    photometric : {'MINISWHITE', 'MINISBLACK', 'RGB', or 'PALETTE'}
9727        The color space of the image data.
9728    title : str
9729        Window and subplot title.
9730    figure : matplotlib.figure.Figure (optional).
9731        Matplotlib to use for plotting.
9732    subplot : int
9733        A matplotlib.pyplot.subplot axis.
9734    maxdim : int
9735        maximum image width and length.
9736    kwargs : optional
9737        Arguments for matplotlib.pyplot.imshow.
9738
9739    """
9740    isrgb = photometric in ('RGB',)  # 'PALETTE', 'YCBCR'
9741    if data.dtype.kind == 'b':
9742        isrgb = False
9743    if isrgb and not (data.shape[-1] in (3, 4) or (
9744            data.ndim > 2 and data.shape[-3] in (3, 4))):
9745        isrgb = False
9746        photometric = 'MINISBLACK'
9747
9748    data = data.squeeze()
9749    if photometric in ('MINISWHITE', 'MINISBLACK', None):
9750        data = reshape_nd(data, 2)
9751    else:
9752        data = reshape_nd(data, 3)
9753
9754    dims = data.ndim
9755    if dims < 2:
9756        raise ValueError('not an image')
9757    elif dims == 2:
9758        dims = 0
9759        isrgb = False
9760    else:
9761        if isrgb and data.shape[-3] in (3, 4):
9762            data = numpy.swapaxes(data, -3, -2)
9763            data = numpy.swapaxes(data, -2, -1)
9764        elif not isrgb and (data.shape[-1] < data.shape[-2] // 8 and
9765                            data.shape[-1] < data.shape[-3] // 8 and
9766                            data.shape[-1] < 5):
9767            data = numpy.swapaxes(data, -3, -1)
9768            data = numpy.swapaxes(data, -2, -1)
9769        isrgb = isrgb and data.shape[-1] in (3, 4)
9770        dims -= 3 if isrgb else 2
9771
9772    if isrgb:
9773        data = data[..., :maxdim, :maxdim, :maxdim]
9774    else:
9775        data = data[..., :maxdim, :maxdim]
9776
9777    if photometric == 'PALETTE' and isrgb:
9778        datamax = data.max()
9779        if datamax > 255:
9780            data = data >> 8  # possible precision loss
9781        data = data.astype('B')
9782    elif data.dtype.kind in 'ui':
9783        if not (isrgb and data.dtype.itemsize <= 1) or bitspersample is None:
9784            try:
9785                bitspersample = int(math.ceil(math.log(data.max(), 2)))
9786            except Exception:
9787                bitspersample = data.dtype.itemsize * 8
9788        elif not isinstance(bitspersample, inttypes):
9789            # bitspersample can be tuple, e.g. (5, 6, 5)
9790            bitspersample = data.dtype.itemsize * 8
9791        datamax = 2**bitspersample
9792        if isrgb:
9793            if bitspersample < 8:
9794                data = data << (8 - bitspersample)
9795            elif bitspersample > 8:
9796                data = data >> (bitspersample - 8)  # precision loss
9797            data = data.astype('B')
9798    elif data.dtype.kind == 'f':
9799        datamax = data.max()
9800        if isrgb and datamax > 1.0:
9801            if data.dtype.char == 'd':
9802                data = data.astype('f')
9803                data /= datamax
9804            else:
9805                data = data / datamax
9806    elif data.dtype.kind == 'b':
9807        datamax = 1
9808    elif data.dtype.kind == 'c':
9809        data = numpy.absolute(data)
9810        datamax = data.max()
9811
9812    if not isrgb:
9813        if vmax is None:
9814            vmax = datamax
9815        if vmin is None:
9816            if data.dtype.kind == 'i':
9817                dtmin = numpy.iinfo(data.dtype).min
9818                vmin = numpy.min(data)
9819                if vmin == dtmin:
9820                    vmin = numpy.min(data > dtmin)
9821            if data.dtype.kind == 'f':
9822                dtmin = numpy.finfo(data.dtype).min
9823                vmin = numpy.min(data)
9824                if vmin == dtmin:
9825                    vmin = numpy.min(data > dtmin)
9826            else:
9827                vmin = 0
9828
9829    pyplot = sys.modules['matplotlib.pyplot']
9830
9831    if figure is None:
9832        pyplot.rc('font', family='sans-serif', weight='normal', size=8)
9833        figure = pyplot.figure(dpi=dpi, figsize=(10.3, 6.3), frameon=True,
9834                               facecolor='1.0', edgecolor='w')
9835        try:
9836            figure.canvas.manager.window.title(title)
9837        except Exception:
9838            pass
9839        size = len(title.splitlines()) if title else 1
9840        pyplot.subplots_adjust(bottom=0.03*(dims+2), top=0.98-size*0.03,
9841                               left=0.1, right=0.95, hspace=0.05, wspace=0.0)
9842    subplot = pyplot.subplot(subplot)
9843
9844    if title:
9845        try:
9846            title = unicode(title, 'Windows-1252')
9847        except TypeError:
9848            pass
9849        pyplot.title(title, size=11)
9850
9851    if cmap is None:
9852        if data.dtype.char == '?':
9853            cmap = 'gray'
9854        elif data.dtype.kind in 'buf' or vmin == 0:
9855            cmap = 'viridis'
9856        else:
9857            cmap = 'coolwarm'
9858        if photometric == 'MINISWHITE':
9859            cmap += '_r'
9860
9861    image = pyplot.imshow(numpy.atleast_2d(data[(0,) * dims].squeeze()),
9862                          vmin=vmin, vmax=vmax, cmap=cmap,
9863                          interpolation=interpolation, **kwargs)
9864
9865    if not isrgb:
9866        pyplot.colorbar()  # panchor=(0.55, 0.5), fraction=0.05
9867
9868    def format_coord(x, y):
9869        # callback function to format coordinate display in toolbar
9870        x = int(x + 0.5)
9871        y = int(y + 0.5)
9872        try:
9873            if dims:
9874                return '%s @ %s [%4i, %4i]' % (
9875                    curaxdat[1][y, x], current, y, x)
9876            return '%s @ [%4i, %4i]' % (data[y, x], y, x)
9877        except IndexError:
9878            return ''
9879
9880    def none(event):
9881        return ''
9882
9883    subplot.format_coord = format_coord
9884    image.get_cursor_data = none
9885    image.format_cursor_data = none
9886
9887    if dims:
9888        current = list((0,) * dims)
9889        curaxdat = [0, data[tuple(current)].squeeze()]
9890        sliders = [pyplot.Slider(
9891            pyplot.axes([0.125, 0.03*(axis+1), 0.725, 0.025]),
9892            'Dimension %i' % axis, 0, data.shape[axis]-1, 0, facecolor='0.5',
9893            valfmt='%%.0f [%i]' % data.shape[axis]) for axis in range(dims)]
9894        for slider in sliders:
9895            slider.drawon = False
9896
9897        def set_image(current, sliders=sliders, data=data):
9898            # change image and redraw canvas
9899            curaxdat[1] = data[tuple(current)].squeeze()
9900            image.set_data(curaxdat[1])
9901            for ctrl, index in zip(sliders, current):
9902                ctrl.eventson = False
9903                ctrl.set_val(index)
9904                ctrl.eventson = True
9905            figure.canvas.draw()
9906
9907        def on_changed(index, axis, data=data, current=current):
9908            # callback function for slider change event
9909            index = int(round(index))
9910            curaxdat[0] = axis
9911            if index == current[axis]:
9912                return
9913            if index >= data.shape[axis]:
9914                index = 0
9915            elif index < 0:
9916                index = data.shape[axis] - 1
9917            current[axis] = index
9918            set_image(current)
9919
9920        def on_keypressed(event, data=data, current=current):
9921            # callback function for key press event
9922            key = event.key
9923            axis = curaxdat[0]
9924            if str(key) in '0123456789':
9925                on_changed(key, axis)
9926            elif key == 'right':
9927                on_changed(current[axis] + 1, axis)
9928            elif key == 'left':
9929                on_changed(current[axis] - 1, axis)
9930            elif key == 'up':
9931                curaxdat[0] = 0 if axis == len(data.shape)-1 else axis + 1
9932            elif key == 'down':
9933                curaxdat[0] = len(data.shape)-1 if axis == 0 else axis - 1
9934            elif key == 'end':
9935                on_changed(data.shape[axis] - 1, axis)
9936            elif key == 'home':
9937                on_changed(0, axis)
9938
9939        figure.canvas.mpl_connect('key_press_event', on_keypressed)
9940        for axis, ctrl in enumerate(sliders):
9941            ctrl.on_changed(lambda k, a=axis: on_changed(k, a))
9942
9943    return figure, subplot, image
9944
9945
9946def _app_show():
9947    """Block the GUI. For use as skimage plugin."""
9948    pyplot = sys.modules['matplotlib.pyplot']
9949    pyplot.show()
9950
9951
9952def askopenfilename(**kwargs):
9953    """Return file name(s) from Tkinter's file open dialog."""
9954    try:
9955        from Tkinter import Tk
9956        import tkFileDialog as filedialog
9957    except ImportError:
9958        from tkinter import Tk, filedialog
9959    root = Tk()
9960    root.withdraw()
9961    root.update()
9962    filenames = filedialog.askopenfilename(**kwargs)
9963    root.destroy()
9964    return filenames
9965
9966
9967def main(argv=None):
9968    """Command line usage main function."""
9969    if float(sys.version[0:3]) < 2.7:
9970        print('This script requires Python version 2.7 or better.')
9971        print('This is Python version %s' % sys.version)
9972        return 0
9973    if argv is None:
9974        argv = sys.argv
9975
9976    import optparse  # TODO: use argparse
9977
9978    parser = optparse.OptionParser(
9979        usage='usage: %prog [options] path',
9980        description='Display image data in TIFF files.',
9981        version='%%prog %s' % __version__)
9982    opt = parser.add_option
9983    opt('-p', '--page', dest='page', type='int', default=-1,
9984        help='display single page')
9985    opt('-s', '--series', dest='series', type='int', default=-1,
9986        help='display series of pages of same shape')
9987    opt('--nomultifile', dest='nomultifile', action='store_true',
9988        default=False, help='do not read OME series from multiple files')
9989    opt('--noplots', dest='noplots', type='int', default=8,
9990        help='maximum number of plots')
9991    opt('--interpol', dest='interpol', metavar='INTERPOL', default='bilinear',
9992        help='image interpolation method')
9993    opt('--dpi', dest='dpi', type='int', default=96,
9994        help='plot resolution')
9995    opt('--vmin', dest='vmin', type='int', default=None,
9996        help='minimum value for colormapping')
9997    opt('--vmax', dest='vmax', type='int', default=None,
9998        help='maximum value for colormapping')
9999    opt('--debug', dest='debug', action='store_true', default=False,
10000        help='raise exception on failures')
10001    opt('--doctest', dest='doctest', action='store_true', default=False,
10002        help='runs the docstring examples')
10003    opt('-v', '--detail', dest='detail', type='int', default=2)
10004    opt('-q', '--quiet', dest='quiet', action='store_true')
10005
10006    settings, path = parser.parse_args()
10007    path = ' '.join(path)
10008
10009    if settings.doctest:
10010        import doctest
10011        doctest.testmod(optionflags=doctest.ELLIPSIS)
10012        return 0
10013    if not path:
10014        path = askopenfilename(title='Select a TIFF file',
10015                               filetypes=TIFF.FILEOPEN_FILTER)
10016        if not path:
10017            parser.error('No file specified')
10018
10019    if any(i in path for i in '?*'):
10020        path = glob.glob(path)
10021        if not path:
10022            print('no files match the pattern')
10023            return 0
10024        # TODO: handle image sequences
10025        path = path[0]
10026
10027    if not settings.quiet:
10028        print('\nReading file structure...', end=' ')
10029    start = time.time()
10030    try:
10031        tif = TiffFile(path, multifile=not settings.nomultifile)
10032    except Exception as e:
10033        if settings.debug:
10034            raise
10035        else:
10036            print('\n', e)
10037            sys.exit(0)
10038    if not settings.quiet:
10039        print('%.3f ms' % ((time.time()-start) * 1e3))
10040
10041    if tif.is_ome:
10042        settings.norgb = True
10043
10044    images = []
10045    if settings.noplots > 0:
10046        if not settings.quiet:
10047            print('Reading image data... ', end=' ')
10048
10049        def notnone(x):
10050            return next(i for i in x if i is not None)
10051
10052        start = time.time()
10053        try:
10054            if settings.page >= 0:
10055                images = [(tif.asarray(key=settings.page),
10056                           tif[settings.page], None)]
10057            elif settings.series >= 0:
10058                images = [(tif.asarray(series=settings.series),
10059                           notnone(tif.series[settings.series]._pages),
10060                           tif.series[settings.series])]
10061            else:
10062                images = []
10063                for i, s in enumerate(tif.series[:settings.noplots]):
10064                    try:
10065                        images.append((tif.asarray(series=i),
10066                                       notnone(s._pages),
10067                                       tif.series[i]))
10068                    except ValueError as e:
10069                        images.append((None, notnone(s.pages), None))
10070                        if settings.debug:
10071                            raise
10072                        else:
10073                            print('\nSeries %i failed: %s... ' % (i, e),
10074                                  end='')
10075            if not settings.quiet:
10076                print('%.3f ms' % ((time.time()-start) * 1e3))
10077        except Exception as e:
10078            if settings.debug:
10079                raise
10080            else:
10081                print(e)
10082
10083    if not settings.quiet:
10084        print()
10085        print(TiffFile.__str__(tif, detail=int(settings.detail)))
10086        print()
10087    tif.close()
10088
10089    if images and settings.noplots > 0:
10090        try:
10091            import matplotlib
10092            matplotlib.use('TkAgg')
10093            from matplotlib import pyplot
10094        except ImportError as e:
10095            warnings.warn('failed to import matplotlib.\n%s' % e)
10096        else:
10097            for img, page, series in images:
10098                if img is None:
10099                    continue
10100                vmin, vmax = settings.vmin, settings.vmax
10101                if 'GDAL_NODATA' in page.tags:
10102                    try:
10103                        vmin = numpy.min(
10104                            img[img > float(page.tags['GDAL_NODATA'].value)])
10105                    except ValueError:
10106                        pass
10107                if tif.is_stk:
10108                    try:
10109                        vmin = tif.stk_metadata['MinScale']
10110                        vmax = tif.stk_metadata['MaxScale']
10111                    except KeyError:
10112                        pass
10113                    else:
10114                        if vmax <= vmin:
10115                            vmin, vmax = settings.vmin, settings.vmax
10116                if series:
10117                    title = '%s\n%s\n%s' % (str(tif), str(page), str(series))
10118                else:
10119                    title = '%s\n %s' % (str(tif), str(page))
10120                photometric = 'MINISBLACK'
10121                if page.photometric not in (3,):
10122                    photometric = TIFF.PHOTOMETRIC(page.photometric).name
10123                imshow(img, title=title, vmin=vmin, vmax=vmax,
10124                       bitspersample=page.bitspersample,
10125                       photometric=photometric,
10126                       interpolation=settings.interpol,
10127                       dpi=settings.dpi)
10128            pyplot.show()
10129
10130
10131if sys.version_info[0] == 2:
10132    inttypes = int, long  # noqa
10133
10134    def print_(*args, **kwargs):
10135        """Print function with flush support."""
10136        flush = kwargs.pop('flush', False)
10137        print(*args, **kwargs)
10138        if flush:
10139            sys.stdout.flush()
10140
10141    def bytes2str(b, encoding=None, errors=None):
10142        """Return string from bytes."""
10143        return b
10144
10145    def str2bytes(s, encoding=None):
10146        """Return bytes from string."""
10147        return s
10148
10149    def byte2int(b):
10150        """Return value of byte as int."""
10151        return ord(b)
10152
10153    class FileNotFoundError(IOError):
10154        pass
10155
10156    TiffFrame = TiffPage  # noqa
10157else:
10158    inttypes = int
10159    basestring = str, bytes
10160    unicode = str
10161    print_ = print
10162
10163    def bytes2str(b, encoding=None, errors='strict'):
10164        """Return unicode string from encoded bytes."""
10165        if encoding is not None:
10166            return b.decode(encoding, errors)
10167        try:
10168            return b.decode('utf-8', errors)
10169        except UnicodeDecodeError:
10170            return b.decode('cp1252', errors)
10171
10172    def str2bytes(s, encoding='cp1252'):
10173        """Return bytes from unicode string."""
10174        return s.encode(encoding)
10175
10176    def byte2int(b):
10177        """Return value of byte as int."""
10178        return b
10179
10180if __name__ == '__main__':
10181    sys.exit(main())
10182
10183