2828from PIL .Image import DecompressionBombError
2929from pillow_heif import register_avif_opener , register_heif_opener
3030from 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+ )
3242from PySide6 .QtGui import QGuiApplication , QImage , QPainter , QPixmap
43+ from PySide6 .QtPdf import QPdfDocument , QPdfDocumentRenderOptions
3344from PySide6 .QtSvg import QSvgRenderer
3445from src .core .constants import FONT_SAMPLE_SIZES , FONT_SAMPLE_TEXT
3546from src .core .media_types import MediaCategories , MediaType
3950from src .qt .helpers .color_overlay import theme_fg_overlay
4051from src .qt .helpers .file_tester import is_readable_video
4152from src .qt .helpers .gradient import four_corner_gradient
53+ from src .qt .helpers .image_effects import replace_transparent_pixels
4254from src .qt .helpers .text_wrapper import wrap_full_text
4355from src .qt .helpers .vendored .pydub .audio_segment import ( # type: ignore
4456 _AudioSegment as AudioSegment ,
@@ -812,6 +824,52 @@ def _model_stl_thumb(self, filepath: Path, size: int) -> Image.Image:
812824
813825 return im
814826
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+
815873 def _text_thumb (self , filepath : Path ) -> Image .Image :
816874 """Render a thumbnail for a plaintext file.
817875
@@ -959,17 +1017,17 @@ def render(
9591017 else :
9601018 image = self ._image_thumb (_filepath )
9611019 # Videos =======================================================
962- if MediaCategories .is_ext_in_category (
1020+ elif MediaCategories .is_ext_in_category (
9631021 ext , MediaCategories .VIDEO_TYPES , mime_fallback = True
9641022 ):
9651023 image = self ._video_thumb (_filepath )
9661024 # Plain Text ===================================================
967- if MediaCategories .is_ext_in_category (
1025+ elif MediaCategories .is_ext_in_category (
9681026 ext , MediaCategories .PLAINTEXT_TYPES , mime_fallback = True
9691027 ):
9701028 image = self ._text_thumb (_filepath )
9711029 # Fonts ========================================================
972- if MediaCategories .is_ext_in_category (
1030+ elif MediaCategories .is_ext_in_category (
9731031 ext , MediaCategories .FONT_TYPES , mime_fallback = True
9741032 ):
9751033 if is_grid_thumb :
@@ -979,23 +1037,26 @@ def render(
9791037 # Large (Full Alphabet) Preview
9801038 image = self ._font_long_thumb (_filepath , adj_size )
9811039 # Audio ========================================================
982- if MediaCategories .is_ext_in_category (
1040+ elif MediaCategories .is_ext_in_category (
9831041 ext , MediaCategories .AUDIO_TYPES , mime_fallback = True
9841042 ):
9851043 image = self ._audio_album_thumb (_filepath , ext )
9861044 if image is None :
9871045 image = self ._audio_waveform_thumb (_filepath , ext , adj_size , pixel_ratio )
9881046 if image is not None :
9891047 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 (
9931050 ext , MediaCategories .BLENDER_TYPES , mime_fallback = True
9941051 ):
9951052 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 )
9971058 # VTF ==========================================================
998- if MediaCategories .is_ext_in_category (
1059+ elif MediaCategories .is_ext_in_category (
9991060 ext , MediaCategories .SOURCE_ENGINE_TYPES , mime_fallback = True
10001061 ):
10011062 image = self ._source_engine (_filepath )
0 commit comments