docs: def save(self, data=None, shape=None, dtype=None, returnoffset=False,
               photometric=None, planarconfig=None, extrasamples=None, tile=None,
               contiguous=True, align=16, truncate=False, compress=0,
               rowsperstrip=None, predictor=False, colormap=None,
               description=None, datetime=None, resolution=None, subfiletype=0,
               software='tifffile.py', metadata={}, ijmetadata=None,
               extratags=()):
          """Write numpy array and tags to TIFF file.
  
          The data shape's last dimensions are assumed to be image depth,
          height (length), width, and samples.
          If a colormap is provided, the data's dtype must be uint8 or uint16
          and the data values are indices into the last dimension of the
          colormap.
          If 'shape' and 'dtype' are specified, an empty array is saved.
          This option cannot be used with compression or multiple tiles.
          Image data are written uncompressed in one strip per plane by default.
          Dimensions larger than 2 to 4 (depending on photometric mode, planar
          configuration, and SGI mode) are flattened and saved as separate pages.
          The SampleFormat and BitsPerSample tags are derived from the data type.
  
          Parameters
          ----------
          data : numpy.ndarray or None
              Input image array.
          shape : tuple or None
              Shape of the empty array to save. Used only if 'data' is None.
          dtype : numpy.dtype or None
              Data-type of the empty array to save. Used only if 'data' is None.
          returnoffset : bool
              If True and the image data in the file is memory-mappable, return
              the offset and number of bytes of the image data in the file.
          photometric : {'MINISBLACK', 'MINISWHITE', 'RGB', 'PALETTE', 'CFA'}
              The color space of the image data.
              By default, this setting is inferred from the data shape and the
              value of colormap.
              For CFA images, DNG tags must be specified in 'extratags'.
          planarconfig : {'CONTIG', 'SEPARATE'}
              Specifies if samples are stored interleaved or in separate planes.
              By default, this setting is inferred from the data shape.
              If this parameter is set, extra samples are used to store grayscale
              images.
              'CONTIG': last dimension contains samples.
              'SEPARATE': third last dimension contains samples.
          extrasamples : tuple of {'UNSPECIFIED', 'ASSOCALPHA', 'UNASSALPHA'}
              Defines the interpretation of extra components in pixels.
              'UNSPECIFIED': no transparency information (default).
              'ASSOCALPHA': single, true transparency with pre-multiplied color.
              'UNASSALPHA': independent transparency masks.
          tile : tuple of int
              The shape (depth, length, width) of image tiles to write.
              If None (default), image data are written in strips.
              The tile length and width must be a multiple of 16.
              If the tile depth is provided, the SGI ImageDepth and TileDepth
              tags are used to save volume data.
              Unless a single tile is used, tiles cannot be used to write
              contiguous files.
              Few software can read the SGI format, e.g. MeVisLab.
          contiguous : bool
              If True (default) and the data and parameters are compatible with
              previous ones, if any, the image data are stored contiguously after
              the previous one. In that case, 'photometric', 'planarconfig',
              'rowsperstrip', are ignored. Metadata such as 'description',
              'metadata', 'datetime', and 'extratags' are written to the first
              page of a contiguous series only.
          align : int
              Byte boundary on which to align the image data in the file.
              Default 16. Use mmap.ALLOCATIONGRANULARITY for memory-mapped data.
              Following contiguous writes are not aligned.
          truncate : bool
              If True, only write the first page including shape metadata if
              possible (uncompressed, contiguous, not tiled).
              Other TIFF readers will only be able to read part of the data.
          compress : int or str or (str, int)
              If 0 (default), data are written uncompressed.
              If 0-9, the level of ADOBE_DEFLATE compression.
              If a str, one of TIFF.COMPRESSION, e.g. 'LZMA' or 'ZSTD'.
              If a tuple, first item is one of TIFF.COMPRESSION and second item
              is compression level.
              Compression cannot be used to write contiguous files.
          rowsperstrip : int
              The number of rows per strip. By default strips will be ~64 KB
              if compression is enabled, else rowsperstrip is set to the image
              length. Bilevel images are always stored in one strip per plane.
          predictor : bool
              If True, apply horizontal differencing or floating-point predictor
              before compression.
          colormap : numpy.ndarray
              RGB color values for the corresponding data value.
              Must be of shape (3, 2**(data.itemsize*8)) and dtype uint16.
          description : str
              The subject of the image. Must be 7-bit ASCII. Cannot be used with
              the ImageJ format. Saved with the first page only.
          datetime : datetime, str, or bool
              Date and time of image creation in '%Y:%m:%d %H:%M:%S' format or
              datetime object. Else if True, the current date and time is used.
              Saved with the first page only.
          resolution : (float, float[, str]) or ((int, int), (int, int)[, str])
              X and Y resolutions in pixels per resolution unit as float or
              rational numbers. A third, optional parameter specifies the
              resolution unit, which must be None (default for ImageJ),
              'INCH' (default), or 'CENTIMETER'.
          subfiletype : int
              Bitfield to indicate the kind of data. Set bit 0 if the image
              is a reduced-resolution version of another image. Set bit 1 if
              the image is part of a multi-page image. Set bit 2 if the image
              is transparency mask for another image (photometric must be
              MASK, SamplesPerPixel and BitsPerSample must be 1).
          software : str
              Name of the software used to create the file. Must be 7-bit ASCII.
              Saved with the first page only.
          metadata : dict
              Additional metadata to be saved along with shape information
              in JSON or ImageJ formats in an ImageDescription tag.
              If None, do not write a second ImageDescription tag.
              Strings must be 7-bit ASCII. Saved with the first page only.
          ijmetadata : dict
              Additional metadata to be saved in application specific
              IJMetadata and IJMetadataByteCounts tags. Refer to the
              imagej_metadata_tag function for valid keys and values.
              Saved with the first page only.
          extratags : sequence of tuples
              Additional tags as [(code, dtype, count, value, writeonce)].
  
              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.
              count : int
                  Number of data values. Not used for string or byte string
                  values.
              value : sequence
                  'Count' values compatible with 'dtype'.
                  Byte strings must contain count values of dtype packed as
                  binary data.
              writeonce : bool
                  If True, the tag is written to the first page only.
  
          """
          # TODO: refactor this function
          fh = self._fh
          byteorder = self._byteorder
  
          if data is None:
              if compress:
                  raise ValueError('cannot save compressed empty file')
              datashape = shape
              datadtype = numpy.dtype(dtype).newbyteorder(byteorder)
              datadtypechar = datadtype.char
          else:
              data = numpy.asarray(data, byteorder+data.dtype.char, 'C')
              if data.size == 0:
                  raise ValueError('cannot save empty array')
              datashape = data.shape
              datadtype = data.dtype
              datadtypechar = data.dtype.char
  
          returnoffset = returnoffset and datadtype.isnative
          bilevel = datadtypechar == '?'
          if bilevel:
              index = -1 if datashape[-1] > 1 else -2
              datasize = product(datashape[:index])
              if datashape[index] % 8:
                  datasize *= datashape[index] // 8 + 1
              else:
                  datasize *= datashape[index] // 8
          else:
              datasize = product(datashape) * datadtype.itemsize
  
          # just append contiguous data if possible
          self._truncate = bool(truncate)
          if self._datashape:
              if (not contiguous
                      or self._datashape[1:] != datashape
                      or self._datadtype != datadtype
                      or (compress and self._tags)
                      or tile
                      or not numpy.array_equal(colormap, self._colormap)):
                  # incompatible shape, dtype, compression mode, or colormap
                  self._write_remaining_pages()
                  self._write_image_description()
                  self._truncate = False
                  self._descriptionoffset = 0
                  self._descriptionlenoffset = 0
                  self._datashape = None
                  self._colormap = None
                  if self._imagej:
                      raise ValueError(
                          'ImageJ does not support non-contiguous data')
              else:
                  # consecutive mode
                  self._datashape = (self._datashape[0] + 1,) + datashape
                  if not compress:
                      # write contiguous data, write IFDs/tags later
                      offset = fh.tell()
                      if data is None:
                          fh.write_empty(datasize)
                      else:
                          fh.write_array(data)
                      if returnoffset:
                          return offset, datasize
                      return None
  
          input_shape = datashape
          tagnoformat = self._tagnoformat
          valueformat = self._valueformat
          offsetformat = self._offsetformat
          offsetsize = self._offsetsize
          tagsize = self._tagsize
  
          MINISBLACK = TIFF.PHOTOMETRIC.MINISBLACK
          MINISWHITE = TIFF.PHOTOMETRIC.MINISWHITE
          RGB = TIFF.PHOTOMETRIC.RGB
          CFA = TIFF.PHOTOMETRIC.CFA
          PALETTE = TIFF.PHOTOMETRIC.PALETTE
          CONTIG = TIFF.PLANARCONFIG.CONTIG
          SEPARATE = TIFF.PLANARCONFIG.SEPARATE
  
          # parse input
          if photometric is not None:
              photometric = enumarg(TIFF.PHOTOMETRIC, photometric)
          if planarconfig:
              planarconfig = enumarg(TIFF.PLANARCONFIG, planarconfig)
          if extrasamples is None:
              extrasamples_ = None
          else:
              extrasamples_ = tuple(enumarg(TIFF.EXTRASAMPLE, es)
                                    for es in sequence(extrasamples))
          if not compress:
              compress = False
              compresstag = 1
              # TODO: support predictors without compression
              predictor = False
              predictortag = 1
          else:
              if isinstance(compress, (tuple, list)):
                  compress, compresslevel = compress
              elif isinstance(compress, int):
                  compress, compresslevel = 'ADOBE_DEFLATE', int(compress)
                  if not 0 <= compresslevel <= 9:
                      raise ValueError('invalid compression level %s' % compress)
              else:
                  compresslevel = None
              compress = compress.upper()
              compresstag = enumarg(TIFF.COMPRESSION, compress)
  
          if predictor:
              if datadtype.kind in 'iu':
                  predictortag = 2
                  predictor = TIFF.PREDICTORS[2]
              elif datadtype.kind == 'f':
                  predictortag = 3
                  predictor = TIFF.PREDICTORS[3]
              else:
                  raise ValueError('cannot apply predictor to %s' % datadtype)
  
          # prepare ImageJ format
          if self._imagej:
              # if predictor or compress:
              #     warnings.warn(
              #         'ImageJ cannot handle predictors or compression')
              if description:
                  warnings.warn('not writing description to ImageJ file')
                  description = None
              volume = False
              if datadtypechar not in 'BHhf':
                  raise ValueError(
                      'ImageJ does not support data type %s' % datadtypechar)
              ijrgb = photometric == RGB if photometric else None
              if datadtypechar not in 'B':
                  ijrgb = False
              ijshape = imagej_shape(datashape, ijrgb)
              if ijshape[-1] in (3, 4):
                  photometric = RGB
                  if datadtypechar not in 'B':
                      raise ValueError('ImageJ does not support data type %s '
                                       'for RGB' % datadtypechar)
              elif photometric is None:
                  photometric = MINISBLACK
                  planarconfig = None
              if planarconfig == SEPARATE:
                  raise ValueError('ImageJ does not support planar images')
              planarconfig = CONTIG if ijrgb else None
  
          # define compress function
          if compress:
              compressor = TIFF.COMPESSORS[compresstag]
              if predictor:
                  def compress(data, level=compresslevel):
                      data = predictor(data, axis=-2)
                      return compressor(data, level)
              else:
                  def compress(data, level=compresslevel):
                      return compressor(data, level)
  
          # verify colormap and indices
          if colormap is not None:
              if datadtypechar not in 'BH':
                  raise ValueError('invalid data dtype for palette mode')
              colormap = numpy.asarray(colormap, dtype=byteorder+'H')
              if colormap.shape != (3, 2**(datadtype.itemsize * 8)):
                  raise ValueError('invalid color map shape')
              self._colormap = colormap
  
          # verify tile shape
          if tile:
              tile = tuple(int(i) for i in tile[:3])
              volume = len(tile) == 3
              if (len(tile) < 2 or tile[-1] % 16 or tile[-2] % 16 or
                      any(i < 1 for i in tile)):
                  raise ValueError('invalid tile shape')
          else:
              tile = ()
              volume = False
  
          # normalize data shape to 5D or 6D, depending on volume:
          #   (pages, planar_samples, [depth,] height, width, contig_samples)
          datashape = reshape_nd(datashape, 3 if photometric == RGB else 2)
          shape = datashape
          ndim = len(datashape)
  
          samplesperpixel = 1
          extrasamples = 0
          if volume and ndim < 3:
              volume = False
          if colormap is not None:
              photometric = PALETTE
              planarconfig = None
          if photometric is None:
              photometric = MINISBLACK
              if bilevel:
                  photometric = MINISWHITE
              elif planarconfig == CONTIG:
                  if ndim > 2 and shape[-1] in (3, 4):
                      photometric = RGB
              elif planarconfig == SEPARATE:
                  if volume and ndim > 3 and shape[-4] in (3, 4):
                      photometric = RGB
                  elif ndim > 2 and shape[-3] in (3, 4):
                      photometric = RGB
              elif ndim > 2 and shape[-1] in (3, 4):
                  photometric = RGB
              elif self._imagej:
                  photometric = MINISBLACK
              elif volume and ndim > 3 and shape[-4] in (3, 4):
                  photometric = RGB
              elif ndim > 2 and shape[-3] in (3, 4):
                  photometric = RGB
          if planarconfig and len(shape) <= (3 if volume else 2):
              planarconfig = None
              if photometric not in (0, 1, 3, 4):
                  photometric = MINISBLACK
          if photometric == RGB:
              if len(shape) < 3:
                  raise ValueError('not a RGB(A) image')
              if len(shape) < 4:
                  volume = False
              if planarconfig is None:
                  if shape[-1] in (3, 4):
                      planarconfig = CONTIG
                  elif shape[-4 if volume else -3] in (3, 4):
                      planarconfig = SEPARATE
                  elif shape[-1] > shape[-4 if volume else -3]:
                      planarconfig = SEPARATE
                  else:
                      planarconfig = CONTIG
              if planarconfig == CONTIG:
                  datashape = (-1, 1) + shape[(-4 if volume else -3):]
                  samplesperpixel = datashape[-1]
              else:
                  datashape = (-1,) + shape[(-4 if volume else -3):] + (1,)
                  samplesperpixel = datashape[1]
              if samplesperpixel > 3:
                  extrasamples = samplesperpixel - 3
          elif photometric == CFA:
              if len(shape) != 2:
                  raise ValueError('invalid CFA image')
              volume = False
              planarconfig = None
              datashape = (-1, 1) + shape[-2:] + (1,)
              if 50706 not in (et[0] for et in extratags):
                  raise ValueError('must specify DNG tags for CFA image')
          elif planarconfig and len(shape) > (3 if volume else 2):
              if planarconfig == CONTIG:
                  datashape = (-1, 1) + shape[(-4 if volume else -3):]
                  samplesperpixel = datashape[-1]
              else:
                  datashape = (-1,) + shape[(-4 if volume else -3):] + (1,)
                  samplesperpixel = datashape[1]
              extrasamples = samplesperpixel - 1
          else:
              planarconfig = None
              while len(shape) > 2 and shape[-1] == 1:
                  shape = shape[:-1]  # remove trailing 1s
              if len(shape) < 3:
                  volume = False
              if extrasamples_ is None:
                  datashape = (-1, 1) + shape[(-3 if volume else -2):] + (1,)
              else:
                  datashape = (-1, 1) + shape[(-4 if volume else -3):]
                  samplesperpixel = datashape[-1]
                  extrasamples = samplesperpixel - 1
  
          if subfiletype & 0b100:
              # FILETYPE_MASK
              if not (bilevel and samplesperpixel == 1 and
                      photometric in (0, 1, 4)):
                  raise ValueError('invalid SubfileType MASK')
              photometric = TIFF.PHOTOMETRIC.MASK
  
          # normalize shape to 6D
          assert len(datashape) in (5, 6)
          if len(datashape) == 5:
              datashape = datashape[:2] + (1,) + datashape[2:]
          if datashape[0] == -1:
              s0 = product(input_shape) // product(datashape[1:])
              datashape = (s0,) + datashape[1:]
          shape = datashape
          if data is not None:
              data = data.reshape(shape)
  
          if tile and not volume:
              tile = (1, tile[-2], tile[-1])
  
          if photometric == PALETTE:
              if (samplesperpixel != 1 or extrasamples or
                      shape[1] != 1 or shape[-1] != 1):
                  raise ValueError('invalid data shape for palette mode')
  
          if photometric == RGB and samplesperpixel == 2:
              raise ValueError('not a RGB image (samplesperpixel=2)')
  
          if bilevel:
              if compresstag not in (1, 32773):
                  raise ValueError('cannot compress bilevel image')
              if tile:
                  raise ValueError('cannot save tiled bilevel image')
              if photometric not in (0, 1, 4):
                  raise ValueError('cannot save bilevel image as %s' %
                                   str(photometric))
              datashape = list(datashape)
              if datashape[-2] % 8:
                  datashape[-2] = datashape[-2] // 8 + 1
              else:
                  datashape[-2] = datashape[-2] // 8
              datashape = tuple(datashape)
              assert datasize == product(datashape)
              if data is not None:
                  data = numpy.packbits(data, axis=-2)
                  assert datashape[-2] == data.shape[-2]
  
          bytestr = bytes if sys.version[0] == '2' else (
              lambda x: bytes(x, 'ascii') if isinstance(x, str) else x)
          tags = []  # list of (code, ifdentry, ifdvalue, writeonce)
  
          strip_or_tile = 'Tile' if tile else 'Strip'
          tagbytecounts = TIFF.TAG_NAMES[strip_or_tile + 'ByteCounts']
          tagoffsets = TIFF.TAG_NAMES[strip_or_tile + 'Offsets']
          self._tagoffsets = tagoffsets
  
          def pack(fmt, *val):
              return struct.pack(byteorder+fmt, *val)
  
          def addtag(code, dtype, count, value, writeonce=False):
              # Compute ifdentry & ifdvalue bytes from code, dtype, count, value
              # Append (code, ifdentry, ifdvalue, writeonce) to tags list
              code = int(TIFF.TAG_NAMES.get(code, code))
              try:
                  tifftype = TIFF.DATA_DTYPES[dtype]
              except KeyError:
                  raise ValueError('unknown dtype %s' % dtype)
              rawcount = count
  
              if dtype == 's':
                  # strings
                  value = bytestr(value) + b'\0'
                  count = rawcount = len(value)
                  rawcount = value.find(b'\0\0')
                  if rawcount < 0:
                      rawcount = count
                  else:
                      rawcount += 1  # length of string without buffer
                  value = (value,)
              elif isinstance(value, bytes):
                  # packed binary data
                  dtsize = struct.calcsize(dtype)
                  if len(value) % dtsize:
                      raise ValueError('invalid packed binary data')
                  count = len(value) // dtsize
              if len(dtype) > 1:
                  count *= int(dtype[:-1])
                  dtype = dtype[-1]
              ifdentry = [pack('HH', code, tifftype),
                          pack(offsetformat, rawcount)]
              ifdvalue = None
              if struct.calcsize(dtype) * 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)))
                  else:
                      ifdentry.append(pack(valueformat,
                                           pack(str(count)+dtype, *value)))
              else:
                  # use offset to value(s)
                  ifdentry.append(pack(offsetformat, 0))
                  if isinstance(value, bytes):
                      ifdvalue = value
                  elif isinstance(value, numpy.ndarray):
                      assert value.size == count
                      assert value.dtype.char == dtype
                      ifdvalue = value.tostring()
                  elif isinstance(value, (tuple, list)):
                      ifdvalue = pack(str(count)+dtype, *value)
                  else:
                      ifdvalue = pack(dtype, value)
              tags.append((code, b''.join(ifdentry), ifdvalue, writeonce))
  
          def rational(arg, max_denominator=1000000):
              """"Return nominator and denominator from float or two integers."""
              from fractions import Fraction  # delayed import
              try:
                  f = Fraction.from_float(arg)
              except TypeError:
                  f = Fraction(arg[0], arg[1])
              f = f.limit_denominator(max_denominator)
              return f.numerator, f.denominator
  
          if description:
              # user provided description
              addtag('ImageDescription', 's', 0, description, writeonce=True)
  
          # write shape and metadata to ImageDescription
          self._metadata = {} if not metadata else metadata.copy()
          if self._imagej:
              description = imagej_description(
                  input_shape, shape[-1] in (3, 4), self._colormap is not None,
                  **self._metadata)
          elif metadata or metadata == {}:
              if self._truncate:
                  self._metadata.update(truncated=True)
              description = json_description(input_shape, **self._metadata)
          # elif metadata is None and self._truncate:
          #     raise ValueError('cannot truncate without writing metadata')
          else:
              description = None
          if description:
              # add 64 bytes buffer
              # the image description might be updated later with the final shape
              description = str2bytes(description, 'ascii')
              description += b'\0' * 64
              self._descriptionlen = len(description)
              addtag('ImageDescription', 's', 0, description, writeonce=True)
  
          if software:
              addtag('Software', 's', 0, software, writeonce=True)
          if datetime:
              if isinstance(datetime, str):
                  if len(datetime) != 19 or datetime[16] != ':':
                      raise ValueError('invalid datetime string')
              else:
                  try:
                      datetime = datetime.strftime('%Y:%m:%d %H:%M:%S')
                  except AttributeError:
                      datetime = self._now().strftime('%Y:%m:%d %H:%M:%S')
              addtag('DateTime', 's', 0, datetime, writeonce=True)
          addtag('Compression', 'H', 1, compresstag)
          if predictor:
              addtag('Predictor', 'H', 1, predictortag)
          addtag('ImageWidth', 'I', 1, shape[-2])
          addtag('ImageLength', 'I', 1, shape[-3])
          if tile:
              addtag('TileWidth', 'I', 1, tile[-1])
              addtag('TileLength', 'I', 1, tile[-2])
              if tile[0] > 1:
                  addtag('ImageDepth', 'I', 1, shape[-4])
                  addtag('TileDepth', 'I', 1, tile[0])
          addtag('NewSubfileType', 'I', 1, subfiletype)
          if not bilevel:
              sampleformat = {'u': 1, 'i': 2, 'f': 3, 'c': 6}[datadtype.kind]
              addtag('SampleFormat', 'H', samplesperpixel,
                     (sampleformat,) * samplesperpixel)
          addtag('PhotometricInterpretation', 'H', 1, photometric.value)
          if colormap is not None:
              addtag('ColorMap', 'H', colormap.size, colormap)
          addtag('SamplesPerPixel', 'H', 1, samplesperpixel)
          if bilevel:
              pass
          elif planarconfig and samplesperpixel > 1:
              addtag('PlanarConfiguration', 'H', 1, planarconfig.value)
              addtag('BitsPerSample', 'H', samplesperpixel,
                     (datadtype.itemsize * 8,) * samplesperpixel)
          else:
              addtag('BitsPerSample', 'H', 1, datadtype.itemsize * 8)
          if extrasamples:
              if extrasamples_ is not None:
                  if extrasamples != len(extrasamples_):
                      raise ValueError('wrong number of extrasamples specified')
                  addtag('ExtraSamples', 'H', extrasamples, extrasamples_)
              elif photometric == RGB and extrasamples == 1:
                  # Unassociated alpha channel
                  addtag('ExtraSamples', 'H', 1, 2)
              else:
                  # Unspecified alpha channel
                  addtag('ExtraSamples', 'H', extrasamples, (0,) * extrasamples)
          if resolution is not None:
              addtag('XResolution', '2I', 1, rational(resolution[0]))
              addtag('YResolution', '2I', 1, rational(resolution[1]))
              if len(resolution) > 2:
                  unit = resolution[2]
                  unit = 1 if unit is None else enumarg(TIFF.RESUNIT, unit)
              elif self._imagej:
                  unit = 1
              else:
                  unit = 2
              addtag('ResolutionUnit', 'H', 1, unit)
          elif not self._imagej:
              addtag('XResolution', '2I', 1, (1, 1))
              addtag('YResolution', '2I', 1, (1, 1))
              addtag('ResolutionUnit', 'H', 1, 1)
          if ijmetadata:
              for t in imagej_metadata_tag(ijmetadata, byteorder):
                  addtag(*t)
  
          contiguous = not compress
          if tile:
              # one chunk per tile per plane
              tiles = ((shape[2] + tile[0] - 1) // tile[0],
                       (shape[3] + tile[1] - 1) // tile[1],
                       (shape[4] + tile[2] - 1) // tile[2])
              numtiles = product(tiles) * shape[1]
              databytecounts = [
                  product(tile) * shape[-1] * datadtype.itemsize] * numtiles
              addtag(tagbytecounts, offsetformat, numtiles, databytecounts)
              addtag(tagoffsets, offsetformat, numtiles, [0] * numtiles)
              contiguous = contiguous and product(tiles) == 1
              if not contiguous:
                  # allocate tile buffer
                  chunk = numpy.empty(tile + (shape[-1],), dtype=datadtype)
          elif contiguous and (bilevel or rowsperstrip is None):
              # one strip per plane
              if bilevel:
                  databytecounts = [product(datashape[2:])] * shape[1]
              else:
                  databytecounts = [
                      product(datashape[2:]) * datadtype.itemsize] * shape[1]
              addtag(tagbytecounts, offsetformat, shape[1], databytecounts)
              addtag(tagoffsets, offsetformat, shape[1], [0] * shape[1])
              addtag('RowsPerStrip', 'I', 1, shape[-3])
          else:
              # use rowsperstrip
              rowsize = product(shape[-2:]) * datadtype.itemsize
              if rowsperstrip is None:
                  # compress ~64 KB chunks by default
                  rowsperstrip = 65536 // rowsize if compress else shape[-3]
              if rowsperstrip < 1:
                  rowsperstrip = 1
              elif rowsperstrip > shape[-3]:
                  rowsperstrip = shape[-3]
              addtag('RowsPerStrip', 'I', 1, rowsperstrip)
  
              numstrips1 = (shape[-3] + rowsperstrip - 1) // rowsperstrip
              numstrips = numstrips1 * shape[1]
              if compress:
                  databytecounts = [0] * numstrips
              else:
                  # TODO: save bilevel data with rowsperstrip
                  stripsize = rowsperstrip * rowsize
                  databytecounts = [stripsize] * numstrips
                  stripsize -= rowsize * (numstrips1 * rowsperstrip - shape[-3])
                  for i in range(numstrips1-1, numstrips, numstrips1):
                      databytecounts[i] = stripsize
              addtag(tagbytecounts, offsetformat, numstrips, databytecounts)
              addtag(tagoffsets, offsetformat, numstrips, [0] * numstrips)
  
          if data is None and not contiguous:
              raise ValueError('cannot write non-contiguous empty file')
  
          # add extra tags from user
          for t in extratags:
              addtag(*t)
  
          # TODO: check TIFFReadDirectoryCheckOrder warning in files containing
          #   multiple tags of same code
          # the entries in an IFD must be sorted in ascending order by tag code
          tags = sorted(tags, key=lambda x: x[0])
  
          fhpos = fh.tell()
          if not (self._bigtiff or self._imagej) and fhpos + datasize > 2**32-1:
              raise ValueError('data too large for standard TIFF file')
  
          # if not compressed or multi-tiled, write the first IFD and then
          # all data contiguously; else, write all IFDs and data interleaved
          for pageindex in range(1 if contiguous else shape[0]):
  
              ifdpos = fhpos
              if ifdpos % 2:
                  # location of IFD must begin on a word boundary
                  fh.write(b'\0')
                  ifdpos += 1
  
              # update pointer at ifdoffset
              fh.seek(self._ifdoffset)
              fh.write(pack(offsetformat, ifdpos))
              fh.seek(ifdpos)
  
              # create IFD in memory
              if pageindex < 2:
                  ifd = io.BytesIO()
                  ifd.write(pack(tagnoformat, len(tags)))
                  tagoffset = ifd.tell()
                  ifd.write(b''.join(t[1] for t in tags))
                  ifdoffset = ifd.tell()
                  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
                      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')
                              pos += 1
                          ifd.seek(offset)
                          ifd.write(pack(offsetformat, ifdpos + pos))
                          ifd.seek(pos)
                          ifd.write(value)
                          if code == tagoffsets:
                              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 == tagoffsets:
                          dataoffsetsoffset = offset, None
                      elif code == tagbytecounts:
                          databytecountsoffset = offset, None
                  ifdsize = ifd.tell()
                  if ifdsize % 2:
                      ifd.write(b'\0')
                      ifdsize += 1
  
              # write IFD later when strip/tile bytecounts and offsets are known
              fh.seek(ifdsize, 1)
  
              # write image data
              dataoffset = fh.tell()
              skip = align - dataoffset % align
              fh.seek(skip, 1)
              dataoffset += skip
              if contiguous:
                  if data is None:
                      fh.write_empty(datasize)
                  else:
                      fh.write_array(data)
              elif tile:
                  if data is None:
                      fh.write_empty(numtiles * databytecounts[0])
                  else:
                      stripindex = 0
                      for plane in data[pageindex]:
                          for tz in range(tiles[0]):
                              for ty in range(tiles[1]):
                                  for tx in range(tiles[2]):
                                      c0 = min(tile[0], shape[2] - tz*tile[0])
                                      c1 = min(tile[1], shape[3] - ty*tile[1])
                                      c2 = min(tile[2], shape[4] - tx*tile[2])
                                      chunk[c0:, c1:, c2:] = 0
                                      chunk[:c0, :c1, :c2] = plane[
                                          tz*tile[0]:tz*tile[0]+c0,
                                          ty*tile[1]:ty*tile[1]+c1,
                                          tx*tile[2]:tx*tile[2]+c2]
                                      if compress:
                                          t = compress(chunk)
                                          fh.write(t)
                                          databytecounts[stripindex] = len(t)
                                          stripindex += 1
                                      else:
                                          fh.write_array(chunk)
                                          # fh.flush()
              elif compress:
                  # write one strip per rowsperstrip
                  assert data.shape[2] == 1  # not handling depth
                  numstrips = (shape[-3] + rowsperstrip - 1) // rowsperstrip
                  stripindex = 0
                  for plane in data[pageindex]:
                      for i in range(numstrips):
                          strip = plane[0, i*rowsperstrip: (i+1)*rowsperstrip]
                          strip = compress(strip)
                          fh.write(strip)
                          databytecounts[stripindex] = len(strip)
                          stripindex += 1
              else:
                  fh.write_array(data[pageindex])
  
              # update strip/tile offsets
              offset, pos = dataoffsetsoffset
              ifd.seek(offset)
              if pos:
                  ifd.write(pack(offsetformat, ifdpos + pos))
                  ifd.seek(pos)
                  offset = dataoffset
                  for size in databytecounts:
                      ifd.write(pack(offsetformat, offset))
                      offset += size
              else:
                  ifd.write(pack(offsetformat, dataoffset))
  
              if compress:
                  # update strip/tile bytecounts
                  offset, pos = databytecountsoffset
                  ifd.seek(offset)
                  if pos:
                      ifd.write(pack(offsetformat, ifdpos + pos))
                      ifd.seek(pos)
                      for size in databytecounts:
                          ifd.write(pack(offsetformat, size))
                  else:
                      ifd.write(pack(offsetformat, databytecounts[0]))
  
              fhpos = fh.tell()
              fh.seek(ifdpos)
              fh.write(iogetbuffer(ifd))
              fh.flush()
              fh.seek(fhpos)
  
              self._ifdoffset = ifdpos + ifdoffset
  
              # remove tags that should be written only once
              if pageindex == 0:
                  tags = [tag for tag in tags if not tag[-1]]
  
          self._shape = shape
          self._datashape = (1,) + input_shape
          self._datadtype = datadtype
          self._dataoffset = dataoffset
          self._databytecounts = databytecounts
  
          if contiguous:
              # write remaining IFDs/tags later
              self._tags = tags
              # return offset and size of image data
              if returnoffset:
                  return dataoffset, sum(databytecounts)
          return None