diff --git a/NEWS.md b/NEWS.md index e999833..1d6b466 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # 📰 Picopt News +## v6.3.1 + +- Fix frame duration erroneously extracted for MPO format, causing an error. + ## v6.3.0 - New progress and logging diff --git a/picopt/walk/detect_format.py b/picopt/walk/detect_format.py index c652696..822989b 100644 --- a/picopt/walk/detect_format.py +++ b/picopt/walk/detect_format.py @@ -31,6 +31,7 @@ from contextlib import suppress from typing import TYPE_CHECKING, Any, BinaryIO +from loguru import logger from PIL import Image, ImageSequence, UnidentifiedImageError from picopt import plugins as registry @@ -41,18 +42,40 @@ if TYPE_CHECKING: from collections.abc import Mapping - import PIL.ImageFile + from PIL.ImageFile import ImageFile from picopt.path import PathInfo # PIL format-string constants. Hardcoded so this module doesn't have to # import the per-format PIL ImageFile subclasses just to read a string. -_WEBP_FORMAT_STR = "WEBP" +_MPO_FORMAT_STR = "MPO" _TIFF_FORMAT_STR = "TIFF" +_WEBP_FORMAT_STR = "WEBP" + + +def _extract_animated_durations(image: ImageFile, info: dict[str, Any]): + durations = {} + if image.format == _MPO_FORMAT_STR: + return durations + + # PIL frequently fails to populate per-frame durations on WebP + if image.format != _WEBP_FORMAT_STR and info.get("durations"): + return durations + + try: + for frame_index, frame in enumerate(ImageSequence.Iterator(image), start=1): + duration = frame.info.get("duration", None) + if duration is not None: + durations[frame_index] = duration + except Exception as exc: + msg = "Error extracting animated frame duration" + logger.warning(msg) + logger.exception(exc) + return durations def _extract_image_info_from_image( - image: PIL.ImageFile.ImageFile, info: dict[str, Any], *, keep_metadata: bool + image: ImageFile, info: dict[str, Any], *, keep_metadata: bool ) -> None: image_format_str = image.format if not image_format_str: @@ -67,14 +90,7 @@ def _extract_image_info_from_image( info["animated"] = animated if animated and (n_frames := getattr(image, "n_frames", 0)): info["n_frames"] = n_frames - # PIL frequently fails to populate per-frame durations on WebP, so - # walk the sequence ourselves. - durations = {} - for frame_index, frame in enumerate(ImageSequence.Iterator(image), start=1): - duration = frame.info.get("duration", None) - if duration is not None: - durations[frame_index] = duration - if durations: + if durations := _extract_animated_durations(image, info): info["durations"] = durations with suppress(AttributeError): info["mpinfo"] = image.mpinfo # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[unresolved-attribute] diff --git a/pyproject.toml b/pyproject.toml index 99bb77d..d86d0d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ readme = "README.md" requires-python = ">=3.10" license = { text = "GPL-3.0-only" } name = "picopt" -version = "6.3.0" +version = "6.3.1" [[project.authors]] name = "AJ Slater" email = "aj@slater.net" diff --git a/uv.lock b/uv.lock index f6db4d2..8c793a7 100644 --- a/uv.lock +++ b/uv.lock @@ -1598,7 +1598,7 @@ wheels = [ [[package]] name = "picopt" -version = "6.3.0" +version = "6.3.1" source = { editable = "." } dependencies = [ { name = "confuse" },