diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/pillow_heif/heif.py')
| -rw-r--r-- | .venv/lib/python3.12/site-packages/pillow_heif/heif.py | 648 |
1 files changed, 648 insertions, 0 deletions
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 _ |
