Read pages with ImageSequence.Iterator(Image.open(path)), write them with
pages[0].save(path, save_all=True, append_images=pages[1:], compression="tiff_lzw"). 8-bit, 16-bit greyscale
and float data round-trip through NumPy exactly. For 16-bit-per-channel colour TIFFs, use the
tifffile package: Pillow reduces them to 8 bits.
Read a multi-page TIFF into NumPy
import numpy as np
from PIL import Image, ImageSequence
with Image.open("stack.tif") as tif: # e.g. a microscope z-stack
print("pages:", tif.n_frames, " mode:", tif.mode, " size:", tif.size)
stack = np.stack([np.asarray(page) for page in ImageSequence.Iterator(tif)])
print(stack.shape, stack.dtype, "max per page:", stack.max(axis=(1, 2)).tolist())
pages: 5 mode: I;16 size: (256, 256)
(5, 256, 256) uint16 max per page: [4000, 5000, 6000, 7000, 8000]
Like animated GIFs, a multi-page TIFF opens on its first page; ImageSequence.Iterator (or
seek(i)) moves through the rest. Each page can have its own size and mode, so check before stacking.
Write a multi-page TIFF
from PIL import Image
pages = [Image.open("photo.jpg"), Image.open("portrait.jpg"), Image.open("logo.png")]
pages[0].save("bundle.tif", save_all=True, append_images=pages[1:], compression="tiff_lzw")
with Image.open("bundle.tif") as tif:
for i in range(tif.n_frames):
tif.seek(i)
print(f"page {i}: {tif.mode:<4} {tif.size}")
page 0: RGB (1600, 1067)
page 1: RGB (800, 1200)
page 2: RGBA (600, 360)
Compression
Without the compression argument, Pillow writes TIFFs uncompressed. The options compared on a photo and a
1-bit scan:
import io
from PIL import Image
photo = Image.open("photo.jpg")
scan = photo.convert("L").point(lambda v: 255 if v > 60 else 0, mode="1") # a 1-bit "scan"
def kb(img, compression):
buf = io.BytesIO()
img.save(buf, "TIFF", **({"compression": compression} if compression else {}))
return buf.tell() / 1024
print("photo (RGB):")
for c in [None, "packbits", "tiff_lzw", "tiff_adobe_deflate", "jpeg"]:
print(f" {str(c):<20} {kb(photo, c):7.0f} KB")
print("scan (1-bit):")
for c in [None, "packbits", "tiff_lzw", "tiff_adobe_deflate", "group4"]:
print(f" {str(c):<20} {kb(scan, c):7.0f} KB")
photo (RGB):
None 5002 KB
packbits 4285 KB
tiff_lzw 3129 KB
tiff_adobe_deflate 1887 KB
jpeg 311 KB
scan (1-bit):
None 209 KB
packbits 40 KB
tiff_lzw 22 KB
tiff_adobe_deflate 20 KB
group4 11 KB
tiff_adobe_deflate (zip) and tiff_lzw are lossless and read everywhere. jpeg is
lossy and only for 8-bit RGB or greyscale photos. group4 (and the older group3) is the fax
standard for 1-bit scans and compresses them far better than anything else. Match the compression to the mode: use the
fax codecs only on mode 1 images and jpeg only on RGB or L, converting
first if necessary. Pillow does not reliably reject other combinations with a clean error.
16-bit and float data
import numpy as np
from PIL import Image
u16 = np.linspace(0, 65535, 200 * 300).astype(np.uint16).reshape(200, 300)
f32 = np.random.default_rng(0).normal(size=(200, 300)).astype(np.float32)
Image.fromarray(u16).save("u16.tif")
Image.fromarray(f32).save("f32.tif")
for name, original in [("u16.tif", u16), ("f32.tif", f32)]:
img = Image.open(name)
back = np.asarray(img)
print(f"{name}: mode {img.mode:<5} dtype {back.dtype} identical: {np.array_equal(back, original)}")
u16.tif: mode I;16 dtype uint16 identical: True
f32.tif: mode F dtype float32 identical: True
Single-channel uint16 and float32 arrays survive the round trip bit for bit, as modes
I;16 and F. Do not call convert("L") on them to view them: it clips instead of scaling
(see cannot write mode); scale explicitly, as in the TIFF-to-JPEG example
below.
The silent problem: 16-bit colour (48-bit RGB) TIFFs
Scanners, RAW converters and scientific cameras often write colour TIFFs with 16 bits per channel. Pillow has no mode for that, so it opens them as ordinary 8-bit RGB, with no warning:
import numpy as np
import tifffile
from PIL import Image
rgb16 = np.random.default_rng(0).integers(0, 65536, (100, 150, 3), dtype=np.uint16) # 16 bits per channel
tifffile.imwrite("rgb48.tif", rgb16)
via_pillow = np.asarray(Image.open("rgb48.tif"))
via_tifffile = tifffile.imread("rgb48.tif")
print("Pillow: ", Image.open("rgb48.tif").mode, via_pillow.dtype, "max", via_pillow.max())
print("tifffile:", via_tifffile.dtype, "max", via_tifffile.max(), " identical:", np.array_equal(via_tifffile, rgb16))
Pillow: RGB uint8 max 255
tifffile: uint16 max 65535 identical: True
The extra precision is gone as soon as the file is opened. If your TIFFs might be 48-bit, read them with
tifffile (or imageio, which uses it) and only hand data to Pillow once it is 8-bit.
Convert TIFF to JPEG
For 8-bit TIFFs, Image.open(path).convert("RGB").save(out) is enough. 16-bit pages need scaling to 0–255
first; stretching between percentiles gives a usable preview of scientific data:
import numpy as np
from PIL import Image, ImageSequence
with Image.open("stack.tif") as tif:
for i, page in enumerate(ImageSequence.Iterator(tif)):
a = np.asarray(page).astype(np.float32)
lo, hi = np.percentile(a, (0.5, 99.5)) # stretch contrast, ignore outliers
out = np.clip((a - lo) / (hi - lo) * 255, 0, 255).astype(np.uint8)
Image.fromarray(out).save(f"page_{i}.jpg", quality=90)
print(f"page {i}: {page.mode} range {int(a.min())}-{int(a.max())} -> L 0-255")
page 0: I;16 range 0-4000 -> L 0-255
page 1: I;16 range 0-5000 -> L 0-255
page 2: I;16 range 0-6000 -> L 0-255
page 3: I;16 range 0-7000 -> L 0-255
page 4: I;16 range 0-8000 -> L 0-255
Related
The outputs and images on this page are the real results of running every example with Pillow 12.3.0 on Python 3.13 (Linux) on 2026-10-15. Every example is also re-run automatically on macOS (Darwin 25.6.0, Pillow 12.3.0), Linux (Linux 6.17.0-1022-azure, Pillow 12.3.0), Windows (Windows 2025Server, Pillow 12.3.0); the line under each example shows the result per operating system. The DejaVu Sans font files sit next to each script so font examples behave the same everywhere. Sample photos: NASA/Sam Lott, public domain. Spotted something wrong? Tell us.