Spaces:
Sleeping
Sleeping
# | |
# The Python Imaging Library. | |
# $Id$ | |
# | |
# TIFF file handling | |
# | |
# TIFF is a flexible, if somewhat aged, image file format originally | |
# defined by Aldus. Although TIFF supports a wide variety of pixel | |
# layouts and compression methods, the name doesn't really stand for | |
# "thousands of incompatible file formats," it just feels that way. | |
# | |
# To read TIFF data from a stream, the stream must be seekable. For | |
# progressive decoding, make sure to use TIFF files where the tag | |
# directory is placed first in the file. | |
# | |
# History: | |
# 1995-09-01 fl Created | |
# 1996-05-04 fl Handle JPEGTABLES tag | |
# 1996-05-18 fl Fixed COLORMAP support | |
# 1997-01-05 fl Fixed PREDICTOR support | |
# 1997-08-27 fl Added support for rational tags (from Perry Stoll) | |
# 1998-01-10 fl Fixed seek/tell (from Jan Blom) | |
# 1998-07-15 fl Use private names for internal variables | |
# 1999-06-13 fl Rewritten for PIL 1.0 (1.0) | |
# 2000-10-11 fl Additional fixes for Python 2.0 (1.1) | |
# 2001-04-17 fl Fixed rewind support (seek to frame 0) (1.2) | |
# 2001-05-12 fl Added write support for more tags (from Greg Couch) (1.3) | |
# 2001-12-18 fl Added workaround for broken Matrox library | |
# 2002-01-18 fl Don't mess up if photometric tag is missing (D. Alan Stewart) | |
# 2003-05-19 fl Check FILLORDER tag | |
# 2003-09-26 fl Added RGBa support | |
# 2004-02-24 fl Added DPI support; fixed rational write support | |
# 2005-02-07 fl Added workaround for broken Corel Draw 10 files | |
# 2006-01-09 fl Added support for float/double tags (from Russell Nelson) | |
# | |
# Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved. | |
# Copyright (c) 1995-1997 by Fredrik Lundh | |
# | |
# See the README file for information on usage and redistribution. | |
# | |
from __future__ import annotations | |
import io | |
import itertools | |
import logging | |
import math | |
import os | |
import struct | |
import warnings | |
from collections.abc import MutableMapping | |
from fractions import Fraction | |
from numbers import Number, Rational | |
from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn | |
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags | |
from ._binary import i16be as i16 | |
from ._binary import i32be as i32 | |
from ._binary import o8 | |
from ._deprecate import deprecate | |
from .TiffTags import TYPES | |
logger = logging.getLogger(__name__) | |
# Set these to true to force use of libtiff for reading or writing. | |
READ_LIBTIFF = False | |
WRITE_LIBTIFF = False | |
IFD_LEGACY_API = True | |
STRIP_SIZE = 65536 | |
II = b"II" # little-endian (Intel style) | |
MM = b"MM" # big-endian (Motorola style) | |
# | |
# -------------------------------------------------------------------- | |
# Read TIFF files | |
# a few tag names, just to make the code below a bit more readable | |
OSUBFILETYPE = 255 | |
IMAGEWIDTH = 256 | |
IMAGELENGTH = 257 | |
BITSPERSAMPLE = 258 | |
COMPRESSION = 259 | |
PHOTOMETRIC_INTERPRETATION = 262 | |
FILLORDER = 266 | |
IMAGEDESCRIPTION = 270 | |
STRIPOFFSETS = 273 | |
SAMPLESPERPIXEL = 277 | |
ROWSPERSTRIP = 278 | |
STRIPBYTECOUNTS = 279 | |
X_RESOLUTION = 282 | |
Y_RESOLUTION = 283 | |
PLANAR_CONFIGURATION = 284 | |
RESOLUTION_UNIT = 296 | |
TRANSFERFUNCTION = 301 | |
SOFTWARE = 305 | |
DATE_TIME = 306 | |
ARTIST = 315 | |
PREDICTOR = 317 | |
COLORMAP = 320 | |
TILEWIDTH = 322 | |
TILELENGTH = 323 | |
TILEOFFSETS = 324 | |
TILEBYTECOUNTS = 325 | |
SUBIFD = 330 | |
EXTRASAMPLES = 338 | |
SAMPLEFORMAT = 339 | |
JPEGTABLES = 347 | |
YCBCRSUBSAMPLING = 530 | |
REFERENCEBLACKWHITE = 532 | |
COPYRIGHT = 33432 | |
IPTC_NAA_CHUNK = 33723 # newsphoto properties | |
PHOTOSHOP_CHUNK = 34377 # photoshop properties | |
ICCPROFILE = 34675 | |
EXIFIFD = 34665 | |
XMP = 700 | |
JPEGQUALITY = 65537 # pseudo-tag by libtiff | |
# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java | |
IMAGEJ_META_DATA_BYTE_COUNTS = 50838 | |
IMAGEJ_META_DATA = 50839 | |
COMPRESSION_INFO = { | |
# Compression => pil compression name | |
1: "raw", | |
2: "tiff_ccitt", | |
3: "group3", | |
4: "group4", | |
5: "tiff_lzw", | |
6: "tiff_jpeg", # obsolete | |
7: "jpeg", | |
8: "tiff_adobe_deflate", | |
32771: "tiff_raw_16", # 16-bit padding | |
32773: "packbits", | |
32809: "tiff_thunderscan", | |
32946: "tiff_deflate", | |
34676: "tiff_sgilog", | |
34677: "tiff_sgilog24", | |
34925: "lzma", | |
50000: "zstd", | |
50001: "webp", | |
} | |
COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()} | |
OPEN_INFO = { | |
# (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, | |
# ExtraSamples) => mode, rawmode | |
(II, 0, (1,), 1, (1,), ()): ("1", "1;I"), | |
(MM, 0, (1,), 1, (1,), ()): ("1", "1;I"), | |
(II, 0, (1,), 2, (1,), ()): ("1", "1;IR"), | |
(MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"), | |
(II, 1, (1,), 1, (1,), ()): ("1", "1"), | |
(MM, 1, (1,), 1, (1,), ()): ("1", "1"), | |
(II, 1, (1,), 2, (1,), ()): ("1", "1;R"), | |
(MM, 1, (1,), 2, (1,), ()): ("1", "1;R"), | |
(II, 0, (1,), 1, (2,), ()): ("L", "L;2I"), | |
(MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"), | |
(II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), | |
(MM, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), | |
(II, 1, (1,), 1, (2,), ()): ("L", "L;2"), | |
(MM, 1, (1,), 1, (2,), ()): ("L", "L;2"), | |
(II, 1, (1,), 2, (2,), ()): ("L", "L;2R"), | |
(MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"), | |
(II, 0, (1,), 1, (4,), ()): ("L", "L;4I"), | |
(MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"), | |
(II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), | |
(MM, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), | |
(II, 1, (1,), 1, (4,), ()): ("L", "L;4"), | |
(MM, 1, (1,), 1, (4,), ()): ("L", "L;4"), | |
(II, 1, (1,), 2, (4,), ()): ("L", "L;4R"), | |
(MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"), | |
(II, 0, (1,), 1, (8,), ()): ("L", "L;I"), | |
(MM, 0, (1,), 1, (8,), ()): ("L", "L;I"), | |
(II, 0, (1,), 2, (8,), ()): ("L", "L;IR"), | |
(MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), | |
(II, 1, (1,), 1, (8,), ()): ("L", "L"), | |
(MM, 1, (1,), 1, (8,), ()): ("L", "L"), | |
(II, 1, (2,), 1, (8,), ()): ("L", "L"), | |
(MM, 1, (2,), 1, (8,), ()): ("L", "L"), | |
(II, 1, (1,), 2, (8,), ()): ("L", "L;R"), | |
(MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), | |
(II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), | |
(II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"), | |
(II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), | |
(MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), | |
(II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"), | |
(II, 1, (2,), 1, (16,), ()): ("I", "I;16S"), | |
(MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"), | |
(II, 0, (3,), 1, (32,), ()): ("F", "F;32F"), | |
(MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"), | |
(II, 1, (1,), 1, (32,), ()): ("I", "I;32N"), | |
(II, 1, (2,), 1, (32,), ()): ("I", "I;32S"), | |
(MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"), | |
(II, 1, (3,), 1, (32,), ()): ("F", "F;32F"), | |
(MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"), | |
(II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), | |
(MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), | |
(II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), | |
(MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), | |
(II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), | |
(MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), | |
(II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples | |
(MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples | |
(II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"), | |
(MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"), | |
(II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"), | |
(MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"), | |
(II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"), | |
(MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"), | |
(II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), | |
(MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), | |
(II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), | |
(MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), | |
(II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), | |
(MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), | |
(II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), | |
(MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), | |
(II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), | |
(MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), | |
(II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), | |
(MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), | |
(II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 | |
(MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 | |
(II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"), | |
(MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"), | |
(II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"), | |
(MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"), | |
(II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16L"), | |
(MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16B"), | |
(II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"), | |
(MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"), | |
(II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"), | |
(MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"), | |
(II, 3, (1,), 1, (1,), ()): ("P", "P;1"), | |
(MM, 3, (1,), 1, (1,), ()): ("P", "P;1"), | |
(II, 3, (1,), 2, (1,), ()): ("P", "P;1R"), | |
(MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"), | |
(II, 3, (1,), 1, (2,), ()): ("P", "P;2"), | |
(MM, 3, (1,), 1, (2,), ()): ("P", "P;2"), | |
(II, 3, (1,), 2, (2,), ()): ("P", "P;2R"), | |
(MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"), | |
(II, 3, (1,), 1, (4,), ()): ("P", "P;4"), | |
(MM, 3, (1,), 1, (4,), ()): ("P", "P;4"), | |
(II, 3, (1,), 2, (4,), ()): ("P", "P;4R"), | |
(MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"), | |
(II, 3, (1,), 1, (8,), ()): ("P", "P"), | |
(MM, 3, (1,), 1, (8,), ()): ("P", "P"), | |
(II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"), | |
(II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), | |
(MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), | |
(II, 3, (1,), 2, (8,), ()): ("P", "P;R"), | |
(MM, 3, (1,), 2, (8,), ()): ("P", "P;R"), | |
(II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), | |
(MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), | |
(II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), | |
(MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), | |
(II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), | |
(MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), | |
(II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"), | |
(II, 6, (1,), 1, (8,), ()): ("L", "L"), | |
(MM, 6, (1,), 1, (8,), ()): ("L", "L"), | |
# JPEG compressed images handled by LibTiff and auto-converted to RGBX | |
# Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel | |
(II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), | |
(MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), | |
(II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), | |
(MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), | |
} | |
MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO) | |
PREFIXES = [ | |
b"MM\x00\x2A", # Valid TIFF header with big-endian byte order | |
b"II\x2A\x00", # Valid TIFF header with little-endian byte order | |
b"MM\x2A\x00", # Invalid TIFF header, assume big-endian | |
b"II\x00\x2A", # Invalid TIFF header, assume little-endian | |
b"MM\x00\x2B", # BigTIFF with big-endian byte order | |
b"II\x2B\x00", # BigTIFF with little-endian byte order | |
] | |
if not getattr(Image.core, "libtiff_support_custom_tags", True): | |
deprecate("Support for LibTIFF earlier than version 4", 12) | |
def _accept(prefix: bytes) -> bool: | |
return prefix[:4] in PREFIXES | |
def _limit_rational(val, max_val): | |
inv = abs(val) > 1 | |
n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) | |
return n_d[::-1] if inv else n_d | |
def _limit_signed_rational(val, max_val, min_val): | |
frac = Fraction(val) | |
n_d = frac.numerator, frac.denominator | |
if min(n_d) < min_val: | |
n_d = _limit_rational(val, abs(min_val)) | |
if max(n_d) > max_val: | |
val = Fraction(*n_d) | |
n_d = _limit_rational(val, max_val) | |
return n_d | |
## | |
# Wrapper for TIFF IFDs. | |
_load_dispatch = {} | |
_write_dispatch = {} | |
def _delegate(op): | |
def delegate(self, *args): | |
return getattr(self._val, op)(*args) | |
return delegate | |
class IFDRational(Rational): | |
"""Implements a rational class where 0/0 is a legal value to match | |
the in the wild use of exif rationals. | |
e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used | |
""" | |
""" If the denominator is 0, store this as a float('nan'), otherwise store | |
as a fractions.Fraction(). Delegate as appropriate | |
""" | |
__slots__ = ("_numerator", "_denominator", "_val") | |
def __init__(self, value, denominator=1): | |
""" | |
:param value: either an integer numerator, a | |
float/rational/other number, or an IFDRational | |
:param denominator: Optional integer denominator | |
""" | |
if isinstance(value, IFDRational): | |
self._numerator = value.numerator | |
self._denominator = value.denominator | |
self._val = value._val | |
return | |
if isinstance(value, Fraction): | |
self._numerator = value.numerator | |
self._denominator = value.denominator | |
else: | |
self._numerator = value | |
self._denominator = denominator | |
if denominator == 0: | |
self._val = float("nan") | |
elif denominator == 1: | |
self._val = Fraction(value) | |
else: | |
self._val = Fraction(value, denominator) | |
def numerator(self): | |
return self._numerator | |
def denominator(self): | |
return self._denominator | |
def limit_rational(self, max_denominator): | |
""" | |
:param max_denominator: Integer, the maximum denominator value | |
:returns: Tuple of (numerator, denominator) | |
""" | |
if self.denominator == 0: | |
return self.numerator, self.denominator | |
f = self._val.limit_denominator(max_denominator) | |
return f.numerator, f.denominator | |
def __repr__(self) -> str: | |
return str(float(self._val)) | |
def __hash__(self) -> int: | |
return self._val.__hash__() | |
def __eq__(self, other: object) -> bool: | |
val = self._val | |
if isinstance(other, IFDRational): | |
other = other._val | |
if isinstance(other, float): | |
val = float(val) | |
return val == other | |
def __getstate__(self): | |
return [self._val, self._numerator, self._denominator] | |
def __setstate__(self, state): | |
IFDRational.__init__(self, 0) | |
_val, _numerator, _denominator = state | |
self._val = _val | |
self._numerator = _numerator | |
self._denominator = _denominator | |
""" a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', | |
'truediv', 'rtruediv', 'floordiv', 'rfloordiv', | |
'mod','rmod', 'pow','rpow', 'pos', 'neg', | |
'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool', | |
'ceil', 'floor', 'round'] | |
print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a)) | |
""" | |
__add__ = _delegate("__add__") | |
__radd__ = _delegate("__radd__") | |
__sub__ = _delegate("__sub__") | |
__rsub__ = _delegate("__rsub__") | |
__mul__ = _delegate("__mul__") | |
__rmul__ = _delegate("__rmul__") | |
__truediv__ = _delegate("__truediv__") | |
__rtruediv__ = _delegate("__rtruediv__") | |
__floordiv__ = _delegate("__floordiv__") | |
__rfloordiv__ = _delegate("__rfloordiv__") | |
__mod__ = _delegate("__mod__") | |
__rmod__ = _delegate("__rmod__") | |
__pow__ = _delegate("__pow__") | |
__rpow__ = _delegate("__rpow__") | |
__pos__ = _delegate("__pos__") | |
__neg__ = _delegate("__neg__") | |
__abs__ = _delegate("__abs__") | |
__trunc__ = _delegate("__trunc__") | |
__lt__ = _delegate("__lt__") | |
__gt__ = _delegate("__gt__") | |
__le__ = _delegate("__le__") | |
__ge__ = _delegate("__ge__") | |
__bool__ = _delegate("__bool__") | |
__ceil__ = _delegate("__ceil__") | |
__floor__ = _delegate("__floor__") | |
__round__ = _delegate("__round__") | |
# Python >= 3.11 | |
if hasattr(Fraction, "__int__"): | |
__int__ = _delegate("__int__") | |
def _register_loader(idx, size): | |
def decorator(func): | |
from .TiffTags import TYPES | |
if func.__name__.startswith("load_"): | |
TYPES[idx] = func.__name__[5:].replace("_", " ") | |
_load_dispatch[idx] = size, func # noqa: F821 | |
return func | |
return decorator | |
def _register_writer(idx): | |
def decorator(func): | |
_write_dispatch[idx] = func # noqa: F821 | |
return func | |
return decorator | |
def _register_basic(idx_fmt_name): | |
from .TiffTags import TYPES | |
idx, fmt, name = idx_fmt_name | |
TYPES[idx] = name | |
size = struct.calcsize(f"={fmt}") | |
_load_dispatch[idx] = ( # noqa: F821 | |
size, | |
lambda self, data, legacy_api=True: ( | |
self._unpack(f"{len(data) // size}{fmt}", data) | |
), | |
) | |
_write_dispatch[idx] = lambda self, *values: ( # noqa: F821 | |
b"".join(self._pack(fmt, value) for value in values) | |
) | |
if TYPE_CHECKING: | |
_IFDv2Base = MutableMapping[int, Any] | |
else: | |
_IFDv2Base = MutableMapping | |
class ImageFileDirectory_v2(_IFDv2Base): | |
"""This class represents a TIFF tag directory. To speed things up, we | |
don't decode tags unless they're asked for. | |
Exposes a dictionary interface of the tags in the directory:: | |
ifd = ImageFileDirectory_v2() | |
ifd[key] = 'Some Data' | |
ifd.tagtype[key] = TiffTags.ASCII | |
print(ifd[key]) | |
'Some Data' | |
Individual values are returned as the strings or numbers, sequences are | |
returned as tuples of the values. | |
The tiff metadata type of each item is stored in a dictionary of | |
tag types in | |
:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types | |
are read from a tiff file, guessed from the type added, or added | |
manually. | |
Data Structures: | |
* ``self.tagtype = {}`` | |
* Key: numerical TIFF tag number | |
* Value: integer corresponding to the data type from | |
:py:data:`.TiffTags.TYPES` | |
.. versionadded:: 3.0.0 | |
'Internal' data structures: | |
* ``self._tags_v2 = {}`` | |
* Key: numerical TIFF tag number | |
* Value: decoded data, as tuple for multiple values | |
* ``self._tagdata = {}`` | |
* Key: numerical TIFF tag number | |
* Value: undecoded byte string from file | |
* ``self._tags_v1 = {}`` | |
* Key: numerical TIFF tag number | |
* Value: decoded data in the v1 format | |
Tags will be found in the private attributes ``self._tagdata``, and in | |
``self._tags_v2`` once decoded. | |
``self.legacy_api`` is a value for internal use, and shouldn't be changed | |
from outside code. In cooperation with | |
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api`` | |
is true, then decoded tags will be populated into both ``_tags_v1`` and | |
``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF | |
save routine. Tags should be read from ``_tags_v1`` if | |
``legacy_api == true``. | |
""" | |
_load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} | |
_write_dispatch: dict[int, Callable[..., Any]] = {} | |
def __init__( | |
self, | |
ifh: bytes = b"II\052\0\0\0\0\0", | |
prefix: bytes | None = None, | |
group: int | None = None, | |
) -> None: | |
"""Initialize an ImageFileDirectory. | |
To construct an ImageFileDirectory from a real file, pass the 8-byte | |
magic header to the constructor. To only set the endianness, pass it | |
as the 'prefix' keyword argument. | |
:param ifh: One of the accepted magic headers (cf. PREFIXES); also sets | |
endianness. | |
:param prefix: Override the endianness of the file. | |
""" | |
if not _accept(ifh): | |
msg = f"not a TIFF file (header {repr(ifh)} not valid)" | |
raise SyntaxError(msg) | |
self._prefix = prefix if prefix is not None else ifh[:2] | |
if self._prefix == MM: | |
self._endian = ">" | |
elif self._prefix == II: | |
self._endian = "<" | |
else: | |
msg = "not a TIFF IFD" | |
raise SyntaxError(msg) | |
self._bigtiff = ifh[2] == 43 | |
self.group = group | |
self.tagtype: dict[int, int] = {} | |
""" Dictionary of tag types """ | |
self.reset() | |
(self.next,) = ( | |
self._unpack("Q", ifh[8:]) if self._bigtiff else self._unpack("L", ifh[4:]) | |
) | |
self._legacy_api = False | |
prefix = property(lambda self: self._prefix) | |
offset = property(lambda self: self._offset) | |
def legacy_api(self) -> bool: | |
return self._legacy_api | |
def legacy_api(self, value: bool) -> NoReturn: | |
msg = "Not allowing setting of legacy api" | |
raise Exception(msg) | |
def reset(self) -> None: | |
self._tags_v1: dict[int, Any] = {} # will remain empty if legacy_api is false | |
self._tags_v2: dict[int, Any] = {} # main tag storage | |
self._tagdata: dict[int, bytes] = {} | |
self.tagtype = {} # added 2008-06-05 by Florian Hoech | |
self._next = None | |
self._offset = None | |
def __str__(self) -> str: | |
return str(dict(self)) | |
def named(self): | |
""" | |
:returns: dict of name|key: value | |
Returns the complete tag dictionary, with named tags where possible. | |
""" | |
return { | |
TiffTags.lookup(code, self.group).name: value | |
for code, value in self.items() | |
} | |
def __len__(self) -> int: | |
return len(set(self._tagdata) | set(self._tags_v2)) | |
def __getitem__(self, tag): | |
if tag not in self._tags_v2: # unpack on the fly | |
data = self._tagdata[tag] | |
typ = self.tagtype[tag] | |
size, handler = self._load_dispatch[typ] | |
self[tag] = handler(self, data, self.legacy_api) # check type | |
val = self._tags_v2[tag] | |
if self.legacy_api and not isinstance(val, (tuple, bytes)): | |
val = (val,) | |
return val | |
def __contains__(self, tag): | |
return tag in self._tags_v2 or tag in self._tagdata | |
def __setitem__(self, tag, value): | |
self._setitem(tag, value, self.legacy_api) | |
def _setitem(self, tag, value, legacy_api): | |
basetypes = (Number, bytes, str) | |
info = TiffTags.lookup(tag, self.group) | |
values = [value] if isinstance(value, basetypes) else value | |
if tag not in self.tagtype: | |
if info.type: | |
self.tagtype[tag] = info.type | |
else: | |
self.tagtype[tag] = TiffTags.UNDEFINED | |
if all(isinstance(v, IFDRational) for v in values): | |
self.tagtype[tag] = ( | |
TiffTags.RATIONAL | |
if all(v >= 0 for v in values) | |
else TiffTags.SIGNED_RATIONAL | |
) | |
elif all(isinstance(v, int) for v in values): | |
if all(0 <= v < 2**16 for v in values): | |
self.tagtype[tag] = TiffTags.SHORT | |
elif all(-(2**15) < v < 2**15 for v in values): | |
self.tagtype[tag] = TiffTags.SIGNED_SHORT | |
else: | |
self.tagtype[tag] = ( | |
TiffTags.LONG | |
if all(v >= 0 for v in values) | |
else TiffTags.SIGNED_LONG | |
) | |
elif all(isinstance(v, float) for v in values): | |
self.tagtype[tag] = TiffTags.DOUBLE | |
elif all(isinstance(v, str) for v in values): | |
self.tagtype[tag] = TiffTags.ASCII | |
elif all(isinstance(v, bytes) for v in values): | |
self.tagtype[tag] = TiffTags.BYTE | |
if self.tagtype[tag] == TiffTags.UNDEFINED: | |
values = [ | |
v.encode("ascii", "replace") if isinstance(v, str) else v | |
for v in values | |
] | |
elif self.tagtype[tag] == TiffTags.RATIONAL: | |
values = [float(v) if isinstance(v, int) else v for v in values] | |
is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict) | |
if not is_ifd: | |
values = tuple(info.cvt_enum(value) for value in values) | |
dest = self._tags_v1 if legacy_api else self._tags_v2 | |
# Three branches: | |
# Spec'd length == 1, Actual length 1, store as element | |
# Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed. | |
# No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple. | |
# Don't mess with the legacy api, since it's frozen. | |
if not is_ifd and ( | |
(info.length == 1) | |
or self.tagtype[tag] == TiffTags.BYTE | |
or (info.length is None and len(values) == 1 and not legacy_api) | |
): | |
# Don't mess with the legacy api, since it's frozen. | |
if legacy_api and self.tagtype[tag] in [ | |
TiffTags.RATIONAL, | |
TiffTags.SIGNED_RATIONAL, | |
]: # rationals | |
values = (values,) | |
try: | |
(dest[tag],) = values | |
except ValueError: | |
# We've got a builtin tag with 1 expected entry | |
warnings.warn( | |
f"Metadata Warning, tag {tag} had too many entries: " | |
f"{len(values)}, expected 1" | |
) | |
dest[tag] = values[0] | |
else: | |
# Spec'd length > 1 or undefined | |
# Unspec'd, and length > 1 | |
dest[tag] = values | |
def __delitem__(self, tag: int) -> None: | |
self._tags_v2.pop(tag, None) | |
self._tags_v1.pop(tag, None) | |
self._tagdata.pop(tag, None) | |
def __iter__(self): | |
return iter(set(self._tagdata) | set(self._tags_v2)) | |
def _unpack(self, fmt, data): | |
return struct.unpack(self._endian + fmt, data) | |
def _pack(self, fmt, *values): | |
return struct.pack(self._endian + fmt, *values) | |
list( | |
map( | |
_register_basic, | |
[ | |
(TiffTags.SHORT, "H", "short"), | |
(TiffTags.LONG, "L", "long"), | |
(TiffTags.SIGNED_BYTE, "b", "signed byte"), | |
(TiffTags.SIGNED_SHORT, "h", "signed short"), | |
(TiffTags.SIGNED_LONG, "l", "signed long"), | |
(TiffTags.FLOAT, "f", "float"), | |
(TiffTags.DOUBLE, "d", "double"), | |
(TiffTags.IFD, "L", "long"), | |
(TiffTags.LONG8, "Q", "long8"), | |
], | |
) | |
) | |
# Basic type, except for the legacy API. | |
def load_byte(self, data, legacy_api=True): | |
return data | |
# Basic type, except for the legacy API. | |
def write_byte(self, data): | |
if isinstance(data, IFDRational): | |
data = int(data) | |
if isinstance(data, int): | |
data = bytes((data,)) | |
return data | |
def load_string(self, data, legacy_api=True): | |
if data.endswith(b"\0"): | |
data = data[:-1] | |
return data.decode("latin-1", "replace") | |
def write_string(self, value): | |
# remerge of https://github.com/python-pillow/Pillow/pull/1416 | |
if isinstance(value, int): | |
value = str(value) | |
if not isinstance(value, bytes): | |
value = value.encode("ascii", "replace") | |
return value + b"\0" | |
def load_rational(self, data, legacy_api=True): | |
vals = self._unpack(f"{len(data) // 4}L", data) | |
def combine(a, b): | |
return (a, b) if legacy_api else IFDRational(a, b) | |
return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) | |
def write_rational(self, *values): | |
return b"".join( | |
self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values | |
) | |
def load_undefined(self, data, legacy_api=True): | |
return data | |
def write_undefined(self, value): | |
if isinstance(value, IFDRational): | |
value = int(value) | |
if isinstance(value, int): | |
value = str(value).encode("ascii", "replace") | |
return value | |
def load_signed_rational(self, data, legacy_api=True): | |
vals = self._unpack(f"{len(data) // 4}l", data) | |
def combine(a, b): | |
return (a, b) if legacy_api else IFDRational(a, b) | |
return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) | |
def write_signed_rational(self, *values): | |
return b"".join( | |
self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) | |
for frac in values | |
) | |
def _ensure_read(self, fp, size): | |
ret = fp.read(size) | |
if len(ret) != size: | |
msg = ( | |
"Corrupt EXIF data. " | |
f"Expecting to read {size} bytes but only got {len(ret)}. " | |
) | |
raise OSError(msg) | |
return ret | |
def load(self, fp): | |
self.reset() | |
self._offset = fp.tell() | |
try: | |
tag_count = ( | |
self._unpack("Q", self._ensure_read(fp, 8)) | |
if self._bigtiff | |
else self._unpack("H", self._ensure_read(fp, 2)) | |
)[0] | |
for i in range(tag_count): | |
tag, typ, count, data = ( | |
self._unpack("HHQ8s", self._ensure_read(fp, 20)) | |
if self._bigtiff | |
else self._unpack("HHL4s", self._ensure_read(fp, 12)) | |
) | |
tagname = TiffTags.lookup(tag, self.group).name | |
typname = TYPES.get(typ, "unknown") | |
msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})" | |
try: | |
unit_size, handler = self._load_dispatch[typ] | |
except KeyError: | |
logger.debug("%s - unsupported type %s", msg, typ) | |
continue # ignore unsupported type | |
size = count * unit_size | |
if size > (8 if self._bigtiff else 4): | |
here = fp.tell() | |
(offset,) = self._unpack("Q" if self._bigtiff else "L", data) | |
msg += f" Tag Location: {here} - Data Location: {offset}" | |
fp.seek(offset) | |
data = ImageFile._safe_read(fp, size) | |
fp.seek(here) | |
else: | |
data = data[:size] | |
if len(data) != size: | |
warnings.warn( | |
"Possibly corrupt EXIF data. " | |
f"Expecting to read {size} bytes but only got {len(data)}." | |
f" Skipping tag {tag}" | |
) | |
logger.debug(msg) | |
continue | |
if not data: | |
logger.debug(msg) | |
continue | |
self._tagdata[tag] = data | |
self.tagtype[tag] = typ | |
msg += " - value: " + ( | |
"<table: %d bytes>" % size if size > 32 else repr(data) | |
) | |
logger.debug(msg) | |
(self.next,) = ( | |
self._unpack("Q", self._ensure_read(fp, 8)) | |
if self._bigtiff | |
else self._unpack("L", self._ensure_read(fp, 4)) | |
) | |
except OSError as msg: | |
warnings.warn(str(msg)) | |
return | |
def tobytes(self, offset=0): | |
# FIXME What about tagdata? | |
result = self._pack("H", len(self._tags_v2)) | |
entries = [] | |
offset = offset + len(result) + len(self._tags_v2) * 12 + 4 | |
stripoffsets = None | |
# pass 1: convert tags to binary format | |
# always write tags in ascending order | |
for tag, value in sorted(self._tags_v2.items()): | |
if tag == STRIPOFFSETS: | |
stripoffsets = len(entries) | |
typ = self.tagtype.get(tag) | |
logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) | |
is_ifd = typ == TiffTags.LONG and isinstance(value, dict) | |
if is_ifd: | |
if self._endian == "<": | |
ifh = b"II\x2A\x00\x08\x00\x00\x00" | |
else: | |
ifh = b"MM\x00\x2A\x00\x00\x00\x08" | |
ifd = ImageFileDirectory_v2(ifh, group=tag) | |
values = self._tags_v2[tag] | |
for ifd_tag, ifd_value in values.items(): | |
ifd[ifd_tag] = ifd_value | |
data = ifd.tobytes(offset) | |
else: | |
values = value if isinstance(value, tuple) else (value,) | |
data = self._write_dispatch[typ](self, *values) | |
tagname = TiffTags.lookup(tag, self.group).name | |
typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") | |
msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})" | |
msg += " - value: " + ( | |
"<table: %d bytes>" % len(data) if len(data) >= 16 else str(values) | |
) | |
logger.debug(msg) | |
# count is sum of lengths for string and arbitrary data | |
if is_ifd: | |
count = 1 | |
elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]: | |
count = len(data) | |
else: | |
count = len(values) | |
# figure out if data fits into the entry | |
if len(data) <= 4: | |
entries.append((tag, typ, count, data.ljust(4, b"\0"), b"")) | |
else: | |
entries.append((tag, typ, count, self._pack("L", offset), data)) | |
offset += (len(data) + 1) // 2 * 2 # pad to word | |
# update strip offset data to point beyond auxiliary data | |
if stripoffsets is not None: | |
tag, typ, count, value, data = entries[stripoffsets] | |
if data: | |
msg = "multistrip support not yet implemented" | |
raise NotImplementedError(msg) | |
value = self._pack("L", self._unpack("L", value)[0] + offset) | |
entries[stripoffsets] = tag, typ, count, value, data | |
# pass 2: write entries to file | |
for tag, typ, count, value, data in entries: | |
logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data)) | |
result += self._pack("HHL4s", tag, typ, count, value) | |
# -- overwrite here for multi-page -- | |
result += b"\0\0\0\0" # end of entries | |
# pass 3: write auxiliary data to file | |
for tag, typ, count, value, data in entries: | |
result += data | |
if len(data) & 1: | |
result += b"\0" | |
return result | |
def save(self, fp): | |
if fp.tell() == 0: # skip TIFF header on subsequent pages | |
# tiff header -- PIL always starts the first IFD at offset 8 | |
fp.write(self._prefix + self._pack("HL", 42, 8)) | |
offset = fp.tell() | |
result = self.tobytes(offset) | |
fp.write(result) | |
return offset + len(result) | |
ImageFileDirectory_v2._load_dispatch = _load_dispatch | |
ImageFileDirectory_v2._write_dispatch = _write_dispatch | |
for idx, name in TYPES.items(): | |
name = name.replace(" ", "_") | |
setattr(ImageFileDirectory_v2, f"load_{name}", _load_dispatch[idx][1]) | |
setattr(ImageFileDirectory_v2, f"write_{name}", _write_dispatch[idx]) | |
del _load_dispatch, _write_dispatch, idx, name | |
# Legacy ImageFileDirectory support. | |
class ImageFileDirectory_v1(ImageFileDirectory_v2): | |
"""This class represents the **legacy** interface to a TIFF tag directory. | |
Exposes a dictionary interface of the tags in the directory:: | |
ifd = ImageFileDirectory_v1() | |
ifd[key] = 'Some Data' | |
ifd.tagtype[key] = TiffTags.ASCII | |
print(ifd[key]) | |
('Some Data',) | |
Also contains a dictionary of tag types as read from the tiff image file, | |
:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`. | |
Values are returned as a tuple. | |
.. deprecated:: 3.0.0 | |
""" | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
self._legacy_api = True | |
tags = property(lambda self: self._tags_v1) | |
tagdata = property(lambda self: self._tagdata) | |
# defined in ImageFileDirectory_v2 | |
tagtype: dict[int, int] | |
"""Dictionary of tag types""" | |
def from_v2(cls, original): | |
"""Returns an | |
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` | |
instance with the same data as is contained in the original | |
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` | |
instance. | |
:returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` | |
""" | |
ifd = cls(prefix=original.prefix) | |
ifd._tagdata = original._tagdata | |
ifd.tagtype = original.tagtype | |
ifd.next = original.next # an indicator for multipage tiffs | |
return ifd | |
def to_v2(self) -> ImageFileDirectory_v2: | |
"""Returns an | |
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` | |
instance with the same data as is contained in the original | |
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` | |
instance. | |
:returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` | |
""" | |
ifd = ImageFileDirectory_v2(prefix=self.prefix) | |
ifd._tagdata = dict(self._tagdata) | |
ifd.tagtype = dict(self.tagtype) | |
ifd._tags_v2 = dict(self._tags_v2) | |
return ifd | |
def __contains__(self, tag): | |
return tag in self._tags_v1 or tag in self._tagdata | |
def __len__(self) -> int: | |
return len(set(self._tagdata) | set(self._tags_v1)) | |
def __iter__(self): | |
return iter(set(self._tagdata) | set(self._tags_v1)) | |
def __setitem__(self, tag, value): | |
for legacy_api in (False, True): | |
self._setitem(tag, value, legacy_api) | |
def __getitem__(self, tag): | |
if tag not in self._tags_v1: # unpack on the fly | |
data = self._tagdata[tag] | |
typ = self.tagtype[tag] | |
size, handler = self._load_dispatch[typ] | |
for legacy in (False, True): | |
self._setitem(tag, handler(self, data, legacy), legacy) | |
val = self._tags_v1[tag] | |
if not isinstance(val, (tuple, bytes)): | |
val = (val,) | |
return val | |
# undone -- switch this pointer when IFD_LEGACY_API == False | |
ImageFileDirectory = ImageFileDirectory_v1 | |
## | |
# Image plugin for TIFF files. | |
class TiffImageFile(ImageFile.ImageFile): | |
format = "TIFF" | |
format_description = "Adobe TIFF" | |
_close_exclusive_fp_after_loading = False | |
def __init__(self, fp=None, filename=None): | |
self.tag_v2 = None | |
""" Image file directory (tag dictionary) """ | |
self.tag = None | |
""" Legacy tag entries """ | |
super().__init__(fp, filename) | |
def _open(self) -> None: | |
"""Open the first image in a TIFF file""" | |
# Header | |
ifh = self.fp.read(8) | |
if ifh[2] == 43: | |
ifh += self.fp.read(8) | |
self.tag_v2 = ImageFileDirectory_v2(ifh) | |
# legacy IFD entries will be filled in later | |
self.ifd = None | |
# setup frame pointers | |
self.__first = self.__next = self.tag_v2.next | |
self.__frame = -1 | |
self._fp = self.fp | |
self._frame_pos: list[int] = [] | |
self._n_frames: int | None = None | |
logger.debug("*** TiffImageFile._open ***") | |
logger.debug("- __first: %s", self.__first) | |
logger.debug("- ifh: %s", repr(ifh)) # Use repr to avoid str(bytes) | |
# and load the first frame | |
self._seek(0) | |
def n_frames(self): | |
if self._n_frames is None: | |
current = self.tell() | |
self._seek(len(self._frame_pos)) | |
while self._n_frames is None: | |
self._seek(self.tell() + 1) | |
self.seek(current) | |
return self._n_frames | |
def seek(self, frame: int) -> None: | |
"""Select a given frame as current image""" | |
if not self._seek_check(frame): | |
return | |
self._seek(frame) | |
# Create a new core image object on second and | |
# subsequent frames in the image. Image may be | |
# different size/mode. | |
Image._decompression_bomb_check(self.size) | |
self.im = Image.core.new(self.mode, self.size) | |
def _seek(self, frame: int) -> None: | |
self.fp = self._fp | |
# reset buffered io handle in case fp | |
# was passed to libtiff, invalidating the buffer | |
self.fp.tell() | |
while len(self._frame_pos) <= frame: | |
if not self.__next: | |
msg = "no more images in TIFF file" | |
raise EOFError(msg) | |
logger.debug( | |
"Seeking to frame %s, on frame %s, __next %s, location: %s", | |
frame, | |
self.__frame, | |
self.__next, | |
self.fp.tell(), | |
) | |
if self.__next >= 2**63: | |
msg = "Unable to seek to frame" | |
raise ValueError(msg) | |
self.fp.seek(self.__next) | |
self._frame_pos.append(self.__next) | |
logger.debug("Loading tags, location: %s", self.fp.tell()) | |
self.tag_v2.load(self.fp) | |
if self.tag_v2.next in self._frame_pos: | |
# This IFD has already been processed | |
# Declare this to be the end of the image | |
self.__next = 0 | |
else: | |
self.__next = self.tag_v2.next | |
if self.__next == 0: | |
self._n_frames = frame + 1 | |
if len(self._frame_pos) == 1: | |
self.is_animated = self.__next != 0 | |
self.__frame += 1 | |
self.fp.seek(self._frame_pos[frame]) | |
self.tag_v2.load(self.fp) | |
if XMP in self.tag_v2: | |
self.info["xmp"] = self.tag_v2[XMP] | |
elif "xmp" in self.info: | |
del self.info["xmp"] | |
self._reload_exif() | |
# fill the legacy tag/ifd entries | |
self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) | |
self.__frame = frame | |
self._setup() | |
def tell(self) -> int: | |
"""Return the current frame number""" | |
return self.__frame | |
def get_photoshop_blocks(self): | |
""" | |
Returns a dictionary of Photoshop "Image Resource Blocks". | |
The keys are the image resource ID. For more information, see | |
https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727 | |
:returns: Photoshop "Image Resource Blocks" in a dictionary. | |
""" | |
blocks = {} | |
val = self.tag_v2.get(ExifTags.Base.ImageResources) | |
if val: | |
while val[:4] == b"8BIM": | |
id = i16(val[4:6]) | |
n = math.ceil((val[6] + 1) / 2) * 2 | |
size = i32(val[6 + n : 10 + n]) | |
data = val[10 + n : 10 + n + size] | |
blocks[id] = {"data": data} | |
val = val[math.ceil((10 + n + size) / 2) * 2 :] | |
return blocks | |
def load(self): | |
if self.tile and self.use_load_libtiff: | |
return self._load_libtiff() | |
return super().load() | |
def load_end(self) -> None: | |
# allow closing if we're on the first frame, there's no next | |
# This is the ImageFile.load path only, libtiff specific below. | |
if not self.is_animated: | |
self._close_exclusive_fp_after_loading = True | |
# reset buffered io handle in case fp | |
# was passed to libtiff, invalidating the buffer | |
self.fp.tell() | |
# load IFD data from fp before it is closed | |
exif = self.getexif() | |
for key in TiffTags.TAGS_V2_GROUPS: | |
if key not in exif: | |
continue | |
exif.get_ifd(key) | |
ImageOps.exif_transpose(self, in_place=True) | |
if ExifTags.Base.Orientation in self.tag_v2: | |
del self.tag_v2[ExifTags.Base.Orientation] | |
def _load_libtiff(self): | |
"""Overload method triggered when we detect a compressed tiff | |
Calls out to libtiff""" | |
Image.Image.load(self) | |
self.load_prepare() | |
if not len(self.tile) == 1: | |
msg = "Not exactly one tile" | |
raise OSError(msg) | |
# (self._compression, (extents tuple), | |
# 0, (rawmode, self._compression, fp)) | |
extents = self.tile[0][1] | |
args = list(self.tile[0][3]) | |
# To be nice on memory footprint, if there's a | |
# file descriptor, use that instead of reading | |
# into a string in python. | |
try: | |
fp = hasattr(self.fp, "fileno") and self.fp.fileno() | |
# flush the file descriptor, prevents error on pypy 2.4+ | |
# should also eliminate the need for fp.tell | |
# in _seek | |
if hasattr(self.fp, "flush"): | |
self.fp.flush() | |
except OSError: | |
# io.BytesIO have a fileno, but returns an OSError if | |
# it doesn't use a file descriptor. | |
fp = False | |
if fp: | |
args[2] = fp | |
decoder = Image._getdecoder( | |
self.mode, "libtiff", tuple(args), self.decoderconfig | |
) | |
try: | |
decoder.setimage(self.im, extents) | |
except ValueError as e: | |
msg = "Couldn't set the image" | |
raise OSError(msg) from e | |
close_self_fp = self._exclusive_fp and not self.is_animated | |
if hasattr(self.fp, "getvalue"): | |
# We've got a stringio like thing passed in. Yay for all in memory. | |
# The decoder needs the entire file in one shot, so there's not | |
# a lot we can do here other than give it the entire file. | |
# unless we could do something like get the address of the | |
# underlying string for stringio. | |
# | |
# Rearranging for supporting byteio items, since they have a fileno | |
# that returns an OSError if there's no underlying fp. Easier to | |
# deal with here by reordering. | |
logger.debug("have getvalue. just sending in a string from getvalue") | |
n, err = decoder.decode(self.fp.getvalue()) | |
elif fp: | |
# we've got a actual file on disk, pass in the fp. | |
logger.debug("have fileno, calling fileno version of the decoder.") | |
if not close_self_fp: | |
self.fp.seek(0) | |
# 4 bytes, otherwise the trace might error out | |
n, err = decoder.decode(b"fpfp") | |
else: | |
# we have something else. | |
logger.debug("don't have fileno or getvalue. just reading") | |
self.fp.seek(0) | |
# UNDONE -- so much for that buffer size thing. | |
n, err = decoder.decode(self.fp.read()) | |
self.tile = [] | |
self.readonly = 0 | |
self.load_end() | |
if close_self_fp: | |
self.fp.close() | |
self.fp = None # might be shared | |
if err < 0: | |
raise OSError(err) | |
return Image.Image.load(self) | |
def _setup(self): | |
"""Setup this image object based on current tags""" | |
if 0xBC01 in self.tag_v2: | |
msg = "Windows Media Photo files not yet supported" | |
raise OSError(msg) | |
# extract relevant tags | |
self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] | |
self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1) | |
# photometric is a required tag, but not everyone is reading | |
# the specification | |
photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0) | |
# old style jpeg compression images most certainly are YCbCr | |
if self._compression == "tiff_jpeg": | |
photo = 6 | |
fillorder = self.tag_v2.get(FILLORDER, 1) | |
logger.debug("*** Summary ***") | |
logger.debug("- compression: %s", self._compression) | |
logger.debug("- photometric_interpretation: %s", photo) | |
logger.debug("- planar_configuration: %s", self._planar_configuration) | |
logger.debug("- fill_order: %s", fillorder) | |
logger.debug("- YCbCr subsampling: %s", self.tag.get(YCBCRSUBSAMPLING)) | |
# size | |
xsize = int(self.tag_v2.get(IMAGEWIDTH)) | |
ysize = int(self.tag_v2.get(IMAGELENGTH)) | |
self._size = xsize, ysize | |
logger.debug("- size: %s", self.size) | |
sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,)) | |
if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1: | |
# SAMPLEFORMAT is properly per band, so an RGB image will | |
# be (1,1,1). But, we don't support per band pixel types, | |
# and anything more than one band is a uint8. So, just | |
# take the first element. Revisit this if adding support | |
# for more exotic images. | |
sample_format = (1,) | |
bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,)) | |
extra_tuple = self.tag_v2.get(EXTRASAMPLES, ()) | |
if photo in (2, 6, 8): # RGB, YCbCr, LAB | |
bps_count = 3 | |
elif photo == 5: # CMYK | |
bps_count = 4 | |
else: | |
bps_count = 1 | |
bps_count += len(extra_tuple) | |
bps_actual_count = len(bps_tuple) | |
samples_per_pixel = self.tag_v2.get( | |
SAMPLESPERPIXEL, | |
3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1, | |
) | |
if samples_per_pixel > MAX_SAMPLESPERPIXEL: | |
# DOS check, samples_per_pixel can be a Long, and we extend the tuple below | |
logger.error( | |
"More samples per pixel than can be decoded: %s", samples_per_pixel | |
) | |
msg = "Invalid value for samples per pixel" | |
raise SyntaxError(msg) | |
if samples_per_pixel < bps_actual_count: | |
# If a file has more values in bps_tuple than expected, | |
# remove the excess. | |
bps_tuple = bps_tuple[:samples_per_pixel] | |
elif samples_per_pixel > bps_actual_count and bps_actual_count == 1: | |
# If a file has only one value in bps_tuple, when it should have more, | |
# presume it is the same number of bits for all of the samples. | |
bps_tuple = bps_tuple * samples_per_pixel | |
if len(bps_tuple) != samples_per_pixel: | |
msg = "unknown data organization" | |
raise SyntaxError(msg) | |
# mode: check photometric interpretation and bits per pixel | |
key = ( | |
self.tag_v2.prefix, | |
photo, | |
sample_format, | |
fillorder, | |
bps_tuple, | |
extra_tuple, | |
) | |
logger.debug("format key: %s", key) | |
try: | |
self._mode, rawmode = OPEN_INFO[key] | |
except KeyError as e: | |
logger.debug("- unsupported format") | |
msg = "unknown pixel mode" | |
raise SyntaxError(msg) from e | |
logger.debug("- raw mode: %s", rawmode) | |
logger.debug("- pil mode: %s", self.mode) | |
self.info["compression"] = self._compression | |
xres = self.tag_v2.get(X_RESOLUTION, 1) | |
yres = self.tag_v2.get(Y_RESOLUTION, 1) | |
if xres and yres: | |
resunit = self.tag_v2.get(RESOLUTION_UNIT) | |
if resunit == 2: # dots per inch | |
self.info["dpi"] = (xres, yres) | |
elif resunit == 3: # dots per centimeter. convert to dpi | |
self.info["dpi"] = (xres * 2.54, yres * 2.54) | |
elif resunit is None: # used to default to 1, but now 2) | |
self.info["dpi"] = (xres, yres) | |
# For backward compatibility, | |
# we also preserve the old behavior | |
self.info["resolution"] = xres, yres | |
else: # No absolute unit of measurement | |
self.info["resolution"] = xres, yres | |
# build tile descriptors | |
x = y = layer = 0 | |
self.tile = [] | |
self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw" | |
if self.use_load_libtiff: | |
# Decoder expects entire file as one tile. | |
# There's a buffer size limit in load (64k) | |
# so large g4 images will fail if we use that | |
# function. | |
# | |
# Setup the one tile for the whole image, then | |
# use the _load_libtiff function. | |
# libtiff handles the fillmode for us, so 1;IR should | |
# actually be 1;I. Including the R double reverses the | |
# bits, so stripes of the image are reversed. See | |
# https://github.com/python-pillow/Pillow/issues/279 | |
if fillorder == 2: | |
# Replace fillorder with fillorder=1 | |
key = key[:3] + (1,) + key[4:] | |
logger.debug("format key: %s", key) | |
# this should always work, since all the | |
# fillorder==2 modes have a corresponding | |
# fillorder=1 mode | |
self._mode, rawmode = OPEN_INFO[key] | |
# libtiff always returns the bytes in native order. | |
# we're expecting image byte order. So, if the rawmode | |
# contains I;16, we need to convert from native to image | |
# byte order. | |
if rawmode == "I;16": | |
rawmode = "I;16N" | |
if ";16B" in rawmode: | |
rawmode = rawmode.replace(";16B", ";16N") | |
if ";16L" in rawmode: | |
rawmode = rawmode.replace(";16L", ";16N") | |
# YCbCr images with new jpeg compression with pixels in one plane | |
# unpacked straight into RGB values | |
if ( | |
photo == 6 | |
and self._compression == "jpeg" | |
and self._planar_configuration == 1 | |
): | |
rawmode = "RGB" | |
# Offset in the tile tuple is 0, we go from 0,0 to | |
# w,h, and we only do this once -- eds | |
a = (rawmode, self._compression, False, self.tag_v2.offset) | |
self.tile.append(("libtiff", (0, 0, xsize, ysize), 0, a)) | |
elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2: | |
# striped image | |
if STRIPOFFSETS in self.tag_v2: | |
offsets = self.tag_v2[STRIPOFFSETS] | |
h = self.tag_v2.get(ROWSPERSTRIP, ysize) | |
w = self.size[0] | |
else: | |
# tiled image | |
offsets = self.tag_v2[TILEOFFSETS] | |
w = self.tag_v2.get(TILEWIDTH) | |
h = self.tag_v2.get(TILELENGTH) | |
for offset in offsets: | |
if x + w > xsize: | |
stride = w * sum(bps_tuple) / 8 # bytes per line | |
else: | |
stride = 0 | |
tile_rawmode = rawmode | |
if self._planar_configuration == 2: | |
# each band on it's own layer | |
tile_rawmode = rawmode[layer] | |
# adjust stride width accordingly | |
stride /= bps_count | |
a = (tile_rawmode, int(stride), 1) | |
self.tile.append( | |
( | |
self._compression, | |
(x, y, min(x + w, xsize), min(y + h, ysize)), | |
offset, | |
a, | |
) | |
) | |
x = x + w | |
if x >= self.size[0]: | |
x, y = 0, y + h | |
if y >= self.size[1]: | |
x = y = 0 | |
layer += 1 | |
else: | |
logger.debug("- unsupported data organization") | |
msg = "unknown data organization" | |
raise SyntaxError(msg) | |
# Fix up info. | |
if ICCPROFILE in self.tag_v2: | |
self.info["icc_profile"] = self.tag_v2[ICCPROFILE] | |
# fixup palette descriptor | |
if self.mode in ["P", "PA"]: | |
palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] | |
self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) | |
# | |
# -------------------------------------------------------------------- | |
# Write TIFF files | |
# little endian is default except for image modes with | |
# explicit big endian byte-order | |
SAVE_INFO = { | |
# mode => rawmode, byteorder, photometrics, | |
# sampleformat, bitspersample, extra | |
"1": ("1", II, 1, 1, (1,), None), | |
"L": ("L", II, 1, 1, (8,), None), | |
"LA": ("LA", II, 1, 1, (8, 8), 2), | |
"P": ("P", II, 3, 1, (8,), None), | |
"PA": ("PA", II, 3, 1, (8, 8), 2), | |
"I": ("I;32S", II, 1, 2, (32,), None), | |
"I;16": ("I;16", II, 1, 1, (16,), None), | |
"I;16S": ("I;16S", II, 1, 2, (16,), None), | |
"F": ("F;32F", II, 1, 3, (32,), None), | |
"RGB": ("RGB", II, 2, 1, (8, 8, 8), None), | |
"RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0), | |
"RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2), | |
"CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None), | |
"YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None), | |
"LAB": ("LAB", II, 8, 1, (8, 8, 8), None), | |
"I;32BS": ("I;32BS", MM, 1, 2, (32,), None), | |
"I;16B": ("I;16B", MM, 1, 1, (16,), None), | |
"I;16BS": ("I;16BS", MM, 1, 2, (16,), None), | |
"F;32BF": ("F;32BF", MM, 1, 3, (32,), None), | |
} | |
def _save(im, fp, filename): | |
try: | |
rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] | |
except KeyError as e: | |
msg = f"cannot write mode {im.mode} as TIFF" | |
raise OSError(msg) from e | |
ifd = ImageFileDirectory_v2(prefix=prefix) | |
encoderinfo = im.encoderinfo | |
encoderconfig = im.encoderconfig | |
try: | |
compression = encoderinfo["compression"] | |
except KeyError: | |
compression = im.info.get("compression") | |
if isinstance(compression, int): | |
# compression value may be from BMP. Ignore it | |
compression = None | |
if compression is None: | |
compression = "raw" | |
elif compression == "tiff_jpeg": | |
# OJPEG is obsolete, so use new-style JPEG compression instead | |
compression = "jpeg" | |
elif compression == "tiff_deflate": | |
compression = "tiff_adobe_deflate" | |
libtiff = WRITE_LIBTIFF or compression != "raw" | |
# required for color libtiff images | |
ifd[PLANAR_CONFIGURATION] = 1 | |
ifd[IMAGEWIDTH] = im.size[0] | |
ifd[IMAGELENGTH] = im.size[1] | |
# write any arbitrary tags passed in as an ImageFileDirectory | |
if "tiffinfo" in encoderinfo: | |
info = encoderinfo["tiffinfo"] | |
elif "exif" in encoderinfo: | |
info = encoderinfo["exif"] | |
if isinstance(info, bytes): | |
exif = Image.Exif() | |
exif.load(info) | |
info = exif | |
else: | |
info = {} | |
logger.debug("Tiffinfo Keys: %s", list(info)) | |
if isinstance(info, ImageFileDirectory_v1): | |
info = info.to_v2() | |
for key in info: | |
if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS: | |
ifd[key] = info.get_ifd(key) | |
else: | |
ifd[key] = info.get(key) | |
try: | |
ifd.tagtype[key] = info.tagtype[key] | |
except Exception: | |
pass # might not be an IFD. Might not have populated type | |
legacy_ifd = {} | |
if hasattr(im, "tag"): | |
legacy_ifd = im.tag.to_v2() | |
supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})} | |
for tag in ( | |
# IFD offset that may not be correct in the saved image | |
EXIFIFD, | |
# Determined by the image format and should not be copied from legacy_ifd. | |
SAMPLEFORMAT, | |
): | |
if tag in supplied_tags: | |
del supplied_tags[tag] | |
# additions written by Greg Couch, [email protected] | |
# inspired by image-sig posting from Kevin Cazabon, [email protected] | |
if hasattr(im, "tag_v2"): | |
# preserve tags from original TIFF image file | |
for key in ( | |
RESOLUTION_UNIT, | |
X_RESOLUTION, | |
Y_RESOLUTION, | |
IPTC_NAA_CHUNK, | |
PHOTOSHOP_CHUNK, | |
XMP, | |
): | |
if key in im.tag_v2: | |
if key == IPTC_NAA_CHUNK and im.tag_v2.tagtype[key] not in ( | |
TiffTags.BYTE, | |
TiffTags.UNDEFINED, | |
): | |
del supplied_tags[key] | |
else: | |
ifd[key] = im.tag_v2[key] | |
ifd.tagtype[key] = im.tag_v2.tagtype[key] | |
# preserve ICC profile (should also work when saving other formats | |
# which support profiles as TIFF) -- 2008-06-06 Florian Hoech | |
icc = encoderinfo.get("icc_profile", im.info.get("icc_profile")) | |
if icc: | |
ifd[ICCPROFILE] = icc | |
for key, name in [ | |
(IMAGEDESCRIPTION, "description"), | |
(X_RESOLUTION, "resolution"), | |
(Y_RESOLUTION, "resolution"), | |
(X_RESOLUTION, "x_resolution"), | |
(Y_RESOLUTION, "y_resolution"), | |
(RESOLUTION_UNIT, "resolution_unit"), | |
(SOFTWARE, "software"), | |
(DATE_TIME, "date_time"), | |
(ARTIST, "artist"), | |
(COPYRIGHT, "copyright"), | |
]: | |
if name in encoderinfo: | |
ifd[key] = encoderinfo[name] | |
dpi = encoderinfo.get("dpi") | |
if dpi: | |
ifd[RESOLUTION_UNIT] = 2 | |
ifd[X_RESOLUTION] = dpi[0] | |
ifd[Y_RESOLUTION] = dpi[1] | |
if bits != (1,): | |
ifd[BITSPERSAMPLE] = bits | |
if len(bits) != 1: | |
ifd[SAMPLESPERPIXEL] = len(bits) | |
if extra is not None: | |
ifd[EXTRASAMPLES] = extra | |
if format != 1: | |
ifd[SAMPLEFORMAT] = format | |
if PHOTOMETRIC_INTERPRETATION not in ifd: | |
ifd[PHOTOMETRIC_INTERPRETATION] = photo | |
elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0: | |
if im.mode == "1": | |
inverted_im = im.copy() | |
px = inverted_im.load() | |
for y in range(inverted_im.height): | |
for x in range(inverted_im.width): | |
px[x, y] = 0 if px[x, y] == 255 else 255 | |
im = inverted_im | |
else: | |
im = ImageOps.invert(im) | |
if im.mode in ["P", "PA"]: | |
lut = im.im.getpalette("RGB", "RGB;L") | |
colormap = [] | |
colors = len(lut) // 3 | |
for i in range(3): | |
colormap += [v * 256 for v in lut[colors * i : colors * (i + 1)]] | |
colormap += [0] * (256 - colors) | |
ifd[COLORMAP] = colormap | |
# data orientation | |
w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH] | |
stride = len(bits) * ((w * bits[0] + 7) // 8) | |
if ROWSPERSTRIP not in ifd: | |
# aim for given strip size (64 KB by default) when using libtiff writer | |
if libtiff: | |
im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE) | |
rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h) | |
# JPEG encoder expects multiple of 8 rows | |
if compression == "jpeg": | |
rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h) | |
else: | |
rows_per_strip = h | |
if rows_per_strip == 0: | |
rows_per_strip = 1 | |
ifd[ROWSPERSTRIP] = rows_per_strip | |
strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP] | |
strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP] | |
if strip_byte_counts >= 2**16: | |
ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG | |
ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( | |
stride * h - strip_byte_counts * (strips_per_image - 1), | |
) | |
ifd[STRIPOFFSETS] = tuple( | |
range(0, strip_byte_counts * strips_per_image, strip_byte_counts) | |
) # this is adjusted by IFD writer | |
# no compression by default: | |
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) | |
if im.mode == "YCbCr": | |
for tag, value in { | |
YCBCRSUBSAMPLING: (1, 1), | |
REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255), | |
}.items(): | |
ifd.setdefault(tag, value) | |
blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS] | |
if libtiff: | |
if "quality" in encoderinfo: | |
quality = encoderinfo["quality"] | |
if not isinstance(quality, int) or quality < 0 or quality > 100: | |
msg = "Invalid quality setting" | |
raise ValueError(msg) | |
if compression != "jpeg": | |
msg = "quality setting only supported for 'jpeg' compression" | |
raise ValueError(msg) | |
ifd[JPEGQUALITY] = quality | |
logger.debug("Saving using libtiff encoder") | |
logger.debug("Items: %s", sorted(ifd.items())) | |
_fp = 0 | |
if hasattr(fp, "fileno"): | |
try: | |
fp.seek(0) | |
_fp = os.dup(fp.fileno()) | |
except io.UnsupportedOperation: | |
pass | |
# optional types for non core tags | |
types = {} | |
# STRIPOFFSETS and STRIPBYTECOUNTS are added by the library | |
# based on the data in the strip. | |
# OSUBFILETYPE is deprecated. | |
# The other tags expect arrays with a certain length (fixed or depending on | |
# BITSPERSAMPLE, etc), passing arrays with a different length will result in | |
# segfaults. Block these tags until we add extra validation. | |
# SUBIFD may also cause a segfault. | |
blocklist += [ | |
OSUBFILETYPE, | |
REFERENCEBLACKWHITE, | |
STRIPBYTECOUNTS, | |
STRIPOFFSETS, | |
TRANSFERFUNCTION, | |
SUBIFD, | |
] | |
# bits per sample is a single short in the tiff directory, not a list. | |
atts = {BITSPERSAMPLE: bits[0]} | |
# Merge the ones that we have with (optional) more bits from | |
# the original file, e.g x,y resolution so that we can | |
# save(load('')) == original file. | |
for tag, value in itertools.chain(ifd.items(), supplied_tags.items()): | |
# Libtiff can only process certain core items without adding | |
# them to the custom dictionary. | |
# Custom items are supported for int, float, unicode, string and byte | |
# values. Other types and tuples require a tagtype. | |
if tag not in TiffTags.LIBTIFF_CORE: | |
if not getattr(Image.core, "libtiff_support_custom_tags", False): | |
continue | |
if tag in ifd.tagtype: | |
types[tag] = ifd.tagtype[tag] | |
elif not (isinstance(value, (int, float, str, bytes))): | |
continue | |
else: | |
type = TiffTags.lookup(tag).type | |
if type: | |
types[tag] = type | |
if tag not in atts and tag not in blocklist: | |
if isinstance(value, str): | |
atts[tag] = value.encode("ascii", "replace") + b"\0" | |
elif isinstance(value, IFDRational): | |
atts[tag] = float(value) | |
else: | |
atts[tag] = value | |
if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1: | |
atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0] | |
logger.debug("Converted items: %s", sorted(atts.items())) | |
# libtiff always expects the bytes in native order. | |
# we're storing image byte order. So, if the rawmode | |
# contains I;16, we need to convert from native to image | |
# byte order. | |
if im.mode in ("I;16B", "I;16"): | |
rawmode = "I;16N" | |
# Pass tags as sorted list so that the tags are set in a fixed order. | |
# This is required by libtiff for some tags. For example, the JPEGQUALITY | |
# pseudo tag requires that the COMPRESS tag was already set. | |
tags = list(atts.items()) | |
tags.sort() | |
a = (rawmode, compression, _fp, filename, tags, types) | |
encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) | |
encoder.setimage(im.im, (0, 0) + im.size) | |
while True: | |
# undone, change to self.decodermaxblock: | |
errcode, data = encoder.encode(16 * 1024)[1:] | |
if not _fp: | |
fp.write(data) | |
if errcode: | |
break | |
if _fp: | |
try: | |
os.close(_fp) | |
except OSError: | |
pass | |
if errcode < 0: | |
msg = f"encoder error {errcode} when writing image file" | |
raise OSError(msg) | |
else: | |
for tag in blocklist: | |
del ifd[tag] | |
offset = ifd.save(fp) | |
ImageFile._save( | |
im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))] | |
) | |
# -- helper for multi-page save -- | |
if "_debug_multipage" in encoderinfo: | |
# just to access o32 and o16 (using correct byte order) | |
im._debug_multipage = ifd | |
class AppendingTiffWriter: | |
fieldSizes = [ | |
0, # None | |
1, # byte | |
1, # ascii | |
2, # short | |
4, # long | |
8, # rational | |
1, # sbyte | |
1, # undefined | |
2, # sshort | |
4, # slong | |
8, # srational | |
4, # float | |
8, # double | |
4, # ifd | |
2, # unicode | |
4, # complex | |
8, # long8 | |
] | |
Tags = { | |
273, # StripOffsets | |
288, # FreeOffsets | |
324, # TileOffsets | |
519, # JPEGQTables | |
520, # JPEGDCTables | |
521, # JPEGACTables | |
} | |
def __init__(self, fn, new=False): | |
if hasattr(fn, "read"): | |
self.f = fn | |
self.close_fp = False | |
else: | |
self.name = fn | |
self.close_fp = True | |
try: | |
self.f = open(fn, "w+b" if new else "r+b") | |
except OSError: | |
self.f = open(fn, "w+b") | |
self.beginning = self.f.tell() | |
self.setup() | |
def setup(self) -> None: | |
# Reset everything. | |
self.f.seek(self.beginning, os.SEEK_SET) | |
self.whereToWriteNewIFDOffset = None | |
self.offsetOfNewPage = 0 | |
self.IIMM = iimm = self.f.read(4) | |
if not iimm: | |
# empty file - first page | |
self.isFirst = True | |
return | |
self.isFirst = False | |
if iimm == b"II\x2a\x00": | |
self.setEndian("<") | |
elif iimm == b"MM\x00\x2a": | |
self.setEndian(">") | |
else: | |
msg = "Invalid TIFF file header" | |
raise RuntimeError(msg) | |
self.skipIFDs() | |
self.goToEnd() | |
def finalize(self) -> None: | |
if self.isFirst: | |
return | |
# fix offsets | |
self.f.seek(self.offsetOfNewPage) | |
iimm = self.f.read(4) | |
if not iimm: | |
# Make it easy to finish a frame without committing to a new one. | |
return | |
if iimm != self.IIMM: | |
msg = "IIMM of new page doesn't match IIMM of first page" | |
raise RuntimeError(msg) | |
ifd_offset = self.readLong() | |
ifd_offset += self.offsetOfNewPage | |
self.f.seek(self.whereToWriteNewIFDOffset) | |
self.writeLong(ifd_offset) | |
self.f.seek(ifd_offset) | |
self.fixIFD() | |
def newFrame(self) -> None: | |
# Call this to finish a frame. | |
self.finalize() | |
self.setup() | |
def __enter__(self) -> AppendingTiffWriter: | |
return self | |
def __exit__(self, *args: object) -> None: | |
if self.close_fp: | |
self.close() | |
def tell(self) -> int: | |
return self.f.tell() - self.offsetOfNewPage | |
def seek(self, offset, whence=io.SEEK_SET): | |
if whence == os.SEEK_SET: | |
offset += self.offsetOfNewPage | |
self.f.seek(offset, whence) | |
return self.tell() | |
def goToEnd(self) -> None: | |
self.f.seek(0, os.SEEK_END) | |
pos = self.f.tell() | |
# pad to 16 byte boundary | |
pad_bytes = 16 - pos % 16 | |
if 0 < pad_bytes < 16: | |
self.f.write(bytes(pad_bytes)) | |
self.offsetOfNewPage = self.f.tell() | |
def setEndian(self, endian: str) -> None: | |
self.endian = endian | |
self.longFmt = f"{self.endian}L" | |
self.shortFmt = f"{self.endian}H" | |
self.tagFormat = f"{self.endian}HHL" | |
def skipIFDs(self) -> None: | |
while True: | |
ifd_offset = self.readLong() | |
if ifd_offset == 0: | |
self.whereToWriteNewIFDOffset = self.f.tell() - 4 | |
break | |
self.f.seek(ifd_offset) | |
num_tags = self.readShort() | |
self.f.seek(num_tags * 12, os.SEEK_CUR) | |
def write(self, data: bytes) -> int | None: | |
return self.f.write(data) | |
def readShort(self) -> int: | |
(value,) = struct.unpack(self.shortFmt, self.f.read(2)) | |
return value | |
def readLong(self) -> int: | |
(value,) = struct.unpack(self.longFmt, self.f.read(4)) | |
return value | |
def rewriteLastShortToLong(self, value: int) -> None: | |
self.f.seek(-2, os.SEEK_CUR) | |
bytes_written = self.f.write(struct.pack(self.longFmt, value)) | |
if bytes_written is not None and bytes_written != 4: | |
msg = f"wrote only {bytes_written} bytes but wanted 4" | |
raise RuntimeError(msg) | |
def rewriteLastShort(self, value: int) -> None: | |
self.f.seek(-2, os.SEEK_CUR) | |
bytes_written = self.f.write(struct.pack(self.shortFmt, value)) | |
if bytes_written is not None and bytes_written != 2: | |
msg = f"wrote only {bytes_written} bytes but wanted 2" | |
raise RuntimeError(msg) | |
def rewriteLastLong(self, value: int) -> None: | |
self.f.seek(-4, os.SEEK_CUR) | |
bytes_written = self.f.write(struct.pack(self.longFmt, value)) | |
if bytes_written is not None and bytes_written != 4: | |
msg = f"wrote only {bytes_written} bytes but wanted 4" | |
raise RuntimeError(msg) | |
def writeShort(self, value: int) -> None: | |
bytes_written = self.f.write(struct.pack(self.shortFmt, value)) | |
if bytes_written is not None and bytes_written != 2: | |
msg = f"wrote only {bytes_written} bytes but wanted 2" | |
raise RuntimeError(msg) | |
def writeLong(self, value: int) -> None: | |
bytes_written = self.f.write(struct.pack(self.longFmt, value)) | |
if bytes_written is not None and bytes_written != 4: | |
msg = f"wrote only {bytes_written} bytes but wanted 4" | |
raise RuntimeError(msg) | |
def close(self) -> None: | |
self.finalize() | |
self.f.close() | |
def fixIFD(self) -> None: | |
num_tags = self.readShort() | |
for i in range(num_tags): | |
tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8)) | |
field_size = self.fieldSizes[field_type] | |
total_size = field_size * count | |
is_local = total_size <= 4 | |
offset: int | None | |
if not is_local: | |
offset = self.readLong() + self.offsetOfNewPage | |
self.rewriteLastLong(offset) | |
if tag in self.Tags: | |
cur_pos = self.f.tell() | |
if is_local: | |
self.fixOffsets( | |
count, isShort=(field_size == 2), isLong=(field_size == 4) | |
) | |
self.f.seek(cur_pos + 4) | |
else: | |
self.f.seek(offset) | |
self.fixOffsets( | |
count, isShort=(field_size == 2), isLong=(field_size == 4) | |
) | |
self.f.seek(cur_pos) | |
offset = cur_pos = None | |
elif is_local: | |
# skip the locally stored value that is not an offset | |
self.f.seek(4, os.SEEK_CUR) | |
def fixOffsets( | |
self, count: int, isShort: bool = False, isLong: bool = False | |
) -> None: | |
if not isShort and not isLong: | |
msg = "offset is neither short nor long" | |
raise RuntimeError(msg) | |
for i in range(count): | |
offset = self.readShort() if isShort else self.readLong() | |
offset += self.offsetOfNewPage | |
if isShort and offset >= 65536: | |
# offset is now too large - we must convert shorts to longs | |
if count != 1: | |
msg = "not implemented" | |
raise RuntimeError(msg) # XXX TODO | |
# simple case - the offset is just one and therefore it is | |
# local (not referenced with another offset) | |
self.rewriteLastShortToLong(offset) | |
self.f.seek(-10, os.SEEK_CUR) | |
self.writeShort(TiffTags.LONG) # rewrite the type to LONG | |
self.f.seek(8, os.SEEK_CUR) | |
elif isShort: | |
self.rewriteLastShort(offset) | |
else: | |
self.rewriteLastLong(offset) | |
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: | |
encoderinfo = im.encoderinfo.copy() | |
encoderconfig = im.encoderconfig | |
append_images = list(encoderinfo.get("append_images", [])) | |
if not hasattr(im, "n_frames") and not append_images: | |
return _save(im, fp, filename) | |
cur_idx = im.tell() | |
try: | |
with AppendingTiffWriter(fp) as tf: | |
for ims in [im] + append_images: | |
ims.encoderinfo = encoderinfo | |
ims.encoderconfig = encoderconfig | |
if not hasattr(ims, "n_frames"): | |
nfr = 1 | |
else: | |
nfr = ims.n_frames | |
for idx in range(nfr): | |
ims.seek(idx) | |
ims.load() | |
_save(ims, tf, filename) | |
tf.newFrame() | |
finally: | |
im.seek(cur_idx) | |
# | |
# -------------------------------------------------------------------- | |
# Register | |
Image.register_open(TiffImageFile.format, TiffImageFile, _accept) | |
Image.register_save(TiffImageFile.format, _save) | |
Image.register_save_all(TiffImageFile.format, _save_all) | |
Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"]) | |
Image.register_mime(TiffImageFile.format, "image/tiff") | |