28
28
from PIL .Image import DecompressionBombError
29
29
from pillow_heif import register_avif_opener , register_heif_opener
30
30
from pydub import exceptions
31
- from PySide6 .QtCore import QBuffer , QObject , QSize , Qt , Signal
31
+ from PySide6 .QtCore import (
32
+ QBuffer ,
33
+ QFile ,
34
+ QFileDevice ,
35
+ QIODeviceBase ,
36
+ QObject ,
37
+ QSize ,
38
+ QSizeF ,
39
+ Qt ,
40
+ Signal ,
41
+ )
32
42
from PySide6 .QtGui import QGuiApplication , QImage , QPainter , QPixmap
43
+ from PySide6 .QtPdf import QPdfDocument , QPdfDocumentRenderOptions
33
44
from PySide6 .QtSvg import QSvgRenderer
34
45
from src .core .constants import FONT_SAMPLE_SIZES , FONT_SAMPLE_TEXT
35
46
from src .core .media_types import MediaCategories , MediaType
39
50
from src .qt .helpers .color_overlay import theme_fg_overlay
40
51
from src .qt .helpers .file_tester import is_readable_video
41
52
from src .qt .helpers .gradient import four_corner_gradient
53
+ from src .qt .helpers .image_effects import replace_transparent_pixels
42
54
from src .qt .helpers .text_wrapper import wrap_full_text
43
55
from src .qt .helpers .vendored .pydub .audio_segment import ( # type: ignore
44
56
_AudioSegment as AudioSegment ,
@@ -812,6 +824,52 @@ def _model_stl_thumb(self, filepath: Path, size: int) -> Image.Image:
812
824
813
825
return im
814
826
827
+ def _pdf_thumb (self , filepath : Path , size : int ) -> Image .Image :
828
+ """Render a thumbnail for a PDF file.
829
+
830
+ filepath (Path): The path of the file.
831
+ size (int): The size of the icon.
832
+ """
833
+ im : Image .Image = None
834
+
835
+ file : QFile = QFile (filepath )
836
+ success : bool = file .open (
837
+ QIODeviceBase .OpenModeFlag .ReadOnly , QFileDevice .Permission .ReadUser
838
+ )
839
+ if not success :
840
+ logger .error ("Couldn't render thumbnail" , filepath = filepath )
841
+ return im
842
+ document : QPdfDocument = QPdfDocument ()
843
+ document .load (file )
844
+ # Transform page_size in points to pixels with proper aspect ratio
845
+ page_size : QSizeF = document .pagePointSize (0 )
846
+ ratio_hw : float = page_size .height () / page_size .width ()
847
+ if ratio_hw >= 1 :
848
+ page_size *= size / page_size .height ()
849
+ else :
850
+ page_size *= size / page_size .width ()
851
+ # Enlarge image for antialiasing
852
+ scale_factor = 2.5
853
+ page_size *= scale_factor
854
+ # Render image with no anti-aliasing for speed
855
+ render_options : QPdfDocumentRenderOptions = QPdfDocumentRenderOptions ()
856
+ render_options .setRenderFlags (
857
+ QPdfDocumentRenderOptions .RenderFlag .TextAliased
858
+ | QPdfDocumentRenderOptions .RenderFlag .ImageAliased
859
+ | QPdfDocumentRenderOptions .RenderFlag .PathAliased
860
+ )
861
+ # Convert QImage to PIL Image
862
+ qimage : QImage = document .render (0 , page_size .toSize (), render_options )
863
+ buffer : QBuffer = QBuffer ()
864
+ buffer .open (QBuffer .OpenModeFlag .ReadWrite )
865
+ try :
866
+ qimage .save (buffer , "PNG" )
867
+ im = Image .open (BytesIO (buffer .buffer ().data ()))
868
+ finally :
869
+ buffer .close ()
870
+ # Replace transparent pixels with white (otherwise Background defaults to transparent)
871
+ return replace_transparent_pixels (im )
872
+
815
873
def _text_thumb (self , filepath : Path ) -> Image .Image :
816
874
"""Render a thumbnail for a plaintext file.
817
875
@@ -959,17 +1017,17 @@ def render(
959
1017
else :
960
1018
image = self ._image_thumb (_filepath )
961
1019
# Videos =======================================================
962
- if MediaCategories .is_ext_in_category (
1020
+ elif MediaCategories .is_ext_in_category (
963
1021
ext , MediaCategories .VIDEO_TYPES , mime_fallback = True
964
1022
):
965
1023
image = self ._video_thumb (_filepath )
966
1024
# Plain Text ===================================================
967
- if MediaCategories .is_ext_in_category (
1025
+ elif MediaCategories .is_ext_in_category (
968
1026
ext , MediaCategories .PLAINTEXT_TYPES , mime_fallback = True
969
1027
):
970
1028
image = self ._text_thumb (_filepath )
971
1029
# Fonts ========================================================
972
- if MediaCategories .is_ext_in_category (
1030
+ elif MediaCategories .is_ext_in_category (
973
1031
ext , MediaCategories .FONT_TYPES , mime_fallback = True
974
1032
):
975
1033
if is_grid_thumb :
@@ -979,23 +1037,26 @@ def render(
979
1037
# Large (Full Alphabet) Preview
980
1038
image = self ._font_long_thumb (_filepath , adj_size )
981
1039
# Audio ========================================================
982
- if MediaCategories .is_ext_in_category (
1040
+ elif MediaCategories .is_ext_in_category (
983
1041
ext , MediaCategories .AUDIO_TYPES , mime_fallback = True
984
1042
):
985
1043
image = self ._audio_album_thumb (_filepath , ext )
986
1044
if image is None :
987
1045
image = self ._audio_waveform_thumb (_filepath , ext , adj_size , pixel_ratio )
988
1046
if image is not None :
989
1047
image = self ._apply_overlay_color (image , UiColor .GREEN )
990
-
991
- # Blender ===========================================================
992
- if MediaCategories .is_ext_in_category (
1048
+ # Blender ======================================================
1049
+ elif MediaCategories .is_ext_in_category (
993
1050
ext , MediaCategories .BLENDER_TYPES , mime_fallback = True
994
1051
):
995
1052
image = self ._blender (_filepath )
996
-
1053
+ # PDF ==========================================================
1054
+ elif MediaCategories .is_ext_in_category (
1055
+ ext , MediaCategories .PDF_TYPES , mime_fallback = True
1056
+ ):
1057
+ image = self ._pdf_thumb (_filepath , adj_size )
997
1058
# VTF ==========================================================
998
- if MediaCategories .is_ext_in_category (
1059
+ elif MediaCategories .is_ext_in_category (
999
1060
ext , MediaCategories .SOURCE_ENGINE_TYPES , mime_fallback = True
1000
1061
):
1001
1062
image = self ._source_engine (_filepath )
0 commit comments