Skip to content

Commit c1e7f7a

Browse files
committed
refactor: extract gif parsing to controller
1 parent 7496935 commit c1e7f7a

File tree

2 files changed

+57
-48
lines changed

2 files changed

+57
-48
lines changed

src/tagstudio/qt/controller/widgets/preview/preview_thumb_controller.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Licensed under the GPL-3.0 License.
22
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
33

4+
import io
45
from pathlib import Path
56
from typing import TYPE_CHECKING
67

@@ -63,6 +64,35 @@ def __get_image_stats(self, filepath: Path) -> FileAttributeData:
6364

6465
return stats
6566

67+
def __get_gif_data(self, filepath: Path) -> tuple[bytes, tuple[int, int]] | None:
68+
"""Loads an animated image and returns gif data and size, if successful."""
69+
ext = filepath.suffix.lower()
70+
71+
try:
72+
image: Image.Image = Image.open(filepath)
73+
74+
if ext == ".apng":
75+
image_bytes_io = io.BytesIO()
76+
image.save(
77+
image_bytes_io,
78+
"GIF",
79+
lossless=True,
80+
save_all=True,
81+
loop=0,
82+
disposal=2,
83+
)
84+
image.close()
85+
image_bytes_io.seek(0)
86+
return (image_bytes_io.read(), (image.width, image.height))
87+
else:
88+
image.close()
89+
with open(filepath, "rb") as f:
90+
return (f.read(), (image.width, image.height))
91+
92+
except (UnidentifiedImageError, FileNotFoundError) as e:
93+
logger.error("[PreviewThumb] Could not load animated image", filepath=filepath, error=e)
94+
return None
95+
6696
def display_file(self, filepath: Path) -> FileAttributeData:
6797
"""Render a single file preview."""
6898
self.__current_file = filepath
@@ -79,7 +109,9 @@ def display_file(self, filepath: Path) -> FileAttributeData:
79109
return self._display_audio(filepath)
80110
# Animated Images
81111
elif MediaCategories.IMAGE_ANIMATED_TYPES.contains(ext, mime_fallback=True):
82-
if (stats := self._display_animated_image(filepath)) is not None:
112+
if (ret := self.__get_gif_data(filepath)) and (
113+
stats := self._display_gif(ret[0], ret[1])
114+
) is not None:
83115
return stats
84116
else:
85117
self._display_image(filepath)

src/tagstudio/qt/view/widgets/preview/preview_thumb_view.py

Lines changed: 24 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
# Licensed under the GPL-3.0 License.
22
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
33

4-
import io
54
import time
65
from pathlib import Path
76
from typing import TYPE_CHECKING, override
87

98
import cv2
109
import structlog
11-
from PIL import Image, UnidentifiedImageError
10+
from PIL import Image
1211
from PySide6.QtCore import QBuffer, QByteArray, QSize, Qt
1312
from PySide6.QtGui import QAction, QMovie, QPixmap, QResizeEvent
1413
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QStackedLayout, QWidget
@@ -277,62 +276,40 @@ def _display_audio(self, filepath: Path) -> FileAttributeData:
277276
self.__render_thumb(filepath)
278277
return FileAttributeData(duration=self.__update_media_player(filepath))
279278

280-
def _display_animated_image(self, filepath: Path) -> FileAttributeData | None:
279+
def _display_gif(self, gif_data: bytes, size: tuple[int, int]) -> FileAttributeData | None:
281280
"""Update the animated image preview from a filepath."""
282-
ext = filepath.suffix.lower()
283281
stats = FileAttributeData()
284282

285283
# Ensure that any movie and buffer from previous animations are cleared.
286284
if self.__preview_gif.movie():
287285
self.__preview_gif.movie().stop()
288286
self.__gif_buffer.close()
289287

290-
try:
291-
image: Image.Image = Image.open(filepath)
292-
stats.width = image.width
293-
stats.height = image.height
294-
295-
self.__image_ratio = image.width / image.height
296-
if ext == ".apng":
297-
image_bytes_io = io.BytesIO()
298-
image.save(
299-
image_bytes_io,
300-
"GIF",
301-
lossless=True,
302-
save_all=True,
303-
loop=0,
304-
disposal=2,
305-
)
306-
image.close()
307-
image_bytes_io.seek(0)
308-
self.__gif_buffer.setData(image_bytes_io.read())
309-
else:
310-
image.close()
311-
with open(filepath, "rb") as f:
312-
self.__gif_buffer.setData(f.read())
313-
movie = QMovie(self.__gif_buffer, QByteArray())
314-
self.__preview_gif.setMovie(movie)
315-
316-
# If the animation only has 1 frame, display it like a normal image.
317-
if movie.frameCount() <= 1:
318-
self._display_image(filepath)
319-
return stats
320-
321-
# The animation has more than 1 frame, continue displaying it as an animation
322-
self.__switch_preview(MediaType.IMAGE_ANIMATED)
323-
self.resizeEvent(
324-
QResizeEvent(
325-
QSize(stats.width, stats.height),
326-
QSize(stats.width, stats.height),
327-
)
328-
)
329-
movie.start()
288+
stats.width = size[0]
289+
stats.height = size[1]
290+
291+
self.__image_ratio = stats.width / stats.height
330292

331-
stats.duration = movie.frameCount() // 60
332-
except (UnidentifiedImageError, FileNotFoundError) as e:
333-
logger.error("[PreviewThumb] Could not load animated image", filepath=filepath, error=e)
293+
self.__gif_buffer.setData(gif_data)
294+
movie = QMovie(self.__gif_buffer, QByteArray())
295+
self.__preview_gif.setMovie(movie)
296+
297+
# If the animation only has 1 frame, it isn't animated and shouldn't be treated as such
298+
if movie.frameCount() <= 1:
334299
return None
335300

301+
# The animation has more than 1 frame, continue displaying it as an animation
302+
self.__switch_preview(MediaType.IMAGE_ANIMATED)
303+
self.resizeEvent(
304+
QResizeEvent(
305+
QSize(stats.width, stats.height),
306+
QSize(stats.width, stats.height),
307+
)
308+
)
309+
movie.start()
310+
311+
stats.duration = movie.frameCount() // 60
312+
336313
return stats
337314

338315
def _display_image(self, filepath: Path):

0 commit comments

Comments
 (0)