diff --git a/.github/transform_to-pi_heif.py b/.github/transform_to-pi_heif.py index e0ddf2b3..8a3f523a 100644 --- a/.github/transform_to-pi_heif.py +++ b/.github/transform_to-pi_heif.py @@ -1,4 +1,4 @@ -# This script transform project to `pi-heif` in place. Should be used only with GA Actions. +"""Script to transform the project to `pi-heif` in place. Should be used only with GA Actions.""" import os @@ -22,7 +22,7 @@ files_list += [os.path.join(dir_name, x)] for file_name in files_list: - with open(file_name, "r") as file: + with open(file_name) as file: data = file.read() modified_data = data.replace("pillow_heif", "pi_heif") if modified_data != data: diff --git a/CHANGELOG.md b/CHANGELOG.md index 130c475b..bd8e0a42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ All notable changes to this project will be documented in this file. -## [0.1x.x - 2023-0x-xx] +## [0.13.1 - 2023-10-0x] ### Changed diff --git a/benchmarks/benchmark_decode.py b/benchmarks/benchmark_decode.py index f2bf4621..aa044021 100644 --- a/benchmarks/benchmark_decode.py +++ b/benchmarks/benchmark_decode.py @@ -35,7 +35,7 @@ def measure_decode(image, n_iterations, op_type: int): cat_image_results = [] pug_image_results = [] large_image_results = [] - for i, v in enumerate(VERSIONS): + for _, v in enumerate(VERSIONS): run(f"{sys.executable} -m pip install pillow-heif=={v}".split(), check=True) cat_image_results.append(measure_decode(cat_image_path, N_ITER_SMALL, operation_type)) pug_image_results.append(measure_decode(pug_image_path, N_ITER_SMALL, operation_type)) diff --git a/benchmarks/benchmark_encode.py b/benchmarks/benchmark_encode.py index b746a402..74e1c12b 100644 --- a/benchmarks/benchmark_encode.py +++ b/benchmarks/benchmark_encode.py @@ -24,7 +24,7 @@ def measure_encode(image, n_iterations): la_image_results = [] l_image_results = [] pug_image_results = [] - for i, v in enumerate(VERSIONS): + for _, v in enumerate(VERSIONS): run(f"{sys.executable} -m pip install pillow-heif=={v}".split(), check=True) sleep(N_ITER) rgba_image_results.append(measure_encode("RGBA", N_ITER)) diff --git a/benchmarks/measure_decode.py b/benchmarks/measure_decode.py index 09728da8..1af06baa 100644 --- a/benchmarks/measure_decode.py +++ b/benchmarks/measure_decode.py @@ -15,11 +15,11 @@ pillow_heif.register_heif_opener() start_time = perf_counter() if int(sys.argv[3]) == PILLOW_LOAD: - for i in range(int(sys.argv[1])): + for _ in range(int(sys.argv[1])): im = Image.open(sys.argv[2]) im.load() elif int(sys.argv[3]) == NUMPY_BGR: - for i in range(int(sys.argv[1])): + for _ in range(int(sys.argv[1])): if pillow_heif.__version__.startswith("0.1"): im = pillow_heif.open_heif(sys.argv[2], bgr_mode=True, convert_hdr_to_8bit=False) else: @@ -27,7 +27,7 @@ im.convert_to("BGR" if im.bit_depth == 8 else "BGR;16") np_array = np.asarray(im) elif int(sys.argv[3]) == NUMPY_RGB: - for i in range(int(sys.argv[1])): + for _ in range(int(sys.argv[1])): if pillow_heif.__version__.startswith("0.1"): im = pillow_heif.open_heif(sys.argv[2], convert_hdr_to_8bit=False) else: diff --git a/benchmarks/measure_encode.py b/benchmarks/measure_encode.py index 78a08cd1..21176238 100644 --- a/benchmarks/measure_encode.py +++ b/benchmarks/measure_encode.py @@ -36,7 +36,7 @@ else: img = L_IMAGE start_time = perf_counter() - for i in range(int(sys.argv[1])): + for _ in range(int(sys.argv[1])): buf = BytesIO() img.save(buf, format="HEIF") total_time = perf_counter() - start_time diff --git a/docs/conf.py b/docs/conf.py index 77a87e70..2286eede 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ # -- Project information ----------------------------------------------------- project = "pillow-heif" -copyright = "2021-2022, Alexander Piskun and Contributors" +copyright = "2021-2022, Alexander Piskun and Contributors" # noqa author = "Alexander Piskun and Contributors" # The short X.Y version. diff --git a/examples/heif_dump_info.py b/examples/heif_dump_info.py index d35707a2..a394554c 100644 --- a/examples/heif_dump_info.py +++ b/examples/heif_dump_info.py @@ -20,7 +20,7 @@ print("\tSize:", image.size) print("\tData size:", len(image.data)) print("\tStride:", image.stride) - print("\tThumbnails:", [t for t in image.info["thumbnails"]]) + print("\tThumbnails:", list(image.info["thumbnails"])) if image.info.get("icc_profile", None) is not None: if len(image.info["icc_profile"]): icc_profile = BytesIO(image.info["icc_profile"]) diff --git a/examples/pillow_dump_info.py b/examples/pillow_dump_info.py index 4de3e7cc..0bc839c5 100644 --- a/examples/pillow_dump_info.py +++ b/examples/pillow_dump_info.py @@ -9,12 +9,12 @@ file = "../tests/images/heif_other/cat.hif" print("Dumping info for file:", file) heif_pillow = Image.open(file) - print("Number of images:", len([i for i in ImageSequence.Iterator(heif_pillow)])) + print("Number of images:", len(list(ImageSequence.Iterator(heif_pillow)))) print("Information about each image:") for image in ImageSequence.Iterator(heif_pillow): print("\tMode:", image.mode) print("\tSize:", image.size) - print("\tThumbnails:", [t for t in image.info["thumbnails"]]) + print("\tThumbnails:", list(image.info["thumbnails"])) print("\tData size:", len(image.tobytes())) if image.info.get("icc_profile", None) is not None: if len(image.info["icc_profile"]): diff --git a/examples/pillow_enumerate.py b/examples/pillow_enumerate.py index 50cdb23e..ba92756c 100644 --- a/examples/pillow_enumerate.py +++ b/examples/pillow_enumerate.py @@ -12,5 +12,5 @@ pillow_heif.register_heif_opener() image_path = Path("images/heif_other/nokia/alpha.heic") img = Image.open(image_path) - for i, frame in enumerate(ImageSequence.Iterator(img)): + for i, _frame in enumerate(ImageSequence.Iterator(img)): img.show(title=f"Image index={i}") diff --git a/libheif/linux_build_libs.py b/libheif/linux_build_libs.py index de010058..37596504 100644 --- a/libheif/linux_build_libs.py +++ b/libheif/linux_build_libs.py @@ -1,3 +1,5 @@ +"""File containing code to build Linux libraries for LibHeif and the LibHeif itself.""" + import sys from os import chdir, environ, getcwd, getenv, makedirs, mkdir, path, remove from platform import machine @@ -42,7 +44,7 @@ def download_file(url: str, out_path: str) -> bool: n_download_clients -= 1 break if not n_download_clients: - raise EnvironmentError("Both curl and wget cannot be found.") + raise OSError("Both curl and wget cannot be found.") return False @@ -62,10 +64,7 @@ def tool_check_version(name: str, min_version: str) -> bool: _ = run([name, "--version"], stdout=PIPE, stderr=DEVNULL, check=True) except (CalledProcessError, FileNotFoundError): return False - if name == "nasm": - _regexp = r"version\s*(\d+(\.\d+){2})" - else: # cmake - _regexp = r"(\d+(\.\d+){2})$" + _regexp = r"version\s*(\d+(\.\d+){2})" if name == "nasm" else r"(\d+(\.\d+){2})$" # cmake m_groups = search(_regexp, _.stdout.decode("utf-8"), flags=MULTILINE + IGNORECASE) if m_groups is None: return False @@ -146,7 +145,7 @@ def build_lib_linux(url: str, name: str, musl: bool = False): run(f"patch -p 1 -i {patch_path}".split(), check=True) else: download_extract_to(url, _lib_path) - if name == "libde265": + if name == "libde265": # noqa chdir(_lib_path) # for patch in ( # "libde265/CVE-2022-1253.patch", diff --git a/pillow_heif/AvifImagePlugin.py b/pillow_heif/AvifImagePlugin.py index c4923480..70460329 100644 --- a/pillow_heif/AvifImagePlugin.py +++ b/pillow_heif/AvifImagePlugin.py @@ -1,6 +1,4 @@ -""" -Import this file to auto register an AVIF plugin for Pillow. -""" +"""Import this file to auto register an AVIF plugin for Pillow.""" from .as_plugin import register_avif_opener diff --git a/pillow_heif/HeifImagePlugin.py b/pillow_heif/HeifImagePlugin.py index 22d47ff7..c9382728 100644 --- a/pillow_heif/HeifImagePlugin.py +++ b/pillow_heif/HeifImagePlugin.py @@ -1,6 +1,4 @@ -""" -Import this file to auto register a HEIF plugin for Pillow. -""" +"""Import this file to auto register a HEIF plugin for Pillow.""" from .as_plugin import register_heif_opener diff --git a/pillow_heif/__init__.py b/pillow_heif/__init__.py index 8a59c3e2..f40b2f84 100644 --- a/pillow_heif/__init__.py +++ b/pillow_heif/__init__.py @@ -1,6 +1,4 @@ -""" -Import all possible stuff that can be used. -""" +"""Provide all possible stuff that can be used.""" from . import options diff --git a/pillow_heif/_deffered_error.py b/pillow_heif/_deffered_error.py index 7de59aea..0e0399ac 100644 --- a/pillow_heif/_deffered_error.py +++ b/pillow_heif/_deffered_error.py @@ -1,10 +1,8 @@ -""" -DeferredError class taken from PIL._util.py file. -""" +"""DeferredError class taken from PIL._util.py file.""" class DeferredError: # pylint: disable=too-few-public-methods - """Allow failing import for doc purposes, as C module will be not build at `.readthedocs`""" + """Allows failing import for doc purposes, as C module will be not build during docs build.""" def __init__(self, ex): self.ex = ex diff --git a/pillow_heif/_lib_info.py b/pillow_heif/_lib_info.py index a0c72d9e..bf392d1d 100644 --- a/pillow_heif/_lib_info.py +++ b/pillow_heif/_lib_info.py @@ -1,6 +1,4 @@ -""" -Functions to get versions of underlying libraries. -""" +"""Functions to get versions of underlying libraries.""" try: import _pillow_heif @@ -12,17 +10,18 @@ def libheif_version() -> str: """Returns ``libheif`` version.""" - return _pillow_heif.lib_info["libheif"] def libheif_info() -> dict: """Returns a dictionary with version information. - The keys `libheif`, `HEIF`, `AVIF` are always present, but values for `HEIF`/`AVIF` can be empty. - {'libheif': '1.14.2', - 'HEIF': 'x265 HEVC encoder (3.4+31-6722fce1f)', - 'AVIF': 'AOMedia Project AV1 Encoder 3.5.0' - }""" + The keys `libheif`, `HEIF`, `AVIF` are always present, but values for `HEIF`/`AVIF` can be empty. + { + 'libheif': '1.14.2', + 'HEIF': 'x265 HEVC encoder (3.4+31-6722fce1f)', + 'AVIF': 'AOMedia Project AV1 Encoder 3.5.0' + } + """ return _pillow_heif.lib_info diff --git a/pillow_heif/_version.py b/pillow_heif/_version.py index a3bb3337..9178e19f 100644 --- a/pillow_heif/_version.py +++ b/pillow_heif/_version.py @@ -1,3 +1,3 @@ -""" Version of pillow_heif""" +"""Version of pillow_heif/pi_heif.""" -__version__ = "0.13.0" +__version__ = "0.13.1.dev0" diff --git a/pillow_heif/as_plugin.py b/pillow_heif/as_plugin.py index 775df11f..9453a9f7 100644 --- a/pillow_heif/as_plugin.py +++ b/pillow_heif/as_plugin.py @@ -1,6 +1,4 @@ -""" -Plugins for Pillow library. -""" +"""Plugins for the Pillow library.""" from itertools import chain from typing import Union @@ -82,8 +80,8 @@ def load(self): def getxmp(self) -> dict: """Returns a dictionary containing the XMP tags. Requires ``defusedxml`` to be installed. - :returns: XMP tags in a dictionary.""" - + :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]: @@ -96,9 +94,8 @@ def seek(self, frame): 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: - if getattr(_exif, "_loaded", None): - _exif._loaded = False # pylint: disable=protected-access + if _exif is not None and getattr(_exif, "_loaded", None): + _exif._loaded = False # pylint: disable=protected-access def tell(self): return self.__frame @@ -110,14 +107,13 @@ def verify(self) -> None: def n_frames(self) -> int: """Returns the number of available frames. - :returns: Frame number, starting with 0.""" - + :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): @@ -140,7 +136,7 @@ def _init_from_heif_file(self, img_index: int) -> None: class HeifImageFile(_LibHeifImageFile): """Pillow plugin class type for a HEIF image format.""" - format = "HEIF" + format = "HEIF" # noqa format_description = "HEIF container" @@ -177,7 +173,6 @@ def register_heif_opener(**kwargs) -> None: :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.lib_info["HEIF"]: @@ -191,7 +186,7 @@ def register_heif_opener(**kwargs) -> None: class AvifImageFile(_LibHeifImageFile): """Pillow plugin class type for an AVIF image format.""" - format = "AVIF" + format = "AVIF" # noqa format_description = "AVIF container" @@ -220,9 +215,8 @@ def register_avif_opener(**kwargs) -> None: :param kwargs: dictionary with values to set in options. See: :ref:`options`. """ - if not _pillow_heif.lib_info["AVIF"]: - warn("This version of `pillow-heif` was built without AVIF support.") + 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) @@ -235,8 +229,7 @@ def register_avif_opener(**kwargs) -> None: def __options_update(**kwargs): - """Internal function to set options from `register_avif_opener` and `register_heif_opener`""" - + """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 @@ -253,7 +246,7 @@ def __options_update(**kwargs): elif k == "save_nclx_profile": options.SAVE_NCLX_PROFILE = v else: - warn(f"Unknown option: {k}") + warn(f"Unknown option: {k}", stacklevel=1) def __save_one(im, fp, compression_format: HeifCompressionFormat): diff --git a/pillow_heif/constants.py b/pillow_heif/constants.py index 7df3f168..b6e42011 100644 --- a/pillow_heif/constants.py +++ b/pillow_heif/constants.py @@ -1,6 +1,4 @@ -""" -Enums from LibHeif that are used. -""" +"""Enums from LibHeif that are used.""" from enum import IntEnum @@ -168,7 +166,7 @@ class HeifMatrixCoefficients(IntEnum): class HeifDepthRepresentationType(IntEnum): - """Possible values of ``HeifDepthImage.info['metadata']['representation_type']``""" + """Possible values of the ``HeifDepthImage.info['metadata']['representation_type']``.""" UNIFORM_INVERSE_Z = 0 """Unknown""" diff --git a/pillow_heif/heif.py b/pillow_heif/heif.py index ff7d8c48..42a05b21 100644 --- a/pillow_heif/heif.py +++ b/pillow_heif/heif.py @@ -1,6 +1,4 @@ -""" -Functions and classes for heif images to read and write. -""" +"""Functions and classes for heif images to read and write.""" from copy import copy, deepcopy from io import SEEK_SET @@ -35,7 +33,7 @@ class BaseImage: - """Base class for :py:class:`HeifImage` and :py:class:`HeifDepthImage`""" + """Base class for :py:class:`HeifImage` and :py:class:`HeifDepthImage`.""" size: tuple """Width and height of the image.""" @@ -55,26 +53,25 @@ def __init__(self, c_image): def data(self): """Decodes image and returns image data. - :returns: ``bytes`` of the decoded image.""" - + :returns: ``bytes`` of the decoded image. + """ self.load() return self._data @property - def stride(self): + 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.""" - + :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""" - + """Numpy array interface support.""" self.load() width = int(self.stride / MODE_INFO[self.mode][0]) if MODE_INFO[self.mode][1] <= 8: @@ -88,10 +85,10 @@ def __array_interface__(self): 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` - - :returns: :external:py:class:`~PIL.Image.Image` class created from an 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() image = Image.frombytes( self.mode, # noqa @@ -107,15 +104,15 @@ 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.""" - + 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 represents one depth image for the :py:class:`~pillow_heif.HeifImage`""" + """Class representing the depth image associated with the :py:class:`~pillow_heif.HeifImage` class.""" def __init__(self, c_image): super().__init__(c_image) @@ -129,17 +126,17 @@ def __repr__(self): return f"<{self.__class__.__name__} {self.size[0]}x{self.size[1]} {self.mode}>" def to_pillow(self) -> Image.Image: - """Helper method to create :external:py:class:`~PIL.Image.Image` - - :returns: :external:py:class:`~PIL.Image.Image` class created from an 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): - """Class represents one image in a :py:class:`~pillow_heif.HeifFile`""" + """One image in a :py:class:`~pillow_heif.HeifFile` container.""" def __init__(self, c_image): super().__init__(c_image) @@ -177,20 +174,14 @@ def __repr__(self): ) @property - def has_alpha(self): - """``True`` for images with the ``alpha`` channel, ``False`` otherwise. - - :returns: "True" or "False" """ - + 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): - """``True`` for images with ``premultiplied alpha`` channel, ``False`` otherwise. - - :returns: "True" or "False" """ - - return self.mode.split(sep=";")[0][-1] == "a" + 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): @@ -198,10 +189,10 @@ def premultiplied_alpha(self, value: bool): 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` - - :returns: :external:py:class:`~PIL.Image.Image` class created from an 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) @@ -209,7 +200,7 @@ def to_pillow(self) -> Image.Image: class HeifFile: - """This class represents the :py:class:`~pillow_heif.HeifImage` classes container. + """Representation of the :py:class:`~pillow_heif.HeifImage` classes container. To create :py:class:`~pillow_heif.HeifFile` object, use the appropriate factory functions. @@ -219,7 +210,8 @@ class HeifFile: * :py:func:`~pillow_heif.from_bytes` Exceptions that can be raised when working with methods: - `ValueError`, `EOFError`, `SyntaxError`, `RuntimeError`, `OSError`""" + `ValueError`, `EOFError`, `SyntaxError`, `RuntimeError`, `OSError` + """ def __init__(self, fp=None, convert_hdr_to_8bit=True, bgr_mode=False, **kwargs): if hasattr(fp, "seek"): @@ -249,38 +241,34 @@ def __init__(self, fp=None, convert_hdr_to_8bit=True, bgr_mode=False, **kwargs): @property def size(self): - """Points to :py:attr:`~pillow_heif.HeifImage.size` property of the - primary :py:class:`~pillow_heif.HeifImage` in the container. - - :exception IndexError: If there are no images.""" + """: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): - """Points to :py:attr:`~pillow_heif.HeifImage.mode` property of the - primary :py:class:`~pillow_heif.HeifImage` in the container. - - :exception IndexError: If there are no images.""" + """: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): - """Points to :py:attr:`~pillow_heif.HeifImage.has_alpha` property of the - primary :py:class:`~pillow_heif.HeifImage` in the container. - - :exception IndexError: If there are no images.""" + """: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): - """Points to :py:attr:`~pillow_heif.HeifImage.premultiplied_alpha` property of the - primary :py:class:`~pillow_heif.HeifImage` in the container. - - :exception IndexError: If there are no images.""" + """: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 @@ -289,35 +277,33 @@ def premultiplied_alpha(self, value: bool): @property def data(self): - """Points to :py:attr:`~pillow_heif.HeifImage.data` property of the - primary :py:class:`~pillow_heif.HeifImage` in the container. - - :exception IndexError: If there are no images.""" + """: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): - """Points to :py:attr:`~pillow_heif.HeifImage.stride` property of the - primary :py:class:`~pillow_heif.HeifImage` in the container. - - :exception IndexError: If there are no images.""" + """: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): - """Points to ``info`` dict of the primary :py:class:`~pillow_heif.HeifImage` in the container. - - :exception IndexError: If there are no images.""" + """`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 :external:py:class:`~PIL.Image.Image` - - :returns: :external:py:class:`~PIL.Image.Image` class created from the primary 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: @@ -352,9 +338,7 @@ def save(self, fp, **kwargs) -> None: ``save_nclx_profile`` - boolean, see :py:attr:`~pillow_heif.options.SAVE_NCLX_PROFILE` :param fp: A filename (string), pathlib.Path object or an object with `write` method. - - :returns: None""" - + """ _encode_images(self._images, fp, **kwargs) def __repr__(self): @@ -364,8 +348,7 @@ def __len__(self): return len(self._images) def __iter__(self): - for _ in self._images: - yield _ + yield from self._images def __getitem__(self, index): if index < 0 or index >= len(self._images): @@ -386,8 +369,8 @@ def add_frombytes(self, mode: str, size: tuple, data, **kwargs): :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.""" - + :returns: :py:class:`~pillow_heif.HeifImage` added object. + """ added_image = HeifImage(MimCImage(mode, size, data, **kwargs)) self._images.append(added_image) return added_image @@ -397,8 +380,8 @@ def add_from_heif(self, image: HeifImage) -> HeifImage: :param image: :py:class:`~pillow_heif.HeifImage` class to add from. - :returns: :py:class:`~pillow_heif.HeifImage` added object.""" - + :returns: :py:class:`~pillow_heif.HeifImage` added object. + """ image.load() added_image = self.add_frombytes( image.mode, @@ -415,8 +398,8 @@ def add_from_pillow(self, image: Image.Image) -> HeifImage: :param image: Pillow :external:py:class:`~PIL.Image.Image` class to add from. - :returns: :py:class:`~pillow_heif.HeifImage` added object.""" - + :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() @@ -445,7 +428,6 @@ def add_from_pillow(self, image: Image.Image) -> HeifImage: @property def __array_interface__(self): """Returns the primary image as a numpy array.""" - return self._images[self.primary_index].__array_interface__ def __getstate__(self): @@ -480,8 +462,8 @@ def is_supported(fp) -> bool: 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.""" - + :returns: A boolean indicating if the object can be opened. + """ __data = _get_bytes(fp, 12) if __data[4:8] != b"ftyp": return False @@ -505,8 +487,8 @@ def open_heif(fp, convert_hdr_to_8bit=True, bgr_mode=False, **kwargs) -> HeifFil :exception EOFError: corrupted image data. :exception SyntaxError: unsupported feature. :exception RuntimeError: some other error. - :exception OSError: out of memory.""" - + :exception OSError: out of memory. + """ return HeifFile(fp, convert_hdr_to_8bit, bgr_mode, **kwargs) @@ -530,8 +512,8 @@ def read_heif(fp, convert_hdr_to_8bit=True, bgr_mode=False, **kwargs) -> HeifFil :exception EOFError: corrupted image data. :exception SyntaxError: unsupported feature. :exception RuntimeError: some other error. - :exception OSError: out of memory.""" - + :exception OSError: out of memory. + """ ret = HeifFile(fp, convert_hdr_to_8bit, bgr_mode, reload_size=True, **kwargs) for img in ret: img.load() @@ -544,8 +526,8 @@ def encode(mode: str, size: tuple, data, fp, **kwargs) -> None: :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.""" - + :param fp: A filename (string), pathlib.Path object or an object with ``write`` method. + """ _encode_images([HeifImage(MimCImage(mode, size, data, **kwargs))], fp, **kwargs) @@ -578,8 +560,8 @@ def from_pillow(pil_image: Image.Image) -> HeifFile: :param pil_image: Pillow :external:py:class:`~PIL.Image.Image` class. - :returns: New :py:class:`~pillow_heif.HeifFile` object.""" - + :returns: New :py:class:`~pillow_heif.HeifFile` object. + """ _ = HeifFile() _.add_from_pillow(pil_image) return _ @@ -594,8 +576,8 @@ def from_bytes(mode: str, size: tuple, data, **kwargs) -> HeifFile: :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.""" - + :returns: New :py:class:`~pillow_heif.HeifFile` object. + """ _ = HeifFile() _.add_frombytes(mode, size, data, **kwargs) return _ diff --git a/pillow_heif/misc.py b/pillow_heif/misc.py index 32479150..920bf6fa 100644 --- a/pillow_heif/misc.py +++ b/pillow_heif/misc.py @@ -1,5 +1,4 @@ -""" -Different miscellaneous helper functions. +"""Different miscellaneous helper functions. Mostly for internal use, so prototypes can change between versions. """ @@ -76,18 +75,19 @@ 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 called automatically for images. - When ``pillow_heif`` used in ``standalone`` mode, if you wish you can call it manually. + 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 present, EXIF orientation tag will be returned, + 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.""" - + :returns: Original orientation or None if it is absent. + """ original_orientation = None if info.get("exif", None): try: @@ -142,7 +142,7 @@ def set_orientation(info: dict) -> Optional[int]: def get_file_mimetype(fp) -> str: - """Gets the MIME type of the HEIF(or AVIF) object.` + """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, @@ -150,7 +150,6 @@ def get_file_mimetype(fp) -> str: :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": @@ -191,9 +190,8 @@ def _retrieve_exif(metadata: List[dict]) -> Optional[bytes]: 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: - if md_block["data"][skip_size - 6 : skip_size] == b"Exif\x00\x00": - skip_size -= 6 + 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 @@ -218,7 +216,7 @@ def _retrieve_xmp(metadata: List[dict]) -> Optional[bytes]: def _exif_from_pillow(img: Image.Image) -> Optional[bytes]: if "exif" in img.info: return img.info["exif"] - if hasattr(img, "getexif"): + if hasattr(img, "getexif"): # noqa if pil_version[:4] not in ("9.1.",): exif = img.getexif() if exif: @@ -263,7 +261,8 @@ def _pil_to_supported_mode(img: Image.Image) -> Image.Image: class Transpose(IntEnum): - # Temporary till we support old Pillows, remove this when minimum Pillow version will have this. + """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 @@ -303,6 +302,8 @@ def _get_primary_index(some_iterator, primary_index: Optional[int]) -> int: 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) @@ -315,6 +316,7 @@ def __init__(self, compression_format: HeifCompressionFormat, **kwargs): 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] @@ -364,6 +366,7 @@ def add_image(self, size: tuple, mode: str, data, **kwargs) -> None: im_out.encode_thumbnail(self.ctx_write, thumb_box) 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)): with builtins.open(fp, "wb") as f: @@ -376,6 +379,8 @@ def save(self, fp) -> None: @dataclass class MimCImage: + """Mimicry of the HeifImage class.""" + def __init__(self, mode: str, size: tuple, data: bytes, **kwargs): self.mode = mode self.size = size @@ -389,8 +394,10 @@ def __init__(self, mode: str, size: tuple, data: bytes, **kwargs): @property def size_mode(self): + """Mimicry of c_image property.""" return self.size, self.mode @property - def bit_depth(self): + def bit_depth(self) -> int: + """Return bit-depth based on image mode.""" return MODE_INFO[self.mode][1] diff --git a/pillow_heif/options.py b/pillow_heif/options.py index 5e949cdd..e85be25a 100644 --- a/pillow_heif/options.py +++ b/pillow_heif/options.py @@ -1,6 +1,4 @@ -""" -Options to change pillow_heif's runtime behaviour. -""" +"""Options to change pillow_heif's runtime behavior.""" DECODE_THREADS = 4 diff --git a/pyproject.toml b/pyproject.toml index dd778b44..3f77def5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,10 +12,22 @@ preview = true [tool.ruff] line-length = 120 target-version = "py38" +select = ["A", "B", "C", "D", "E", "F", "G", "I", "UP", "SIM", "Q", "W"] +extend-ignore = ["D107", "D105", "D203", "D213", "D401", "I001"] [tool.ruff.per-file-ignores] "pillow_heif/__init__.py" = ["F401"] +[tool.ruff.extend-per-file-ignores] +"benchmarks/**/*.py" = ["D"] +"docs/**/*.py" = ["D"] +"examples/**/*.py" = ["D"] +"libheif/**/*.py" = ["D"] +"tests/**/*.py" = ["B009", "D", "E402", "UP"] + +[tool.ruff.mccabe] +max-complexity = 16 + [tool.isort] profile = "black" @@ -43,9 +55,11 @@ exclude_lines = [ ] [tool.mypy] +warn_unreachable = true ignore_missing_imports = true warn_no_return = true strict_optional = true +show_error_codes = true [tool.pylint] master.py-version = "3.8" diff --git a/setup.py b/setup.py index 64893e19..0df007be 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -"""Script to build wheel""" +"""Script to build wheel.""" import os import re import subprocess @@ -17,6 +17,7 @@ def get_version(): + """Returns version of the project.""" version_file = "pillow_heif/_version.py" with open(version_file, encoding="utf-8") as f: exec(compile(f.read(), version_file, "exec")) # pylint: disable=exec-used @@ -24,6 +25,7 @@ def get_version(): def _cmd_exists(cmd: str) -> bool: + """Checks if specified command exists on the machine.""" if "PATH" not in os.environ: return False return any(os.access(os.path.join(path, cmd), os.X_OK) for path in os.environ["PATH"].split(os.pathsep)) @@ -62,9 +64,10 @@ def _pkg_config(name): class PillowHeifBuildExt(build_ext): - """This class is based on the Pillow setup method""" + """Class based on the Pillow setup method.""" - def build_extensions(self): # pylint: disable=too-many-branches disable=too-many-statements + def build_extensions(self): # noqa pylint: disable=too-many-branches disable=too-many-statements + """Builds all required python binary extensions of the project.""" if os.getenv("PRE_COMMIT"): return @@ -129,7 +132,10 @@ def build_extensions(self): # pylint: disable=too-many-branches disable=too-man include_path_prefix = os.getenv("MSYS2_PREFIX") if include_path_prefix is None: include_path_prefix = "C:\\msys64\\mingw64" - warn(f"MSYS2_PREFIX environment variable is not set. Assuming `MSYS2_PREFIX={include_path_prefix}`") + warn( + f"MSYS2_PREFIX environment variable is not set. Assuming `MSYS2_PREFIX={include_path_prefix}`", + stacklevel=1, + ) if not os.path.isdir(include_path_prefix): raise ValueError("MSYS2 not found and `MSYS2_PREFIX` is not set or is invalid.") @@ -142,7 +148,7 @@ def build_extensions(self): # pylint: disable=too-many-branches disable=too-man if lib_export_file.is_file(): copy(lib_export_file, os.path.join(library_dir, "libheif.lib")) else: - warn("If you build this with MSYS2, you should not see this warning.") + warn("If you build this with MSYS2, you should not see this warning.", stacklevel=1) # on Windows, we include "root" of the project instead of MSYS2 directory. # Including MSYS2 directory leads to compilation errors, theirs `stdio.h` and other files are different. diff --git a/tests/basic_test.py b/tests/basic_test.py index 09e208ae..bf53266a 100644 --- a/tests/basic_test.py +++ b/tests/basic_test.py @@ -14,7 +14,7 @@ def test_libheif_info(): info = pillow_heif.libheif_info() for key in ("HEIF", "AVIF"): - assert key in info.keys() + assert key in info assert pillow_heif.libheif_version() in ( "1.12.0", "1.13.0", diff --git a/tests/dataset.py b/tests/dataset.py index 5ec7aefe..0a52a76a 100644 --- a/tests/dataset.py +++ b/tests/dataset.py @@ -18,7 +18,7 @@ FULL_DATASET = [i for i in FULL_DATASET if not i.name.endswith(".txt")] if not helpers.aom(): - warn("Skipping tests for `AVIF` format due to lack of codecs.") + warn("Skipping tests for `AVIF` format due to lack of codecs.", stacklevel=1) CORRUPTED_DATASET = [i for i in CORRUPTED_DATASET if not i.name.endswith(".avif")] TRUNCATED_DATASET = [i for i in TRUNCATED_DATASET if not i.name.endswith(".avif")] MINIMAL_DATASET = [i for i in MINIMAL_DATASET if not i.name.endswith(".avif")] diff --git a/tests/import_error_test.py b/tests/import_error_test.py index 05aa4e99..839c37f0 100644 --- a/tests/import_error_test.py +++ b/tests/import_error_test.py @@ -4,7 +4,7 @@ import pytest # This test should be run in a separate interpreter, and needed mostly only for coverage. -if not any([str(i).find("import_error_test") != -1 for i in sys.argv]): +if not any([str(i).find("import_error_test") != -1 for i in sys.argv]): # noqa pytest.skip(allow_module_level=True) diff --git a/tests/leaks_test.py b/tests/leaks_test.py index 618b76c2..8d21d2c5 100644 --- a/tests/leaks_test.py +++ b/tests/leaks_test.py @@ -36,7 +36,8 @@ def perform_open_save(iterations, image_path): def test_open_save_objects_leaks(image): from pympler import summary, tracker - image_file_data = BytesIO(open(image, mode="rb").read()) + with open(image, mode="rb") as file: + image_file_data = BytesIO(file.read()) perform_open_save(1, image_file_data) gc.collect() _summary1 = tracker.SummaryTracker().create_summary() @@ -67,7 +68,8 @@ def test_open_to_numpy_mem_leaks(): import numpy as np mem_limit = None - image_file_data = BytesIO(open(Path("images/heif/L_10__29x100.heif"), mode="rb").read()) + with open(Path("images/heif/L_10__29x100.heif"), mode="rb") as file: + image_file_data = BytesIO(file.read()) for i in range(1000): heif_file = pillow_heif.open_heif(image_file_data, convert_hdr_to_8bit=False) _array = np.asarray(heif_file[0]) # noqa @@ -119,7 +121,8 @@ def test_metadata_leaks(): @pytest.mark.skipif(machine().find("x86_64") == -1, reason="run only on x86_64") def test_pillow_plugin_leaks(): mem_limit = None - image_file_data = BytesIO(open(Path("images/heif/zPug_3.heic"), mode="rb").read()) + with open(Path("images/heif/zPug_3.heic"), mode="rb") as file: + image_file_data = BytesIO(file.read()) for i in range(1000): im = Image.open(image_file_data) for frame in ImageSequence.Iterator(im): diff --git a/tests/metadata_etc_test.py b/tests/metadata_etc_test.py index 19184861..4e322b68 100644 --- a/tests/metadata_etc_test.py +++ b/tests/metadata_etc_test.py @@ -89,7 +89,7 @@ def test_heif_info_changing(save_format): for i in range(3): im[i].info["xmp"] = xmp im[i].info["exif"] = exif.tobytes() - im[i].info["primary"] = False if i != 2 else True + im[i].info["primary"] = bool(i == 2) im.save(out_buf, format=save_format) im_out = pillow_heif.open_heif(out_buf) assert im_out.info["primary"] @@ -136,7 +136,7 @@ def test_pillow_info_changing(save_format): im.seek(i) im.info["xmp"] = xmp im.info["exif"] = exif.tobytes() - im.info["primary"] = False if i != 2 else True + im.info["primary"] = bool(i == 2) im.save(out_buf, format=save_format, save_all=True) im_out = Image.open(out_buf) assert im_out.info["primary"] diff --git a/tests/opencv_test.py b/tests/opencv_test.py index 400aee92..df4943e3 100644 --- a/tests/opencv_test.py +++ b/tests/opencv_test.py @@ -30,7 +30,7 @@ ) def test_save_bgr_16bit_to_10_12_bit(enc_bits, img): try: - options.SAVE_HDR_TO_12_BIT = True if enc_bits == 12 else False + options.SAVE_HDR_TO_12_BIT = bool(enc_bits == 12) cv_img = cv2.imread(img, cv2.IMREAD_UNCHANGED) assert cv_img.shape[2] == 3 # 3 channels(BGR) heif_file = from_bytes(mode="BGR;16", size=(cv_img.shape[1], cv_img.shape[0]), data=bytes(cv_img)) @@ -56,7 +56,7 @@ def test_save_bgr_16bit_to_10_12_bit(enc_bits, img): ) def test_save_bgra_16bit_to_10_12_bit(enc_bits, img): try: - options.SAVE_HDR_TO_12_BIT = True if enc_bits == 12 else False + options.SAVE_HDR_TO_12_BIT = bool(enc_bits == 12) cv_img = cv2.imread(img, cv2.IMREAD_UNCHANGED) assert cv_img.shape[2] == 4 # 4 channels(BGRA) heif_file = from_bytes(mode="BGRA;16", size=(cv_img.shape[1], cv_img.shape[0]), data=bytes(cv_img)) diff --git a/tests/read_test.py b/tests/read_test.py index 8ebeb12c..1162483f 100644 --- a/tests/read_test.py +++ b/tests/read_test.py @@ -91,7 +91,7 @@ def test_heif_corrupted_open(img_path): for input_type in [img_path.read_bytes(), BytesIO(img_path.read_bytes()), img_path, builtins.open(img_path, "rb")]: try: _ = pillow_heif.open_heif(input_type).data - assert False + raise AssertionError("Opening corrupted image should raise proper exception.") except ValueError as exception: assert str(exception).find("Invalid input") != -1 @@ -174,7 +174,7 @@ def test_pillow_inputs(img_path): def test_pillow_after_load(): img = Image.open(Path("images/heif/RGBA_10__29x100.heif")) assert getattr(img, "_heif_file") is not None - for i in range(3): + for _ in range(3): img.load() collect() assert not getattr(img, "is_animated") diff --git a/tests/thumbnails_test.py b/tests/thumbnails_test.py index 82a0bc4b..b7346763 100644 --- a/tests/thumbnails_test.py +++ b/tests/thumbnails_test.py @@ -64,7 +64,7 @@ def test_pillow_remove_thumbnails(): ImageSequence.Iterator(im)[0].info.pop("thumbnails") ImageSequence.Iterator(im)[1].info.pop("thumbnails") im.save(buf, format="HEIF", save_all=True) - for i, img in enumerate(ImageSequence.Iterator(Image.open(buf))): + for _, img in enumerate(ImageSequence.Iterator(Image.open(buf))): assert len(img.info["thumbnails"]) == 0 diff --git a/tests/write_test.py b/tests/write_test.py index 183a093f..3fcfaf18 100644 --- a/tests/write_test.py +++ b/tests/write_test.py @@ -281,7 +281,7 @@ def test_YCbCr_color_mode(): # noqa @pytest.mark.parametrize("save_format", ("HEIF", "AVIF")) def test_I_color_modes_to_10_12_bit(enc_bits, save_format): # noqa try: - pillow_heif.options.SAVE_HDR_TO_12_BIT = True if enc_bits == 12 else False + pillow_heif.options.SAVE_HDR_TO_12_BIT = bool(enc_bits == 12) src_pillow = Image.open(Path("images/non_heif/L_16__29x100.png")) assert src_pillow.mode == "I" for mode in ("I", "I;16", "I;16L"):