2019-07-24 21:39:02 +00:00
import hashlib
2019-05-08 21:06:42 +00:00
import io
2022-11-09 22:24:07 +00:00
import os
2022-07-27 21:18:33 +00:00
import typing
2019-05-08 21:06:42 +00:00
import numpy
import numpy . core . multiarray # important this comes before cv!
2020-07-29 20:52:44 +00:00
import warnings
2019-09-05 00:05:32 +00:00
try :
2020-05-13 19:03:16 +00:00
# more hidden imports for pyinstaller
import numpy . random . common # pylint: disable=E0401
import numpy . random . bounded_integers # pylint: disable=E0401
import numpy . random . entropy # pylint: disable=E0401
2019-09-05 00:05:32 +00:00
except :
pass # old version of numpy, screw it
2017-11-08 22:07:12 +00:00
from PIL import ImageFile as PILImageFile
2013-02-19 00:11:43 +00:00
from PIL import Image as PILImage
2021-12-01 22:12:16 +00:00
from PIL import ImageCms as PILImageCms
2020-07-29 20:52:44 +00:00
2023-08-05 20:00:29 +00:00
try :
from pillow_heif import register_heif_opener
from pillow_heif import register_avif_opener
register_heif_opener ( thumbnails = False )
register_avif_opener ( thumbnails = False )
HEIF_OK = True
except :
HEIF_OK = False
2023-08-02 10:37:45 +00:00
2020-07-29 20:52:44 +00:00
from hydrus . core import HydrusConstants as HC
2020-04-22 21:00:35 +00:00
from hydrus . core import HydrusData
2020-07-29 20:52:44 +00:00
from hydrus . core import HydrusExceptions
2020-04-22 21:00:35 +00:00
from hydrus . core import HydrusGlobals as HG
from hydrus . core import HydrusPaths
2021-10-27 21:12:33 +00:00
from hydrus . core import HydrusTemp
2023-07-16 13:23:08 +00:00
from hydrus . core import HydrusPSDHandling
2017-10-04 17:51:58 +00:00
2023-09-23 19:13:21 +00:00
from hydrus . external import blurhash
2021-12-01 22:12:16 +00:00
PIL_SRGB_PROFILE = PILImageCms . createProfile ( ' sRGB ' )
2019-09-25 21:34:18 +00:00
def EnableLoadTruncatedImages ( ) :
if hasattr ( PILImageFile , ' LOAD_TRUNCATED_IMAGES ' ) :
# this can now cause load hangs due to the trunc load code adding infinite fake EOFs to the file stream, wew lad
# hence debug only
PILImageFile . LOAD_TRUNCATED_IMAGES = True
return True
else :
return False
2017-11-08 22:07:12 +00:00
2023-08-02 21:11:08 +00:00
2020-05-27 21:27:52 +00:00
if not hasattr ( PILImage , ' DecompressionBombError ' ) :
# super old versions don't have this, so let's just make a stub, wew
2022-10-12 20:18:22 +00:00
class DBEStub ( Exception ) :
2020-05-27 21:27:52 +00:00
pass
2022-10-12 20:18:22 +00:00
PILImage . DecompressionBombError = DBEStub
2020-05-27 21:27:52 +00:00
2023-08-02 21:11:08 +00:00
2017-10-11 17:38:14 +00:00
if not hasattr ( PILImage , ' DecompressionBombWarning ' ) :
# super old versions don't have this, so let's just make a stub, wew
2022-10-12 20:18:22 +00:00
class DBWStub ( Exception ) :
2017-10-11 17:38:14 +00:00
pass
2022-10-12 20:18:22 +00:00
PILImage . DecompressionBombWarning = DBWStub
2017-10-11 17:38:14 +00:00
2023-08-02 21:11:08 +00:00
2017-10-04 17:51:58 +00:00
warnings . simplefilter ( ' ignore ' , PILImage . DecompressionBombWarning )
2020-05-27 21:27:52 +00:00
warnings . simplefilter ( ' ignore ' , PILImage . DecompressionBombError )
2013-02-19 00:11:43 +00:00
2023-08-02 21:11:08 +00:00
# PIL moaning about weirdo TIFFs
warnings . filterwarnings ( " ignore " , " (Possibly )?corrupt EXIF data " , UserWarning )
2023-08-09 21:12:17 +00:00
warnings . filterwarnings ( " ignore " , " Metadata Warning " , UserWarning )
2023-08-02 21:11:08 +00:00
2018-04-11 22:30:40 +00:00
OLD_PIL_MAX_IMAGE_PIXELS = PILImage . MAX_IMAGE_PIXELS
PILImage . MAX_IMAGE_PIXELS = None # this turns off decomp check entirely, wew
2023-09-13 18:26:31 +00:00
PIL_ONLY_MIMETYPES = { HC . ANIMATION_GIF , HC . IMAGE_ICON , HC . IMAGE_WEBP , HC . IMAGE_QOI , HC . IMAGE_BMP } . union ( HC . PIL_HEIF_MIMES )
2019-05-15 20:35:00 +00:00
2019-05-08 21:06:42 +00:00
try :
import cv2
if cv2 . __version__ . startswith ( ' 2 ' ) :
2022-01-19 21:28:59 +00:00
CV_IMREAD_FLAGS_PNG = cv2 . CV_LOAD_IMAGE_UNCHANGED
CV_IMREAD_FLAGS_JPEG = CV_IMREAD_FLAGS_PNG
CV_IMREAD_FLAGS_WEIRD = CV_IMREAD_FLAGS_PNG
2019-05-08 21:06:42 +00:00
CV_JPEG_THUMBNAIL_ENCODE_PARAMS = [ ]
CV_PNG_THUMBNAIL_ENCODE_PARAMS = [ ]
else :
2022-01-19 21:28:59 +00:00
# allows alpha channel
CV_IMREAD_FLAGS_PNG = cv2 . IMREAD_UNCHANGED
# this preserves colour info but does EXIF reorientation and flipping
CV_IMREAD_FLAGS_JPEG = cv2 . IMREAD_ANYDEPTH | cv2 . IMREAD_ANYCOLOR
# this seems to allow weirdass tiffs to load as non greyscale, although the LAB conversion 'whitepoint' or whatever can be wrong
2022-03-30 20:28:13 +00:00
CV_IMREAD_FLAGS_WEIRD = CV_IMREAD_FLAGS_PNG
2019-05-08 21:06:42 +00:00
CV_JPEG_THUMBNAIL_ENCODE_PARAMS = [ cv2 . IMWRITE_JPEG_QUALITY , 92 ]
CV_PNG_THUMBNAIL_ENCODE_PARAMS = [ cv2 . IMWRITE_PNG_COMPRESSION , 9 ]
OPENCV_OK = True
except :
OPENCV_OK = False
2023-08-09 21:12:17 +00:00
2022-02-02 22:14:01 +00:00
def MakeClipRectFit ( image_resolution , clip_rect ) :
( im_width , im_height ) = image_resolution
( x , y , clip_width , clip_height ) = clip_rect
x = max ( 0 , x )
y = max ( 0 , y )
clip_width = min ( clip_width , im_width )
clip_height = min ( clip_height , im_height )
if x + clip_width > im_width :
x = im_width - clip_width
if y + clip_height > im_height :
y = im_height - clip_height
return ( x , y , clip_width , clip_height )
def ClipNumPyImage ( numpy_image : numpy . array , clip_rect ) :
if len ( numpy_image . shape ) == 3 :
( im_height , im_width , depth ) = numpy_image . shape
else :
( im_height , im_width ) = numpy_image . shape
( x , y , clip_width , clip_height ) = MakeClipRectFit ( ( im_width , im_height ) , clip_rect )
return numpy_image [ y : y + clip_height , x : x + clip_width ]
2023-09-13 18:26:31 +00:00
2022-02-02 22:14:01 +00:00
def ClipPILImage ( pil_image : PILImage . Image , clip_rect ) :
( x , y , clip_width , clip_height ) = MakeClipRectFit ( pil_image . size , clip_rect )
return pil_image . crop ( box = ( x , y , x + clip_width , y + clip_height ) )
2023-09-13 18:26:31 +00:00
2021-12-01 22:12:16 +00:00
def DequantizeNumPyImage ( numpy_image : numpy . array ) - > numpy . array :
# OpenCV loads images in BGR, and we want to normalise to RGB in general
if numpy_image . dtype == ' uint16 ' :
numpy_image = numpy . array ( numpy_image / / 256 , dtype = ' uint8 ' )
shape = numpy_image . shape
2016-06-29 19:55:46 +00:00
2021-12-01 22:12:16 +00:00
if len ( shape ) == 2 :
# monochrome image
convert = cv2 . COLOR_GRAY2RGB
else :
2016-06-29 19:55:46 +00:00
2021-12-01 22:12:16 +00:00
( im_y , im_x , depth ) = shape
if depth == 4 :
2016-06-29 19:55:46 +00:00
2021-12-01 22:12:16 +00:00
convert = cv2 . COLOR_BGRA2RGBA
2016-06-29 19:55:46 +00:00
else :
2021-12-01 22:12:16 +00:00
convert = cv2 . COLOR_BGR2RGB
numpy_image = cv2 . cvtColor ( numpy_image , convert )
return numpy_image
def DequantizePILImage ( pil_image : PILImage . Image ) - > PILImage . Image :
if HasICCProfile ( pil_image ) :
try :
pil_image = NormaliseICCProfilePILImageToSRGB ( pil_image )
except Exception as e :
HydrusData . ShowException ( e )
HydrusData . ShowText ( ' Failed to normalise image ICC profile. ' )
2016-06-29 19:55:46 +00:00
2021-12-22 22:31:23 +00:00
pil_image = NormalisePILImageToRGB ( pil_image )
2021-12-01 22:12:16 +00:00
2016-06-29 19:55:46 +00:00
return pil_image
2021-12-01 22:12:16 +00:00
def GenerateNumPyImage ( path , mime , force_pil = False ) - > numpy . array :
2019-05-08 21:06:42 +00:00
if HG . media_load_report_mode :
HydrusData . ShowText ( ' Loading media: ' + path )
2023-09-09 19:39:09 +00:00
if mime == HC . APPLICATION_PSD :
if HG . media_load_report_mode :
HydrusData . ShowText ( ' Loading PSD ' )
2023-09-13 18:26:31 +00:00
2023-09-09 19:39:09 +00:00
pil_image = HydrusPSDHandling . MergedPILImageFromPSD ( path )
pil_image = DequantizePILImage ( pil_image )
numpy_image = GenerateNumPyImageFromPILImage ( pil_image )
return StripOutAnyUselessAlphaChannel ( numpy_image )
2019-05-08 21:06:42 +00:00
if not OPENCV_OK :
force_pil = True
2021-12-01 22:12:16 +00:00
if not force_pil :
2021-12-22 22:31:23 +00:00
try :
2021-12-01 22:12:16 +00:00
2021-12-22 22:31:23 +00:00
pil_image = RawOpenPILImage ( path )
2021-12-01 22:12:16 +00:00
2022-01-05 22:15:56 +00:00
try :
pil_image . verify ( )
except :
raise HydrusExceptions . UnsupportedFileException ( )
2022-01-26 21:57:04 +00:00
# I and F are some sort of 32-bit monochrome or whatever, doesn't seem to work in PIL well, with or without ICC
if pil_image . mode not in ( ' I ' , ' F ' ) :
2021-12-22 22:31:23 +00:00
2022-01-26 21:57:04 +00:00
if pil_image . mode == ' LAB ' :
2022-01-19 21:28:59 +00:00
2022-01-26 21:57:04 +00:00
force_pil = True
2022-01-19 21:28:59 +00:00
2022-01-26 21:57:04 +00:00
if HasICCProfile ( pil_image ) :
if HG . media_load_report_mode :
HydrusData . ShowText ( ' Image has ICC, so switching to PIL ' )
force_pil = True
2021-12-22 22:31:23 +00:00
2021-12-01 22:12:16 +00:00
2021-12-22 22:31:23 +00:00
except HydrusExceptions . UnsupportedFileException :
# pil had trouble, let's cross our fingers cv can do it
pass
2021-12-01 22:12:16 +00:00
2023-09-09 19:39:09 +00:00
if mime in PIL_ONLY_MIMETYPES or force_pil :
2019-05-08 21:06:42 +00:00
if HG . media_load_report_mode :
HydrusData . ShowText ( ' Loading with PIL ' )
pil_image = GeneratePILImage ( path )
numpy_image = GenerateNumPyImageFromPILImage ( pil_image )
else :
if HG . media_load_report_mode :
HydrusData . ShowText ( ' Loading with OpenCV ' )
2022-03-30 20:28:13 +00:00
if mime in ( HC . IMAGE_JPEG , HC . IMAGE_TIFF ) :
2019-05-08 21:06:42 +00:00
2022-01-19 21:28:59 +00:00
flags = CV_IMREAD_FLAGS_JPEG
elif mime == HC . IMAGE_PNG :
flags = CV_IMREAD_FLAGS_PNG
2019-05-08 21:06:42 +00:00
else :
2022-01-19 21:28:59 +00:00
flags = CV_IMREAD_FLAGS_WEIRD
2019-05-08 21:06:42 +00:00
numpy_image = cv2 . imread ( path , flags = flags )
2021-12-01 22:12:16 +00:00
if numpy_image is None : # doesn't support some random stuff
2019-05-08 21:06:42 +00:00
if HG . media_load_report_mode :
HydrusData . ShowText ( ' OpenCV Failed, loading with PIL ' )
pil_image = GeneratePILImage ( path )
numpy_image = GenerateNumPyImageFromPILImage ( pil_image )
else :
2021-12-01 22:12:16 +00:00
numpy_image = DequantizeNumPyImage ( numpy_image )
2019-05-08 21:06:42 +00:00
2022-12-14 22:22:11 +00:00
numpy_image = StripOutAnyUselessAlphaChannel ( numpy_image )
2022-02-02 22:14:01 +00:00
2019-05-08 21:06:42 +00:00
return numpy_image
2021-12-01 22:12:16 +00:00
def GenerateNumPyImageFromPILImage ( pil_image : PILImage . Image ) - > numpy . array :
2019-05-08 21:06:42 +00:00
2023-09-13 18:26:31 +00:00
# this seems to magically work, I guess asarray either has a match for Image or Image provides some common shape/datatype properties that it can hook into
return numpy . asarray ( pil_image )
# old method:
'''
2019-05-08 21:06:42 +00:00
( w , h ) = pil_image . size
2022-01-05 22:15:56 +00:00
try :
s = pil_image . tobytes ( )
except OSError as e : # e.g. OSError: unrecognized data stream contents when reading image file
raise HydrusExceptions . UnsupportedFileException ( str ( e ) )
2019-05-08 21:06:42 +00:00
2021-12-01 22:12:16 +00:00
depth = len ( s ) / / ( w * h )
2019-05-08 21:06:42 +00:00
2021-12-01 22:12:16 +00:00
return numpy . fromstring ( s , dtype = ' uint8 ' ) . reshape ( ( h , w , depth ) )
2023-09-13 18:26:31 +00:00
'''
2015-10-21 21:53:10 +00:00
2023-09-13 18:26:31 +00:00
2021-12-01 22:12:16 +00:00
def GeneratePILImage ( path , dequantize = True ) - > PILImage . Image :
2015-10-21 21:53:10 +00:00
2021-12-01 22:12:16 +00:00
pil_image = RawOpenPILImage ( path )
if pil_image is None :
2017-11-01 20:37:39 +00:00
2021-12-01 22:12:16 +00:00
raise Exception ( ' The file at {} could not be rendered! ' . format ( path ) )
2017-11-01 20:37:39 +00:00
2022-05-04 21:40:27 +00:00
pil_image = RotateEXIFPILImage ( pil_image )
2021-12-01 22:12:16 +00:00
if dequantize :
2015-10-21 21:53:10 +00:00
2021-12-01 22:12:16 +00:00
# note this destroys animated gifs atm, it collapses down to one frame
pil_image = DequantizePILImage ( pil_image )
2015-10-21 21:53:10 +00:00
return pil_image
2021-12-01 22:12:16 +00:00
def GeneratePILImageFromNumPyImage ( numpy_image : numpy . array ) - > PILImage . Image :
2014-06-25 20:37:06 +00:00
2021-12-01 22:12:16 +00:00
# I'll leave this here as a neat artifact, but I really shouldn't ever be making a PIL from a cv2 image. the only PIL benefits are the .info dict, which this won't generate
2014-06-25 20:37:06 +00:00
2021-12-01 22:12:16 +00:00
if len ( numpy_image . shape ) == 2 :
( h , w ) = numpy_image . shape
2016-10-12 21:52:50 +00:00
2021-12-01 22:12:16 +00:00
format = ' L '
else :
2016-10-12 21:52:50 +00:00
2021-12-01 22:12:16 +00:00
( h , w , depth ) = numpy_image . shape
2016-10-12 21:52:50 +00:00
2021-12-01 22:12:16 +00:00
if depth == 1 :
format = ' L '
elif depth == 2 :
format = ' LA '
elif depth == 3 :
format = ' RGB '
elif depth == 4 :
format = ' RGBA '
2016-10-12 21:52:50 +00:00
2014-06-25 20:37:06 +00:00
2019-01-09 22:59:03 +00:00
pil_image = PILImage . frombytes ( format , ( w , h ) , numpy_image . data . tobytes ( ) )
2014-06-25 20:37:06 +00:00
return pil_image
2023-09-23 19:13:21 +00:00
def GenerateThumbnailNumPyFromStaticImagePath ( path , target_resolution , mime , clip_rect = None ) :
2019-05-08 21:06:42 +00:00
2019-05-15 20:35:00 +00:00
if OPENCV_OK :
2019-05-08 21:06:42 +00:00
numpy_image = GenerateNumPyImage ( path , mime )
2022-02-02 22:14:01 +00:00
if clip_rect is not None :
numpy_image = ClipNumPyImage ( numpy_image , clip_rect )
2019-05-08 21:06:42 +00:00
thumbnail_numpy_image = ResizeNumPyImage ( numpy_image , target_resolution )
2023-09-23 19:13:21 +00:00
return thumbnail_numpy_image
2019-05-08 21:06:42 +00:00
pil_image = GeneratePILImage ( path )
2022-03-30 20:28:13 +00:00
if clip_rect is not None :
2022-02-02 22:14:01 +00:00
pil_image = ClipPILImage ( pil_image , clip_rect )
2023-08-12 02:58:36 +00:00
thumbnail_pil_image = pil_image . resize ( target_resolution , PILImage . LANCZOS )
2019-05-08 21:06:42 +00:00
2023-09-23 19:13:21 +00:00
thumbnail_numpy_image = GenerateNumPyImageFromPILImage ( thumbnail_pil_image )
2019-05-08 21:06:42 +00:00
2023-09-23 19:13:21 +00:00
return thumbnail_numpy_image
2019-05-08 21:06:42 +00:00
2022-12-07 22:41:53 +00:00
def GenerateThumbnailBytesNumPy ( numpy_image ) - > bytes :
2019-05-08 21:06:42 +00:00
2021-05-12 20:49:20 +00:00
( im_height , im_width , depth ) = numpy_image . shape
2019-05-08 21:06:42 +00:00
2022-12-14 22:22:11 +00:00
numpy_image = StripOutAnyUselessAlphaChannel ( numpy_image )
2022-12-07 22:41:53 +00:00
2019-05-08 21:06:42 +00:00
if depth == 4 :
2022-12-07 22:41:53 +00:00
convert = cv2 . COLOR_RGBA2BGRA
2019-05-08 21:06:42 +00:00
else :
convert = cv2 . COLOR_RGB2BGR
numpy_image = cv2 . cvtColor ( numpy_image , convert )
2022-04-13 21:39:26 +00:00
( im_height , im_width , depth ) = numpy_image . shape
if depth == 4 :
2019-05-08 21:06:42 +00:00
2021-12-01 22:12:16 +00:00
ext = ' .png '
2019-05-08 21:06:42 +00:00
2021-12-01 22:12:16 +00:00
params = CV_PNG_THUMBNAIL_ENCODE_PARAMS
2019-05-08 21:06:42 +00:00
else :
2021-12-01 22:12:16 +00:00
ext = ' .jpg '
2019-05-08 21:06:42 +00:00
2021-12-01 22:12:16 +00:00
params = CV_JPEG_THUMBNAIL_ENCODE_PARAMS
2019-05-08 21:06:42 +00:00
( result_success , result_byte_array ) = cv2 . imencode ( ext , numpy_image , params )
if result_success :
thumbnail_bytes = result_byte_array . tostring ( )
return thumbnail_bytes
else :
raise HydrusExceptions . CantRenderWithCVException ( ' Thumb failed to encode! ' )
2022-12-07 22:41:53 +00:00
def GenerateThumbnailBytesPIL ( pil_image : PILImage . Image ) - > bytes :
2019-05-08 21:06:42 +00:00
f = io . BytesIO ( )
2023-02-15 21:26:44 +00:00
if PILImageHasTransparency ( pil_image ) :
2019-05-08 21:06:42 +00:00
pil_image . save ( f , ' PNG ' )
else :
pil_image . save ( f , ' JPEG ' , quality = 92 )
f . seek ( 0 )
thumbnail_bytes = f . read ( )
f . close ( )
return thumbnail_bytes
2023-09-16 20:21:22 +00:00
def GeneratePNGBytesNumPy ( numpy_image ) - > bytes :
( im_height , im_width , depth ) = numpy_image . shape
ext = ' .png '
if depth == 4 :
convert = cv2 . COLOR_RGBA2BGRA
else :
convert = cv2 . COLOR_RGB2BGR
numpy_image = cv2 . cvtColor ( numpy_image , convert )
( result_success , result_byte_array ) = cv2 . imencode ( ext , numpy_image )
if result_success :
return result_byte_array . tostring ( )
else :
raise HydrusExceptions . CantRenderWithCVException ( ' Image failed to encode! ' )
2022-07-27 21:18:33 +00:00
def GetEXIFDict ( pil_image : PILImage . Image ) - > typing . Optional [ dict ] :
2023-08-02 13:29:18 +00:00
if pil_image . format in ( ' JPEG ' , ' TIFF ' , ' PNG ' , ' WEBP ' , ' HEIF ' , ' AVIF ' ) :
2022-07-27 21:18:33 +00:00
try :
2023-08-02 13:29:18 +00:00
exif_dict = pil_image . getexif ( ) . _get_merged_dict ( )
2022-07-27 21:18:33 +00:00
2022-11-09 22:24:07 +00:00
if len ( exif_dict ) > 0 :
return exif_dict
2022-07-27 21:18:33 +00:00
except :
pass
return None
2023-08-16 20:46:51 +00:00
2021-12-01 22:12:16 +00:00
def GetICCProfileBytes ( pil_image : PILImage . Image ) - > bytes :
if HasICCProfile ( pil_image ) :
return pil_image . info [ ' icc_profile ' ]
raise HydrusExceptions . DataMissing ( ' This image has no ICC profile! ' )
def GetImagePixelHash ( path , mime ) - > bytes :
2019-07-24 21:39:02 +00:00
numpy_image = GenerateNumPyImage ( path , mime )
2023-05-24 20:44:12 +00:00
return GetImagePixelHashNumPy ( numpy_image )
def GetImagePixelHashNumPy ( numpy_image ) :
2019-07-24 21:39:02 +00:00
return hashlib . sha256 ( numpy_image . data . tobytes ( ) ) . digest ( )
2023-05-24 20:44:12 +00:00
2023-09-06 19:49:46 +00:00
def GetImageResolution ( path , mime ) :
2014-05-14 20:46:38 +00:00
2023-09-06 19:49:46 +00:00
# PIL first here, rather than numpy, as it loads image headers real quick
try :
pil_image = GeneratePILImage ( path , dequantize = False )
( width , height ) = pil_image . size
except HydrusExceptions . DamagedOrUnusualFileException :
# desperate situation
numpy_image = GenerateNumPyImage ( path , mime )
if len ( numpy_image . shape ) == 3 :
( height , width , depth ) = numpy_image . shape
else :
( height , width ) = numpy_image . shape
2023-08-09 21:12:17 +00:00
2022-03-09 22:18:23 +00:00
width = max ( width , 1 )
height = max ( height , 1 )
2023-08-16 20:46:51 +00:00
return ( width , height )
2014-05-14 20:46:38 +00:00
2023-08-16 20:46:51 +00:00
2019-05-15 20:35:00 +00:00
# bigger number is worse quality
# this is very rough and misses some finesse
def GetJPEGQuantizationQualityEstimate ( path ) :
2021-12-22 22:31:23 +00:00
try :
pil_image = RawOpenPILImage ( path )
except HydrusExceptions . UnsupportedFileException :
return ( ' unknown ' , None )
2019-05-15 20:35:00 +00:00
if hasattr ( pil_image , ' quantization ' ) :
table_arrays = list ( pil_image . quantization . values ( ) )
2019-06-19 22:08:48 +00:00
if len ( table_arrays ) == 0 :
return ( ' unknown ' , None )
2019-05-15 20:35:00 +00:00
quality = sum ( ( sum ( table_array ) for table_array in table_arrays ) )
quality / = len ( table_arrays )
if quality > = 3400 :
label = ' very low '
elif quality > = 2000 :
label = ' low '
elif quality > = 1400 :
2019-06-05 19:42:39 +00:00
label = ' medium low '
2019-05-15 20:35:00 +00:00
elif quality > = 1000 :
label = ' medium '
elif quality > = 700 :
2019-06-05 19:42:39 +00:00
label = ' medium high '
2019-05-15 20:35:00 +00:00
elif quality > = 400 :
label = ' high '
elif quality > = 200 :
label = ' very high '
else :
label = ' extremely high '
return ( label , quality )
return ( ' unknown ' , None )
2022-11-09 22:24:07 +00:00
2023-04-26 21:10:03 +00:00
def GetJpegSubsampling ( pil_image : PILImage . Image ) - > str :
from PIL import JpegImagePlugin
result = JpegImagePlugin . get_sampling ( pil_image )
subsampling_str_lookup = {
0 : ' 4:4:4 ' ,
1 : ' 4:2:2 ' ,
2 : ' 4:2:0 '
}
return subsampling_str_lookup . get ( result , ' unknown ' )
2022-11-09 22:24:07 +00:00
def GetEmbeddedFileText ( pil_image : PILImage . Image ) - > typing . Optional [ str ] :
def render_dict ( d , prefix ) :
texts = [ ]
keys = sorted ( d . keys ( ) )
for key in keys :
if key in ( ' exif ' , ' icc_profile ' ) :
continue
value = d [ key ]
if isinstance ( value , bytes ) :
continue
if isinstance ( value , dict ) :
value_string = render_dict ( value , prefix = ' ' + prefix )
if value_string is None :
continue
else :
value_string = ' {} {} ' . format ( prefix , value )
row_text = ' {} {} : ' . format ( prefix , key )
row_text + = os . linesep
row_text + = value_string
texts . append ( row_text )
if len ( texts ) > 0 :
return os . linesep . join ( texts )
else :
return None
if hasattr ( pil_image , ' info ' ) :
try :
return render_dict ( pil_image . info , ' ' )
except :
pass
return None
2019-03-20 21:22:10 +00:00
2019-05-08 21:06:42 +00:00
def GetResolutionNumPy ( numpy_image ) :
( image_height , image_width , depth ) = numpy_image . shape
return ( image_width , image_height )
2023-08-09 21:12:17 +00:00
2022-02-02 22:14:01 +00:00
THUMBNAIL_SCALE_DOWN_ONLY = 0
THUMBNAIL_SCALE_TO_FIT = 1
THUMBNAIL_SCALE_TO_FILL = 2
thumbnail_scale_str_lookup = {
THUMBNAIL_SCALE_DOWN_ONLY : ' scale down only ' ,
THUMBNAIL_SCALE_TO_FIT : ' scale to fit ' ,
THUMBNAIL_SCALE_TO_FILL : ' scale to fill '
}
2022-12-21 22:00:27 +00:00
def GetThumbnailResolutionAndClipRegion ( image_resolution : typing . Tuple [ int , int ] , bounding_dimensions : typing . Tuple [ int , int ] , thumbnail_scale_type : int , thumbnail_dpr_percent : int ) :
2022-02-02 22:14:01 +00:00
clip_rect = None
2019-01-09 22:59:03 +00:00
2019-04-03 22:45:57 +00:00
( im_width , im_height ) = image_resolution
2019-05-08 21:06:42 +00:00
( bounding_width , bounding_height ) = bounding_dimensions
2014-05-14 20:46:38 +00:00
2022-12-21 22:00:27 +00:00
if thumbnail_dpr_percent != 100 :
thumbnail_dpr = thumbnail_dpr_percent / 100
bounding_height = int ( bounding_height * thumbnail_dpr )
bounding_width = int ( bounding_width * thumbnail_dpr )
2023-09-02 19:36:17 +00:00
if im_width is None :
im_width = bounding_width
if im_height is None :
im_height = bounding_height
2022-12-21 22:00:27 +00:00
2023-07-08 18:35:49 +00:00
# TODO SVG thumbs should always scale up to the bounding dimensions
2022-12-21 22:00:27 +00:00
2022-02-02 22:14:01 +00:00
if thumbnail_scale_type == THUMBNAIL_SCALE_DOWN_ONLY :
2016-06-01 20:04:15 +00:00
2022-02-02 22:14:01 +00:00
if bounding_width > = im_width and bounding_height > = im_height :
return ( clip_rect , ( im_width , im_height ) )
2016-06-01 20:04:15 +00:00
2019-04-03 22:45:57 +00:00
width_ratio = im_width / bounding_width
height_ratio = im_height / bounding_height
thumbnail_width = bounding_width
thumbnail_height = bounding_height
2014-05-21 21:37:35 +00:00
2022-02-02 22:14:01 +00:00
if thumbnail_scale_type in ( THUMBNAIL_SCALE_DOWN_ONLY , THUMBNAIL_SCALE_TO_FIT ) :
2016-06-01 20:04:15 +00:00
2022-02-02 22:14:01 +00:00
if width_ratio > height_ratio :
thumbnail_height = im_height / width_ratio
elif height_ratio > width_ratio :
thumbnail_width = im_width / height_ratio
2016-06-01 20:04:15 +00:00
2022-02-02 22:14:01 +00:00
elif thumbnail_scale_type == THUMBNAIL_SCALE_TO_FILL :
2016-06-01 20:04:15 +00:00
2022-02-02 22:14:01 +00:00
if width_ratio == height_ratio :
# we have something that fits bounding region perfectly, no clip region required
pass
else :
clip_x = 0
clip_y = 0
clip_width = im_width
clip_height = im_height
if width_ratio > height_ratio :
clip_width = max ( int ( im_width * height_ratio / width_ratio ) , 1 )
clip_x = ( im_width - clip_width ) / / 2
elif height_ratio > width_ratio :
clip_height = max ( int ( im_height * width_ratio / height_ratio ) , 1 )
clip_y = ( im_height - clip_height ) / / 2
clip_rect = ( clip_x , clip_y , clip_width , clip_height )
2016-06-01 20:04:15 +00:00
2013-02-19 00:11:43 +00:00
2019-04-03 22:45:57 +00:00
thumbnail_width = max ( int ( thumbnail_width ) , 1 )
thumbnail_height = max ( int ( thumbnail_height ) , 1 )
2014-05-14 20:46:38 +00:00
2022-02-02 22:14:01 +00:00
return ( clip_rect , ( thumbnail_width , thumbnail_height ) )
2017-01-25 22:56:55 +00:00
2022-11-09 22:24:07 +00:00
def HasEXIF ( path : str ) - > bool :
try :
pil_image = RawOpenPILImage ( path )
except :
return False
result = GetEXIFDict ( pil_image )
return result is not None
def HasHumanReadableEmbeddedMetadata ( path : str ) - > bool :
try :
pil_image = RawOpenPILImage ( path )
except :
return False
result = GetEmbeddedFileText ( pil_image )
return result is not None
2021-12-01 22:12:16 +00:00
def HasICCProfile ( pil_image : PILImage . Image ) - > bool :
if ' icc_profile ' in pil_image . info :
icc_profile = pil_image . info [ ' icc_profile ' ]
if isinstance ( icc_profile , bytes ) and len ( icc_profile ) > 0 :
return True
return False
2022-11-09 22:24:07 +00:00
2021-12-01 22:12:16 +00:00
def IsDecompressionBomb ( path ) - > bool :
2017-10-04 17:51:58 +00:00
2020-05-27 21:27:52 +00:00
# there are two errors here, the 'Warning' and the 'Error', which atm is just a test vs a test x 2 for number of pixels
# 256MB bmp by default, ( 1024 ** 3 ) // 4 // 3
# we'll set it at 512MB, and now catching error should be about 1GB
2021-12-01 22:12:16 +00:00
PILImage . MAX_IMAGE_PIXELS = ( 512 * ( 1024 * * 2 ) ) / / 3
2018-04-11 22:30:40 +00:00
2020-05-27 21:27:52 +00:00
warnings . simplefilter ( ' error ' , PILImage . DecompressionBombError )
2017-10-04 17:51:58 +00:00
try :
2021-12-01 22:12:16 +00:00
RawOpenPILImage ( path )
2017-10-04 17:51:58 +00:00
2020-05-27 21:27:52 +00:00
except ( PILImage . DecompressionBombError ) :
2017-10-04 17:51:58 +00:00
return True
2020-05-27 21:27:52 +00:00
except :
# pil was unable to load it, which does not mean it was a decomp bomb
return False
2017-10-04 17:51:58 +00:00
finally :
2018-04-11 22:30:40 +00:00
PILImage . MAX_IMAGE_PIXELS = None
2020-05-27 21:27:52 +00:00
warnings . simplefilter ( ' ignore ' , PILImage . DecompressionBombError )
2017-10-04 17:51:58 +00:00
return False
2022-06-03 21:26:23 +00:00
def NormaliseICCProfilePILImageToSRGB ( pil_image : PILImage . Image ) - > PILImage . Image :
2021-12-01 22:12:16 +00:00
try :
icc_profile_bytes = GetICCProfileBytes ( pil_image )
except HydrusExceptions . DataMissing :
return pil_image
try :
f = io . BytesIO ( icc_profile_bytes )
src_profile = PILImageCms . ImageCmsProfile ( f )
2021-12-22 22:31:23 +00:00
if pil_image . mode in ( ' L ' , ' LA ' ) :
2021-12-01 22:12:16 +00:00
2021-12-22 22:31:23 +00:00
# had a bunch of LA pngs that turned pure white on RGBA ICC conversion
# but seem to work fine if keep colourspace the same for now
# it is a mystery, I guess a PIL bug, but presumably L and LA are technically sRGB so it is still ok to this
outputMode = pil_image . mode
2021-12-01 22:12:16 +00:00
else :
2023-02-15 21:26:44 +00:00
if PILImageHasTransparency ( pil_image ) :
2021-12-22 22:31:23 +00:00
outputMode = ' RGBA '
else :
outputMode = ' RGB '
2021-12-01 22:12:16 +00:00
pil_image = PILImageCms . profileToProfile ( pil_image , src_profile , PIL_SRGB_PROFILE , outputMode = outputMode )
2021-12-08 22:40:59 +00:00
except ( PILImageCms . PyCMSError , OSError ) :
2021-12-01 22:12:16 +00:00
# 'cannot build transform' and presumably some other fun errors
2021-12-08 22:40:59 +00:00
# way more advanced than we can deal with, so we'll just no-op
# OSError is due to a "OSError: cannot open profile from string" a user got
# no idea, but that seems to be an ImageCms issue doing byte handling and ending up with an odd OSError?
# or maybe somehow my PIL reader or bytesIO sending string for some reason?
# in any case, nuke it for now
2021-12-01 22:12:16 +00:00
pass
2021-12-22 22:31:23 +00:00
pil_image = NormalisePILImageToRGB ( pil_image )
return pil_image
2022-06-03 21:26:23 +00:00
def NormalisePILImageToRGB ( pil_image : PILImage . Image ) - > PILImage . Image :
2021-12-22 22:31:23 +00:00
2023-02-15 21:26:44 +00:00
if PILImageHasTransparency ( pil_image ) :
2021-12-22 22:31:23 +00:00
desired_mode = ' RGBA '
else :
desired_mode = ' RGB '
if pil_image . mode != desired_mode :
2022-01-19 21:28:59 +00:00
if pil_image . mode == ' LAB ' :
pil_image = PILImageCms . profileToProfile ( pil_image , PILImageCms . createProfile ( ' LAB ' ) , PIL_SRGB_PROFILE , outputMode = ' RGB ' )
else :
pil_image = pil_image . convert ( desired_mode )
2021-12-22 22:31:23 +00:00
2021-12-01 22:12:16 +00:00
return pil_image
2022-12-14 22:22:11 +00:00
def NumPyImageHasAllCellsTheSame ( numpy_image : numpy . array , value : int ) :
# I looked around for ways to do this iteratively at the c++ level but didn't have huge luck.
# unless some magic is going on, the '==' actually creates the bool array
# its ok for now!
return numpy . all ( numpy_image == value )
# old way, which makes a third array:
# alpha_channel == numpy.full( ( shape[0], shape[1] ), 255, dtype = 'uint8' ) ).all()
def NumPyImageHasUselessAlphaChannel ( numpy_image : numpy . array ) - > bool :
2022-02-02 22:14:01 +00:00
2023-02-15 21:26:44 +00:00
if not NumPyImageHasAlphaChannel ( numpy_image ) :
2022-02-02 22:14:01 +00:00
return False
2023-02-15 21:26:44 +00:00
# RGBA image
alpha_channel = numpy_image [ : , : , 3 ] . copy ( )
if NumPyImageHasAllCellsTheSame ( alpha_channel , 255 ) : # all opaque
2022-02-02 22:14:01 +00:00
2023-02-15 21:26:44 +00:00
return True
2022-02-02 22:14:01 +00:00
2023-02-15 21:26:44 +00:00
if NumPyImageHasAllCellsTheSame ( alpha_channel , 0 ) : # all transparent
2022-02-02 22:14:01 +00:00
2023-02-15 21:26:44 +00:00
underlying_image_is_black = NumPyImageHasAllCellsTheSame ( numpy_image , 0 )
2022-02-02 22:14:01 +00:00
2023-02-15 21:26:44 +00:00
return not underlying_image_is_black
2022-12-14 22:22:11 +00:00
return False
def NumPyImageHasOpaqueAlphaChannel ( numpy_image : numpy . array ) - > bool :
2023-02-15 21:26:44 +00:00
if not NumPyImageHasAlphaChannel ( numpy_image ) :
2022-12-14 22:22:11 +00:00
return False
2023-02-15 21:26:44 +00:00
# RGBA image
# opaque means 255
2022-12-14 22:22:11 +00:00
2023-02-15 21:26:44 +00:00
alpha_channel = numpy_image [ : , : , 3 ] . copy ( )
return NumPyImageHasAllCellsTheSame ( alpha_channel , 255 )
2022-12-14 22:22:11 +00:00
2023-02-15 21:26:44 +00:00
def NumPyImageHasAlphaChannel ( numpy_image : numpy . array ) - > bool :
# note this does not test how useful the channel is, just if it exists
2022-12-14 22:22:11 +00:00
shape = numpy_image . shape
if len ( shape ) < = 2 :
return False
2023-02-15 21:26:44 +00:00
# 2 for LA? think this works
return shape [ 2 ] in ( 2 , 4 )
def NumPyImageHasTransparentAlphaChannel ( numpy_image : numpy . array ) - > bool :
if not NumPyImageHasAlphaChannel ( numpy_image ) :
2022-12-14 22:22:11 +00:00
2023-02-15 21:26:44 +00:00
return False
2022-12-14 22:22:11 +00:00
2022-02-02 22:14:01 +00:00
2023-02-15 21:26:44 +00:00
# RGBA image
# transparent means 0
alpha_channel = numpy_image [ : , : , 3 ] . copy ( )
2022-02-02 22:14:01 +00:00
2023-02-15 21:26:44 +00:00
return NumPyImageHasAllCellsTheSame ( alpha_channel , 0 )
def PILImageHasTransparency ( pil_image : PILImage . Image ) - > bool :
2021-12-01 22:12:16 +00:00
return pil_image . mode in ( ' LA ' , ' RGBA ' ) or ( pil_image . mode == ' P ' and ' transparency ' in pil_image . info )
2023-02-15 21:26:44 +00:00
2021-12-01 22:12:16 +00:00
def RawOpenPILImage ( path ) - > PILImage . Image :
try :
pil_image = PILImage . open ( path )
except Exception as e :
raise HydrusExceptions . DamagedOrUnusualFileException ( ' Could not load the image--it was likely malformed! ' )
return pil_image
2023-02-15 21:26:44 +00:00
2021-12-01 22:12:16 +00:00
def ResizeNumPyImage ( numpy_image : numpy . array , target_resolution ) - > numpy . array :
2019-05-08 21:06:42 +00:00
( target_width , target_height ) = target_resolution
( image_width , image_height ) = GetResolutionNumPy ( numpy_image )
2019-03-27 22:01:02 +00:00
2019-05-08 21:06:42 +00:00
if target_width == image_width and target_height == target_width :
return numpy_image
elif target_width > image_height or target_height > image_width :
interpolation = cv2 . INTER_LANCZOS4
else :
interpolation = cv2 . INTER_AREA
2019-03-27 22:01:02 +00:00
2019-05-08 21:06:42 +00:00
return cv2 . resize ( numpy_image , ( target_width , target_height ) , interpolation = interpolation )
2019-03-27 22:01:02 +00:00
2022-06-03 21:26:23 +00:00
def RotateEXIFPILImage ( pil_image : PILImage . Image ) - > PILImage . Image :
2021-12-01 22:12:16 +00:00
2022-07-27 21:18:33 +00:00
exif_dict = GetEXIFDict ( pil_image )
if exif_dict is not None :
2021-12-01 22:12:16 +00:00
2022-07-27 21:18:33 +00:00
EXIF_ORIENTATION = 274
2021-12-01 22:12:16 +00:00
2022-07-27 21:18:33 +00:00
if EXIF_ORIENTATION in exif_dict :
2021-12-01 22:12:16 +00:00
2022-07-27 21:18:33 +00:00
orientation = exif_dict [ EXIF_ORIENTATION ]
2021-12-01 22:12:16 +00:00
2022-07-27 21:18:33 +00:00
if orientation == 1 :
2021-12-01 22:12:16 +00:00
2022-07-27 21:18:33 +00:00
pass # normal
2021-12-01 22:12:16 +00:00
2022-07-27 21:18:33 +00:00
elif orientation == 2 :
# mirrored horizontal
pil_image = pil_image . transpose ( PILImage . FLIP_LEFT_RIGHT )
elif orientation == 3 :
# 180
pil_image = pil_image . transpose ( PILImage . ROTATE_180 )
elif orientation == 4 :
# mirrored vertical
pil_image = pil_image . transpose ( PILImage . FLIP_TOP_BOTTOM )
elif orientation == 5 :
# seems like these 90 degree rotations are wrong, but fliping them works for my posh example images, so I guess the PIL constants are odd
# mirrored horizontal, then 90 CCW
pil_image = pil_image . transpose ( PILImage . FLIP_LEFT_RIGHT ) . transpose ( PILImage . ROTATE_90 )
elif orientation == 6 :
# 90 CW
pil_image = pil_image . transpose ( PILImage . ROTATE_270 )
elif orientation == 7 :
# mirrored horizontal, then 90 CCW
pil_image = pil_image . transpose ( PILImage . FLIP_LEFT_RIGHT ) . transpose ( PILImage . ROTATE_270 )
elif orientation == 8 :
# 90 CCW
pil_image = pil_image . transpose ( PILImage . ROTATE_90 )
2021-12-01 22:12:16 +00:00
return pil_image
2022-12-07 22:41:53 +00:00
2022-12-14 22:22:11 +00:00
def StripOutAnyUselessAlphaChannel ( numpy_image : numpy . array ) - > numpy . array :
2022-12-07 22:41:53 +00:00
2022-12-14 22:22:11 +00:00
if NumPyImageHasUselessAlphaChannel ( numpy_image ) :
2022-12-07 22:41:53 +00:00
numpy_image = numpy_image [ : , : , : 3 ] . copy ( )
# old way, which doesn't actually remove the channel lmao lmao lmao
'''
convert = cv2 . COLOR_RGBA2RGB
numpy_image = cv2 . cvtColor ( numpy_image , convert )
'''
return numpy_image
2023-09-23 19:13:21 +00:00
def GetImageBlurHashNumPy ( numpy_image , components_x = 4 , components_y = 4 ) :
return blurhash . blurhash_encode ( numpy_image , components_x , components_y )