about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/pillow_heif/misc.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/pillow_heif/misc.py')
-rw-r--r--.venv/lib/python3.12/site-packages/pillow_heif/misc.py501
1 files changed, 501 insertions, 0 deletions
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)