10
10
from datetime import datetime as dt
11
11
import cv2
12
12
import rawpy
13
+ import io
13
14
from PIL import Image , UnidentifiedImageError , ImageFont
14
15
from PIL .Image import DecompressionBombError
15
16
from PySide6 .QtCore import QModelIndex , Signal , Qt , QSize , QByteArray , QBuffer
@@ -120,15 +121,44 @@ def __init__(self, library: Library, driver: "QtDriver"):
120
121
self .preview_img .addAction (self .open_explorer_action )
121
122
self .preview_img .addAction (self .delete_action )
122
123
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 }
132
162
133
163
self .preview_vid = VideoPlayer (driver )
134
164
self .preview_vid .hide ()
@@ -152,8 +182,8 @@ def __init__(self, library: Library, driver: "QtDriver"):
152
182
153
183
image_layout .addWidget (self .preview_img )
154
184
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 )
157
187
image_layout .addWidget (self .preview_vid )
158
188
image_layout .setAlignment (self .preview_vid , Qt .AlignmentFlag .AlignCenter )
159
189
self .image_container .setMinimumSize (* self .img_button_size )
@@ -435,12 +465,12 @@ def update_image_size(self, size: tuple[int, int], ratio: float = None):
435
465
self .preview_vid .resizeVideo (adj_size )
436
466
self .preview_vid .setMaximumSize (adj_size )
437
467
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 )
440
470
proxy_style = RoundedPixmapStyle (radius = 8 )
441
- self .preview_gif .setStyle (proxy_style )
471
+ self .preview_ani_img .setStyle (proxy_style )
442
472
self .preview_vid .setStyle (proxy_style )
443
- m = self .preview_gif .movie ()
473
+ m = self .preview_ani_img .movie ()
444
474
if m :
445
475
m .setScaledSize (adj_size )
446
476
@@ -520,7 +550,7 @@ def update_widgets(self):
520
550
self .preview_img .show ()
521
551
self .preview_vid .stop ()
522
552
self .preview_vid .hide ()
523
- self .preview_gif .hide ()
553
+ self .preview_ani_img .hide ()
524
554
self .selected = list (self .driver .selected )
525
555
self .add_field_button .setHidden (True )
526
556
@@ -531,7 +561,7 @@ def update_widgets(self):
531
561
self .preview_img .show ()
532
562
self .preview_vid .stop ()
533
563
self .preview_vid .hide ()
534
- self .preview_gif .hide ()
564
+ self .preview_ani_img .hide ()
535
565
item : Entry = self .lib .get_entry (self .driver .selected [0 ][1 ])
536
566
# If a new selection is made, update the thumbnail and filepath.
537
567
if not self .selected or self .selected != self .driver .selected :
@@ -574,28 +604,97 @@ def update_widgets(self):
574
604
# TODO: Do this all somewhere else, this is just here temporarily.
575
605
ext : str = filepath .suffix .lower ()
576
606
try :
577
- if filepath .suffix .lower () in [".gif" ]:
607
+ if MediaType .IMAGE_ANIMATION in MediaCategories .get_types (ext ):
608
+ anim_failed = False
578
609
with open (filepath , mode = "rb" ) as f :
579
- if self .preview_gif .movie ():
580
- self .preview_gif .movie ().stop ()
581
- self .gif_buffer .close ()
582
610
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 ()
588
688
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 ()
599
698
600
699
image = None
601
700
if (
@@ -732,7 +831,7 @@ def update_widgets(self):
732
831
# Multiple Selected Items
733
832
elif len (self .driver .selected ) > 1 :
734
833
self .preview_img .show ()
735
- self .preview_gif .hide ()
834
+ self .preview_ani_img .hide ()
736
835
self .preview_vid .stop ()
737
836
self .preview_vid .hide ()
738
837
if self .selected != self .driver .selected :
0 commit comments