Write image data to TIFF file.

    Image data are written uncompressed in one stripe per plane.
    Dimensions larger than 2 or 3 (depending on photometric mode and
    planar configuration) are flattened and saved as separate pages.

    Parameters
    ----------
    filename : str
        Name of file to write.
    data : array_like
        Input image. The last dimensions are assumed to be image height,
        width, and samples.
    photometric : {'minisblack', 'miniswhite', 'rgb'}
        The color space of the image data.
        By default this setting is inferred from the data shape.
    planarconfig : {'contig', 'planar'}
        Specifies if samples are stored contiguous or in separate planes.
        By default this setting is inferred from the data shape.
        'contig': last dimension contains samples.
        'planar': third last dimension contains samples.
    resolution : ((int, int), (int, int))
        X and Y resolution in dots per inch as rational numbers.
    description : str
        The subject of the image. Saved with the first page only.
    software : str
        Name of the software used to create the image.
        Saved with the first page only.
    byteorder : {'<', '>'}
        The endianness of the data in the file.
        By default this is the system's native byte order.
    bigtiff : bool
        If True the BigTIFF format is used.
        By default the standard TIFF format is used for data less than 2040 MB.

    Examples
    --------
    >>> data = numpy.random.rand(10, 3, 301, 219)
    >>> imsave('temp.tif', data)

