[med-svn] [Git][python-team/packages/tifffile][upstream] New upstream version 20201126

Ole Streicher gitlab at salsa.debian.org
Mon Nov 30 09:07:28 GMT 2020



Ole Streicher pushed to branch upstream at Debian Python Team / packages / tifffile


Commits:
3bb27ff4 by Ole Streicher at 2020-11-30T09:46:15+01:00
New upstream version 20201126
- - - - -


10 changed files:

- CHANGES.rst
- PKG-INFO
- README.rst
- setup.py
- tests/test_tifffile.py
- tifffile.egg-info/PKG-INFO
- tifffile.egg-info/SOURCES.txt
- tifffile.egg-info/entry_points.txt
- + tifffile/tiffcomment.py
- tifffile/tifffile.py


Changes:

=====================================
CHANGES.rst
=====================================
@@ -1,9 +1,17 @@
 Revisions
 ---------
+2020.11.26
+    Pass 4372 tests.
+    Add option to pass axes metadata to ImageJ writer.
+    Pad incomplete tiles passed to TiffWriter.write (#38).
+    Split TiffTag constructor (breaking).
+    Change TiffTag.dtype to TIFF.DATATYPES (breaking).
+    Add TiffTag.overwrite method.
+    Add script to change ImageDescription in files.
+    Add TiffWriter.overwrite_description method (WIP).
 2020.11.18
-    Pass 4363 tests.
-    Support writing SEPARATED colorspace (#37).
-    Use imagecodecs.deflate if available.
+    Support writing SEPARATED color space (#37).
+    Use imagecodecs.deflate codec if available.
     Fix SCN and NDPI series with Z dimensions.
     Add TiffReader alias for TiffFile.
     TiffPage.is_volumetric returns True if ImageDepth > 1.
@@ -12,7 +20,7 @@ Revisions
     Formally deprecate unused TiffFile parameters (scikit-image #4996).
 2020.9.30
     Allow to pass additional arguments to compression codecs.
-    Deprecate TiffWriter.save function (use TiffWriter.write).
+    Deprecate TiffWriter.save method (use TiffWriter.write).
     Deprecate TiffWriter.save compress parameter (use compression).
     Remove multifile parameter from TiffFile (breaking).
     Pass all is_flag arguments from imread to TiffFile.
@@ -40,7 +48,7 @@ Revisions
     Return full size tiles from Tiffpage.segments.
     Rename TiffPage.is_sgi property to is_volumetric (breaking).
     Rename TiffPageSeries.is_pyramid to is_pyramidal (breaking).
-    Fix TypeError when passing jpegtables to non-JPEG decode function (#25).
+    Fix TypeError when passing jpegtables to non-JPEG decode method (#25).
 2020.9.3
     Do not write contiguous series by default (breaking).
     Allow to write to SubIFDs (WIP).
@@ -89,7 +97,7 @@ Revisions
 2020.5.25
     Make imagecodecs an optional dependency again.
     Disable multi-threaded decoding of small LZW compressed segments.
-    Fix caching of TiffPage.decode function.
+    Fix caching of TiffPage.decode method.
     Fix xml.etree.cElementTree ImportError on Python 3.9.
     Fix tostring DeprecationWarning.
 2020.5.11
@@ -101,7 +109,7 @@ Revisions
     Always store ExtraSamples values in tuple (breaking).
 2020.5.5
     Allow to write tiled TIFF from iterable of tiles (WIP).
-    Add function to iterate over decoded segments of TiffPage (WIP).
+    Add method to iterate over decoded segments of TiffPage (WIP).
     Pass chunks of segments to ThreadPoolExecutor.map to reduce memory usage.
     Fix reading invalid files with too many strips.
     Fix writing over-aligned image data.
@@ -111,7 +119,7 @@ Revisions
     Remove maxsize parameter from asarray (breaking).
     Deprecate ijmetadata parameter from TiffWriter.save (use metadata).
 2020.2.16
-    Add function to decode individual strips or tiles.
+    Add method to decode individual strips or tiles.
     Read strips and tiles in order of their offsets.
     Enable multi-threading when decompressing multiple strips.
     Replace TiffPage.tags dictionary with TiffTags (breaking).
@@ -126,7 +134,7 @@ Revisions
     Fix xml2dict.
     Require imagecodecs >= 2020.1.31.
     Remove support for imagecodecs-lite (breaking).
-    Remove verify parameter to asarray function (breaking).
+    Remove verify parameter to asarray method (breaking).
     Remove deprecated lzw_decode functions (breaking).
     Remove support for Python 2.7 and 3.5 (breaking).
 2019.7.26


=====================================
PKG-INFO
=====================================
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: tifffile
-Version: 2020.11.18
+Version: 2020.11.26
 Summary: Read and write TIFF(r) files
 Home-page: https://www.lfd.uci.edu/~gohlke/
 Author: Christoph Gohlke
@@ -51,7 +51,7 @@ Description: Read and write TIFF(r) files
         
         :License: BSD 3-Clause
         
-        :Version: 2020.11.18
+        :Version: 2020.11.26
         
         Requirements
         ------------
@@ -71,10 +71,18 @@ Description: Read and write TIFF(r) files
         
         Revisions
         ---------
+        2020.11.26
+            Pass 4372 tests.
+            Add option to pass axes metadata to ImageJ writer.
+            Pad incomplete tiles passed to TiffWriter.write (#38).
+            Split TiffTag constructor (breaking).
+            Change TiffTag.dtype to TIFF.DATATYPES (breaking).
+            Add TiffTag.overwrite method.
+            Add script to change ImageDescription in files.
+            Add TiffWriter.overwrite_description method (WIP).
         2020.11.18
-            Pass 4363 tests.
-            Support writing SEPARATED colorspace (#37).
-            Use imagecodecs.deflate if available.
+            Support writing SEPARATED color space (#37).
+            Use imagecodecs.deflate codec if available.
             Fix SCN and NDPI series with Z dimensions.
             Add TiffReader alias for TiffFile.
             TiffPage.is_volumetric returns True if ImageDepth > 1.
@@ -83,7 +91,7 @@ Description: Read and write TIFF(r) files
             Formally deprecate unused TiffFile parameters (scikit-image #4996).
         2020.9.30
             Allow to pass additional arguments to compression codecs.
-            Deprecate TiffWriter.save function (use TiffWriter.write).
+            Deprecate TiffWriter.save method (use TiffWriter.write).
             Deprecate TiffWriter.save compress parameter (use compression).
             Remove multifile parameter from TiffFile (breaking).
             Pass all is_flag arguments from imread to TiffFile.
@@ -111,7 +119,7 @@ Description: Read and write TIFF(r) files
             Return full size tiles from Tiffpage.segments.
             Rename TiffPage.is_sgi property to is_volumetric (breaking).
             Rename TiffPageSeries.is_pyramid to is_pyramidal (breaking).
-            Fix TypeError when passing jpegtables to non-JPEG decode function (#25).
+            Fix TypeError when passing jpegtables to non-JPEG decode method (#25).
         2020.9.3
             Do not write contiguous series by default (breaking).
             Allow to write to SubIFDs (WIP).
@@ -160,7 +168,7 @@ Description: Read and write TIFF(r) files
         2020.5.25
             Make imagecodecs an optional dependency again.
             Disable multi-threaded decoding of small LZW compressed segments.
-            Fix caching of TiffPage.decode function.
+            Fix caching of TiffPage.decode method.
             Fix xml.etree.cElementTree ImportError on Python 3.9.
             Fix tostring DeprecationWarning.
         2020.5.11
@@ -172,7 +180,7 @@ Description: Read and write TIFF(r) files
             Always store ExtraSamples values in tuple (breaking).
         2020.5.5
             Allow to write tiled TIFF from iterable of tiles (WIP).
-            Add function to iterate over decoded segments of TiffPage (WIP).
+            Add method to iterate over decoded segments of TiffPage (WIP).
             Pass chunks of segments to ThreadPoolExecutor.map to reduce memory usage.
             Fix reading invalid files with too many strips.
             Fix writing over-aligned image data.
@@ -182,7 +190,7 @@ Description: Read and write TIFF(r) files
             Remove maxsize parameter from asarray (breaking).
             Deprecate ijmetadata parameter from TiffWriter.save (use metadata).
         2020.2.16
-            Add function to decode individual strips or tiles.
+            Add method to decode individual strips or tiles.
             Read strips and tiles in order of their offsets.
             Enable multi-threading when decompressing multiple strips.
             Replace TiffPage.tags dictionary with TiffTags (breaking).
@@ -197,7 +205,7 @@ Description: Read and write TIFF(r) files
             Fix xml2dict.
             Require imagecodecs >= 2020.1.31.
             Remove support for imagecodecs-lite (breaking).
-            Remove verify parameter to asarray function (breaking).
+            Remove verify parameter to asarray method (breaking).
             Remove deprecated lzw_decode functions (breaking).
             Remove support for Python 2.7 and 3.5 (breaking).
         2019.7.26
@@ -323,16 +331,27 @@ Description: Read and write TIFF(r) files
         
         Examples
         --------
-        Save a 3D numpy array to a multi-page, 16-bit grayscale TIFF file:
+        Write a numpy array to a single-page RGB TIFF file:
         
-        >>> data = numpy.random.randint(0, 2**12, (4, 301, 219), 'uint16')
+        >>> data = numpy.random.randint(0, 255, (256, 256, 3), 'uint8')
+        >>> imwrite('temp.tif', data, photometric='rgb')
+        
+        Read the image from the TIFF file as numpy array:
+        
+        >>> image = imread('temp.tif')
+        >>> image.shape
+        (256, 256, 3)
+        
+        Write a 3D numpy array to a multi-page, 16-bit grayscale TIFF file:
+        
+        >>> data = numpy.random.randint(0, 2**12, (64, 301, 219), 'uint16')
         >>> imwrite('temp.tif', data, photometric='minisblack')
         
         Read the whole image stack from the TIFF file as numpy array:
         
         >>> image_stack = imread('temp.tif')
         >>> image_stack.shape
-        (4, 301, 219)
+        (64, 301, 219)
         >>> image_stack.dtype
         dtype('uint16')
         
@@ -342,67 +361,46 @@ Description: Read and write TIFF(r) files
         >>> image.shape
         (301, 219)
         
-        Read images from a sequence of TIFF files as numpy array:
-        
-        >>> image_sequence = imread(['temp.tif', 'temp.tif'])
-        >>> image_sequence.shape
-        (2, 4, 301, 219)
-        
-        Save a numpy array to a single-page RGB TIFF file:
-        
-        >>> data = numpy.random.randint(0, 255, (256, 256, 3), 'uint8')
-        >>> imwrite('temp.tif', data, photometric='rgb')
-        
-        Save a floating-point array and metadata, using zlib compression:
+        Read images from a selected range of pages:
         
-        >>> data = numpy.random.rand(2, 5, 3, 301, 219).astype('float32')
-        >>> imwrite('temp.tif', data, compression='zlib', metadata={'axes': 'TZCYX'})
+        >>> image = imread('temp.tif', key=range(4, 40, 2))
+        >>> image.shape
+        (18, 301, 219)
         
-        Save a volume with xyz voxel size 2.6755x2.6755x3.9474 micron^3 to an ImageJ
-        formatted TIFF file:
+        Iterate over all pages in the TIFF file and successively read images:
         
-        >>> volume = numpy.random.randn(57*256*256).astype('float32')
-        >>> volume.shape = 1, 57, 1, 256, 256, 1  # dimensions in TZCYXS order
-        >>> imwrite('temp.tif', volume, imagej=True, resolution=(1./2.6755, 1./2.6755),
-        ...         metadata={'spacing': 3.947368, 'unit': 'um'})
+        >>> with TiffFile('temp.tif') as tif:
+        ...     for page in tif.pages:
+        ...         image = page.asarray()
         
-        Get the shape and dtype of the volume stored in the TIFF file:
+        Get information about the image stack in the TIFF file without reading
+        the image data:
         
         >>> tif = TiffFile('temp.tif')
         >>> len(tif.pages)  # number of pages in the file
-        57
+        64
         >>> page = tif.pages[0]  # get shape and dtype of the image in the first page
         >>> page.shape
-        (256, 256)
+        (301, 219)
         >>> page.dtype
-        dtype('float32')
+        dtype('uint16')
         >>> page.axes
         'YX'
         >>> series = tif.series[0]  # get shape and dtype of the first image series
         >>> series.shape
-        (57, 256, 256)
+        (64, 301, 219)
         >>> series.dtype
-        dtype('float32')
+        dtype('uint16')
         >>> series.axes
-        'ZYX'
+        'QYX'
         >>> tif.close()
         
-        Read hyperstack and metadata from the ImageJ file:
-        
-        >>> with TiffFile('temp.tif') as tif:
-        ...     imagej_hyperstack = tif.asarray()
-        ...     imagej_metadata = tif.imagej_metadata
-        >>> imagej_hyperstack.shape
-        (57, 256, 256)
-        >>> imagej_metadata['slices']
-        57
-        
-        Read the "XResolution" tag from the first page in the TIFF file:
+        Inspect the "XResolution" tag from the first page in the TIFF file:
         
         >>> with TiffFile('temp.tif') as tif:
         ...     tag = tif.pages[0].tags['XResolution']
         >>> tag.value
-        (2000, 5351)
+        (1, 1)
         >>> tag.name
         'XResolution'
         >>> tag.code
@@ -410,28 +408,71 @@ Description: Read and write TIFF(r) files
         >>> tag.count
         1
         >>> tag.dtype
-        '2I'
+        <DATATYPES.RATIONAL: 5>
         
-        Read images from a selected range of pages:
+        Iterate over all tags in the TIFF file:
         
-        >>> image = imread('temp.tif', key=range(4, 40, 2))
-        >>> image.shape
-        (18, 256, 256)
+        >>> with TiffFile('temp.tif') as tif:
+        ...     for page in tif.pages:
+        ...         for tag in page.tags:
+        ...             tag_name, tag_value = tag.name, tag.value
+        
+        Write a floating-point ndarray and metadata using BigTIFF format, tiling,
+        compression, and planar storage:
+        
+        >>> data = numpy.random.rand(2, 5, 3, 301, 219).astype('float32')
+        >>> imwrite('temp.tif', data, bigtiff=True, photometric='minisblack',
+        ...         compression='deflate', planarconfig='separate', tile=(32, 32),
+        ...         metadata={'axes': 'TZCYX'})
+        
+        Write a volume with xyz voxel size 2.6755x2.6755x3.9474 micron^3 to an
+        ImageJ hyperstack formatted TIFF file:
+        
+        >>> volume = numpy.random.randn(57, 256, 256).astype('float32')
+        >>> imwrite('temp.tif', volume, imagej=True, resolution=(1./2.6755, 1./2.6755),
+        ...         metadata={'spacing': 3.947368, 'unit': 'um', 'axes': 'ZYX'})
+        
+        Read the volume and metadata from the ImageJ file:
+        
+        >>> with TiffFile('temp.tif') as tif:
+        ...     volume = tif.asarray()
+        ...     axes = tif.series[0].axes
+        ...     imagej_metadata = tif.imagej_metadata
+        >>> volume.shape
+        (57, 256, 256)
+        >>> axes
+        'ZYX'
+        >>> imagej_metadata['slices']
+        57
         
         Create an empty TIFF file and write to the memory-mapped numpy array:
         
-        >>> memmap_image = memmap('temp.tif', shape=(256, 256), dtype='float32')
-        >>> memmap_image[255, 255] = 1.0
+        >>> memmap_image = memmap('temp.tif', shape=(3, 256, 256), dtype='float32')
+        >>> memmap_image[1, 255, 255] = 1.0
         >>> memmap_image.flush()
         >>> del memmap_image
         
         Memory-map image data of the first page in the TIFF file:
         
         >>> memmap_image = memmap('temp.tif', page=0)
-        >>> memmap_image[255, 255]
+        >>> memmap_image[1, 255, 255]
         1.0
         >>> del memmap_image
         
+        Write two numpy arrays to a multi-series TIFF file:
+        
+        >>> series0 = numpy.random.randint(0, 255, (32, 32, 3), 'uint8')
+        >>> series1 = numpy.random.randint(0, 1023, (4, 256, 256), 'uint16')
+        >>> with TiffWriter('temp.tif') as tif:
+        ...     tif.write(series0, photometric='rgb')
+        ...     tif.write(series1, photometric='minisblack')
+        
+        Read the second image series from the TIFF file:
+        
+        >>> series1 = imread('temp.tif', series=1)
+        >>> series1.shape
+        (4, 256, 256)
+        
         Successively write the frames of one contiguous series to a TIFF file:
         
         >>> data = numpy.random.randint(0, 255, (30, 301, 219), 'uint8')
@@ -439,58 +480,40 @@ Description: Read and write TIFF(r) files
         ...     for frame in data:
         ...         tif.write(frame, contiguous=True)
         
-        Successively append image series to a BigTIFF file, which can exceed 4 GB:
-        
-        >>> data = numpy.random.randint(0, 255, (5, 2, 3, 301, 219), 'uint8')
-        >>> with TiffWriter('temp.tif', bigtiff=True) as tif:
-        ...     for i in range(data.shape[0]):
-        ...         tif.write(data[i], photometric='minisblack')
-        
-        Append an image to the existing TIFF file:
+        Append an image series to the existing TIFF file:
         
         >>> data = numpy.random.randint(0, 255, (301, 219, 3), 'uint8')
         >>> imwrite('temp.tif', data, append=True)
         
-        Iterate over pages and tags in the TIFF file and successively read images:
+        Create a TIFF file from a generator of tiles:
         
-        >>> with TiffFile('temp.tif') as tif:
-        ...     for page in tif.pages:
-        ...         for tag in page.tags:
-        ...             tag_name, tag_value = tag.name, tag.value
-        ...         image = page.asarray()
+        >>> data = numpy.random.randint(0, 2**12, (31, 33, 3), 'uint16')
+        >>> def tiles(data, tileshape):
+        ...     for y in range(0, data.shape[0], tileshape[0]):
+        ...         for x in range(0, data.shape[1], tileshape[1]):
+        ...             yield data[y : y + tileshape[0], x : x + tileshape[1]]
+        >>> imwrite('temp.tif', tiles(data, (16, 16)), tile=(16, 16),
+        ...         shape=data.shape, dtype=data.dtype)
         
         Write two numpy arrays to a multi-series OME-TIFF file:
         
-        >>> data0 = numpy.random.randint(0, 255, (32, 32, 3), 'uint8')
-        >>> data1 = numpy.random.randint(0, 1023, (4, 256, 256), 'uint16')
+        >>> series0 = numpy.random.randint(0, 255, (32, 32, 3), 'uint8')
+        >>> series1 = numpy.random.randint(0, 1023, (4, 256, 256), 'uint16')
         >>> with TiffWriter('temp.ome.tif') as tif:
-        ...     tif.write(data0, photometric='rgb')
-        ...     tif.write(data1, photometric='minisblack',
+        ...     tif.write(series0, photometric='rgb')
+        ...     tif.write(series1, photometric='minisblack',
         ...              metadata={'axes': 'ZYX', 'SignificantBits': 10,
         ...                        'Plane': {'PositionZ': [0.0, 1.0, 2.0, 3.0]}})
         
-        Read the second image series from the OME-TIFF file:
-        
-        >>> series1 = imread('temp.ome.tif', series=1)
-        >>> series1.shape
-        (4, 256, 256)
-        
-        Create a TIFF file from a generator of tiles:
-        
-        >>> def tiles():
-        ...     data = numpy.arange(3*4*16*16, dtype='uint16').reshape((3*4, 16, 16))
-        ...     for i in range(data.shape[0]): yield data[i]
-        >>> imwrite('temp.tif', tiles(), dtype='uint16', shape=(48, 64), tile=(16, 16))
-        
-        Write a tiled, multi-resolution, pyramidal OME-TIFF file using JPEG
-        compression. Sub-resolution images are written to SubIFDs:
+        Write a tiled, multi-resolution, pyramidal, OME-TIFF file using
+        JPEG compression. Sub-resolution images are written to SubIFDs:
         
         >>> data = numpy.arange(1024*1024*3, dtype='uint8').reshape((1024, 1024, 3))
-        >>> with TiffWriter('temp.ome.tif') as tif:
+        >>> with TiffWriter('temp.ome.tif', bigtiff=True) as tif:
         ...     options = dict(tile=(256, 256), compression='jpeg')
         ...     tif.write(data, subifds=2, **options)
         ...     # save pyramid levels to the two subifds
-        ...     # in production use resampling to generate sub-resolutions!
+        ...     # in production use resampling to generate sub-resolutions
         ...     tif.write(data[::2, ::2], subfiletype=1, **options)
         ...     tif.write(data[::4, ::4], subfiletype=1, **options)
         
@@ -527,10 +550,17 @@ Description: Read and write TIFF(r) files
         <zarr.core.Array '/0' (1024, 1024, 3) uint8 read-only>
         >>> store.close()
         
-        Read an image stack from a series of TIFF files with a file name pattern:
+        Read images from a sequence of TIFF files as numpy array:
         
         >>> imwrite('temp_C001T001.tif', numpy.random.rand(64, 64))
         >>> imwrite('temp_C001T002.tif', numpy.random.rand(64, 64))
+        >>> image_sequence = imread(['temp_C001T001.tif', 'temp_C001T002.tif'])
+        >>> image_sequence.shape
+        (2, 64, 64)
+        
+        Read an image stack from a series of TIFF files with a file name pattern
+        as numpy or zarr arrays:
+        
         >>> image_sequence = TiffSequence('temp_C001*.tif', pattern='axes')
         >>> image_sequence.shape
         (1, 2)


=====================================
README.rst
=====================================
@@ -41,7 +41,7 @@ For command line usage run ``python -m tifffile --help``
 
 :License: BSD 3-Clause
 
-:Version: 2020.11.18
+:Version: 2020.11.26
 
 Requirements
 ------------
@@ -61,10 +61,18 @@ This release has been tested with the following requirements and dependencies
 
 Revisions
 ---------
+2020.11.26
+    Pass 4372 tests.
+    Add option to pass axes metadata to ImageJ writer.
+    Pad incomplete tiles passed to TiffWriter.write (#38).
+    Split TiffTag constructor (breaking).
+    Change TiffTag.dtype to TIFF.DATATYPES (breaking).
+    Add TiffTag.overwrite method.
+    Add script to change ImageDescription in files.
+    Add TiffWriter.overwrite_description method (WIP).
 2020.11.18
-    Pass 4363 tests.
-    Support writing SEPARATED colorspace (#37).
-    Use imagecodecs.deflate if available.
+    Support writing SEPARATED color space (#37).
+    Use imagecodecs.deflate codec if available.
     Fix SCN and NDPI series with Z dimensions.
     Add TiffReader alias for TiffFile.
     TiffPage.is_volumetric returns True if ImageDepth > 1.
@@ -73,7 +81,7 @@ Revisions
     Formally deprecate unused TiffFile parameters (scikit-image #4996).
 2020.9.30
     Allow to pass additional arguments to compression codecs.
-    Deprecate TiffWriter.save function (use TiffWriter.write).
+    Deprecate TiffWriter.save method (use TiffWriter.write).
     Deprecate TiffWriter.save compress parameter (use compression).
     Remove multifile parameter from TiffFile (breaking).
     Pass all is_flag arguments from imread to TiffFile.
@@ -101,7 +109,7 @@ Revisions
     Return full size tiles from Tiffpage.segments.
     Rename TiffPage.is_sgi property to is_volumetric (breaking).
     Rename TiffPageSeries.is_pyramid to is_pyramidal (breaking).
-    Fix TypeError when passing jpegtables to non-JPEG decode function (#25).
+    Fix TypeError when passing jpegtables to non-JPEG decode method (#25).
 2020.9.3
     Do not write contiguous series by default (breaking).
     Allow to write to SubIFDs (WIP).
@@ -150,7 +158,7 @@ Revisions
 2020.5.25
     Make imagecodecs an optional dependency again.
     Disable multi-threaded decoding of small LZW compressed segments.
-    Fix caching of TiffPage.decode function.
+    Fix caching of TiffPage.decode method.
     Fix xml.etree.cElementTree ImportError on Python 3.9.
     Fix tostring DeprecationWarning.
 2020.5.11
@@ -162,7 +170,7 @@ Revisions
     Always store ExtraSamples values in tuple (breaking).
 2020.5.5
     Allow to write tiled TIFF from iterable of tiles (WIP).
-    Add function to iterate over decoded segments of TiffPage (WIP).
+    Add method to iterate over decoded segments of TiffPage (WIP).
     Pass chunks of segments to ThreadPoolExecutor.map to reduce memory usage.
     Fix reading invalid files with too many strips.
     Fix writing over-aligned image data.
@@ -172,7 +180,7 @@ Revisions
     Remove maxsize parameter from asarray (breaking).
     Deprecate ijmetadata parameter from TiffWriter.save (use metadata).
 2020.2.16
-    Add function to decode individual strips or tiles.
+    Add method to decode individual strips or tiles.
     Read strips and tiles in order of their offsets.
     Enable multi-threading when decompressing multiple strips.
     Replace TiffPage.tags dictionary with TiffTags (breaking).
@@ -187,7 +195,7 @@ Revisions
     Fix xml2dict.
     Require imagecodecs >= 2020.1.31.
     Remove support for imagecodecs-lite (breaking).
-    Remove verify parameter to asarray function (breaking).
+    Remove verify parameter to asarray method (breaking).
     Remove deprecated lzw_decode functions (breaking).
     Remove support for Python 2.7 and 3.5 (breaking).
 2019.7.26
@@ -313,16 +321,27 @@ References
 
 Examples
 --------
-Save a 3D numpy array to a multi-page, 16-bit grayscale TIFF file:
+Write a numpy array to a single-page RGB TIFF file:
 
->>> data = numpy.random.randint(0, 2**12, (4, 301, 219), 'uint16')
+>>> data = numpy.random.randint(0, 255, (256, 256, 3), 'uint8')
+>>> imwrite('temp.tif', data, photometric='rgb')
+
+Read the image from the TIFF file as numpy array:
+
+>>> image = imread('temp.tif')
+>>> image.shape
+(256, 256, 3)
+
+Write a 3D numpy array to a multi-page, 16-bit grayscale TIFF file:
+
+>>> data = numpy.random.randint(0, 2**12, (64, 301, 219), 'uint16')
 >>> imwrite('temp.tif', data, photometric='minisblack')
 
 Read the whole image stack from the TIFF file as numpy array:
 
 >>> image_stack = imread('temp.tif')
 >>> image_stack.shape
-(4, 301, 219)
+(64, 301, 219)
 >>> image_stack.dtype
 dtype('uint16')
 
@@ -332,67 +351,46 @@ Read the image from the first page in the TIFF file as numpy array:
 >>> image.shape
 (301, 219)
 
-Read images from a sequence of TIFF files as numpy array:
-
->>> image_sequence = imread(['temp.tif', 'temp.tif'])
->>> image_sequence.shape
-(2, 4, 301, 219)
-
-Save a numpy array to a single-page RGB TIFF file:
-
->>> data = numpy.random.randint(0, 255, (256, 256, 3), 'uint8')
->>> imwrite('temp.tif', data, photometric='rgb')
-
-Save a floating-point array and metadata, using zlib compression:
+Read images from a selected range of pages:
 
->>> data = numpy.random.rand(2, 5, 3, 301, 219).astype('float32')
->>> imwrite('temp.tif', data, compression='zlib', metadata={'axes': 'TZCYX'})
+>>> image = imread('temp.tif', key=range(4, 40, 2))
+>>> image.shape
+(18, 301, 219)
 
-Save a volume with xyz voxel size 2.6755x2.6755x3.9474 micron^3 to an ImageJ
-formatted TIFF file:
+Iterate over all pages in the TIFF file and successively read images:
 
->>> volume = numpy.random.randn(57*256*256).astype('float32')
->>> volume.shape = 1, 57, 1, 256, 256, 1  # dimensions in TZCYXS order
->>> imwrite('temp.tif', volume, imagej=True, resolution=(1./2.6755, 1./2.6755),
-...         metadata={'spacing': 3.947368, 'unit': 'um'})
+>>> with TiffFile('temp.tif') as tif:
+...     for page in tif.pages:
+...         image = page.asarray()
 
-Get the shape and dtype of the volume stored in the TIFF file:
+Get information about the image stack in the TIFF file without reading
+the image data:
 
 >>> tif = TiffFile('temp.tif')
 >>> len(tif.pages)  # number of pages in the file
-57
+64
 >>> page = tif.pages[0]  # get shape and dtype of the image in the first page
 >>> page.shape
-(256, 256)
+(301, 219)
 >>> page.dtype
-dtype('float32')
+dtype('uint16')
 >>> page.axes
 'YX'
 >>> series = tif.series[0]  # get shape and dtype of the first image series
 >>> series.shape
-(57, 256, 256)
+(64, 301, 219)
 >>> series.dtype
-dtype('float32')
+dtype('uint16')
 >>> series.axes
-'ZYX'
+'QYX'
 >>> tif.close()
 
-Read hyperstack and metadata from the ImageJ file:
-
->>> with TiffFile('temp.tif') as tif:
-...     imagej_hyperstack = tif.asarray()
-...     imagej_metadata = tif.imagej_metadata
->>> imagej_hyperstack.shape
-(57, 256, 256)
->>> imagej_metadata['slices']
-57
-
-Read the "XResolution" tag from the first page in the TIFF file:
+Inspect the "XResolution" tag from the first page in the TIFF file:
 
 >>> with TiffFile('temp.tif') as tif:
 ...     tag = tif.pages[0].tags['XResolution']
 >>> tag.value
-(2000, 5351)
+(1, 1)
 >>> tag.name
 'XResolution'
 >>> tag.code
@@ -400,28 +398,71 @@ Read the "XResolution" tag from the first page in the TIFF file:
 >>> tag.count
 1
 >>> tag.dtype
-'2I'
+<DATATYPES.RATIONAL: 5>
 
-Read images from a selected range of pages:
+Iterate over all tags in the TIFF file:
 
->>> image = imread('temp.tif', key=range(4, 40, 2))
->>> image.shape
-(18, 256, 256)
+>>> with TiffFile('temp.tif') as tif:
+...     for page in tif.pages:
+...         for tag in page.tags:
+...             tag_name, tag_value = tag.name, tag.value
+
+Write a floating-point ndarray and metadata using BigTIFF format, tiling,
+compression, and planar storage:
+
+>>> data = numpy.random.rand(2, 5, 3, 301, 219).astype('float32')
+>>> imwrite('temp.tif', data, bigtiff=True, photometric='minisblack',
+...         compression='deflate', planarconfig='separate', tile=(32, 32),
+...         metadata={'axes': 'TZCYX'})
+
+Write a volume with xyz voxel size 2.6755x2.6755x3.9474 micron^3 to an
+ImageJ hyperstack formatted TIFF file:
+
+>>> volume = numpy.random.randn(57, 256, 256).astype('float32')
+>>> imwrite('temp.tif', volume, imagej=True, resolution=(1./2.6755, 1./2.6755),
+...         metadata={'spacing': 3.947368, 'unit': 'um', 'axes': 'ZYX'})
+
+Read the volume and metadata from the ImageJ file:
+
+>>> with TiffFile('temp.tif') as tif:
+...     volume = tif.asarray()
+...     axes = tif.series[0].axes
+...     imagej_metadata = tif.imagej_metadata
+>>> volume.shape
+(57, 256, 256)
+>>> axes
+'ZYX'
+>>> imagej_metadata['slices']
+57
 
 Create an empty TIFF file and write to the memory-mapped numpy array:
 
->>> memmap_image = memmap('temp.tif', shape=(256, 256), dtype='float32')
->>> memmap_image[255, 255] = 1.0
+>>> memmap_image = memmap('temp.tif', shape=(3, 256, 256), dtype='float32')
+>>> memmap_image[1, 255, 255] = 1.0
 >>> memmap_image.flush()
 >>> del memmap_image
 
 Memory-map image data of the first page in the TIFF file:
 
 >>> memmap_image = memmap('temp.tif', page=0)
->>> memmap_image[255, 255]
+>>> memmap_image[1, 255, 255]
 1.0
 >>> del memmap_image
 
+Write two numpy arrays to a multi-series TIFF file:
+
+>>> series0 = numpy.random.randint(0, 255, (32, 32, 3), 'uint8')
+>>> series1 = numpy.random.randint(0, 1023, (4, 256, 256), 'uint16')
+>>> with TiffWriter('temp.tif') as tif:
+...     tif.write(series0, photometric='rgb')
+...     tif.write(series1, photometric='minisblack')
+
+Read the second image series from the TIFF file:
+
+>>> series1 = imread('temp.tif', series=1)
+>>> series1.shape
+(4, 256, 256)
+
 Successively write the frames of one contiguous series to a TIFF file:
 
 >>> data = numpy.random.randint(0, 255, (30, 301, 219), 'uint8')
@@ -429,58 +470,40 @@ Successively write the frames of one contiguous series to a TIFF file:
 ...     for frame in data:
 ...         tif.write(frame, contiguous=True)
 
-Successively append image series to a BigTIFF file, which can exceed 4 GB:
-
->>> data = numpy.random.randint(0, 255, (5, 2, 3, 301, 219), 'uint8')
->>> with TiffWriter('temp.tif', bigtiff=True) as tif:
-...     for i in range(data.shape[0]):
-...         tif.write(data[i], photometric='minisblack')
-
-Append an image to the existing TIFF file:
+Append an image series to the existing TIFF file:
 
 >>> data = numpy.random.randint(0, 255, (301, 219, 3), 'uint8')
 >>> imwrite('temp.tif', data, append=True)
 
-Iterate over pages and tags in the TIFF file and successively read images:
+Create a TIFF file from a generator of tiles:
 
->>> with TiffFile('temp.tif') as tif:
-...     for page in tif.pages:
-...         for tag in page.tags:
-...             tag_name, tag_value = tag.name, tag.value
-...         image = page.asarray()
+>>> data = numpy.random.randint(0, 2**12, (31, 33, 3), 'uint16')
+>>> def tiles(data, tileshape):
+...     for y in range(0, data.shape[0], tileshape[0]):
+...         for x in range(0, data.shape[1], tileshape[1]):
+...             yield data[y : y + tileshape[0], x : x + tileshape[1]]
+>>> imwrite('temp.tif', tiles(data, (16, 16)), tile=(16, 16),
+...         shape=data.shape, dtype=data.dtype)
 
 Write two numpy arrays to a multi-series OME-TIFF file:
 
->>> data0 = numpy.random.randint(0, 255, (32, 32, 3), 'uint8')
->>> data1 = numpy.random.randint(0, 1023, (4, 256, 256), 'uint16')
+>>> series0 = numpy.random.randint(0, 255, (32, 32, 3), 'uint8')
+>>> series1 = numpy.random.randint(0, 1023, (4, 256, 256), 'uint16')
 >>> with TiffWriter('temp.ome.tif') as tif:
-...     tif.write(data0, photometric='rgb')
-...     tif.write(data1, photometric='minisblack',
+...     tif.write(series0, photometric='rgb')
+...     tif.write(series1, photometric='minisblack',
 ...              metadata={'axes': 'ZYX', 'SignificantBits': 10,
 ...                        'Plane': {'PositionZ': [0.0, 1.0, 2.0, 3.0]}})
 
-Read the second image series from the OME-TIFF file:
-
->>> series1 = imread('temp.ome.tif', series=1)
->>> series1.shape
-(4, 256, 256)
-
-Create a TIFF file from a generator of tiles:
-
->>> def tiles():
-...     data = numpy.arange(3*4*16*16, dtype='uint16').reshape((3*4, 16, 16))
-...     for i in range(data.shape[0]): yield data[i]
->>> imwrite('temp.tif', tiles(), dtype='uint16', shape=(48, 64), tile=(16, 16))
-
-Write a tiled, multi-resolution, pyramidal OME-TIFF file using JPEG
-compression. Sub-resolution images are written to SubIFDs:
+Write a tiled, multi-resolution, pyramidal, OME-TIFF file using
+JPEG compression. Sub-resolution images are written to SubIFDs:
 
 >>> data = numpy.arange(1024*1024*3, dtype='uint8').reshape((1024, 1024, 3))
->>> with TiffWriter('temp.ome.tif') as tif:
+>>> with TiffWriter('temp.ome.tif', bigtiff=True) as tif:
 ...     options = dict(tile=(256, 256), compression='jpeg')
 ...     tif.write(data, subifds=2, **options)
 ...     # save pyramid levels to the two subifds
-...     # in production use resampling to generate sub-resolutions!
+...     # in production use resampling to generate sub-resolutions
 ...     tif.write(data[::2, ::2], subfiletype=1, **options)
 ...     tif.write(data[::4, ::4], subfiletype=1, **options)
 
@@ -517,10 +540,17 @@ Use zarr to access the tiled, pyramidal images in the TIFF file:
 <zarr.core.Array '/0' (1024, 1024, 3) uint8 read-only>
 >>> store.close()
 
-Read an image stack from a series of TIFF files with a file name pattern:
+Read images from a sequence of TIFF files as numpy array:
 
 >>> imwrite('temp_C001T001.tif', numpy.random.rand(64, 64))
 >>> imwrite('temp_C001T002.tif', numpy.random.rand(64, 64))
+>>> image_sequence = imread(['temp_C001T001.tif', 'temp_C001T002.tif'])
+>>> image_sequence.shape
+(2, 64, 64)
+
+Read an image stack from a series of TIFF files with a file name pattern
+as numpy or zarr arrays:
+
 >>> image_sequence = TiffSequence('temp_C001*.tif', pattern='axes')
 >>> image_sequence.shape
 (1, 2)


=====================================
setup.py
=====================================
@@ -106,6 +106,7 @@ setup(
     entry_points={
         'console_scripts': [
             'tifffile = tifffile:main',
+            'tiffcomment = tifffile.tiffcomment:main',
             'lsm2bin = tifffile.lsm2bin:main',
         ],
         # 'napari.plugin': ['tifffile = tifffile.napari_tifffile'],


=====================================
tests/test_tifffile.py
=====================================
@@ -42,7 +42,7 @@ Private data files are not available due to size and copyright restrictions.
 
 :License: BSD 3-Clause
 
-:Version: 2020.11.18
+:Version: 2020.11.26
 
 """
 
@@ -59,16 +59,15 @@ import sys
 import tempfile
 from io import BytesIO
 
-import pytest
 import numpy
+import pytest
+import tifffile
 from numpy.testing import (
     assert_allclose,
     assert_array_almost_equal,
     assert_array_equal,
 )
 
-import tifffile
-
 try:
     from tifffile import *
 
@@ -111,16 +110,16 @@ from tifffile.tifffile import (
     FileSequence,
     OmeXml,
     OmeXmlError,
-    TiffWriter,
-    TiffReader,
     TiffFile,
     TiffFileError,
     TiffFrame,
     TiffPage,
     TiffPageSeries,
+    TiffReader,
     TiffSequence,
     TiffTag,
     TiffTags,
+    TiffWriter,
     apply_colormap,
     asbool,
     byteorder_compare,
@@ -166,6 +165,7 @@ from tifffile.tifffile import (
     stripnull,
     subresolution,
     svs_description_metadata,
+    tiffcomment,
     transpose_axes,
     unpack_rgb,
     validate_jhove,
@@ -854,6 +854,41 @@ def test_issue_tile_partial():
             assert__str__(tif)
 
 
+ at pytest.mark.parametrize('samples', [1, 3])
+def test_issue_tiles_pad(samples):
+    """Test tiles from iterator get padded."""
+    # https://github.com/cgohlke/tifffile/issues/38
+    if samples == 3:
+        data = numpy.random.randint(0, 2 ** 12, (31, 33, 3), 'uint16')
+    else:
+        data = numpy.random.randint(0, 2 ** 12, (31, 33), 'uint16')
+
+    def tiles(data, tileshape, pad=False):
+        for y in range(0, data.shape[0], tileshape[0]):
+            for x in range(0, data.shape[1], tileshape[1]):
+                tile = data[y : y + tileshape[0], x : x + tileshape[1]]
+                if pad and tile.shape != tileshape:
+                    tile = numpy.pad(
+                        tile,
+                        (
+                            (0, tileshape[0] - tile.shape[0]),
+                            (0, tileshape[1] - tile.shape[1]),
+                        ),
+                    )
+                yield tile
+
+    with TempFileName(f'issue_tiles_pad_{samples}') as fname:
+        imwrite(
+            fname,
+            tiles(data, (16, 16)),
+            dtype=data.dtype,
+            shape=data.shape,
+            tile=(16, 16),
+        )
+        assert_array_equal(imread(fname), data)
+        assert_valid_tiff(fname)
+
+
 def test_issue_fcontiguous():
     """Test writing F-contiguous arrays."""
     # https://github.com/cgohlke/tifffile/issues/24
@@ -954,7 +989,7 @@ def test_issue_write_separated():
     contig = random_data('uint8', (63, 95, 4))
     separate = random_data('uint8', (4, 63, 95))
     extrasample = random_data('uint8', (63, 95, 5))
-    with TempFileName(f'issue_write_separated') as fname:
+    with TempFileName('issue_write_separated') as fname:
         with TiffWriter(fname) as tif:
             tif.write(contig, photometric='SEPARATED')
             tif.write(separate, photometric=SEPARATED)
@@ -1075,6 +1110,103 @@ def test_class_filecache():
         handles[0].close()
 
 
+ at pytest.mark.parametrize('bigtiff', [False, True])
+ at pytest.mark.parametrize('byteorder', ['<', '>'])
+def test_class_tifftag_overwrite(bigtiff, byteorder):
+    """Test TiffTag.overwrite method."""
+    data = numpy.ones((16, 16, 3), dtype=byteorder + 'i2')
+    bt = '_bigtiff' if bigtiff else ''
+    bo = 'be' if byteorder == '>' else 'le'
+
+    with TempFileName(f'class_tifftag_overwrite_{bo}{bt}') as fname:
+        imwrite(fname, data, bigtiff=bigtiff, software='in')
+
+        with TiffFile(fname, mode='r+b') as tif:
+            tags = tif.pages[0].tags
+            # inline -> inline
+            tag = tags[305]
+            t305 = tags[305].overwrite(tif, 'inl')
+            assert tag.valueoffset == t305.valueoffset
+            valueoffset = tag.valueoffset
+            # xresolution
+            tag = tags[282]
+            t282 = tags[282].overwrite(tif, (2000, 1000))
+            assert tag.valueoffset == t282.valueoffset
+            # sampleformat, int -> uint
+            tag = tags[339]
+            t339 = tags[339].overwrite(tif, (1, 1, 1))
+            assert tag.valueoffset == t339.valueoffset
+
+        with TiffFile(fname) as tif:
+            tags = tif.pages[0].tags
+            tag = tags[305]
+            assert tag.value == 'inl'
+            assert tag.count == t305.count
+            tag = tags[282]
+            assert tag.value == (2000, 1000)
+            assert tag.count == t282.count
+            tag = tags[339]
+            assert tag.value == (1, 1, 1)
+            assert tag.count == t339.count
+
+        # inline -> separate
+        with TiffFile(fname, mode='r+b') as tif:
+            tag = tif.pages[0].tags[305]
+            t305 = tag.overwrite(tif, 'separate')
+            assert tag.valueoffset != t305.valueoffset
+
+        # separate at end -> separate longer
+        with TiffFile(fname, mode='r+b') as tif:
+            tag = tif.pages[0].tags[305]
+            assert tag.value == 'separate'
+            assert tag.valueoffset == t305.valueoffset
+            t305 = tag.overwrite(tif, 'separate longer')
+            assert tag.valueoffset == t305.valueoffset  # overwrite, not append
+
+        # separate -> separate shorter
+        with TiffFile(fname, mode='r+b') as tif:
+            tag = tif.pages[0].tags[305]
+            assert tag.value == 'separate longer'
+            assert tag.valueoffset == t305.valueoffset
+            t305 = tag.overwrite(tif, 'separate short')
+            assert tag.valueoffset == t305.valueoffset
+
+        # separate -> separate longer
+        with TiffFile(fname, mode='r+b') as tif:
+            tag = tif.pages[0].tags[305]
+            assert tag.value == 'separate short'
+            assert tag.valueoffset == t305.valueoffset
+            filesize = tif.filehandle.size
+            t305 = tag.overwrite(tif, 'separate longer')
+            assert tag.valueoffset != t305.valueoffset
+            assert t305.valueoffset == filesize  # append to end
+
+        # separate -> inline
+        with TiffFile(fname, mode='r+b') as tif:
+            tag = tif.pages[0].tags[305]
+            assert tag.value == 'separate longer'
+            assert tag.valueoffset == t305.valueoffset
+            t305 = tag.overwrite(tif, 'inl')
+            assert tag.valueoffset != t305.valueoffset
+            assert t305.valueoffset == valueoffset
+
+        # inline - > erase
+        with TiffFile(fname, mode='r+b') as tif:
+            tag = tif.pages[0].tags[305]
+            assert tag.value == 'inl'
+            assert tag.valueoffset == t305.valueoffset
+            t305 = tag.overwrite(tif, '')
+            assert tag.valueoffset == t305.valueoffset
+
+        with TiffFile(fname) as tif:
+            tag = tif.pages[0].tags[305]
+            assert tag.value == ''
+            assert tag.valueoffset == t305.valueoffset
+
+        if not bigtiff:
+            assert_valid_tiff(fname)
+
+
 def test_class_tifftags():
     """Test TiffTags interface."""
     data = random_data('uint8', (21, 31))
@@ -1871,15 +2003,30 @@ def test_func_imagej_shape():
 
 def test_func_imagej_description():
     """Test imagej_description function."""
-    imagej_str = (
+    expected = (
         'ImageJ=1.11a\nimages=510\nchannels=2\nslices=5\n'
         'frames=51\nhyperstack=true\nmode=grayscale\nloop=false\n'
     )
-    assert imagej_description((51, 5, 2, 196, 171)) == imagej_str
+    assert imagej_description((51, 5, 2, 196, 171)) == expected
+    assert imagej_description((51, 5, 2, 196, 171), axes='TZCYX') == expected
+    expected = (
+        'ImageJ=1.11a\nimages=2\nslices=2\nhyperstack=true\nmode=grayscale\n'
+    )
+    assert imagej_description((1, 2, 1, 196, 171)) == expected
+    assert imagej_description((2, 196, 171), axes='ZYX') == expected
     expected = 'ImageJ=1.11a\nimages=1\nhyperstack=true\nmode=grayscale\n'
     assert imagej_description((196, 171)) == expected
+    assert imagej_description((196, 171), axes='YX') == expected
     expected = 'ImageJ=1.11a\nimages=1\nhyperstack=true\n'
     assert imagej_description((196, 171, 3)) == expected
+    assert imagej_description((196, 171, 3), axes='YXS') == expected
+
+    with pytest.raises(ValueError):
+        imagej_description((196, 171, 3), axes='TYXS')
+    with pytest.raises(ValueError):
+        imagej_description((196, 171, 2), axes='TYXS')
+    with pytest.raises(ValueError):
+        imagej_description((3, 196, 171, 3), axes='ZTYX')
 
 
 def test_func_imagej_description_metadata():
@@ -2351,7 +2498,7 @@ def test_func_pformat_xml():
 @pytest.mark.skipif(SKIP_PRIVATE or SKIP_32BIT or SKIP_HUGE, reason=REASON)
 def test_func_lsm2bin():
     """Test lsm2bin function."""
-    # Converst LSM to BIN
+    # Convert LSM to BIN
     fname = private_file(
         'lsm/Twoareas_Zstacks54slices_3umintervals_5cycles.lsm'
     )
@@ -2360,6 +2507,19 @@ def test_func_lsm2bin():
     lsm2bin(fname, '', verbose=True)
 
 
+def test_func_tiffcomment():
+    """Test tiffcomment function."""
+    data = random_data('uint8', (33, 31, 3))
+    with TempFileName('func_tiffcomment') as fname:
+        comment = 'A comment'
+        imwrite(fname, data, description=comment, metadata=None)
+        assert comment == tiffcomment(fname)
+        comment = 'changed comment'
+        tiffcomment(fname, comment)
+        assert comment == tiffcomment(fname)
+        assert_valid_tiff(fname)
+
+
 def test_func_create_output():
     """Test create_output function."""
     shape = (16, 17)
@@ -8220,11 +8380,11 @@ def test_write_bytecount(bigtiff, tiled, compression, count, bytecount):
     data = random_data('uint8', shape)
 
     if count == 1:
-        dtype = 'Q' if bigtiff else 'I'
+        dtype = TIFF.DATATYPES.LONG8 if bigtiff else TIFF.DATATYPES.LONG
     elif bytecount == 256:
-        dtype = 'I'
+        dtype = TIFF.DATATYPES.LONG
     else:
-        dtype = 'H'
+        dtype = TIFF.DATATYPES.SHORT
 
     with TempFileName(
         'bytecounts_{}{}{}{}{}'.format(
@@ -8245,7 +8405,7 @@ def test_write_bytecount(bigtiff, tiled, compression, count, bytecount):
             assert len(tif.pages) == 1
             page = tif.pages[0]
             assert page.tags[tag].count == count
-            assert page.tags[tag].dtype[-1] == dtype
+            assert page.tags[tag].dtype == dtype
             assert page.is_contiguous != bool(compression)
             assert page.planarconfig == CONTIG
             assert page.photometric == MINISBLACK
@@ -10373,7 +10533,7 @@ def test_write_tileiter():
         with pytest.raises(ValueError):
             # shape mismatch
             imwrite(
-                fname, tiles(), shape=(43, 61), tile=(32, 32), dtype='uint16'
+                fname, tiles(), shape=(43, 61), tile=(8, 8), dtype='uint16'
             )
 
         imwrite(fname, tiles(), shape=(43, 61), tile=(16, 16), dtype='uint16')
@@ -10391,9 +10551,8 @@ def test_write_tileiter():
 
 def test_write_tileiter_separate():
     """Test write separate tiles from iterator."""
-    data = numpy.arange(2 * 3 * 4 * 16 * 16, dtype='uint16').reshape(
-        (2 * 3 * 4, 16, 16)
-    )
+    data = numpy.arange(2 * 3 * 4 * 16 * 16, dtype='uint16')
+    data = data.reshape((2 * 3 * 4, 16, 16))
 
     def tiles():
         for i in range(data.shape[0]):
@@ -11505,7 +11664,6 @@ def test_write_ome_methods(method):
             imwrite(
                 fname,
                 data,
-                bigtiff=True,
                 byteorder='>',
                 photometric='minisblack',
                 metadata=metadata,
@@ -11523,7 +11681,7 @@ def test_write_ome_methods(method):
             # make sure storage options (compression, byteorder, photometric)
             #   match OME-XML
             # write OME-XML to first page only
-            with TiffWriter(fname, bigtiff=True, byteorder='>') as tif:
+            with TiffWriter(fname, byteorder='>') as tif:
                 for i, image in enumerate(pages()):
                     description = omexml if i == 0 else None
                     tif.write(
@@ -11541,7 +11699,6 @@ def test_write_ome_methods(method):
                 pages(),
                 shape=shape,
                 dtype=dtype,
-                bigtiff=True,
                 byteorder='>',
                 photometric='minisblack',
                 metadata={'axes': axes},
@@ -11555,7 +11712,6 @@ def test_write_ome_methods(method):
                 shape=shape,
                 dtype=dtype,
                 compression='zlib',
-                bigtiff=True,
                 byteorder='>',
                 photometric='minisblack',
                 metadata={'axes': axes},
@@ -11566,7 +11722,6 @@ def test_write_ome_methods(method):
             imwrite(
                 fname,
                 data,
-                bigtiff=True,
                 byteorder='>',
                 photometric='minisblack',
                 metadata={'axes': axes},
@@ -11596,6 +11751,56 @@ def test_write_ome_methods(method):
             assert_valid_omexml(tif.ome_metadata)
             assert__str__(tif)
 
+        assert_valid_tiff(fname)
+
+
+ at pytest.mark.parametrize('contiguous', [True, False])
+def test_write_ome_manual(contiguous):
+    """Test writing OME-TIFF manually."""
+    data = numpy.random.randint(0, 255, (19, 31, 21), 'uint8')
+
+    with TempFileName(f'write_ome__manual{int(contiguous)}.ome') as fname:
+
+        with TiffWriter(fname) as tif:
+            # sucessively write image data to TIFF pages
+            # disable tifffile from writing any metadata
+            # add empty ImageDescription tag to first page
+            for i, frame in enumerate(data):
+                tif.write(
+                    frame,
+                    contiguous=contiguous,
+                    metadata=None,
+                    description=None if i else b'',
+                )
+            # update ImageDescription tag with custom OME-XML
+            xml = OmeXml()
+            xml.addimage(
+                'uint8', (16, 31, 21), (16, 1, 1, 31, 21, 1), axes='ZYX'
+            )
+            xml.addimage(
+                'uint8', (3, 31, 21), (3, 1, 1, 31, 21, 1), axes='CYX'
+            )
+            tif.overwrite_description(xml.tostring())
+
+        with TiffFile(fname) as tif:
+            assert tif.is_ome
+            assert len(tif.pages) == 19
+            assert len(tif.series) == 2
+            # assert series properties
+            series = tif.series[0]
+            assert series.axes == 'ZYX'
+            assert bool(series.offset) == contiguous
+            assert_array_equal(data[:16], series.asarray())
+            series = tif.series[1]
+            assert series.axes == 'CYX'
+            assert bool(series.offset) == contiguous
+            assert_array_equal(data[16:], series.asarray())
+            #
+            assert_valid_omexml(tif.ome_metadata)
+            assert__str__(tif)
+
+        assert_valid_tiff(fname)
+
 
 ###############################################################################
 


=====================================
tifffile.egg-info/PKG-INFO
=====================================
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: tifffile
-Version: 2020.11.18
+Version: 2020.11.26
 Summary: Read and write TIFF(r) files
 Home-page: https://www.lfd.uci.edu/~gohlke/
 Author: Christoph Gohlke
@@ -51,7 +51,7 @@ Description: Read and write TIFF(r) files
         
         :License: BSD 3-Clause
         
-        :Version: 2020.11.18
+        :Version: 2020.11.26
         
         Requirements
         ------------
@@ -71,10 +71,18 @@ Description: Read and write TIFF(r) files
         
         Revisions
         ---------
+        2020.11.26
+            Pass 4372 tests.
+            Add option to pass axes metadata to ImageJ writer.
+            Pad incomplete tiles passed to TiffWriter.write (#38).
+            Split TiffTag constructor (breaking).
+            Change TiffTag.dtype to TIFF.DATATYPES (breaking).
+            Add TiffTag.overwrite method.
+            Add script to change ImageDescription in files.
+            Add TiffWriter.overwrite_description method (WIP).
         2020.11.18
-            Pass 4363 tests.
-            Support writing SEPARATED colorspace (#37).
-            Use imagecodecs.deflate if available.
+            Support writing SEPARATED color space (#37).
+            Use imagecodecs.deflate codec if available.
             Fix SCN and NDPI series with Z dimensions.
             Add TiffReader alias for TiffFile.
             TiffPage.is_volumetric returns True if ImageDepth > 1.
@@ -83,7 +91,7 @@ Description: Read and write TIFF(r) files
             Formally deprecate unused TiffFile parameters (scikit-image #4996).
         2020.9.30
             Allow to pass additional arguments to compression codecs.
-            Deprecate TiffWriter.save function (use TiffWriter.write).
+            Deprecate TiffWriter.save method (use TiffWriter.write).
             Deprecate TiffWriter.save compress parameter (use compression).
             Remove multifile parameter from TiffFile (breaking).
             Pass all is_flag arguments from imread to TiffFile.
@@ -111,7 +119,7 @@ Description: Read and write TIFF(r) files
             Return full size tiles from Tiffpage.segments.
             Rename TiffPage.is_sgi property to is_volumetric (breaking).
             Rename TiffPageSeries.is_pyramid to is_pyramidal (breaking).
-            Fix TypeError when passing jpegtables to non-JPEG decode function (#25).
+            Fix TypeError when passing jpegtables to non-JPEG decode method (#25).
         2020.9.3
             Do not write contiguous series by default (breaking).
             Allow to write to SubIFDs (WIP).
@@ -160,7 +168,7 @@ Description: Read and write TIFF(r) files
         2020.5.25
             Make imagecodecs an optional dependency again.
             Disable multi-threaded decoding of small LZW compressed segments.
-            Fix caching of TiffPage.decode function.
+            Fix caching of TiffPage.decode method.
             Fix xml.etree.cElementTree ImportError on Python 3.9.
             Fix tostring DeprecationWarning.
         2020.5.11
@@ -172,7 +180,7 @@ Description: Read and write TIFF(r) files
             Always store ExtraSamples values in tuple (breaking).
         2020.5.5
             Allow to write tiled TIFF from iterable of tiles (WIP).
-            Add function to iterate over decoded segments of TiffPage (WIP).
+            Add method to iterate over decoded segments of TiffPage (WIP).
             Pass chunks of segments to ThreadPoolExecutor.map to reduce memory usage.
             Fix reading invalid files with too many strips.
             Fix writing over-aligned image data.
@@ -182,7 +190,7 @@ Description: Read and write TIFF(r) files
             Remove maxsize parameter from asarray (breaking).
             Deprecate ijmetadata parameter from TiffWriter.save (use metadata).
         2020.2.16
-            Add function to decode individual strips or tiles.
+            Add method to decode individual strips or tiles.
             Read strips and tiles in order of their offsets.
             Enable multi-threading when decompressing multiple strips.
             Replace TiffPage.tags dictionary with TiffTags (breaking).
@@ -197,7 +205,7 @@ Description: Read and write TIFF(r) files
             Fix xml2dict.
             Require imagecodecs >= 2020.1.31.
             Remove support for imagecodecs-lite (breaking).
-            Remove verify parameter to asarray function (breaking).
+            Remove verify parameter to asarray method (breaking).
             Remove deprecated lzw_decode functions (breaking).
             Remove support for Python 2.7 and 3.5 (breaking).
         2019.7.26
@@ -323,16 +331,27 @@ Description: Read and write TIFF(r) files
         
         Examples
         --------
-        Save a 3D numpy array to a multi-page, 16-bit grayscale TIFF file:
+        Write a numpy array to a single-page RGB TIFF file:
         
-        >>> data = numpy.random.randint(0, 2**12, (4, 301, 219), 'uint16')
+        >>> data = numpy.random.randint(0, 255, (256, 256, 3), 'uint8')
+        >>> imwrite('temp.tif', data, photometric='rgb')
+        
+        Read the image from the TIFF file as numpy array:
+        
+        >>> image = imread('temp.tif')
+        >>> image.shape
+        (256, 256, 3)
+        
+        Write a 3D numpy array to a multi-page, 16-bit grayscale TIFF file:
+        
+        >>> data = numpy.random.randint(0, 2**12, (64, 301, 219), 'uint16')
         >>> imwrite('temp.tif', data, photometric='minisblack')
         
         Read the whole image stack from the TIFF file as numpy array:
         
         >>> image_stack = imread('temp.tif')
         >>> image_stack.shape
-        (4, 301, 219)
+        (64, 301, 219)
         >>> image_stack.dtype
         dtype('uint16')
         
@@ -342,67 +361,46 @@ Description: Read and write TIFF(r) files
         >>> image.shape
         (301, 219)
         
-        Read images from a sequence of TIFF files as numpy array:
-        
-        >>> image_sequence = imread(['temp.tif', 'temp.tif'])
-        >>> image_sequence.shape
-        (2, 4, 301, 219)
-        
-        Save a numpy array to a single-page RGB TIFF file:
-        
-        >>> data = numpy.random.randint(0, 255, (256, 256, 3), 'uint8')
-        >>> imwrite('temp.tif', data, photometric='rgb')
-        
-        Save a floating-point array and metadata, using zlib compression:
+        Read images from a selected range of pages:
         
-        >>> data = numpy.random.rand(2, 5, 3, 301, 219).astype('float32')
-        >>> imwrite('temp.tif', data, compression='zlib', metadata={'axes': 'TZCYX'})
+        >>> image = imread('temp.tif', key=range(4, 40, 2))
+        >>> image.shape
+        (18, 301, 219)
         
-        Save a volume with xyz voxel size 2.6755x2.6755x3.9474 micron^3 to an ImageJ
-        formatted TIFF file:
+        Iterate over all pages in the TIFF file and successively read images:
         
-        >>> volume = numpy.random.randn(57*256*256).astype('float32')
-        >>> volume.shape = 1, 57, 1, 256, 256, 1  # dimensions in TZCYXS order
-        >>> imwrite('temp.tif', volume, imagej=True, resolution=(1./2.6755, 1./2.6755),
-        ...         metadata={'spacing': 3.947368, 'unit': 'um'})
+        >>> with TiffFile('temp.tif') as tif:
+        ...     for page in tif.pages:
+        ...         image = page.asarray()
         
-        Get the shape and dtype of the volume stored in the TIFF file:
+        Get information about the image stack in the TIFF file without reading
+        the image data:
         
         >>> tif = TiffFile('temp.tif')
         >>> len(tif.pages)  # number of pages in the file
-        57
+        64
         >>> page = tif.pages[0]  # get shape and dtype of the image in the first page
         >>> page.shape
-        (256, 256)
+        (301, 219)
         >>> page.dtype
-        dtype('float32')
+        dtype('uint16')
         >>> page.axes
         'YX'
         >>> series = tif.series[0]  # get shape and dtype of the first image series
         >>> series.shape
-        (57, 256, 256)
+        (64, 301, 219)
         >>> series.dtype
-        dtype('float32')
+        dtype('uint16')
         >>> series.axes
-        'ZYX'
+        'QYX'
         >>> tif.close()
         
-        Read hyperstack and metadata from the ImageJ file:
-        
-        >>> with TiffFile('temp.tif') as tif:
-        ...     imagej_hyperstack = tif.asarray()
-        ...     imagej_metadata = tif.imagej_metadata
-        >>> imagej_hyperstack.shape
-        (57, 256, 256)
-        >>> imagej_metadata['slices']
-        57
-        
-        Read the "XResolution" tag from the first page in the TIFF file:
+        Inspect the "XResolution" tag from the first page in the TIFF file:
         
         >>> with TiffFile('temp.tif') as tif:
         ...     tag = tif.pages[0].tags['XResolution']
         >>> tag.value
-        (2000, 5351)
+        (1, 1)
         >>> tag.name
         'XResolution'
         >>> tag.code
@@ -410,28 +408,71 @@ Description: Read and write TIFF(r) files
         >>> tag.count
         1
         >>> tag.dtype
-        '2I'
+        <DATATYPES.RATIONAL: 5>
         
-        Read images from a selected range of pages:
+        Iterate over all tags in the TIFF file:
         
-        >>> image = imread('temp.tif', key=range(4, 40, 2))
-        >>> image.shape
-        (18, 256, 256)
+        >>> with TiffFile('temp.tif') as tif:
+        ...     for page in tif.pages:
+        ...         for tag in page.tags:
+        ...             tag_name, tag_value = tag.name, tag.value
+        
+        Write a floating-point ndarray and metadata using BigTIFF format, tiling,
+        compression, and planar storage:
+        
+        >>> data = numpy.random.rand(2, 5, 3, 301, 219).astype('float32')
+        >>> imwrite('temp.tif', data, bigtiff=True, photometric='minisblack',
+        ...         compression='deflate', planarconfig='separate', tile=(32, 32),
+        ...         metadata={'axes': 'TZCYX'})
+        
+        Write a volume with xyz voxel size 2.6755x2.6755x3.9474 micron^3 to an
+        ImageJ hyperstack formatted TIFF file:
+        
+        >>> volume = numpy.random.randn(57, 256, 256).astype('float32')
+        >>> imwrite('temp.tif', volume, imagej=True, resolution=(1./2.6755, 1./2.6755),
+        ...         metadata={'spacing': 3.947368, 'unit': 'um', 'axes': 'ZYX'})
+        
+        Read the volume and metadata from the ImageJ file:
+        
+        >>> with TiffFile('temp.tif') as tif:
+        ...     volume = tif.asarray()
+        ...     axes = tif.series[0].axes
+        ...     imagej_metadata = tif.imagej_metadata
+        >>> volume.shape
+        (57, 256, 256)
+        >>> axes
+        'ZYX'
+        >>> imagej_metadata['slices']
+        57
         
         Create an empty TIFF file and write to the memory-mapped numpy array:
         
-        >>> memmap_image = memmap('temp.tif', shape=(256, 256), dtype='float32')
-        >>> memmap_image[255, 255] = 1.0
+        >>> memmap_image = memmap('temp.tif', shape=(3, 256, 256), dtype='float32')
+        >>> memmap_image[1, 255, 255] = 1.0
         >>> memmap_image.flush()
         >>> del memmap_image
         
         Memory-map image data of the first page in the TIFF file:
         
         >>> memmap_image = memmap('temp.tif', page=0)
-        >>> memmap_image[255, 255]
+        >>> memmap_image[1, 255, 255]
         1.0
         >>> del memmap_image
         
+        Write two numpy arrays to a multi-series TIFF file:
+        
+        >>> series0 = numpy.random.randint(0, 255, (32, 32, 3), 'uint8')
+        >>> series1 = numpy.random.randint(0, 1023, (4, 256, 256), 'uint16')
+        >>> with TiffWriter('temp.tif') as tif:
+        ...     tif.write(series0, photometric='rgb')
+        ...     tif.write(series1, photometric='minisblack')
+        
+        Read the second image series from the TIFF file:
+        
+        >>> series1 = imread('temp.tif', series=1)
+        >>> series1.shape
+        (4, 256, 256)
+        
         Successively write the frames of one contiguous series to a TIFF file:
         
         >>> data = numpy.random.randint(0, 255, (30, 301, 219), 'uint8')
@@ -439,58 +480,40 @@ Description: Read and write TIFF(r) files
         ...     for frame in data:
         ...         tif.write(frame, contiguous=True)
         
-        Successively append image series to a BigTIFF file, which can exceed 4 GB:
-        
-        >>> data = numpy.random.randint(0, 255, (5, 2, 3, 301, 219), 'uint8')
-        >>> with TiffWriter('temp.tif', bigtiff=True) as tif:
-        ...     for i in range(data.shape[0]):
-        ...         tif.write(data[i], photometric='minisblack')
-        
-        Append an image to the existing TIFF file:
+        Append an image series to the existing TIFF file:
         
         >>> data = numpy.random.randint(0, 255, (301, 219, 3), 'uint8')
         >>> imwrite('temp.tif', data, append=True)
         
-        Iterate over pages and tags in the TIFF file and successively read images:
+        Create a TIFF file from a generator of tiles:
         
-        >>> with TiffFile('temp.tif') as tif:
-        ...     for page in tif.pages:
-        ...         for tag in page.tags:
-        ...             tag_name, tag_value = tag.name, tag.value
-        ...         image = page.asarray()
+        >>> data = numpy.random.randint(0, 2**12, (31, 33, 3), 'uint16')
+        >>> def tiles(data, tileshape):
+        ...     for y in range(0, data.shape[0], tileshape[0]):
+        ...         for x in range(0, data.shape[1], tileshape[1]):
+        ...             yield data[y : y + tileshape[0], x : x + tileshape[1]]
+        >>> imwrite('temp.tif', tiles(data, (16, 16)), tile=(16, 16),
+        ...         shape=data.shape, dtype=data.dtype)
         
         Write two numpy arrays to a multi-series OME-TIFF file:
         
-        >>> data0 = numpy.random.randint(0, 255, (32, 32, 3), 'uint8')
-        >>> data1 = numpy.random.randint(0, 1023, (4, 256, 256), 'uint16')
+        >>> series0 = numpy.random.randint(0, 255, (32, 32, 3), 'uint8')
+        >>> series1 = numpy.random.randint(0, 1023, (4, 256, 256), 'uint16')
         >>> with TiffWriter('temp.ome.tif') as tif:
-        ...     tif.write(data0, photometric='rgb')
-        ...     tif.write(data1, photometric='minisblack',
+        ...     tif.write(series0, photometric='rgb')
+        ...     tif.write(series1, photometric='minisblack',
         ...              metadata={'axes': 'ZYX', 'SignificantBits': 10,
         ...                        'Plane': {'PositionZ': [0.0, 1.0, 2.0, 3.0]}})
         
-        Read the second image series from the OME-TIFF file:
-        
-        >>> series1 = imread('temp.ome.tif', series=1)
-        >>> series1.shape
-        (4, 256, 256)
-        
-        Create a TIFF file from a generator of tiles:
-        
-        >>> def tiles():
-        ...     data = numpy.arange(3*4*16*16, dtype='uint16').reshape((3*4, 16, 16))
-        ...     for i in range(data.shape[0]): yield data[i]
-        >>> imwrite('temp.tif', tiles(), dtype='uint16', shape=(48, 64), tile=(16, 16))
-        
-        Write a tiled, multi-resolution, pyramidal OME-TIFF file using JPEG
-        compression. Sub-resolution images are written to SubIFDs:
+        Write a tiled, multi-resolution, pyramidal, OME-TIFF file using
+        JPEG compression. Sub-resolution images are written to SubIFDs:
         
         >>> data = numpy.arange(1024*1024*3, dtype='uint8').reshape((1024, 1024, 3))
-        >>> with TiffWriter('temp.ome.tif') as tif:
+        >>> with TiffWriter('temp.ome.tif', bigtiff=True) as tif:
         ...     options = dict(tile=(256, 256), compression='jpeg')
         ...     tif.write(data, subifds=2, **options)
         ...     # save pyramid levels to the two subifds
-        ...     # in production use resampling to generate sub-resolutions!
+        ...     # in production use resampling to generate sub-resolutions
         ...     tif.write(data[::2, ::2], subfiletype=1, **options)
         ...     tif.write(data[::4, ::4], subfiletype=1, **options)
         
@@ -527,10 +550,17 @@ Description: Read and write TIFF(r) files
         <zarr.core.Array '/0' (1024, 1024, 3) uint8 read-only>
         >>> store.close()
         
-        Read an image stack from a series of TIFF files with a file name pattern:
+        Read images from a sequence of TIFF files as numpy array:
         
         >>> imwrite('temp_C001T001.tif', numpy.random.rand(64, 64))
         >>> imwrite('temp_C001T002.tif', numpy.random.rand(64, 64))
+        >>> image_sequence = imread(['temp_C001T001.tif', 'temp_C001T002.tif'])
+        >>> image_sequence.shape
+        (2, 64, 64)
+        
+        Read an image stack from a series of TIFF files with a file name pattern
+        as numpy or zarr arrays:
+        
         >>> image_sequence = TiffSequence('temp_C001*.tif', pattern='axes')
         >>> image_sequence.shape
         (1, 2)


=====================================
tifffile.egg-info/SOURCES.txt
=====================================
@@ -9,6 +9,7 @@ tests/test_tifffile.py
 tifffile/__init__.py
 tifffile/__main__.py
 tifffile/lsm2bin.py
+tifffile/tiffcomment.py
 tifffile/tifffile.py
 tifffile/tifffile_geodb.py
 tifffile.egg-info/PKG-INFO


=====================================
tifffile.egg-info/entry_points.txt
=====================================
@@ -1,4 +1,5 @@
 [console_scripts]
 lsm2bin = tifffile.lsm2bin:main
+tiffcomment = tifffile.tiffcomment:main
 tifffile = tifffile:main
 


=====================================
tifffile/tiffcomment.py
=====================================
@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+# tiffcomment.py
+
+"""Print or replace ImageDescription in first page of TIFF file.
+
+Usage: tiffcomment [--set comment] file
+
+"""
+
+import os
+import sys
+
+try:
+    from .tifffile import tiffcomment
+except ImportError:
+    try:
+        from tifffile.tifffile import tiffcomment
+    except ImportError:
+        from tifffile import tiffcomment
+
+
+def main(argv=None):
+    """Tiffcomment command line usage main function."""
+    if argv is None:
+        argv = sys.argv
+
+    if len(argv) > 2 and argv[1] in '--set':
+        comment = argv[2]
+        files = argv[3:]
+    else:
+        comment = None
+        files = argv[1:]
+
+    if len(files) == 0 or any(f.startswith('-') for f in files):
+        print()
+        print(__doc__.strip())
+        return
+
+    if comment is None:
+        pass
+    elif os.path.exists(comment):
+        with open(comment, 'rb') as fh:
+            comment = fh.read()
+    else:
+        try:
+            comment = comment.encode('ascii')
+        except UnicodeEncodeError as exc:
+            print(f'{file}: {exc}')
+            comment = comment.encode()
+
+    for file in files:
+        try:
+            result = tiffcomment(file, comment)
+        except Exception as exc:
+            print(f'{file}: {exc}')
+        else:
+            if result:
+                print(result)
+
+
+if __name__ == '__main__':
+    sys.exit(main())


=====================================
tifffile/tifffile.py
=====================================
@@ -71,7 +71,7 @@ For command line usage run ``python -m tifffile --help``
 
 :License: BSD 3-Clause
 
-:Version: 2020.11.18
+:Version: 2020.11.26
 
 Requirements
 ------------
@@ -91,10 +91,18 @@ This release has been tested with the following requirements and dependencies
 
 Revisions
 ---------
+2020.11.26
+    Pass 4372 tests.
+    Add option to pass axes metadata to ImageJ writer.
+    Pad incomplete tiles passed to TiffWriter.write (#38).
+    Split TiffTag constructor (breaking).
+    Change TiffTag.dtype to TIFF.DATATYPES (breaking).
+    Add TiffTag.overwrite method.
+    Add script to change ImageDescription in files.
+    Add TiffWriter.overwrite_description method (WIP).
 2020.11.18
-    Pass 4363 tests.
-    Support writing SEPARATED colorspace (#37).
-    Use imagecodecs.deflate if available.
+    Support writing SEPARATED color space (#37).
+    Use imagecodecs.deflate codec if available.
     Fix SCN and NDPI series with Z dimensions.
     Add TiffReader alias for TiffFile.
     TiffPage.is_volumetric returns True if ImageDepth > 1.
@@ -103,7 +111,7 @@ Revisions
     Formally deprecate unused TiffFile parameters (scikit-image #4996).
 2020.9.30
     Allow to pass additional arguments to compression codecs.
-    Deprecate TiffWriter.save function (use TiffWriter.write).
+    Deprecate TiffWriter.save method (use TiffWriter.write).
     Deprecate TiffWriter.save compress parameter (use compression).
     Remove multifile parameter from TiffFile (breaking).
     Pass all is_flag arguments from imread to TiffFile.
@@ -131,7 +139,7 @@ Revisions
     Return full size tiles from Tiffpage.segments.
     Rename TiffPage.is_sgi property to is_volumetric (breaking).
     Rename TiffPageSeries.is_pyramid to is_pyramidal (breaking).
-    Fix TypeError when passing jpegtables to non-JPEG decode function (#25).
+    Fix TypeError when passing jpegtables to non-JPEG decode method (#25).
 2020.9.3
     Do not write contiguous series by default (breaking).
     Allow to write to SubIFDs (WIP).
@@ -180,7 +188,7 @@ Revisions
 2020.5.25
     Make imagecodecs an optional dependency again.
     Disable multi-threaded decoding of small LZW compressed segments.
-    Fix caching of TiffPage.decode function.
+    Fix caching of TiffPage.decode method.
     Fix xml.etree.cElementTree ImportError on Python 3.9.
     Fix tostring DeprecationWarning.
 2020.5.11
@@ -192,7 +200,7 @@ Revisions
     Always store ExtraSamples values in tuple (breaking).
 2020.5.5
     Allow to write tiled TIFF from iterable of tiles (WIP).
-    Add function to iterate over decoded segments of TiffPage (WIP).
+    Add method to iterate over decoded segments of TiffPage (WIP).
     Pass chunks of segments to ThreadPoolExecutor.map to reduce memory usage.
     Fix reading invalid files with too many strips.
     Fix writing over-aligned image data.
@@ -202,7 +210,7 @@ Revisions
     Remove maxsize parameter from asarray (breaking).
     Deprecate ijmetadata parameter from TiffWriter.save (use metadata).
 2020.2.16
-    Add function to decode individual strips or tiles.
+    Add method to decode individual strips or tiles.
     Read strips and tiles in order of their offsets.
     Enable multi-threading when decompressing multiple strips.
     Replace TiffPage.tags dictionary with TiffTags (breaking).
@@ -217,7 +225,7 @@ Revisions
     Fix xml2dict.
     Require imagecodecs >= 2020.1.31.
     Remove support for imagecodecs-lite (breaking).
-    Remove verify parameter to asarray function (breaking).
+    Remove verify parameter to asarray method (breaking).
     Remove deprecated lzw_decode functions (breaking).
     Remove support for Python 2.7 and 3.5 (breaking).
 2019.7.26
@@ -343,16 +351,27 @@ References
 
 Examples
 --------
-Save a 3D numpy array to a multi-page, 16-bit grayscale TIFF file:
+Write a numpy array to a single-page RGB TIFF file:
 
->>> data = numpy.random.randint(0, 2**12, (4, 301, 219), 'uint16')
+>>> data = numpy.random.randint(0, 255, (256, 256, 3), 'uint8')
+>>> imwrite('temp.tif', data, photometric='rgb')
+
+Read the image from the TIFF file as numpy array:
+
+>>> image = imread('temp.tif')
+>>> image.shape
+(256, 256, 3)
+
+Write a 3D numpy array to a multi-page, 16-bit grayscale TIFF file:
+
+>>> data = numpy.random.randint(0, 2**12, (64, 301, 219), 'uint16')
 >>> imwrite('temp.tif', data, photometric='minisblack')
 
 Read the whole image stack from the TIFF file as numpy array:
 
 >>> image_stack = imread('temp.tif')
 >>> image_stack.shape
-(4, 301, 219)
+(64, 301, 219)
 >>> image_stack.dtype
 dtype('uint16')
 
@@ -362,67 +381,46 @@ Read the image from the first page in the TIFF file as numpy array:
 >>> image.shape
 (301, 219)
 
-Read images from a sequence of TIFF files as numpy array:
-
->>> image_sequence = imread(['temp.tif', 'temp.tif'])
->>> image_sequence.shape
-(2, 4, 301, 219)
-
-Save a numpy array to a single-page RGB TIFF file:
-
->>> data = numpy.random.randint(0, 255, (256, 256, 3), 'uint8')
->>> imwrite('temp.tif', data, photometric='rgb')
-
-Save a floating-point array and metadata, using zlib compression:
+Read images from a selected range of pages:
 
->>> data = numpy.random.rand(2, 5, 3, 301, 219).astype('float32')
->>> imwrite('temp.tif', data, compression='zlib', metadata={'axes': 'TZCYX'})
+>>> image = imread('temp.tif', key=range(4, 40, 2))
+>>> image.shape
+(18, 301, 219)
 
-Save a volume with xyz voxel size 2.6755x2.6755x3.9474 micron^3 to an ImageJ
-formatted TIFF file:
+Iterate over all pages in the TIFF file and successively read images:
 
->>> volume = numpy.random.randn(57*256*256).astype('float32')
->>> volume.shape = 1, 57, 1, 256, 256, 1  # dimensions in TZCYXS order
->>> imwrite('temp.tif', volume, imagej=True, resolution=(1./2.6755, 1./2.6755),
-...         metadata={'spacing': 3.947368, 'unit': 'um'})
+>>> with TiffFile('temp.tif') as tif:
+...     for page in tif.pages:
+...         image = page.asarray()
 
-Get the shape and dtype of the volume stored in the TIFF file:
+Get information about the image stack in the TIFF file without reading
+the image data:
 
 >>> tif = TiffFile('temp.tif')
 >>> len(tif.pages)  # number of pages in the file
-57
+64
 >>> page = tif.pages[0]  # get shape and dtype of the image in the first page
 >>> page.shape
-(256, 256)
+(301, 219)
 >>> page.dtype
-dtype('float32')
+dtype('uint16')
 >>> page.axes
 'YX'
 >>> series = tif.series[0]  # get shape and dtype of the first image series
 >>> series.shape
-(57, 256, 256)
+(64, 301, 219)
 >>> series.dtype
-dtype('float32')
+dtype('uint16')
 >>> series.axes
-'ZYX'
+'QYX'
 >>> tif.close()
 
-Read hyperstack and metadata from the ImageJ file:
-
->>> with TiffFile('temp.tif') as tif:
-...     imagej_hyperstack = tif.asarray()
-...     imagej_metadata = tif.imagej_metadata
->>> imagej_hyperstack.shape
-(57, 256, 256)
->>> imagej_metadata['slices']
-57
-
-Read the "XResolution" tag from the first page in the TIFF file:
+Inspect the "XResolution" tag from the first page in the TIFF file:
 
 >>> with TiffFile('temp.tif') as tif:
 ...     tag = tif.pages[0].tags['XResolution']
 >>> tag.value
-(2000, 5351)
+(1, 1)
 >>> tag.name
 'XResolution'
 >>> tag.code
@@ -430,28 +428,71 @@ Read the "XResolution" tag from the first page in the TIFF file:
 >>> tag.count
 1
 >>> tag.dtype
-'2I'
+<DATATYPES.RATIONAL: 5>
 
-Read images from a selected range of pages:
+Iterate over all tags in the TIFF file:
 
->>> image = imread('temp.tif', key=range(4, 40, 2))
->>> image.shape
-(18, 256, 256)
+>>> with TiffFile('temp.tif') as tif:
+...     for page in tif.pages:
+...         for tag in page.tags:
+...             tag_name, tag_value = tag.name, tag.value
+
+Write a floating-point ndarray and metadata using BigTIFF format, tiling,
+compression, and planar storage:
+
+>>> data = numpy.random.rand(2, 5, 3, 301, 219).astype('float32')
+>>> imwrite('temp.tif', data, bigtiff=True, photometric='minisblack',
+...         compression='deflate', planarconfig='separate', tile=(32, 32),
+...         metadata={'axes': 'TZCYX'})
+
+Write a volume with xyz voxel size 2.6755x2.6755x3.9474 micron^3 to an
+ImageJ hyperstack formatted TIFF file:
+
+>>> volume = numpy.random.randn(57, 256, 256).astype('float32')
+>>> imwrite('temp.tif', volume, imagej=True, resolution=(1./2.6755, 1./2.6755),
+...         metadata={'spacing': 3.947368, 'unit': 'um', 'axes': 'ZYX'})
+
+Read the volume and metadata from the ImageJ file:
+
+>>> with TiffFile('temp.tif') as tif:
+...     volume = tif.asarray()
+...     axes = tif.series[0].axes
+...     imagej_metadata = tif.imagej_metadata
+>>> volume.shape
+(57, 256, 256)
+>>> axes
+'ZYX'
+>>> imagej_metadata['slices']
+57
 
 Create an empty TIFF file and write to the memory-mapped numpy array:
 
->>> memmap_image = memmap('temp.tif', shape=(256, 256), dtype='float32')
->>> memmap_image[255, 255] = 1.0
+>>> memmap_image = memmap('temp.tif', shape=(3, 256, 256), dtype='float32')
+>>> memmap_image[1, 255, 255] = 1.0
 >>> memmap_image.flush()
 >>> del memmap_image
 
 Memory-map image data of the first page in the TIFF file:
 
 >>> memmap_image = memmap('temp.tif', page=0)
->>> memmap_image[255, 255]
+>>> memmap_image[1, 255, 255]
 1.0
 >>> del memmap_image
 
+Write two numpy arrays to a multi-series TIFF file:
+
+>>> series0 = numpy.random.randint(0, 255, (32, 32, 3), 'uint8')
+>>> series1 = numpy.random.randint(0, 1023, (4, 256, 256), 'uint16')
+>>> with TiffWriter('temp.tif') as tif:
+...     tif.write(series0, photometric='rgb')
+...     tif.write(series1, photometric='minisblack')
+
+Read the second image series from the TIFF file:
+
+>>> series1 = imread('temp.tif', series=1)
+>>> series1.shape
+(4, 256, 256)
+
 Successively write the frames of one contiguous series to a TIFF file:
 
 >>> data = numpy.random.randint(0, 255, (30, 301, 219), 'uint8')
@@ -459,58 +500,40 @@ Successively write the frames of one contiguous series to a TIFF file:
 ...     for frame in data:
 ...         tif.write(frame, contiguous=True)
 
-Successively append image series to a BigTIFF file, which can exceed 4 GB:
-
->>> data = numpy.random.randint(0, 255, (5, 2, 3, 301, 219), 'uint8')
->>> with TiffWriter('temp.tif', bigtiff=True) as tif:
-...     for i in range(data.shape[0]):
-...         tif.write(data[i], photometric='minisblack')
-
-Append an image to the existing TIFF file:
+Append an image series to the existing TIFF file:
 
 >>> data = numpy.random.randint(0, 255, (301, 219, 3), 'uint8')
 >>> imwrite('temp.tif', data, append=True)
 
-Iterate over pages and tags in the TIFF file and successively read images:
+Create a TIFF file from a generator of tiles:
 
->>> with TiffFile('temp.tif') as tif:
-...     for page in tif.pages:
-...         for tag in page.tags:
-...             tag_name, tag_value = tag.name, tag.value
-...         image = page.asarray()
+>>> data = numpy.random.randint(0, 2**12, (31, 33, 3), 'uint16')
+>>> def tiles(data, tileshape):
+...     for y in range(0, data.shape[0], tileshape[0]):
+...         for x in range(0, data.shape[1], tileshape[1]):
+...             yield data[y : y + tileshape[0], x : x + tileshape[1]]
+>>> imwrite('temp.tif', tiles(data, (16, 16)), tile=(16, 16),
+...         shape=data.shape, dtype=data.dtype)
 
 Write two numpy arrays to a multi-series OME-TIFF file:
 
->>> data0 = numpy.random.randint(0, 255, (32, 32, 3), 'uint8')
->>> data1 = numpy.random.randint(0, 1023, (4, 256, 256), 'uint16')
+>>> series0 = numpy.random.randint(0, 255, (32, 32, 3), 'uint8')
+>>> series1 = numpy.random.randint(0, 1023, (4, 256, 256), 'uint16')
 >>> with TiffWriter('temp.ome.tif') as tif:
-...     tif.write(data0, photometric='rgb')
-...     tif.write(data1, photometric='minisblack',
+...     tif.write(series0, photometric='rgb')
+...     tif.write(series1, photometric='minisblack',
 ...              metadata={'axes': 'ZYX', 'SignificantBits': 10,
 ...                        'Plane': {'PositionZ': [0.0, 1.0, 2.0, 3.0]}})
 
-Read the second image series from the OME-TIFF file:
-
->>> series1 = imread('temp.ome.tif', series=1)
->>> series1.shape
-(4, 256, 256)
-
-Create a TIFF file from a generator of tiles:
-
->>> def tiles():
-...     data = numpy.arange(3*4*16*16, dtype='uint16').reshape((3*4, 16, 16))
-...     for i in range(data.shape[0]): yield data[i]
->>> imwrite('temp.tif', tiles(), dtype='uint16', shape=(48, 64), tile=(16, 16))
-
-Write a tiled, multi-resolution, pyramidal OME-TIFF file using JPEG
-compression. Sub-resolution images are written to SubIFDs:
+Write a tiled, multi-resolution, pyramidal, OME-TIFF file using
+JPEG compression. Sub-resolution images are written to SubIFDs:
 
 >>> data = numpy.arange(1024*1024*3, dtype='uint8').reshape((1024, 1024, 3))
->>> with TiffWriter('temp.ome.tif') as tif:
+>>> with TiffWriter('temp.ome.tif', bigtiff=True) as tif:
 ...     options = dict(tile=(256, 256), compression='jpeg')
 ...     tif.write(data, subifds=2, **options)
 ...     # save pyramid levels to the two subifds
-...     # in production use resampling to generate sub-resolutions!
+...     # in production use resampling to generate sub-resolutions
 ...     tif.write(data[::2, ::2], subfiletype=1, **options)
 ...     tif.write(data[::4, ::4], subfiletype=1, **options)
 
@@ -547,10 +570,17 @@ Use zarr to access the tiled, pyramidal images in the TIFF file:
 <zarr.core.Array '/0' (1024, 1024, 3) uint8 read-only>
 >>> store.close()
 
-Read an image stack from a series of TIFF files with a file name pattern:
+Read images from a sequence of TIFF files as numpy array:
 
 >>> imwrite('temp_C001T001.tif', numpy.random.rand(64, 64))
 >>> imwrite('temp_C001T002.tif', numpy.random.rand(64, 64))
+>>> image_sequence = imread(['temp_C001T001.tif', 'temp_C001T002.tif'])
+>>> image_sequence.shape
+(2, 64, 64)
+
+Read an image stack from a series of TIFF files with a file name pattern
+as numpy or zarr arrays:
+
 >>> image_sequence = TiffSequence('temp_C001*.tif', pattern='axes')
 >>> image_sequence.shape
 (1, 2)
@@ -566,7 +596,7 @@ Read an image stack from a series of TIFF files with a file name pattern:
 
 """
 
-__version__ = '2020.11.18'
+__version__ = '2020.11.26'
 
 __all__ = (
     'imwrite',
@@ -768,19 +798,19 @@ def imwrite(file, data=None, shape=None, dtype=None, **kwargs):
     )
     if data is None:
         dtype = numpy.dtype(dtype)
-        size = product(shape) * dtype.itemsize
+        datasize = product(shape) * dtype.itemsize
         byteorder = dtype.byteorder
     else:
         try:
-            size = data.nbytes
+            datasize = data.nbytes
             byteorder = data.dtype.byteorder
         except Exception:
-            size = 0
+            datasize = 0
             byteorder = None
     bigsize = kwargs.pop('bigsize', 2 ** 32 - 2 ** 25)
     if (
         'bigtiff' not in tifargs
-        and size > bigsize
+        and datasize > bigsize
         and not tifargs.get('imagej', False)
         and not tifargs.get('truncate', False)
         and not kwargs.get('compression', False)
@@ -987,29 +1017,31 @@ class TiffWriter:
             except (OSError, FileNotFoundError):
                 append = False
 
-        if byteorder in (None, '=', '|'):
-            byteorder = '<' if sys.byteorder == 'little' else '>'
-        elif byteorder not in ('<', '>'):
-            raise ValueError(f'invalid byteorder {byteorder}')
         if imagej and bigtiff:
             warnings.warn(
                 'TiffWriter: writing nonconformant BigTIFF ImageJ', UserWarning
             )
 
-        self._byteorder = byteorder
+        if byteorder in (None, '=', '|'):
+            byteorder = '<' if sys.byteorder == 'little' else '>'
+        elif byteorder not in ('<', '>'):
+            raise ValueError(f'invalid byteorder {byteorder}')
+
+        if byteorder == '<':
+            self.tiff = TIFF.BIG_LE if bigtiff else TIFF.CLASSIC_LE
+        else:
+            self.tiff = TIFF.BIG_BE if bigtiff else TIFF.CLASSIC_BE
+
         self._truncate = False
         self._metadata = None
         self._colormap = None
-
-        self._descriptionoffset = 0
-        self._descriptionlen = 0
-        self._descriptionlenoffset = 0
         self._tags = None
         self._datashape = None  # shape of data in consecutive pages
         self._datadtype = None  # data type
         self._dataoffset = None  # offset to data
         self._databytecounts = None  # byte counts per plane
-        self._tagoffsets = None  # strip or tile offset tag code
+        self._dataoffsetstag = None  # strip or tile offset tag code
+        self._descriptiontag = None  # TiffTag for updating comment
         self._subifds = 0  # number of subifds
         self._subifdslevel = -1  # index of current subifd level
         self._subifdsoffsets = []  # offsets to offsets to subifds
@@ -1020,21 +1052,6 @@ class TiffWriter:
         # (pages, separate_samples, depth, length, width, contig_samples)
         self._storedshape = None
 
-        if bigtiff:
-            self._bigtiff = True
-            self._offsetsize = 8
-            self._tagsize = 20
-            self._tagnoformat = 'Q'
-            self._offsetformat = 'Q'
-            self._valueformat = '8s'
-        else:
-            self._bigtiff = False
-            self._offsetsize = 4
-            self._tagsize = 12
-            self._tagnoformat = 'H'
-            self._offsetformat = 'I'
-            self._valueformat = '4s'
-
         if append:
             self._fh = FileHandle(file, mode='r+b', size=0)
             self._fh.seek(0, 2)
@@ -1047,13 +1064,18 @@ class TiffWriter:
                 self._fh.write(struct.pack(byteorder + 'H', 42))
             # first IFD
             self._ifdoffset = self._fh.tell()
-            self._fh.write(struct.pack(byteorder + self._offsetformat, 0))
+            self._fh.write(struct.pack(self.tiff.offsetformat, 0))
 
         self._ome = None if ome is None else bool(ome)
         self._imagej = False if self._ome else bool(imagej)
         if self._imagej:
             self._ome = False
 
+    @property
+    def filehandle(self):
+        """Return file handle."""
+        return self._fh
+
     def write(
         self,
         data=None,
@@ -1118,9 +1140,10 @@ class TiffWriter:
             Input image or iterable of tiles or images.
             A copy of the image data is made if 'data' is not a C-contiguous
             numpy array with the same byteorder as the TIFF file.
+            Iterables must yield C-contiguous numpy array of TIFF byteorder.
             Iterable tiles must match 'dtype' and the shape specified in
-            'tile'. Iterable images must match 'dtype' and 'shape[1:]'.
-            Iterables must contain C-contiguous numpy array of TIFF byteorder.
+            'tile'. Incomplete tiles are zero-padded.
+            Iterable images must match 'dtype' and 'shape[1:]'.
         shape : tuple or None
             Shape of the empty or iterable data to save.
             Use only if 'data' is None or an iterable of tiles or images.
@@ -1263,9 +1286,8 @@ class TiffWriter:
 
             code : int
                 The TIFF tag Id.
-            dtype : str
-                Data type of items in 'value' in Python struct format.
-                One of B, s, H, I, 2I, b, h, i, 2i, f, d, Q, or q.
+            dtype : int or str
+                Data type of items in 'value'. One of TIFF.DATATYPES.
             count : int
                 Number of data values. Not used for string or bytes values.
             value : sequence
@@ -1288,7 +1310,7 @@ class TiffWriter:
         """
         # TODO: refactor this function
         fh = self._fh
-        byteorder = self._byteorder
+        byteorder = self.tiff.byteorder
 
         if data is None:
             # empty
@@ -1404,10 +1426,8 @@ class TiffWriter:
                             self._storedshape,
                             **self._metadata,
                         )
-                else:
+                elif metadata is not None:
                     self._write_image_description()
-                    self._descriptionoffset = 0
-                    self._descriptionlenoffset = 0
 
                 if self._subifds:
                     if self._truncate or truncate:
@@ -1449,7 +1469,7 @@ class TiffWriter:
                 return None
 
         if self._ome is None:
-            if not description:
+            if description is None:
                 self._ome = '.ome.tif' in fh.name
             else:
                 self._ome = False
@@ -1465,11 +1485,11 @@ class TiffWriter:
             # TODO: reconsider this
             # raise ValueError('cannot save zero size array')
 
-        tagnoformat = self._tagnoformat
-        valueformat = self._valueformat
-        offsetformat = self._offsetformat
-        offsetsize = self._offsetsize
-        tagsize = self._tagsize
+        valueformat = f'{self.tiff.offsetsize}s'
+        tagnoformat = self.tiff.tagnoformat
+        offsetformat = self.tiff.offsetformat
+        offsetsize = self.tiff.offsetsize
+        tagsize = self.tiff.tagsize
 
         MINISBLACK = TIFF.PHOTOMETRIC.MINISBLACK
         MINISWHITE = TIFF.PHOTOMETRIC.MINISWHITE
@@ -1534,7 +1554,7 @@ class TiffWriter:
                 raise ValueError(f'cannot apply predictor to {datadtype}')
 
         if self._ome:
-            if description:
+            if description is not None:
                 warnings.warn(
                     'TiffWriter: not writing description to OME-TIFF',
                     UserWarning,
@@ -1551,7 +1571,7 @@ class TiffWriter:
             #     warnings.warn(
             #         'ImageJ does not support tiles, predictors, compression'
             #     )
-            if description:
+            if description is not None:
                 warnings.warn(
                     'TiffWriter: not writing description to ImageJ file',
                     UserWarning,
@@ -1567,7 +1587,9 @@ class TiffWriter:
             ijrgb = photometric == RGB if photometric else None
             if datadtypechar not in 'B':
                 ijrgb = False
-            ijshape = imagej_shape(datashape, ijrgb)
+            ijshape = imagej_shape(
+                datashape, ijrgb, metadata.get('axes', None)
+            )
             if ijshape[-1] in (3, 4):
                 photometric = RGB
                 if datadtypechar not in 'B':
@@ -1796,10 +1818,12 @@ class TiffWriter:
         else:
             tagbytecounts = 279  # StripByteCounts
             tagoffsets = 273  # StripOffsets
-        self._tagoffsets = tagoffsets
+        self._dataoffsetstag = tagoffsets
 
         def pack(fmt, *val):
-            return struct.pack(byteorder + fmt, *val)
+            if fmt[0] not in '<>':
+                fmt = byteorder + fmt
+            return struct.pack(fmt, *val)
 
         def addtag(code, dtype, count, value, writeonce=False):
             # compute ifdentry & ifdvalue bytes from code, dtype, count, value
@@ -1807,50 +1831,76 @@ class TiffWriter:
             if not isinstance(code, int):
                 code = TIFF.TAGS[code]
             try:
-                tifftype = TIFF.DATA_DTYPES[dtype]
+                datatype = dtype
+                dataformat = TIFF.DATA_FORMATS[datatype][-1]
             except KeyError as exc:
                 try:
-                    tifftype = dtype
-                    dtype = TIFF.DATA_FORMATS[tifftype]
-                except KeyError:
+                    dataformat = dtype
+                    if dataformat[0] in '<>':
+                        dataformat = dataformat[1:]
+                    datatype = TIFF.DATA_DTYPES[dataformat]
+                except (KeyError, TypeError):
                     raise ValueError(f'unknown dtype {dtype}') from exc
-            rawcount = count
+            del dtype
 
-            if dtype == 's':
-                # strings; enforce 7-bit ASCII on Unicode strings
-                value = bytestr(value, 'ascii') + b'\0'
-                count = rawcount = len(value)
-                rawcount = value.find(b'\0\0')
-                if rawcount < 0:
-                    rawcount = count
+            rawcount = count
+            if datatype == 2:
+                # string
+                if isinstance(value, str):
+                    # enforce 7-bit ASCII on Unicode strings
+                    try:
+                        value = value.encode('ascii')
+                    except UnicodeEncodeError as exc:
+                        raise ValueError(
+                            'TIFF strings must be 7-bit ASCII'
+                        ) from exc
+                elif not isinstance(value, bytes):
+                    raise ValueError('TIFF strings must be 7-bit ASCII')
+                if len(value) == 0 or value[-1] != b'\x00':
+                    value += b'\x00'
+                count = len(value)
+                if code == 270:
+                    self._descriptiontag = TiffTag(270, 2, count, None, 0, 0)
+                    rawcount = value.rfind(b'\x00\x00')
+                    if rawcount < 0:
+                        rawcount = count
+                    else:
+                        # length of string without buffer
+                        rawcount = max(offsetsize + 1, rawcount + 1)
+                        rawcount = min(count, rawcount)
                 else:
-                    rawcount += 1  # length of string without buffer
+                    rawcount = count
                 value = (value,)
+
             elif isinstance(value, bytes):
                 # packed binary data
-                dtsize = struct.calcsize(dtype)
-                if len(value) % dtsize:
+                itemsize = struct.calcsize(dataformat)
+                if len(value) % itemsize:
                     raise ValueError('invalid packed binary data')
-                count = len(value) // dtsize
-            if len(dtype) > 1:
-                count *= int(dtype[:-1])
-                dtype = dtype[-1]
+                count = len(value) // itemsize
+                rawcount = count
+
+            if datatype in (5, 10):  # rational
+                count *= 2
+                dataformat = dataformat[-1]
+
             ifdentry = [
-                pack('HH', code, tifftype),
+                pack('HH', code, datatype),
                 pack(offsetformat, rawcount),
             ]
+
             ifdvalue = None
-            if struct.calcsize(dtype) * count <= offsetsize:
+            if struct.calcsize(dataformat) * count <= offsetsize:
                 # value(s) can be written directly
                 if isinstance(value, bytes):
                     ifdentry.append(pack(valueformat, value))
                 elif count == 1:
                     if isinstance(value, (tuple, list, numpy.ndarray)):
                         value = value[0]
-                    ifdentry.append(pack(valueformat, pack(dtype, value)))
+                    ifdentry.append(pack(valueformat, pack(dataformat, value)))
                 else:
                     ifdentry.append(
-                        pack(valueformat, pack(str(count) + dtype, *value))
+                        pack(valueformat, pack(f'{count}{dataformat}', *value))
                     )
             else:
                 # use offset to value(s)
@@ -1860,13 +1910,13 @@ class TiffWriter:
                 elif isinstance(value, numpy.ndarray):
                     if value.size != count:
                         raise RuntimeError('value.size != count')
-                    if value.dtype.char != dtype:
+                    if value.dtype.char != dataformat:
                         raise RuntimeError('value.dtype.char != dtype')
                     ifdvalue = value.tobytes()
                 elif isinstance(value, (tuple, list)):
-                    ifdvalue = pack(str(count) + dtype, *value)
+                    ifdvalue = pack(f'{count}{dataformat}', *value)
                 else:
-                    ifdvalue = pack(dtype, value)
+                    ifdvalue = pack(dataformat, value)
             tags.append((code, b''.join(ifdentry), ifdvalue, writeonce))
 
         def rational(arg, max_denominator=1000000):
@@ -1880,15 +1930,15 @@ class TiffWriter:
             f = f.limit_denominator(max_denominator)
             return f.numerator, f.denominator
 
-        if description:
+        if description is not None:
             # ImageDescription: user provided description
-            addtag(270, 's', 0, description, writeonce=True)
+            addtag(270, 2, 0, description, writeonce=True)
 
         # write shape and metadata to ImageDescription
         self._metadata = {} if not metadata else metadata.copy()
         if self._ome:
             if len(self._ome.images) == 0:
-                description = '\0' * 10  # rewritten at end of file
+                description = ''  # rewritten later at end of file
             else:
                 description = None
         elif self._imagej:
@@ -1932,20 +1982,20 @@ class TiffWriter:
         #     raise ValueError('cannot truncate without writing metadata')
         else:
             description = None
-        if description:
+        if description is not None:
             description = description.encode('ascii')
             if not self._ome:
                 # add 64 bytes buffer
-                # the description might be updated later with the final shape
-                description += b'\0' * 64
-            self._descriptionlen = len(description)
-            addtag(270, 's', 0, description, writeonce=True)
+                # the description might be updated later inplace with the
+                # final shape
+                description += b'\x00' * 64
+            addtag(270, 2, 0, description, writeonce=True)
         del description
 
         if software is None:
             software = 'tifffile.py'
         if software:
-            addtag(305, 's', 0, software, writeonce=True)
+            addtag(305, 2, 0, software, writeonce=True)
         if datetime:
             if isinstance(datetime, str):
                 if len(datetime) != 19 or datetime[16] != ':':
@@ -1955,24 +2005,24 @@ class TiffWriter:
                     datetime = datetime.strftime('%Y:%m:%d %H:%M:%S')
                 except AttributeError:
                     datetime = self._now().strftime('%Y:%m:%d %H:%M:%S')
-            addtag(306, 's', 0, datetime, writeonce=True)
-        addtag(259, 'H', 1, compressiontag)  # Compression
+            addtag(306, 2, 0, datetime, writeonce=True)
+        addtag(259, 3, 1, compressiontag)  # Compression
         if compressiontag == 34887:
             # LERC without additional compression
-            addtag(50674, 'I', 2, (4, 0))
+            addtag(50674, 4, 2, (4, 0))
         if predictor:
-            addtag(317, 'H', 1, predictortag)
-        addtag(256, 'I', 1, storedshape[-2])  # ImageWidth
-        addtag(257, 'I', 1, storedshape[-3])  # ImageLength
+            addtag(317, 3, 1, predictortag)
+        addtag(256, 4, 1, storedshape[-2])  # ImageWidth
+        addtag(257, 4, 1, storedshape[-3])  # ImageLength
         if tile:
-            addtag(322, 'I', 1, tile[-1])  # TileWidth
-            addtag(323, 'I', 1, tile[-2])  # TileLength
+            addtag(322, 4, 1, tile[-1])  # TileWidth
+            addtag(323, 4, 1, tile[-2])  # TileLength
         if volumetric:
-            addtag(32997, 'I', 1, storedshape[-4])  # ImageDepth
+            addtag(32997, 4, 1, storedshape[-4])  # ImageDepth
             if tile:
-                addtag(32998, 'I', 1, tile[0])  # TileDepth
+                addtag(32998, 4, 1, tile[0])  # TileDepth
         if subfiletype:
-            addtag(254, 'I', 1, subfiletype)  # NewSubfileType
+            addtag(254, 4, 1, subfiletype)  # NewSubfileType
         if (subifds or self._subifds) and self._subifdslevel < 0:
             if self._subifds:
                 subifds = self._subifds
@@ -1982,35 +2032,33 @@ class TiffWriter:
                 except TypeError:
                     # allow TiffPage.subifds tuple
                     self._subifds = subifds = len(subifds)
-            addtag(330, 18 if self._bigtiff else 13, subifds, [0] * subifds)
+            addtag(330, 18 if offsetsize > 4 else 13, subifds, [0] * subifds)
         if not bilevel and not datadtype.kind == 'u':
             sampleformat = {'u': 1, 'i': 2, 'f': 3, 'c': 6}[datadtype.kind]
-            addtag(
-                339, 'H', samplesperpixel, (sampleformat,) * samplesperpixel
-            )
+            addtag(339, 3, samplesperpixel, (sampleformat,) * samplesperpixel)
         if colormap is not None:
-            addtag(320, 'H', colormap.size, colormap)
-        addtag(277, 'H', 1, samplesperpixel)
+            addtag(320, 3, colormap.size, colormap)
+        addtag(277, 3, 1, samplesperpixel)
         if bilevel:
             pass
         elif planarconfig and samplesperpixel > 1:
-            addtag(284, 'H', 1, planarconfig.value)  # PlanarConfiguration
+            addtag(284, 3, 1, planarconfig.value)  # PlanarConfiguration
             addtag(  # BitsPerSample
-                258, 'H', samplesperpixel, (bitspersample,) * samplesperpixel
+                258, 3, samplesperpixel, (bitspersample,) * samplesperpixel
             )
         else:
-            addtag(258, 'H', 1, bitspersample)
+            addtag(258, 3, 1, bitspersample)
         if extrasamples:
             if extrasamples_ is not None:
                 if extrasamples != len(extrasamples_):
                     raise ValueError('wrong number of extrasamples specified')
-                addtag(338, 'H', extrasamples, extrasamples_)
+                addtag(338, 3, extrasamples, extrasamples_)
             elif photometric == RGB and extrasamples == 1:
                 # Unassociated alpha channel
-                addtag(338, 'H', 1, 2)
+                addtag(338, 3, 1, 2)
             else:
                 # Unspecified alpha channel
-                addtag(338, 'H', extrasamples, (0,) * extrasamples)
+                addtag(338, 3, extrasamples, (0,) * extrasamples)
 
         if compressiontag == 7 and photometric == RGB and planarconfig == 1:
             # JPEG compression with subsampling. Store as YCbCr
@@ -2024,25 +2072,23 @@ class TiffWriter:
                 raise ValueError(f'tile shape not a multiple of {maxsampling}')
             if extrasamples > 1:
                 raise ValueError('JPEG subsampling requires RGB(A) images')
-            addtag(530, 'H', 2, subsampling)  # YCbCrSubSampling
-            addtag(262, 'H', 1, 6)  # PhotometricInterpretation YCBCR
+            addtag(530, 3, 2, subsampling)  # YCbCrSubSampling
+            addtag(262, 3, 1, 6)  # PhotometricInterpretation YCBCR
             # ReferenceBlackWhite is required for YCBCR
-            addtag(
-                532, '2I', 6, (0, 1, 255, 1, 128, 1, 255, 1, 128, 1, 255, 1)
-            )
+            addtag(532, 5, 6, (0, 1, 255, 1, 128, 1, 255, 1, 128, 1, 255, 1))
         else:
             if subsampling not in (None, (1, 1)):
                 log_warning('TiffWriter: cannot apply subsampling')
             subsampling = None
             maxsampling = 1
             # PhotometricInterpretation
-            addtag(262, 'H', 1, photometric.value)
+            addtag(262, 3, 1, photometric.value)
             # if compresstag == 7:
-            #     addtag(530, 'H', 2, (1, 1))  # YCbCrSubSampling
+            #     addtag(530, 3, 2, (1, 1))  # YCbCrSubSampling
 
         if resolution is not None:
-            addtag(282, '2I', 1, rational(resolution[0]))  # XResolution
-            addtag(283, '2I', 1, rational(resolution[1]))  # YResolution
+            addtag(282, 5, 1, rational(resolution[0]))  # XResolution
+            addtag(283, 5, 1, rational(resolution[1]))  # YResolution
             if len(resolution) > 2:
                 unit = resolution[2]
                 unit = 1 if unit is None else enumarg(TIFF.RESUNIT, unit)
@@ -2050,11 +2096,11 @@ class TiffWriter:
                 unit = 1
             else:
                 unit = 2
-            addtag(296, 'H', 1, unit)  # ResolutionUnit
+            addtag(296, 3, 1, unit)  # ResolutionUnit
         elif not self._imagej:
-            addtag(282, '2I', 1, (1, 1))  # XResolution
-            addtag(283, '2I', 1, (1, 1))  # YResolution
-            addtag(296, 'H', 1, 1)  # ResolutionUnit
+            addtag(282, 5, 1, (1, 1))  # XResolution
+            addtag(283, 5, 1, (1, 1))  # YResolution
+            addtag(296, 3, 1, 1)  # ResolutionUnit
 
         def bytecount_format(
             bytecounts, compression=compression, size=offsetsize
@@ -2120,7 +2166,7 @@ class TiffWriter:
             bytecountformat = bytecount_format(databytecounts)
             addtag(tagbytecounts, bytecountformat, count, databytecounts)
             addtag(tagoffsets, offsetformat, count, [0] * count)
-            addtag(278, 'I', 1, storedshape[-3])  # RowsPerStrip
+            addtag(278, 4, 1, storedshape[-3])  # RowsPerStrip
             bytecountformat = bytecountformat * storedshape[1]
             if contiguous or dataiter is not None:
                 pass
@@ -2144,7 +2190,7 @@ class TiffWriter:
                 rowsperstrip = (
                     math.ceil(rowsperstrip / maxsampling) * maxsampling
                 )
-            addtag(278, 'I', 1, rowsperstrip)  # RowsPerStrip
+            addtag(278, 4, 1, rowsperstrip)  # RowsPerStrip
 
             numstrips1 = (storedshape[-3] + rowsperstrip - 1) // rowsperstrip
             numstrips = numstrips1 * storedshape[1] * storedshape[2]
@@ -2250,7 +2296,7 @@ class TiffWriter:
 
         fhpos = fh.tell()
         if (
-            not (self._bigtiff or self._imagej or compress)
+            not (offsetsize > 4 or self._imagej or compress)
             and fhpos + datasize > 2 ** 32 - 1
         ):
             raise ValueError('data too large for standard TIFF file')
@@ -2262,7 +2308,7 @@ class TiffWriter:
             ifdpos = fhpos
             if ifdpos % 2:
                 # location of IFD must begin on a word boundary
-                fh.write(b'\0')
+                fh.write(b'\x00')
                 ifdpos += 1
 
             if self._subifdslevel < 0:
@@ -2283,14 +2329,14 @@ class TiffWriter:
                 ifd.write(pack(offsetformat, 0))  # offset to next IFD
                 # write tag values and patch offsets in ifdentries
                 for tagindex, tag in enumerate(tags):
-                    offset = tagoffset + tagindex * tagsize + offsetsize + 4
+                    offset = tagoffset + tagindex * tagsize + 4 + offsetsize
                     code = tag[0]
                     value = tag[2]
                     if value:
                         pos = ifd.tell()
                         if pos % 2:
                             # tag value is expected to begin on word boundary
-                            ifd.write(b'\0')
+                            ifd.write(b'\x00')
                             pos += 1
                         ifd.seek(offset)
                         ifd.write(pack(offsetformat, ifdpos + pos))
@@ -2300,23 +2346,29 @@ class TiffWriter:
                             dataoffsetsoffset = offset, pos
                         elif code == tagbytecounts:
                             databytecountsoffset = offset, pos
-                        elif code == 270 and value.endswith(b'\0\0\0\0'):
-                            # image description buffer
-                            self._descriptionoffset = ifdpos + pos
-                            self._descriptionlenoffset = (
-                                ifdpos + tagoffset + tagindex * tagsize + 4
+                        elif code == 270:
+                            self._descriptiontag.offset = (
+                                ifdpos + tagoffset + tagindex * tagsize
                             )
+                            self._descriptiontag.valueoffset = ifdpos + pos
                         elif code == 330:
                             subifdsoffsets = offset, pos
                     elif code == tagoffsets:
                         dataoffsetsoffset = offset, None
                     elif code == tagbytecounts:
                         databytecountsoffset = offset, None
+                    elif code == 270:
+                        self._descriptiontag.offset = (
+                            ifdpos + tagoffset + tagindex * tagsize
+                        )
+                        self._descriptiontag.valueoffset = (
+                            self._descriptiontag.offset + offsetsize + 4
+                        )
                     elif code == 330:
                         subifdsoffsets = offset, None
                 ifdsize = ifd.tell()
                 if ifdsize % 2:
-                    ifd.write(b'\0')
+                    ifd.write(b'\x00')
                     ifdsize += 1
 
             # write IFD later when strip/tile bytecounts and offsets are known
@@ -2342,9 +2394,13 @@ class TiffWriter:
                 else:
                     fh.write_array(data)
             elif tile:
-                tilesize = product(tile) * storedshape[-1] * datadtype.itemsize
+                if storedshape[-1] == 1:
+                    tileshape = tile
+                else:
+                    tileshape = tile + (storedshape[-1],)
+                tilesize = product(tileshape) * datadtype.itemsize
                 if data is None:
-                    fh.write_empty(numtiles * databytecounts[0])
+                    fh.write_empty(numtiles * tilesize)
                 elif compress:
                     isbytes = True
                     for tileindex in range(storedshape[1] * product(tiles)):
@@ -2355,9 +2411,9 @@ class TiffWriter:
                         if isbytes and isinstance(chunk, bytes):
                             # pre-compressed
                             pass
-                        elif chunk.size * chunk.itemsize != tilesize:
-                            raise ValueError('invalid tile shape or dtype')
                         else:
+                            if chunk.nbytes != tilesize:
+                                chunk = pad_tile(chunk, tileshape, datadtype)
                             isbytes = False
                             chunk = compress(chunk)
                         fh.write(chunk)
@@ -2366,10 +2422,11 @@ class TiffWriter:
                     for tileindex in range(storedshape[1] * product(tiles)):
                         chunk = next(dataiter)
                         if chunk is None:
-                            fh.write_empty(databytecounts[0])
+                            # databytecounts[tileindex] = 0  # not contiguous
+                            fh.write_empty(tilesize)
                             continue
-                        if chunk.size * chunk.itemsize != tilesize:
-                            raise ValueError('invalid tile shape or dtype')
+                        if chunk.nbytes != tilesize:
+                            chunk = pad_tile(chunk, tileshape, datadtype)
                         fh.write_array(chunk)
             elif compress:
                 # write one strip per rowsperstrip
@@ -2439,7 +2496,7 @@ class TiffWriter:
                 # update SubIFDs tag values
                 fh.seek(
                     self._subifdsoffsets[self._ifdindex]
-                    + self._subifdslevel * self._offsetsize
+                    + self._subifdslevel * offsetsize
                 )
                 fh.write(pack(offsetformat, ifdpos))
 
@@ -2473,6 +2530,19 @@ class TiffWriter:
                 return dataoffset, sum(databytecounts)
         return None
 
+    def overwrite_description(self, description):
+        """Overwrite the value of the last ImageDescription tag.
+
+        Can be used to write OME-XML after writing the image data.
+        Ends a contiguous series.
+
+        """
+        if self._descriptiontag is None:
+            raise ValueError('no ImageDescription tag found')
+        self._write_remaining_pages()
+        self._descriptiontag.overwrite(self, description, erase=False)
+        self._descriptiontag = None
+
     def _write_remaining_pages(self):
         """Write outstanding IFDs and tags to file."""
         if not self._tags or self._truncate or self._datashape is None:
@@ -2488,14 +2558,14 @@ class TiffWriter:
         fh = self._fh
         fhpos = fh.tell()
         if fhpos % 2:
-            fh.write(b'\0')
+            fh.write(b'\x00')
             fhpos += 1
 
         pack = struct.pack
-        offsetformat = self._byteorder + self._offsetformat
-        offsetsize = self._offsetsize
-        tagnoformat = self._byteorder + self._tagnoformat
-        tagsize = self._tagsize
+        offsetformat = self.tiff.offsetformat
+        offsetsize = self.tiff.offsetsize
+        tagnoformat = self.tiff.tagnoformat
+        tagsize = self.tiff.tagsize
         dataoffset = self._dataoffset
         pagedatasize = sum(self._databytecounts)
         subifdsoffsets = None
@@ -2517,7 +2587,7 @@ class TiffWriter:
                 pos = ifd.tell()
                 if pos % 2:
                     # tag value is expected to begin on word boundary
-                    ifd.write(b'\0')
+                    ifd.write(b'\x00')
                     pos += 1
                 ifd.seek(offset)
                 try:
@@ -2532,24 +2602,24 @@ class TiffWriter:
                     raise ValueError('data too large for non-BigTIFF file')
                 ifd.seek(pos)
                 ifd.write(value)
-                if code == self._tagoffsets:
+                if code == self._dataoffsetstag:
                     # save strip/tile offsets for later updates
                     dataoffsetsoffset = offset, pos
                 elif code == 330:
                     # save subifds offsets for later updates
                     subifdsoffsets = offset, pos
-            elif code == self._tagoffsets:
+            elif code == self._dataoffsetstag:
                 dataoffsetsoffset = offset, None
             elif code == 330:
                 subifdsoffsets = offset, None
 
         ifdsize = ifd.tell()
         if ifdsize % 2:
-            ifd.write(b'\0')
+            ifd.write(b'\x00')
             ifdsize += 1
 
         # check if all IFDs fit in file
-        if not self._bigtiff and fhpos + ifdsize * pageno > 2 ** 32 - 32:
+        if offsetsize < 8 and fhpos + ifdsize * pageno > 2 ** 32 - 32:
             if self._imagej:
                 warnings.warn(
                     'TiffWriter: truncating ImageJ file', UserWarning
@@ -2599,7 +2669,7 @@ class TiffWriter:
                 # update SubIFDs tag values in file
                 fh.seek(
                     self._subifdsoffsets[self._ifdindex]
-                    + self._subifdslevel * self._offsetsize
+                    + self._subifdslevel * offsetsize
                 )
                 fh.write(pack(offsetformat, ifdpos))
 
@@ -2641,7 +2711,8 @@ class TiffWriter:
 
     def _write_image_description(self):
         """Write metadata to ImageDescription tag."""
-        if self._datashape is None or self._descriptionoffset <= 0:
+        if self._datashape is None or self._descriptiontag is None:
+            self._descriptiontag = None
             return
 
         if self._ome:
@@ -2655,8 +2726,9 @@ class TiffWriter:
             description = self._ome.tostring(declaration=True)
         elif self._datashape[0] == 1:
             # description already up-to-date
+            self._descriptiontag = None
             return
-        # elif self._subifdlevel >= 0:
+        # elif self._subifdslevel >= 0:
         #     # don't write metadata to SubIFDs
         #     return
         elif self._imagej:
@@ -2668,31 +2740,8 @@ class TiffWriter:
         else:
             description = json_description(self._datashape, **self._metadata)
 
-        # (re)write description, its position, and length to file
-        description = description.encode()
-        pos = self._fh.tell()
-        if self._ome:
-            self._descriptionoffset = pos
-            self._descriptionlen = len(description)
-            pos += self._descriptionlen
-        else:
-            description = description[: self._descriptionlen]
-
-        self._fh.seek(self._descriptionoffset)
-        self._fh.write(description)
-        self._fh.seek(self._descriptionlenoffset)
-        self._fh.write(
-            struct.pack(
-                self._byteorder + self._offsetformat + self._offsetformat,
-                self._descriptionlen,
-                self._descriptionoffset,
-            )
-        )
-        self._fh.seek(pos)
-
-        self._descriptionoffset = 0
-        self._descriptionlenoffset = 0
-        self._descriptionlen = 0
+        self._descriptiontag.overwrite(self, description, erase=False)
+        self._descriptiontag = None
 
     def _now(self):
         """Return current date and time."""
@@ -4524,7 +4573,7 @@ class TiffFile:
         result = {}
         result['NumberPlanes'] = page.tags[33629].count  # UIC2tag
         if page.description:
-            result['PlaneDescriptions'] = page.description.split('\0')
+            result['PlaneDescriptions'] = page.description.split('\x00')
             # result['plane_descriptions'] = stk_description_metadata(
             #    page.image_description)
         tag = page.tags.get(33628)  # UIC1tag
@@ -5266,8 +5315,7 @@ class TiffPage:
 
         data = fh.read(tagsize * tagno)
 
-        isndpi = tiff.version == 42 and tiff.offsetsize == 8
-        if isndpi:
+        if tiff.version == 42 and tiff.offsetsize == 8:
             # patch offsets/values for 64-bit NDPI file
             tagsize = 16
             fh.seek(8, 1)
@@ -5282,7 +5330,7 @@ class TiffPage:
             tagindex += tagsize
             tagdata = data[tagindex : tagindex + tagsize]
             try:
-                tag = TiffTag(parent, tagdata, tagoffset + tagindex, isndpi)
+                tag = TiffTag.fromfile(parent, tagoffset + tagindex, tagdata)
             except TiffFileError as exc:
                 log_warning(
                     f'TiffPage {self.index}: {exc.__class__.__name__}: {exc}'
@@ -6927,10 +6975,10 @@ class TiffFrame:
                 if codes and code not in codes:
                     continue
                 try:
-                    tag = TiffTag(
+                    tag = TiffTag.fromfile(
                         self.parent,
-                        tagbytes[tagindex : tagindex + tagsize],
                         tagoffset + tagindex,
+                        tagbytes[tagindex : tagindex + tagsize],
                     )
                 except TiffFileError as exc:
                     log_warning(
@@ -7058,70 +7106,97 @@ class TiffTag:
         Name of tag, TIFF.TAGS[code].
     code : int
         Decimal code of tag.
-    dtype : str
-        Datatype of tag data. One of TIFF DATA_FORMATS.
+    dtype : int
+        Datatype of tag data. One of TIFF.DATATYPES.
     count : int
         Number of values.
     value : various types
         Tag data as Python object.
-    offset : int
-        Location of tag structure in file.
     valueoffset : int
         Location of value in file.
+    offset : int
+        Location of tag structure in file.
 
     All attributes are read-only.
 
     """
 
-    __slots__ = ('code', 'count', 'dtype', 'value', 'offset', 'valueoffset')
+    __slots__ = ('offset', 'code', 'dtype', 'count', 'value', 'valueoffset')
+
+    def __init__(self, code, dtype, count, value, offset, valueoffset):
+        """ """
+        self.code = code
+        self.dtype = TIFF.DATATYPES(dtype)
+        self.count = count
+        self.value = value
+        self.offset = offset
+        self.valueoffset = valueoffset
 
-    def __init__(self, parent, tagheader, tagoffset, isndpi=False):
-        """Initialize instance from tag header."""
+    @classmethod
+    def fromfile(cls, parent, offset=None, header=None):
+        """Return TiffTag instance read from file."""
         fh = parent.filehandle
         tiff = parent.tiff
-        byteorder = tiff.byteorder
-        offsetsize = tiff.offsetsize
         unpack = struct.unpack
 
-        self.offset = tagoffset
-        self.valueoffset = tagoffset + offsetsize + 4
-        code, type_ = unpack(tiff.tagformat1, tagheader[:4])
-        count, value = unpack(tiff.tagformat2, tagheader[4:])
+        if header is None:
+            if offset is None:
+                offset = fh.tell()
+            else:
+                fh.seek(offset)
+            header = fh.read(tiff.tagsize)
+        elif offset is None:
+            offset = fh.tell()
+
+        valueoffset = offset + tiff.offsetsize + 4
+        code, dtype = unpack(tiff.tagformat1, header[:4])
+        count, value = unpack(tiff.tagformat2, header[4:])
 
         try:
-            dtype = TIFF.DATA_FORMATS[type_]
+            dataformat = TIFF.DATA_FORMATS[dtype]
         except KeyError:
-            raise TiffFileError(f'unknown tag data type {type_!r}')
+            raise TiffFileError(f'unknown tag data type {dtype!r}')
 
-        fmt = '{}{}{}'.format(byteorder, count * int(dtype[0]), dtype[1])
+        fmt = '{}{}{}'.format(
+            tiff.byteorder, count * int(dataformat[0]), dataformat[1]
+        )
         size = struct.calcsize(fmt)
         if size > tiff.tagoffsetthreshold or code in TIFF.TAG_READERS:
-            self.valueoffset = offset = unpack(tiff.offsetformat, value)[0]
-            if offset < 8 or offset > fh.size - size:
+            valueoffset = unpack(tiff.offsetformat, value)[0]
+            if valueoffset < 8 or valueoffset > fh.size - size:
                 raise TiffFileError('invalid tag value offset')
-            # if offset % 2:
+            # if valueoffset % 2:
             #     log_warning('TiffTag: value does not begin on word boundary')
-            fh.seek(offset)
+            fh.seek(valueoffset)
             if code in TIFF.TAG_READERS:
                 readfunc = TIFF.TAG_READERS[code]
-                value = readfunc(fh, byteorder, dtype, count, offsetsize)
-            elif type_ == 7 or (count > 1 and dtype[-1] == 'B'):
-                value = read_bytes(fh, byteorder, dtype, count, offsetsize)
-            # elif code in TIFF.TAGS or dtype[-1] == 's':
+                value = readfunc(
+                    fh, tiff.byteorder, dtype, count, tiff.offsetsize
+                )
+            elif dtype == 7 or (count > 1 and dtype == 1):
+                value = read_bytes(
+                    fh, tiff.byteorder, dtype, count, tiff.offsetsize
+                )
+            # elif code in TIFF.TAGS or dtype == 2:
             else:
                 value = unpack(fmt, fh.read(size))
             # else:
-            #     value = read_numpy(fh, byteorder, dtype, count, offsetsize)
-        elif dtype[-1] == 'B' or type_ == 7:
+            #     value = read_numpy(
+            #         fh, tiff.byteorder, dtype, count, tiff.offsetsize
+            #     )
+        elif dtype in (1, 7):
+            # BYTES, UNDEFINED
             value = value[:size]
         elif (
-            isndpi
+            tiff.version == 42
+            and tiff.offsetsize == 8
             and count == 1
-            and dtype == '1I'
+            and dtype in (4, 13)
             and value[4:] != b'\x00\x00\x00\x00'
         ):
-            # patch NDPI offsets and sizes
-            dtype = '1Q'
+            # NDPI LONG or IFD
+            # dtype = 16 if dtype == 4 else 18
+            # dataformat = '1Q'
             value = unpack('<Q', value)
         else:
             value = unpack(fmt, value[:size])
@@ -7129,9 +7204,9 @@ class TiffTag:
         process = (
             code not in TIFF.TAG_READERS
             and code not in TIFF.TAG_TUPLE
-            and type_ != 7
+            and dtype != 7  # UNDEFINED
         )
-        if process and dtype[-1] == 's' and isinstance(value[0], bytes):
+        if process and dtype == 2 and isinstance(value[0], bytes):
             # TIFF ASCII fields can contain multiple strings,
             #   each terminated with a NUL
             value = value[0]
@@ -7139,7 +7214,8 @@ class TiffTag:
                 value = bytes2str(stripnull(value, first=False).strip())
             except UnicodeDecodeError:
                 log_warning(f'TiffTag {code}: coercing invalid ASCII to bytes')
-                dtype = '1B'
+                # dtype = 1
+                # dataformat = '1B'
         else:
             if code in TIFF.TAG_ENUM:
                 t = TIFF.TAG_ENUM[code]
@@ -7150,17 +7226,176 @@ class TiffTag:
             if process:
                 if len(value) == 1:
                     value = value[0]
-
-        self.code = code
-        self.dtype = dtype
-        self.count = count
-        self.value = value
+        return cls(code, dtype, count, value, offset, valueoffset)
 
     @property
     def name(self):
         """Return name of tag from TIFF.TAGS registry."""
         return TIFF.TAGS.get(self.code, str(self.code))
 
+    @property
+    def dataformat(self):
+        """Return data type as Python struct format."""
+        return TIFF.DATA_FORMATS[self.dtype]
+
+    @property
+    def valuebytecount(self):
+        """Return size of value in file."""
+        return self.count * struct.calcsize(TIFF.DATA_FORMATS[self.dtype])
+
+    def overwrite(self, tifffile, value, dtype=None, erase=True):
+        """Write new tag value to file and return new TiffTag instance.
+
+        The value must be compatible with the struct.pack formats in
+        TIFF.DATA_FORMATS.
+
+        The new packed value is appended to the file if it is longer than the
+        old value. The old value is zeroed.
+
+        """
+        if self.offset < 8 or self.valueoffset < 8:
+            raise ValueError('cannot rewrite tag at offset < 8')
+
+        fh = tifffile.filehandle
+        tiff = tifffile.tiff
+
+        if tiff.version == 42 and tiff.offsetsize == 8:
+            # TODO: support patching NDPI files
+            raise NotImplementedError('cannot patch NDPI files')
+
+        if value is None:
+            value = b''
+        if dtype is None:
+            dtype = self.dtype
+
+        try:
+            dataformat = TIFF.DATA_FORMATS[dtype]
+        except KeyError as exc:
+            try:
+                dataformat = dtype[-1:]
+                if dataformat[0] in '<>':
+                    dataformat = dataformat[1:]
+                dtype = TIFF.DATA_DTYPES[dataformat]
+            except (KeyError, TypeError):
+                raise ValueError(f'unknown dtype {dtype}') from exc
+
+        if dtype == 2:
+            # strings
+            if isinstance(value, str):
+                # enforce 7-bit ASCII on Unicode strings
+                try:
+                    value = value.encode('ascii')
+                except UnicodeEncodeError as exc:
+                    raise ValueError(
+                        'TIFF strings must be 7-bit ASCII'
+                    ) from exc
+            elif not isinstance(value, bytes):
+                raise ValueError('TIFF strings must be 7-bit ASCII')
+            if len(value) == 0 or value[-1] != b'\x00':
+                value += b'\x00'
+            count = len(value)
+            value = (value,)
+        elif isinstance(value, bytes):
+            # pre-packed binary data
+            dtsize = struct.calcsize(dataformat)
+            if len(value) % dtsize:
+                raise ValueError('invalid packed binary data')
+            count = len(value) // dtsize
+            value = (value,)
+        else:
+            try:
+                count = len(value)
+            except TypeError:
+                value = (value,)
+                count = 1
+
+        if dtype in (5, 10):
+            if count < 2 or count % 2:
+                raise ValueError('invalid RATIONAL value')
+            count //= 2  # rational
+
+        packedvalue = struct.pack(
+            '{}{}{}'.format(
+                tiff.byteorder, count * int(dataformat[0]), dataformat[1]
+            ),
+            *value,
+        )
+        newsize = len(packedvalue)
+        oldsize = self.count * struct.calcsize(TIFF.DATA_FORMATS[self.dtype])
+        valueoffset = self.valueoffset
+
+        pos = fh.tell()
+        try:
+            if dtype != self.dtype:
+                # rewrite data type
+                fh.seek(self.offset + 2)
+                fh.write(struct.pack(tiff.byteorder + 'H', dtype))
+
+            if oldsize <= tiff.tagoffsetthreshold:
+                if newsize <= tiff.tagoffsetthreshold:
+                    # inline -> inline: overwrite
+                    fh.seek(self.offset + 4)
+                    fh.write(struct.pack(tiff.tagformat2, count, packedvalue))
+                else:
+                    # inline -> separate: append to file
+                    fh.seek(0, 2)
+                    valueoffset = fh.tell()
+                    if valueoffset % 2:
+                        # value offset must begin on a word boundary
+                        fh.write(b'\x00')
+                        valueoffset += 1
+                    fh.write(packedvalue)
+                    fh.seek(self.offset + 4)
+                    fh.write(
+                        struct.pack(
+                            tiff.tagformat2,
+                            count,
+                            struct.pack(tiff.offsetformat, valueoffset),
+                        )
+                    )
+            elif newsize <= tiff.tagoffsetthreshold:
+                # separate -> inline: erase old value
+                valueoffset = self.offset + 4 + tiff.offsetsize
+                fh.seek(self.offset + 4)
+                fh.write(struct.pack(tiff.tagformat2, count, packedvalue))
+                if erase:
+                    fh.seek(self.valueoffset)
+                    fh.write(b'\x00' * oldsize)
+            elif newsize <= oldsize or self.valueoffset + oldsize == fh.size:
+                # separate -> separate smaller: overwrite, erase remaining
+                fh.seek(self.offset + 4)
+                fh.write(struct.pack(tiff.offsetformat, count))
+                fh.seek(self.valueoffset)
+                fh.write(packedvalue)
+                if erase and oldsize - newsize > 0:
+                    fh.write(b'\x00' * (oldsize - newsize))
+            else:
+                # separate -> separate larger: erase old value, append to file
+                if erase:
+                    fh.seek(self.valueoffset)
+                    fh.write(b'\x00' * oldsize)
+                fh.seek(0, 2)
+                valueoffset = fh.tell()
+                if valueoffset % 2:
+                    # value offset must begin on a word boundary
+                    fh.write(b'\x00')
+                    valueoffset += 1
+                fh.write(packedvalue)
+                fh.seek(self.offset + 4)
+                fh.write(
+                    struct.pack(
+                        tiff.tagformat2,
+                        count,
+                        struct.pack(tiff.offsetformat, valueoffset),
+                    )
+                )
+        finally:
+            fh.seek(pos)  # must restore file position
+
+        return TiffTag(
+            self.code, dtype, count, value, self.offset, valueoffset
+        )
+
     def _fix_lsm_bitspersample(self, parent):
         """Correct LSM bitspersample tag.
 
@@ -7180,13 +7415,15 @@ class TiffTag:
     def __str__(self, detail=0, width=79):
         """Return string containing information about TiffTag."""
         height = 1 if detail <= 0 else 8 * detail
-        tcode = '{}{}'.format(self.count * int(self.dtype[0]), self.dtype[1])
+        dtype = self.dtype.name
+        if self.count > 1:
+            dtype += f'[{self.count}]'
         name = '|'.join(TIFF.TAGS.getall(self.code, ()))
         if name:
             name = f'{self.code} {name} @{self.offset}'
         else:
             name = f'{self.code} @{self.offset}'
-        line = f'TiffTag {name} {tcode} @{self.valueoffset}  '
+        line = f'TiffTag {name} {dtype} @{self.valueoffset} '
         line = line[:width]
         try:
             if self.count == 1:
@@ -8426,19 +8663,19 @@ class FileHandle:
         dtype = numpy.dtype(dtype)
 
         if count < 0:
-            size = self._size if out is None else out.nbytes
-            count = size // dtype.itemsize
+            nbytes = self._size if out is None else out.nbytes
+            count = nbytes // dtype.itemsize
         else:
-            size = count * dtype.itemsize
+            nbytes = count * dtype.itemsize
 
         result = numpy.empty(count, dtype) if out is None else out
 
-        if result.nbytes != size:
+        if result.nbytes != nbytes:
             raise ValueError('size mismatch')
 
         n = self._fh.readinto(result)
-        if n != size:
-            raise ValueError(f'failed to read {size} bytes')
+        if n != nbytes:
+            raise ValueError(f'failed to read {nbytes} bytes')
 
         if not result.dtype.isnative:
             if not dtype.isnative:
@@ -10525,55 +10762,52 @@ class TIFF:
 
     def DATATYPES():
         class DATATYPES(enum.IntEnum):
-            NOTYPE = 0
-            BYTE = 1
-            ASCII = 2
-            SHORT = 3
-            LONG = 4
-            RATIONAL = 5
-            SBYTE = 6
-            UNDEFINED = 7
-            SSHORT = 8
-            SLONG = 9
-            SRATIONAL = 10
-            FLOAT = 11
-            DOUBLE = 12
-            IFD = 13
+            BYTE = 1  # 8-bit unsigned integer
+            ASCII = 2  # 8-bit byte that contains a 7-bit ASCII code;
+            #           the last byte must be NULL (binary zero)
+            SHORT = 3  # 16-bit (2-byte) unsigned integer
+            LONG = 4  # 32-bit (4-byte) unsigned integer
+            RATIONAL = 5  # two LONGs: the first represents the numerator
+            #               of a fraction; the second, the denominator
+            SBYTE = 6  # an 8-bit signed (twos-complement) integer
+            UNDEFINED = 7  # an 8-bit byte that may contain anything,
+            #                depending on the definition of the field
+            SSHORT = 8  # A 16-bit (2-byte) signed (twos-complement) integer
+            SLONG = 9  # a 32-bit (4-byte) signed (twos-complement) integer
+            SRATIONAL = 10  # two SLONGs: the first represents the numerator
+            #                 of a fraction, the second the denominator
+            FLOAT = 11  # single precision (4-byte) IEEE format
+            DOUBLE = 12  # double precision (8-byte) IEEE format
+            IFD = 13  # unsigned 4 byte IFD offset
             UNICODE = 14
             COMPLEX = 15
-            LONG8 = 16
-            SLONG8 = 17
-            IFD8 = 18
+            LONG8 = 16  # unsigned 8 byte integer (BigTiff)
+            SLONG8 = 17  # signed 8 byte integer (BigTiff)
+            IFD8 = 18  # unsigned 8 byte IFD offset (BigTiff)
 
         return DATATYPES
 
     def DATA_FORMATS():
         # map TIFF DATATYPES to Python struct formats
         return {
-            1: '1B',  # BYTE 8-bit unsigned integer.
-            2: '1s',  # ASCII 8-bit byte that contains a 7-bit ASCII code;
-            #           the last byte must be NULL (binary zero).
-            3: '1H',  # SHORT 16-bit (2-byte) unsigned integer
-            4: '1I',  # LONG 32-bit (4-byte) unsigned integer.
-            5: '2I',  # RATIONAL Two LONGs: the first represents the numerator
-            #           of a fraction; the second, the denominator.
-            6: '1b',  # SBYTE An 8-bit signed (twos-complement) integer.
-            7: '1B',  # UNDEFINED An 8-bit byte that may contain anything,
-            #           depending on the definition of the field.
-            8: '1h',  # SSHORT A 16-bit (2-byte) signed (twos-complement)
-            #           integer.
-            9: '1i',  # SLONG A 32-bit (4-byte) signed (twos-complement)
-            #           integer.
-            10: '2i',  # SRATIONAL Two SLONGs: the first represents the
-            #            numerator of a fraction, the second the denominator.
-            11: '1f',  # FLOAT Single precision (4-byte) IEEE format.
-            12: '1d',  # DOUBLE Double precision (8-byte) IEEE format.
-            13: '1I',  # IFD unsigned 4 byte IFD offset.
-            # 14: '',  # UNICODE
-            # 15: '',  # COMPLEX
-            16: '1Q',  # LONG8 unsigned 8 byte integer (BigTiff)
-            17: '1q',  # SLONG8 signed 8 byte integer (BigTiff)
-            18: '1Q',  # IFD8 unsigned 8 byte IFD offset (BigTiff)
+            1: '1B',
+            2: '1s',
+            3: '1H',
+            4: '1I',
+            5: '2I',
+            6: '1b',
+            7: '1B',
+            8: '1h',
+            9: '1i',
+            10: '2i',
+            11: '1f',
+            12: '1d',
+            13: '1I',
+            # 14: '',
+            # 15: '',
+            16: '1Q',
+            17: '1q',
+            18: '1Q',
         }
 
     def DATA_DTYPES():
@@ -11965,18 +12199,20 @@ def read_tags(
         pos = fh.tell()
         index = 0
         for _ in range(tagno):
-            code, type_ = unpack(tagformat1, data[index : index + 4])
+            code, dtype = unpack(tagformat1, data[index : index + 4])
             count, value = unpack(
                 tagformat2, data[index + 4 : index + tagsize]
             )
             index += tagsize
             name = tagnames.get(code, str(code))
             try:
-                dtype = TIFF.DATA_FORMATS[type_]
+                dataformat = TIFF.DATA_FORMATS[dtype]
             except KeyError:
-                raise TiffFileError(f'unknown tag data type {type_}')
+                raise TiffFileError(f'unknown tag data type {dtype}')
 
-            fmt = '{}{}{}'.format(byteorder, count * int(dtype[0]), dtype[1])
+            fmt = '{}{}{}'.format(
+                byteorder, count * int(dataformat[0]), dataformat[1]
+            )
             size = struct.calcsize(fmt)
             if size > offsetsize or code in customtags:
                 offset = unpack(offsetformat, value)[0]
@@ -11986,13 +12222,14 @@ def read_tags(
                 if code in customtags:
                     readfunc = customtags[code][1]
                     value = readfunc(fh, byteorder, dtype, count, offsetsize)
-                elif type_ == 7 or (count > 1 and dtype[-1] == 'B'):
+                elif dtype == 7 or (count > 1 and dtype == 1):
                     value = read_bytes(fh, byteorder, dtype, count, offsetsize)
-                elif code in tagnames or dtype[-1] == 's':
+                elif code in tagnames or dtype == 2:
                     value = unpack(fmt, fh.read(size))
                 else:
                     value = read_numpy(fh, byteorder, dtype, count, offsetsize)
-            elif dtype[-1] == 'B' or type_ == 7:
+            elif dtype in (1, 7):
+                # BYTES, UNDEFINED
                 value = value[:size]
             else:
                 value = unpack(fmt, value[:size])
@@ -12000,7 +12237,7 @@ def read_tags(
             if code not in customtags and code not in TIFF.TAG_TUPLE:
                 if len(value) == 1:
                     value = value[0]
-            if type_ != 7 and dtype[-1] == 's' and isinstance(value, bytes):
+            if dtype == 2 and isinstance(value, bytes):
                 # TIFF ASCII fields can contain multiple strings,
                 #   each terminated with a NUL
                 try:
@@ -12061,7 +12298,7 @@ def read_interoperability_ifd(fh, byteorder, dtype, count, offsetsize):
 
 def read_bytes(fh, byteorder, dtype, count, offsetsize):
     """Read tag data from file and return as bytes."""
-    dtype = 'B' if dtype[-1] == 's' else byteorder + dtype[-1]
+    dtype = 'B' if dtype == 2 else byteorder + TIFF.DATA_FORMATS[dtype][-1]
     count *= numpy.dtype(dtype).itemsize
     data = fh.read(count)
     if len(data) != count:
@@ -12078,13 +12315,13 @@ def read_utf8(fh, byteorder, dtype, count, offsetsize):
 
 def read_numpy(fh, byteorder, dtype, count, offsetsize):
     """Read tag data from file and return as numpy array."""
-    dtype = 'b' if dtype[-1] == 's' else byteorder + dtype[-1]
+    dtype = 'b' if dtype == 2 else byteorder + TIFF.DATA_FORMATS[dtype][-1]
     return fh.read_array(dtype, count)
 
 
 def read_colormap(fh, byteorder, dtype, count, offsetsize):
     """Read ColorMap data from file and return as numpy array."""
-    cmap = fh.read_array(byteorder + dtype[-1], count)
+    cmap = fh.read_array(byteorder + TIFF.DATA_FORMATS[dtype][-1], count)
     cmap.shape = (3, -1)
     return cmap
 
@@ -12128,10 +12365,10 @@ def read_uic1tag(fh, byteorder, dtype, count, offsetsize, planecount=None):
     Return empty dictionary if planecount is unknown.
 
     """
-    if dtype not in ('2I', '1I') or byteorder != '<':
-        raise ValueError('invalid UIC1Tag')
+    if dtype not in (4, 5) or byteorder != '<':
+        raise ValueError(f'invalid UIC1Tag {byteorder}{dtype}')
     result = {}
-    if dtype == '2I':
+    if dtype == 5:
         # pre MetaMorph 2.5 (not tested)
         values = fh.read_array('<u4', 2 * count).reshape(count, 2)
         result = {'ZDistance': values[:, 0] / values[:, 1]}
@@ -12149,7 +12386,7 @@ def read_uic1tag(fh, byteorder, dtype, count, offsetsize, planecount=None):
 
 def read_uic2tag(fh, byteorder, dtype, planecount, offsetsize):
     """Read MetaMorph STK UIC2Tag from file and return as dict."""
-    if dtype != '2I' or byteorder != '<':
+    if dtype != 5 or byteorder != '<':
         raise ValueError('invalid UIC2Tag')
     values = fh.read_array('<u4', 6 * planecount).reshape(planecount, 6)
     return {
@@ -12163,7 +12400,7 @@ def read_uic2tag(fh, byteorder, dtype, planecount, offsetsize):
 
 def read_uic3tag(fh, byteorder, dtype, planecount, offsetsize):
     """Read MetaMorph STK UIC3Tag from file and return as dict."""
-    if dtype != '2I' or byteorder != '<':
+    if dtype != 5 or byteorder != '<':
         raise ValueError('invalid UIC3Tag')
     values = fh.read_array('<u4', 2 * planecount).reshape(planecount, 2)
     return {'Wavelengths': values[:, 0] / values[:, 1]}
@@ -12171,7 +12408,7 @@ def read_uic3tag(fh, byteorder, dtype, planecount, offsetsize):
 
 def read_uic4tag(fh, byteorder, dtype, planecount, offsetsize):
     """Read MetaMorph STK UIC4Tag from file and return as dict."""
-    if dtype != '1I' or byteorder != '<':
+    if dtype != 4 or byteorder != '<':
         raise ValueError('invalid UIC4Tag')
     result = {}
     while True:
@@ -12582,7 +12819,7 @@ def read_tvips_header(fh, byteorder, dtype, count, offsetsize):
         for name, typestr in TIFF.TVIPS_HEADER_V2:
             if typestr.startswith('V'):
                 s = header[name].tobytes().decode('utf-16', errors='ignore')
-                result[name] = stripnull(s, null='\0')
+                result[name] = stripnull(s, null='\x00')
             else:
                 result[name] = header[name].tolist()
         # convert nm to m
@@ -12862,8 +13099,8 @@ def imagej_metadata_tag(metadata, byteorder):
     bytecounts[0] = len(header)
     bytecounts = struct.pack(byteorder + ('I' * len(bytecounts)), *bytecounts)
     return (
-        (50839, 'B', len(data), data, True),
-        (50838, 'I', len(bytecounts) // 4, bytecounts, True),
+        (50839, 1, len(data), data, True),
+        (50838, 4, len(bytecounts) // 4, bytecounts, True),
     )
 
 
@@ -12996,7 +13233,8 @@ def imagej_description(
         raise NotImplementedError('ImageJ colormapping not supported')
     if version is None:
         version = kwargs.pop('ImageJ', '1.11a')
-    shape = imagej_shape(shape, rgb=rgb)
+    axes = kwargs.pop('axes', None)
+    shape = imagej_shape(shape, rgb=rgb, axes=axes)
     rgb = shape[-1] in (3, 4)
 
     append = []
@@ -13029,10 +13267,10 @@ def imagej_description(
     return '\n'.join(result + append + [''])
 
 
-def imagej_shape(shape, rgb=None):
+def imagej_shape(shape, rgb=None, axes=None):
     """Return shape normalized to 6D ImageJ hyperstack TZCYXS.
 
-    Raise ValueError if not a valid ImageJ hyperstack shape.
+    Raise ValueError if not a valid ImageJ hyperstack shape or axes order.
 
     >>> imagej_shape((2, 3, 4, 5, 3), False)
     (2, 3, 4, 5, 3, 1)
@@ -13041,13 +13279,41 @@ def imagej_shape(shape, rgb=None):
     shape = tuple(int(i) for i in shape)
     ndim = len(shape)
     if 1 > ndim > 6:
-        raise ValueError('invalid ImageJ hyperstack: not 2 to 6 dimensional')
+        raise ValueError('ImageJ hyperstack must be 2-6 dimensional')
+
+    if axes:
+        if len(axes) != ndim:
+            raise ValueError('ImageJ hyperstack shape and axes do not match')
+        i = 0
+        axes = axes.upper()
+        for ax in axes:
+            j = 'TZCYXS'.find(ax)
+            if j < i:
+                raise ValueError(
+                    'ImageJ hyperstack axes must be in TZCYXS order'
+                )
+            i = j
+        ndims = len(axes)
+        newshape = []
+        i = 0
+        for ax in 'TZCYXS':
+            if i < ndims and ax == axes[i]:
+                newshape.append(shape[i])
+                i += 1
+            else:
+                newshape.append(1)
+        if newshape[-1] not in (1, 3, 4):
+            raise ValueError(
+                'ImageJ hyperstack must contain 1, 3, or 4 samples'
+            )
+        return tuple(newshape)
+
     if rgb is None:
         rgb = shape[-1] in (3, 4) and ndim > 2
     if rgb and shape[-1] not in (3, 4):
-        raise ValueError('invalid ImageJ hyperstack: not a RGB image')
+        raise ValueError('ImageJ hyperstack is not a RGB image')
     if not rgb and ndim == 6 and shape[-1] != 1:
-        raise ValueError('invalid ImageJ hyperstack: not a non-RGB image')
+        raise ValueError('ImageJ hyperstack is not a grayscale image')
     if rgb or shape[-1] == 1:
         return (1,) * (6 - ndim) + shape
     return (1,) * (5 - ndim) + shape + (1,)
@@ -13823,6 +14089,14 @@ def iter_tiles(data, tile, tiles):
                                 yield chunk
 
 
+def pad_tile(tile, shape, dtype):
+    """Return tile padded to tile shape."""
+    if tile.dtype != dtype or tile.nbytes > product(shape) * dtype.itemsize:
+        raise ValueError('invalid tile shape or dtype')
+    pad = tuple((0, i - j) for i, j in zip(shape, tile.shape))
+    return numpy.pad(tile, pad)
+
+
 def reorient(image, orientation):
     """Return reoriented view of image array.
 
@@ -15053,6 +15327,22 @@ def validate_jhove(filename, jhove=None, ignore=None):
                 break
 
 
+def tiffcomment(arg, comment=None, index=None, code=None):
+    """Return or replace ImageDescription value in first page of TIFF file."""
+    if index is None:
+        index = 0
+    if code is None:
+        code = 270
+    mode = None if comment is None else 'r+b'
+    with TiffFile(arg, mode=mode) as tif:
+        tag = tif.pages[index].tags.get(code, None)
+        if tag is None:
+            raise ValueError(f'no {TIFF.TAGS[code]} tag found')
+        if comment is None:
+            return tag.value
+        tag.overwrite(tif, comment)
+
+
 def lsm2bin(lsmfile, binfile=None, tile=None, verbose=True):
     """Convert [MP]TZCYX LSM file to series of BIN files.
 



View it on GitLab: https://salsa.debian.org/python-team/packages/tifffile/-/commit/3bb27ff4ead7f945a7bddc32bd8dde4c761ffb23

-- 
View it on GitLab: https://salsa.debian.org/python-team/packages/tifffile/-/commit/3bb27ff4ead7f945a7bddc32bd8dde4c761ffb23
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-med-commit/attachments/20201130/ea898290/attachment-0001.html>


More information about the debian-med-commit mailing list