diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/pillow_heif')
11 files changed, 1816 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pillow_heif/AvifImagePlugin.py b/.venv/lib/python3.12/site-packages/pillow_heif/AvifImagePlugin.py new file mode 100644 index 00000000..70460329 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pillow_heif/AvifImagePlugin.py @@ -0,0 +1,5 @@ +"""Import this file to auto register an AVIF plugin for Pillow.""" + +from .as_plugin import register_avif_opener + +register_avif_opener() diff --git a/.venv/lib/python3.12/site-packages/pillow_heif/HeifImagePlugin.py b/.venv/lib/python3.12/site-packages/pillow_heif/HeifImagePlugin.py new file mode 100644 index 00000000..c9382728 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pillow_heif/HeifImagePlugin.py @@ -0,0 +1,5 @@ +"""Import this file to auto register a HEIF plugin for Pillow.""" + +from .as_plugin import register_heif_opener + +register_heif_opener() diff --git a/.venv/lib/python3.12/site-packages/pillow_heif/__init__.py b/.venv/lib/python3.12/site-packages/pillow_heif/__init__.py new file mode 100644 index 00000000..41f814af --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pillow_heif/__init__.py @@ -0,0 +1,30 @@ +"""Provide all possible stuff that can be used.""" + +from . import options +from ._lib_info import libheif_info, libheif_version +from ._version import __version__ +from .as_plugin import ( + AvifImageFile, + HeifImageFile, + register_avif_opener, + register_heif_opener, +) +from .constants import ( + HeifColorPrimaries, + HeifDepthRepresentationType, + HeifMatrixCoefficients, + HeifTransferCharacteristics, +) +from .heif import ( + HeifAuxImage, + HeifDepthImage, + HeifFile, + HeifImage, + encode, + from_bytes, + from_pillow, + is_supported, + open_heif, + read_heif, +) +from .misc import get_file_mimetype, load_libheif_plugin, set_orientation diff --git a/.venv/lib/python3.12/site-packages/pillow_heif/_deffered_error.py b/.venv/lib/python3.12/site-packages/pillow_heif/_deffered_error.py new file mode 100644 index 00000000..0e0399ac --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pillow_heif/_deffered_error.py @@ -0,0 +1,11 @@ +"""DeferredError class taken from PIL._util.py file.""" + + +class DeferredError: # pylint: disable=too-few-public-methods + """Allows failing import for doc purposes, as C module will be not build during docs build.""" + + def __init__(self, ex): + self.ex = ex + + def __getattr__(self, elt): + raise self.ex diff --git a/.venv/lib/python3.12/site-packages/pillow_heif/_lib_info.py b/.venv/lib/python3.12/site-packages/pillow_heif/_lib_info.py new file mode 100644 index 00000000..a2d0bc03 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pillow_heif/_lib_info.py @@ -0,0 +1,36 @@ +"""Functions to get versions of underlying libraries.""" + +try: + import _pillow_heif +except ImportError as ex: + from ._deffered_error import DeferredError + + _pillow_heif = DeferredError(ex) + + +def libheif_version() -> str: + """Returns ``libheif`` version.""" + return _pillow_heif.get_lib_info()["libheif"] + + +def libheif_info() -> dict: + """Returns a dictionary with version information. + + The keys `libheif`, `HEIF`, `AVIF`, `encoders`, `decoders` are always present, but values for all except + `libheif` can be empty. + + { + 'libheif': '1.15.2', + 'HEIF': 'x265 HEVC encoder (3.4+31-6722fce1f)', + 'AVIF': 'AOMedia Project AV1 Encoder 3.5.0', + 'encoders': { + 'encoder1_id': 'encoder1_full_name', + 'encoder2_id': 'encoder2_full_name', + }, + 'decoders': { + 'decoder1_id': 'decoder1_full_name', + 'decoder2_id': 'decoder2_full_name', + }, + } + """ + return _pillow_heif.get_lib_info() diff --git a/.venv/lib/python3.12/site-packages/pillow_heif/_version.py b/.venv/lib/python3.12/site-packages/pillow_heif/_version.py new file mode 100644 index 00000000..04be03c5 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pillow_heif/_version.py @@ -0,0 +1,3 @@ +"""Version of pillow_heif/pi_heif.""" + +__version__ = "0.21.0" diff --git a/.venv/lib/python3.12/site-packages/pillow_heif/as_plugin.py b/.venv/lib/python3.12/site-packages/pillow_heif/as_plugin.py new file mode 100644 index 00000000..324d6430 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pillow_heif/as_plugin.py @@ -0,0 +1,283 @@ +"""Plugins for the Pillow library.""" + +from __future__ import annotations + +from itertools import chain +from typing import IO +from warnings import warn + +from PIL import Image, ImageFile, ImageSequence +from PIL import __version__ as pil_version + +from . import options +from .constants import HeifCompressionFormat +from .heif import HeifFile +from .misc import ( + CtxEncode, + _exif_from_pillow, + _get_bytes, + _get_orientation_for_encoder, + _get_primary_index, + _pil_to_supported_mode, + _xmp_from_pillow, + set_orientation, +) + +try: + import _pillow_heif +except ImportError as ex: + from ._deffered_error import DeferredError + + _pillow_heif = DeferredError(ex) + + +class _LibHeifImageFile(ImageFile.ImageFile): + """Base class with all functionality for ``HeifImageFile`` and ``AvifImageFile`` classes.""" + + _heif_file: HeifFile | None = None + _close_exclusive_fp_after_loading = True + _mode: str # only for Pillow 10.1+ + + def __init__(self, *args, **kwargs): + self.__frame = 0 + super().__init__(*args, **kwargs) + + def _open(self): + try: + # when Pillow starts supporting 16-bit multichannel images change `convert_hdr_to_8bit` to False + _heif_file = HeifFile(self.fp, convert_hdr_to_8bit=True, hdr_to_16bit=True, remove_stride=False) + except (OSError, ValueError, SyntaxError, RuntimeError, EOFError) as exception: + raise SyntaxError(str(exception)) from None + self.custom_mimetype = _heif_file.mimetype + self._heif_file = _heif_file + self.__frame = _heif_file.primary_index + self._init_from_heif_file(self.__frame) + self.tile = [] + + def load(self): + if self._heif_file: + frame_heif = self._heif_file[self.tell()] + try: + data = frame_heif.data # Size of Image can change during decoding + self._size = frame_heif.size # noqa + self.load_prepare() + self.frombytes(data, "raw", (frame_heif.mode, frame_heif.stride)) + except EOFError: + if not ImageFile.LOAD_TRUNCATED_IMAGES: + raise + self.load_prepare() + # In any case, we close `fp`, since the input data bytes are held by the `HeifFile` class. + if self.fp and getattr(self, "_exclusive_fp", False) and hasattr(self.fp, "close"): + self.fp.close() + self.fp = None + if not self.is_animated: + self._heif_file = None + return super().load() + + if pil_version[:4] in ("10.1", "10.2", "10.3"): + + def getxmp(self) -> dict: + """Returns a dictionary containing the XMP tags. Requires ``defusedxml`` to be installed. + + :returns: XMP tags in a dictionary. + """ + if self.info.get("xmp", None): + xmp_data = self.info["xmp"].rsplit(b"\x00", 1) + if xmp_data[0]: + return self._getxmp(xmp_data[0]) # pylint: disable=no-member + return {} + + def seek(self, frame: int): + if not self._seek_check(frame): + return + self.__frame = frame + self._init_from_heif_file(frame) + + if pil_version[:3] != "10.": + # Pillow 11.0+ + # We need to create a new core image object on second and + # subsequent frames in the image. Image may be different size/mode. + # https://github.com/python-pillow/Pillow/issues/8439 + self.im = Image.core.new(self._mode, self._size) # pylint: disable=too-many-function-args + + _exif = getattr(self, "_exif", None) # Pillow 9.2+ do no reload exif between frames. + if _exif is not None and getattr(_exif, "_loaded", None): + _exif._loaded = False # pylint: disable=protected-access + + def tell(self) -> int: + return self.__frame + + def verify(self) -> None: + pass + + @property + def n_frames(self) -> int: + """Returns the number of available frames. + + :returns: Frame number, starting with 0. + """ + return len(self._heif_file) if self._heif_file else 1 + + @property + def is_animated(self) -> bool: + """Returns ``True`` if this image contains more than one frame, or ``False`` otherwise.""" + return self.n_frames > 1 + + def _seek_check(self, frame: int): + if frame < 0 or frame >= self.n_frames: + raise EOFError("attempt to seek outside sequence") + return self.tell() != frame + + def _init_from_heif_file(self, img_index: int) -> None: + if self._heif_file: + self._size = self._heif_file[img_index].size + self._mode = self._heif_file[img_index].mode + self.info = self._heif_file[img_index].info + self.info["original_orientation"] = set_orientation(self.info) + + +class HeifImageFile(_LibHeifImageFile): + """Pillow plugin class type for a HEIF image format.""" + + format = "HEIF" # noqa + format_description = "HEIF container" + + +def _is_supported_heif(fp) -> bool: + magic = _get_bytes(fp, 12) + if magic[4:8] != b"ftyp": + return False + return magic[8:12] in (b"heic", b"heix", b"heim", b"heis", b"hevc", b"hevx", b"hevm", b"hevs", b"mif1", b"msf1") + + +def _save_heif(im: Image.Image, fp: IO[bytes], _filename: str | bytes): + __save_one(im, fp, HeifCompressionFormat.HEVC) + + +def _save_all_heif(im: Image.Image, fp: IO[bytes], _filename: str | bytes): + __save_all(im, fp, HeifCompressionFormat.HEVC) + + +def register_heif_opener(**kwargs) -> None: + """Registers a Pillow plugin for HEIF format. + + :param kwargs: dictionary with values to set in options. See: :ref:`options`. + """ + __options_update(**kwargs) + Image.register_open(HeifImageFile.format, HeifImageFile, _is_supported_heif) + if _pillow_heif.get_lib_info()["HEIF"]: + Image.register_save(HeifImageFile.format, _save_heif) + Image.register_save_all(HeifImageFile.format, _save_all_heif) + extensions = [".heic", ".heics", ".heif", ".heifs", ".hif"] + Image.register_mime(HeifImageFile.format, "image/heif") + Image.register_extensions(HeifImageFile.format, extensions) + + +class AvifImageFile(_LibHeifImageFile): + """Pillow plugin class type for an AVIF image format.""" + + format = "AVIF" # noqa + format_description = "AVIF container" + + +def _is_supported_avif(fp) -> bool: + magic = _get_bytes(fp, 12) + if magic[4:8] != b"ftyp": + return False + return magic[8:12] == b"avif" + # if magic[8:12] in ( + # b"avif", + # b"avis", + # ): + # return True + # return False + + +def _save_avif(im: Image.Image, fp: IO[bytes], _filename: str | bytes): + __save_one(im, fp, HeifCompressionFormat.AV1) + + +def _save_all_avif(im: Image.Image, fp: IO[bytes], _filename: str | bytes): + __save_all(im, fp, HeifCompressionFormat.AV1) + + +def register_avif_opener(**kwargs) -> None: + """Registers a Pillow plugin for AVIF format. + + :param kwargs: dictionary with values to set in options. See: :ref:`options`. + """ + if not _pillow_heif.get_lib_info()["AVIF"]: + warn("This version of `pillow-heif` was built without AVIF support.", stacklevel=1) + return + __options_update(**kwargs) + Image.register_open(AvifImageFile.format, AvifImageFile, _is_supported_avif) + Image.register_save(AvifImageFile.format, _save_avif) + Image.register_save_all(AvifImageFile.format, _save_all_avif) + # extensions = [".avif", ".avifs"] + extensions = [".avif"] + Image.register_mime(AvifImageFile.format, "image/avif") + Image.register_extensions(AvifImageFile.format, extensions) + + +def __options_update(**kwargs): + """Internal function to set options from `register_avif_opener` and `register_heif_opener` methods.""" + for k, v in kwargs.items(): + if k == "thumbnails": + options.THUMBNAILS = v + elif k == "depth_images": + options.DEPTH_IMAGES = v + elif k == "aux_images": + options.AUX_IMAGES = v + elif k == "quality": + options.QUALITY = v + elif k == "save_to_12bit": + options.SAVE_HDR_TO_12_BIT = v + elif k == "decode_threads": + options.DECODE_THREADS = v + elif k == "allow_incorrect_headers": + options.ALLOW_INCORRECT_HEADERS = v + elif k == "save_nclx_profile": + options.SAVE_NCLX_PROFILE = v + elif k == "preferred_encoder": + options.PREFERRED_ENCODER = v + elif k == "preferred_decoder": + options.PREFERRED_DECODER = v + else: + warn(f"Unknown option: {k}", stacklevel=1) + + +def __save_one(im: Image.Image, fp: IO[bytes], compression_format: HeifCompressionFormat): + ctx_write = CtxEncode(compression_format, **im.encoderinfo) + _pil_encode_image(ctx_write, im, True, **im.encoderinfo) + ctx_write.save(fp) + + +def __save_all(im: Image.Image, fp: IO[bytes], compression_format: HeifCompressionFormat): + ctx_write = CtxEncode(compression_format, **im.encoderinfo) + current_frame = im.tell() if hasattr(im, "tell") else None + append_images = im.encoderinfo.get("append_images", []) + primary_index = _get_primary_index( + chain(ImageSequence.Iterator(im), append_images), im.encoderinfo.get("primary_index", None) + ) + for i, frame in enumerate(chain(ImageSequence.Iterator(im), append_images)): + _pil_encode_image(ctx_write, frame, i == primary_index, **im.encoderinfo) + if current_frame is not None and hasattr(im, "seek"): + im.seek(current_frame) + ctx_write.save(fp) + + +def _pil_encode_image(ctx: CtxEncode, img: Image.Image, primary: bool, **kwargs) -> None: + if img.size[0] <= 0 or img.size[1] <= 0: + raise ValueError("Empty images are not supported.") + _info = img.info.copy() + _info["exif"] = _exif_from_pillow(img) + _info["xmp"] = _xmp_from_pillow(img) + _info.update(**kwargs) + _info["primary"] = primary + if img.mode == "YCbCr": + ctx.add_image_ycbcr(img, image_orientation=_get_orientation_for_encoder(_info), **_info) + else: + _img = _pil_to_supported_mode(img) + ctx.add_image( + _img.size, _img.mode, _img.tobytes(), image_orientation=_get_orientation_for_encoder(_info), **_info + ) diff --git a/.venv/lib/python3.12/site-packages/pillow_heif/constants.py b/.venv/lib/python3.12/site-packages/pillow_heif/constants.py new file mode 100644 index 00000000..98e64b2e --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pillow_heif/constants.py @@ -0,0 +1,203 @@ +"""Enums from LibHeif that are used.""" + +from enum import IntEnum + + +class HeifChroma(IntEnum): + """Chroma subsampling definitions.""" + + UNDEFINED = 99 + """Undefined chroma.""" + MONOCHROME = 0 + """Mono chroma.""" + CHROMA_420 = 1 + """``Cb`` and ``Cr`` are each subsampled at a factor of 2 both horizontally and vertically.""" + CHROMA_422 = 2 + """The two chroma components are sampled at half the horizontal sample rate of luma.""" + CHROMA_444 = 3 + """Each of the three Y'CbCr components has the same sample rate.""" + INTERLEAVED_RGB = 10 + """Simple interleaved RGB.""" + INTERLEAVED_RGBA = 11 + """Interleaved RGB with Alpha channel.""" + INTERLEAVED_RRGGBB_BE = 12 + """10 bit RGB BE.""" + INTERLEAVED_RRGGBBAA_BE = 13 + """10 bit RGB BE with Alpha channel.""" + INTERLEAVED_RRGGBB_LE = 14 + """10 bit RGB LE.""" + INTERLEAVED_RRGGBBAA_LE = 15 + """10 bit RGB LE with Alpha channel.""" + + +class HeifColorspace(IntEnum): + """Colorspace format of the image.""" + + UNDEFINED = 99 + """Undefined colorspace.""" + YCBCR = 0 + """https://en.wikipedia.org/wiki/YCbCr""" + RGB = 1 + """RGB colorspace.""" + MONOCHROME = 2 + """Monochrome colorspace.""" + + +class HeifCompressionFormat(IntEnum): + """Possible LibHeif compression formats.""" + + UNDEFINED = 0 + """The compression format is not defined.""" + HEVC = 1 + """Equivalent to H.265.""" + AVC = 2 + """Equivalent to H.264. Defined in ISO/IEC 14496-10.""" + JPEG = 3 + """JPEG compression. Defined in ISO/IEC 10918-1.""" + AV1 = 4 + """AV1 compression, used for AVIF images.""" + VVC = 5 + """Equivalent to H.266. Defined in ISO/IEC 23090-3.""" + EVC = 6 + """Equivalent to H.266. Defined in ISO/IEC 23094-1.""" + JPEG2000 = 7 + """The compression format is JPEG200 ISO/IEC 15444-16:2021""" + UNCOMPRESSED = 8 + """Defined in ISO/IEC 23001-17:2023 (Final Draft International Standard).""" + MASK = 9 + """Mask image encoding. See ISO/IEC 23008-12:2022 Section 6.10.2""" + + +class HeifColorPrimaries(IntEnum): + """Possible NCLX color_primaries values.""" + + ITU_R_BT_709_5 = 1 + """g=0.3;0.6, b=0.15;0.06, r=0.64;0.33, w=0.3127,0.3290""" + UNSPECIFIED = 2 + """No color primaries""" + ITU_R_BT_470_6_SYSTEM_M = 4 + """Unknown""" + ITU_R_BT_470_6_SYSTEM_B_G = 5 + """Unknown""" + ITU_R_BT_601_6 = 6 + """Unknown""" + SMPTE_240M = 7 + """Unknown""" + GENERIC_FILM = 8 + """Unknown""" + ITU_R_BT_2020_2_AND_2100_0 = 9 + """Unknown""" + SMPTE_ST_428_1 = 10 + """Unknown""" + SMPTE_RP_431_2 = 11 + """Unknown""" + SMPTE_EG_432_1 = 12 + """Unknown""" + EBU_TECH_3213_E = 22 + """Unknown""" + + +class HeifTransferCharacteristics(IntEnum): + """Possible NCLX transfer_characteristics values.""" + + ITU_R_BT_709_5 = 1 + """Unknown""" + UNSPECIFIED = 2 + """No transfer characteristics""" + ITU_R_BT_470_6_SYSTEM_M = 4 + """Unknown""" + ITU_R_BT_470_6_SYSTEM_B_G = 5 + """Unknown""" + ITU_R_BT_601_6 = 6 + """Unknown""" + SMPTE_240M = 7 + """Unknown""" + LINEAR = 8 + """Unknown""" + LOGARITHMIC_100 = 9 + """Unknown""" + LOGARITHMIC_100_SQRT10 = 10 + """Unknown""" + IEC_61966_2_4 = 11 + """Unknown""" + ITU_R_BT_1361 = 12 + """Unknown""" + IEC_61966_2_1 = 13 + """Unknown""" + ITU_R_BT_2020_2_10BIT = 14 + """Unknown""" + ITU_R_BT_2020_2_12BIT = 15 + """Unknown""" + ITU_R_BT_2100_0_PQ = 16 + """Unknown""" + SMPTE_ST_428_1 = 17 + """Unknown""" + ITU_R_BT_2100_0_HLG = 18 + """Unknown""" + + +class HeifMatrixCoefficients(IntEnum): + """Possible NCLX matrix_coefficients values.""" + + RGB_GBR = 0 + """Unknown""" + ITU_R_BT_709_5 = 1 + """Unknown""" + UNSPECIFIED = 2 + """Unknown""" + US_FCC_T47 = 4 + """Unknown""" + ITU_R_BT_470_6_SYSTEM_B_G = 5 + """Unknown""" + ITU_R_BT_601_6 = 6 + """Unknown""" + SMPTE_240M = 7 + """Unknown""" + YCGCO = 8 + """Unknown""" + ITU_R_BT_2020_2_NON_CONSTANT_LUMINANCE = 9 + """Unknown""" + ITU_R_BT_2020_2_CONSTANT_LUMINANCE = 10 + """Unknown""" + SMPTE_ST_2085 = 11 + """Unknown""" + CHROMATICITY_DERIVED_NON_CONSTANT_LUMINANCE = 12 + """Unknown""" + CHROMATICITY_DERIVED_CONSTANT_LUMINANCE = 13 + """Unknown""" + ICTCP = 14 + """Unknown""" + + +class HeifDepthRepresentationType(IntEnum): + """Possible values of the ``HeifDepthImage.info['metadata']['representation_type']``.""" + + UNIFORM_INVERSE_Z = 0 + """Unknown""" + UNIFORM_DISPARITY = 1 + """Unknown""" + UNIFORM_Z = 2 + """Unknown""" + NON_UNIFORM_DISPARITY = 3 + """Unknown""" + + +class HeifChannel(IntEnum): + """Internal libheif values, used in ``CtxEncode``.""" + + CHANNEL_Y = 0 + """Monochrome or YCbCR""" + CHANNEL_CB = 1 + """Only for YCbCR""" + CHANNEL_CR = 2 + """Only for YCbCR""" + CHANNEL_R = 3 + """RGB or RGBA""" + CHANNEL_G = 4 + """RGB or RGBA""" + CHANNEL_B = 5 + """RGB or RGBA""" + CHANNEL_ALPHA = 6 + """Monochrome or RGBA""" + CHANNEL_INTERLEAVED = 10 + """RGB or RGBA""" diff --git a/.venv/lib/python3.12/site-packages/pillow_heif/heif.py b/.venv/lib/python3.12/site-packages/pillow_heif/heif.py new file mode 100644 index 00000000..ddecd1b4 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pillow_heif/heif.py @@ -0,0 +1,648 @@ +"""Functions and classes for heif images to read and write.""" + +from __future__ import annotations + +from copy import copy, deepcopy +from io import SEEK_SET +from typing import Any + +from PIL import Image + +from . import options +from .constants import HeifCompressionFormat +from .misc import ( + MODE_INFO, + CtxEncode, + MimCImage, + _exif_from_pillow, + _get_bytes, + _get_heif_meta, + _get_orientation_for_encoder, + _get_primary_index, + _pil_to_supported_mode, + _retrieve_exif, + _retrieve_xmp, + _rotate_pil, + _xmp_from_pillow, + get_file_mimetype, + save_colorspace_chroma, + set_orientation, +) + +try: + import _pillow_heif +except ImportError as ex: + from ._deffered_error import DeferredError + + _pillow_heif = DeferredError(ex) + + +class BaseImage: + """Base class for :py:class:`HeifImage`, :py:class:`HeifDepthImage` and :py:class:`HeifAuxImage`.""" + + size: tuple[int, int] + """Width and height of the image.""" + + mode: str + """A string which defines the type and depth of a pixel in the image: + `Pillow Modes <https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes>`_ + + For currently supported modes by Pillow-Heif see :ref:`image-modes`.""" + + def __init__(self, c_image): + self.size, self.mode = c_image.size_mode + self._c_image = c_image + self._data = None + + @property + def data(self): + """Decodes image and returns image data. + + :returns: ``bytes`` of the decoded image. + """ + self.load() + return self._data + + @property + def stride(self) -> int: + """Stride of the image. + + .. note:: from `0.10.0` version this value always will have width * sizeof pixel in default usage mode. + + :returns: An Int value indicating the image stride after decoding. + """ + self.load() + return self._c_image.stride + + @property + def __array_interface__(self): + """Numpy array interface support.""" + self.load() + width = int(self.stride / MODE_INFO[self.mode][0]) + if MODE_INFO[self.mode][1] <= 8: + typestr = "|u1" + else: + width = int(width / 2) + typestr = "<u2" + shape: tuple[Any, ...] = (self.size[1], width) + if MODE_INFO[self.mode][0] > 1: + shape += (MODE_INFO[self.mode][0],) + return {"shape": shape, "typestr": typestr, "version": 3, "data": self.data} + + def to_pillow(self) -> Image.Image: + """Helper method to create :external:py:class:`~PIL.Image.Image` class. + + :returns: :external:py:class:`~PIL.Image.Image` class created from an image. + """ + self.load() + return Image.frombytes( + self.mode, # noqa + self.size, + self.data, + "raw", + self.mode, + self.stride, + ) + + def load(self) -> None: + """Method to decode image. + + .. note:: In normal cases, you should not call this method directly, + when reading `data` or `stride` property of image will be loaded automatically. + """ + if not self._data: + self._data = self._c_image.data + self.size, _ = self._c_image.size_mode + + +class HeifDepthImage(BaseImage): + """Class representing the depth image associated with the :py:class:`~pillow_heif.HeifImage` class.""" + + def __init__(self, c_image): + super().__init__(c_image) + _metadata: dict = c_image.metadata + self.info = { + "metadata": _metadata, + } + save_colorspace_chroma(c_image, self.info) + + def __repr__(self): + return f"<{self.__class__.__name__} {self.size[0]}x{self.size[1]} {self.mode}>" + + def to_pillow(self) -> Image.Image: + """Helper method to create :external:py:class:`~PIL.Image.Image` class. + + :returns: :external:py:class:`~PIL.Image.Image` class created from an image. + """ + image = super().to_pillow() + image.info = self.info.copy() + return image + + +class HeifAuxImage(BaseImage): + """Class representing the auxiliary image associated with the :py:class:`~pillow_heif.HeifImage` class.""" + + def __repr__(self): + return f"<{self.__class__.__name__} {self.size[0]}x{self.size[1]} {self.mode}>" + + +class HeifImage(BaseImage): + """One image in a :py:class:`~pillow_heif.HeifFile` container.""" + + def __init__(self, c_image): + super().__init__(c_image) + _metadata: list[dict] = c_image.metadata + _exif = _retrieve_exif(_metadata) + _xmp = _retrieve_xmp(_metadata) + _thumbnails: list[int | None] = [i for i in c_image.thumbnails if i is not None] if options.THUMBNAILS else [] + _depth_images: list[HeifDepthImage | None] = ( + [HeifDepthImage(i) for i in c_image.depth_image_list if i is not None] if options.DEPTH_IMAGES else [] + ) + self.info = { + "primary": bool(c_image.primary), + "bit_depth": int(c_image.bit_depth), + "exif": _exif, + "metadata": _metadata, + "thumbnails": _thumbnails, + "depth_images": _depth_images, + } + if options.AUX_IMAGES: + _ctx_aux_info = {} + for aux_id in c_image.aux_image_ids: + aux_type = c_image.get_aux_type(aux_id) + if aux_type not in _ctx_aux_info: + _ctx_aux_info[aux_type] = [] + _ctx_aux_info[aux_type].append(aux_id) + self.info["aux"] = _ctx_aux_info + _heif_meta = _get_heif_meta(c_image) + if _xmp: + self.info["xmp"] = _xmp + if _heif_meta: + self.info["heif"] = _heif_meta + save_colorspace_chroma(c_image, self.info) + _color_profile: dict[str, Any] = c_image.color_profile + if _color_profile: + if _color_profile["type"] in ("rICC", "prof"): + self.info["icc_profile"] = _color_profile["data"] + self.info["icc_profile_type"] = _color_profile["type"] + else: + self.info["nclx_profile"] = _color_profile["data"] + + def __repr__(self): + _bytes = f"{len(self.data)} bytes" if self._data or isinstance(self._c_image, MimCImage) else "no" + return ( + f"<{self.__class__.__name__} {self.size[0]}x{self.size[1]} {self.mode} " + f"with {_bytes} image data and {len(self.info.get('thumbnails', []))} thumbnails>" + ) + + @property + def has_alpha(self) -> bool: + """``True`` for images with the ``alpha`` channel, ``False`` otherwise.""" + return self.mode.split(sep=";")[0][-1] in ("A", "a") + + @property + def premultiplied_alpha(self) -> bool: + """``True`` for images with ``premultiplied alpha`` channel, ``False`` otherwise.""" + return bool(self.mode.split(sep=";")[0][-1] == "a") + + @premultiplied_alpha.setter + def premultiplied_alpha(self, value: bool): + if self.has_alpha: + self.mode = self.mode.replace("A" if value else "a", "a" if value else "A") + + def to_pillow(self) -> Image.Image: + """Helper method to create :external:py:class:`~PIL.Image.Image` class. + + :returns: :external:py:class:`~PIL.Image.Image` class created from an image. + """ + image = super().to_pillow() + image.info = self.info.copy() + image.info["original_orientation"] = set_orientation(image.info) + return image + + def get_aux_image(self, aux_id: int) -> HeifAuxImage: + """Method to retrieve the auxiliary image at the given ID. + + :returns: a :py:class:`~pillow_heif.HeifAuxImage` class instance. + """ + aux_image = self._c_image.get_aux_image(aux_id) + return HeifAuxImage(aux_image) + + +class HeifFile: + """Representation of the :py:class:`~pillow_heif.HeifImage` classes container. + + To create :py:class:`~pillow_heif.HeifFile` object, use the appropriate factory functions. + + * :py:func:`~pillow_heif.open_heif` + * :py:func:`~pillow_heif.read_heif` + * :py:func:`~pillow_heif.from_pillow` + * :py:func:`~pillow_heif.from_bytes` + + Exceptions that can be raised when working with methods: + `ValueError`, `EOFError`, `SyntaxError`, `RuntimeError`, `OSError` + """ + + def __init__(self, fp=None, convert_hdr_to_8bit=True, bgr_mode=False, **kwargs): + if hasattr(fp, "seek"): + fp.seek(0, SEEK_SET) + + if fp is None: + images = [] + mimetype = "" + else: + fp_bytes = _get_bytes(fp) + mimetype = get_file_mimetype(fp_bytes) + if mimetype.find("avif") != -1: + preferred_decoder = options.PREFERRED_DECODER.get("AVIF", "") + elif mimetype.find("heic") != -1 or mimetype.find("heif") != -1: + preferred_decoder = options.PREFERRED_DECODER.get("HEIF", "") + else: + preferred_decoder = "" + images = _pillow_heif.load_file( + fp_bytes, + options.DECODE_THREADS, + convert_hdr_to_8bit, + bgr_mode, + kwargs.get("remove_stride", True), + kwargs.get("hdr_to_16bit", True), + kwargs.get("reload_size", options.ALLOW_INCORRECT_HEADERS), + preferred_decoder, + ) + self.mimetype = mimetype + self._images: list[HeifImage] = [HeifImage(i) for i in images if i is not None] + self.primary_index = 0 + for index, _ in enumerate(self._images): + if _.info.get("primary", False): + self.primary_index = index + + @property + def size(self): + """:attr:`~pillow_heif.HeifImage.size` property of the primary :class:`~pillow_heif.HeifImage`. + + :exception IndexError: If there are no images. + """ + return self._images[self.primary_index].size + + @property + def mode(self): + """:attr:`~pillow_heif.HeifImage.mode` property of the primary :class:`~pillow_heif.HeifImage`. + + :exception IndexError: If there are no images. + """ + return self._images[self.primary_index].mode + + @property + def has_alpha(self): + """:attr:`~pillow_heif.HeifImage.has_alpha` property of the primary :class:`~pillow_heif.HeifImage`. + + :exception IndexError: If there are no images. + """ + return self._images[self.primary_index].has_alpha + + @property + def premultiplied_alpha(self): + """:attr:`~pillow_heif.HeifImage.premultiplied_alpha` property of the primary :class:`~pillow_heif.HeifImage`. + + :exception IndexError: If there are no images. + """ + return self._images[self.primary_index].premultiplied_alpha + + @premultiplied_alpha.setter + def premultiplied_alpha(self, value: bool): + self._images[self.primary_index].premultiplied_alpha = value + + @property + def data(self): + """:attr:`~pillow_heif.HeifImage.data` property of the primary :class:`~pillow_heif.HeifImage`. + + :exception IndexError: If there are no images. + """ + return self._images[self.primary_index].data + + @property + def stride(self): + """:attr:`~pillow_heif.HeifImage.stride` property of the primary :class:`~pillow_heif.HeifImage`. + + :exception IndexError: If there are no images. + """ + return self._images[self.primary_index].stride + + @property + def info(self): + """`info`` dict of the primary :class:`~pillow_heif.HeifImage` in the container. + + :exception IndexError: If there are no images. + """ + return self._images[self.primary_index].info + + def to_pillow(self) -> Image.Image: + """Helper method to create Pillow :external:py:class:`~PIL.Image.Image`. + + :returns: :external:py:class:`~PIL.Image.Image` class created from the primary image. + """ + return self._images[self.primary_index].to_pillow() + + def save(self, fp, **kwargs) -> None: + """Saves image(s) under the given fp. + + Keyword options can be used to provide additional instructions to the writer. + If a writer does not recognize an option, it is silently ignored. + + Supported options: + ``save_all`` - boolean. Should all images from ``HeiFile`` be saved? + (default = ``True``) + + ``append_images`` - do the same as in Pillow. Accepts the list of ``HeifImage`` + + .. note:: Appended images always will have ``info["primary"]=False`` + + ``quality`` - see :py:attr:`~pillow_heif.options.QUALITY` + + ``enc_params`` - dictionary with key:value to pass to :ref:`x265 <hevc-encoder>` encoder. + + ``exif`` - override primary image's EXIF with specified. + Accepts ``None``, ``bytes`` or ``PIL.Image.Exif`` class. + + ``xmp`` - override primary image's XMP with specified. Accepts ``None`` or ``bytes``. + + ``primary_index`` - ignore ``info["primary"]`` and set `PrimaryImage` by index. + + ``chroma`` - custom subsampling value. Possible values: ``444``, ``422`` or ``420`` (``x265`` default). + + ``subsampling`` - synonym for *chroma*. Format is string, compatible with Pillow: ``x:x:x``, e.g. '4:4:4'. + + ``format`` - string with encoder format name. Possible values: ``HEIF`` (default) or ``AVIF``. + + ``save_nclx_profile`` - boolean, see :py:attr:`~pillow_heif.options.SAVE_NCLX_PROFILE` + + ``matrix_coefficients`` - int, nclx profile: color conversion matrix coefficients, default=6 (see h.273) + + ``color_primaries`` - int, nclx profile: color primaries (see h.273) + + ``transfer_characteristic`` - int, nclx profile: transfer characteristics (see h.273) + + ``full_range_flag`` - nclx profile: full range flag, default: 1 + + :param fp: A filename (string), pathlib.Path object or an object with `write` method. + """ + _encode_images(self._images, fp, **kwargs) + + def __repr__(self): + return f"<{self.__class__.__name__} with {len(self)} images: {[str(i) for i in self]}>" + + def __len__(self): + return len(self._images) + + def __iter__(self): + yield from self._images + + def __getitem__(self, index): + if index < 0 or index >= len(self._images): + raise IndexError(f"invalid image index: {index}") + return self._images[index] + + def __delitem__(self, key): + if key < 0 or key >= len(self._images): + raise IndexError(f"invalid image index: {key}") + del self._images[key] + + def add_frombytes(self, mode: str, size: tuple[int, int], data, **kwargs): + """Adds image from bytes to container. + + .. note:: Supports ``stride`` value if needed. + + :param mode: see :ref:`image-modes`. + :param size: tuple with ``width`` and ``height`` of image. + :param data: bytes object with raw image data. + + :returns: :py:class:`~pillow_heif.HeifImage` added object. + """ + added_image = HeifImage(MimCImage(mode, size, data, **kwargs)) + self._images.append(added_image) + return added_image + + def add_from_heif(self, image: HeifImage) -> HeifImage: + """Add image to the container. + + :param image: :py:class:`~pillow_heif.HeifImage` class to add from. + + :returns: :py:class:`~pillow_heif.HeifImage` added object. + """ + image.load() + added_image = self.add_frombytes( + image.mode, + image.size, + image.data, + stride=image.stride, + ) + added_image.info = deepcopy(image.info) + added_image.info.pop("primary", None) + return added_image + + def add_from_pillow(self, image: Image.Image) -> HeifImage: + """Add image to the container. + + :param image: Pillow :external:py:class:`~PIL.Image.Image` class to add from. + + :returns: :py:class:`~pillow_heif.HeifImage` added object. + """ + if image.size[0] <= 0 or image.size[1] <= 0: + raise ValueError("Empty images are not supported.") + _info = image.info.copy() + _info["exif"] = _exif_from_pillow(image) + _xmp = _xmp_from_pillow(image) + if _xmp: + _info["xmp"] = _xmp + original_orientation = set_orientation(_info) + _img = _pil_to_supported_mode(image) + if original_orientation is not None and original_orientation != 1: + _img = _rotate_pil(_img, original_orientation) + _img.load() + added_image = self.add_frombytes( + _img.mode, + _img.size, + _img.tobytes(), + ) + for key in ["bit_depth", "thumbnails", "icc_profile", "icc_profile_type"]: + if key in image.info: + added_image.info[key] = image.info[key] + for key in ["nclx_profile", "metadata"]: + if key in image.info: + added_image.info[key] = deepcopy(image.info[key]) + added_image.info["exif"] = _exif_from_pillow(image) + _xmp = _xmp_from_pillow(image) + if _xmp: + added_image.info["xmp"] = _xmp + return added_image + + @property + def __array_interface__(self): + """Returns the primary image as a numpy array.""" + return self._images[self.primary_index].__array_interface__ + + def __getstate__(self): + im_desc = [] + for im in self._images: + im_data = bytes(im.data) + im_desc.append([im.mode, im.size, im_data, im.info]) + return [self.primary_index, self.mimetype, im_desc] + + def __setstate__(self, state): + self.__init__() + self.primary_index, self.mimetype, images = state + for im_desc in images: + im_mode, im_size, im_data, im_info = im_desc + added_image = self.add_frombytes(im_mode, im_size, im_data) + added_image.info = im_info + + def __copy(self): + _im_copy = HeifFile() + _im_copy._images = copy(self._images) # pylint: disable=protected-access + _im_copy.mimetype = self.mimetype + _im_copy.primary_index = self.primary_index + return _im_copy + + def get_aux_image(self, aux_id): + """`get_aux_image`` method of the primary :class:`~pillow_heif.HeifImage` in the container. + + :exception IndexError: If there are no images. + """ + return self._images[self.primary_index].get_aux_image(aux_id) + + __copy__ = __copy + + +def is_supported(fp) -> bool: + """Checks if the given `fp` object contains a supported file type. + + :param fp: A filename (string), pathlib.Path object or a file object. + The file object must implement ``file.read``, ``file.seek``, and ``file.tell`` methods, + and be opened in binary mode. + + :returns: A boolean indicating if the object can be opened. + """ + __data = _get_bytes(fp, 12) + if __data[4:8] != b"ftyp": + return False + return get_file_mimetype(__data) != "" + + +def open_heif(fp, convert_hdr_to_8bit=True, bgr_mode=False, **kwargs) -> HeifFile: + """Opens the given HEIF(AVIF) image file. + + :param fp: See parameter ``fp`` in :func:`is_supported` + :param convert_hdr_to_8bit: Boolean indicating should 10 bit or 12 bit images + be converted to 8-bit images during decoding. Otherwise, they will open in 16-bit mode. + ``Does not affect "monochrome" or "depth images".`` + :param bgr_mode: Boolean indicating should be `RGB(A)` images be opened in `BGR(A)` mode. + :param kwargs: **hdr_to_16bit** a boolean value indicating that 10/12-bit image data + should be converted to 16-bit mode during decoding. `Has lower priority than convert_hdr_to_8bit`! + Default = **True** + + :returns: :py:class:`~pillow_heif.HeifFile` object. + :exception ValueError: invalid input data. + :exception EOFError: corrupted image data. + :exception SyntaxError: unsupported feature. + :exception RuntimeError: some other error. + :exception OSError: out of memory. + """ + return HeifFile(fp, convert_hdr_to_8bit, bgr_mode, **kwargs) + + +def read_heif(fp, convert_hdr_to_8bit=True, bgr_mode=False, **kwargs) -> HeifFile: + """Opens the given HEIF(AVIF) image file and decodes all images. + + .. note:: In most cases it is better to call :py:meth:`~pillow_heif.open_heif`, and + let images decoded automatically only when needed. + + :param fp: See parameter ``fp`` in :func:`is_supported` + :param convert_hdr_to_8bit: Boolean indicating should 10 bit or 12 bit images + be converted to 8-bit images during decoding. Otherwise, they will open in 16-bit mode. + ``Does not affect "monochrome" or "depth images".`` + :param bgr_mode: Boolean indicating should be `RGB(A)` images be opened in `BGR(A)` mode. + :param kwargs: **hdr_to_16bit** a boolean value indicating that 10/12-bit image data + should be converted to 16-bit mode during decoding. `Has lower priority than convert_hdr_to_8bit`! + Default = **True** + + :returns: :py:class:`~pillow_heif.HeifFile` object. + :exception ValueError: invalid input data. + :exception EOFError: corrupted image data. + :exception SyntaxError: unsupported feature. + :exception RuntimeError: some other error. + :exception OSError: out of memory. + """ + ret = HeifFile(fp, convert_hdr_to_8bit, bgr_mode, reload_size=True, **kwargs) + for img in ret: + img.load() + return ret + + +def encode(mode: str, size: tuple[int, int], data, fp, **kwargs) -> None: + """Encodes data in a ``fp``. + + :param mode: `BGR(A);16`, `RGB(A);16`, LA;16`, `L;16`, `I;16L`, `BGR(A)`, `RGB(A)`, `LA`, `L` + :param size: tuple with ``width`` and ``height`` of an image. + :param data: bytes object with raw image data. + :param fp: A filename (string), pathlib.Path object or an object with ``write`` method. + """ + _encode_images([HeifImage(MimCImage(mode, size, data, **kwargs))], fp, **kwargs) + + +def _encode_images(images: list[HeifImage], fp, **kwargs) -> None: + compression = kwargs.get("format", "HEIF") + compression_format = HeifCompressionFormat.AV1 if compression == "AVIF" else HeifCompressionFormat.HEVC + if not _pillow_heif.get_lib_info()[compression]: + raise RuntimeError(f"No {compression} encoder found.") + images_to_save: list[HeifImage] = images + kwargs.get("append_images", []) + if not kwargs.get("save_all", True): + images_to_save = images_to_save[:1] + if not images_to_save: + raise ValueError("Cannot write file with no images as HEIF.") + primary_index = _get_primary_index(images_to_save, kwargs.get("primary_index")) + ctx_write = CtxEncode(compression_format, **kwargs) + for i, img in enumerate(images_to_save): + img.load() + _info = img.info.copy() + _info["primary"] = False + if i == primary_index: + _info.update(**kwargs) + _info["primary"] = True + _info.pop("stride", 0) + ctx_write.add_image( + img.size, + img.mode, + img.data, + image_orientation=_get_orientation_for_encoder(_info), + **_info, + stride=img.stride, + ) + ctx_write.save(fp) + + +def from_pillow(pil_image: Image.Image) -> HeifFile: + """Creates :py:class:`~pillow_heif.HeifFile` from a Pillow Image. + + :param pil_image: Pillow :external:py:class:`~PIL.Image.Image` class. + + :returns: New :py:class:`~pillow_heif.HeifFile` object. + """ + _ = HeifFile() + _.add_from_pillow(pil_image) + return _ + + +def from_bytes(mode: str, size: tuple[int, int], data, **kwargs) -> HeifFile: + """Creates :py:class:`~pillow_heif.HeifFile` from bytes. + + .. note:: Supports ``stride`` value if needed. + + :param mode: see :ref:`image-modes`. + :param size: tuple with ``width`` and ``height`` of an image. + :param data: bytes object with raw image data. + + :returns: New :py:class:`~pillow_heif.HeifFile` object. + """ + _ = HeifFile() + _.add_frombytes(mode, size, data, **kwargs) + return _ diff --git a/.venv/lib/python3.12/site-packages/pillow_heif/misc.py b/.venv/lib/python3.12/site-packages/pillow_heif/misc.py new file mode 100644 index 00000000..08c01e73 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pillow_heif/misc.py @@ -0,0 +1,501 @@ +"""Different miscellaneous helper functions. + +Mostly for internal use, so prototypes can change between versions. +""" + +from __future__ import annotations + +import builtins +import re +from dataclasses import dataclass +from enum import IntEnum +from math import ceil +from pathlib import Path +from struct import pack, unpack + +from PIL import Image + +from . import options +from .constants import HeifChannel, HeifChroma, HeifColorspace, HeifCompressionFormat + +try: + import _pillow_heif +except ImportError as ex: + from ._deffered_error import DeferredError + + _pillow_heif = DeferredError(ex) + + +MODE_INFO = { + # name -> [channels, bits per pixel channel, colorspace, chroma] + "BGRA;16": (4, 16, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBBAA_LE), + "BGRa;16": (4, 16, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBBAA_LE), + "BGR;16": (3, 16, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBB_LE), + "RGBA;16": (4, 16, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBBAA_LE), + "RGBa;16": (4, 16, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBBAA_LE), + "RGB;16": (3, 16, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBB_LE), + "LA;16": (2, 16, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "La;16": (2, 16, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "L;16": (1, 16, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "I;16": (1, 16, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "I;16L": (1, 16, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "BGRA;12": (4, 12, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBBAA_LE), + "BGRa;12": (4, 12, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBBAA_LE), + "BGR;12": (3, 12, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBB_LE), + "RGBA;12": (4, 12, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBBAA_LE), + "RGBa;12": (4, 12, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBBAA_LE), + "RGB;12": (3, 12, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBB_LE), + "LA;12": (2, 12, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "La;12": (2, 12, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "L;12": (1, 12, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "I;12": (1, 12, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "I;12L": (1, 12, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "BGRA;10": (4, 10, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBBAA_LE), + "BGRa;10": (4, 10, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBBAA_LE), + "BGR;10": (3, 10, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBB_LE), + "RGBA;10": (4, 10, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBBAA_LE), + "RGBa;10": (4, 10, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBBAA_LE), + "RGB;10": (3, 10, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RRGGBB_LE), + "LA;10": (2, 10, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "La;10": (2, 10, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "L;10": (1, 10, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "I;10": (1, 10, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "I;10L": (1, 10, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "RGBA": (4, 8, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RGBA), + "RGBa": (4, 8, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RGBA), + "RGB": (3, 8, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RGB), + "BGRA": (4, 8, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RGBA), + "BGRa": (4, 8, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RGBA), + "BGR": (3, 8, HeifColorspace.RGB, HeifChroma.INTERLEAVED_RGB), + "LA": (2, 8, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "La": (2, 8, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "L": (1, 8, HeifColorspace.MONOCHROME, HeifChroma.MONOCHROME), + "YCbCr": (3, 8, HeifColorspace.YCBCR, HeifChroma.CHROMA_444), +} + +SUBSAMPLING_CHROMA_MAP = { + "4:4:4": 444, + "4:2:2": 422, + "4:2:0": 420, +} + +LIBHEIF_CHROMA_MAP = { + 1: 420, + 2: 422, + 3: 444, +} + + +def save_colorspace_chroma(c_image, info: dict) -> None: + """Converts `chroma` value from `c_image` to useful values and stores them in ``info`` dict.""" + # Saving of `colorspace` was removed, as currently is not clear where to use that value. + chroma = LIBHEIF_CHROMA_MAP.get(c_image.chroma, None) + if chroma is not None: + info["chroma"] = chroma + + +def set_orientation(info: dict) -> int | None: + """Reset orientation in ``EXIF`` to ``1`` if any orientation present. + + Removes ``XMP`` orientation tag if it is present. + In Pillow plugin mode, it is called automatically for images. + When ``pillow_heif`` used in ``standalone`` mode, if you wish, you can call it manually. + + .. note:: If there is no orientation tag, this function will not add it and do nothing. + + If both XMP and EXIF orientation tags are present, EXIF orientation tag will be returned, + but both tags will be removed. + + :param info: `info` dictionary from :external:py:class:`~PIL.Image.Image` or :py:class:`~pillow_heif.HeifImage`. + :returns: Original orientation or None if it is absent. + """ + return _get_orientation(info, True) + + +def _get_orientation_for_encoder(info: dict) -> int: + image_orientation = _get_orientation(info, False) + return 1 if image_orientation is None else image_orientation + + +def _get_orientation_xmp(info: dict, exif_orientation: int | None, reset: bool = False) -> int | None: + xmp_orientation = 1 + if info.get("xmp"): + xmp_data = info["xmp"].rsplit(b"\x00", 1) + if xmp_data[0]: + decoded_xmp_data = None + for encoding in ("utf-8", "latin1"): + try: + decoded_xmp_data = xmp_data[0].decode(encoding) + break + except Exception: # noqa # pylint: disable=broad-except + pass + if decoded_xmp_data: + match = re.search(r'tiff:Orientation(="|>)([0-9])', decoded_xmp_data) + if match: + xmp_orientation = int(match[2]) + if reset: + decoded_xmp_data = re.sub(r'tiff:Orientation="([0-9])"', "", decoded_xmp_data) + decoded_xmp_data = re.sub(r"<tiff:Orientation>([0-9])</tiff:Orientation>", "", decoded_xmp_data) + # should encode in "utf-8" anyway, as `defusedxml` do not work with `latin1` encoding. + if encoding != "utf-8" or xmp_orientation != 1: + info["xmp"] = b"".join([decoded_xmp_data.encode("utf-8"), b"\x00" if len(xmp_data) > 1 else b""]) + return xmp_orientation if exif_orientation is None and xmp_orientation != 1 else None + + +def _get_orientation(info: dict, reset: bool = False) -> int | None: + original_orientation = None + if info.get("exif"): + try: + tif_tag = info["exif"] + skipped_exif00 = False + if tif_tag.startswith(b"Exif\x00\x00"): + skipped_exif00 = True + tif_tag = tif_tag[6:] + endian_mark = "<" if tif_tag[0:2] == b"\x49\x49" else ">" + pointer = unpack(endian_mark + "L", tif_tag[4:8])[0] + tag_count = unpack(endian_mark + "H", tif_tag[pointer : pointer + 2])[0] + offset = pointer + 2 + for tag_n in range(tag_count): + pointer = offset + 12 * tag_n + if unpack(endian_mark + "H", tif_tag[pointer : pointer + 2])[0] != 274: + continue + value = tif_tag[pointer + 8 : pointer + 12] + _original_orientation = unpack(endian_mark + "H", value[0:2])[0] + if _original_orientation != 1: + original_orientation = _original_orientation + if not reset: + break + p_value = pointer + 8 + if skipped_exif00: + p_value += 6 + new_orientation = pack(endian_mark + "H", 1) + info["exif"] = info["exif"][:p_value] + new_orientation + info["exif"][p_value + 2 :] + break + except Exception: # noqa # pylint: disable=broad-except + pass + xmp_orientation = _get_orientation_xmp(info, original_orientation, reset=reset) + return xmp_orientation or original_orientation + + +def get_file_mimetype(fp) -> str: + """Gets the MIME type of the HEIF(or AVIF) object. + + :param fp: A filename (string), pathlib.Path object, file object or bytes. + The file object must implement ``file.read``, ``file.seek`` and ``file.tell`` methods, + and be opened in binary mode. + :returns: "image/heic", "image/heif", "image/heic-sequence", "image/heif-sequence", + "image/avif", "image/avif-sequence" or "". + """ + heif_brand = _get_bytes(fp, 12)[8:] + if heif_brand: + if heif_brand == b"avif": + return "image/avif" + if heif_brand == b"avis": + return "image/avif-sequence" + if heif_brand in (b"heic", b"heix", b"heim", b"heis"): + return "image/heic" + if heif_brand in (b"hevc", b"hevx", b"hevm", b"hevs"): + return "image/heic-sequence" + if heif_brand == b"mif1": + return "image/heif" + if heif_brand == b"msf1": + return "image/heif-sequence" + return "" + + +def _get_bytes(fp, length=None) -> bytes: + if isinstance(fp, (str, Path)): + with builtins.open(fp, "rb") as file: + return file.read(length or -1) + if hasattr(fp, "read"): + offset = fp.tell() if hasattr(fp, "tell") else None + result = fp.read(length or -1) + if offset is not None and hasattr(fp, "seek"): + fp.seek(offset) + return result + return bytes(fp)[:length] + + +def _retrieve_exif(metadata: list[dict]) -> bytes | None: + _result = None + _purge = [] + for i, md_block in enumerate(metadata): + if md_block["type"] == "Exif": + _purge.append(i) + skip_size = int.from_bytes(md_block["data"][:4], byteorder="big", signed=False) + skip_size += 4 # skip 4 bytes with offset + if len(md_block["data"]) - skip_size <= 4: # bad EXIF data, skip first 4 bytes + skip_size = 4 + elif skip_size >= 6 and md_block["data"][skip_size - 6 : skip_size] == b"Exif\x00\x00": + skip_size -= 6 + _data = md_block["data"][skip_size:] + if not _result and _data: + _result = _data + for i in reversed(_purge): + del metadata[i] + return _result + + +def _retrieve_xmp(metadata: list[dict]) -> bytes | None: + _result = None + _purge = [] + for i, md_block in enumerate(metadata): + if md_block["type"] == "mime": + _purge.append(i) + if not _result: + _result = md_block["data"] + for i in reversed(_purge): + del metadata[i] + return _result + + +def _exif_from_pillow(img: Image.Image) -> bytes | None: + if "exif" in img.info: + return img.info["exif"] + if hasattr(img, "getexif"): # noqa + exif = img.getexif() + if exif: + return exif.tobytes() + return None + + +def _xmp_from_pillow(img: Image.Image) -> bytes | None: + _xmp = None + if "xmp" in img.info: + _xmp = img.info["xmp"] + elif "XML:com.adobe.xmp" in img.info: # PNG + _xmp = img.info["XML:com.adobe.xmp"] + elif hasattr(img, "tag_v2"): # TIFF + if 700 in img.tag_v2: + _xmp = img.tag_v2[700] + elif hasattr(img, "applist"): # JPEG + for segment, content in img.applist: + if segment == "APP1": + marker, xmp_tags = content.rsplit(b"\x00", 1) + if marker == b"http://ns.adobe.com/xap/1.0/": + _xmp = xmp_tags + break + if isinstance(_xmp, str): + _xmp = _xmp.encode("utf-8") + return _xmp + + +def _pil_to_supported_mode(img: Image.Image) -> Image.Image: + # We support "YCbCr" for encoding in Pillow plugin mode and do not call this function. + if img.mode == "P": + mode = "RGBA" if img.info.get("transparency", None) is not None else "RGB" + img = img.convert(mode=mode) + elif img.mode == "I": + img = img.convert(mode="I;16L") + elif img.mode == "1": + img = img.convert(mode="L") + elif img.mode == "CMYK": + img = img.convert(mode="RGBA") + elif img.mode == "YCbCr": + img = img.convert(mode="RGB") + return img + + +class Transpose(IntEnum): + """Temporary workaround till we support old Pillows, remove this when a minimum Pillow version will have this.""" + + FLIP_LEFT_RIGHT = 0 + FLIP_TOP_BOTTOM = 1 + ROTATE_90 = 2 + ROTATE_180 = 3 + ROTATE_270 = 4 + TRANSPOSE = 5 + TRANSVERSE = 6 + + +def _rotate_pil(img: Image.Image, orientation: int) -> Image.Image: + # Probably need create issue in Pillow to add support + # for info["xmp"] or `getxmp()` for ImageOps.exif_transpose and remove this func. + method = { + 2: Transpose.FLIP_LEFT_RIGHT, + 3: Transpose.ROTATE_180, + 4: Transpose.FLIP_TOP_BOTTOM, + 5: Transpose.TRANSPOSE, + 6: Transpose.ROTATE_270, + 7: Transpose.TRANSVERSE, + 8: Transpose.ROTATE_90, + }.get(orientation) + if method is not None: + return img.transpose(method) + return img + + +def _get_primary_index(some_iterator, primary_index: int | None) -> int: + primary_attrs = [_.info.get("primary", False) for _ in some_iterator] + if primary_index is None: + primary_index = 0 + for i, v in enumerate(primary_attrs): + if v: + primary_index = i + elif primary_index == -1 or primary_index >= len(primary_attrs): + primary_index = len(primary_attrs) - 1 + return primary_index + + +def __get_camera_intrinsic_matrix(values: tuple | None): + return ( + { + "focal_length_x": values[0], + "focal_length_y": values[1], + "principal_point_x": values[2], + "principal_point_y": values[3], + "skew": values[4], + } + if values + else None + ) + + +def _get_heif_meta(c_image) -> dict: + r = {} + _camera_intrinsic_matrix = __get_camera_intrinsic_matrix(c_image.camera_intrinsic_matrix) + if _camera_intrinsic_matrix: + r["camera_intrinsic_matrix"] = _camera_intrinsic_matrix + _camera_extrinsic_matrix_rot = c_image.camera_extrinsic_matrix_rot + if _camera_extrinsic_matrix_rot: + r["camera_extrinsic_matrix_rot"] = _camera_extrinsic_matrix_rot + return r + + +class CtxEncode: + """Encoder bindings from python to python C module.""" + + def __init__(self, compression_format: HeifCompressionFormat, **kwargs): + quality = kwargs.get("quality", options.QUALITY) + self.ctx_write = _pillow_heif.CtxWrite( + compression_format, + -2 if quality is None else quality, + options.PREFERRED_ENCODER.get("HEIF" if compression_format == HeifCompressionFormat.HEVC else "AVIF", ""), + ) + enc_params = kwargs.get("enc_params", {}) + chroma = None + if "subsampling" in kwargs: + chroma = SUBSAMPLING_CHROMA_MAP.get(kwargs["subsampling"], None) + if chroma is None: + chroma = kwargs.get("chroma") + if chroma: + enc_params["chroma"] = chroma + for key, value in enc_params.items(): + _value = value if isinstance(value, str) else str(value) + self.ctx_write.set_parameter(key, _value) + + def add_image(self, size: tuple[int, int], mode: str, data, **kwargs) -> None: + """Adds image to the encoder.""" + if size[0] <= 0 or size[1] <= 0: + raise ValueError("Empty images are not supported.") + bit_depth_in = MODE_INFO[mode][1] + bit_depth_out = 8 if bit_depth_in == 8 else kwargs.get("bit_depth", 16) + if bit_depth_out == 16: + bit_depth_out = 12 if options.SAVE_HDR_TO_12_BIT else 10 + premultiplied_alpha = int(mode.split(sep=";")[0][-1] == "a") + # creating image + im_out = self.ctx_write.create_image(size, MODE_INFO[mode][2], MODE_INFO[mode][3], premultiplied_alpha) + # image data + if MODE_INFO[mode][0] == 1: + im_out.add_plane_l(size, bit_depth_out, bit_depth_in, data, kwargs.get("stride", 0), HeifChannel.CHANNEL_Y) + elif MODE_INFO[mode][0] == 2: + im_out.add_plane_la(size, bit_depth_out, bit_depth_in, data, kwargs.get("stride", 0)) + else: + im_out.add_plane(size, bit_depth_out, bit_depth_in, data, mode.find("BGR") != -1, kwargs.get("stride", 0)) + self._finish_add_image(im_out, size, **kwargs) + + def add_image_ycbcr(self, img: Image.Image, **kwargs) -> None: + """Adds image in `YCbCR` mode to the encoder.""" + # creating image + im_out = self.ctx_write.create_image(img.size, MODE_INFO[img.mode][2], MODE_INFO[img.mode][3], 0) + # image data + for i in (HeifChannel.CHANNEL_Y, HeifChannel.CHANNEL_CB, HeifChannel.CHANNEL_CR): + im_out.add_plane_l(img.size, 8, 8, bytes(img.getdata(i)), kwargs.get("stride", 0), i) + self._finish_add_image(im_out, img.size, **kwargs) + + def _finish_add_image(self, im_out, size: tuple[int, int], **kwargs): + # set ICC color profile + __icc_profile = kwargs.get("icc_profile") + if __icc_profile is not None: + im_out.set_icc_profile(kwargs.get("icc_profile_type", "prof"), __icc_profile) + # set NCLX color profile + if kwargs.get("nclx_profile"): + im_out.set_nclx_profile( + *[ + kwargs["nclx_profile"][i] + for i in ("color_primaries", "transfer_characteristics", "matrix_coefficients", "full_range_flag") + ] + ) + # encode + image_orientation = kwargs.get("image_orientation", 1) + im_out.encode( + self.ctx_write, + kwargs.get("primary", False), + kwargs.get("save_nclx_profile", options.SAVE_NCLX_PROFILE), + kwargs.get("color_primaries", -1), + kwargs.get("transfer_characteristics", -1), + kwargs.get("matrix_coefficients", -1), + kwargs.get("full_range_flag", -1), + image_orientation, + ) + # adding metadata + exif = kwargs.get("exif") + if exif is not None: + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + im_out.set_exif(self.ctx_write, exif) + xmp = kwargs.get("xmp") + if xmp is not None: + im_out.set_xmp(self.ctx_write, xmp) + for metadata in kwargs.get("metadata", []): + im_out.set_metadata(self.ctx_write, metadata["type"], metadata["content_type"], metadata["data"]) + # adding thumbnails + for thumb_box in kwargs.get("thumbnails", []): + if max(size) > thumb_box > 3: + im_out.encode_thumbnail(self.ctx_write, thumb_box, image_orientation) + + def save(self, fp) -> None: + """Ask encoder to produce output based on previously added images.""" + data = self.ctx_write.finalize() + if isinstance(fp, (str, Path)): + Path(fp).write_bytes(data) + elif hasattr(fp, "write"): + fp.write(data) + else: + raise TypeError("`fp` must be a path to file or an object with `write` method.") + + +@dataclass +class MimCImage: + """Mimicry of the HeifImage class.""" + + def __init__(self, mode: str, size: tuple[int, int], data: bytes, **kwargs): + self.mode = mode + self.size = size + self.stride: int = kwargs.get("stride", size[0] * MODE_INFO[mode][0] * ceil(MODE_INFO[mode][1] / 8)) + self.data = data + self.metadata: list[dict] = [] + self.color_profile = None + self.thumbnails: list[int] = [] + self.depth_image_list: list = [] + self.aux_image_ids: list[int] = [] + self.primary = False + self.chroma = HeifChroma.UNDEFINED.value + self.colorspace = HeifColorspace.UNDEFINED.value + self.camera_intrinsic_matrix = None + self.camera_extrinsic_matrix_rot = None + + @property + def size_mode(self): + """Mimicry of c_image property.""" + return self.size, self.mode + + @property + def bit_depth(self) -> int: + """Return bit-depth based on image mode.""" + return MODE_INFO[self.mode][1] + + +def load_libheif_plugin(plugin_path: str | Path) -> None: + """Load specified LibHeif plugin.""" + _pillow_heif.load_plugin(plugin_path) diff --git a/.venv/lib/python3.12/site-packages/pillow_heif/options.py b/.venv/lib/python3.12/site-packages/pillow_heif/options.py new file mode 100644 index 00000000..0bbd90b5 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pillow_heif/options.py @@ -0,0 +1,91 @@ +"""Options to change pillow_heif's runtime behavior.""" + +DECODE_THREADS = 4 +"""Maximum number of threads to use for decoding images(when it is possible) + +When use pillow_heif as a plugin you can set it with: `register_*_opener(decode_threads=8)`""" + + +THUMBNAILS = True +"""Option to enable/disable thumbnail support + +When use pillow_heif as a plugin you can set it with: `register_*_opener(thumbnails=False)`""" + + +DEPTH_IMAGES = True +"""Option to enable/disable depth image support + +When use pillow_heif as a plugin you can set it with: `register_*_opener(depth_images=False)`""" + + +AUX_IMAGES = True +"""Option to enable/disable auxiliary image support + +When use pillow_heif as a plugin you can set it with: `register_*_opener(aux_images=False)`""" + + +QUALITY = None +"""Default encoding quality + +.. note:: Quality specified during calling ``save`` has higher priority then this. + +Possible values: None, -1, range(0-100). +Set -1 for lossless quality or from 0 to 100, where 0 is lowest and 100 is highest. + +.. note:: Also for lossless encoding you should specify ``chroma=444`` during save. + +When use pillow_heif as a plugin you can set it with: `register_*_opener(quality=-1)`""" + + +SAVE_HDR_TO_12_BIT = False +"""Should 16 bit images be saved to 12 bit instead of 10 bit`` + +When use pillow_heif as a plugin you can set it with: `register_*_opener(save_to_12bit=True)`""" + + +ALLOW_INCORRECT_HEADERS = False +"""Can or not the ``size`` of image in header differ from decoded one. + +.. note:: If enabled, ``Image.size`` can change after loading for images where it is invalid in header. + +To learn more read: `here <https://github.com/strukturag/libheif/issues/784>`_ + +When use pillow_heif as a plugin you can set it with: `register_*_opener(allow_incorrect_headers=True)`""" + + +SAVE_NCLX_PROFILE = True +"""Should be ``nclx`` profile saved or not. + +Default for all previous versions(pillow_heif<0.14.0) was NOT TO save `nclx` profile, +due to an old bug in Apple software refusing to open images with `nclx` profiles. +Apple has already fixed this and there is no longer a need to not save the default profile. + +.. note:: `save_nclx_profile` specified during calling ``save`` has higher priority than this. + +When use pillow_heif as a plugin you can unset it with: `register_*_opener(save_nclx_profile=False)`""" + + +PREFERRED_ENCODER = { + "AVIF": "", + "HEIF": "", +} +"""Use the specified encoder for format. + +You can get the available encoders IDs using ``libheif_info()`` function. + +When use pillow_heif as a plugin you can set this option with ``preferred_encoder`` key. + +.. note:: If the specified encoder is missing, the option will be ignored.""" + + +PREFERRED_DECODER = { + "AVIF": "", + "HEIF": "", +} +"""Use the specified decoder for format. + +You can get the available decoders IDs using ``libheif_info()`` function. + +When use pillow_heif as a plugin you can set this option with ``preferred_decoder`` key. + +.. note:: If the specified decoder is missing, the option will be ignored.""" |