################################################################################
# tiff16.py
#
# Simple method WriteTiff16() to write a numpy array as a 16-bit uncompressed
# tiff, in grayscale, RGB color or palette color.
#
# Associated method ReadTiff16() is not a general implementation of the Tiff
# standard; however, it will successfully read 16-bit Tiffs written by
# WriteTiff16().
#
# Note, however, that 16-bit palette color is not widely supported. For this
# reason, a "translate" option is provided, which converts palette color to RGB.
#
# Mark R. Showalter, SETI Institute, July 2009
################################################################################
import sys
from struct import pack, unpack
from typing import Any
import numpy as np
from PIL import Image
[docs]
def WriteTiff16(
filename: str,
array: Any,
palette: Any = None,
up: bool = False,
byteorder: str = "native",
translate: bool = True,
transpose: Any = None,
) -> None:
"""Write a 16-bit TIFF file from a 2-D or 3-D numpy array.
Three TIFF formats are supported: grayscale, RGB, and palette.
Parameters:
filename: The name of the file to write.
array: A numpy 2-D or 3-D array containing the image pixels.
Values are converted to unsigned 16-bit if they are not
already in that format. Indices are
``(line, sample, band)``; the third axis is optional. If
present with size ``>= 3`` and no ``palette`` is provided,
``array[:, :, 0:3]`` is interpreted as the ``(R, G, B)``
values for each pixel.
palette: Optional ``(65536, 3)`` array. When provided, the 0th
band of ``array`` indexes into the palette to derive each
pixel's ``(R, G, B)`` triple.
up: True for line numbers to increase upward; False (default)
for downward.
byteorder: One of ``'native'``, ``'little'``, or ``'big'``.
Default is ``'native'``.
translate: True to translate a palette image to RGB on write
(default; many TIFF readers do not support 16-bit
palettes). False writes the palette index plus the palette
table.
transpose: Optional geometric transformation before writing.
Choices match ``PIL.Image.Transpose``:
``FLIP_LEFT_RIGHT``, ``FLIP_TOP_BOTTOM``, ``ROTATE_90``,
``ROTATE_180``, ``ROTATE_270``.
"""
# Open the output file
with open(filename, "wb") as f:
# Flip if line numbers increase upward
if up:
array = np.flipud(array)
# Apply transpose operation if needed
if transpose == Image.Transpose.FLIP_LEFT_RIGHT:
array = np.fliplr(array)
if transpose == Image.Transpose.FLIP_TOP_BOTTOM:
array = np.flipud(array)
if transpose == Image.Transpose.ROTATE_90:
array = np.rot90(array,1)
if transpose == Image.Transpose.ROTATE_180:
array = np.rot90(array,2)
if transpose == Image.Transpose.ROTATE_270:
array = np.rot90(array,3)
# Interpret the shape of the image
if array.ndim == 3:
(height, width, bands) = array.shape
else:
(height, width) = array.shape
bands = 1
array = array.reshape((height, width, 1))
# What type of tiff?
has_palette = (palette is not None)
is_rgb = bands >= 3
if has_palette:
is_rgb = False # Palette overrides RGB data
# Determine how to handle the palette in the output file
if translate and has_palette:
has_palette = False
is_rgb = True
translate_to_rgb = True
else:
translate_to_rgb = False
# Interpret the byte order. Validate explicitly so an unknown
# `byteorder` value fails fast rather than silently being
# treated as big-endian.
byteorder = byteorder.lower()
if byteorder == "native":
byteorder = sys.byteorder
if byteorder == "little":
o = "<"
flag = b"I"
elif byteorder == "big":
o = ">"
flag = b"M"
else:
raise ValueError(
f"byteorder must be 'native', 'little', or 'big'; got {byteorder!r}"
)
# Write the Image File Header
#------- 0 bytes
f.write(pack("cc", flag, flag))
f.write(pack(o+"H", 42)) # TIFF's 42
f.write(pack(o+"L", 8)) # IFD begins at offset 8
#------- 8 bytes
# Determine the key offsets into the file
ifd_offset = 8
if has_palette:
ifd_entries = 12
after_bytes = 16 + 65536 * 3 * 2
pixel_bytes = 2
elif is_rgb:
ifd_entries = 12
after_bytes = 16 + 8
pixel_bytes = 6
else:
ifd_entries = 11
after_bytes = 16
pixel_bytes = 2
ifd_bytes = 2 + 12 * ifd_entries + 4
after_offset = ifd_offset + ifd_bytes
data_offset = after_offset + after_bytes
# Write the Image File Directory
f.write(pack(o+"H", ifd_entries)) # number of entries
f.write(pack(o+"HHLL", 256, 4, 1, width)) # image width
f.write(pack(o+"HHLL", 257, 4, 1, height)) # image height
if is_rgb: # bits per sample
f.write(pack(o+"HHLL", 258, 3, 3, after_offset + 16))
else:
f.write(pack(o+"HHLHH", 258, 3, 1, 16, 0))
f.write(pack(o+"HHLHH", 259, 3, 1, 1, 0)) # no compression
if has_palette: # photometric model
f.write(pack(o+"HHLHH", 262, 3, 1, 3, 0)) # palette
elif is_rgb:
f.write(pack(o+"HHLHH", 262, 3, 1, 2, 0)) # RGB
else:
f.write(pack(o+"HHLHH", 262, 3, 1, 1, 0)) # black is zero
f.write(pack(o+"HHLL", 273, 4, 1, data_offset)) # where data begins
if is_rgb: # samples per pixel
f.write(pack(o+"HHLHH", 277, 3, 1, 3, 0))
f.write(pack(o+"HHLL", 278, 4, 1, height)) # rows per strip
f.write(pack(o+"HHLL", 279, 4, 1, width * height * pixel_bytes))
# bytes per strip
f.write(pack(o+"HHLL", 282, 5, 1, after_offset)) # x resolution
f.write(pack(o+"HHLL", 283, 5, 1, after_offset + 8))
# y resolution
f.write(pack(o+"HHLHH", 296, 3, 1, 2, 0)) # unit is inches
if has_palette: # start of table
f.write(pack(o+"HHLL", 320, 3, 3*65536, after_offset + 16))
f.write(pack(o+"L", 0)) # end of IFD
# "After" values pointed to by every IFD
f.write(pack(o+"LL", 72, 1)) # x unit is 72/inch
f.write(pack(o+"LL", 72, 1)) # y unit is 72/inch
# Write the palette here if there is one
if has_palette:
palette[0:65536,0:3].astype(o+"u2").transpose().tofile(f,sep="")
# Otherwise, write the size of each RGB pixel if necessary
elif is_rgb: # samples per pixel
f.write(pack(o+"HHHH", 16, 16, 16, 0))
# Translate colors if necessary
if translate_to_rgb:
array = palette[array[:,:,0],0:3]
# Otherwise just slice off the needed bands
elif is_rgb:
array = array[:,:,0:3]
else:
array = array[:,:,0]
# Append data to output file
array.astype(o+"u2").tofile(f, sep="")
[docs]
def ReadTiff16(
filename: str,
up: bool = False,
transpose: Any = None,
) -> tuple[Any, Any]:
"""Read a 16-bit TIFF file written by :func:`WriteTiff16`.
No other TIFF file formats are supported.
Parameters:
filename: The name of the file to read.
up: True for line numbers to increase upward; False (default)
for downward.
transpose: Optional geometric transformation to undo on the
image before returning. Choices match
``PIL.Image.Transpose``: ``FLIP_LEFT_RIGHT``,
``FLIP_TOP_BOTTOM``, ``ROTATE_90``, ``ROTATE_180``,
``ROTATE_270``.
Returns:
``(array, palette)``:
* ``array`` — a numpy 2-D or 3-D ``uint16`` array indexed
``(line, sample, band)``. The third axis is present only for
RGB inputs (``size >= 3``).
* ``palette`` — an optional ``(65536, 3)`` array. When present,
the 0th band of ``array`` indexes into the palette to derive
each pixel's ``(R, G, B)`` triple. ``None`` for non-palette
inputs.
"""
# Open the output file inside a `with` so the handle is closed
# promptly if any `raise OSError` / `my_assert` short-circuits the
# parse (otherwise the file leaks until GC and `pytest -W error`
# turns the ResourceWarning into a test failure).
with open(filename, "rb") as f:
# Read the Image File Header
#------- 0 bytes
# f.read(pack("cc", flag, flag))
# f.read(pack(o+"H", fortytwo)) # TIFF's 42
# f.read(pack(o+"L", eight)) # IFD begins at offset 8
#------- 8 bytes
flag1 = f.read(1)
flag2 = f.read(1)
if flag1 != flag2:
raise OSError("File format is not TIFF")
if flag1 == b'I':
o = "<"
elif flag1 == b"M":
o = ">"
else:
raise OSError("File format is not TIFF")
t = unpack(o+"H", f.read(2))
if t[0] != 42:
raise OSError("File format is not TIFF")
t = unpack(o+"L", f.read(4))
my_assert(t[0] == 8)
# f.write(pack(o+"H", ifd_entries)) # number of entries
# f.write(pack(o+"HHLL", 256, 4, 1, width)) # image width
# f.write(pack(o+"HHLL", 257, 4, 1, height)) # image height
t = unpack(o+"H", f.read(2))
ifd_entries = t[0]
t = unpack(o+"HHLL", f.read(12))
my_assert((t[0],t[1],t[2]) == (256, 4, 1))
width = t[3]
t = unpack(o+"HHLL", f.read(12))
my_assert((t[0],t[1],t[2]) == (257, 4, 1))
height = t[3]
# if is_rgb: # bits per sample
# f.write(pack(o+"HHLL", 258, 3, 3, after_offset + 16))
# else:
# f.write(pack(o+"HHLHH", 258, 3, 1, 16, 0))
t = unpack(o+"HHLHH", f.read(12))
my_assert((t[0],t[1]) == (258, 3))
if t[2] == 3:
is_rgb = True
elif t[2] == 1:
is_rgb = False
else:
my_assert(False)
if is_rgb:
after_offset = t[3] - 16
else:
my_assert((t[3],t[4]) == (16, 0))
# f.write(pack(o+"HHLHH", 259, 3, 1, 1, 0)) # no compression
t = unpack(o+"HHLHH", f.read(12))
my_assert(t == (259, 3, 1, 1, 0))
# if has_palette: # photometric model
# f.write(pack(o+"HHLHH", 262, 3, 1, 3, 0)) # palette
# elif is_rgb:
# f.write(pack(o+"HHLHH", 262, 3, 1, 2, 0)) # RGB
# else:
# f.write(pack(o+"HHLHH", 262, 3, 1, 1, 0)) # black is zero
t = unpack(o+"HHLHH", f.read(12))
has_palette = (t == (262, 3, 1, 3, 0))
test_rgb = (t == (262, 3, 1, 2, 0))
is_gray = (t == (262, 3, 1, 1, 0))
my_assert(test_rgb == is_rgb)
my_assert(has_palette or is_rgb or is_gray)
# Fill in expected sizes now
ifd_offset = 8
if has_palette:
ifd_entries = 12
after_bytes = 16 + 65536 * 3 * 2
pixel_bytes = 2
elif is_rgb:
ifd_entries = 12
after_bytes = 16 + 8
pixel_bytes = 6
else:
ifd_entries = 11
after_bytes = 16
pixel_bytes = 2
ifd_bytes = 2 + 12 * ifd_entries + 4
after_offset = ifd_offset + ifd_bytes
data_offset = after_offset + after_bytes
# f.write(pack(o+"HHLL", 273, 4, 1, data_offset)) # where data begins
t = unpack(o+"HHLL", f.read(12))
my_assert(t == (273, 4, 1, data_offset))
# if is_rgb: # samples per pixel
# f.write(pack(o+"HHLHH", 277, 3, 1, 3, 0))
if is_rgb:
t = unpack(o+"HHLHH", f.read(12))
my_assert(t == (277, 3, 1, 3, 0))
# f.write(pack(o+"HHLL", 278, 4, 1, height)) # rows per strip
# f.write(pack(o+"HHLL", 279, 4, 1, width * height * pixel_bytes))
# bytes per strip
t = unpack(o+"HHLL", f.read(12))
my_assert(t == (278, 4, 1, height))
if is_rgb:
pixel_bytes = 6
else:
pixel_bytes = 2
t = unpack(o+"HHLL", f.read(12))
my_assert(t == (279, 4, 1, width * height * pixel_bytes))
# f.write(pack(o+"HHLL", 282, 5, 1, after_offset)) # x resolution
# f.write(pack(o+"HHLL", 283, 5, 1, after_offset + 8))
# y resolution
t = unpack(o+"HHLL", f.read(12))
my_assert(t == (282, 5, 1, after_offset))
t = unpack(o+"HHLL", f.read(12))
my_assert(t == (283, 5, 1, after_offset + 8))
# f.write(pack(o+"HHLHH", 296, 3, 1, 2, 0)) # unit is inches
t = unpack(o+"HHLHH", f.read(12))
my_assert(t == (296, 3, 1, 2, 0))
# if has_palette: # start of table
# f.write(pack(o+"HHLL", 320, 3, 3*65536, after_offset + 16))
if has_palette:
t = unpack(o+"HHLL", f.read(12))
my_assert(t == (320, 3, 3*65536, after_offset + 16))
# f.write(pack(o+"L", 0)) # end of IFD
t = unpack(o+"L", f.read(4))
my_assert(t[0] == 0)
# "After" values pointed to by every IFD
# f.write(pack(o+"LL", 72, 1)) # x unit is 72/inch
# f.write(pack(o+"LL", 72, 1)) # y unit is 72/inch
t = unpack(o+"LL", f.read(8))
my_assert(t == (72,1))
t = unpack(o+"LL", f.read(8))
my_assert(t == (72,1))
# Read the palette here if there is one
# if has_palette:
# palette[0:65536,0:3].astype(o+"u2").transpose().tofile(f,sep="")
palette = None
if has_palette:
palette = np.fromfile(f, dtype=o+"u2", count=65536*3, sep="")
palette = palette.reshape((3,65536)).transpose()
# Otherwise, write the size of each RGB pixel if necessary
# elif is_rgb: # samples per pixel
# f.write(pack(o+"HHHH", 16, 16, 16, 0))
elif is_rgb:
t = unpack(o+"HHHH", f.read(8))
my_assert(t == (16, 16, 16, 0))
# Append data to output file
# array.astype(o+"u2").tofile(f, sep="")
items = width * height
if is_rgb:
items *= 3
array = np.fromfile(f, dtype=o+"u2", count=items, sep="")
# Interpret the shape of the image
# if array.ndim == 3:
# (height, width, bands) = array.shape
# else:
# (height, width) = array.shape
# bands = 1
# array = array.reshape((height, width, 1))
if is_rgb:
array = array.reshape(height, width, 3)
else:
array = array.reshape(height, width)
# Apply transpose operation if needed
if transpose == Image.Transpose.FLIP_LEFT_RIGHT:
array = np.fliplr(array)
if transpose == Image.Transpose.FLIP_TOP_BOTTOM:
array = np.flipud(array)
if transpose == Image.Transpose.ROTATE_90:
array = np.rot90(array,3)
if transpose == Image.Transpose.ROTATE_180:
array = np.rot90(array,2)
if transpose == Image.Transpose.ROTATE_270:
array = np.rot90(array,1)
# Flip if line numbers increase upward
# if up: array = np.flipud(array)
if up:
array = np.flipud(array)
return(array, palette)
def my_assert(test: bool) -> None:
if not test:
raise OSError("Not a recognized TIFF16 file.")