diff options
author | Matthias Baumgartner <dev@igsor.net> | 2023-02-08 19:25:19 +0100 |
---|---|---|
committer | Matthias Baumgartner <dev@igsor.net> | 2023-02-08 19:25:19 +0100 |
commit | 7bf6b33fa6d6b901e4933bfe0b2a9939d7b3f3f3 (patch) | |
tree | d280d9d1e19e4f7a9d0d4b5405603c729e1fdcce /bsie/reader | |
parent | 05a841215c82ef40d4679dfc4d2c26572bd4d349 (diff) | |
parent | 0d0144466919cfb168e75c2af26d5cb74e10bfa0 (diff) | |
download | bsie-7bf6b33fa6d6b901e4933bfe0b2a9939d7b3f3f3.tar.gz bsie-7bf6b33fa6d6b901e4933bfe0b2a9939d7b3f3f3.tar.bz2 bsie-7bf6b33fa6d6b901e4933bfe0b2a9939d7b3f3f3.zip |
Merge branch 'previews' into develop
Diffstat (limited to 'bsie/reader')
-rw-r--r-- | bsie/reader/chain.py | 11 | ||||
-rw-r--r-- | bsie/reader/image/__init__.py | 1 | ||||
-rw-r--r-- | bsie/reader/image/_pillow.py | 2 | ||||
-rw-r--r-- | bsie/reader/image/_raw.py | 6 | ||||
-rw-r--r-- | bsie/reader/preview/__init__.py | 39 | ||||
-rw-r--r-- | bsie/reader/preview/_pg.py | 86 | ||||
-rw-r--r-- | bsie/reader/preview/_pillow.py | 44 | ||||
-rw-r--r-- | bsie/reader/preview/_rawpy.py | 66 | ||||
-rw-r--r-- | bsie/reader/preview/utils.py | 39 |
9 files changed, 285 insertions, 9 deletions
diff --git a/bsie/reader/chain.py b/bsie/reader/chain.py index 5e9e0d5..1dbc52b 100644 --- a/bsie/reader/chain.py +++ b/bsie/reader/chain.py @@ -73,16 +73,19 @@ class ReaderChain(base.Reader, typing.Generic[T_CONTENT]): return hash((super().__hash__(), self._children)) def __call__(self, path: str) -> T_CONTENT: - raise_error = errors.UnsupportedFileFormatError + raise_error = False for child in self._children: try: return child(path) except errors.UnsupportedFileFormatError: + # child cannot read the file, skip. pass except errors.ReaderError: - # child cannot read the file, skip. - raise_error = errors.ReaderError # type: ignore [assignment] # mypy is confused + # child failed to read the file, skip. + raise_error = True - raise raise_error(path) + if raise_error: + raise errors.ReaderError(path) + raise errors.UnsupportedFileFormatError(path) ## EOF ## diff --git a/bsie/reader/image/__init__.py b/bsie/reader/image/__init__.py index 1f290b5..c5d2a2a 100644 --- a/bsie/reader/image/__init__.py +++ b/bsie/reader/image/__init__.py @@ -27,7 +27,6 @@ __all__: typing.Sequence[str] = ( ## code ## -# FIXME: Check if PIL.Image or PIL.Image.Image, or if version-dependent class Image(chain.ReaderChain[PIL.Image.Image]): # pylint: disable=too-few-public-methods """Read an image file.""" diff --git a/bsie/reader/image/_pillow.py b/bsie/reader/image/_pillow.py index 3144509..5b2bdf2 100644 --- a/bsie/reader/image/_pillow.py +++ b/bsie/reader/image/_pillow.py @@ -27,7 +27,7 @@ __all__: typing.Sequence[str] = ( class PillowImage(base.Reader): """Use PIL to read content of a variety of image file types.""" - def __call__(self, path: str) -> PIL.Image: + def __call__(self, path: str) -> PIL.Image.Image: try: # open file with PIL return PIL.Image.open(path) diff --git a/bsie/reader/image/_raw.py b/bsie/reader/image/_raw.py index cd60453..257fdb3 100644 --- a/bsie/reader/image/_raw.py +++ b/bsie/reader/image/_raw.py @@ -32,17 +32,17 @@ class RawImage(base.Reader): """Use rawpy to read content of raw image file types.""" # file matcher - match: filematcher.Matcher + _match: filematcher.Matcher # additional kwargs to rawpy's postprocess - rawpy_kwargs: typing.Dict[str, typing.Any] + _rawpy_kwargs: typing.Dict[str, typing.Any] def __init__(self, **rawpy_kwargs): match_rule = rawpy_kwargs.pop('file_match_rule', MATCH_RULE) self._match = filematcher.parse(match_rule) self._rawpy_kwargs = rawpy_kwargs - def __call__(self, path: str) -> PIL.Image: + def __call__(self, path: str) -> PIL.Image.Image: # perform quick checks first if not self._match(path): raise errors.UnsupportedFileFormatError(path) diff --git a/bsie/reader/preview/__init__.py b/bsie/reader/preview/__init__.py new file mode 100644 index 0000000..3e69a4a --- /dev/null +++ b/bsie/reader/preview/__init__.py @@ -0,0 +1,39 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import typing + +# external imports +import PIL.Image + +# inner-module imports +from .. import chain + +# constants +_FILE_FORMAT_READERS: typing.Sequence[str] = ( + # native image formats + __package__ + '._pillow.PillowPreviewReader', + __package__ + '._rawpy.RawpyPreviewReader', + # multiformat readers + __package__ + '._pg.PreviewGeneratorReader', + ) + +# exports +__all__: typing.Sequence[str] = ( + 'Preview', + ) + + +## code ## + +class Preview(chain.ReaderChain[typing.Callable[[int], PIL.Image.Image]]): # pylint: disable=too-few-public-methods + """Create a preview from a file.""" + + def __init__(self, cfg: typing.Optional[typing.Any] = None): + super().__init__(_FILE_FORMAT_READERS, cfg) + +## EOF ## diff --git a/bsie/reader/preview/_pg.py b/bsie/reader/preview/_pg.py new file mode 100644 index 0000000..097c513 --- /dev/null +++ b/bsie/reader/preview/_pg.py @@ -0,0 +1,86 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from functools import partial +import contextlib +import io +import os +import shutil +import tempfile +import typing + +# external imports +from preview_generator.manager import PreviewManager +import PIL.Image + +# bsie imports +from bsie.utils import errors + +# inner-module imports +from .. import base + +# exports +__all__: typing.Sequence[str] = ( + 'PreviewGeneratorReader', + ) + + +## code ## + +class PreviewGeneratorReader(base.Reader): + """Uses preview_generator to create previews for various data formats. + See `https://github.com/algoo/preview-generator`_ for details. + """ + + # PreviewManager instance. + _mngr: PreviewManager + + # Set of mime types supported by PreviewManager. + _supported_mimetypes: typing.Set[str] + + # PreviewManager cache. + _cache: str + + # Determines whether the cache directory should be deleted after use. + _cleanup: bool + + def __init__(self, cache: typing.Optional[str] = None): + # initialize cache directory + # TODO: initialize in memory, e.g., via PyFilesystem + if cache is None: + self._cache = tempfile.mkdtemp(prefix='bsie-preview-cache-') + self._cleanup = True + else: + self._cache = cache + self._cleanup = False + # create preview generator + with contextlib.redirect_stderr(io.StringIO()): + self._mngr = PreviewManager(self._cache, create_folder=True) + self._supported_mimetypes = set(self._mngr.get_supported_mimetypes()) + + def __del__(self): + if self._cleanup: + shutil.rmtree(self._cache, ignore_errors=True) + + def __call__(self, path: str) -> typing.Callable[[int], PIL.Image.Image]: + if not os.path.exists(path): + raise errors.ReaderError(path) + if self._mngr.get_mimetype(path) not in self._supported_mimetypes: + raise errors.UnsupportedFileFormatError(path) + return partial(self._preview_callback, path) + + def _preview_callback(self, path: str, max_side: int) -> PIL.Image.Image: + """Produce a jpeg preview of *path* with at most *max_side* side length.""" + try: + # generate the preview + preview_path = self._mngr.get_jpeg_preview(path, width=max_side, height=max_side) + # open the preview and return + return PIL.Image.open(preview_path) + except Exception as err: # FIXME: less generic exception! + raise errors.ReaderError(path) from err + +## EOF ## diff --git a/bsie/reader/preview/_pillow.py b/bsie/reader/preview/_pillow.py new file mode 100644 index 0000000..174d509 --- /dev/null +++ b/bsie/reader/preview/_pillow.py @@ -0,0 +1,44 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from functools import partial +import typing + +# external imports +import PIL.Image + +# bsie imports +from bsie.utils import errors + +# inner-module imports +from . import utils +from .. import base + +# exports +__all__: typing.Sequence[str] = ( + 'PillowPreviewReader', + ) + + +## code ## + +class PillowPreviewReader(base.Reader): + """Produce previews for image files using the Pillow library.""" + + def __call__(self, path: str) -> typing.Callable[[int], PIL.Image.Image]: + try: + # open file with PIL + img = PIL.Image.open(path) + # return callback + return partial(utils.resize, img) + except PIL.UnidentifiedImageError as err: + # failed to open, skip file + raise errors.UnsupportedFileFormatError(path) from err + except IOError as err: + raise errors.ReaderError(path) from err + +# EOF ## diff --git a/bsie/reader/preview/_rawpy.py b/bsie/reader/preview/_rawpy.py new file mode 100644 index 0000000..2c20a48 --- /dev/null +++ b/bsie/reader/preview/_rawpy.py @@ -0,0 +1,66 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from functools import partial +import typing + +# external imports +import PIL.Image +import rawpy + +# bsie imports +from bsie.utils import errors, filematcher + +# inner-module imports +from . import utils +from .. import base + +# constants +MATCH_RULE = 'mime={image/x-nikon-nef} | extension={nef}' + +# exports +__all__: typing.Sequence[str] = ( + 'RawpyPreviewReader', + ) + + +## code ## + +class RawpyPreviewReader(base.Reader): + """Produce previews for raw image files using the rawpy library.""" + + # file matcher + _match: filematcher.Matcher + + # additional kwargs to rawpy's postprocess + _rawpy_kwargs: typing.Dict[str, typing.Any] + + def __init__(self, **rawpy_kwargs): + match_rule = rawpy_kwargs.pop('file_match_rule', MATCH_RULE) + self._match = filematcher.parse(match_rule) + self._rawpy_kwargs = rawpy_kwargs + + def __call__(self, path: str) -> typing.Callable[[int], PIL.Image.Image]: + # perform quick checks first + if not self._match(path): + raise errors.UnsupportedFileFormatError(path) + + try: + # open file with rawpy + ary = rawpy.imread(path).postprocess(**self._rawpy_kwargs) + # convert to PIL.Image + img = PIL.Image.fromarray(ary) + # return callback + return partial(utils.resize, img) + + except (rawpy.LibRawFatalError, # pylint: disable=no-member # pylint doesn't find the errors + rawpy.NotSupportedError, # pylint: disable=no-member + rawpy.LibRawNonFatalError, # pylint: disable=no-member + ) as err: + raise errors.ReaderError(path) from err + +## EOF ## diff --git a/bsie/reader/preview/utils.py b/bsie/reader/preview/utils.py new file mode 100644 index 0000000..2ef1562 --- /dev/null +++ b/bsie/reader/preview/utils.py @@ -0,0 +1,39 @@ +""" + +Part of the tagit module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# external imports +import PIL.Image + +# exports +__all__: typing.Sequence[str] = ( + 'resize', + ) + + +## code ## + +def resize( + img: PIL.Image.Image, + max_size: int, + ) -> PIL.Image.Image: + """Resize an image to a given maximum side length.""" + # determine target dimensions + ratio = img.width / img.height + if img.width > img.height: + width, height = max_size, round(max_size / ratio) + else: + width, height = round(ratio * max_size), max_size + # rescale and return + return img.resize( + (width, height), + resample=PIL.Image.Resampling.LANCZOS, # create high-quality image + reducing_gap=3.0, # optimize computation via fast size reduction + ) + +## EOF ## |