Initial commit
This commit is contained in:
5
env/lib/python3.10/site-packages/pillow_heif/AvifImagePlugin.py
vendored
Normal file
5
env/lib/python3.10/site-packages/pillow_heif/AvifImagePlugin.py
vendored
Normal file
@@ -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()
|
||||
5
env/lib/python3.10/site-packages/pillow_heif/HeifImagePlugin.py
vendored
Normal file
5
env/lib/python3.10/site-packages/pillow_heif/HeifImagePlugin.py
vendored
Normal file
@@ -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()
|
||||
29
env/lib/python3.10/site-packages/pillow_heif/__init__.py
vendored
Normal file
29
env/lib/python3.10/site-packages/pillow_heif/__init__.py
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
"""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 (
|
||||
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
|
||||
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/AvifImagePlugin.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/AvifImagePlugin.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/HeifImagePlugin.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/HeifImagePlugin.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/_deffered_error.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/_deffered_error.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/_lib_info.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/_lib_info.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/_version.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/_version.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/as_plugin.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/as_plugin.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/constants.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/constants.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/heif.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/heif.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/misc.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/misc.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/options.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/pillow_heif/__pycache__/options.cpython-310.pyc
vendored
Normal file
Binary file not shown.
11
env/lib/python3.10/site-packages/pillow_heif/_deffered_error.py
vendored
Normal file
11
env/lib/python3.10/site-packages/pillow_heif/_deffered_error.py
vendored
Normal file
@@ -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
|
||||
36
env/lib/python3.10/site-packages/pillow_heif/_lib_info.py
vendored
Normal file
36
env/lib/python3.10/site-packages/pillow_heif/_lib_info.py
vendored
Normal file
@@ -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()
|
||||
3
env/lib/python3.10/site-packages/pillow_heif/_version.py
vendored
Normal file
3
env/lib/python3.10/site-packages/pillow_heif/_version.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Version of pillow_heif/pi_heif."""
|
||||
|
||||
__version__ = "0.18.0"
|
||||
272
env/lib/python3.10/site-packages/pillow_heif/as_plugin.py
vendored
Normal file
272
env/lib/python3.10/site-packages/pillow_heif/as_plugin.py
vendored
Normal file
@@ -0,0 +1,272 @@
|
||||
"""Plugins for the Pillow library."""
|
||||
|
||||
from itertools import chain
|
||||
from typing import Union
|
||||
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: Union[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):
|
||||
if not self._seek_check(frame):
|
||||
return
|
||||
self.__frame = frame
|
||||
self._init_from_heif_file(frame)
|
||||
_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):
|
||||
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):
|
||||
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, fp, _filename):
|
||||
__save_one(im, fp, HeifCompressionFormat.HEVC)
|
||||
|
||||
|
||||
def _save_all_heif(im, fp, _filename):
|
||||
__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, fp, _filename):
|
||||
__save_one(im, fp, HeifCompressionFormat.AV1)
|
||||
|
||||
|
||||
def _save_all_avif(im, fp, _filename):
|
||||
__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 == "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, fp, 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, fp, 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)
|
||||
if primary:
|
||||
_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
|
||||
)
|
||||
203
env/lib/python3.10/site-packages/pillow_heif/constants.py
vendored
Normal file
203
env/lib/python3.10/site-packages/pillow_heif/constants.py
vendored
Normal file
@@ -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"""
|
||||
619
env/lib/python3.10/site-packages/pillow_heif/heif.py
vendored
Normal file
619
env/lib/python3.10/site-packages/pillow_heif/heif.py
vendored
Normal file
@@ -0,0 +1,619 @@
|
||||
"""Functions and classes for heif images to read and write."""
|
||||
|
||||
from copy import copy, deepcopy
|
||||
from io import SEEK_SET
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
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` and :py:class:`HeifDepthImage`."""
|
||||
|
||||
size: tuple
|
||||
"""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):
|
||||
_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}>"
|
||||
|
||||
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 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[Optional[int]] = (
|
||||
[i for i in c_image.thumbnails if i is not None] if options.THUMBNAILS else []
|
||||
)
|
||||
_depth_images: List[Optional[HeifDepthImage]] = (
|
||||
[HeifDepthImage(i) for i in c_image.depth_image_list if i is not None] if options.DEPTH_IMAGES else []
|
||||
)
|
||||
_heif_meta = _get_heif_meta(c_image)
|
||||
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 _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
|
||||
|
||||
|
||||
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, 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
|
||||
|
||||
__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, 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", None))
|
||||
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, 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 _
|
||||
499
env/lib/python3.10/site-packages/pillow_heif/misc.py
vendored
Normal file
499
env/lib/python3.10/site-packages/pillow_heif/misc.py
vendored
Normal file
@@ -0,0 +1,499 @@
|
||||
"""Different miscellaneous helper functions.
|
||||
|
||||
Mostly for internal use, so prototypes can change between versions.
|
||||
"""
|
||||
|
||||
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 typing import List, Optional, Union
|
||||
|
||||
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) -> Optional[int]:
|
||||
"""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: Optional[int], reset: bool = False) -> Optional[int]:
|
||||
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) -> Optional[int]:
|
||||
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]) -> Optional[bytes]:
|
||||
_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]) -> Optional[bytes]:
|
||||
_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) -> Optional[bytes]:
|
||||
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) -> Optional[bytes]:
|
||||
_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: Optional[int]) -> 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: Optional[tuple]):
|
||||
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", None)
|
||||
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, 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, **kwargs):
|
||||
# set ICC color profile
|
||||
__icc_profile = kwargs.get("icc_profile", None)
|
||||
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", None):
|
||||
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", None)
|
||||
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", None)
|
||||
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, 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.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: Union[str, Path]) -> None:
|
||||
"""Load specified LibHeif plugin."""
|
||||
_pillow_heif.load_plugin(plugin_path)
|
||||
85
env/lib/python3.10/site-packages/pillow_heif/options.py
vendored
Normal file
85
env/lib/python3.10/site-packages/pillow_heif/options.py
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
"""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)`"""
|
||||
|
||||
|
||||
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."""
|
||||
Reference in New Issue
Block a user