| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | from __future__ import annotations |
| |
|
| | import abc |
| | import atexit |
| | import builtins |
| | import io |
| | import logging |
| | import math |
| | import os |
| | import re |
| | import struct |
| | import sys |
| | import tempfile |
| | import warnings |
| | from collections.abc import MutableMapping |
| | from enum import IntEnum |
| | from typing import IO, Protocol, cast |
| |
|
| | |
| | |
| | |
| | from . import ( |
| | ExifTags, |
| | ImageMode, |
| | TiffTags, |
| | UnidentifiedImageError, |
| | __version__, |
| | _plugins, |
| | ) |
| | from ._binary import i32le, o32be, o32le |
| | from ._deprecate import deprecate |
| | from ._util import DeferredError, is_path |
| |
|
| | ElementTree: ModuleType | None |
| | try: |
| | from defusedxml import ElementTree |
| | except ImportError: |
| | ElementTree = None |
| |
|
| | TYPE_CHECKING = False |
| | if TYPE_CHECKING: |
| | from collections.abc import Callable, Iterator, Sequence |
| | from types import ModuleType |
| | from typing import Any, Literal |
| |
|
| | logger = logging.getLogger(__name__) |
| |
|
| |
|
| | class DecompressionBombWarning(RuntimeWarning): |
| | pass |
| |
|
| |
|
| | class DecompressionBombError(Exception): |
| | pass |
| |
|
| |
|
| | WARN_POSSIBLE_FORMATS: bool = False |
| |
|
| | |
| | MAX_IMAGE_PIXELS: int | None = 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: |
| | |
| | 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 |
| |
|
| |
|
| | |
| | |
| |
|
| |
|
| | |
| | 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 |
| |
|
| |
|
| | |
| | |
| |
|
| | TYPE_CHECKING = False |
| | if TYPE_CHECKING: |
| | import mmap |
| | from xml.etree.ElementTree import Element |
| |
|
| | from IPython.lib.pretty import PrettyPrinter |
| |
|
| | from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin |
| | from ._typing import CapsuleType, NumpyArray, StrOrBytesPath |
| | ID: list[str] = [] |
| | OPEN: dict[ |
| | str, |
| | tuple[ |
| | Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], |
| | Callable[[bytes], bool | str] | None, |
| | ], |
| | ] = {} |
| | MIME: dict[str, str] = {} |
| | SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} |
| | SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} |
| | EXTENSION: dict[str, str] = {} |
| | DECODERS: dict[str, type[ImageFile.PyDecoder]] = {} |
| | ENCODERS: dict[str, type[ImageFile.PyEncoder]] = {} |
| |
|
| | |
| | |
| |
|
| | _ENDIAN = "<" if sys.byteorder == "little" else ">" |
| |
|
| |
|
| | def _conv_type_shape(im: Image) -> tuple[tuple[int, ...], str]: |
| | m = ImageMode.getmode(im.mode) |
| | shape: tuple[int, ...] = (im.height, im.width) |
| | extra = len(m.bands) |
| | if extra != 1: |
| | shape += (extra,) |
| | return shape, m.typestr |
| |
|
| |
|
| | MODES = [ |
| | "1", |
| | "CMYK", |
| | "F", |
| | "HSV", |
| | "I", |
| | "I;16", |
| | "I;16B", |
| | "I;16L", |
| | "I;16N", |
| | "L", |
| | "LA", |
| | "La", |
| | "LAB", |
| | "P", |
| | "PA", |
| | "RGB", |
| | "RGBA", |
| | "RGBa", |
| | "RGBX", |
| | "YCbCr", |
| | ] |
| |
|
| | |
| | |
| | _MAPMODES = ("L", "P", "RGBX", "RGBA", "CMYK", "I;16", "I;16L", "I;16B") |
| |
|
| |
|
| | def getmodebase(mode: str) -> str: |
| | """ |
| | Gets the "base" mode for given mode. This function returns "L" for |
| | images that contain grayscale data, and "RGB" for images that |
| | contain color data. |
| | |
| | :param mode: Input mode. |
| | :returns: "L" or "RGB". |
| | :exception KeyError: If the input mode was not a standard mode. |
| | """ |
| | return ImageMode.getmode(mode).basemode |
| |
|
| |
|
| | def getmodetype(mode: str) -> str: |
| | """ |
| | Gets the storage type mode. Given a mode, this function returns a |
| | single-layer mode suitable for storing individual bands. |
| | |
| | :param mode: Input mode. |
| | :returns: "L", "I", or "F". |
| | :exception KeyError: If the input mode was not a standard mode. |
| | """ |
| | return ImageMode.getmode(mode).basetype |
| |
|
| |
|
| | def getmodebandnames(mode: str) -> tuple[str, ...]: |
| | """ |
| | Gets a list of individual band names. Given a mode, this function returns |
| | a tuple containing the names of individual bands (use |
| | :py:method:`~PIL.Image.getmodetype` to get the mode used to store each |
| | individual band. |
| | |
| | :param mode: Input mode. |
| | :returns: A tuple containing band names. The length of the tuple |
| | gives the number of bands in an image of the given mode. |
| | :exception KeyError: If the input mode was not a standard mode. |
| | """ |
| | return ImageMode.getmode(mode).bands |
| |
|
| |
|
| | def getmodebands(mode: str) -> int: |
| | """ |
| | Gets the number of individual bands for this mode. |
| | |
| | :param mode: Input mode. |
| | :returns: The number of bands in this mode. |
| | :exception KeyError: If the input mode was not a standard mode. |
| | """ |
| | return len(ImageMode.getmode(mode).bands) |
| |
|
| |
|
| | |
| | |
| |
|
| | _initialized = 0 |
| |
|
| |
|
| | def preinit() -> None: |
| | """ |
| | Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers. |
| | |
| | It is called when opening or saving images. |
| | """ |
| |
|
| | global _initialized |
| | if _initialized >= 1: |
| | return |
| |
|
| | try: |
| | from . import BmpImagePlugin |
| |
|
| | assert BmpImagePlugin |
| | except ImportError: |
| | pass |
| | try: |
| | from . import GifImagePlugin |
| |
|
| | assert GifImagePlugin |
| | except ImportError: |
| | pass |
| | try: |
| | from . import JpegImagePlugin |
| |
|
| | assert JpegImagePlugin |
| | except ImportError: |
| | pass |
| | try: |
| | from . import PpmImagePlugin |
| |
|
| | assert PpmImagePlugin |
| | except ImportError: |
| | pass |
| | try: |
| | from . import PngImagePlugin |
| |
|
| | assert PngImagePlugin |
| | except ImportError: |
| | pass |
| |
|
| | _initialized = 1 |
| |
|
| |
|
| | def init() -> bool: |
| | """ |
| | 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 False |
| |
|
| | 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 True |
| | return False |
| |
|
| |
|
| | |
| | |
| |
|
| |
|
| | def _getdecoder( |
| | mode: str, decoder_name: str, args: Any, extra: tuple[Any, ...] = () |
| | ) -> core.ImagingDecoder | ImageFile.PyDecoder: |
| | |
| | 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, f"{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: str, encoder_name: str, args: Any, extra: tuple[Any, ...] = () |
| | ) -> core.ImagingEncoder | ImageFile.PyEncoder: |
| | |
| | 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, f"{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 ImagePointTransform: |
| | """ |
| | Used with :py:meth:`~PIL.Image.Image.point` for single band images with more than |
| | 8 bits, this represents an affine transformation, where the value is multiplied by |
| | ``scale`` and ``offset`` is added. |
| | """ |
| |
|
| | def __init__(self, scale: float, offset: float) -> None: |
| | self.scale = scale |
| | self.offset = offset |
| |
|
| | def __neg__(self) -> ImagePointTransform: |
| | return ImagePointTransform(-self.scale, -self.offset) |
| |
|
| | def __add__(self, other: ImagePointTransform | float) -> ImagePointTransform: |
| | if isinstance(other, ImagePointTransform): |
| | return ImagePointTransform( |
| | self.scale + other.scale, self.offset + other.offset |
| | ) |
| | return ImagePointTransform(self.scale, self.offset + other) |
| |
|
| | __radd__ = __add__ |
| |
|
| | def __sub__(self, other: ImagePointTransform | float) -> ImagePointTransform: |
| | return self + -other |
| |
|
| | def __rsub__(self, other: ImagePointTransform | float) -> ImagePointTransform: |
| | return other + -self |
| |
|
| | def __mul__(self, other: ImagePointTransform | float) -> ImagePointTransform: |
| | if isinstance(other, ImagePointTransform): |
| | return NotImplemented |
| | return ImagePointTransform(self.scale * other, self.offset * other) |
| |
|
| | __rmul__ = __mul__ |
| |
|
| | def __truediv__(self, other: ImagePointTransform | float) -> ImagePointTransform: |
| | if isinstance(other, ImagePointTransform): |
| | return NotImplemented |
| | return ImagePointTransform(self.scale / other, self.offset / other) |
| |
|
| |
|
| | def _getscaleoffset( |
| | expr: Callable[[ImagePointTransform], ImagePointTransform | float], |
| | ) -> tuple[float, float]: |
| | a = expr(ImagePointTransform(1, 0)) |
| | return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a) |
| |
|
| |
|
| | |
| | |
| |
|
| |
|
| | class SupportsGetData(Protocol): |
| | def getdata( |
| | self, |
| | ) -> tuple[Transform, Sequence[int]]: ... |
| |
|
| |
|
| | 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) -> None: |
| | |
| | self._im: core.ImagingCore | DeferredError | None = None |
| | self._mode = "" |
| | self._size = (0, 0) |
| | self.palette: ImagePalette.ImagePalette | None = None |
| | self.info: dict[str | tuple[int, int], Any] = {} |
| | self.readonly = 0 |
| | self._exif: Exif | None = None |
| |
|
| | @property |
| | def im(self) -> core.ImagingCore: |
| | if isinstance(self._im, DeferredError): |
| | raise self._im.ex |
| | assert self._im is not None |
| | return self._im |
| |
|
| | @im.setter |
| | def im(self, im: core.ImagingCore) -> None: |
| | self._im = im |
| |
|
| | @property |
| | def width(self) -> int: |
| | return self.size[0] |
| |
|
| | @property |
| | def height(self) -> int: |
| | return self.size[1] |
| |
|
| | @property |
| | def size(self) -> tuple[int, int]: |
| | return self._size |
| |
|
| | @property |
| | def mode(self) -> str: |
| | return self._mode |
| |
|
| | @property |
| | def readonly(self) -> int: |
| | return (self._im and self._im.readonly) or self._readonly |
| |
|
| | @readonly.setter |
| | def readonly(self, readonly: int) -> None: |
| | self._readonly = readonly |
| |
|
| | def _new(self, im: core.ImagingCore) -> Image: |
| | new = Image() |
| | new.im = im |
| | new._mode = im.mode |
| | new._size = im.size |
| | if im.mode in ("P", "PA"): |
| | if self.palette: |
| | new.palette = self.palette.copy() |
| | else: |
| | from . import ImagePalette |
| |
|
| | new.palette = ImagePalette.ImagePalette() |
| | new.info = self.info.copy() |
| | return new |
| |
|
| | |
| | def __enter__(self) -> Image: |
| | return self |
| |
|
| | def __exit__(self, *args: object) -> None: |
| | pass |
| |
|
| | def close(self) -> None: |
| | """ |
| | 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 getattr(self, "map", None): |
| | if sys.platform == "win32" and hasattr(sys, "pypy_version_info"): |
| | self.map.close() |
| | self.map: mmap.mmap | None = None |
| |
|
| | |
| | |
| | |
| | self._im = DeferredError(ValueError("Operation on closed image")) |
| |
|
| | def _copy(self) -> None: |
| | self.load() |
| | self.im = self.im.copy() |
| | self.readonly = 0 |
| |
|
| | def _ensure_mutable(self) -> None: |
| | if self.readonly: |
| | self._copy() |
| | else: |
| | self.load() |
| |
|
| | def _dump( |
| | self, file: str | None = None, format: str | None = None, **options: Any |
| | ) -> str: |
| | suffix = "" |
| | if format: |
| | suffix = f".{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: object) -> bool: |
| | if self.__class__ is not other.__class__: |
| | return False |
| | assert isinstance(other, Image) |
| | return ( |
| | self.mode == other.mode |
| | and self.size == other.size |
| | and self.info == other.info |
| | and self.getpalette() == other.getpalette() |
| | and self.tobytes() == other.tobytes() |
| | ) |
| |
|
| | def __repr__(self) -> str: |
| | return ( |
| | f"<{self.__class__.__module__}.{self.__class__.__name__} " |
| | f"image mode={self.mode} size={self.size[0]}x{self.size[1]} " |
| | f"at 0x{id(self):X}>" |
| | ) |
| |
|
| | def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: |
| | """IPython plain text display support""" |
| |
|
| | |
| | |
| | p.text( |
| | f"<{self.__class__.__module__}.{self.__class__.__name__} " |
| | f"image mode={self.mode} size={self.size[0]}x{self.size[1]}>" |
| | ) |
| |
|
| | def _repr_image(self, image_format: str, **kwargs: Any) -> bytes | None: |
| | """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) -> bytes | None: |
| | """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) -> bytes | None: |
| | """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) -> dict[str, str | bytes | int | tuple[int, ...]]: |
| | |
| | new: dict[str, str | bytes | int | tuple[int, ...]] = {"version": 3} |
| | if self.mode == "1": |
| | |
| | |
| | new["data"] = self.tobytes("raw", "L") |
| | else: |
| | new["data"] = self.tobytes() |
| | new["shape"], new["typestr"] = _conv_type_shape(self) |
| | return new |
| |
|
| | def __arrow_c_schema__(self) -> object: |
| | self.load() |
| | return self.im.__arrow_c_schema__() |
| |
|
| | def __arrow_c_array__( |
| | self, requested_schema: object | None = None |
| | ) -> tuple[object, object]: |
| | self.load() |
| | return (self.im.__arrow_c_schema__(), self.im.__arrow_c_array__()) |
| |
|
| | def __getstate__(self) -> list[Any]: |
| | im_data = self.tobytes() |
| | return [self.info, self.mode, self.size, self.getpalette(), im_data] |
| |
|
| | def __setstate__(self, state: list[Any]) -> None: |
| | Image.__init__(self) |
| | info, mode, size, palette, data = state[:5] |
| | self.info = info |
| | self._mode = mode |
| | self._size = size |
| | self.im = core.new(mode, size) |
| | if mode in ("L", "LA", "P", "PA") and palette: |
| | self.putpalette(palette) |
| | self.frombytes(data) |
| |
|
| | def tobytes(self, encoder_name: str = "raw", *args: Any) -> bytes: |
| | """ |
| | Return image as a bytes object. |
| | |
| | .. warning:: |
| | |
| | This method returns raw image data derived from Pillow's 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. |
| | To see how this packs pixel data into the returned |
| | bytes, see :file:`libImaging/Pack.c`. |
| | |
| | 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. |
| | """ |
| |
|
| | encoder_args: Any = args |
| | if len(encoder_args) == 1 and isinstance(encoder_args[0], tuple): |
| | |
| | encoder_args = encoder_args[0] |
| |
|
| | if encoder_name == "raw" and encoder_args == (): |
| | encoder_args = self.mode |
| |
|
| | self.load() |
| |
|
| | if self.width == 0 or self.height == 0: |
| | return b"" |
| |
|
| | |
| | e = _getencoder(self.mode, encoder_name, encoder_args) |
| | e.setimage(self.im) |
| |
|
| | from . import ImageFile |
| |
|
| | bufsize = max(ImageFile.MAXBLOCK, 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: str = "image") -> bytes: |
| | """ |
| | Returns the image converted to an X11 bitmap. |
| | |
| | .. note:: This method only works for mode "1" images. |
| | |
| | :param name: The name prefix to use for the bitmap variables. |
| | :returns: A string containing an X11 bitmap. |
| | :raises ValueError: If the mode is not "1" |
| | """ |
| |
|
| | self.load() |
| | if self.mode != "1": |
| | msg = "not a bitmap" |
| | raise ValueError(msg) |
| | data = self.tobytes("xbm") |
| | return b"".join( |
| | [ |
| | f"#define {name}_width {self.size[0]}\n".encode("ascii"), |
| | f"#define {name}_height {self.size[1]}\n".encode("ascii"), |
| | f"static char {name}_bits[] = {{\n".encode("ascii"), |
| | data, |
| | b"};", |
| | ] |
| | ) |
| |
|
| | def frombytes( |
| | self, |
| | data: bytes | bytearray | SupportsArrayInterface, |
| | decoder_name: str = "raw", |
| | *args: Any, |
| | ) -> None: |
| | """ |
| | Loads this image with pixel data from a bytes object. |
| | |
| | This method is similar to the :py:func:`~PIL.Image.frombytes` function, |
| | but loads data into this image instead of creating a new image object. |
| | """ |
| |
|
| | if self.width == 0 or self.height == 0: |
| | return |
| |
|
| | decoder_args: Any = args |
| | if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple): |
| | |
| | decoder_args = decoder_args[0] |
| |
|
| | |
| | if decoder_name == "raw" and decoder_args == (): |
| | decoder_args = self.mode |
| |
|
| | |
| | d = _getdecoder(self.mode, decoder_name, decoder_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) -> core.PixelAccess | None: |
| | """ |
| | 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: :py:class:`.PixelAccess` |
| | """ |
| | if self._im is not None and self.palette and self.palette.dirty: |
| | |
| | mode, arr = self.palette.getdata() |
| | self.im.putpalette(self.palette.mode, 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: |
| | self.palette.palette = self.im.getpalette( |
| | self.palette.mode, self.palette.mode |
| | ) |
| |
|
| | if self._im is not None: |
| | return self.im.pixel_access(self.readonly) |
| | return None |
| |
|
| | def verify(self) -> None: |
| | """ |
| | Verifies the contents of a file. For data read from a file, this |
| | method attempts to determine if the file is broken, without |
| | actually decoding the image data. If this method finds any |
| | problems, it raises suitable exceptions. If you need to load |
| | the image after using this method, you must reopen the image |
| | file. |
| | """ |
| | pass |
| |
|
| | def convert( |
| | self, |
| | mode: str | None = None, |
| | matrix: tuple[float, ...] | None = None, |
| | dither: Dither | None = None, |
| | palette: Palette = Palette.WEB, |
| | colors: int = 256, |
| | ) -> Image: |
| | """ |
| | Returns a converted copy of this image. For the "P" mode, this |
| | method translates pixels through the palette. If mode is |
| | omitted, a mode is chosen so that all information in the image |
| | and the palette can be represented without a palette. |
| | |
| | This supports all possible conversions between "L", "RGB" and "CMYK". The |
| | ``matrix`` argument only supports "L" and "RGB". |
| | |
| | When translating a color image to grayscale (mode "L"), |
| | the library uses the ITU-R 601-2 luma transform:: |
| | |
| | L = R * 299/1000 + G * 587/1000 + B * 114/1000 |
| | |
| | The default method of converting a grayscale ("L") or "RGB" |
| | image into a bilevel (mode "1") image uses Floyd-Steinberg |
| | dither to approximate the original image luminosity levels. If |
| | dither is ``None``, all values larger than 127 are set to 255 (white), |
| | all other values to 0 (black). To use other thresholds, use the |
| | :py:meth:`~PIL.Image.Image.point` method. |
| | |
| | When converting from "RGBA" to "P" without a ``matrix`` argument, |
| | this passes the operation to :py:meth:`~PIL.Image.Image.quantize`, |
| | and ``dither`` and ``palette`` are ignored. |
| | |
| | When converting from "PA", if an "RGBA" palette is present, the alpha |
| | channel from the image will be used instead of the values from the palette. |
| | |
| | :param mode: The requested mode. See: :ref:`concept-modes`. |
| | :param matrix: An optional conversion matrix. If given, this |
| | should be 4- or 12-tuple containing floating point values. |
| | :param dither: Dithering method, used when converting from |
| | mode "RGB" to "P" or from "RGB" or "L" to "1". |
| | Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` |
| | (default). Note that this is not used when ``matrix`` is supplied. |
| | :param palette: Palette to use when converting from mode "RGB" |
| | to "P". Available palettes are :data:`Palette.WEB` or |
| | :data:`Palette.ADAPTIVE`. |
| | :param colors: Number of colors to use for the :data:`Palette.ADAPTIVE` |
| | palette. Defaults to 256. |
| | :rtype: :py:class:`~PIL.Image.Image` |
| | :returns: An :py:class:`~PIL.Image.Image` object. |
| | """ |
| |
|
| | self.load() |
| |
|
| | has_transparency = "transparency" in self.info |
| | if not mode and self.mode == "P": |
| | |
| | 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: tuple[float, ...], v: tuple[int, int, int] |
| | ) -> int: |
| | value = m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * 0.5 |
| | return max(0, min(255, int(value))) |
| |
|
| | 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(len(transparency)) |
| | ) |
| | new_im.info["transparency"] = transparency |
| | return new_im |
| |
|
| | if self.mode == "RGBA": |
| | if mode == "P": |
| | return self.quantize(colors) |
| | elif mode == "PA": |
| | r, g, b, a = self.split() |
| | rgb = merge("RGB", (r, g, b)) |
| | p = rgb.quantize(colors) |
| | return merge("PA", (p, a)) |
| |
|
| | trns = None |
| | delete_trns = False |
| | |
| | if has_transparency: |
| | if (self.mode in ("1", "L", "I", "I;16") and mode in ("LA", "RGBA")) or ( |
| | self.mode == "RGB" and mode in ("La", "LA", "RGBa", "RGBA") |
| | ): |
| | |
| | |
| | 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": |
| | assert self.palette is not None |
| | trns_im.putpalette(self.palette, self.palette.mode) |
| | if isinstance(t, tuple): |
| | err = "Couldn't allocate a palette color for transparency" |
| | assert trns_im.palette is not None |
| | 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( |
| | cast(tuple[int, ...], 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): |
| | im = self |
| | if mode == "LAB": |
| | if im.mode not in ("RGB", "RGBA", "RGBX"): |
| | im = im.convert("RGBA") |
| | other_mode = im.mode |
| | else: |
| | other_mode = mode |
| | if other_mode in ("RGB", "RGBA", "RGBX"): |
| | from . import ImageCms |
| |
|
| | srgb = ImageCms.createProfile("sRGB") |
| | lab = ImageCms.createProfile("LAB") |
| | profiles = [lab, srgb] if im.mode == "LAB" else [srgb, lab] |
| | transform = ImageCms.buildTransform( |
| | profiles[0], profiles[1], im.mode, mode |
| | ) |
| | return transform.apply(im) |
| |
|
| | |
| | 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 in ("P", "PA") 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" and new_im.palette: |
| | try: |
| | new_im.info["transparency"] = new_im.palette.getcolor( |
| | cast(tuple[int, ...], 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: int = 256, |
| | method: int | None = None, |
| | kmeans: int = 0, |
| | palette: Image | None = None, |
| | dither: Dither = Dither.FLOYDSTEINBERG, |
| | ) -> Image: |
| | """ |
| | Convert the image to 'P' mode with the specified number |
| | of colors. |
| | |
| | :param colors: The desired number of colors, <= 256 |
| | :param method: :data:`Quantize.MEDIANCUT` (median cut), |
| | :data:`Quantize.MAXCOVERAGE` (maximum coverage), |
| | :data:`Quantize.FASTOCTREE` (fast octree), |
| | :data:`Quantize.LIBIMAGEQUANT` (libimagequant; check support |
| | using :py:func:`PIL.features.check_feature` with |
| | ``feature="libimagequant"``). |
| | |
| | By default, :data:`Quantize.MEDIANCUT` will be used. |
| | |
| | The exception to this is RGBA images. :data:`Quantize.MEDIANCUT` |
| | and :data:`Quantize.MAXCOVERAGE` do not support RGBA images, so |
| | :data:`Quantize.FASTOCTREE` is used by default instead. |
| | :param kmeans: Integer greater than or equal to zero. |
| | :param palette: Quantize to the palette of given |
| | :py:class:`PIL.Image.Image`. |
| | :param dither: Dithering method, used when converting from |
| | mode "RGB" to "P" or from "RGB" or "L" to "1". |
| | Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` |
| | (default). |
| | :returns: A new image |
| | """ |
| |
|
| | self.load() |
| |
|
| | if method is None: |
| | |
| | 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) |
| | assert palette.palette is not None |
| | new_im.palette = palette.palette.copy() |
| | return new_im |
| |
|
| | if kmeans < 0: |
| | msg = "kmeans must not be negative" |
| | raise ValueError(msg) |
| |
|
| | im = self._new(self.im.quantize(colors, method, kmeans)) |
| |
|
| | from . import ImagePalette |
| |
|
| | mode = im.im.getpalettemode() |
| | palette_data = im.im.getpalette(mode, mode)[: colors * len(mode)] |
| | im.palette = ImagePalette.ImagePalette(mode, palette_data) |
| |
|
| | return im |
| |
|
| | def copy(self) -> Image: |
| | """ |
| | Copies this image. Use this method if you wish to paste things |
| | into an image, but still retain the original. |
| | |
| | :rtype: :py:class:`~PIL.Image.Image` |
| | :returns: An :py:class:`~PIL.Image.Image` object. |
| | """ |
| | self.load() |
| | return self._new(self.im.copy()) |
| |
|
| | __copy__ = copy |
| |
|
| | def crop(self, box: tuple[float, float, float, float] | None = None) -> Image: |
| | """ |
| | Returns a rectangular region from this image. The box is a |
| | 4-tuple defining the left, upper, right, and lower pixel |
| | coordinate. See :ref:`coordinate-system`. |
| | |
| | Note: Prior to Pillow 3.4.0, this was a lazy operation. |
| | |
| | :param box: The crop rectangle, as a (left, upper, right, lower)-tuple. |
| | :rtype: :py:class:`~PIL.Image.Image` |
| | :returns: An :py:class:`~PIL.Image.Image` object. |
| | """ |
| |
|
| | if box is None: |
| | return self.copy() |
| |
|
| | if box[2] < box[0]: |
| | msg = "Coordinate 'right' is less than 'left'" |
| | raise ValueError(msg) |
| | elif box[3] < box[1]: |
| | msg = "Coordinate 'lower' is less than 'upper'" |
| | raise ValueError(msg) |
| |
|
| | self.load() |
| | return self._new(self._crop(self.im, box)) |
| |
|
| | def _crop( |
| | self, im: core.ImagingCore, box: tuple[float, float, float, float] |
| | ) -> core.ImagingCore: |
| | """ |
| | 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: str | None, size: tuple[int, int] | None |
| | ) -> tuple[str, tuple[int, int, float, float]] | None: |
| | """ |
| | 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 filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: |
| | """ |
| | 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 callable(filter): |
| | filter = filter() |
| | if not hasattr(filter, "filter"): |
| | msg = "filter argument should be ImageFilter.Filter instance or class" |
| | raise TypeError(msg) |
| |
|
| | multiband = isinstance(filter, ImageFilter.MultibandFilter) |
| | if self.im.bands == 1 or multiband: |
| | return self._new(filter.filter(self.im)) |
| |
|
| | ims = [ |
| | self._new(filter.filter(self.im.getband(c))) for c in range(self.im.bands) |
| | ] |
| | return merge(self.mode, ims) |
| |
|
| | def getbands(self) -> tuple[str, ...]: |
| | """ |
| | Returns a tuple containing the name of each band in this image. |
| | For example, ``getbands`` on an RGB image returns ("R", "G", "B"). |
| | |
| | :returns: A tuple containing band names. |
| | :rtype: tuple |
| | """ |
| | return ImageMode.getmode(self.mode).bands |
| |
|
| | def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int] | None: |
| | """ |
| | Calculates the bounding box of the non-zero regions in the |
| | image. |
| | |
| | :param alpha_only: Optional flag, defaulting to ``True``. |
| | If ``True`` and the image has an alpha channel, trim transparent pixels. |
| | Otherwise, trim pixels when all channels are zero. |
| | Keyword-only argument. |
| | :returns: The bounding box is returned as a 4-tuple defining the |
| | left, upper, right, and lower pixel coordinate. See |
| | :ref:`coordinate-system`. If the image is completely empty, this |
| | method returns None. |
| | |
| | """ |
| |
|
| | self.load() |
| | return self.im.getbbox(alpha_only) |
| |
|
| | def getcolors( |
| | self, maxcolors: int = 256 |
| | ) -> list[tuple[int, tuple[int, ...]]] | list[tuple[int, float]] | None: |
| | """ |
| | 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: list[tuple[int, float]] = [(h[i], i) for i in range(256) if h[i]] |
| | if len(out) > maxcolors: |
| | return None |
| | return out |
| | return self.im.getcolors(maxcolors) |
| |
|
| | def getdata(self, band: int | None = None) -> core.ImagingCore: |
| | """ |
| | 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. |
| | """ |
| | deprecate("Image.Image.getdata", 14, "get_flattened_data") |
| |
|
| | self.load() |
| | if band is not None: |
| | return self.im.getband(band) |
| | return self.im |
| |
|
| | def get_flattened_data( |
| | self, band: int | None = None |
| | ) -> tuple[tuple[int, ...], ...] | tuple[float, ...]: |
| | """ |
| | Returns the contents of this image as a tuple 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. |
| | |
| | :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 tuple containing pixel values. |
| | """ |
| | self.load() |
| | if band is not None: |
| | return tuple(self.im.getband(band)) |
| | return tuple(self.im) |
| |
|
| | def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]: |
| | """ |
| | Gets the minimum and maximum pixel values for each band in |
| | the image. |
| | |
| | :returns: For a single-band image, a 2-tuple containing the |
| | minimum and maximum pixel value. For a multi-band image, |
| | a tuple containing one 2-tuple for each band. |
| | """ |
| |
|
| | self.load() |
| | if self.im.bands > 1: |
| | return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands)) |
| | return self.im.getextrema() |
| |
|
| | def getxmp(self) -> dict[str, Any]: |
| | """ |
| | Returns a dictionary containing the XMP tags. |
| | Requires defusedxml to be installed. |
| | |
| | :returns: XMP tags in a dictionary. |
| | """ |
| |
|
| | def get_name(tag: str) -> str: |
| | return re.sub("^{[^}]+}", "", tag) |
| |
|
| | def get_value(element: Element) -> str | dict[str, Any] | None: |
| | value: dict[str, Any] = {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 {} |
| | if "xmp" not in self.info: |
| | return {} |
| | root = ElementTree.fromstring(self.info["xmp"].rstrip(b"\x00 ")) |
| | return {get_name(root.tag): get_value(root)} |
| |
|
| | def getexif(self) -> Exif: |
| | """ |
| | Gets EXIF data from the image. |
| | |
| | :returns: an :py:class:`~PIL.Image.Exif` object. |
| | """ |
| | if self._exif is None: |
| | self._exif = Exif() |
| | elif self._exif._loaded: |
| | return self._exif |
| | self._exif._loaded = True |
| |
|
| | exif_info = self.info.get("exif") |
| | if exif_info is None: |
| | if "Raw profile type exif" in self.info: |
| | exif_info = bytes.fromhex( |
| | "".join(self.info["Raw profile type exif"].split("\n")[3:]) |
| | ) |
| | elif hasattr(self, "tag_v2"): |
| | from . import TiffImagePlugin |
| |
|
| | assert isinstance(self, TiffImagePlugin.TiffImageFile) |
| | self._exif.bigtiff = self.tag_v2._bigtiff |
| | self._exif.endian = self.tag_v2._endian |
| |
|
| | assert self.fp is not None |
| | 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") |
| | pattern: str | bytes = r'tiff:Orientation(="|>)([0-9])' |
| | if not xmp_tags and (xmp_tags := self.info.get("xmp")): |
| | pattern = rb'tiff:Orientation(="|>)([0-9])' |
| | if xmp_tags: |
| | match = re.search(pattern, xmp_tags) |
| | if match: |
| | self._exif[ExifTags.Base.Orientation] = int(match[2]) |
| |
|
| | return self._exif |
| |
|
| | def _reload_exif(self) -> None: |
| | if self._exif is None or not self._exif._loaded: |
| | return |
| | self._exif._loaded = False |
| | self.getexif() |
| |
|
| | def get_child_images(self) -> list[ImageFile.ImageFile]: |
| | from . import ImageFile |
| |
|
| | deprecate("Image.Image.get_child_images", 13) |
| | return ImageFile.ImageFile.get_child_images(self) |
| |
|
| | def getim(self) -> CapsuleType: |
| | """ |
| | Returns a capsule that points to the internal image memory. |
| | |
| | :returns: A capsule object. |
| | """ |
| |
|
| | self.load() |
| | return self.im.ptr |
| |
|
| | def getpalette(self, rawmode: str | None = "RGB") -> list[int] | None: |
| | """ |
| | Returns the image palette as a list. |
| | |
| | :param rawmode: The mode in which to return the palette. ``None`` will |
| | return the palette in its current mode. |
| | |
| | .. versionadded:: 9.1.0 |
| | |
| | :returns: A list of color values [r, g, b, ...], or None if the |
| | image has no palette. |
| | """ |
| |
|
| | self.load() |
| | try: |
| | mode = self.im.getpalettemode() |
| | except ValueError: |
| | return None |
| | 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. |
| | """ |
| | if ( |
| | self.mode in ("LA", "La", "PA", "RGBA", "RGBa") |
| | or "transparency" in self.info |
| | ): |
| | return True |
| | if self.mode == "P": |
| | assert self.palette is not None |
| | return self.palette.mode.endswith("A") |
| | return False |
| |
|
| | def apply_transparency(self) -> None: |
| | """ |
| | 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") |
| | assert palette is not None |
| | 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: tuple[int, int] | list[int] |
| | ) -> float | tuple[int, ...] | None: |
| | """ |
| | 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() |
| | return self.im.getpixel(tuple(xy)) |
| |
|
| | def getprojection(self) -> tuple[list[int], list[int]]: |
| | """ |
| | Get projection to x and y axes |
| | |
| | :returns: Two sequences, indicating where there are non-zero |
| | pixels along the X-axis and the Y-axis, respectively. |
| | """ |
| |
|
| | self.load() |
| | x, y = self.im.getprojection() |
| | return list(x), list(y) |
| |
|
| | def histogram( |
| | self, mask: Image | None = None, extrema: tuple[float, float] | None = None |
| | ) -> list[int]: |
| | """ |
| | Returns a histogram for the image. The histogram is returned as a |
| | list of pixel counts, one for each pixel value in the source |
| | image. Counts are grouped into 256 bins for each band, even if |
| | the image has more than 8 bits per band. If the image has more |
| | than one band, the histograms for all bands are concatenated (for |
| | example, the histogram for an "RGB" image contains 768 values). |
| | |
| | A bilevel image (mode "1") is treated as a grayscale ("L") image |
| | by this method. |
| | |
| | If a mask is provided, the method returns a histogram for those |
| | parts of the image where the mask image is non-zero. The mask |
| | image must have the same size as the image, and be either a |
| | bi-level image (mode "1") or a grayscale image ("L"). |
| | |
| | :param mask: An optional mask. |
| | :param extrema: An optional tuple of manually-specified extrema. |
| | :returns: A list containing pixel counts. |
| | """ |
| | self.load() |
| | if mask: |
| | mask.load() |
| | return self.im.histogram((0, 0), mask.im) |
| | if self.mode in ("I", "F"): |
| | return self.im.histogram( |
| | extrema if extrema is not None else self.getextrema() |
| | ) |
| | return self.im.histogram() |
| |
|
| | def entropy( |
| | self, mask: Image | None = None, extrema: tuple[float, float] | None = None |
| | ) -> float: |
| | """ |
| | 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"): |
| | return self.im.entropy( |
| | extrema if extrema is not None else self.getextrema() |
| | ) |
| | return self.im.entropy() |
| |
|
| | def paste( |
| | self, |
| | im: Image | str | float | tuple[float, ...], |
| | box: Image | tuple[int, int, int, int] | tuple[int, int] | None = None, |
| | mask: Image | None = 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. See |
| | :ref:`colors` for more information. |
| | |
| | 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, float 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 isinstance(box, Image): |
| | if mask is not None: |
| | msg = "If using second argument as mask, third argument must be None" |
| | raise ValueError(msg) |
| | |
| | mask = box |
| | box = None |
| |
|
| | if box is None: |
| | box = (0, 0) |
| |
|
| | if len(box) == 2: |
| | |
| | if isinstance(im, Image): |
| | size = im.size |
| | elif isinstance(mask, Image): |
| | 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]) |
| |
|
| | source: core.ImagingCore | str | float | tuple[float, ...] |
| | if isinstance(im, str): |
| | from . import ImageColor |
| |
|
| | source = ImageColor.getcolor(im, self.mode) |
| | elif isinstance(im, Image): |
| | im.load() |
| | if self.mode != im.mode: |
| | if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): |
| | |
| | im = im.convert(self.mode) |
| | source = im.im |
| | else: |
| | source = im |
| |
|
| | self._ensure_mutable() |
| |
|
| | if mask: |
| | mask.load() |
| | self.im.paste(source, box, mask.im) |
| | else: |
| | self.im.paste(source, box) |
| |
|
| | def alpha_composite( |
| | self, im: Image, dest: Sequence[int] = (0, 0), source: Sequence[int] = (0, 0) |
| | ) -> None: |
| | """'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 list or tuple" |
| | raise ValueError(msg) |
| | if not isinstance(dest, (list, tuple)): |
| | msg = "Destination must be a list or tuple" |
| | raise ValueError(msg) |
| |
|
| | if len(source) == 4: |
| | overlay_crop_box = tuple(source) |
| | elif len(source) == 2: |
| | overlay_crop_box = tuple(source) + im.size |
| | else: |
| | msg = "Source must be a sequence of length 2 or 4" |
| | raise ValueError(msg) |
| |
|
| | if not len(dest) == 2: |
| | msg = "Destination must be a sequence of length 2" |
| | raise ValueError(msg) |
| | if min(source) < 0: |
| | msg = "Source must be non-negative" |
| | raise ValueError(msg) |
| |
|
| | |
| | if overlay_crop_box == (0, 0) + im.size: |
| | overlay = im |
| | else: |
| | overlay = im.crop(overlay_crop_box) |
| |
|
| | |
| | box = tuple(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: ( |
| | Sequence[float] |
| | | NumpyArray |
| | | Callable[[int], float] |
| | | Callable[[ImagePointTransform], ImagePointTransform | float] |
| | | ImagePointHandler |
| | ), |
| | mode: str | None = None, |
| | ) -> Image: |
| | """ |
| | Maps this image through a lookup table or function. |
| | |
| | :param lut: A lookup table, containing 256 (or 65536 if |
| | self.mode=="I" and mode == "L") values per band in the |
| | image. A function can be used instead, it should take a |
| | single argument. The function is called once for each |
| | possible pixel value, and the resulting table is applied to |
| | all bands of the image. |
| | |
| | It may also be an :py:class:`~PIL.Image.ImagePointHandler` |
| | object:: |
| | |
| | class Example(Image.ImagePointHandler): |
| | def point(self, im: Image) -> Image: |
| | # Return result |
| | :param mode: Output mode (default is same as input). This can only be used if |
| | the source image has mode "L" or "P", and the output has mode "1" or the |
| | source image mode is "I" and the output mode is "L". |
| | :returns: An :py:class:`~PIL.Image.Image` object. |
| | """ |
| |
|
| | self.load() |
| |
|
| | if isinstance(lut, ImagePointHandler): |
| | return lut.point(self) |
| |
|
| | if callable(lut): |
| | |
| | if self.mode in ("I", "I;16", "F"): |
| | |
| | |
| | |
| | scale, offset = _getscaleoffset(lut) |
| | return self._new(self.im.point_transform(scale, offset)) |
| | |
| | flatLut = [lut(i) for i in range(256)] * self.im.bands |
| | else: |
| | flatLut = lut |
| |
|
| | if self.mode == "F": |
| | |
| | msg = "point operation not supported for this mode" |
| | raise ValueError(msg) |
| |
|
| | if mode != "F": |
| | flatLut = [round(i) for i in flatLut] |
| | return self._new(self.im.point(flatLut, mode)) |
| |
|
| | def putalpha(self, alpha: Image | int) -> None: |
| | """ |
| | 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. |
| | """ |
| |
|
| | 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._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 isinstance(alpha, Image): |
| | |
| | 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: Sequence[float] | Sequence[Sequence[int]] | core.ImagingCore | NumpyArray, |
| | scale: float = 1.0, |
| | offset: float = 0.0, |
| | ) -> None: |
| | """ |
| | 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. See :ref:`colors` for more |
| | information about values. |
| | :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: ImagePalette.ImagePalette | bytes | Sequence[int], |
| | rawmode: str = "RGB", |
| | ) -> None: |
| | """ |
| | Attaches a palette to this image. The image must be a "P", "PA", "L" |
| | or "LA" image. |
| | |
| | The palette sequence must contain at most 256 colors, made up of one |
| | integer value for each channel in the raw mode. |
| | For example, if the raw mode is "RGB", then it can contain at most 768 |
| | values, made up of red, green and blue values for the corresponding pixel |
| | index in the 256 colors. |
| | If the raw mode is "RGBA", then it can contain at most 1024 values, |
| | containing red, green, blue and alpha values. |
| | |
| | Alternatively, an 8-bit string may be used instead of an integer sequence. |
| | |
| | :param data: A palette sequence (either a list or a string). |
| | :param rawmode: The raw mode of the palette. Either "RGB", "RGBA", or a mode |
| | that can be transformed to "RGB" or "RGBA" (e.g. "R", "BGR;15", "RGBA;L"). |
| | """ |
| | from . import ImagePalette |
| |
|
| | if self.mode not in ("L", "LA", "P", "PA"): |
| | msg = "illegal image mode" |
| | raise ValueError(msg) |
| | if isinstance(data, ImagePalette.ImagePalette): |
| | if data.rawmode is not None: |
| | palette = ImagePalette.raw(data.rawmode, data.palette) |
| | else: |
| | palette = ImagePalette.ImagePalette(palette=data.palette) |
| | palette.dirty = 1 |
| | 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 = "RGBA" if "A" in rawmode else "RGB" |
| | self.load() |
| |
|
| | def putpixel( |
| | self, xy: tuple[int, int], value: float | tuple[int, ...] | list[int] |
| | ) -> None: |
| | """ |
| | 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. See :ref:`colors` for more information. |
| | |
| | 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. |
| | """ |
| |
|
| | self._ensure_mutable() |
| |
|
| | 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] |
| | assert self.palette is not None |
| | palette_index = self.palette.getcolor(tuple(value), self) |
| | value = (palette_index, alpha) if self.mode == "PA" else palette_index |
| | return self.im.putpixel(xy, value) |
| |
|
| | def remap_palette( |
| | self, dest_map: list[int], source_palette: bytes | bytearray | None = None |
| | ) -> Image: |
| | """ |
| | 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)) |
| | elif len(source_palette) > 768: |
| | bands = 4 |
| | palette_mode = "RGBA" |
| |
|
| | 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, 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: tuple[int, int], |
| | resample: Resampling, |
| | box: tuple[float, float, float, float], |
| | ) -> tuple[int, int, int, int]: |
| | """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: tuple[int, int] | list[int] | NumpyArray, |
| | resample: int | None = None, |
| | box: tuple[float, float, float, float] | None = None, |
| | reducing_gap: float | None = None, |
| | ) -> Image: |
| | """ |
| | Returns a resized copy of this image. |
| | |
| | :param size: The requested size in pixels, as a tuple or array: |
| | (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`. 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: |
| | resample = 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 += f" 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) |
| |
|
| | if box is None: |
| | box = (0, 0) + self.size |
| |
|
| | size = tuple(size) |
| | 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, cast(Resampling, resample), box) |
| | factor = (factor_x, factor_y) |
| | self = ( |
| | self.reduce(factor, box=reduce_box) |
| | if callable(self.reduce) |
| | else Image.reduce(self, factor, box=reduce_box) |
| | ) |
| | box = ( |
| | (box[0] - reduce_box[0]) / factor_x, |
| | (box[1] - reduce_box[1]) / factor_y, |
| | (box[2] - reduce_box[0]) / factor_x, |
| | (box[3] - reduce_box[1]) / factor_y, |
| | ) |
| |
|
| | return self._new(self.im.resize(size, resample, box)) |
| |
|
| | def reduce( |
| | self, |
| | factor: int | tuple[int, int], |
| | box: tuple[int, int, int, int] | None = None, |
| | ) -> Image: |
| | """ |
| | 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 |
| |
|
| | 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: float, |
| | resample: Resampling = Resampling.NEAREST, |
| | expand: int | bool = False, |
| | center: tuple[float, float] | None = None, |
| | translate: tuple[int, int] | None = None, |
| | fillcolor: float | tuple[float, ...] | str | None = None, |
| | ) -> Image: |
| | """ |
| | 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: |
| | center = (w / 2, h / 2) |
| |
|
| | 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: float, y: float, matrix: list[float]) -> tuple[float, float]: |
| | (a, b, c, d, e, f) = matrix |
| | return a * x + b * y + c, d * x + e * y + f |
| |
|
| | matrix[2], matrix[5] = transform( |
| | -center[0] - post_trans[0], -center[1] - post_trans[1], matrix |
| | ) |
| | matrix[2] += center[0] |
| | matrix[5] += center[1] |
| |
|
| | if expand: |
| | |
| | xx = [] |
| | yy = [] |
| | for x, y in ((0, 0), (w, 0), (w, h), (0, h)): |
| | transformed_x, transformed_y = transform(x, y, matrix) |
| | xx.append(transformed_x) |
| | yy.append(transformed_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: StrOrBytesPath | IO[bytes], format: str | None = None, **params: Any |
| | ) -> None: |
| | """ |
| | Saves this image under the given filename. If no format is |
| | specified, the format to use is determined from the filename |
| | extension, if possible. |
| | |
| | Keyword options can be used to provide additional instructions |
| | to the writer. If a writer doesn't recognise an option, it is |
| | silently ignored. The available options are described in the |
| | :doc:`image format documentation |
| | <../handbook/image-file-formats>` for each writer. |
| | |
| | You can use a file object instead of a filename. In this case, |
| | you must always specify the format. The file object must |
| | implement the ``seek``, ``tell``, and ``write`` |
| | methods, and be opened in binary mode. |
| | |
| | :param fp: A filename (string), os.PathLike object or file object. |
| | :param format: Optional format override. If omitted, the |
| | format to use is determined from the filename extension. |
| | If a file object was used instead of a filename, this |
| | parameter should always be used. |
| | :param params: Extra parameters to the image writer. These can also be |
| | set on the image itself through ``encoderinfo``. This is useful when |
| | saving multiple images:: |
| | |
| | # Saving XMP data to a single image |
| | from PIL import Image |
| | red = Image.new("RGB", (1, 1), "#f00") |
| | red.save("out.mpo", xmp=b"test") |
| | |
| | # Saving XMP data to the second frame of an image |
| | from PIL import Image |
| | black = Image.new("RGB", (1, 1)) |
| | red = Image.new("RGB", (1, 1), "#f00") |
| | red.encoderinfo = {"xmp": b"test"} |
| | black.save("out.mpo", save_all=True, append_images=[red]) |
| | :returns: None |
| | :exception ValueError: If the output format could not be determined |
| | from the file name. Use the format option to solve this. |
| | :exception OSError: If the file could not be written. The file |
| | may have been created, and may contain partial data. |
| | """ |
| |
|
| | filename: str | bytes = "" |
| | open_fp = False |
| | if is_path(fp): |
| | filename = os.fspath(fp) |
| | open_fp = True |
| | elif fp == sys.stdout: |
| | try: |
| | fp = sys.stdout.buffer |
| | except AttributeError: |
| | pass |
| | if not filename and hasattr(fp, "name") and is_path(fp.name): |
| | |
| | filename = os.fspath(fp.name) |
| |
|
| | preinit() |
| |
|
| | filename_ext = os.path.splitext(filename)[1].lower() |
| | ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext |
| |
|
| | if not format: |
| | if ext not in EXTENSION: |
| | init() |
| | try: |
| | format = EXTENSION[ext] |
| | except KeyError as e: |
| | msg = f"unknown file extension: {ext}" |
| | raise ValueError(msg) from e |
| |
|
| | from . import ImageFile |
| |
|
| | |
| | if isinstance(self, ImageFile.ImageFile) and os.path.abspath( |
| | filename |
| | ) == os.path.abspath(self.filename): |
| | self._ensure_mutable() |
| | else: |
| | self.load() |
| |
|
| | save_all = params.pop("save_all", None) |
| | self._default_encoderinfo = params |
| | encoderinfo = getattr(self, "encoderinfo", {}) |
| | self._attach_default_encoderinfo(self) |
| | self.encoderconfig: tuple[Any, ...] = () |
| |
|
| | if format.upper() not in SAVE: |
| | init() |
| | if save_all or ( |
| | save_all is None |
| | and params.get("append_images") |
| | and format.upper() in 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") |
| | else: |
| | fp = cast(IO[bytes], fp) |
| |
|
| | try: |
| | save_handler(self, fp, filename) |
| | except Exception: |
| | if open_fp: |
| | fp.close() |
| | if created: |
| | try: |
| | os.remove(filename) |
| | except PermissionError: |
| | pass |
| | raise |
| | finally: |
| | self.encoderinfo = encoderinfo |
| | if open_fp: |
| | fp.close() |
| |
|
| | def _attach_default_encoderinfo(self, im: Image) -> dict[str, Any]: |
| | encoderinfo = getattr(self, "encoderinfo", {}) |
| | self.encoderinfo = {**im._default_encoderinfo, **encoderinfo} |
| | return encoderinfo |
| |
|
| | def seek(self, frame: int) -> None: |
| | """ |
| | Seeks to the given frame in this sequence file. If you seek |
| | beyond the end of the sequence, the method raises an |
| | ``EOFError`` exception. When a sequence file is opened, the |
| | library automatically seeks to frame 0. |
| | |
| | See :py:meth:`~PIL.Image.Image.tell`. |
| | |
| | If defined, :attr:`~PIL.Image.Image.n_frames` refers to the |
| | number of available frames. |
| | |
| | :param frame: Frame number, starting at 0. |
| | :exception EOFError: If the call attempts to seek beyond the end |
| | of the sequence. |
| | """ |
| |
|
| | |
| | if frame != 0: |
| | msg = "no more images in file" |
| | raise EOFError(msg) |
| |
|
| | def show(self, title: str | None = None) -> None: |
| | """ |
| | Displays this image. This method is mainly intended for debugging purposes. |
| | |
| | This method calls :py:func:`PIL.ImageShow.show` internally. You can use |
| | :py:func:`PIL.ImageShow.register` to override its default behaviour. |
| | |
| | The image is first saved to a temporary file. By default, it will be in |
| | PNG format. |
| | |
| | On Unix, the image is then opened using the **xdg-open**, **display**, |
| | **gm**, **eog** or **xv** utility, depending on which one can be found. |
| | |
| | On macOS, the image is opened with the native Preview application. |
| | |
| | On Windows, the image is opened with the standard PNG display utility. |
| | |
| | :param title: Optional title to use for the image window, where possible. |
| | """ |
| |
|
| | from . import ImageShow |
| |
|
| | ImageShow.show(self, title) |
| |
|
| | def split(self) -> tuple[Image, ...]: |
| | """ |
| | Split this image into individual bands. This method returns a |
| | tuple of individual image bands from an image. For example, |
| | splitting an "RGB" image creates three new images each |
| | containing a copy of one of the original bands (red, green, |
| | blue). |
| | |
| | If you need only one band, :py:meth:`~PIL.Image.Image.getchannel` |
| | method can be more convenient and faster. |
| | |
| | :returns: A tuple containing bands. |
| | """ |
| |
|
| | self.load() |
| | if self.im.bands == 1: |
| | return (self.copy(),) |
| | return tuple(map(self._new, self.im.split())) |
| |
|
| | def getchannel(self, channel: int | str) -> Image: |
| | """ |
| | Returns an image containing a single channel of the source image. |
| | |
| | :param channel: What channel to return. Could be index |
| | (0 for "R" channel of "RGB") or channel name |
| | ("A" for alpha channel of "RGBA"). |
| | :returns: An image in "L" mode. |
| | |
| | .. versionadded:: 4.3.0 |
| | """ |
| | self.load() |
| |
|
| | if isinstance(channel, str): |
| | try: |
| | channel = self.getbands().index(channel) |
| | except ValueError as e: |
| | msg = f'The image has no channel "{channel}"' |
| | raise ValueError(msg) from e |
| |
|
| | return self._new(self.im.getband(channel)) |
| |
|
| | def tell(self) -> int: |
| | """ |
| | Returns the current frame number. See :py:meth:`~PIL.Image.Image.seek`. |
| | |
| | If defined, :attr:`~PIL.Image.Image.n_frames` refers to the |
| | number of available frames. |
| | |
| | :returns: Frame number, starting with 0. |
| | """ |
| | return 0 |
| |
|
| | def thumbnail( |
| | self, |
| | size: tuple[float, float], |
| | resample: Resampling = Resampling.BICUBIC, |
| | reducing_gap: float | None = 2.0, |
| | ) -> None: |
| | """ |
| | Make this image into a thumbnail. This method modifies the |
| | image to contain a thumbnail version of itself, no larger than |
| | the given size. This method calculates an appropriate thumbnail |
| | size to preserve the aspect of the image, calls the |
| | :py:meth:`~PIL.Image.Image.draft` method to configure the file reader |
| | (where applicable), and finally resizes the image. |
| | |
| | Note that this function modifies the :py:class:`~PIL.Image.Image` |
| | object in place. If you need to use the full resolution image as well, |
| | apply this method to a :py:meth:`~PIL.Image.Image.copy` of the original |
| | image. |
| | |
| | :param size: The requested size in pixels, as a 2-tuple: |
| | (width, height). |
| | :param resample: Optional resampling filter. This can be one |
| | of :py:data:`Resampling.NEAREST`, :py:data:`Resampling.BOX`, |
| | :py:data:`Resampling.BILINEAR`, :py:data:`Resampling.HAMMING`, |
| | :py:data:`Resampling.BICUBIC` or :py:data:`Resampling.LANCZOS`. |
| | If omitted, it defaults to :py:data:`Resampling.BICUBIC`. |
| | (was :py:data:`Resampling.NEAREST` prior to version 2.5.0). |
| | See: :ref:`concept-filters`. |
| | :param reducing_gap: Apply optimization by resizing the image |
| | in two steps. First, reducing the image by integer times |
| | using :py:meth:`~PIL.Image.Image.reduce` or |
| | :py:meth:`~PIL.Image.Image.draft` for JPEG images. |
| | Second, resizing using regular resampling. The last step |
| | changes size no less than by ``reducing_gap`` times. |
| | ``reducing_gap`` may be None (no first step is performed) |
| | or should be greater than 1.0. The bigger ``reducing_gap``, |
| | the closer the result to the fair resampling. |
| | The smaller ``reducing_gap``, the faster resizing. |
| | With ``reducing_gap`` greater or equal to 3.0, the result is |
| | indistinguishable from fair resampling in most cases. |
| | The default value is 2.0 (very close to fair resampling |
| | while still being faster in many cases). |
| | :returns: None |
| | """ |
| |
|
| | provided_size = tuple(map(math.floor, size)) |
| |
|
| | def preserve_aspect_ratio() -> tuple[int, int] | None: |
| | def round_aspect(number: float, key: Callable[[int], float]) -> int: |
| | return max(min(math.floor(number), math.ceil(number), key=key), 1) |
| |
|
| | x, y = provided_size |
| | if x >= self.width and y >= self.height: |
| | return None |
| |
|
| | aspect = self.width / self.height |
| | if x / y >= aspect: |
| | x = round_aspect(y * aspect, key=lambda n: abs(aspect - n / y)) |
| | else: |
| | y = round_aspect( |
| | x / aspect, key=lambda n: 0 if n == 0 else abs(aspect - x / n) |
| | ) |
| | return x, y |
| |
|
| | preserved_size = preserve_aspect_ratio() |
| | if preserved_size is None: |
| | return |
| | final_size = preserved_size |
| |
|
| | box = None |
| | if reducing_gap is not None: |
| | res = self.draft( |
| | None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap)) |
| | ) |
| | if res is not None: |
| | box = res[1] |
| |
|
| | if self.size != final_size: |
| | im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap) |
| |
|
| | self.im = im.im |
| | self._size = final_size |
| | self._mode = self.im.mode |
| |
|
| | self.readonly = 0 |
| |
|
| | |
| | |
| | def transform( |
| | self, |
| | size: tuple[int, int], |
| | method: Transform | ImageTransformHandler | SupportsGetData, |
| | data: Sequence[Any] | None = None, |
| | resample: int = Resampling.NEAREST, |
| | fill: int = 1, |
| | fillcolor: float | tuple[float, ...] | str | None = None, |
| | ) -> Image: |
| | """ |
| | Transforms this image. This method creates a new image with the |
| | given size, and the same mode as the original, and copies data |
| | to the new image using the given transform. |
| | |
| | :param size: The output size in pixels, as a 2-tuple: |
| | (width, height). |
| | :param method: The transformation method. This is one of |
| | :py:data:`Transform.EXTENT` (cut out a rectangular subregion), |
| | :py:data:`Transform.AFFINE` (affine transform), |
| | :py:data:`Transform.PERSPECTIVE` (perspective transform), |
| | :py:data:`Transform.QUAD` (map a quadrilateral to a rectangle), or |
| | :py:data:`Transform.MESH` (map a number of source quadrilaterals |
| | in one operation). |
| | |
| | It may also be an :py:class:`~PIL.Image.ImageTransformHandler` |
| | object:: |
| | |
| | class Example(Image.ImageTransformHandler): |
| | def transform(self, size, data, resample, fill=1): |
| | # Return result |
| | |
| | Implementations of :py:class:`~PIL.Image.ImageTransformHandler` |
| | for some of the :py:class:`Transform` methods are provided |
| | in :py:mod:`~PIL.ImageTransform`. |
| | |
| | It may also be an object with a ``method.getdata`` method |
| | that returns a tuple supplying new ``method`` and ``data`` values:: |
| | |
| | class Example: |
| | def getdata(self): |
| | method = Image.Transform.EXTENT |
| | data = (0, 0, 100, 100) |
| | return method, data |
| | :param data: Extra data to the transformation method. |
| | :param resample: Optional resampling filter. It can be one of |
| | :py:data:`Resampling.NEAREST` (use nearest neighbour), |
| | :py:data:`Resampling.BILINEAR` (linear interpolation in a 2x2 |
| | environment), or :py:data:`Resampling.BICUBIC` (cubic spline |
| | interpolation in a 4x4 environment). If omitted, or if the image |
| | has mode "1" or "P", it is set to :py:data:`Resampling.NEAREST`. |
| | See: :ref:`concept-filters`. |
| | :param fill: If ``method`` is an |
| | :py:class:`~PIL.Image.ImageTransformHandler` object, this is one of |
| | the arguments passed to it. Otherwise, it is unused. |
| | :param fillcolor: Optional fill color for the area outside the |
| | transform in the output image. |
| | :returns: An :py:class:`~PIL.Image.Image` object. |
| | """ |
| |
|
| | if self.mode in ("LA", "RGBA") and resample != Resampling.NEAREST: |
| | return ( |
| | self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) |
| | .transform(size, method, data, resample, fill, fillcolor) |
| | .convert(self.mode) |
| | ) |
| |
|
| | if isinstance(method, ImageTransformHandler): |
| | return method.transform(size, self, resample=resample, fill=fill) |
| |
|
| | if hasattr(method, "getdata"): |
| | |
| | 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: tuple[int, int, int, int], |
| | image: Image, |
| | method: Transform, |
| | data: Sequence[float], |
| | resample: int = Resampling.NEAREST, |
| | fill: bool = True, |
| | ) -> None: |
| | 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): |
| | unusable: dict[int, str] = { |
| | Resampling.BOX: "Image.Resampling.BOX", |
| | Resampling.HAMMING: "Image.Resampling.HAMMING", |
| | Resampling.LANCZOS: "Image.Resampling.LANCZOS", |
| | } |
| | msg = unusable[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 += f" 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.transform(box, image.im, method, data, resample, fill) |
| |
|
| | def transpose(self, method: Transpose) -> Image: |
| | """ |
| | Transpose image (flip or rotate in 90 degree steps) |
| | |
| | :param method: One of :py:data:`Transpose.FLIP_LEFT_RIGHT`, |
| | :py:data:`Transpose.FLIP_TOP_BOTTOM`, :py:data:`Transpose.ROTATE_90`, |
| | :py:data:`Transpose.ROTATE_180`, :py:data:`Transpose.ROTATE_270`, |
| | :py:data:`Transpose.TRANSPOSE` or :py:data:`Transpose.TRANSVERSE`. |
| | :returns: Returns a flipped or rotated copy of this image. |
| | """ |
| |
|
| | self.load() |
| | return self._new(self.im.transpose(method)) |
| |
|
| | def effect_spread(self, distance: int) -> Image: |
| | """ |
| | 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) -> ImageQt.ImageQt: |
| | """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) -> ImageQt.QPixmap: |
| | """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(abc.ABC): |
| | """ |
| | Used as a mixin by point transforms |
| | (for use with :py:meth:`~PIL.Image.Image.point`) |
| | """ |
| |
|
| | @abc.abstractmethod |
| | def point(self, im: Image) -> Image: |
| | pass |
| |
|
| |
|
| | class ImageTransformHandler(abc.ABC): |
| | """ |
| | Used as a mixin by geometry transforms |
| | (for use with :py:meth:`~PIL.Image.Image.transform`) |
| | """ |
| |
|
| | @abc.abstractmethod |
| | def transform( |
| | self, |
| | size: tuple[int, int], |
| | image: Image, |
| | **options: Any, |
| | ) -> Image: |
| | pass |
| |
|
| |
|
| | |
| | |
| |
|
| |
|
| | def _check_size(size: Any) -> None: |
| | """ |
| | Common check to enforce type and sanity check on size tuples |
| | |
| | :param size: Should be a 2 tuple of (width, height) |
| | :returns: None, or raises a ValueError |
| | """ |
| |
|
| | if not isinstance(size, (list, tuple)): |
| | msg = "Size must be a list or tuple" |
| | raise ValueError(msg) |
| | if len(size) != 2: |
| | msg = "Size must be a sequence of length 2" |
| | raise ValueError(msg) |
| | if size[0] < 0 or size[1] < 0: |
| | msg = "Width and height must be >= 0" |
| | raise ValueError(msg) |
| |
|
| |
|
| | def new( |
| | mode: str, |
| | size: tuple[int, int] | list[int], |
| | color: float | tuple[float, ...] | str | None = 0, |
| | ) -> Image: |
| | """ |
| | Creates a new image with the given mode and size. |
| | |
| | :param mode: The mode to use for the new image. See: |
| | :ref:`concept-modes`. |
| | :param size: A 2-tuple, containing (width, height) in pixels. |
| | :param color: What color to use for the image. Default is black. If given, |
| | this should be a single integer or floating point value for single-band |
| | modes, and a tuple for multi-band modes (one value per band). When |
| | creating RGB or HSV images, you can also use color strings as supported |
| | by the ImageColor module. See :ref:`colors` for more information. 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 all(isinstance(i, int) for i in color) |
| | ): |
| | color_ints: tuple[int, ...] = cast(tuple[int, ...], tuple(color)) |
| | if len(color_ints) == 3 or len(color_ints) == 4: |
| | |
| | from . import ImagePalette |
| |
|
| | im.palette = ImagePalette.ImagePalette() |
| | color = im.palette.getcolor(color_ints) |
| | return im._new(core.fill(mode, size, color)) |
| |
|
| |
|
| | def frombytes( |
| | mode: str, |
| | size: tuple[int, int], |
| | data: bytes | bytearray | SupportsArrayInterface, |
| | decoder_name: str = "raw", |
| | *args: Any, |
| | ) -> 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: |
| | decoder_args: Any = args |
| | if len(decoder_args) == 1 and isinstance(decoder_args[0], tuple): |
| | |
| | decoder_args = decoder_args[0] |
| |
|
| | if decoder_name == "raw" and decoder_args == (): |
| | decoder_args = mode |
| |
|
| | im.frombytes(data, decoder_name, decoder_args) |
| | return im |
| |
|
| |
|
| | def frombuffer( |
| | mode: str, |
| | size: tuple[int, int], |
| | data: bytes | SupportsArrayInterface, |
| | decoder_name: str = "raw", |
| | *args: Any, |
| | ) -> Image: |
| | """ |
| | Creates an image memory referencing pixel data in a byte buffer. |
| | |
| | This function is similar to :py:func:`~PIL.Image.frombytes`, but uses data |
| | in the byte buffer, where possible. This means that changes to the |
| | original buffer object are reflected in this image). Not all modes can |
| | share memory; supported modes include "L", "RGBX", "RGBA", and "CMYK". |
| | |
| | Note that this function decodes pixel data only, not entire images. |
| | If you have an entire image file in a string, wrap it in a |
| | :py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load it. |
| | |
| | The default parameters used for the "raw" decoder differs from that used for |
| | :py:func:`~PIL.Image.frombytes`. This is a bug, and will probably be fixed in a |
| | future release. The current release issues a warning if you do this; to disable |
| | the warning, you should provide the full set of parameters. See below for details. |
| | |
| | :param mode: The image mode. See: :ref:`concept-modes`. |
| | :param size: The image size. |
| | :param data: A bytes or other buffer object containing raw |
| | data for the given mode. |
| | :param decoder_name: What decoder to use. |
| | :param args: Additional parameters for the given decoder. For the |
| | default encoder ("raw"), it's recommended that you provide the |
| | full set of parameters:: |
| | |
| | frombuffer(mode, size, data, "raw", mode, 0, 1) |
| | |
| | :returns: An :py:class:`~PIL.Image.Image` object. |
| | |
| | .. versionadded:: 1.1.4 |
| | """ |
| |
|
| | _check_size(size) |
| |
|
| | |
| | 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) |
| |
|
| |
|
| | class SupportsArrayInterface(Protocol): |
| | """ |
| | An object that has an ``__array_interface__`` dictionary. |
| | """ |
| |
|
| | @property |
| | def __array_interface__(self) -> dict[str, Any]: |
| | raise NotImplementedError() |
| |
|
| |
|
| | class SupportsArrowArrayInterface(Protocol): |
| | """ |
| | An object that has an ``__arrow_c_array__`` method corresponding to the arrow c |
| | data interface. |
| | """ |
| |
|
| | def __arrow_c_array__( |
| | self, requested_schema: "PyCapsule" = None |
| | ) -> tuple["PyCapsule", "PyCapsule"]: |
| | raise NotImplementedError() |
| |
|
| |
|
| | def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: |
| | """ |
| | 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``. Since pixel values do not |
| | contain information about palettes or color spaces, this can be used to place |
| | grayscale L mode data within a P mode image, or read RGB data as YCbCr for |
| | example. |
| | |
| | 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) |
| | try: |
| | typekey = (1, 1) + shape[2:], arr["typestr"] |
| | except KeyError as e: |
| | if mode is not None: |
| | typekey = None |
| | color_modes: list[str] = [] |
| | else: |
| | msg = "Cannot handle this data type" |
| | raise TypeError(msg) from e |
| | if typekey is not None: |
| | try: |
| | typemode, rawmode, color_modes = _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 |
| | if mode is not None: |
| | if mode != typemode and mode not in color_modes: |
| | deprecate("'mode' parameter for changing data types", 13) |
| | rawmode = mode |
| | else: |
| | mode = typemode |
| | 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() |
| | elif hasattr(obj, "tostring"): |
| | obj = obj.tostring() |
| | else: |
| | msg = "'strides' requires either tobytes() or tostring()" |
| | raise ValueError(msg) |
| |
|
| | return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) |
| |
|
| |
|
| | def fromarrow( |
| | obj: SupportsArrowArrayInterface, mode: str, size: tuple[int, int] |
| | ) -> Image: |
| | """Creates an image with zero-copy shared memory from an object exporting |
| | the arrow_c_array interface protocol:: |
| | |
| | from PIL import Image |
| | import pyarrow as pa |
| | arr = pa.array([0]*(5*5*4), type=pa.uint8()) |
| | im = Image.fromarrow(arr, 'RGBA', (5, 5)) |
| | |
| | If the data representation of the ``obj`` is not compatible with |
| | Pillow internal storage, a ValueError is raised. |
| | |
| | Pillow images can also be converted to Arrow objects:: |
| | |
| | from PIL import Image |
| | import pyarrow as pa |
| | im = Image.open('hopper.jpg') |
| | arr = pa.array(im) |
| | |
| | As with array support, when converting Pillow images to arrays, |
| | only pixel values are transferred. This means that P and PA mode |
| | images will lose their palette. |
| | |
| | :param obj: Object with an arrow_c_array interface |
| | :param mode: Image mode. |
| | :param size: Image size. This must match the storage of the arrow object. |
| | :returns: An Image object |
| | |
| | Note that according to the Arrow spec, both the producer and the |
| | consumer should consider the exported array to be immutable, as |
| | unsynchronized updates will potentially cause inconsistent data. |
| | |
| | See: :ref:`arrow-support` for more detailed information |
| | |
| | .. versionadded:: 11.2.1 |
| | |
| | """ |
| | if not hasattr(obj, "__arrow_c_array__"): |
| | msg = "arrow_c_array interface not found" |
| | raise ValueError(msg) |
| |
|
| | (schema_capsule, array_capsule) = obj.__arrow_c_array__() |
| | _im = core.new_arrow(mode, size, schema_capsule, array_capsule) |
| | if _im: |
| | return Image()._new(_im) |
| |
|
| | msg = "new_arrow returned None without an exception" |
| | raise ValueError(msg) |
| |
|
| |
|
| | def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile: |
| | """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: ImageQt.QPixmap) -> ImageFile.ImageFile: |
| | """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", ["P"]), |
| | ((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", ["La", "PA"]), |
| | ((1, 1, 3), "|u1"): ("RGB", "RGB", ["YCbCr", "LAB", "HSV"]), |
| | ((1, 1, 4), "|u1"): ("RGBA", "RGBA", ["RGBa", "RGBX", "CMYK"]), |
| | |
| | ((1, 1), f"{_ENDIAN}i4"): ("I", "I", []), |
| | ((1, 1), f"{_ENDIAN}f4"): ("F", "F", []), |
| | } |
| |
|
| |
|
| | def _decompression_bomb_check(size: tuple[int, int]) -> None: |
| | if MAX_IMAGE_PIXELS is None: |
| | return |
| |
|
| | pixels = max(1, size[0]) * max(1, size[1]) |
| |
|
| | if pixels > 2 * MAX_IMAGE_PIXELS: |
| | msg = ( |
| | f"Image size ({pixels} pixels) exceeds limit of {2 * MAX_IMAGE_PIXELS} " |
| | "pixels, could be decompression bomb DOS attack." |
| | ) |
| | raise DecompressionBombError(msg) |
| |
|
| | if pixels > MAX_IMAGE_PIXELS: |
| | warnings.warn( |
| | f"Image size ({pixels} pixels) exceeds limit of {MAX_IMAGE_PIXELS} pixels, " |
| | "could be decompression bomb DOS attack.", |
| | DecompressionBombWarning, |
| | ) |
| |
|
| |
|
| | def open( |
| | fp: StrOrBytesPath | IO[bytes], |
| | mode: Literal["r"] = "r", |
| | formats: list[str] | tuple[str, ...] | None = None, |
| | ) -> ImageFile.ImageFile: |
| | """ |
| | Opens and identifies the given image file. |
| | |
| | This is a lazy operation; this function identifies the file, but |
| | the file remains open and the actual image data is not read from |
| | the file until you try to process the data (or call the |
| | :py:meth:`~PIL.Image.Image.load` method). See |
| | :py:func:`~PIL.Image.new`. See :ref:`file-handling`. |
| | |
| | :param fp: A filename (string), os.PathLike object or a file object. |
| | The file object must implement ``file.read``, |
| | ``file.seek``, and ``file.tell`` methods, |
| | and be opened in binary mode. The file object will also seek to zero |
| | before reading. |
| | :param mode: The mode. If given, this argument must be "r". |
| | :param formats: A list or tuple of formats to attempt to load the file in. |
| | This can be used to restrict the set of formats checked. |
| | Pass ``None`` to try all supported formats. You can print the set of |
| | available formats by running ``python3 -m PIL`` or using |
| | the :py:func:`PIL.features.pilinfo` function. |
| | :returns: An :py:class:`~PIL.Image.Image` object. |
| | :exception FileNotFoundError: If the file cannot be found. |
| | :exception PIL.UnidentifiedImageError: If the image cannot be opened and |
| | identified. |
| | :exception ValueError: If the ``mode`` is not "r", or if a ``StringIO`` |
| | instance is used for ``fp``. |
| | :exception TypeError: If ``formats`` is not ``None``, a list or a tuple. |
| | """ |
| |
|
| | if mode != "r": |
| | msg = f"bad mode {repr(mode)}" |
| | raise ValueError(msg) |
| | elif isinstance(fp, io.StringIO): |
| | msg = ( |
| | "StringIO cannot be used to open an image. " |
| | "Binary data must be used instead." |
| | ) |
| | raise ValueError(msg) |
| |
|
| | if formats is None: |
| | formats = ID |
| | elif not isinstance(formats, (list, tuple)): |
| | msg = "formats must be a list or tuple" |
| | raise TypeError(msg) |
| |
|
| | exclusive_fp = False |
| | filename: str | bytes = "" |
| | if is_path(fp): |
| | filename = os.fspath(fp) |
| | fp = builtins.open(filename, "rb") |
| | exclusive_fp = True |
| | else: |
| | fp = cast(IO[bytes], fp) |
| |
|
| | try: |
| | fp.seek(0) |
| | except (AttributeError, io.UnsupportedOperation): |
| | fp = io.BytesIO(fp.read()) |
| | exclusive_fp = True |
| |
|
| | prefix = fp.read(16) |
| |
|
| | preinit() |
| |
|
| | warning_messages: list[str] = [] |
| |
|
| | def _open_core( |
| | fp: IO[bytes], |
| | filename: str | bytes, |
| | prefix: bytes, |
| | formats: list[str] | tuple[str, ...], |
| | ) -> ImageFile.ImageFile | None: |
| | 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 isinstance(result, str): |
| | warning_messages.append(result) |
| | elif result: |
| | fp.seek(0) |
| | im = factory(fp, filename) |
| | _decompression_bomb_check(im.size) |
| | return im |
| | except (SyntaxError, IndexError, TypeError, struct.error) as e: |
| | if WARN_POSSIBLE_FORMATS: |
| | warning_messages.append(i + " opening failed. " + str(e)) |
| | 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 = ID.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 warning_messages: |
| | warnings.warn(message) |
| | msg = "cannot identify image file %r" % (filename if filename else fp) |
| | raise UnidentifiedImageError(msg) |
| |
|
| |
|
| | |
| | |
| |
|
| |
|
| | def alpha_composite(im1: Image, im2: Image) -> Image: |
| | """ |
| | Alpha composite im2 over im1. |
| | |
| | :param im1: The first image. Must have mode RGBA or LA. |
| | :param im2: The second image. Must have the same mode and size as the first image. |
| | :returns: An :py:class:`~PIL.Image.Image` object. |
| | """ |
| |
|
| | im1.load() |
| | im2.load() |
| | return im1._new(core.alpha_composite(im1.im, im2.im)) |
| |
|
| |
|
| | def blend(im1: Image, im2: Image, alpha: float) -> Image: |
| | """ |
| | Creates a new image by interpolating between two input images, using |
| | a constant alpha:: |
| | |
| | out = image1 * (1.0 - alpha) + image2 * alpha |
| | |
| | :param im1: The first image. |
| | :param im2: The second image. Must have the same mode and size as |
| | the first image. |
| | :param alpha: The interpolation alpha factor. If alpha is 0.0, a |
| | copy of the first image is returned. If alpha is 1.0, a copy of |
| | the second image is returned. There are no restrictions on the |
| | alpha value. If necessary, the result is clipped to fit into |
| | the allowed output range. |
| | :returns: An :py:class:`~PIL.Image.Image` object. |
| | """ |
| |
|
| | im1.load() |
| | im2.load() |
| | return im1._new(core.blend(im1.im, im2.im, alpha)) |
| |
|
| |
|
| | def composite(image1: Image, image2: Image, mask: Image) -> Image: |
| | """ |
| | Create composite image by blending images using a transparency mask. |
| | |
| | :param image1: The first image. |
| | :param image2: The second image. Must have the same mode and |
| | size as the first image. |
| | :param mask: A mask image. This image can have mode |
| | "1", "L", or "RGBA", and must have the same size as the |
| | other two images. |
| | """ |
| |
|
| | image = image2.copy() |
| | image.paste(image1, None, mask) |
| | return image |
| |
|
| |
|
| | def eval(image: Image, *args: Callable[[int], float]) -> Image: |
| | """ |
| | 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: str, bands: Sequence[Image]) -> Image: |
| | """ |
| | 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: str, |
| | factory: ( |
| | Callable[[IO[bytes], str | bytes], ImageFile.ImageFile] |
| | | type[ImageFile.ImageFile] |
| | ), |
| | accept: Callable[[bytes], bool | str] | None = None, |
| | ) -> None: |
| | """ |
| | Register an image file plugin. This function should not be used |
| | in application code. |
| | |
| | :param id: An image format identifier. |
| | :param factory: An image file factory method. |
| | :param accept: An optional function that can be used to quickly |
| | reject images having another format. |
| | """ |
| | id = id.upper() |
| | if id not in ID: |
| | ID.append(id) |
| | OPEN[id] = factory, accept |
| |
|
| |
|
| | def register_mime(id: str, mimetype: str) -> None: |
| | """ |
| | Registers an image MIME type by populating ``Image.MIME``. This function |
| | should not be used in application code. |
| | |
| | ``Image.MIME`` provides a mapping from image format identifiers to mime |
| | formats, but :py:meth:`~PIL.ImageFile.ImageFile.get_format_mimetype` can |
| | provide a different result for specific images. |
| | |
| | :param id: An image format identifier. |
| | :param mimetype: The image MIME type for this format. |
| | """ |
| | MIME[id.upper()] = mimetype |
| |
|
| |
|
| | def register_save( |
| | id: str, driver: Callable[[Image, IO[bytes], str | bytes], None] |
| | ) -> None: |
| | """ |
| | Registers an image save function. This function should not be |
| | used in application code. |
| | |
| | :param id: An image format identifier. |
| | :param driver: A function to save images in this format. |
| | """ |
| | SAVE[id.upper()] = driver |
| |
|
| |
|
| | def register_save_all( |
| | id: str, driver: Callable[[Image, IO[bytes], str | bytes], None] |
| | ) -> None: |
| | """ |
| | Registers an image function to save all the frames |
| | of a multiframe format. This function should not be |
| | used in application code. |
| | |
| | :param id: An image format identifier. |
| | :param driver: A function to save images in this format. |
| | """ |
| | SAVE_ALL[id.upper()] = driver |
| |
|
| |
|
| | def register_extension(id: str, extension: str) -> 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: str, extensions: list[str]) -> None: |
| | """ |
| | Registers image extensions. This function should not be |
| | used in application code. |
| | |
| | :param id: An image format identifier. |
| | :param extensions: A list of extensions used for this format. |
| | """ |
| | for extension in extensions: |
| | register_extension(id, extension) |
| |
|
| |
|
| | def registered_extensions() -> dict[str, str]: |
| | """ |
| | Returns a dictionary containing all file extensions belonging |
| | to registered plugins |
| | """ |
| | init() |
| | return EXTENSION |
| |
|
| |
|
| | def register_decoder(name: str, decoder: type[ImageFile.PyDecoder]) -> None: |
| | """ |
| | Registers an image decoder. This function should not be |
| | used in application code. |
| | |
| | :param name: The name of the decoder |
| | :param decoder: An ImageFile.PyDecoder object |
| | |
| | .. versionadded:: 4.1.0 |
| | """ |
| | DECODERS[name] = decoder |
| |
|
| |
|
| | def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None: |
| | """ |
| | Registers an image encoder. This function should not be |
| | used in application code. |
| | |
| | :param name: The name of the encoder |
| | :param encoder: An ImageFile.PyEncoder object |
| | |
| | .. versionadded:: 4.1.0 |
| | """ |
| | ENCODERS[name] = encoder |
| |
|
| |
|
| | |
| | |
| |
|
| |
|
| | def _show(image: Image, **options: Any) -> None: |
| | from . import ImageShow |
| |
|
| | deprecate("Image._show", 13, "ImageShow.show") |
| | ImageShow.show(image, **options) |
| |
|
| |
|
| | |
| | |
| |
|
| |
|
| | def effect_mandelbrot( |
| | size: tuple[int, int], extent: tuple[float, float, float, float], quality: int |
| | ) -> Image: |
| | """ |
| | 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: tuple[int, int], sigma: float) -> Image: |
| | """ |
| | 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: str) -> Image: |
| | """ |
| | 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: str) -> Image: |
| | """ |
| | 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: dict[str, str] | None = None) -> None: |
| | env_dict = env if env is not None else 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_dict: |
| | continue |
| |
|
| | var = env_dict[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 = int(var) * units |
| | except ValueError: |
| | warnings.warn(f"{var_name} is not int") |
| | continue |
| |
|
| | try: |
| | setter(var_int) |
| | except ValueError as e: |
| | warnings.warn(f"{var_name}: {e}") |
| |
|
| |
|
| | _apply_env_variables() |
| | atexit.register(core.clear_cache) |
| |
|
| |
|
| | if TYPE_CHECKING: |
| | _ExifBase = MutableMapping[int, Any] |
| | else: |
| | _ExifBase = MutableMapping |
| |
|
| |
|
| | class Exif(_ExifBase): |
| | """ |
| | This class provides read and write access to EXIF image data:: |
| | |
| | from PIL import Image |
| | im = Image.open("exif.png") |
| | exif = im.getexif() # Returns an instance of this class |
| | |
| | Information can be read and written, iterated over or deleted:: |
| | |
| | print(exif[274]) # 1 |
| | exif[274] = 2 |
| | for k, v in exif.items(): |
| | print("Tag", k, "Value", v) # Tag 274 Value 2 |
| | del exif[274] |
| | |
| | To access information beyond IFD0, :py:meth:`~PIL.Image.Exif.get_ifd` |
| | returns a dictionary:: |
| | |
| | from PIL import ExifTags |
| | im = Image.open("exif_gps.jpg") |
| | exif = im.getexif() |
| | gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) |
| | print(gps_ifd) |
| | |
| | Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.MakerNote``, |
| | ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. |
| | |
| | :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: |
| | |
| | print(exif[ExifTags.Base.Software]) # PIL |
| | print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 |
| | """ |
| |
|
| | endian: str | None = None |
| | bigtiff = False |
| | _loaded = False |
| |
|
| | def __init__(self) -> None: |
| | self._data: dict[int, Any] = {} |
| | self._hidden_data: dict[int, Any] = {} |
| | self._ifds: dict[int, dict[int, Any]] = {} |
| | self._info: TiffImagePlugin.ImageFileDirectory_v2 | None = None |
| | self._loaded_exif: bytes | None = None |
| |
|
| | def _fixup(self, value: Any) -> Any: |
| | try: |
| | if len(value) == 1 and isinstance(value, tuple): |
| | return value[0] |
| | except Exception: |
| | pass |
| | return value |
| |
|
| | def _fixup_dict(self, src_dict: dict[int, Any]) -> dict[int, Any]: |
| | |
| | |
| | return {k: self._fixup(v) for k, v in src_dict.items()} |
| |
|
| | def _get_ifd_dict( |
| | self, offset: int, group: int | None = None |
| | ) -> dict[int, Any] | None: |
| | try: |
| | |
| | |
| | self.fp.seek(offset) |
| | except (KeyError, TypeError): |
| | return None |
| | else: |
| | from . import TiffImagePlugin |
| |
|
| | info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group) |
| | info.load(self.fp) |
| | return self._fixup_dict(dict(info)) |
| |
|
| | def _get_head(self) -> bytes: |
| | 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: bytes) -> None: |
| | |
| | |
| | |
| |
|
| | |
| | |
| | if data == self._loaded_exif: |
| | return |
| | self._loaded_exif = data |
| | self._data.clear() |
| | self._hidden_data.clear() |
| | self._ifds.clear() |
| | while data and data.startswith(b"Exif\x00\x00"): |
| | data = data[6:] |
| | if not data: |
| | self._info = None |
| | return |
| |
|
| | self.fp: IO[bytes] = 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: IO[bytes], offset: int | None = None) -> 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) -> dict[int, Any]: |
| | merged_dict = dict(self) |
| |
|
| | |
| | if ExifTags.IFD.Exif in self: |
| | ifd = self._get_ifd_dict(self[ExifTags.IFD.Exif], 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], ExifTags.IFD.GPSInfo |
| | ) |
| |
|
| | return merged_dict |
| |
|
| | def tobytes(self, offset: int = 8) -> bytes: |
| | from . import TiffImagePlugin |
| |
|
| | head = self._get_head() |
| | ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) |
| | for tag, ifd_dict in self._ifds.items(): |
| | if tag not in self: |
| | ifd[tag] = ifd_dict |
| | 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: int) -> dict[int, Any]: |
| | if tag not in self._ifds: |
| | if tag == ExifTags.IFD.IFD1: |
| | if self._info is not None and self._info.next != 0: |
| | ifd = self._get_ifd_dict(self._info.next) |
| | if ifd is not None: |
| | self._ifds[tag] = ifd |
| | elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: |
| | offset = self._hidden_data.get(tag, self.get(tag)) |
| | if offset is not None: |
| | ifd = self._get_ifd_dict(offset, tag) |
| | if ifd is not None: |
| | self._ifds[tag] = ifd |
| | 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.startswith(b"FUJIFILM"): |
| | ifd_offset = i32le(tag_data, 8) |
| | ifd_data = tag_data[ifd_offset:] |
| |
|
| | makernote = {} |
| | for i in range(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(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: dict[str, int | bytes] = { |
| | "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 |
| | )[0] |
| |
|
| | self.fp.read(4) |
| | camerainfo["Category"] = self.fp.read(2) |
| |
|
| | makernote = {0x1101: camerainfo} |
| | self._ifds[tag] = makernote |
| | else: |
| | |
| | ifd = self._get_ifd_dict(tag_data, tag) |
| | if ifd is not None: |
| | self._ifds[tag] = ifd |
| | ifd = self._ifds.setdefault(tag, {}) |
| | if tag == ExifTags.IFD.Exif and self._hidden_data: |
| | ifd = { |
| | k: v |
| | for (k, v) in ifd.items() |
| | if k not in (ExifTags.IFD.Interop, ExifTags.IFD.MakerNote) |
| | } |
| | return ifd |
| |
|
| | def hide_offsets(self) -> None: |
| | for tag in (ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo): |
| | if tag in self: |
| | self._hidden_data[tag] = self[tag] |
| | del self[tag] |
| |
|
| | def __str__(self) -> str: |
| | if self._info is not None: |
| | |
| | for tag in self._info: |
| | self[tag] |
| |
|
| | return str(self._data) |
| |
|
| | def __len__(self) -> int: |
| | keys = set(self._data) |
| | if self._info is not None: |
| | keys.update(self._info) |
| | return len(keys) |
| |
|
| | def __getitem__(self, tag: int) -> Any: |
| | 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: object) -> bool: |
| | return tag in self._data or (self._info is not None and tag in self._info) |
| |
|
| | def __setitem__(self, tag: int, value: Any) -> None: |
| | if self._info is not None and tag in self._info: |
| | del self._info[tag] |
| | self._data[tag] = value |
| |
|
| | def __delitem__(self, tag: int) -> None: |
| | if self._info is not None and tag in self._info: |
| | del self._info[tag] |
| | else: |
| | del self._data[tag] |
| | if tag in self._ifds: |
| | del self._ifds[tag] |
| |
|
| | def __iter__(self) -> Iterator[int]: |
| | keys = set(self._data) |
| | if self._info is not None: |
| | keys.update(self._info) |
| | return iter(keys) |
| |
|