diff --git a/hydrus/client/caches/ClientCaches.py b/hydrus/client/caches/ClientCaches.py index e505182d..1c40a885 100644 --- a/hydrus/client/caches/ClientCaches.py +++ b/hydrus/client/caches/ClientCaches.py @@ -744,7 +744,7 @@ class ThumbnailCache( object ): self._special_thumbs = {} - names = [ 'hydrus', 'pdf', 'psd', 'clip', 'sai', 'krita', 'audio', 'video', 'zip' ] + names = [ 'hydrus', 'pdf', 'psd', 'clip', 'sai', 'krita', 'svg', 'audio', 'video', 'zip' ] bounding_dimensions = self._controller.options[ 'thumbnail_dimensions' ] thumbnail_scale_type = self._controller.new_options.GetInteger( 'thumbnail_scale_type' ) @@ -862,6 +862,7 @@ class ThumbnailCache( object ): elif mime == HC.APPLICATION_PSD: return self._special_thumbs[ 'psd' ] elif mime == HC.APPLICATION_SAI2: return self._special_thumbs[ 'sai' ] elif mime == HC.APPLICATION_KRITA: return self._special_thumbs[ 'krita' ] + elif mime == HC.IMAGE_SVG: return self._special_thumbs[ 'svg' ] elif mime in HC.ARCHIVES: return self._special_thumbs[ 'zip' ] else: return self._special_thumbs[ 'hydrus' ] diff --git a/hydrus/core/HydrusConstants.py b/hydrus/core/HydrusConstants.py index d40ac979..9d95b519 100644 --- a/hydrus/core/HydrusConstants.py +++ b/hydrus/core/HydrusConstants.py @@ -715,12 +715,13 @@ APPLICATION_WINDOWS_EXE = 52 AUDIO_WAVPACK = 53 APPLICATION_SAI2 = 54 APPLICATION_KRITA = 55 +IMAGE_SVG = 56 APPLICATION_OCTET_STREAM = 100 APPLICATION_UNKNOWN = 101 GENERAL_FILETYPES = { GENERAL_APPLICATION, GENERAL_AUDIO, GENERAL_IMAGE, GENERAL_VIDEO, GENERAL_ANIMATION } -SEARCHABLE_MIMES = { IMAGE_JPEG, IMAGE_PNG, IMAGE_APNG, IMAGE_GIF, IMAGE_WEBP, IMAGE_TIFF, IMAGE_ICON, APPLICATION_FLASH, VIDEO_AVI, VIDEO_FLV, VIDEO_MOV, VIDEO_MP4, VIDEO_MKV, VIDEO_REALMEDIA, VIDEO_WEBM, VIDEO_OGV, VIDEO_MPEG, APPLICATION_CLIP, APPLICATION_PSD, APPLICATION_SAI2, APPLICATION_KRITA, APPLICATION_PDF, APPLICATION_ZIP, APPLICATION_RAR, APPLICATION_7Z, AUDIO_M4A, AUDIO_MP3, AUDIO_REALMEDIA, AUDIO_OGG, AUDIO_FLAC, AUDIO_WAVE, AUDIO_TRUEAUDIO, AUDIO_WMA, VIDEO_WMV, AUDIO_MKV, AUDIO_MP4, AUDIO_WAVPACK } +SEARCHABLE_MIMES = { IMAGE_JPEG, IMAGE_PNG, IMAGE_APNG, IMAGE_GIF, IMAGE_WEBP, IMAGE_TIFF, IMAGE_ICON, IMAGE_SVG, APPLICATION_FLASH, VIDEO_AVI, VIDEO_FLV, VIDEO_MOV, VIDEO_MP4, VIDEO_MKV, VIDEO_REALMEDIA, VIDEO_WEBM, VIDEO_OGV, VIDEO_MPEG, APPLICATION_CLIP, APPLICATION_PSD, APPLICATION_SAI2, APPLICATION_KRITA, APPLICATION_PDF, APPLICATION_ZIP, APPLICATION_RAR, APPLICATION_7Z, AUDIO_M4A, AUDIO_MP3, AUDIO_REALMEDIA, AUDIO_OGG, AUDIO_FLAC, AUDIO_WAVE, AUDIO_TRUEAUDIO, AUDIO_WMA, VIDEO_WMV, AUDIO_MKV, AUDIO_MP4, AUDIO_WAVPACK } STORABLE_MIMES = set( SEARCHABLE_MIMES ).union( { APPLICATION_HYDRUS_UPDATE_CONTENT, APPLICATION_HYDRUS_UPDATE_DEFINITIONS } ) @@ -736,7 +737,7 @@ AUDIO = { AUDIO_M4A, AUDIO_MP3, AUDIO_OGG, AUDIO_FLAC, AUDIO_WAVE, AUDIO_WMA, AU VIDEO = { VIDEO_AVI, VIDEO_FLV, VIDEO_MOV, VIDEO_MP4, VIDEO_WMV, VIDEO_MKV, VIDEO_REALMEDIA, VIDEO_WEBM, VIDEO_OGV, VIDEO_MPEG } -APPLICATIONS = { APPLICATION_FLASH, APPLICATION_PSD, APPLICATION_CLIP, APPLICATION_SAI2, APPLICATION_KRITA, APPLICATION_PDF, APPLICATION_ZIP, APPLICATION_RAR, APPLICATION_7Z } +APPLICATIONS = { IMAGE_SVG, APPLICATION_FLASH, APPLICATION_PSD, APPLICATION_CLIP, APPLICATION_SAI2, APPLICATION_KRITA, APPLICATION_PDF, APPLICATION_ZIP, APPLICATION_RAR, APPLICATION_7Z } general_mimetypes_to_mime_groups = { GENERAL_APPLICATION : APPLICATIONS, @@ -761,7 +762,7 @@ MIMES_THAT_MAY_HAVE_AUDIO = tuple( list( MIMES_THAT_DEFINITELY_HAVE_AUDIO ) + li ARCHIVES = { APPLICATION_ZIP, APPLICATION_HYDRUS_ENCRYPTED_ZIP, APPLICATION_RAR, APPLICATION_7Z } -MIMES_WITH_THUMBNAILS = set( IMAGES ).union( ANIMATIONS ).union( VIDEO ).union( { APPLICATION_FLASH, APPLICATION_CLIP, APPLICATION_PSD, APPLICATION_KRITA } ) +MIMES_WITH_THUMBNAILS = set( IMAGES ).union( ANIMATIONS ).union( VIDEO ).union( { IMAGE_SVG, APPLICATION_FLASH, APPLICATION_CLIP, APPLICATION_PSD, APPLICATION_KRITA } ) FILES_THAT_CAN_HAVE_ICC_PROFILE = { IMAGE_JPEG, IMAGE_PNG, IMAGE_GIF, IMAGE_TIFF } @@ -787,6 +788,7 @@ mime_enum_lookup = { 'image/webp' : IMAGE_WEBP, 'image/tiff' : IMAGE_TIFF, 'image/x-icon' : IMAGE_ICON, + 'image/svg+xml': IMAGE_SVG, 'image/vnd.microsoft.icon' : IMAGE_ICON, 'image' : IMAGES, 'application/x-shockwave-flash' : APPLICATION_FLASH, @@ -846,6 +848,7 @@ mime_string_lookup = { IMAGE_WEBP : 'webp', IMAGE_TIFF : 'tiff', IMAGE_ICON : 'icon', + IMAGE_SVG : 'svg', APPLICATION_FLASH : 'flash', APPLICATION_OCTET_STREAM : 'application/octet-stream', APPLICATION_YAML : 'yaml', @@ -907,6 +910,7 @@ mime_mimetype_string_lookup = { IMAGE_WEBP : 'image/webp', IMAGE_TIFF : 'image/tiff', IMAGE_ICON : 'image/x-icon', + IMAGE_SVG : 'image/svg+xml', APPLICATION_FLASH : 'application/x-shockwave-flash', APPLICATION_OCTET_STREAM : 'application/octet-stream', APPLICATION_YAML : 'application/x-yaml', @@ -969,6 +973,7 @@ mime_ext_lookup = { IMAGE_WEBP : '.webp', IMAGE_TIFF : '.tiff', IMAGE_ICON : '.ico', + IMAGE_SVG : '.svg', APPLICATION_FLASH : '.swf', APPLICATION_OCTET_STREAM : '.bin', APPLICATION_YAML : '.yaml', diff --git a/hydrus/core/HydrusFileHandling.py b/hydrus/core/HydrusFileHandling.py index 95b09119..fb2ca2f5 100644 --- a/hydrus/core/HydrusFileHandling.py +++ b/hydrus/core/HydrusFileHandling.py @@ -5,6 +5,7 @@ import struct from hydrus.core import HydrusAudioHandling from hydrus.core import HydrusClipHandling from hydrus.core import HydrusKritaHandling +from hydrus.core import HydrusSVGHandling from hydrus.core import HydrusConstants as HC from hydrus.core import HydrusData from hydrus.core import HydrusDocumentHandling @@ -156,7 +157,7 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames, finally: HydrusTemp.CleanUpTempPath( os_file_handle, temp_path ) - + elif mime == HC.APPLICATION_KRITA: @@ -178,7 +179,23 @@ def GenerateThumbnailBytes( path, target_resolution, mime, duration, num_frames, HydrusTemp.CleanUpTempPath( os_file_handle, temp_path ) + + elif mime == HC.IMAGE_SVG: + try: + + thumbnail_bytes = HydrusSVGHandling.GenerateThumbnailBytesFromSVGPath( path, target_resolution, clip_rect = clip_rect ) + + except Exception as e: + + HydrusData.Print( 'Problem generating thumbnail for "{}":'.format( path ) ) + HydrusData.PrintException( e ) + + thumb_path = os.path.join( HC.STATIC_DIR, 'svg.png' ) + + thumbnail_bytes = HydrusImageHandling.GenerateThumbnailBytesFromStaticImagePath( thumb_path, target_resolution, HC.IMAGE_PNG, clip_rect = clip_rect ) + + elif mime == HC.APPLICATION_FLASH: ( os_file_handle, temp_path ) = HydrusTemp.GetTempPath() @@ -344,6 +361,10 @@ def GetFileInfo( path, mime = None, ok_to_look_for_hydrus_updates = False ): ( width, height ) = HydrusKritaHandling.GetKraProperties( path ) + elif mime == HC.IMAGE_SVG: + + ( width, height ) = HydrusSVGHandling.GetSVGResolution( path ) + elif mime == HC.APPLICATION_FLASH: ( ( width, height ), duration, num_frames ) = HydrusFlashHandling.GetFlashProperties( path ) @@ -490,6 +511,9 @@ def GetMime( path, ok_to_look_for_hydrus_updates = False ): return HC.TEXT_HTML + if HydrusText.LooksLikeSVG( bit_to_check ): + + return HC.IMAGE_SVG # it is important this goes at the end, because ffmpeg has a billion false positives! # for instance, it once thought some hydrus update files were mpegs diff --git a/hydrus/core/HydrusImageHandling.py b/hydrus/core/HydrusImageHandling.py index ffd18d0f..efa9881b 100644 --- a/hydrus/core/HydrusImageHandling.py +++ b/hydrus/core/HydrusImageHandling.py @@ -879,6 +879,7 @@ def GetThumbnailResolutionAndClipRegion( image_resolution: typing.Tuple[ int, in bounding_height = int( bounding_height * thumbnail_dpr ) bounding_width = int( bounding_width * thumbnail_dpr ) + # TODO SVG thumbs should always scale up to the bounding dimensions if thumbnail_scale_type == THUMBNAIL_SCALE_DOWN_ONLY: diff --git a/hydrus/core/HydrusPaths.py b/hydrus/core/HydrusPaths.py index d2a1b475..2e1c819d 100644 --- a/hydrus/core/HydrusPaths.py +++ b/hydrus/core/HydrusPaths.py @@ -25,6 +25,7 @@ mimes_to_default_thumbnail_paths[ HC.APPLICATION_PSD ] = os.path.join( HC.STATIC mimes_to_default_thumbnail_paths[ HC.APPLICATION_CLIP ] = os.path.join( HC.STATIC_DIR, 'clip.png' ) mimes_to_default_thumbnail_paths[ HC.APPLICATION_SAI2 ] = os.path.join( HC.STATIC_DIR, 'sai.png' ) mimes_to_default_thumbnail_paths[ HC.APPLICATION_KRITA ] = os.path.join( HC.STATIC_DIR, 'krita.png' ) +mimes_to_default_thumbnail_paths[ HC.IMAGE_SVG ] = os.path.join( HC.STATIC_DIR, 'svg.png' ) for mime in HC.AUDIO: diff --git a/hydrus/core/HydrusSVGHandling.py b/hydrus/core/HydrusSVGHandling.py new file mode 100644 index 00000000..e77b9d00 --- /dev/null +++ b/hydrus/core/HydrusSVGHandling.py @@ -0,0 +1,65 @@ +from qtpy import QtSvg +from qtpy import QtGui as QG +from qtpy import QtCore as QC + +from hydrus.core import HydrusExceptions +from hydrus.core import HydrusImageHandling + +from hydrus.client.gui import ClientGUIFunctions + +def LoadSVGRenderer(path: str): + + renderer = QtSvg.QSvgRenderer(); + + try: + renderer.load(path) + + except: + + raise HydrusExceptions.DamagedOrUnusualFileException('Could not load SVG file.') + + if not renderer.isValid(): + + raise HydrusExceptions.DamagedOrUnusualFileException('SVG file is invalid!') + + return renderer + +def GenerateThumbnailBytesFromSVGPath(path: str, target_resolution: tuple[int, int], clip_rect = None) -> bytes: + + # TODO handle clipping + + ( target_width, target_height ) = target_resolution + + renderer = LoadSVGRenderer(path) + + # Seems to help for some weird floating point dimension SVGs + renderer.setAspectRatioMode(QC.Qt.AspectRatioMode.KeepAspectRatio) + + try: + + qt_image = QG.QImage( target_width, target_height, QG.QImage.Format_RGBA8888 ) + + qt_image.fill( QC.Qt.transparent ) + + painter = QG.QPainter(qt_image) + + renderer.render(painter) + + numpy_image = ClientGUIFunctions.ConvertQtImageToNumPy(qt_image) + + painter.end() + + return HydrusImageHandling.GenerateThumbnailBytesNumPy(numpy_image) + + except: + + raise HydrusExceptions.UnsupportedFileException() + + +def GetSVGResolution( path: str ): + + renderer = LoadSVGRenderer(path) + + resolution = renderer.defaultSize().toTuple() + + return resolution \ No newline at end of file diff --git a/hydrus/core/HydrusText.py b/hydrus/core/HydrusText.py index 4504e525..77ec56c4 100644 --- a/hydrus/core/HydrusText.py +++ b/hydrus/core/HydrusText.py @@ -96,11 +96,33 @@ def LooksLikeHTML( file_data ): if isinstance( file_data, bytes ): - search_elements = ( b'