Spaces:
Running
Running
# | |
# The Python Imaging Library. | |
# $Id$ | |
# | |
# the Image class wrapper | |
# | |
# partial release history: | |
# 1995-09-09 fl Created | |
# 1996-03-11 fl PIL release 0.0 (proof of concept) | |
# 1996-04-30 fl PIL release 0.1b1 | |
# 1999-07-28 fl PIL release 1.0 final | |
# 2000-06-07 fl PIL release 1.1 | |
# 2000-10-20 fl PIL release 1.1.1 | |
# 2001-05-07 fl PIL release 1.1.2 | |
# 2002-03-15 fl PIL release 1.1.3 | |
# 2003-05-10 fl PIL release 1.1.4 | |
# 2005-03-28 fl PIL release 1.1.5 | |
# 2006-12-02 fl PIL release 1.1.6 | |
# 2009-11-15 fl PIL release 1.1.7 | |
# | |
# Copyright (c) 1997-2009 by Secret Labs AB. All rights reserved. | |
# Copyright (c) 1995-2009 by Fredrik Lundh. | |
# | |
# See the README file for information on usage and redistribution. | |
# | |
from __future__ import annotations | |
import abc | |
import atexit | |
import builtins | |
import io | |
import logging | |
import math | |
import os | |
import re | |
import struct | |
import sys | |
import tempfile | |
import warnings | |
from collections.abc import Callable, MutableMapping | |
from enum import IntEnum | |
from types import ModuleType | |
from typing import IO, TYPE_CHECKING, Any | |
# VERSION was removed in Pillow 6.0.0. | |
# PILLOW_VERSION was removed in Pillow 9.0.0. | |
# Use __version__ instead. | |
from . import ( | |
ExifTags, | |
ImageMode, | |
TiffTags, | |
UnidentifiedImageError, | |
__version__, | |
_plugins, | |
) | |
from ._binary import i32le, o32be, o32le | |
from ._typing import TypeGuard | |
from ._util import DeferredError, is_path | |
ElementTree: ModuleType | None | |
try: | |
from defusedxml import ElementTree | |
except ImportError: | |
ElementTree = None | |
logger = logging.getLogger(__name__) | |
class DecompressionBombWarning(RuntimeWarning): | |
pass | |
class DecompressionBombError(Exception): | |
pass | |
# Limit to around a quarter gigabyte for a 24-bit (3 bpp) image | |
MAX_IMAGE_PIXELS: int | None = int(1024 * 1024 * 1024 // 4 // 3) | |
try: | |
# If the _imaging C module is not present, Pillow will not load. | |
# Note that other modules should not refer to _imaging directly; | |
# import Image and use the Image.core variable instead. | |
# Also note that Image.core is not a publicly documented interface, | |
# and should be considered private and subject to change. | |
from . import _imaging as core | |
if __version__ != getattr(core, "PILLOW_VERSION", None): | |
msg = ( | |
"The _imaging extension was built for another version of Pillow or PIL:\n" | |
f"Core version: {getattr(core, 'PILLOW_VERSION', None)}\n" | |
f"Pillow version: {__version__}" | |
) | |
raise ImportError(msg) | |
except ImportError as v: | |
core = DeferredError.new(ImportError("The _imaging C module is not installed.")) | |
# Explanations for ways that we know we might have an import error | |
if str(v).startswith("Module use of python"): | |
# The _imaging C module is present, but not compiled for | |
# the right version (windows only). Print a warning, if | |
# possible. | |
warnings.warn( | |
"The _imaging extension was built for another version of Python.", | |
RuntimeWarning, | |
) | |
elif str(v).startswith("The _imaging extension"): | |
warnings.warn(str(v), RuntimeWarning) | |
# Fail here anyway. Don't let people run with a mostly broken Pillow. | |
# see docs/porting.rst | |
raise | |
USE_CFFI_ACCESS = False | |
cffi: ModuleType | None | |
try: | |
import cffi | |
except ImportError: | |
cffi = None | |
def isImageType(t: Any) -> TypeGuard[Image]: | |
""" | |
Checks if an object is an image object. | |
.. warning:: | |
This function is for internal use only. | |
:param t: object to check if it's an image | |
:returns: True if the object is an image | |
""" | |
return hasattr(t, "im") | |
# | |
# Constants | |
# transpose | |
class Transpose(IntEnum): | |
FLIP_LEFT_RIGHT = 0 | |
FLIP_TOP_BOTTOM = 1 | |
ROTATE_90 = 2 | |
ROTATE_180 = 3 | |
ROTATE_270 = 4 | |
TRANSPOSE = 5 | |
TRANSVERSE = 6 | |
# transforms (also defined in Imaging.h) | |
class Transform(IntEnum): | |
AFFINE = 0 | |
EXTENT = 1 | |
PERSPECTIVE = 2 | |
QUAD = 3 | |
MESH = 4 | |
# resampling filters (also defined in Imaging.h) | |
class Resampling(IntEnum): | |
NEAREST = 0 | |
BOX = 4 | |
BILINEAR = 2 | |
HAMMING = 5 | |
BICUBIC = 3 | |
LANCZOS = 1 | |
_filters_support = { | |
Resampling.BOX: 0.5, | |
Resampling.BILINEAR: 1.0, | |
Resampling.HAMMING: 1.0, | |
Resampling.BICUBIC: 2.0, | |
Resampling.LANCZOS: 3.0, | |
} | |
# dithers | |
class Dither(IntEnum): | |
NONE = 0 | |
ORDERED = 1 # Not yet implemented | |
RASTERIZE = 2 # Not yet implemented | |
FLOYDSTEINBERG = 3 # default | |
# palettes/quantizers | |
class Palette(IntEnum): | |
WEB = 0 | |
ADAPTIVE = 1 | |
class Quantize(IntEnum): | |
MEDIANCUT = 0 | |
MAXCOVERAGE = 1 | |
FASTOCTREE = 2 | |
LIBIMAGEQUANT = 3 | |
module = sys.modules[__name__] | |
for enum in (Transpose, Transform, Resampling, Dither, Palette, Quantize): | |
for item in enum: | |
setattr(module, item.name, item.value) | |
if hasattr(core, "DEFAULT_STRATEGY"): | |
DEFAULT_STRATEGY = core.DEFAULT_STRATEGY | |
FILTERED = core.FILTERED | |
HUFFMAN_ONLY = core.HUFFMAN_ONLY | |
RLE = core.RLE | |
FIXED = core.FIXED | |
# -------------------------------------------------------------------- | |
# Registries | |
if TYPE_CHECKING: | |
from . import ImageFile | |
ID: list[str] = [] | |
OPEN: dict[ | |
str, | |
tuple[ | |
Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], | |
Callable[[bytes], bool] | None, | |
], | |
] = {} | |
MIME: dict[str, str] = {} | |
SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} | |
SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} | |
EXTENSION: dict[str, str] = {} | |
DECODERS: dict[str, type[ImageFile.PyDecoder]] = {} | |
ENCODERS: dict[str, type[ImageFile.PyEncoder]] = {} | |
# -------------------------------------------------------------------- | |
# Modes | |
_ENDIAN = "<" if sys.byteorder == "little" else ">" | |
def _conv_type_shape(im): | |
m = ImageMode.getmode(im.mode) | |
shape = (im.height, im.width) | |
extra = len(m.bands) | |
if extra != 1: | |
shape += (extra,) | |
return shape, m.typestr | |
MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr"] | |
# raw modes that may be memory mapped. NOTE: if you change this, you | |
# may have to modify the stride calculation in map.c too! | |
_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B") | |
def getmodebase(mode: str) -> str: | |
""" | |
Gets the "base" mode for given mode. This function returns "L" for | |
images that contain grayscale data, and "RGB" for images that | |
contain color data. | |
:param mode: Input mode. | |
:returns: "L" or "RGB". | |
:exception KeyError: If the input mode was not a standard mode. | |
""" | |
return ImageMode.getmode(mode).basemode | |
def getmodetype(mode: str) -> str: | |
""" | |
Gets the storage type mode. Given a mode, this function returns a | |
single-layer mode suitable for storing individual bands. | |
:param mode: Input mode. | |
:returns: "L", "I", or "F". | |
:exception KeyError: If the input mode was not a standard mode. | |
""" | |
return ImageMode.getmode(mode).basetype | |
def getmodebandnames(mode: str) -> tuple[str, ...]: | |
""" | |
Gets a list of individual band names. Given a mode, this function returns | |
a tuple containing the names of individual bands (use | |
:py:method:`~PIL.Image.getmodetype` to get the mode used to store each | |
individual band. | |
:param mode: Input mode. | |
:returns: A tuple containing band names. The length of the tuple | |
gives the number of bands in an image of the given mode. | |
:exception KeyError: If the input mode was not a standard mode. | |
""" | |
return ImageMode.getmode(mode).bands | |
def getmodebands(mode: str) -> int: | |
""" | |
Gets the number of individual bands for this mode. | |
:param mode: Input mode. | |
:returns: The number of bands in this mode. | |
:exception KeyError: If the input mode was not a standard mode. | |
""" | |
return len(ImageMode.getmode(mode).bands) | |
# -------------------------------------------------------------------- | |
# Helpers | |
_initialized = 0 | |
def preinit() -> None: | |
""" | |
Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers. | |
It is called when opening or saving images. | |
""" | |
global _initialized | |
if _initialized >= 1: | |
return | |
try: | |
from . import BmpImagePlugin | |
assert BmpImagePlugin | |
except ImportError: | |
pass | |
try: | |
from . import GifImagePlugin | |
assert GifImagePlugin | |
except ImportError: | |
pass | |
try: | |
from . import JpegImagePlugin | |
assert JpegImagePlugin | |
except ImportError: | |
pass | |
try: | |
from . import PpmImagePlugin | |
assert PpmImagePlugin | |
except ImportError: | |
pass | |
try: | |
from . import PngImagePlugin | |
assert PngImagePlugin | |
except ImportError: | |
pass | |
_initialized = 1 | |
def init(): | |
""" | |
Explicitly initializes the Python Imaging Library. This function | |
loads all available file format drivers. | |
It is called when opening or saving images if :py:meth:`~preinit()` is | |
insufficient, and by :py:meth:`~PIL.features.pilinfo`. | |
""" | |
global _initialized | |
if _initialized >= 2: | |
return 0 | |
parent_name = __name__.rpartition(".")[0] | |
for plugin in _plugins: | |
try: | |
logger.debug("Importing %s", plugin) | |
__import__(f"{parent_name}.{plugin}", globals(), locals(), []) | |
except ImportError as e: | |
logger.debug("Image: failed to import %s: %s", plugin, e) | |
if OPEN or SAVE: | |
_initialized = 2 | |
return 1 | |
# -------------------------------------------------------------------- | |
# Codec factories (used by tobytes/frombytes and ImageFile.load) | |
def _getdecoder(mode, decoder_name, args, extra=()): | |
# tweak arguments | |
if args is None: | |
args = () | |
elif not isinstance(args, tuple): | |
args = (args,) | |
try: | |
decoder = DECODERS[decoder_name] | |
except KeyError: | |
pass | |
else: | |
return decoder(mode, *args + extra) | |
try: | |
# get decoder | |
decoder = getattr(core, decoder_name + "_decoder") | |
except AttributeError as e: | |
msg = f"decoder {decoder_name} not available" | |
raise OSError(msg) from e | |
return decoder(mode, *args + extra) | |
def _getencoder(mode, encoder_name, args, extra=()): | |
# tweak arguments | |
if args is None: | |
args = () | |
elif not isinstance(args, tuple): | |
args = (args,) | |
try: | |
encoder = ENCODERS[encoder_name] | |
except KeyError: | |
pass | |
else: | |
return encoder(mode, *args + extra) | |
try: | |
# get encoder | |
encoder = getattr(core, encoder_name + "_encoder") | |
except AttributeError as e: | |
msg = f"encoder {encoder_name} not available" | |
raise OSError(msg) from e | |
return encoder(mode, *args + extra) | |
# -------------------------------------------------------------------- | |
# Simple expression analyzer | |
class _E: | |
def __init__(self, scale, offset) -> None: | |
self.scale = scale | |
self.offset = offset | |
def __neg__(self): | |
return _E(-self.scale, -self.offset) | |
def __add__(self, other): | |
if isinstance(other, _E): | |
return _E(self.scale + other.scale, self.offset + other.offset) | |
return _E(self.scale, self.offset + other) | |
__radd__ = __add__ | |
def __sub__(self, other): | |
return self + -other | |
def __rsub__(self, other): | |
return other + -self | |
def __mul__(self, other): | |
if isinstance(other, _E): | |
return NotImplemented | |
return _E(self.scale * other, self.offset * other) | |
__rmul__ = __mul__ | |
def __truediv__(self, other): | |
if isinstance(other, _E): | |
return NotImplemented | |
return _E(self.scale / other, self.offset / other) | |
def _getscaleoffset(expr): | |
a = expr(_E(1, 0)) | |
return (a.scale, a.offset) if isinstance(a, _E) else (0, a) | |
# -------------------------------------------------------------------- | |
# Implementation wrapper | |
class Image: | |
""" | |
This class represents an image object. To create | |
:py:class:`~PIL.Image.Image` objects, use the appropriate factory | |
functions. There's hardly ever any reason to call the Image constructor | |
directly. | |
* :py:func:`~PIL.Image.open` | |
* :py:func:`~PIL.Image.new` | |
* :py:func:`~PIL.Image.frombytes` | |
""" | |
format: str | None = None | |
format_description: str | None = None | |
_close_exclusive_fp_after_loading = True | |
def __init__(self): | |
# FIXME: take "new" parameters / other image? | |
# FIXME: turn mode and size into delegating properties? | |
self.im = None | |
self._mode = "" | |
self._size = (0, 0) | |
self.palette = None | |
self.info = {} | |
self.readonly = 0 | |
self.pyaccess = None | |
self._exif = None | |
def width(self) -> int: | |
return self.size[0] | |
def height(self) -> int: | |
return self.size[1] | |
def size(self) -> tuple[int, int]: | |
return self._size | |
def mode(self): | |
return self._mode | |
def _new(self, im) -> Image: | |
new = Image() | |
new.im = im | |
new._mode = im.mode | |
new._size = im.size | |
if im.mode in ("P", "PA"): | |
if self.palette: | |
new.palette = self.palette.copy() | |
else: | |
from . import ImagePalette | |
new.palette = ImagePalette.ImagePalette() | |
new.info = self.info.copy() | |
return new | |
# Context manager support | |
def __enter__(self): | |
return self | |
def _close_fp(self): | |
if getattr(self, "_fp", False): | |
if self._fp != self.fp: | |
self._fp.close() | |
self._fp = DeferredError(ValueError("Operation on closed image")) | |
if self.fp: | |
self.fp.close() | |
def __exit__(self, *args): | |
if hasattr(self, "fp"): | |
if getattr(self, "_exclusive_fp", False): | |
self._close_fp() | |
self.fp = None | |
def close(self) -> None: | |
""" | |
Closes the file pointer, if possible. | |
This operation will destroy the image core and release its memory. | |
The image data will be unusable afterward. | |
This function is required to close images that have multiple frames or | |
have not had their file read and closed by the | |
:py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for | |
more information. | |
""" | |
if hasattr(self, "fp"): | |
try: | |
self._close_fp() | |
self.fp = None | |
except Exception as msg: | |
logger.debug("Error closing: %s", msg) | |
if getattr(self, "map", None): | |
self.map = None | |
# Instead of simply setting to None, we're setting up a | |
# deferred error that will better explain that the core image | |
# object is gone. | |
self.im = DeferredError(ValueError("Operation on closed image")) | |
def _copy(self) -> None: | |
self.load() | |
self.im = self.im.copy() | |
self.pyaccess = None | |
self.readonly = 0 | |
def _ensure_mutable(self) -> None: | |
if self.readonly: | |
self._copy() | |
else: | |
self.load() | |
def _dump( | |
self, file: str | None = None, format: str | None = None, **options | |
) -> str: | |
suffix = "" | |
if format: | |
suffix = "." + format | |
if not file: | |
f, filename = tempfile.mkstemp(suffix) | |
os.close(f) | |
else: | |
filename = file | |
if not filename.endswith(suffix): | |
filename = filename + suffix | |
self.load() | |
if not format or format == "PPM": | |
self.im.save_ppm(filename) | |
else: | |
self.save(filename, format, **options) | |
return filename | |
def __eq__(self, other): | |
return ( | |
self.__class__ is other.__class__ | |
and self.mode == other.mode | |
and self.size == other.size | |
and self.info == other.info | |
and self.getpalette() == other.getpalette() | |
and self.tobytes() == other.tobytes() | |
) | |
def __repr__(self) -> str: | |
return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % ( | |
self.__class__.__module__, | |
self.__class__.__name__, | |
self.mode, | |
self.size[0], | |
self.size[1], | |
id(self), | |
) | |
def _repr_pretty_(self, p, cycle) -> None: | |
"""IPython plain text display support""" | |
# Same as __repr__ but without unpredictable id(self), | |
# to keep Jupyter notebook `text/plain` output stable. | |
p.text( | |
"<%s.%s image mode=%s size=%dx%d>" | |
% ( | |
self.__class__.__module__, | |
self.__class__.__name__, | |
self.mode, | |
self.size[0], | |
self.size[1], | |
) | |
) | |
def _repr_image(self, image_format, **kwargs): | |
"""Helper function for iPython display hook. | |
:param image_format: Image format. | |
:returns: image as bytes, saved into the given format. | |
""" | |
b = io.BytesIO() | |
try: | |
self.save(b, image_format, **kwargs) | |
except Exception: | |
return None | |
return b.getvalue() | |
def _repr_png_(self): | |
"""iPython display hook support for PNG format. | |
:returns: PNG version of the image as bytes | |
""" | |
return self._repr_image("PNG", compress_level=1) | |
def _repr_jpeg_(self): | |
"""iPython display hook support for JPEG format. | |
:returns: JPEG version of the image as bytes | |
""" | |
return self._repr_image("JPEG") | |
def __array_interface__(self): | |
# numpy array interface support | |
new = {"version": 3} | |
try: | |
if self.mode == "1": | |
# Binary images need to be extended from bits to bytes | |
# See: https://github.com/python-pillow/Pillow/issues/350 | |
new["data"] = self.tobytes("raw", "L") | |
else: | |
new["data"] = self.tobytes() | |
except Exception as e: | |
if not isinstance(e, (MemoryError, RecursionError)): | |
try: | |
import numpy | |
from packaging.version import parse as parse_version | |
except ImportError: | |
pass | |
else: | |
if parse_version(numpy.__version__) < parse_version("1.23"): | |
warnings.warn(str(e)) | |
raise | |
new["shape"], new["typestr"] = _conv_type_shape(self) | |
return new | |
def __getstate__(self): | |
im_data = self.tobytes() # load image first | |
return [self.info, self.mode, self.size, self.getpalette(), im_data] | |
def __setstate__(self, state) -> None: | |
Image.__init__(self) | |
info, mode, size, palette, data = state | |
self.info = info | |
self._mode = mode | |
self._size = size | |
self.im = core.new(mode, size) | |
if mode in ("L", "LA", "P", "PA") and palette: | |
self.putpalette(palette) | |
self.frombytes(data) | |
def tobytes(self, encoder_name: str = "raw", *args) -> bytes: | |
""" | |
Return image as a bytes object. | |
.. warning:: | |
This method returns the raw image data from the internal | |
storage. For compressed image data (e.g. PNG, JPEG) use | |
:meth:`~.save`, with a BytesIO parameter for in-memory | |
data. | |
:param encoder_name: What encoder to use. The default is to | |
use the standard "raw" encoder. | |
A list of C encoders can be seen under | |
codecs section of the function array in | |
:file:`_imaging.c`. Python encoders are | |
registered within the relevant plugins. | |
:param args: Extra arguments to the encoder. | |
:returns: A :py:class:`bytes` object. | |
""" | |
# may pass tuple instead of argument list | |
if len(args) == 1 and isinstance(args[0], tuple): | |
args = args[0] | |
if encoder_name == "raw" and args == (): | |
args = self.mode | |
self.load() | |
if self.width == 0 or self.height == 0: | |
return b"" | |
# unpack data | |
e = _getencoder(self.mode, encoder_name, args) | |
e.setimage(self.im) | |
bufsize = max(65536, self.size[0] * 4) # see RawEncode.c | |
output = [] | |
while True: | |
bytes_consumed, errcode, data = e.encode(bufsize) | |
output.append(data) | |
if errcode: | |
break | |
if errcode < 0: | |
msg = f"encoder error {errcode} in tobytes" | |
raise RuntimeError(msg) | |
return b"".join(output) | |
def tobitmap(self, name: str = "image") -> bytes: | |
""" | |
Returns the image converted to an X11 bitmap. | |
.. note:: This method only works for mode "1" images. | |
:param name: The name prefix to use for the bitmap variables. | |
:returns: A string containing an X11 bitmap. | |
:raises ValueError: If the mode is not "1" | |
""" | |
self.load() | |
if self.mode != "1": | |
msg = "not a bitmap" | |
raise ValueError(msg) | |
data = self.tobytes("xbm") | |
return b"".join( | |
[ | |
f"#define {name}_width {self.size[0]}\n".encode("ascii"), | |
f"#define {name}_height {self.size[1]}\n".encode("ascii"), | |
f"static char {name}_bits[] = {{\n".encode("ascii"), | |
data, | |
b"};", | |
] | |
) | |
def frombytes(self, data: bytes, decoder_name: str = "raw", *args) -> None: | |
""" | |
Loads this image with pixel data from a bytes object. | |
This method is similar to the :py:func:`~PIL.Image.frombytes` function, | |
but loads data into this image instead of creating a new image object. | |
""" | |
if self.width == 0 or self.height == 0: | |
return | |
# may pass tuple instead of argument list | |
if len(args) == 1 and isinstance(args[0], tuple): | |
args = args[0] | |
# default format | |
if decoder_name == "raw" and args == (): | |
args = self.mode | |
# unpack data | |
d = _getdecoder(self.mode, decoder_name, args) | |
d.setimage(self.im) | |
s = d.decode(data) | |
if s[0] >= 0: | |
msg = "not enough image data" | |
raise ValueError(msg) | |
if s[1] != 0: | |
msg = "cannot decode image data" | |
raise ValueError(msg) | |
def load(self): | |
""" | |
Allocates storage for the image and loads the pixel data. In | |
normal cases, you don't need to call this method, since the | |
Image class automatically loads an opened image when it is | |
accessed for the first time. | |
If the file associated with the image was opened by Pillow, then this | |
method will close it. The exception to this is if the image has | |
multiple frames, in which case the file will be left open for seek | |
operations. See :ref:`file-handling` for more information. | |
:returns: An image access object. | |
:rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess` | |
""" | |
if self.im is not None and self.palette and self.palette.dirty: | |
# realize palette | |
mode, arr = self.palette.getdata() | |
self.im.putpalette(mode, arr) | |
self.palette.dirty = 0 | |
self.palette.rawmode = None | |
if "transparency" in self.info and mode in ("LA", "PA"): | |
if isinstance(self.info["transparency"], int): | |
self.im.putpalettealpha(self.info["transparency"], 0) | |
else: | |
self.im.putpalettealphas(self.info["transparency"]) | |
self.palette.mode = "RGBA" | |
else: | |
palette_mode = "RGBA" if mode.startswith("RGBA") else "RGB" | |
self.palette.mode = palette_mode | |
self.palette.palette = self.im.getpalette(palette_mode, palette_mode) | |
if self.im is not None: | |
if cffi and USE_CFFI_ACCESS: | |
if self.pyaccess: | |
return self.pyaccess | |
from . import PyAccess | |
self.pyaccess = PyAccess.new(self, self.readonly) | |
if self.pyaccess: | |
return self.pyaccess | |
return self.im.pixel_access(self.readonly) | |
def verify(self): | |
""" | |
Verifies the contents of a file. For data read from a file, this | |
method attempts to determine if the file is broken, without | |
actually decoding the image data. If this method finds any | |
problems, it raises suitable exceptions. If you need to load | |
the image after using this method, you must reopen the image | |
file. | |
""" | |
pass | |
def convert( | |
self, | |
mode: str | None = None, | |
matrix: tuple[float, ...] | None = None, | |
dither: Dither | None = None, | |
palette: Palette = Palette.WEB, | |
colors: int = 256, | |
) -> Image: | |
""" | |
Returns a converted copy of this image. For the "P" mode, this | |
method translates pixels through the palette. If mode is | |
omitted, a mode is chosen so that all information in the image | |
and the palette can be represented without a palette. | |
This supports all possible conversions between "L", "RGB" and "CMYK". The | |
``matrix`` argument only supports "L" and "RGB". | |
When translating a color image to grayscale (mode "L"), | |
the library uses the ITU-R 601-2 luma transform:: | |
L = R * 299/1000 + G * 587/1000 + B * 114/1000 | |
The default method of converting a grayscale ("L") or "RGB" | |
image into a bilevel (mode "1") image uses Floyd-Steinberg | |
dither to approximate the original image luminosity levels. If | |
dither is ``None``, all values larger than 127 are set to 255 (white), | |
all other values to 0 (black). To use other thresholds, use the | |
:py:meth:`~PIL.Image.Image.point` method. | |
When converting from "RGBA" to "P" without a ``matrix`` argument, | |
this passes the operation to :py:meth:`~PIL.Image.Image.quantize`, | |
and ``dither`` and ``palette`` are ignored. | |
When converting from "PA", if an "RGBA" palette is present, the alpha | |
channel from the image will be used instead of the values from the palette. | |
:param mode: The requested mode. See: :ref:`concept-modes`. | |
:param matrix: An optional conversion matrix. If given, this | |
should be 4- or 12-tuple containing floating point values. | |
:param dither: Dithering method, used when converting from | |
mode "RGB" to "P" or from "RGB" or "L" to "1". | |
Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` | |
(default). Note that this is not used when ``matrix`` is supplied. | |
:param palette: Palette to use when converting from mode "RGB" | |
to "P". Available palettes are :data:`Palette.WEB` or | |
:data:`Palette.ADAPTIVE`. | |
:param colors: Number of colors to use for the :data:`Palette.ADAPTIVE` | |
palette. Defaults to 256. | |
:rtype: :py:class:`~PIL.Image.Image` | |
:returns: An :py:class:`~PIL.Image.Image` object. | |
""" | |
self.load() | |
has_transparency = "transparency" in self.info | |
if not mode and self.mode == "P": | |
# determine default mode | |
if self.palette: | |
mode = self.palette.mode | |
else: | |
mode = "RGB" | |
if mode == "RGB" and has_transparency: | |
mode = "RGBA" | |
if not mode or (mode == self.mode and not matrix): | |
return self.copy() | |
if matrix: | |
# matrix conversion | |
if mode not in ("L", "RGB"): | |
msg = "illegal conversion" | |
raise ValueError(msg) | |
im = self.im.convert_matrix(mode, matrix) | |
new_im = self._new(im) | |
if has_transparency and self.im.bands == 3: | |
transparency = new_im.info["transparency"] | |
def convert_transparency(m, v): | |
v = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5 | |
return max(0, min(255, int(v))) | |
if mode == "L": | |
transparency = convert_transparency(matrix, transparency) | |
elif len(mode) == 3: | |
transparency = tuple( | |
convert_transparency(matrix[i * 4 : i * 4 + 4], transparency) | |
for i in range(0, len(transparency)) | |
) | |
new_im.info["transparency"] = transparency | |
return new_im | |
if mode == "P" and self.mode == "RGBA": | |
return self.quantize(colors) | |
trns = None | |
delete_trns = False | |
# transparency handling | |
if has_transparency: | |
if (self.mode in ("1", "L", "I", "I;16") and mode in ("LA", "RGBA")) or ( | |
self.mode == "RGB" and mode in ("La", "LA", "RGBa", "RGBA") | |
): | |
# Use transparent conversion to promote from transparent | |
# color to an alpha channel. | |
new_im = self._new( | |
self.im.convert_transparent(mode, self.info["transparency"]) | |
) | |
del new_im.info["transparency"] | |
return new_im | |
elif self.mode in ("L", "RGB", "P") and mode in ("L", "RGB", "P"): | |
t = self.info["transparency"] | |
if isinstance(t, bytes): | |
# Dragons. This can't be represented by a single color | |
warnings.warn( | |
"Palette images with Transparency expressed in bytes should be " | |
"converted to RGBA images" | |
) | |
delete_trns = True | |
else: | |
# get the new transparency color. | |
# use existing conversions | |
trns_im = new(self.mode, (1, 1)) | |
if self.mode == "P": | |
trns_im.putpalette(self.palette) | |
if isinstance(t, tuple): | |
err = "Couldn't allocate a palette color for transparency" | |
try: | |
t = trns_im.palette.getcolor(t, self) | |
except ValueError as e: | |
if str(e) == "cannot allocate more than 256 colors": | |
# If all 256 colors are in use, | |
# then there is no need for transparency | |
t = None | |
else: | |
raise ValueError(err) from e | |
if t is None: | |
trns = None | |
else: | |
trns_im.putpixel((0, 0), t) | |
if mode in ("L", "RGB"): | |
trns_im = trns_im.convert(mode) | |
else: | |
# can't just retrieve the palette number, got to do it | |
# after quantization. | |
trns_im = trns_im.convert("RGB") | |
trns = trns_im.getpixel((0, 0)) | |
elif self.mode == "P" and mode in ("LA", "PA", "RGBA"): | |
t = self.info["transparency"] | |
delete_trns = True | |
if isinstance(t, bytes): | |
self.im.putpalettealphas(t) | |
elif isinstance(t, int): | |
self.im.putpalettealpha(t, 0) | |
else: | |
msg = "Transparency for P mode should be bytes or int" | |
raise ValueError(msg) | |
if mode == "P" and palette == Palette.ADAPTIVE: | |
im = self.im.quantize(colors) | |
new_im = self._new(im) | |
from . import ImagePalette | |
new_im.palette = ImagePalette.ImagePalette( | |
"RGB", new_im.im.getpalette("RGB") | |
) | |
if delete_trns: | |
# This could possibly happen if we requantize to fewer colors. | |
# The transparency would be totally off in that case. | |
del new_im.info["transparency"] | |
if trns is not None: | |
try: | |
new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) | |
except Exception: | |
# if we can't make a transparent color, don't leave the old | |
# transparency hanging around to mess us up. | |
del new_im.info["transparency"] | |
warnings.warn("Couldn't allocate palette entry for transparency") | |
return new_im | |
if "LAB" in (self.mode, mode): | |
other_mode = mode if self.mode == "LAB" else self.mode | |
if other_mode in ("RGB", "RGBA", "RGBX"): | |
from . import ImageCms | |
srgb = ImageCms.createProfile("sRGB") | |
lab = ImageCms.createProfile("LAB") | |
profiles = [lab, srgb] if self.mode == "LAB" else [srgb, lab] | |
transform = ImageCms.buildTransform( | |
profiles[0], profiles[1], self.mode, mode | |
) | |
return transform.apply(self) | |
# colorspace conversion | |
if dither is None: | |
dither = Dither.FLOYDSTEINBERG | |
try: | |
im = self.im.convert(mode, dither) | |
except ValueError: | |
try: | |
# normalize source image and try again | |
modebase = getmodebase(self.mode) | |
if modebase == self.mode: | |
raise | |
im = self.im.convert(modebase) | |
im = im.convert(mode, dither) | |
except KeyError as e: | |
msg = "illegal conversion" | |
raise ValueError(msg) from e | |
new_im = self._new(im) | |
if mode == "P" and palette != Palette.ADAPTIVE: | |
from . import ImagePalette | |
new_im.palette = ImagePalette.ImagePalette("RGB", im.getpalette("RGB")) | |
if delete_trns: | |
# crash fail if we leave a bytes transparency in an rgb/l mode. | |
del new_im.info["transparency"] | |
if trns is not None: | |
if new_im.mode == "P": | |
try: | |
new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) | |
except ValueError as e: | |
del new_im.info["transparency"] | |
if str(e) != "cannot allocate more than 256 colors": | |
# If all 256 colors are in use, | |
# then there is no need for transparency | |
warnings.warn( | |
"Couldn't allocate palette entry for transparency" | |
) | |
else: | |
new_im.info["transparency"] = trns | |
return new_im | |
def quantize( | |
self, | |
colors: int = 256, | |
method: Quantize | None = None, | |
kmeans: int = 0, | |
palette=None, | |
dither: Dither = Dither.FLOYDSTEINBERG, | |
) -> Image: | |
""" | |
Convert the image to 'P' mode with the specified number | |
of colors. | |
:param colors: The desired number of colors, <= 256 | |
:param method: :data:`Quantize.MEDIANCUT` (median cut), | |
:data:`Quantize.MAXCOVERAGE` (maximum coverage), | |
:data:`Quantize.FASTOCTREE` (fast octree), | |
:data:`Quantize.LIBIMAGEQUANT` (libimagequant; check support | |
using :py:func:`PIL.features.check_feature` with | |
``feature="libimagequant"``). | |
By default, :data:`Quantize.MEDIANCUT` will be used. | |
The exception to this is RGBA images. :data:`Quantize.MEDIANCUT` | |
and :data:`Quantize.MAXCOVERAGE` do not support RGBA images, so | |
:data:`Quantize.FASTOCTREE` is used by default instead. | |
:param kmeans: Integer greater than or equal to zero. | |
:param palette: Quantize to the palette of given | |
:py:class:`PIL.Image.Image`. | |
:param dither: Dithering method, used when converting from | |
mode "RGB" to "P" or from "RGB" or "L" to "1". | |
Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` | |
(default). | |
:returns: A new image | |
""" | |
self.load() | |
if method is None: | |
# defaults: | |
method = Quantize.MEDIANCUT | |
if self.mode == "RGBA": | |
method = Quantize.FASTOCTREE | |
if self.mode == "RGBA" and method not in ( | |
Quantize.FASTOCTREE, | |
Quantize.LIBIMAGEQUANT, | |
): | |
# Caller specified an invalid mode. | |
msg = ( | |
"Fast Octree (method == 2) and libimagequant (method == 3) " | |
"are the only valid methods for quantizing RGBA images" | |
) | |
raise ValueError(msg) | |
if palette: | |
# use palette from reference image | |
palette.load() | |
if palette.mode != "P": | |
msg = "bad mode for palette image" | |
raise ValueError(msg) | |
if self.mode not in {"RGB", "L"}: | |
msg = "only RGB or L mode images can be quantized to a palette" | |
raise ValueError(msg) | |
im = self.im.convert("P", dither, palette.im) | |
new_im = self._new(im) | |
new_im.palette = palette.palette.copy() | |
return new_im | |
if kmeans < 0: | |
msg = "kmeans must not be negative" | |
raise ValueError(msg) | |
im = self._new(self.im.quantize(colors, method, kmeans)) | |
from . import ImagePalette | |
mode = im.im.getpalettemode() | |
palette = im.im.getpalette(mode, mode)[: colors * len(mode)] | |
im.palette = ImagePalette.ImagePalette(mode, palette) | |
return im | |
def copy(self) -> Image: | |
""" | |
Copies this image. Use this method if you wish to paste things | |
into an image, but still retain the original. | |
:rtype: :py:class:`~PIL.Image.Image` | |
:returns: An :py:class:`~PIL.Image.Image` object. | |
""" | |
self.load() | |
return self._new(self.im.copy()) | |
__copy__ = copy | |
def crop(self, box: tuple[int, int, int, int] | None = None) -> Image: | |
""" | |
Returns a rectangular region from this image. The box is a | |
4-tuple defining the left, upper, right, and lower pixel | |
coordinate. See :ref:`coordinate-system`. | |
Note: Prior to Pillow 3.4.0, this was a lazy operation. | |
:param box: The crop rectangle, as a (left, upper, right, lower)-tuple. | |
:rtype: :py:class:`~PIL.Image.Image` | |
:returns: An :py:class:`~PIL.Image.Image` object. | |
""" | |
if box is None: | |
return self.copy() | |
if box[2] < box[0]: | |
msg = "Coordinate 'right' is less than 'left'" | |
raise ValueError(msg) | |
elif box[3] < box[1]: | |
msg = "Coordinate 'lower' is less than 'upper'" | |
raise ValueError(msg) | |
self.load() | |
return self._new(self._crop(self.im, box)) | |
def _crop(self, im, box): | |
""" | |
Returns a rectangular region from the core image object im. | |
This is equivalent to calling im.crop((x0, y0, x1, y1)), but | |
includes additional sanity checks. | |
:param im: a core image object | |
:param box: The crop rectangle, as a (left, upper, right, lower)-tuple. | |
:returns: A core image object. | |
""" | |
x0, y0, x1, y1 = map(int, map(round, box)) | |
absolute_values = (abs(x1 - x0), abs(y1 - y0)) | |
_decompression_bomb_check(absolute_values) | |
return im.crop((x0, y0, x1, y1)) | |
def draft(self, mode, size): | |
""" | |
Configures the image file loader so it returns a version of the | |
image that as closely as possible matches the given mode and | |
size. For example, you can use this method to convert a color | |
JPEG to grayscale while loading it. | |
If any changes are made, returns a tuple with the chosen ``mode`` and | |
``box`` with coordinates of the original image within the altered one. | |
Note that this method modifies the :py:class:`~PIL.Image.Image` object | |
in place. If the image has already been loaded, this method has no | |
effect. | |
Note: This method is not implemented for most images. It is | |
currently implemented only for JPEG and MPO images. | |
:param mode: The requested mode. | |
:param size: The requested size in pixels, as a 2-tuple: | |
(width, height). | |
""" | |
pass | |
def _expand(self, xmargin, ymargin=None): | |
if ymargin is None: | |
ymargin = xmargin | |
self.load() | |
return self._new(self.im.expand(xmargin, ymargin)) | |
def filter(self, filter): | |
""" | |
Filters this image using the given filter. For a list of | |
available filters, see the :py:mod:`~PIL.ImageFilter` module. | |
:param filter: Filter kernel. | |
:returns: An :py:class:`~PIL.Image.Image` object.""" | |
from . import ImageFilter | |
self.load() | |
if isinstance(filter, Callable): | |
filter = filter() | |
if not hasattr(filter, "filter"): | |
msg = "filter argument should be ImageFilter.Filter instance or class" | |
raise TypeError(msg) | |
multiband = isinstance(filter, ImageFilter.MultibandFilter) | |
if self.im.bands == 1 or multiband: | |
return self._new(filter.filter(self.im)) | |
ims = [ | |
self._new(filter.filter(self.im.getband(c))) for c in range(self.im.bands) | |
] | |
return merge(self.mode, ims) | |
def getbands(self) -> tuple[str, ...]: | |
""" | |
Returns a tuple containing the name of each band in this image. | |
For example, ``getbands`` on an RGB image returns ("R", "G", "B"). | |
:returns: A tuple containing band names. | |
:rtype: tuple | |
""" | |
return ImageMode.getmode(self.mode).bands | |
def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int]: | |
""" | |
Calculates the bounding box of the non-zero regions in the | |
image. | |
:param alpha_only: Optional flag, defaulting to ``True``. | |
If ``True`` and the image has an alpha channel, trim transparent pixels. | |
Otherwise, trim pixels when all channels are zero. | |
Keyword-only argument. | |
:returns: The bounding box is returned as a 4-tuple defining the | |
left, upper, right, and lower pixel coordinate. See | |
:ref:`coordinate-system`. If the image is completely empty, this | |
method returns None. | |
""" | |
self.load() | |
return self.im.getbbox(alpha_only) | |
def getcolors(self, maxcolors: int = 256): | |
""" | |
Returns a list of colors used in this image. | |
The colors will be in the image's mode. For example, an RGB image will | |
return a tuple of (red, green, blue) color values, and a P image will | |
return the index of the color in the palette. | |
:param maxcolors: Maximum number of colors. If this number is | |
exceeded, this method returns None. The default limit is | |
256 colors. | |
:returns: An unsorted list of (count, pixel) values. | |
""" | |
self.load() | |
if self.mode in ("1", "L", "P"): | |
h = self.im.histogram() | |
out = [(h[i], i) for i in range(256) if h[i]] | |
if len(out) > maxcolors: | |
return None | |
return out | |
return self.im.getcolors(maxcolors) | |
def getdata(self, band: int | None = None): | |
""" | |
Returns the contents of this image as a sequence object | |
containing pixel values. The sequence object is flattened, so | |
that values for line one follow directly after the values of | |
line zero, and so on. | |
Note that the sequence object returned by this method is an | |
internal PIL data type, which only supports certain sequence | |
operations. To convert it to an ordinary sequence (e.g. for | |
printing), use ``list(im.getdata())``. | |
:param band: What band to return. The default is to return | |
all bands. To return a single band, pass in the index | |
value (e.g. 0 to get the "R" band from an "RGB" image). | |
:returns: A sequence-like object. | |
""" | |
self.load() | |
if band is not None: | |
return self.im.getband(band) | |
return self.im # could be abused | |
def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]: | |
""" | |
Gets the minimum and maximum pixel values for each band in | |
the image. | |
:returns: For a single-band image, a 2-tuple containing the | |
minimum and maximum pixel value. For a multi-band image, | |
a tuple containing one 2-tuple for each band. | |
""" | |
self.load() | |
if self.im.bands > 1: | |
return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands)) | |
return self.im.getextrema() | |
def _getxmp(self, xmp_tags): | |
def get_name(tag): | |
return re.sub("^{[^}]+}", "", tag) | |
def get_value(element): | |
value = {get_name(k): v for k, v in element.attrib.items()} | |
children = list(element) | |
if children: | |
for child in children: | |
name = get_name(child.tag) | |
child_value = get_value(child) | |
if name in value: | |
if not isinstance(value[name], list): | |
value[name] = [value[name]] | |
value[name].append(child_value) | |
else: | |
value[name] = child_value | |
elif value: | |
if element.text: | |
value["text"] = element.text | |
else: | |
return element.text | |
return value | |
if ElementTree is None: | |
warnings.warn("XMP data cannot be read without defusedxml dependency") | |
return {} | |
else: | |
root = ElementTree.fromstring(xmp_tags) | |
return {get_name(root.tag): get_value(root)} | |
def getexif(self) -> Exif: | |
""" | |
Gets EXIF data from the image. | |
:returns: an :py:class:`~PIL.Image.Exif` object. | |
""" | |
if self._exif is None: | |
self._exif = Exif() | |
elif self._exif._loaded: | |
return self._exif | |
self._exif._loaded = True | |
exif_info = self.info.get("exif") | |
if exif_info is None: | |
if "Raw profile type exif" in self.info: | |
exif_info = bytes.fromhex( | |
"".join(self.info["Raw profile type exif"].split("\n")[3:]) | |
) | |
elif hasattr(self, "tag_v2"): | |
self._exif.bigtiff = self.tag_v2._bigtiff | |
self._exif.endian = self.tag_v2._endian | |
self._exif.load_from_fp(self.fp, self.tag_v2._offset) | |
if exif_info is not None: | |
self._exif.load(exif_info) | |
# XMP tags | |
if ExifTags.Base.Orientation not in self._exif: | |
xmp_tags = self.info.get("XML:com.adobe.xmp") | |
if xmp_tags: | |
match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) | |
if match: | |
self._exif[ExifTags.Base.Orientation] = int(match[2]) | |
return self._exif | |
def _reload_exif(self) -> None: | |
if self._exif is None or not self._exif._loaded: | |
return | |
self._exif._loaded = False | |
self.getexif() | |
def get_child_images(self): | |
child_images = [] | |
exif = self.getexif() | |
ifds = [] | |
if ExifTags.Base.SubIFDs in exif: | |
subifd_offsets = exif[ExifTags.Base.SubIFDs] | |
if subifd_offsets: | |
if not isinstance(subifd_offsets, tuple): | |
subifd_offsets = (subifd_offsets,) | |
for subifd_offset in subifd_offsets: | |
ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) | |
ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) | |
if ifd1 and ifd1.get(513): | |
ifds.append((ifd1, exif._info.next)) | |
offset = None | |
for ifd, ifd_offset in ifds: | |
current_offset = self.fp.tell() | |
if offset is None: | |
offset = current_offset | |
fp = self.fp | |
thumbnail_offset = ifd.get(513) | |
if thumbnail_offset is not None: | |
try: | |
thumbnail_offset += self._exif_offset | |
except AttributeError: | |
pass | |
self.fp.seek(thumbnail_offset) | |
data = self.fp.read(ifd.get(514)) | |
fp = io.BytesIO(data) | |
with open(fp) as im: | |
if thumbnail_offset is None: | |
im._frame_pos = [ifd_offset] | |
im._seek(0) | |
im.load() | |
child_images.append(im) | |
if offset is not None: | |
self.fp.seek(offset) | |
return child_images | |
def getim(self): | |
""" | |
Returns a capsule that points to the internal image memory. | |
:returns: A capsule object. | |
""" | |
self.load() | |
return self.im.ptr | |
def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None: | |
""" | |
Returns the image palette as a list. | |
:param rawmode: The mode in which to return the palette. ``None`` will | |
return the palette in its current mode. | |
.. versionadded:: 9.1.0 | |
:returns: A list of color values [r, g, b, ...], or None if the | |
image has no palette. | |
""" | |
self.load() | |
try: | |
mode = self.im.getpalettemode() | |
except ValueError: | |
return None # no palette | |
if rawmode is None: | |
rawmode = mode | |
return list(self.im.getpalette(mode, rawmode)) | |
def has_transparency_data(self) -> bool: | |
""" | |
Determine if an image has transparency data, whether in the form of an | |
alpha channel, a palette with an alpha channel, or a "transparency" key | |
in the info dictionary. | |
Note the image might still appear solid, if all of the values shown | |
within are opaque. | |
:returns: A boolean. | |
""" | |
return ( | |
self.mode in ("LA", "La", "PA", "RGBA", "RGBa") | |
or (self.mode == "P" and self.palette.mode.endswith("A")) | |
or "transparency" in self.info | |
) | |
def apply_transparency(self): | |
""" | |
If a P mode image has a "transparency" key in the info dictionary, | |
remove the key and instead apply the transparency to the palette. | |
Otherwise, the image is unchanged. | |
""" | |
if self.mode != "P" or "transparency" not in self.info: | |
return | |
from . import ImagePalette | |
palette = self.getpalette("RGBA") | |
transparency = self.info["transparency"] | |
if isinstance(transparency, bytes): | |
for i, alpha in enumerate(transparency): | |
palette[i * 4 + 3] = alpha | |
else: | |
palette[transparency * 4 + 3] = 0 | |
self.palette = ImagePalette.ImagePalette("RGBA", bytes(palette)) | |
self.palette.dirty = 1 | |
del self.info["transparency"] | |
def getpixel(self, xy): | |
""" | |
Returns the pixel value at a given position. | |
:param xy: The coordinate, given as (x, y). See | |
:ref:`coordinate-system`. | |
:returns: The pixel value. If the image is a multi-layer image, | |
this method returns a tuple. | |
""" | |
self.load() | |
if self.pyaccess: | |
return self.pyaccess.getpixel(xy) | |
return self.im.getpixel(tuple(xy)) | |
def getprojection(self) -> tuple[list[int], list[int]]: | |
""" | |
Get projection to x and y axes | |
:returns: Two sequences, indicating where there are non-zero | |
pixels along the X-axis and the Y-axis, respectively. | |
""" | |
self.load() | |
x, y = self.im.getprojection() | |
return list(x), list(y) | |
def histogram(self, mask: Image | None = None, extrema=None) -> list[int]: | |
""" | |
Returns a histogram for the image. The histogram is returned as a | |
list of pixel counts, one for each pixel value in the source | |
image. Counts are grouped into 256 bins for each band, even if | |
the image has more than 8 bits per band. If the image has more | |
than one band, the histograms for all bands are concatenated (for | |
example, the histogram for an "RGB" image contains 768 values). | |
A bilevel image (mode "1") is treated as a grayscale ("L") image | |
by this method. | |
If a mask is provided, the method returns a histogram for those | |
parts of the image where the mask image is non-zero. The mask | |
image must have the same size as the image, and be either a | |
bi-level image (mode "1") or a grayscale image ("L"). | |
:param mask: An optional mask. | |
:param extrema: An optional tuple of manually-specified extrema. | |
:returns: A list containing pixel counts. | |
""" | |
self.load() | |
if mask: | |
mask.load() | |
return self.im.histogram((0, 0), mask.im) | |
if self.mode in ("I", "F"): | |
if extrema is None: | |
extrema = self.getextrema() | |
return self.im.histogram(extrema) | |
return self.im.histogram() | |
def entropy(self, mask=None, extrema=None): | |
""" | |
Calculates and returns the entropy for the image. | |
A bilevel image (mode "1") is treated as a grayscale ("L") | |
image by this method. | |
If a mask is provided, the method employs the histogram for | |
those parts of the image where the mask image is non-zero. | |
The mask image must have the same size as the image, and be | |
either a bi-level image (mode "1") or a grayscale image ("L"). | |
:param mask: An optional mask. | |
:param extrema: An optional tuple of manually-specified extrema. | |
:returns: A float value representing the image entropy | |
""" | |
self.load() | |
if mask: | |
mask.load() | |
return self.im.entropy((0, 0), mask.im) | |
if self.mode in ("I", "F"): | |
if extrema is None: | |
extrema = self.getextrema() | |
return self.im.entropy(extrema) | |
return self.im.entropy() | |
def paste(self, im, box=None, mask=None) -> None: | |
""" | |
Pastes another image into this image. The box argument is either | |
a 2-tuple giving the upper left corner, a 4-tuple defining the | |
left, upper, right, and lower pixel coordinate, or None (same as | |
(0, 0)). See :ref:`coordinate-system`. If a 4-tuple is given, the size | |
of the pasted image must match the size of the region. | |
If the modes don't match, the pasted image is converted to the mode of | |
this image (see the :py:meth:`~PIL.Image.Image.convert` method for | |
details). | |
Instead of an image, the source can be a integer or tuple | |
containing pixel values. The method then fills the region | |
with the given color. When creating RGB images, you can | |
also use color strings as supported by the ImageColor module. | |
If a mask is given, this method updates only the regions | |
indicated by the mask. You can use either "1", "L", "LA", "RGBA" | |
or "RGBa" images (if present, the alpha band is used as mask). | |
Where the mask is 255, the given image is copied as is. Where | |
the mask is 0, the current value is preserved. Intermediate | |
values will mix the two images together, including their alpha | |
channels if they have them. | |
See :py:meth:`~PIL.Image.Image.alpha_composite` if you want to | |
combine images with respect to their alpha channels. | |
:param im: Source image or pixel value (integer or tuple). | |
:param box: An optional 4-tuple giving the region to paste into. | |
If a 2-tuple is used instead, it's treated as the upper left | |
corner. If omitted or None, the source is pasted into the | |
upper left corner. | |
If an image is given as the second argument and there is no | |
third, the box defaults to (0, 0), and the second argument | |
is interpreted as a mask image. | |
:param mask: An optional mask image. | |
""" | |
if isImageType(box) and mask is None: | |
# abbreviated paste(im, mask) syntax | |
mask = box | |
box = None | |
if box is None: | |
box = (0, 0) | |
if len(box) == 2: | |
# upper left corner given; get size from image or mask | |
if isImageType(im): | |
size = im.size | |
elif isImageType(mask): | |
size = mask.size | |
else: | |
# FIXME: use self.size here? | |
msg = "cannot determine region size; use 4-item box" | |
raise ValueError(msg) | |
box += (box[0] + size[0], box[1] + size[1]) | |
if isinstance(im, str): | |
from . import ImageColor | |
im = ImageColor.getcolor(im, self.mode) | |
elif isImageType(im): | |
im.load() | |
if self.mode != im.mode: | |
if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): | |
# should use an adapter for this! | |
im = im.convert(self.mode) | |
im = im.im | |
self._ensure_mutable() | |
if mask: | |
mask.load() | |
self.im.paste(im, box, mask.im) | |
else: | |
self.im.paste(im, box) | |
def alpha_composite(self, im, dest=(0, 0), source=(0, 0)): | |
"""'In-place' analog of Image.alpha_composite. Composites an image | |
onto this image. | |
:param im: image to composite over this one | |
:param dest: Optional 2 tuple (left, top) specifying the upper | |
left corner in this (destination) image. | |
:param source: Optional 2 (left, top) tuple for the upper left | |
corner in the overlay source image, or 4 tuple (left, top, right, | |
bottom) for the bounds of the source rectangle | |
Performance Note: Not currently implemented in-place in the core layer. | |
""" | |
if not isinstance(source, (list, tuple)): | |
msg = "Source must be a tuple" | |
raise ValueError(msg) | |
if not isinstance(dest, (list, tuple)): | |
msg = "Destination must be a tuple" | |
raise ValueError(msg) | |
if len(source) not in (2, 4): | |
msg = "Source must be a 2 or 4-tuple" | |
raise ValueError(msg) | |
if not len(dest) == 2: | |
msg = "Destination must be a 2-tuple" | |
raise ValueError(msg) | |
if min(source) < 0: | |
msg = "Source must be non-negative" | |
raise ValueError(msg) | |
if len(source) == 2: | |
source = source + im.size | |
# over image, crop if it's not the whole thing. | |
if source == (0, 0) + im.size: | |
overlay = im | |
else: | |
overlay = im.crop(source) | |
# target for the paste | |
box = dest + (dest[0] + overlay.width, dest[1] + overlay.height) | |
# destination image. don't copy if we're using the whole image. | |
if box == (0, 0) + self.size: | |
background = self | |
else: | |
background = self.crop(box) | |
result = alpha_composite(background, overlay) | |
self.paste(result, box) | |
def point(self, lut, mode: str | None = None) -> Image: | |
""" | |
Maps this image through a lookup table or function. | |
:param lut: A lookup table, containing 256 (or 65536 if | |
self.mode=="I" and mode == "L") values per band in the | |
image. A function can be used instead, it should take a | |
single argument. The function is called once for each | |
possible pixel value, and the resulting table is applied to | |
all bands of the image. | |
It may also be an :py:class:`~PIL.Image.ImagePointHandler` | |
object:: | |
class Example(Image.ImagePointHandler): | |
def point(self, data): | |
# Return result | |
:param mode: Output mode (default is same as input). This can only be used if | |
the source image has mode "L" or "P", and the output has mode "1" or the | |
source image mode is "I" and the output mode is "L". | |
:returns: An :py:class:`~PIL.Image.Image` object. | |
""" | |
self.load() | |
if isinstance(lut, ImagePointHandler): | |
return lut.point(self) | |
if callable(lut): | |
# if it isn't a list, it should be a function | |
if self.mode in ("I", "I;16", "F"): | |
# check if the function can be used with point_transform | |
# UNDONE wiredfool -- I think this prevents us from ever doing | |
# a gamma function point transform on > 8bit images. | |
scale, offset = _getscaleoffset(lut) | |
return self._new(self.im.point_transform(scale, offset)) | |
# for other modes, convert the function to a table | |
lut = [lut(i) for i in range(256)] * self.im.bands | |
if self.mode == "F": | |
# FIXME: _imaging returns a confusing error message for this case | |
msg = "point operation not supported for this mode" | |
raise ValueError(msg) | |
if mode != "F": | |
lut = [round(i) for i in lut] | |
return self._new(self.im.point(lut, mode)) | |
def putalpha(self, alpha): | |
""" | |
Adds or replaces the alpha layer in this image. If the image | |
does not have an alpha layer, it's converted to "LA" or "RGBA". | |
The new layer must be either "L" or "1". | |
:param alpha: The new alpha layer. This can either be an "L" or "1" | |
image having the same size as this image, or an integer or | |
other color value. | |
""" | |
self._ensure_mutable() | |
if self.mode not in ("LA", "PA", "RGBA"): | |
# attempt to promote self to a matching alpha mode | |
try: | |
mode = getmodebase(self.mode) + "A" | |
try: | |
self.im.setmode(mode) | |
except (AttributeError, ValueError) as e: | |
# do things the hard way | |
im = self.im.convert(mode) | |
if im.mode not in ("LA", "PA", "RGBA"): | |
msg = "alpha channel could not be added" | |
raise ValueError(msg) from e # sanity check | |
self.im = im | |
self.pyaccess = None | |
self._mode = self.im.mode | |
except KeyError as e: | |
msg = "illegal image mode" | |
raise ValueError(msg) from e | |
if self.mode in ("LA", "PA"): | |
band = 1 | |
else: | |
band = 3 | |
if isImageType(alpha): | |
# alpha layer | |
if alpha.mode not in ("1", "L"): | |
msg = "illegal image mode" | |
raise ValueError(msg) | |
alpha.load() | |
if alpha.mode == "1": | |
alpha = alpha.convert("L") | |
else: | |
# constant alpha | |
try: | |
self.im.fillband(band, alpha) | |
except (AttributeError, ValueError): | |
# do things the hard way | |
alpha = new("L", self.size, alpha) | |
else: | |
return | |
self.im.putband(alpha.im, band) | |
def putdata(self, data, scale=1.0, offset=0.0): | |
""" | |
Copies pixel data from a flattened sequence object into the image. The | |
values should start at the upper left corner (0, 0), continue to the | |
end of the line, followed directly by the first value of the second | |
line, and so on. Data will be read until either the image or the | |
sequence ends. The scale and offset values are used to adjust the | |
sequence values: **pixel = value*scale + offset**. | |
:param data: A flattened sequence object. | |
:param scale: An optional scale value. The default is 1.0. | |
:param offset: An optional offset value. The default is 0.0. | |
""" | |
self._ensure_mutable() | |
self.im.putdata(data, scale, offset) | |
def putpalette(self, data, rawmode="RGB") -> None: | |
""" | |
Attaches a palette to this image. The image must be a "P", "PA", "L" | |
or "LA" image. | |
The palette sequence must contain at most 256 colors, made up of one | |
integer value for each channel in the raw mode. | |
For example, if the raw mode is "RGB", then it can contain at most 768 | |
values, made up of red, green and blue values for the corresponding pixel | |
index in the 256 colors. | |
If the raw mode is "RGBA", then it can contain at most 1024 values, | |
containing red, green, blue and alpha values. | |
Alternatively, an 8-bit string may be used instead of an integer sequence. | |
:param data: A palette sequence (either a list or a string). | |
:param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a mode | |
that can be transformed to "RGB" or "RGBA" (e.g. "R", "BGR;15", "RGBA;L"). | |
""" | |
from . import ImagePalette | |
if self.mode not in ("L", "LA", "P", "PA"): | |
msg = "illegal image mode" | |
raise ValueError(msg) | |
if isinstance(data, ImagePalette.ImagePalette): | |
palette = ImagePalette.raw(data.rawmode, data.palette) | |
else: | |
if not isinstance(data, bytes): | |
data = bytes(data) | |
palette = ImagePalette.raw(rawmode, data) | |
self._mode = "PA" if "A" in self.mode else "P" | |
self.palette = palette | |
self.palette.mode = "RGB" | |
self.load() # install new palette | |
def putpixel(self, xy, value): | |
""" | |
Modifies the pixel at the given position. The color is given as | |
a single numerical value for single-band images, and a tuple for | |
multi-band images. In addition to this, RGB and RGBA tuples are | |
accepted for P and PA images. | |
Note that this method is relatively slow. For more extensive changes, | |
use :py:meth:`~PIL.Image.Image.paste` or the :py:mod:`~PIL.ImageDraw` | |
module instead. | |
See: | |
* :py:meth:`~PIL.Image.Image.paste` | |
* :py:meth:`~PIL.Image.Image.putdata` | |
* :py:mod:`~PIL.ImageDraw` | |
:param xy: The pixel coordinate, given as (x, y). See | |
:ref:`coordinate-system`. | |
:param value: The pixel value. | |
""" | |
if self.readonly: | |
self._copy() | |
self.load() | |
if self.pyaccess: | |
return self.pyaccess.putpixel(xy, value) | |
if ( | |
self.mode in ("P", "PA") | |
and isinstance(value, (list, tuple)) | |
and len(value) in [3, 4] | |
): | |
# RGB or RGBA value for a P or PA image | |
if self.mode == "PA": | |
alpha = value[3] if len(value) == 4 else 255 | |
value = value[:3] | |
value = self.palette.getcolor(value, self) | |
if self.mode == "PA": | |
value = (value, alpha) | |
return self.im.putpixel(xy, value) | |
def remap_palette(self, dest_map, source_palette=None): | |
""" | |
Rewrites the image to reorder the palette. | |
:param dest_map: A list of indexes into the original palette. | |
e.g. ``[1,0]`` would swap a two item palette, and ``list(range(256))`` | |
is the identity transform. | |
:param source_palette: Bytes or None. | |
:returns: An :py:class:`~PIL.Image.Image` object. | |
""" | |
from . import ImagePalette | |
if self.mode not in ("L", "P"): | |
msg = "illegal image mode" | |
raise ValueError(msg) | |
bands = 3 | |
palette_mode = "RGB" | |
if source_palette is None: | |
if self.mode == "P": | |
self.load() | |
palette_mode = self.im.getpalettemode() | |
if palette_mode == "RGBA": | |
bands = 4 | |
source_palette = self.im.getpalette(palette_mode, palette_mode) | |
else: # L-mode | |
source_palette = bytearray(i // 3 for i in range(768)) | |
palette_bytes = b"" | |
new_positions = [0] * 256 | |
# pick only the used colors from the palette | |
for i, oldPosition in enumerate(dest_map): | |
palette_bytes += source_palette[ | |
oldPosition * bands : oldPosition * bands + bands | |
] | |
new_positions[oldPosition] = i | |
# replace the palette color id of all pixel with the new id | |
# Palette images are [0..255], mapped through a 1 or 3 | |
# byte/color map. We need to remap the whole image | |
# from palette 1 to palette 2. New_positions is | |
# an array of indexes into palette 1. Palette 2 is | |
# palette 1 with any holes removed. | |
# We're going to leverage the convert mechanism to use the | |
# C code to remap the image from palette 1 to palette 2, | |
# by forcing the source image into 'L' mode and adding a | |
# mapping 'L' mode palette, then converting back to 'L' | |
# sans palette thus converting the image bytes, then | |
# assigning the optimized RGB palette. | |
# perf reference, 9500x4000 gif, w/~135 colors | |
# 14 sec prepatch, 1 sec postpatch with optimization forced. | |
mapping_palette = bytearray(new_positions) | |
m_im = self.copy() | |
m_im._mode = "P" | |
m_im.palette = ImagePalette.ImagePalette( | |
palette_mode, palette=mapping_palette * bands | |
) | |
# possibly set palette dirty, then | |
# m_im.putpalette(mapping_palette, 'L') # converts to 'P' | |
# or just force it. | |
# UNDONE -- this is part of the general issue with palettes | |
m_im.im.putpalette(palette_mode + ";L", m_im.palette.tobytes()) | |
m_im = m_im.convert("L") | |
m_im.putpalette(palette_bytes, palette_mode) | |
m_im.palette = ImagePalette.ImagePalette(palette_mode, palette=palette_bytes) | |
if "transparency" in self.info: | |
try: | |
m_im.info["transparency"] = dest_map.index(self.info["transparency"]) | |
except ValueError: | |
if "transparency" in m_im.info: | |
del m_im.info["transparency"] | |
return m_im | |
def _get_safe_box(self, size, resample, box): | |
"""Expands the box so it includes adjacent pixels | |
that may be used by resampling with the given resampling filter. | |
""" | |
filter_support = _filters_support[resample] - 0.5 | |
scale_x = (box[2] - box[0]) / size[0] | |
scale_y = (box[3] - box[1]) / size[1] | |
support_x = filter_support * scale_x | |
support_y = filter_support * scale_y | |
return ( | |
max(0, int(box[0] - support_x)), | |
max(0, int(box[1] - support_y)), | |
min(self.size[0], math.ceil(box[2] + support_x)), | |
min(self.size[1], math.ceil(box[3] + support_y)), | |
) | |
def resize(self, size, resample=None, box=None, reducing_gap=None) -> Image: | |
""" | |
Returns a resized copy of this image. | |
:param size: The requested size in pixels, as a 2-tuple: | |
(width, height). | |
:param resample: An optional resampling filter. This can be | |
one of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, | |
:py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, | |
:py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. | |
If the image has mode "1" or "P", it is always set to | |
:py:data:`Resampling.NEAREST`. If the image mode specifies a number | |
of bits, such as "I;16", then the default filter is | |
:py:data:`Resampling.NEAREST`. Otherwise, the default filter is | |
:py:data:`Resampling.BICUBIC`. See: :ref:`concept-filters`. | |
:param box: An optional 4-tuple of floats providing | |
the source image region to be scaled. | |
The values must be within (0, 0, width, height) rectangle. | |
If omitted or None, the entire source is used. | |
:param reducing_gap: Apply optimization by resizing the image | |
in two steps. First, reducing the image by integer times | |
using :py:meth:`~PIL.Image.Image.reduce`. | |
Second, resizing using regular resampling. The last step | |
changes size no less than by ``reducing_gap`` times. | |
``reducing_gap`` may be None (no first step is performed) | |
or should be greater than 1.0. The bigger ``reducing_gap``, | |
the closer the result to the fair resampling. | |
The smaller ``reducing_gap``, the faster resizing. | |
With ``reducing_gap`` greater or equal to 3.0, the result is | |
indistinguishable from fair resampling in most cases. | |
The default value is None (no optimization). | |
:returns: An :py:class:`~PIL.Image.Image` object. | |
""" | |
if resample is None: | |
type_special = ";" in self.mode | |
resample = Resampling.NEAREST if type_special else Resampling.BICUBIC | |
elif resample not in ( | |
Resampling.NEAREST, | |
Resampling.BILINEAR, | |
Resampling.BICUBIC, | |
Resampling.LANCZOS, | |
Resampling.BOX, | |
Resampling.HAMMING, | |
): | |
msg = f"Unknown resampling filter ({resample})." | |
filters = [ | |
f"{filter[1]} ({filter[0]})" | |
for filter in ( | |
(Resampling.NEAREST, "Image.Resampling.NEAREST"), | |
(Resampling.LANCZOS, "Image.Resampling.LANCZOS"), | |
(Resampling.BILINEAR, "Image.Resampling.BILINEAR"), | |
(Resampling.BICUBIC, "Image.Resampling.BICUBIC"), | |
(Resampling.BOX, "Image.Resampling.BOX"), | |
(Resampling.HAMMING, "Image.Resampling.HAMMING"), | |
) | |
] | |
msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] | |
raise ValueError(msg) | |
if reducing_gap is not None and reducing_gap < 1.0: | |
msg = "reducing_gap must be 1.0 or greater" | |
raise ValueError(msg) | |
size = tuple(size) | |
self.load() | |
if box is None: | |
box = (0, 0) + self.size | |
else: | |
box = tuple(box) | |
if self.size == size and box == (0, 0) + self.size: | |
return self.copy() | |
if self.mode in ("1", "P"): | |
resample = Resampling.NEAREST | |
if self.mode in ["LA", "RGBA"] and resample != Resampling.NEAREST: | |
im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) | |
im = im.resize(size, resample, box) | |
return im.convert(self.mode) | |
self.load() | |
if reducing_gap is not None and resample != Resampling.NEAREST: | |
factor_x = int((box[2] - box[0]) / size[0] / reducing_gap) or 1 | |
factor_y = int((box[3] - box[1]) / size[1] / reducing_gap) or 1 | |
if factor_x > 1 or factor_y > 1: | |
reduce_box = self._get_safe_box(size, resample, box) | |
factor = (factor_x, factor_y) | |
self = ( | |
self.reduce(factor, box=reduce_box) | |
if callable(self.reduce) | |
else Image.reduce(self, factor, box=reduce_box) | |
) | |
box = ( | |
(box[0] - reduce_box[0]) / factor_x, | |
(box[1] - reduce_box[1]) / factor_y, | |
(box[2] - reduce_box[0]) / factor_x, | |
(box[3] - reduce_box[1]) / factor_y, | |
) | |
return self._new(self.im.resize(size, resample, box)) | |
def reduce(self, factor, box=None): | |
""" | |
Returns a copy of the image reduced ``factor`` times. | |
If the size of the image is not dividable by ``factor``, | |
the resulting size will be rounded up. | |
:param factor: A greater than 0 integer or tuple of two integers | |
for width and height separately. | |
:param box: An optional 4-tuple of ints providing | |
the source image region to be reduced. | |
The values must be within ``(0, 0, width, height)`` rectangle. | |
If omitted or ``None``, the entire source is used. | |
""" | |
if not isinstance(factor, (list, tuple)): | |
factor = (factor, factor) | |
if box is None: | |
box = (0, 0) + self.size | |
else: | |
box = tuple(box) | |
if factor == (1, 1) and box == (0, 0) + self.size: | |
return self.copy() | |
if self.mode in ["LA", "RGBA"]: | |
im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) | |
im = im.reduce(factor, box) | |
return im.convert(self.mode) | |
self.load() | |
return self._new(self.im.reduce(factor, box)) | |
def rotate( | |
self, | |
angle, | |
resample=Resampling.NEAREST, | |
expand=0, | |
center=None, | |
translate=None, | |
fillcolor=None, | |
): | |
""" | |
Returns a rotated copy of this image. This method returns a | |
copy of this image, rotated the given number of degrees counter | |
clockwise around its centre. | |
:param angle: In degrees counter clockwise. | |
:param resample: An optional resampling filter. This can be | |
one of :py:data:`Resampling.NEAREST` (use nearest neighbour), | |
:py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 | |
environment), or :py:data:`Resampling.BICUBIC` (cubic spline | |
interpolation in a 4x4 environment). If omitted, or if the image has | |
mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. | |
See :ref:`concept-filters`. | |
:param expand: Optional expansion flag. If true, expands the output | |
image to make it large enough to hold the entire rotated image. | |
If false or omitted, make the output image the same size as the | |
input image. Note that the expand flag assumes rotation around | |
the center and no translation. | |
:param center: Optional center of rotation (a 2-tuple). Origin is | |
the upper left corner. Default is the center of the image. | |
:param translate: An optional post-rotate translation (a 2-tuple). | |
:param fillcolor: An optional color for area outside the rotated image. | |
:returns: An :py:class:`~PIL.Image.Image` object. | |
""" | |
angle = angle % 360.0 | |
# Fast paths regardless of filter, as long as we're not | |
# translating or changing the center. | |
if not (center or translate): | |
if angle == 0: | |
return self.copy() | |
if angle == 180: | |
return self.transpose(Transpose.ROTATE_180) | |
if angle in (90, 270) and (expand or self.width == self.height): | |
return self.transpose( | |
Transpose.ROTATE_90 if angle == 90 else Transpose.ROTATE_270 | |
) | |
# Calculate the affine matrix. Note that this is the reverse | |
# transformation (from destination image to source) because we | |
# want to interpolate the (discrete) destination pixel from | |
# the local area around the (floating) source pixel. | |
# The matrix we actually want (note that it operates from the right): | |
# (1, 0, tx) (1, 0, cx) ( cos a, sin a, 0) (1, 0, -cx) | |
# (0, 1, ty) * (0, 1, cy) * (-sin a, cos a, 0) * (0, 1, -cy) | |
# (0, 0, 1) (0, 0, 1) ( 0, 0, 1) (0, 0, 1) | |
# The reverse matrix is thus: | |
# (1, 0, cx) ( cos -a, sin -a, 0) (1, 0, -cx) (1, 0, -tx) | |
# (0, 1, cy) * (-sin -a, cos -a, 0) * (0, 1, -cy) * (0, 1, -ty) | |
# (0, 0, 1) ( 0, 0, 1) (0, 0, 1) (0, 0, 1) | |
# In any case, the final translation may be updated at the end to | |
# compensate for the expand flag. | |
w, h = self.size | |
if translate is None: | |
post_trans = (0, 0) | |
else: | |
post_trans = translate | |
if center is None: | |
# FIXME These should be rounded to ints? | |
rotn_center = (w / 2.0, h / 2.0) | |
else: | |
rotn_center = center | |
angle = -math.radians(angle) | |
matrix = [ | |
round(math.cos(angle), 15), | |
round(math.sin(angle), 15), | |
0.0, | |
round(-math.sin(angle), 15), | |
round(math.cos(angle), 15), | |
0.0, | |
] | |
def transform(x, y, matrix): | |
(a, b, c, d, e, f) = matrix | |
return a * x + b * y + c, d * x + e * y + f | |
matrix[2], matrix[5] = transform( | |
-rotn_center[0] - post_trans[0], -rotn_center[1] - post_trans[1], matrix | |
) | |
matrix[2] += rotn_center[0] | |
matrix[5] += rotn_center[1] | |
if expand: | |
# calculate output size | |
xx = [] | |
yy = [] | |
for x, y in ((0, 0), (w, 0), (w, h), (0, h)): | |
x, y = transform(x, y, matrix) | |
xx.append(x) | |
yy.append(y) | |
nw = math.ceil(max(xx)) - math.floor(min(xx)) | |
nh = math.ceil(max(yy)) - math.floor(min(yy)) | |
# We multiply a translation matrix from the right. Because of its | |
# special form, this is the same as taking the image of the | |
# translation vector as new translation vector. | |
matrix[2], matrix[5] = transform(-(nw - w) / 2.0, -(nh - h) / 2.0, matrix) | |
w, h = nw, nh | |
return self.transform( | |
(w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor | |
) | |
def save(self, fp, format=None, **params) -> None: | |
""" | |
Saves this image under the given filename. If no format is | |
specified, the format to use is determined from the filename | |
extension, if possible. | |
Keyword options can be used to provide additional instructions | |
to the writer. If a writer doesn't recognise an option, it is | |
silently ignored. The available options are described in the | |
:doc:`image format documentation | |
<../handbook/image-file-formats>` for each writer. | |
You can use a file object instead of a filename. In this case, | |
you must always specify the format. The file object must | |
implement the ``seek``, ``tell``, and ``write`` | |
methods, and be opened in binary mode. | |
:param fp: A filename (string), os.PathLike object or file object. | |
:param format: Optional format override. If omitted, the | |
format to use is determined from the filename extension. | |
If a file object was used instead of a filename, this | |
parameter should always be used. | |
:param params: Extra parameters to the image writer. | |
:returns: None | |
:exception ValueError: If the output format could not be determined | |
from the file name. Use the format option to solve this. | |
:exception OSError: If the file could not be written. The file | |
may have been created, and may contain partial data. | |
""" | |
filename: str | bytes = "" | |
open_fp = False | |
if is_path(fp): | |
filename = os.path.realpath(os.fspath(fp)) | |
open_fp = True | |
elif fp == sys.stdout: | |
try: | |
fp = sys.stdout.buffer | |
except AttributeError: | |
pass | |
if not filename and hasattr(fp, "name") and is_path(fp.name): | |
# only set the name for metadata purposes | |
filename = os.path.realpath(os.fspath(fp.name)) | |
# may mutate self! | |
self._ensure_mutable() | |
save_all = params.pop("save_all", False) | |
self.encoderinfo = params | |
self.encoderconfig = () | |
preinit() | |
filename_ext = os.path.splitext(filename)[1].lower() | |
ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext | |
if not format: | |
if ext not in EXTENSION: | |
init() | |
try: | |
format = EXTENSION[ext] | |
except KeyError as e: | |
msg = f"unknown file extension: {ext}" | |
raise ValueError(msg) from e | |
if format.upper() not in SAVE: | |
init() | |
if save_all: | |
save_handler = SAVE_ALL[format.upper()] | |
else: | |
save_handler = SAVE[format.upper()] | |
created = False | |
if open_fp: | |
created = not os.path.exists(filename) | |
if params.get("append", False): | |
# Open also for reading ("+"), because TIFF save_all | |
# writer needs to go back and edit the written data. | |
fp = builtins.open(filename, "r+b") | |
else: | |
fp = builtins.open(filename, "w+b") | |
try: | |
save_handler(self, fp, filename) | |
except Exception: | |
if open_fp: | |
fp.close() | |
if created: | |
try: | |
os.remove(filename) | |
except PermissionError: | |
pass | |
raise | |
if open_fp: | |
fp.close() | |
def seek(self, frame: int) -> None: | |
""" | |
Seeks to the given frame in this sequence file. If you seek | |
beyond the end of the sequence, the method raises an | |
``EOFError`` exception. When a sequence file is opened, the | |
library automatically seeks to frame 0. | |
See :py:meth:`~PIL.Image.Image.tell`. | |
If defined, :attr:`~PIL.Image.Image.n_frames` refers to the | |
number of available frames. | |
:param frame: Frame number, starting at 0. | |
:exception EOFError: If the call attempts to seek beyond the end | |
of the sequence. | |
""" | |
# overridden by file handlers | |
if frame != 0: | |
msg = "no more images in file" | |
raise EOFError(msg) | |
def show(self, title: str | None = None) -> None: | |
""" | |
Displays this image. This method is mainly intended for debugging purposes. | |
This method calls :py:func:`PIL.ImageShow.show` internally. You can use | |
:py:func:`PIL.ImageShow.register` to override its default behaviour. | |
The image is first saved to a temporary file. By default, it will be in | |
PNG format. | |
On Unix, the image is then opened using the **xdg-open**, **display**, | |
**gm**, **eog** or **xv** utility, depending on which one can be found. | |
On macOS, the image is opened with the native Preview application. | |
On Windows, the image is opened with the standard PNG display utility. | |
:param title: Optional title to use for the image window, where possible. | |
""" | |
_show(self, title=title) | |
def split(self) -> tuple[Image, ...]: | |
""" | |
Split this image into individual bands. This method returns a | |
tuple of individual image bands from an image. For example, | |
splitting an "RGB" image creates three new images each | |
containing a copy of one of the original bands (red, green, | |
blue). | |
If you need only one band, :py:meth:`~PIL.Image.Image.getchannel` | |
method can be more convenient and faster. | |
:returns: A tuple containing bands. | |
""" | |
self.load() | |
if self.im.bands == 1: | |
return (self.copy(),) | |
return tuple(map(self._new, self.im.split())) | |
def getchannel(self, channel: int | str) -> Image: | |
""" | |
Returns an image containing a single channel of the source image. | |
:param channel: What channel to return. Could be index | |
(0 for "R" channel of "RGB") or channel name | |
("A" for alpha channel of "RGBA"). | |
:returns: An image in "L" mode. | |
.. versionadded:: 4.3.0 | |
""" | |
self.load() | |
if isinstance(channel, str): | |
try: | |
channel = self.getbands().index(channel) | |
except ValueError as e: | |
msg = f'The image has no channel "{channel}"' | |
raise ValueError(msg) from e | |
return self._new(self.im.getband(channel)) | |
def tell(self) -> int: | |
""" | |
Returns the current frame number. See :py:meth:`~PIL.Image.Image.seek`. | |
If defined, :attr:`~PIL.Image.Image.n_frames` refers to the | |
number of available frames. | |
:returns: Frame number, starting with 0. | |
""" | |
return 0 | |
def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0): | |
""" | |
Make this image into a thumbnail. This method modifies the | |
image to contain a thumbnail version of itself, no larger than | |
the given size. This method calculates an appropriate thumbnail | |
size to preserve the aspect of the image, calls the | |
:py:meth:`~PIL.Image.Image.draft` method to configure the file reader | |
(where applicable), and finally resizes the image. | |
Note that this function modifies the :py:class:`~PIL.Image.Image` | |
object in place. If you need to use the full resolution image as well, | |
apply this method to a :py:meth:`~PIL.Image.Image.copy` of the original | |
image. | |
:param size: The requested size in pixels, as a 2-tuple: | |
(width, height). | |
:param resample: Optional resampling filter. This can be one | |
of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, | |
:py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, | |
:py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. | |
If omitted, it defaults to :py:data:`Resampling.BICUBIC`. | |
(was :py:data:`Resampling.NEAREST` prior to version 2.5.0). | |
See: :ref:`concept-filters`. | |
:param reducing_gap: Apply optimization by resizing the image | |
in two steps. First, reducing the image by integer times | |
using :py:meth:`~PIL.Image.Image.reduce` or | |
:py:meth:`~PIL.Image.Image.draft` for JPEG images. | |
Second, resizing using regular resampling. The last step | |
changes size no less than by ``reducing_gap`` times. | |
``reducing_gap`` may be None (no first step is performed) | |
or should be greater than 1.0. The bigger ``reducing_gap``, | |
the closer the result to the fair resampling. | |
The smaller ``reducing_gap``, the faster resizing. | |
With ``reducing_gap`` greater or equal to 3.0, the result is | |
indistinguishable from fair resampling in most cases. | |
The default value is 2.0 (very close to fair resampling | |
while still being faster in many cases). | |
:returns: None | |
""" | |
provided_size = tuple(map(math.floor, size)) | |
def preserve_aspect_ratio() -> tuple[int, int] | None: | |
def round_aspect(number, key): | |
return max(min(math.floor(number), math.ceil(number), key=key), 1) | |
x, y = provided_size | |
if x >= self.width and y >= self.height: | |
return None | |
aspect = self.width / self.height | |
if x / y >= aspect: | |
x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y)) | |
else: | |
y = round_aspect( | |
x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n) | |
) | |
return x, y | |
box = None | |
if reducing_gap is not None: | |
size = preserve_aspect_ratio() | |
if size is None: | |
return | |
res = self.draft(None, (size[0] * reducing_gap, size[1] * reducing_gap)) | |
if res is not None: | |
box = res[1] | |
if box is None: | |
self.load() | |
# load() may have changed the size of the image | |
size = preserve_aspect_ratio() | |
if size is None: | |
return | |
if self.size != size: | |
im = self.resize(size, resample, box=box, reducing_gap=reducing_gap) | |
self.im = im.im | |
self._size = size | |
self._mode = self.im.mode | |
self.readonly = 0 | |
self.pyaccess = None | |
# FIXME: the different transform methods need further explanation | |
# instead of bloating the method docs, add a separate chapter. | |
def transform( | |
self, | |
size, | |
method, | |
data=None, | |
resample=Resampling.NEAREST, | |
fill=1, | |
fillcolor=None, | |
) -> Image: | |
""" | |
Transforms this image. This method creates a new image with the | |
given size, and the same mode as the original, and copies data | |
to the new image using the given transform. | |
:param size: The output size in pixels, as a 2-tuple: | |
(width, height). | |
:param method: The transformation method. This is one of | |
:py:data:`Transform.EXTENT` (cut out a rectangular subregion), | |
:py:data:`Transform.AFFINE` (affine transform), | |
:py:data:`Transform.PERSPECTIVE` (perspective transform), | |
:py:data:`Transform.QUAD` (map a quadrilateral to a rectangle), or | |
:py:data:`Transform.MESH` (map a number of source quadrilaterals | |
in one operation). | |
It may also be an :py:class:`~PIL.Image.ImageTransformHandler` | |
object:: | |
class Example(Image.ImageTransformHandler): | |
def transform(self, size, data, resample, fill=1): | |
# Return result | |
Implementations of :py:class:`~PIL.Image.ImageTransformHandler` | |
for some of the :py:class:`Transform` methods are provided | |
in :py:mod:`~PIL.ImageTransform`. | |
It may also be an object with a ``method.getdata`` method | |
that returns a tuple supplying new ``method`` and ``data`` values:: | |
class Example: | |
def getdata(self): | |
method = Image.Transform.EXTENT | |
data = (0, 0, 100, 100) | |
return method, data | |
:param data: Extra data to the transformation method. | |
:param resample: Optional resampling filter. It can be one of | |
:py:data:`Resampling.NEAREST` (use nearest neighbour), | |
:py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 | |
environment), or :py:data:`Resampling.BICUBIC` (cubic spline | |
interpolation in a 4x4 environment). If omitted, or if the image | |
has mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. | |
See: :ref:`concept-filters`. | |
:param fill: If ``method`` is an | |
:py:class:`~PIL.Image.ImageTransformHandler` object, this is one of | |
the arguments passed to it. Otherwise, it is unused. | |
:param fillcolor: Optional fill color for the area outside the | |
transform in the output image. | |
:returns: An :py:class:`~PIL.Image.Image` object. | |
""" | |
if self.mode in ("LA", "RGBA") and resample != Resampling.NEAREST: | |
return ( | |
self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) | |
.transform(size, method, data, resample, fill, fillcolor) | |
.convert(self.mode) | |
) | |
if isinstance(method, ImageTransformHandler): | |
return method.transform(size, self, resample=resample, fill=fill) | |
if hasattr(method, "getdata"): | |
# compatibility w. old-style transform objects | |
method, data = method.getdata() | |
if data is None: | |
msg = "missing method data" | |
raise ValueError(msg) | |
im = new(self.mode, size, fillcolor) | |
if self.mode == "P" and self.palette: | |
im.palette = self.palette.copy() | |
im.info = self.info.copy() | |
if method == Transform.MESH: | |
# list of quads | |
for box, quad in data: | |
im.__transformer( | |
box, self, Transform.QUAD, quad, resample, fillcolor is None | |
) | |
else: | |
im.__transformer( | |
(0, 0) + size, self, method, data, resample, fillcolor is None | |
) | |
return im | |
def __transformer( | |
self, box, image, method, data, resample=Resampling.NEAREST, fill=1 | |
): | |
w = box[2] - box[0] | |
h = box[3] - box[1] | |
if method == Transform.AFFINE: | |
data = data[:6] | |
elif method == Transform.EXTENT: | |
# convert extent to an affine transform | |
x0, y0, x1, y1 = data | |
xs = (x1 - x0) / w | |
ys = (y1 - y0) / h | |
method = Transform.AFFINE | |
data = (xs, 0, x0, 0, ys, y0) | |
elif method == Transform.PERSPECTIVE: | |
data = data[:8] | |
elif method == Transform.QUAD: | |
# quadrilateral warp. data specifies the four corners | |
# given as NW, SW, SE, and NE. | |
nw = data[:2] | |
sw = data[2:4] | |
se = data[4:6] | |
ne = data[6:8] | |
x0, y0 = nw | |
As = 1.0 / w | |
At = 1.0 / h | |
data = ( | |
x0, | |
(ne[0] - x0) * As, | |
(sw[0] - x0) * At, | |
(se[0] - sw[0] - ne[0] + x0) * As * At, | |
y0, | |
(ne[1] - y0) * As, | |
(sw[1] - y0) * At, | |
(se[1] - sw[1] - ne[1] + y0) * As * At, | |
) | |
else: | |
msg = "unknown transformation method" | |
raise ValueError(msg) | |
if resample not in ( | |
Resampling.NEAREST, | |
Resampling.BILINEAR, | |
Resampling.BICUBIC, | |
): | |
if resample in (Resampling.BOX, Resampling.HAMMING, Resampling.LANCZOS): | |
msg = { | |
Resampling.BOX: "Image.Resampling.BOX", | |
Resampling.HAMMING: "Image.Resampling.HAMMING", | |
Resampling.LANCZOS: "Image.Resampling.LANCZOS", | |
}[resample] + f" ({resample}) cannot be used." | |
else: | |
msg = f"Unknown resampling filter ({resample})." | |
filters = [ | |
f"{filter[1]} ({filter[0]})" | |
for filter in ( | |
(Resampling.NEAREST, "Image.Resampling.NEAREST"), | |
(Resampling.BILINEAR, "Image.Resampling.BILINEAR"), | |
(Resampling.BICUBIC, "Image.Resampling.BICUBIC"), | |
) | |
] | |
msg += " Use " + ", ".join(filters[:-1]) + " or " + filters[-1] | |
raise ValueError(msg) | |
image.load() | |
self.load() | |
if image.mode in ("1", "P"): | |
resample = Resampling.NEAREST | |
self.im.transform2(box, image.im, method, data, resample, fill) | |
def transpose(self, method: Transpose) -> Image: | |
""" | |
Transpose image (flip or rotate in 90 degree steps) | |
:param method: One of :py:data:`Transpose.FLIP_LEFT_RIGHT`, | |
:py:data:`Transpose.FLIP_TOP_BOTTOM`, :py:data:`Transpose.ROTATE_90`, | |
:py:data:`Transpose.ROTATE_180`, :py:data:`Transpose.ROTATE_270`, | |
:py:data:`Transpose.TRANSPOSE` or :py:data:`Transpose.TRANSVERSE`. | |
:returns: Returns a flipped or rotated copy of this image. | |
""" | |
self.load() | |
return self._new(self.im.transpose(method)) | |
def effect_spread(self, distance): | |
""" | |
Randomly spread pixels in an image. | |
:param distance: Distance to spread pixels. | |
""" | |
self.load() | |
return self._new(self.im.effect_spread(distance)) | |
def toqimage(self): | |
"""Returns a QImage copy of this image""" | |
from . import ImageQt | |
if not ImageQt.qt_is_installed: | |
msg = "Qt bindings are not installed" | |
raise ImportError(msg) | |
return ImageQt.toqimage(self) | |
def toqpixmap(self): | |
"""Returns a QPixmap copy of this image""" | |
from . import ImageQt | |
if not ImageQt.qt_is_installed: | |
msg = "Qt bindings are not installed" | |
raise ImportError(msg) | |
return ImageQt.toqpixmap(self) | |
# -------------------------------------------------------------------- | |
# Abstract handlers. | |
class ImagePointHandler: | |
""" | |
Used as a mixin by point transforms | |
(for use with :py:meth:`~PIL.Image.Image.point`) | |
""" | |
def point(self, im: Image) -> Image: | |
pass | |
class ImageTransformHandler: | |
""" | |
Used as a mixin by geometry transforms | |
(for use with :py:meth:`~PIL.Image.Image.transform`) | |
""" | |
def transform( | |
self, | |
size: tuple[int, int], | |
image: Image, | |
**options: dict[str, str | int | tuple[int, ...] | list[int]], | |
) -> Image: | |
pass | |
# -------------------------------------------------------------------- | |
# Factories | |
# | |
# Debugging | |
def _wedge(): | |
"""Create grayscale wedge (for debugging only)""" | |
return Image()._new(core.wedge("L")) | |
def _check_size(size): | |
""" | |
Common check to enforce type and sanity check on size tuples | |
:param size: Should be a 2 tuple of (width, height) | |
:returns: True, or raises a ValueError | |
""" | |
if not isinstance(size, (list, tuple)): | |
msg = "Size must be a tuple" | |
raise ValueError(msg) | |
if len(size) != 2: | |
msg = "Size must be a tuple of length 2" | |
raise ValueError(msg) | |
if size[0] < 0 or size[1] < 0: | |
msg = "Width and height must be >= 0" | |
raise ValueError(msg) | |
return True | |
def new( | |
mode: str, size: tuple[int, int], color: float | tuple[float, ...] | str | None = 0 | |
) -> Image: | |
""" | |
Creates a new image with the given mode and size. | |
:param mode: The mode to use for the new image. See: | |
:ref:`concept-modes`. | |
:param size: A 2-tuple, containing (width, height) in pixels. | |
:param color: What color to use for the image. Default is black. | |
If given, this should be a single integer or floating point value | |
for single-band modes, and a tuple for multi-band modes (one value | |
per band). When creating RGB or HSV images, you can also use color | |
strings as supported by the ImageColor module. If the color is | |
None, the image is not initialised. | |
:returns: An :py:class:`~PIL.Image.Image` object. | |
""" | |
_check_size(size) | |
if color is None: | |
# don't initialize | |
return Image()._new(core.new(mode, size)) | |
if isinstance(color, str): | |
# css3-style specifier | |
from . import ImageColor | |
color = ImageColor.getcolor(color, mode) | |
im = Image() | |
if mode == "P" and isinstance(color, (list, tuple)) and len(color) in [3, 4]: | |
# RGB or RGBA value for a P image | |
from . import ImagePalette | |
im.palette = ImagePalette.ImagePalette() | |
color = im.palette.getcolor(color) | |
return im._new(core.fill(mode, size, color)) | |
def frombytes(mode, size, data, decoder_name="raw", *args) -> Image: | |
""" | |
Creates a copy of an image memory from pixel data in a buffer. | |
In its simplest form, this function takes three arguments | |
(mode, size, and unpacked pixel data). | |
You can also use any pixel decoder supported by PIL. For more | |
information on available decoders, see the section | |
:ref:`Writing Your Own File Codec <file-codecs>`. | |
Note that this function decodes pixel data only, not entire images. | |
If you have an entire image in a string, wrap it in a | |
:py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load | |
it. | |
:param mode: The image mode. See: :ref:`concept-modes`. | |
:param size: The image size. | |
:param data: A byte buffer containing raw data for the given mode. | |
:param decoder_name: What decoder to use. | |
:param args: Additional parameters for the given decoder. | |
:returns: An :py:class:`~PIL.Image.Image` object. | |
""" | |
_check_size(size) | |
im = new(mode, size) | |
if im.width != 0 and im.height != 0: | |
# may pass tuple instead of argument list | |
if len(args) == 1 and isinstance(args[0], tuple): | |
args = args[0] | |
if decoder_name == "raw" and args == (): | |
args = mode | |
im.frombytes(data, decoder_name, args) | |
return im | |
def frombuffer(mode, size, data, decoder_name="raw", *args): | |
""" | |
Creates an image memory referencing pixel data in a byte buffer. | |
This function is similar to :py:func:`~PIL.Image.frombytes`, but uses data | |
in the byte buffer, where possible. This means that changes to the | |
original buffer object are reflected in this image). Not all modes can | |
share memory; supported modes include "L", "RGBX", "RGBA", and "CMYK". | |
Note that this function decodes pixel data only, not entire images. | |
If you have an entire image file in a string, wrap it in a | |
:py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load it. | |
The default parameters used for the "raw" decoder differs from that used for | |
:py:func:`~PIL.Image.frombytes`. This is a bug, and will probably be fixed in a | |
future release. The current release issues a warning if you do this; to disable | |
the warning, you should provide the full set of parameters. See below for details. | |
:param mode: The image mode. See: :ref:`concept-modes`. | |
:param size: The image size. | |
:param data: A bytes or other buffer object containing raw | |
data for the given mode. | |
:param decoder_name: What decoder to use. | |
:param args: Additional parameters for the given decoder. For the | |
default encoder ("raw"), it's recommended that you provide the | |
full set of parameters:: | |
frombuffer(mode, size, data, "raw", mode, 0, 1) | |
:returns: An :py:class:`~PIL.Image.Image` object. | |
.. versionadded:: 1.1.4 | |
""" | |
_check_size(size) | |
# may pass tuple instead of argument list | |
if len(args) == 1 and isinstance(args[0], tuple): | |
args = args[0] | |
if decoder_name == "raw": | |
if args == (): | |
args = mode, 0, 1 | |
if args[0] in _MAPMODES: | |
im = new(mode, (0, 0)) | |
im = im._new(core.map_buffer(data, size, decoder_name, 0, args)) | |
if mode == "P": | |
from . import ImagePalette | |
im.palette = ImagePalette.ImagePalette("RGB", im.im.getpalette("RGB")) | |
im.readonly = 1 | |
return im | |
return frombytes(mode, size, data, decoder_name, args) | |
def fromarray(obj, mode=None): | |
""" | |
Creates an image memory from an object exporting the array interface | |
(using the buffer protocol):: | |
from PIL import Image | |
import numpy as np | |
a = np.zeros((5, 5)) | |
im = Image.fromarray(a) | |
If ``obj`` is not contiguous, then the ``tobytes`` method is called | |
and :py:func:`~PIL.Image.frombuffer` is used. | |
In the case of NumPy, be aware that Pillow modes do not always correspond | |
to NumPy dtypes. Pillow modes only offer 1-bit pixels, 8-bit pixels, | |
32-bit signed integer pixels, and 32-bit floating point pixels. | |
Pillow images can also be converted to arrays:: | |
from PIL import Image | |
import numpy as np | |
im = Image.open("hopper.jpg") | |
a = np.asarray(im) | |
When converting Pillow images to arrays however, only pixel values are | |
transferred. This means that P and PA mode images will lose their palette. | |
:param obj: Object with array interface | |
:param mode: Optional mode to use when reading ``obj``. Will be determined from | |
type if ``None``. | |
This will not be used to convert the data after reading, but will be used to | |
change how the data is read:: | |
from PIL import Image | |
import numpy as np | |
a = np.full((1, 1), 300) | |
im = Image.fromarray(a, mode="L") | |
im.getpixel((0, 0)) # 44 | |
im = Image.fromarray(a, mode="RGB") | |
im.getpixel((0, 0)) # (44, 1, 0) | |
See: :ref:`concept-modes` for general information about modes. | |
:returns: An image object. | |
.. versionadded:: 1.1.6 | |
""" | |
arr = obj.__array_interface__ | |
shape = arr["shape"] | |
ndim = len(shape) | |
strides = arr.get("strides", None) | |
if mode is None: | |
try: | |
typekey = (1, 1) + shape[2:], arr["typestr"] | |
except KeyError as e: | |
msg = "Cannot handle this data type" | |
raise TypeError(msg) from e | |
try: | |
mode, rawmode = _fromarray_typemap[typekey] | |
except KeyError as e: | |
typekey_shape, typestr = typekey | |
msg = f"Cannot handle this data type: {typekey_shape}, {typestr}" | |
raise TypeError(msg) from e | |
else: | |
rawmode = mode | |
if mode in ["1", "L", "I", "P", "F"]: | |
ndmax = 2 | |
elif mode == "RGB": | |
ndmax = 3 | |
else: | |
ndmax = 4 | |
if ndim > ndmax: | |
msg = f"Too many dimensions: {ndim} > {ndmax}." | |
raise ValueError(msg) | |
size = 1 if ndim == 1 else shape[1], shape[0] | |
if strides is not None: | |
if hasattr(obj, "tobytes"): | |
obj = obj.tobytes() | |
else: | |
obj = obj.tostring() | |
return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) | |
def fromqimage(im): | |
"""Creates an image instance from a QImage image""" | |
from . import ImageQt | |
if not ImageQt.qt_is_installed: | |
msg = "Qt bindings are not installed" | |
raise ImportError(msg) | |
return ImageQt.fromqimage(im) | |
def fromqpixmap(im): | |
"""Creates an image instance from a QPixmap image""" | |
from . import ImageQt | |
if not ImageQt.qt_is_installed: | |
msg = "Qt bindings are not installed" | |
raise ImportError(msg) | |
return ImageQt.fromqpixmap(im) | |
_fromarray_typemap = { | |
# (shape, typestr) => mode, rawmode | |
# first two members of shape are set to one | |
((1, 1), "|b1"): ("1", "1;8"), | |
((1, 1), "|u1"): ("L", "L"), | |
((1, 1), "|i1"): ("I", "I;8"), | |
((1, 1), "<u2"): ("I", "I;16"), | |
((1, 1), ">u2"): ("I", "I;16B"), | |
((1, 1), "<i2"): ("I", "I;16S"), | |
((1, 1), ">i2"): ("I", "I;16BS"), | |
((1, 1), "<u4"): ("I", "I;32"), | |
((1, 1), ">u4"): ("I", "I;32B"), | |
((1, 1), "<i4"): ("I", "I;32S"), | |
((1, 1), ">i4"): ("I", "I;32BS"), | |
((1, 1), "<f4"): ("F", "F;32F"), | |
((1, 1), ">f4"): ("F", "F;32BF"), | |
((1, 1), "<f8"): ("F", "F;64F"), | |
((1, 1), ">f8"): ("F", "F;64BF"), | |
((1, 1, 2), "|u1"): ("LA", "LA"), | |
((1, 1, 3), "|u1"): ("RGB", "RGB"), | |
((1, 1, 4), "|u1"): ("RGBA", "RGBA"), | |
# shortcuts: | |
((1, 1), _ENDIAN + "i4"): ("I", "I"), | |
((1, 1), _ENDIAN + "f4"): ("F", "F"), | |
} | |
def _decompression_bomb_check(size: tuple[int, int]) -> None: | |
if MAX_IMAGE_PIXELS is None: | |
return | |
pixels = max(1, size[0]) * max(1, size[1]) | |
if pixels > 2 * MAX_IMAGE_PIXELS: | |
msg = ( | |
f"Image size ({pixels} pixels) exceeds limit of {2 * MAX_IMAGE_PIXELS} " | |
"pixels, could be decompression bomb DOS attack." | |
) | |
raise DecompressionBombError(msg) | |
if pixels > MAX_IMAGE_PIXELS: | |
warnings.warn( | |
f"Image size ({pixels} pixels) exceeds limit of {MAX_IMAGE_PIXELS} pixels, " | |
"could be decompression bomb DOS attack.", | |
DecompressionBombWarning, | |
) | |
def open(fp, mode="r", formats=None) -> Image: | |
""" | |
Opens and identifies the given image file. | |
This is a lazy operation; this function identifies the file, but | |
the file remains open and the actual image data is not read from | |
the file until you try to process the data (or call the | |
:py:meth:`~PIL.Image.Image.load` method). See | |
:py:func:`~PIL.Image.new`. See :ref:`file-handling`. | |
:param fp: A filename (string), os.PathLike object or a file object. | |
The file object must implement ``file.read``, | |
``file.seek``, and ``file.tell`` methods, | |
and be opened in binary mode. The file object will also seek to zero | |
before reading. | |
:param mode: The mode. If given, this argument must be "r". | |
:param formats: A list or tuple of formats to attempt to load the file in. | |
This can be used to restrict the set of formats checked. | |
Pass ``None`` to try all supported formats. You can print the set of | |
available formats by running ``python3 -m PIL`` or using | |
the :py:func:`PIL.features.pilinfo` function. | |
:returns: An :py:class:`~PIL.Image.Image` object. | |
:exception FileNotFoundError: If the file cannot be found. | |
:exception PIL.UnidentifiedImageError: If the image cannot be opened and | |
identified. | |
:exception ValueError: If the ``mode`` is not "r", or if a ``StringIO`` | |
instance is used for ``fp``. | |
:exception TypeError: If ``formats`` is not ``None``, a list or a tuple. | |
""" | |
if mode != "r": | |
msg = f"bad mode {repr(mode)}" | |
raise ValueError(msg) | |
elif isinstance(fp, io.StringIO): | |
msg = ( | |
"StringIO cannot be used to open an image. " | |
"Binary data must be used instead." | |
) | |
raise ValueError(msg) | |
if formats is None: | |
formats = ID | |
elif not isinstance(formats, (list, tuple)): | |
msg = "formats must be a list or tuple" | |
raise TypeError(msg) | |
exclusive_fp = False | |
filename: str | bytes = "" | |
if is_path(fp): | |
filename = os.path.realpath(os.fspath(fp)) | |
if filename: | |
fp = builtins.open(filename, "rb") | |
exclusive_fp = True | |
try: | |
fp.seek(0) | |
except (AttributeError, io.UnsupportedOperation): | |
fp = io.BytesIO(fp.read()) | |
exclusive_fp = True | |
prefix = fp.read(16) | |
preinit() | |
accept_warnings = [] | |
def _open_core(fp, filename, prefix, formats): | |
for i in formats: | |
i = i.upper() | |
if i not in OPEN: | |
init() | |
try: | |
factory, accept = OPEN[i] | |
result = not accept or accept(prefix) | |
if type(result) in [str, bytes]: | |
accept_warnings.append(result) | |
elif result: | |
fp.seek(0) | |
im = factory(fp, filename) | |
_decompression_bomb_check(im.size) | |
return im | |
except (SyntaxError, IndexError, TypeError, struct.error): | |
# Leave disabled by default, spams the logs with image | |
# opening failures that are entirely expected. | |
# logger.debug("", exc_info=True) | |
continue | |
except BaseException: | |
if exclusive_fp: | |
fp.close() | |
raise | |
return None | |
im = _open_core(fp, filename, prefix, formats) | |
if im is None and formats is ID: | |
checked_formats = formats.copy() | |
if init(): | |
im = _open_core( | |
fp, | |
filename, | |
prefix, | |
tuple(format for format in formats if format not in checked_formats), | |
) | |
if im: | |
im._exclusive_fp = exclusive_fp | |
return im | |
if exclusive_fp: | |
fp.close() | |
for message in accept_warnings: | |
warnings.warn(message) | |
msg = "cannot identify image file %r" % (filename if filename else fp) | |
raise UnidentifiedImageError(msg) | |
# | |
# Image processing. | |
def alpha_composite(im1: Image, im2: Image) -> Image: | |
""" | |
Alpha composite im2 over im1. | |
:param im1: The first image. Must have mode RGBA. | |
:param im2: The second image. Must have mode RGBA, and the same size as | |
the first image. | |
:returns: An :py:class:`~PIL.Image.Image` object. | |
""" | |
im1.load() | |
im2.load() | |
return im1._new(core.alpha_composite(im1.im, im2.im)) | |
def blend(im1: Image, im2: Image, alpha: float) -> Image: | |
""" | |
Creates a new image by interpolating between two input images, using | |
a constant alpha:: | |
out = image1 * (1.0 - alpha) + image2 * alpha | |
:param im1: The first image. | |
:param im2: The second image. Must have the same mode and size as | |
the first image. | |
:param alpha: The interpolation alpha factor. If alpha is 0.0, a | |
copy of the first image is returned. If alpha is 1.0, a copy of | |
the second image is returned. There are no restrictions on the | |
alpha value. If necessary, the result is clipped to fit into | |
the allowed output range. | |
:returns: An :py:class:`~PIL.Image.Image` object. | |
""" | |
im1.load() | |
im2.load() | |
return im1._new(core.blend(im1.im, im2.im, alpha)) | |
def composite(image1: Image, image2: Image, mask: Image) -> Image: | |
""" | |
Create composite image by blending images using a transparency mask. | |
:param image1: The first image. | |
:param image2: The second image. Must have the same mode and | |
size as the first image. | |
:param mask: A mask image. This image can have mode | |
"1", "L", or "RGBA", and must have the same size as the | |
other two images. | |
""" | |
image = image2.copy() | |
image.paste(image1, None, mask) | |
return image | |
def eval(image, *args): | |
""" | |
Applies the function (which should take one argument) to each pixel | |
in the given image. If the image has more than one band, the same | |
function is applied to each band. Note that the function is | |
evaluated once for each possible pixel value, so you cannot use | |
random components or other generators. | |
:param image: The input image. | |
:param function: A function object, taking one integer argument. | |
:returns: An :py:class:`~PIL.Image.Image` object. | |
""" | |
return image.point(args[0]) | |
def merge(mode, bands): | |
""" | |
Merge a set of single band images into a new multiband image. | |
:param mode: The mode to use for the output image. See: | |
:ref:`concept-modes`. | |
:param bands: A sequence containing one single-band image for | |
each band in the output image. All bands must have the | |
same size. | |
:returns: An :py:class:`~PIL.Image.Image` object. | |
""" | |
if getmodebands(mode) != len(bands) or "*" in mode: | |
msg = "wrong number of bands" | |
raise ValueError(msg) | |
for band in bands[1:]: | |
if band.mode != getmodetype(mode): | |
msg = "mode mismatch" | |
raise ValueError(msg) | |
if band.size != bands[0].size: | |
msg = "size mismatch" | |
raise ValueError(msg) | |
for band in bands: | |
band.load() | |
return bands[0]._new(core.merge(mode, *[b.im for b in bands])) | |
# -------------------------------------------------------------------- | |
# Plugin registry | |
def register_open( | |
id, | |
factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], | |
accept: Callable[[bytes], bool] | None = None, | |
) -> None: | |
""" | |
Register an image file plugin. This function should not be used | |
in application code. | |
:param id: An image format identifier. | |
:param factory: An image file factory method. | |
:param accept: An optional function that can be used to quickly | |
reject images having another format. | |
""" | |
id = id.upper() | |
if id not in ID: | |
ID.append(id) | |
OPEN[id] = factory, accept | |
def register_mime(id: str, mimetype: str) -> None: | |
""" | |
Registers an image MIME type by populating ``Image.MIME``. This function | |
should not be used in application code. | |
``Image.MIME`` provides a mapping from image format identifiers to mime | |
formats, but :py:meth:`~PIL.ImageFile.ImageFile.get_format_mimetype` can | |
provide a different result for specific images. | |
:param id: An image format identifier. | |
:param mimetype: The image MIME type for this format. | |
""" | |
MIME[id.upper()] = mimetype | |
def register_save(id: str, driver) -> None: | |
""" | |
Registers an image save function. This function should not be | |
used in application code. | |
:param id: An image format identifier. | |
:param driver: A function to save images in this format. | |
""" | |
SAVE[id.upper()] = driver | |
def register_save_all(id, driver) -> None: | |
""" | |
Registers an image function to save all the frames | |
of a multiframe format. This function should not be | |
used in application code. | |
:param id: An image format identifier. | |
:param driver: A function to save images in this format. | |
""" | |
SAVE_ALL[id.upper()] = driver | |
def register_extension(id, extension) -> None: | |
""" | |
Registers an image extension. This function should not be | |
used in application code. | |
:param id: An image format identifier. | |
:param extension: An extension used for this format. | |
""" | |
EXTENSION[extension.lower()] = id.upper() | |
def register_extensions(id, extensions) -> None: | |
""" | |
Registers image extensions. This function should not be | |
used in application code. | |
:param id: An image format identifier. | |
:param extensions: A list of extensions used for this format. | |
""" | |
for extension in extensions: | |
register_extension(id, extension) | |
def registered_extensions(): | |
""" | |
Returns a dictionary containing all file extensions belonging | |
to registered plugins | |
""" | |
init() | |
return EXTENSION | |
def register_decoder(name: str, decoder: type[ImageFile.PyDecoder]) -> None: | |
""" | |
Registers an image decoder. This function should not be | |
used in application code. | |
:param name: The name of the decoder | |
:param decoder: An ImageFile.PyDecoder object | |
.. versionadded:: 4.1.0 | |
""" | |
DECODERS[name] = decoder | |
def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None: | |
""" | |
Registers an image encoder. This function should not be | |
used in application code. | |
:param name: The name of the encoder | |
:param encoder: An ImageFile.PyEncoder object | |
.. versionadded:: 4.1.0 | |
""" | |
ENCODERS[name] = encoder | |
# -------------------------------------------------------------------- | |
# Simple display support. | |
def _show(image, **options) -> None: | |
from . import ImageShow | |
ImageShow.show(image, **options) | |
# -------------------------------------------------------------------- | |
# Effects | |
def effect_mandelbrot(size, extent, quality): | |
""" | |
Generate a Mandelbrot set covering the given extent. | |
:param size: The requested size in pixels, as a 2-tuple: | |
(width, height). | |
:param extent: The extent to cover, as a 4-tuple: | |
(x0, y0, x1, y1). | |
:param quality: Quality. | |
""" | |
return Image()._new(core.effect_mandelbrot(size, extent, quality)) | |
def effect_noise(size, sigma): | |
""" | |
Generate Gaussian noise centered around 128. | |
:param size: The requested size in pixels, as a 2-tuple: | |
(width, height). | |
:param sigma: Standard deviation of noise. | |
""" | |
return Image()._new(core.effect_noise(size, sigma)) | |
def linear_gradient(mode): | |
""" | |
Generate 256x256 linear gradient from black to white, top to bottom. | |
:param mode: Input mode. | |
""" | |
return Image()._new(core.linear_gradient(mode)) | |
def radial_gradient(mode): | |
""" | |
Generate 256x256 radial gradient from black to white, centre to edge. | |
:param mode: Input mode. | |
""" | |
return Image()._new(core.radial_gradient(mode)) | |
# -------------------------------------------------------------------- | |
# Resources | |
def _apply_env_variables(env=None) -> None: | |
if env is None: | |
env = os.environ | |
for var_name, setter in [ | |
("PILLOW_ALIGNMENT", core.set_alignment), | |
("PILLOW_BLOCK_SIZE", core.set_block_size), | |
("PILLOW_BLOCKS_MAX", core.set_blocks_max), | |
]: | |
if var_name not in env: | |
continue | |
var = env[var_name].lower() | |
units = 1 | |
for postfix, mul in [("k", 1024), ("m", 1024 * 1024)]: | |
if var.endswith(postfix): | |
units = mul | |
var = var[: -len(postfix)] | |
try: | |
var = int(var) * units | |
except ValueError: | |
warnings.warn(f"{var_name} is not int") | |
continue | |
try: | |
setter(var) | |
except ValueError as e: | |
warnings.warn(f"{var_name}: {e}") | |
_apply_env_variables() | |
atexit.register(core.clear_cache) | |
if TYPE_CHECKING: | |
_ExifBase = MutableMapping[int, Any] | |
else: | |
_ExifBase = MutableMapping | |
class Exif(_ExifBase): | |
""" | |
This class provides read and write access to EXIF image data:: | |
from PIL import Image | |
im = Image.open("exif.png") | |
exif = im.getexif() # Returns an instance of this class | |
Information can be read and written, iterated over or deleted:: | |
print(exif[274]) # 1 | |
exif[274] = 2 | |
for k, v in exif.items(): | |
print("Tag", k, "Value", v) # Tag 274 Value 2 | |
del exif[274] | |
To access information beyond IFD0, :py:meth:`~PIL.Image.Exif.get_ifd` | |
returns a dictionary:: | |
from PIL import ExifTags | |
im = Image.open("exif_gps.jpg") | |
exif = im.getexif() | |
gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) | |
print(gps_ifd) | |
Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.Makernote``, | |
``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. | |
:py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: | |
print(exif[ExifTags.Base.Software]) # PIL | |
print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 | |
""" | |
endian = None | |
bigtiff = False | |
_loaded = False | |
def __init__(self): | |
self._data = {} | |
self._hidden_data = {} | |
self._ifds = {} | |
self._info = None | |
self._loaded_exif = None | |
def _fixup(self, value): | |
try: | |
if len(value) == 1 and isinstance(value, tuple): | |
return value[0] | |
except Exception: | |
pass | |
return value | |
def _fixup_dict(self, src_dict): | |
# Helper function | |
# returns a dict with any single item tuples/lists as individual values | |
return {k: self._fixup(v) for k, v in src_dict.items()} | |
def _get_ifd_dict(self, offset, group=None): | |
try: | |
# an offset pointer to the location of the nested embedded IFD. | |
# It should be a long, but may be corrupted. | |
self.fp.seek(offset) | |
except (KeyError, TypeError): | |
pass | |
else: | |
from . import TiffImagePlugin | |
info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group) | |
info.load(self.fp) | |
return self._fixup_dict(info) | |
def _get_head(self): | |
version = b"\x2B" if self.bigtiff else b"\x2A" | |
if self.endian == "<": | |
head = b"II" + version + b"\x00" + o32le(8) | |
else: | |
head = b"MM\x00" + version + o32be(8) | |
if self.bigtiff: | |
head += o32le(8) if self.endian == "<" else o32be(8) | |
head += b"\x00\x00\x00\x00" | |
return head | |
def load(self, data): | |
# Extract EXIF information. This is highly experimental, | |
# and is likely to be replaced with something better in a future | |
# version. | |
# The EXIF record consists of a TIFF file embedded in a JPEG | |
# application marker (!). | |
if data == self._loaded_exif: | |
return | |
self._loaded_exif = data | |
self._data.clear() | |
self._hidden_data.clear() | |
self._ifds.clear() | |
if data and data.startswith(b"Exif\x00\x00"): | |
data = data[6:] | |
if not data: | |
self._info = None | |
return | |
self.fp = io.BytesIO(data) | |
self.head = self.fp.read(8) | |
# process dictionary | |
from . import TiffImagePlugin | |
self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) | |
self.endian = self._info._endian | |
self.fp.seek(self._info.next) | |
self._info.load(self.fp) | |
def load_from_fp(self, fp, offset=None): | |
self._loaded_exif = None | |
self._data.clear() | |
self._hidden_data.clear() | |
self._ifds.clear() | |
# process dictionary | |
from . import TiffImagePlugin | |
self.fp = fp | |
if offset is not None: | |
self.head = self._get_head() | |
else: | |
self.head = self.fp.read(8) | |
self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) | |
if self.endian is None: | |
self.endian = self._info._endian | |
if offset is None: | |
offset = self._info.next | |
self.fp.tell() | |
self.fp.seek(offset) | |
self._info.load(self.fp) | |
def _get_merged_dict(self): | |
merged_dict = dict(self) | |
# get EXIF extension | |
if ExifTags.IFD.Exif in self: | |
ifd = self._get_ifd_dict(self[ExifTags.IFD.Exif], ExifTags.IFD.Exif) | |
if ifd: | |
merged_dict.update(ifd) | |
# GPS | |
if ExifTags.IFD.GPSInfo in self: | |
merged_dict[ExifTags.IFD.GPSInfo] = self._get_ifd_dict( | |
self[ExifTags.IFD.GPSInfo], ExifTags.IFD.GPSInfo | |
) | |
return merged_dict | |
def tobytes(self, offset: int = 8) -> bytes: | |
from . import TiffImagePlugin | |
head = self._get_head() | |
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) | |
for tag, value in self.items(): | |
if tag in [ | |
ExifTags.IFD.Exif, | |
ExifTags.IFD.GPSInfo, | |
] and not isinstance(value, dict): | |
value = self.get_ifd(tag) | |
if ( | |
tag == ExifTags.IFD.Exif | |
and ExifTags.IFD.Interop in value | |
and not isinstance(value[ExifTags.IFD.Interop], dict) | |
): | |
value = value.copy() | |
value[ExifTags.IFD.Interop] = self.get_ifd(ExifTags.IFD.Interop) | |
ifd[tag] = value | |
return b"Exif\x00\x00" + head + ifd.tobytes(offset) | |
def get_ifd(self, tag): | |
if tag not in self._ifds: | |
if tag == ExifTags.IFD.IFD1: | |
if self._info is not None and self._info.next != 0: | |
self._ifds[tag] = self._get_ifd_dict(self._info.next) | |
elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: | |
offset = self._hidden_data.get(tag, self.get(tag)) | |
if offset is not None: | |
self._ifds[tag] = self._get_ifd_dict(offset, tag) | |
elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: | |
if ExifTags.IFD.Exif not in self._ifds: | |
self.get_ifd(ExifTags.IFD.Exif) | |
tag_data = self._ifds[ExifTags.IFD.Exif][tag] | |
if tag == ExifTags.IFD.Makernote: | |
from .TiffImagePlugin import ImageFileDirectory_v2 | |
if tag_data[:8] == b"FUJIFILM": | |
ifd_offset = i32le(tag_data, 8) | |
ifd_data = tag_data[ifd_offset:] | |
makernote = {} | |
for i in range(0, struct.unpack("<H", ifd_data[:2])[0]): | |
ifd_tag, typ, count, data = struct.unpack( | |
"<HHL4s", ifd_data[i * 12 + 2 : (i + 1) * 12 + 2] | |
) | |
try: | |
( | |
unit_size, | |
handler, | |
) = ImageFileDirectory_v2._load_dispatch[typ] | |
except KeyError: | |
continue | |
size = count * unit_size | |
if size > 4: | |
(offset,) = struct.unpack("<L", data) | |
data = ifd_data[offset - 12 : offset + size - 12] | |
else: | |
data = data[:size] | |
if len(data) != size: | |
warnings.warn( | |
"Possibly corrupt EXIF MakerNote data. " | |
f"Expecting to read {size} bytes but only got " | |
f"{len(data)}. Skipping tag {ifd_tag}" | |
) | |
continue | |
if not data: | |
continue | |
makernote[ifd_tag] = handler( | |
ImageFileDirectory_v2(), data, False | |
) | |
self._ifds[tag] = dict(self._fixup_dict(makernote)) | |
elif self.get(0x010F) == "Nintendo": | |
makernote = {} | |
for i in range(0, struct.unpack(">H", tag_data[:2])[0]): | |
ifd_tag, typ, count, data = struct.unpack( | |
">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] | |
) | |
if ifd_tag == 0x1101: | |
# CameraInfo | |
(offset,) = struct.unpack(">L", data) | |
self.fp.seek(offset) | |
camerainfo = {"ModelID": self.fp.read(4)} | |
self.fp.read(4) | |
# Seconds since 2000 | |
camerainfo["TimeStamp"] = i32le(self.fp.read(12)) | |
self.fp.read(4) | |
camerainfo["InternalSerialNumber"] = self.fp.read(4) | |
self.fp.read(12) | |
parallax = self.fp.read(4) | |
handler = ImageFileDirectory_v2._load_dispatch[ | |
TiffTags.FLOAT | |
][1] | |
camerainfo["Parallax"] = handler( | |
ImageFileDirectory_v2(), parallax, False | |
) | |
self.fp.read(4) | |
camerainfo["Category"] = self.fp.read(2) | |
makernote = {0x1101: dict(self._fixup_dict(camerainfo))} | |
self._ifds[tag] = makernote | |
else: | |
# Interop | |
self._ifds[tag] = self._get_ifd_dict(tag_data, tag) | |
ifd = self._ifds.get(tag, {}) | |
if tag == ExifTags.IFD.Exif and self._hidden_data: | |
ifd = { | |
k: v | |
for (k, v) in ifd.items() | |
if k not in (ExifTags.IFD.Interop, ExifTags.IFD.Makernote) | |
} | |
return ifd | |
def hide_offsets(self) -> None: | |
for tag in (ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo): | |
if tag in self: | |
self._hidden_data[tag] = self[tag] | |
del self[tag] | |
def __str__(self) -> str: | |
if self._info is not None: | |
# Load all keys into self._data | |
for tag in self._info: | |
self[tag] | |
return str(self._data) | |
def __len__(self) -> int: | |
keys = set(self._data) | |
if self._info is not None: | |
keys.update(self._info) | |
return len(keys) | |
def __getitem__(self, tag): | |
if self._info is not None and tag not in self._data and tag in self._info: | |
self._data[tag] = self._fixup(self._info[tag]) | |
del self._info[tag] | |
return self._data[tag] | |
def __contains__(self, tag) -> bool: | |
return tag in self._data or (self._info is not None and tag in self._info) | |
def __setitem__(self, tag, value) -> None: | |
if self._info is not None and tag in self._info: | |
del self._info[tag] | |
self._data[tag] = value | |
def __delitem__(self, tag: int) -> None: | |
if self._info is not None and tag in self._info: | |
del self._info[tag] | |
else: | |
del self._data[tag] | |
def __iter__(self): | |
keys = set(self._data) | |
if self._info is not None: | |
keys.update(self._info) | |
return iter(keys) | |