Skip to content

Commit cddea72

Browse files
committed
manually merged work from jxl alpha 9.4
1 parent 1bbb212 commit cddea72

File tree

3 files changed

+138
-37
lines changed

3 files changed

+138
-37
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
humanfriendly==10.0
22
opencv_python>=4.8.0.74,<=4.9.0.80
33
Pillow==10.3.0
4+
pillow-jxl-plugin==1.2.6
45
PySide6==6.7.1
56
PySide6_Addons==6.7.1
67
PySide6_Essentials==6.7.1

tagstudio/src/qt/widgets/preview_panel.py

Lines changed: 136 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from datetime import datetime as dt
1111
import cv2
1212
import rawpy
13+
import io
1314
from PIL import Image, UnidentifiedImageError, ImageFont
1415
from PIL.Image import DecompressionBombError
1516
from PySide6.QtCore import QModelIndex, Signal, Qt, QSize, QByteArray, QBuffer
@@ -120,15 +121,44 @@ def __init__(self, library: Library, driver: "QtDriver"):
120121
self.preview_img.addAction(self.open_explorer_action)
121122
self.preview_img.addAction(self.delete_action)
122123

123-
self.preview_gif = QLabel()
124-
self.preview_gif.setMinimumSize(*self.img_button_size)
125-
self.preview_gif.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
126-
self.preview_gif.setCursor(Qt.CursorShape.ArrowCursor)
127-
self.preview_gif.addAction(self.open_file_action)
128-
self.preview_gif.addAction(self.open_explorer_action)
129-
self.preview_gif.addAction(self.delete_action)
130-
self.preview_gif.hide()
131-
self.gif_buffer: QBuffer = QBuffer()
124+
self.preview_ani_img = QLabel()
125+
self.preview_ani_img.setMinimumSize(*self.img_button_size)
126+
self.preview_ani_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
127+
self.preview_ani_img.setCursor(Qt.CursorShape.ArrowCursor)
128+
self.preview_ani_img.addAction(self.open_file_action)
129+
self.preview_ani_img.addAction(self.open_explorer_action)
130+
self.preview_ani_img.addAction(self.delete_action)
131+
self.preview_ani_img.hide()
132+
self.ani_img_buffer: QBuffer = QBuffer()
133+
134+
self.preview_ani_img_fmts = []
135+
136+
qmovie_formats = QMovie.supportedFormats()
137+
138+
self.preview_ani_img_fmts = [
139+
fmt.data().decode("utf-8") for fmt in qmovie_formats
140+
]
141+
142+
ani_img_priority_order = ["jxl", "apng", "png", "webp", "gif"]
143+
144+
self.preview_ani_img_pil_map = {"apng": "png"}
145+
146+
self.preview_ani_img_pil_map_args = {"gif": {"disposal": 2}}
147+
148+
self.preview_ani_img_pil_known_good = {"webp", "gif"}
149+
150+
self.preview_ani_img_fmts.sort(
151+
key=lambda x: ani_img_priority_order.index(x)
152+
if x in ani_img_priority_order
153+
else len(ani_img_priority_order)
154+
)
155+
156+
logging.info(
157+
"supported qmovie image format(s): " + str(self.preview_ani_img_fmts)
158+
)
159+
160+
pil_exts = Image.registered_extensions()
161+
self.pil_save_exts = {ex for ex, f in pil_exts.items() if f in Image.SAVE}
132162

133163
self.preview_vid = VideoPlayer(driver)
134164
self.preview_vid.hide()
@@ -152,8 +182,8 @@ def __init__(self, library: Library, driver: "QtDriver"):
152182

153183
image_layout.addWidget(self.preview_img)
154184
image_layout.setAlignment(self.preview_img, Qt.AlignmentFlag.AlignCenter)
155-
image_layout.addWidget(self.preview_gif)
156-
image_layout.setAlignment(self.preview_gif, Qt.AlignmentFlag.AlignCenter)
185+
image_layout.addWidget(self.preview_ani_img)
186+
image_layout.setAlignment(self.preview_ani_img, Qt.AlignmentFlag.AlignCenter)
157187
image_layout.addWidget(self.preview_vid)
158188
image_layout.setAlignment(self.preview_vid, Qt.AlignmentFlag.AlignCenter)
159189
self.image_container.setMinimumSize(*self.img_button_size)
@@ -435,12 +465,12 @@ def update_image_size(self, size: tuple[int, int], ratio: float = None):
435465
self.preview_vid.resizeVideo(adj_size)
436466
self.preview_vid.setMaximumSize(adj_size)
437467
self.preview_vid.setMinimumSize(adj_size)
438-
self.preview_gif.setMaximumSize(adj_size)
439-
self.preview_gif.setMinimumSize(adj_size)
468+
self.preview_ani_img.setMaximumSize(adj_size)
469+
self.preview_ani_img.setMinimumSize(adj_size)
440470
proxy_style = RoundedPixmapStyle(radius=8)
441-
self.preview_gif.setStyle(proxy_style)
471+
self.preview_ani_img.setStyle(proxy_style)
442472
self.preview_vid.setStyle(proxy_style)
443-
m = self.preview_gif.movie()
473+
m = self.preview_ani_img.movie()
444474
if m:
445475
m.setScaledSize(adj_size)
446476