def imsave(filename, data, photometric=None, planarconfig=None,
           resolution=None, description=None, software='tifffile.py',
           byteorder=None, bigtiff=False):
    """Write image data to TIFF file.

    Image data are written uncompressed in one stripe per plane.
    Dimensions larger than 2 or 3 (depending on photometric mode and
    planar configuration) are flattened and saved as separate pages.

    Parameters
    ----------
    filename : str
        Name of file to write.
    data : array_like
        Input image. The last dimensions are assumed to be image height,
        width, and samples.
    photometric : {'minisblack', 'miniswhite', 'rgb'}
        The color space of the image data.
        By default this setting is inferred from the data shape.
    planarconfig : {'contig', 'planar'}
        Specifies if samples are stored contiguous or in separate planes.
        By default this setting is inferred from the data shape.
        'contig': last dimension contains samples.
        'planar': third last dimension contains samples.
    resolution : ((int, int), (int, int))
        X and Y resolution in dots per inch as rational numbers.
    description : str
        The subject of the image. Saved with the first page only.
    software : str
        Name of the software used to create the image.
        Saved with the first page only.
    byteorder : {'<', '>'}
        The endianness of the data in the file.
        By default this is the system's native byte order.
    bigtiff : bool
        If True the BigTIFF format is used.
        By default the standard TIFF format is used for data less than 2040 MB.

    Examples
    --------
    >>> data = numpy.random.rand(10, 3, 301, 219)
    >>> imsave('temp.tif', data)

    """
    assert(photometric in (None, 'minisblack', 'miniswhite', 'rgb'))
    assert(planarconfig in (None, 'contig', 'planar'))
    assert(byteorder in (None, '<', '>'))

    if byteorder is None:
        byteorder = '<' if sys.byteorder == 'little' else '>'

    data = numpy.asarray(data, dtype=byteorder+data.dtype.char, order='C')
    data_shape = shape = data.shape
    data = numpy.atleast_2d(data)

    if not bigtiff and data.size * data.dtype.itemsize < 2040*2**20:
        bigtiff = False
        offset_size = 4
        tag_size = 12
        numtag_format = 'H'
        offset_format = 'I'
        val_format = '4s'
    else:
        bigtiff = True
        offset_size = 8
        tag_size = 20
        numtag_format = 'Q'
        offset_format = 'Q'
        val_format = '8s'

    # unify shape of data
    samplesperpixel = 1
    extrasamples = 0
    if photometric is None:
        if data.ndim > 2 and (shape[-3] in (3, 4) or shape[-1] in (3, 4)):
            photometric = 'rgb'
        else:
            photometric = 'minisblack'
    if photometric == 'rgb':
        if len(shape) < 3:
            raise ValueError("not a RGB(A) image")
        if planarconfig is None:
            planarconfig = 'planar' if shape[-3] in (3, 4) else 'contig'
        if planarconfig == 'contig':
            if shape[-1] not in (3, 4):
                raise ValueError("not a contiguous RGB(A) image")
            data = data.reshape((-1, 1) + shape[-3:])
            samplesperpixel = shape[-1]
        else:
            if shape[-3] not in (3, 4):
                raise ValueError("not a planar RGB(A) image")
            data = data.reshape((-1, ) + shape[-3:] + (1, ))
            samplesperpixel = shape[-3]
        if samplesperpixel == 4:
            extrasamples = 1
    elif planarconfig and len(shape) > 2:
        if planarconfig == 'contig':
            data = data.reshape((-1, 1) + shape[-3:])
            samplesperpixel = shape[-1]
        else:
            data = data.reshape((-1, ) + shape[-3:] + (1, ))
            samplesperpixel = shape[-3]
        extrasamples = samplesperpixel - 1
    else:
        planarconfig = None
        data = data.reshape((-1, 1) + shape[-2:] + (1, ))

    shape = data.shape  # (pages, planes, height, width, contig samples)

    bytestr = bytes if sys.version[0] == '2' else lambda x: bytes(x, 'ascii')
    tifftypes = {'B': 1, 's': 2, 'H': 3, 'I': 4, '2I': 5, 'b': 6,
                 'h': 8, 'i': 9, 'f': 11, 'd': 12, 'Q': 16, 'q': 17}
    tifftags = {'new_subfile_type': 254, 'subfile_type': 255,
        'image_width': 256, 'image_length': 257, 'bits_per_sample': 258,
        'compression': 259, 'photometric': 262, 'fill_order': 266,
        'document_name': 269, 'image_description': 270, 'strip_offsets': 273,
        'orientation': 274, 'samples_per_pixel': 277, 'rows_per_strip': 278,
        'strip_byte_counts': 279, 'x_resolution': 282, 'y_resolution': 283,
        'planar_configuration': 284, 'page_name': 285, 'resolution_unit': 296,
        'software': 305, 'datetime': 306, 'predictor': 317, 'color_map': 320,
        'extra_samples': 338, 'sample_format': 339}

    tags = []
    tag_data = []

    def pack(fmt, *val):
        return struct.pack(byteorder+fmt, *val)

    def tag(name, dtype, number, value, offset=[0]):
        # append tag binary string to tags list
        # append (offset, value as binary string) to tag_data list
        # increment offset by tag_size
        if dtype == 's':
            value = bytestr(value) + b'\0'
            number = len(value)
            value = (value, )
        t = [pack('HH', tifftags[name], tifftypes[dtype]),
             pack(offset_format, number)]
        if len(dtype) > 1:
            number *= int(dtype[:-1])
            dtype = dtype[-1]
        if number == 1:
            if isinstance(value, (tuple, list)):
                value = value[0]
            t.append(pack(val_format, pack(dtype, value)))
        elif struct.calcsize(dtype) * number <= offset_size:
            t.append(pack(val_format, pack(str(number)+dtype, *value)))
        else:
            t.append(pack(offset_format, 0))
            tag_data.append((offset[0] + offset_size + 4,
                             pack(str(number)+dtype, *value)))
        tags.append(b''.join(t))
        offset[0] += tag_size

    if software:
        tag('software', 's', 0, software)
    if description:
        tag('image_description', 's', 0, description)
    elif shape != data_shape:
        tag('image_description', 's', 0,
            "shape=(%s)" % (",".join('%i' % i for i in data_shape)))
    tag('datetime', 's', 0,
        datetime.datetime.now().strftime("%Y:%m:%d %H:%M:%S"))
    # write previous tags only once
    writeonce = (len(tags), len(tag_data)) if shape[0] > 1 else None
    tag('compression', 'H', 1, 1)
    tag('orientation', 'H', 1, 1)
    tag('image_width', 'I', 1, shape[-2])
    tag('image_length', 'I', 1, shape[-3])
    tag('new_subfile_type', 'I', 1, 0 if shape[0] == 1 else 2)
    tag('sample_format', 'H', 1,
        {'u': 1, 'i': 2, 'f': 3, 'c': 6}[data.dtype.kind])
    tag('photometric', 'H', 1,
        {'miniswhite': 0, 'minisblack': 1, 'rgb': 2}[photometric])
    tag('samples_per_pixel', 'H', 1, samplesperpixel)
    if planarconfig:
        tag('planar_configuration', 'H', 1, 1 if planarconfig=='contig' else 2)
        tag('bits_per_sample', 'H', samplesperpixel,
            (data.dtype.itemsize * 8, ) * samplesperpixel)
    else:
        tag('bits_per_sample', 'H', 1, data.dtype.itemsize * 8)
    if extrasamples:
        if photometric == 'rgb':
            tag('extra_samples', 'H', 1, 1)  # alpha channel
        else:
            tag('extra_samples', 'H', extrasamples, (0, ) * extrasamples)
    if resolution:
        tag('x_resolution', '2I', 1, resolution[0])
        tag('y_resolution', '2I', 1, resolution[1])
        tag('resolution_unit', 'H', 1, 2)
    tag('rows_per_strip', 'I', 1, shape[-3])
    # use one strip per plane
    strip_byte_counts = (data[0, 0].size * data.dtype.itemsize, ) * shape[1]
    tag('strip_byte_counts', offset_format, shape[1], strip_byte_counts)
    # strip_offsets must be the last tag; will be updated later
    tag('strip_offsets', offset_format, shape[1], (0, ) * shape[1])

    fd = open(filename, 'wb')
    seek = fd.seek
    tell = fd.tell

    def write(arg, *args):
        fd.write(pack(arg, *args) if args else arg)

    write({'<': b'II', '>': b'MM'}[byteorder])
    if bigtiff:
        write('HHH', 43, 8, 0)
    else:
        write('H', 42)
    ifd_offset = tell()
    write(offset_format, 0)  # first IFD
    for i in range(shape[0]):
        # update pointer at ifd_offset
        pos = tell()
        seek(ifd_offset)
        write(offset_format, pos)
        seek(pos)
        # write tags
        write(numtag_format, len(tags))
        tag_offset = tell()
        write(b''.join(tags))
        ifd_offset = tell()
        write(offset_format, 0)  # offset to next ifd
        # write extra tag data and update pointers
        for off, dat in tag_data:
            pos = tell()
            seek(tag_offset + off)
            write(offset_format, pos)
            seek(pos)
            write(dat)
        # update strip_offsets
        pos = tell()
        if len(strip_byte_counts) == 1:
            seek(ifd_offset - offset_size)
            write(offset_format, pos)
        else:
            seek(pos - offset_size*shape[1])
            strip_offset = pos
            for size in strip_byte_counts:
                write(offset_format, strip_offset)
                strip_offset += size
        seek(pos)
        # write data
        data[i].tofile(fd)  # if this fails, try update Python and numpy
        fd.flush()
        # remove tags that should be written only once
        if writeonce:
            tags = tags[writeonce[0]:]
            d = writeonce[0] * tag_size
            tag_data = [(o-d, v) for (o, v) in tag_data[writeonce[1]:]]
            writeonce = None
    fd.close()