Initial commit

This commit is contained in:
2024-08-27 20:33:44 +02:00
commit 1f1832267d
14794 changed files with 1599592 additions and 0 deletions

View 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()

View 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()

View 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

View 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

View 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()

View File

@@ -0,0 +1,3 @@
"""Version of pillow_heif/pi_heif."""
__version__ = "0.18.0"

View 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
)

View 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"""

View 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 _

View 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)

View 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."""