|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations |
|
|
|
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 pathlib import Path |
|
|
|
try: |
|
from defusedxml import ElementTree |
|
except ImportError: |
|
ElementTree = None |
|
|
|
|
|
|
|
|
|
from . import ( |
|
ExifTags, |
|
ImageMode, |
|
TiffTags, |
|
UnidentifiedImageError, |
|
__version__, |
|
_plugins, |
|
) |
|
from ._binary import i32le, o32be, o32le |
|
from ._util import DeferredError, is_path |
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
class DecompressionBombWarning(RuntimeWarning): |
|
pass |
|
|
|
|
|
class DecompressionBombError(Exception): |
|
pass |
|
|
|
|
|
|
|
MAX_IMAGE_PIXELS = int(1024 * 1024 * 1024 // 4 // 3) |
|
|
|
|
|
try: |
|
|
|
|
|
|
|
|
|
|
|
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.")) |
|
|
|
if str(v).startswith("Module use of python"): |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
raise |
|
|
|
|
|
USE_CFFI_ACCESS = False |
|
try: |
|
import cffi |
|
except ImportError: |
|
cffi = None |
|
|
|
|
|
def isImageType(t): |
|
""" |
|
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") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Transpose(IntEnum): |
|
FLIP_LEFT_RIGHT = 0 |
|
FLIP_TOP_BOTTOM = 1 |
|
ROTATE_90 = 2 |
|
ROTATE_180 = 3 |
|
ROTATE_270 = 4 |
|
TRANSPOSE = 5 |
|
TRANSVERSE = 6 |
|
|
|
|
|
|
|
class Transform(IntEnum): |
|
AFFINE = 0 |
|
EXTENT = 1 |
|
PERSPECTIVE = 2 |
|
QUAD = 3 |
|
MESH = 4 |
|
|
|
|
|
|
|
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, |
|
} |
|
|
|
|
|
|
|
class Dither(IntEnum): |
|
NONE = 0 |
|
ORDERED = 1 |
|
RASTERIZE = 2 |
|
FLOYDSTEINBERG = 3 |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
ID = [] |
|
OPEN = {} |
|
MIME = {} |
|
SAVE = {} |
|
SAVE_ALL = {} |
|
EXTENSION = {} |
|
DECODERS = {} |
|
ENCODERS = {} |
|
|
|
|
|
|
|
|
|
_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"] |
|
|
|
|
|
|
|
_MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B") |
|
|
|
|
|
def getmodebase(mode): |
|
""" |
|
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): |
|
""" |
|
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): |
|
""" |
|
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): |
|
""" |
|
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) |
|
|
|
|
|
|
|
|
|
|
|
_initialized = 0 |
|
|
|
|
|
def preinit(): |
|
""" |
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
def _getdecoder(mode, decoder_name, args, extra=()): |
|
|
|
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: |
|
|
|
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=()): |
|
|
|
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: |
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
class _E: |
|
def __init__(self, scale, offset): |
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
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): |
|
|
|
|
|
self.im = None |
|
self._mode = "" |
|
self._size = (0, 0) |
|
self.palette = None |
|
self.info = {} |
|
self.readonly = 0 |
|
self.pyaccess = None |
|
self._exif = None |
|
|
|
@property |
|
def width(self): |
|
return self.size[0] |
|
|
|
@property |
|
def height(self): |
|
return self.size[1] |
|
|
|
@property |
|
def size(self): |
|
return self._size |
|
|
|
@property |
|
def mode(self): |
|
return self._mode |
|
|
|
def _new(self, im): |
|
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 |
|
|
|
|
|
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): |
|
""" |
|
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 |
|
|
|
|
|
|
|
|
|
self.im = DeferredError(ValueError("Operation on closed image")) |
|
|
|
def _copy(self): |
|
self.load() |
|
self.im = self.im.copy() |
|
self.pyaccess = None |
|
self.readonly = 0 |
|
|
|
def _ensure_mutable(self): |
|
if self.readonly: |
|
self._copy() |
|
else: |
|
self.load() |
|
|
|
def _dump(self, file=None, format=None, **options): |
|
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): |
|
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): |
|
"""IPython plain text display support""" |
|
|
|
|
|
|
|
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") |
|
|
|
@property |
|
def __array_interface__(self): |
|
|
|
new = {"version": 3} |
|
try: |
|
if self.mode == "1": |
|
|
|
|
|
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(e) |
|
raise |
|
new["shape"], new["typestr"] = _conv_type_shape(self) |
|
return new |
|
|
|
def __getstate__(self): |
|
im_data = self.tobytes() |
|
return [self.info, self.mode, self.size, self.getpalette(), im_data] |
|
|
|
def __setstate__(self, state): |
|
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="raw", *args): |
|
""" |
|
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. |
|
""" |
|
|
|
|
|
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"" |
|
|
|
|
|
e = _getencoder(self.mode, encoder_name, args) |
|
e.setimage(self.im) |
|
|
|
bufsize = max(65536, self.size[0] * 4) |
|
|
|
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="image"): |
|
""" |
|
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, decoder_name="raw", *args): |
|
""" |
|
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 |
|
|
|
|
|
if len(args) == 1 and isinstance(args[0], tuple): |
|
args = args[0] |
|
|
|
|
|
if decoder_name == "raw" and args == (): |
|
args = self.mode |
|
|
|
|
|
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: |
|
|
|
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=None, matrix=None, dither=None, palette=Palette.WEB, colors=256 |
|
): |
|
""" |
|
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. |
|
|
|
The current version 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": |
|
|
|
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: |
|
|
|
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 |
|
|
|
if has_transparency: |
|
if (self.mode in ("1", "L", "I") and mode in ("LA", "RGBA")) or ( |
|
self.mode == "RGB" and mode == "RGBA" |
|
): |
|
|
|
|
|
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): |
|
|
|
warnings.warn( |
|
"Palette images with Transparency expressed in bytes should be " |
|
"converted to RGBA images" |
|
) |
|
delete_trns = True |
|
else: |
|
|
|
|
|
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": |
|
|
|
|
|
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: |
|
|
|
|
|
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: |
|
|
|
|
|
del new_im.info["transparency"] |
|
if trns is not None: |
|
try: |
|
new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) |
|
except Exception: |
|
|
|
|
|
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) |
|
|
|
|
|
if dither is None: |
|
dither = Dither.FLOYDSTEINBERG |
|
|
|
try: |
|
im = self.im.convert(mode, dither) |
|
except ValueError: |
|
try: |
|
|
|
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: |
|
|
|
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": |
|
|
|
|
|
warnings.warn( |
|
"Couldn't allocate palette entry for transparency" |
|
) |
|
else: |
|
new_im.info["transparency"] = trns |
|
return new_im |
|
|
|
def quantize( |
|
self, |
|
colors=256, |
|
method=None, |
|
kmeans=0, |
|
palette=None, |
|
dither=Dither.FLOYDSTEINBERG, |
|
): |
|
""" |
|
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 |
|
: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: |
|
|
|
method = Quantize.MEDIANCUT |
|
if self.mode == "RGBA": |
|
method = Quantize.FASTOCTREE |
|
|
|
if self.mode == "RGBA" and method not in ( |
|
Quantize.FASTOCTREE, |
|
Quantize.LIBIMAGEQUANT, |
|
): |
|
|
|
msg = ( |
|
"Fast Octree (method == 2) and libimagequant (method == 3) " |
|
"are the only valid methods for quantizing RGBA images" |
|
) |
|
raise ValueError(msg) |
|
|
|
if palette: |
|
|
|
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 |
|
|
|
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=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): |
|
""" |
|
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=True): |
|
""" |
|
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=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=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 |
|
|
|
def getextrema(self): |
|
""" |
|
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): |
|
""" |
|
Gets EXIF data from the image. |
|
|
|
:returns: an :py:class:`~PIL.Image.Exif` object. |
|
""" |
|
if self._exif is None: |
|
self._exif = Exif() |
|
self._exif._loaded = False |
|
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) |
|
|
|
|
|
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): |
|
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="RGB"): |
|
""" |
|
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 |
|
if rawmode is None: |
|
rawmode = mode |
|
return list(self.im.getpalette(mode, rawmode)) |
|
|
|
@property |
|
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): |
|
""" |
|
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=None, extrema=None): |
|
""" |
|
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: |
|
|
|
mask = box |
|
box = None |
|
|
|
if box is None: |
|
box = (0, 0) |
|
|
|
if len(box) == 2: |
|
|
|
if isImageType(im): |
|
size = im.size |
|
elif isImageType(mask): |
|
size = mask.size |
|
else: |
|
|
|
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"): |
|
|
|
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 |
|
|
|
|
|
if source == (0, 0) + im.size: |
|
overlay = im |
|
else: |
|
overlay = im.crop(source) |
|
|
|
|
|
box = dest + (dest[0] + overlay.width, dest[1] + overlay.height) |
|
|
|
|
|
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=None): |
|
""" |
|
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). In the |
|
current version, 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 self.mode in ("I", "I;16", "F"): |
|
|
|
|
|
|
|
scale, offset = _getscaleoffset(lut) |
|
return self._new(self.im.point_transform(scale, offset)) |
|
|
|
lut = [lut(i) for i in range(256)] * self.im.bands |
|
|
|
if self.mode == "F": |
|
|
|
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"): |
|
|
|
try: |
|
mode = getmodebase(self.mode) + "A" |
|
try: |
|
self.im.setmode(mode) |
|
except (AttributeError, ValueError) as e: |
|
|
|
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 |
|
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): |
|
|
|
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: |
|
|
|
try: |
|
self.im.fillband(band, alpha) |
|
except (AttributeError, ValueError): |
|
|
|
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"): |
|
""" |
|
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() |
|
|
|
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] |
|
): |
|
|
|
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: |
|
source_palette = bytearray(i // 3 for i in range(768)) |
|
|
|
palette_bytes = b"" |
|
new_positions = [0] * 256 |
|
|
|
|
|
for i, oldPosition in enumerate(dest_map): |
|
palette_bytes += source_palette[ |
|
oldPosition * bands : oldPosition * bands + bands |
|
] |
|
new_positions[oldPosition] = i |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
mapping_palette = bytearray(new_positions) |
|
|
|
m_im = self.copy() |
|
m_im._mode = "P" |
|
|
|
m_im.palette = ImagePalette.ImagePalette( |
|
palette_mode, palette=mapping_palette * bands |
|
) |
|
|
|
|
|
|
|
|
|
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): |
|
""" |
|
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) |
|
if callable(self.reduce): |
|
self = self.reduce(factor, box=reduce_box) |
|
else: |
|
self = 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 |
|
|
|
|
|
|
|
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 |
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
w, h = self.size |
|
|
|
if translate is None: |
|
post_trans = (0, 0) |
|
else: |
|
post_trans = translate |
|
if center is None: |
|
|
|
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: |
|
|
|
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)) |
|
|
|
|
|
|
|
|
|
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), pathlib.Path 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 = "" |
|
open_fp = False |
|
if isinstance(fp, Path): |
|
filename = str(fp) |
|
open_fp = True |
|
elif is_path(fp): |
|
filename = 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): |
|
|
|
filename = fp.name |
|
|
|
|
|
self._ensure_mutable() |
|
|
|
save_all = params.pop("save_all", False) |
|
self.encoderinfo = params |
|
self.encoderconfig = () |
|
|
|
preinit() |
|
|
|
ext = os.path.splitext(filename)[1].lower() |
|
|
|
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): |
|
|
|
|
|
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) -> Image: |
|
""" |
|
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. |
|
""" |
|
|
|
|
|
if frame != 0: |
|
msg = "no more images in file" |
|
raise EOFError(msg) |
|
|
|
def show(self, title=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): |
|
""" |
|
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: |
|
ims = [self.copy()] |
|
else: |
|
ims = map(self._new, self.im.split()) |
|
return tuple(ims) |
|
|
|
def getchannel(self, channel): |
|
""" |
|
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(): |
|
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 |
|
|
|
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() |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
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 |
|
|
|
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"): |
|
|
|
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: |
|
|
|
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: |
|
|
|
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: |
|
|
|
|
|
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 (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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
class ImagePointHandler: |
|
""" |
|
Used as a mixin by point transforms |
|
(for use with :py:meth:`~PIL.Image.Image.point`) |
|
""" |
|
|
|
pass |
|
|
|
|
|
class ImageTransformHandler: |
|
""" |
|
Used as a mixin by geometry transforms |
|
(for use with :py:meth:`~PIL.Image.Image.transform`) |
|
""" |
|
|
|
pass |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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, size, color=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: |
|
|
|
return Image()._new(core.new(mode, size)) |
|
|
|
if isinstance(color, str): |
|
|
|
|
|
from . import ImageColor |
|
|
|
color = ImageColor.getcolor(color, mode) |
|
|
|
im = Image() |
|
if mode == "P" and isinstance(color, (list, tuple)) and len(color) in [3, 4]: |
|
|
|
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: |
|
|
|
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. |
|
|
|
In the current version, 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) |
|
|
|
|
|
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 = { |
|
|
|
|
|
((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"), |
|
|
|
((1, 1), _ENDIAN + "i4"): ("I", "I"), |
|
((1, 1), _ENDIAN + "f4"): ("F", "F"), |
|
} |
|
|
|
|
|
def _decompression_bomb_check(size): |
|
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), pathlib.Path 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 = "" |
|
if isinstance(fp, Path): |
|
filename = str(fp.resolve()) |
|
elif is_path(fp): |
|
filename = 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): |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
def alpha_composite(im1, im2): |
|
""" |
|
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, im2, alpha): |
|
""" |
|
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, image2, mask): |
|
""" |
|
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])) |
|
|
|
|
|
|
|
|
|
|
|
|
|
def register_open(id, factory, accept=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, mimetype): |
|
""" |
|
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, driver): |
|
""" |
|
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): |
|
""" |
|
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): |
|
""" |
|
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, decoder): |
|
""" |
|
Registers an image decoder. This function should not be |
|
used in application code. |
|
|
|
:param name: The name of the decoder |
|
:param decoder: A callable(mode, args) that returns an |
|
ImageFile.PyDecoder object |
|
|
|
.. versionadded:: 4.1.0 |
|
""" |
|
DECODERS[name] = decoder |
|
|
|
|
|
def register_encoder(name, encoder): |
|
""" |
|
Registers an image encoder. This function should not be |
|
used in application code. |
|
|
|
:param name: The name of the encoder |
|
:param encoder: A callable(mode, args) that returns an |
|
ImageFile.PyEncoder object |
|
|
|
.. versionadded:: 4.1.0 |
|
""" |
|
ENCODERS[name] = encoder |
|
|
|
|
|
|
|
|
|
|
|
|
|
def _show(image, **options): |
|
from . import ImageShow |
|
|
|
ImageShow.show(image, **options) |
|
|
|
|
|
|
|
|
|
|
|
|
|
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)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_env_variables(env=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) |
|
|
|
|
|
class Exif(MutableMapping): |
|
""" |
|
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 |
|
|
|
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): |
|
|
|
|
|
return {k: self._fixup(v) for k, v in src_dict.items()} |
|
|
|
def _get_ifd_dict(self, offset): |
|
try: |
|
|
|
|
|
self.fp.seek(offset) |
|
except (KeyError, TypeError): |
|
pass |
|
else: |
|
from . import TiffImagePlugin |
|
|
|
info = TiffImagePlugin.ImageFileDirectory_v2(self.head) |
|
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): |
|
|
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
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() |
|
|
|
|
|
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) |
|
|
|
|
|
if ExifTags.IFD.Exif in self: |
|
ifd = self._get_ifd_dict(self[ExifTags.IFD.Exif]) |
|
if ifd: |
|
merged_dict.update(ifd) |
|
|
|
|
|
if ExifTags.IFD.GPSInfo in self: |
|
merged_dict[ExifTags.IFD.GPSInfo] = self._get_ifd_dict( |
|
self[ExifTags.IFD.GPSInfo] |
|
) |
|
|
|
return merged_dict |
|
|
|
def tobytes(self, offset=8): |
|
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) |
|
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: |
|
|
|
(offset,) = struct.unpack(">L", data) |
|
self.fp.seek(offset) |
|
|
|
camerainfo = {"ModelID": self.fp.read(4)} |
|
|
|
self.fp.read(4) |
|
|
|
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: |
|
|
|
self._ifds[tag] = self._get_ifd_dict(tag_data) |
|
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): |
|
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): |
|
if self._info is not None: |
|
|
|
for tag in self._info: |
|
self[tag] |
|
|
|
return str(self._data) |
|
|
|
def __len__(self): |
|
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): |
|
return tag in self._data or (self._info is not None and tag in self._info) |
|
|
|
def __setitem__(self, tag, value): |
|
if self._info is not None and tag in self._info: |
|
del self._info[tag] |
|
self._data[tag] = value |
|
|
|
def __delitem__(self, tag): |
|
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) |
|
|