@@ -520,7 +550,7 @@ def update_widgets(self):
520550
self.preview_img.show()
521551
self.preview_vid.stop()
522552
self.preview_vid.hide()
523-
self.preview_gif.hide()
553+
self.preview_ani_img.hide()
524554
self.selected = list(self.driver.selected)
525555
self.add_field_button.setHidden(True)
526556

@@ -531,7 +561,7 @@ def update_widgets(self):
531561
self.preview_img.show()
532562
self.preview_vid.stop()
533563
self.preview_vid.hide()
534-
self.preview_gif.hide()
564+
self.preview_ani_img.hide()
535565
item: Entry = self.lib.get_entry(self.driver.selected[0][1])
536566
# If a new selection is made, update the thumbnail and filepath.
537567
if not self.selected or self.selected != self.driver.selected:
@@ -574,28 +604,97 @@ def update_widgets(self):
574604
# TODO: Do this all somewhere else, this is just here temporarily.
575605
ext: str = filepath.suffix.lower()
576606
try:
577-
if filepath.suffix.lower() in [".gif"]:
607+
if MediaType.IMAGE_ANIMATION in MediaCategories.get_types(ext):
608+
anim_failed = False
578609
with open(filepath, mode="rb") as f:
579-
if self.preview_gif.movie():
580-
self.preview_gif.movie().stop()
581-
self.gif_buffer.close()
582610

583-
ba = f.read()
584-
self.gif_buffer.setData(ba)
585-
movie = QMovie(self.gif_buffer, QByteArray())
586-
self.preview_gif.setMovie(movie)
587-
movie.start()
611+
image = Image.open(str(filepath))
612+
613+
if hasattr(image, "n_frames"):
614+
if image.n_frames > 1:
615+
if self.preview_ani_img.movie():
616+
logging.info(
617+
"treating as animated image: "
618+
+ str(filepath.name)
619+
+ " with: "
620+
+ str(image.n_frames)
621+
+ " frames"
622+
)
623+
624+
self.preview_ani_img.movie().stop()
625+
self.ani_img_buffer.close()
626+
627+
ba = f.read()
628+
movie = QMovie()
629+
if not ext.lstrip(".") in self.preview_ani_img_fmts:
630+
631+
try:
632+
633+
logging.info(
634+
"converting image not nativly supported by qt"
635+
)
636+
save_buf = io.BytesIO()
637+
save_ext = ""
638+
639+
for fmt_ext in self.preview_ani_img_fmts:
640+
fmt_ext = (
641+
self.preview_ani_img_pil_map.get(
642+
fmt_ext, fmt_ext
643+
)
644+
)
645+
646+
if (
647+
fmt_ext
648+
in self.preview_ani_img_pil_known_good
649+
):
650+
if (
651+
f".{fmt_ext}"
652+
in self.pil_save_exts
653+
):
654+
save_ext = fmt_ext
655+
break
656+
657+
if save_ext == "":
658+
anim_failed = True
659+
660+
else:
661+
extra_args = self.preview_ani_img_pil_map_args.get(
662+
save_ext, {}
663+
)
664+
image.save(
665+
save_buf,
666+
format=save_ext,
667+
lossless=True,
668+
save_all=True,
669+
loop=0,
670+
**extra_args,
671+
)
672+
673+
self.ani_img_buffer.setData(save_buf.getvalue())
674+
675+
except Exception as err:
676+
anim_failed = True
677+
print(
678+
f"error occurred while converting animated image: {err}"
679+
)
680+
681+
else:
682+
self.ani_img_buffer.setData(ba)
683+
684+
685+
movie = QMovie(self.ani_img_buffer, QByteArray())
686+
self.preview_ani_img.setMovie(movie)
687+
movie.start()
588688

589-
image = Image.open(str(filepath))
590-
self.resizeEvent(
591-
QResizeEvent(
592-
QSize(image.width, image.height),
593-
QSize(image.width, image.height),
594-
)
595-
)
596-
self.preview_img.hide()
597-
self.preview_vid.hide()
598-
self.preview_gif.show()
689+
self.resizeEvent(
690+
QResizeEvent(
691+
QSize(image.width, image.height),
692+
QSize(image.width, image.height),
693+
)
694+
)
695+
self.preview_img.hide()
696+
self.preview_vid.hide()
697+
self.preview_ani_img.show()
599698

600699
image = None
601700
if (
@@ -732,7 +831,7 @@ def update_widgets(self):
732831
# Multiple Selected Items
733832
elif len(self.driver.selected) > 1:
734833
self.preview_img.show()
735-
self.preview_gif.hide()
834+
self.preview_ani_img.hide()
736835
self.preview_vid.stop()
737836
self.preview_vid.hide()
738837
if self.selected != self.driver.selected:

tagstudio/src/qt/widgets/thumb_renderer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import numpy as np
1515
import rawpy
1616
from mutagen import MutagenError, flac, id3, mp4
17+
import pillow_jxl
1718
from PIL import (
1819
Image,
1920
ImageChops,

0 commit comments

Comments
 (0)