2019-07-24 21:39:02 +00:00
import hashlib
2019-05-08 21:06:42 +00:00
import io
import numpy
import numpy . core . multiarray # important this comes before cv!
2020-07-29 20:52:44 +00:00
import struct
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
2014-10-22 22:31:58 +00:00
from PIL import _imaging
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
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
2017-10-04 17:51:58 +00:00
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
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
class DBE_stub ( Exception ) :
pass
PILImage . DecompressionBombError = DBE_stub
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
class DBW_stub ( Exception ) :
pass
PILImage . DecompressionBombWarning = DBW_stub
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
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
2019-05-15 20:35:00 +00:00
PIL_ONLY_MIMETYPES = { HC . IMAGE_GIF , HC . IMAGE_ICON }
2019-05-08 21:06:42 +00:00
try :
import cv2
if cv2 . __version__ . startswith ( ' 2 ' ) :
CV_IMREAD_FLAGS_SUPPORTS_ALPHA = cv2 . CV_LOAD_IMAGE_UNCHANGED
CV_IMREAD_FLAGS_SUPPORTS_EXIF_REORIENTATION = CV_IMREAD_FLAGS_SUPPORTS_ALPHA
# there's something wrong with these, but I don't have an easy test env for it atm
# CV_IMREAD_FLAGS_SUPPORTS_EXIF_REORIENTATION = cv2.CV_LOAD_IMAGE_ANYDEPTH | cv2.CV_LOAD_IMAGE_ANYCOLOR
CV_JPEG_THUMBNAIL_ENCODE_PARAMS = [ ]
CV_PNG_THUMBNAIL_ENCODE_PARAMS = [ ]
else :
CV_IMREAD_FLAGS_SUPPORTS_ALPHA = cv2 . IMREAD_UNCHANGED
CV_IMREAD_FLAGS_SUPPORTS_EXIF_REORIENTATION = cv2 . IMREAD_ANYDEPTH | cv2 . IMREAD_ANYCOLOR # this preserves colour info but does EXIF reorientation and flipping
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
2020-06-24 21:25:24 +00:00
def ConvertToPNGIfBMP ( path ) :
2013-02-19 00:11:43 +00:00
2017-07-19 21:21:41 +00:00
with open ( path , ' rb ' ) as f :
header = f . read ( 2 )
2013-08-07 22:25:18 +00:00
2019-01-09 22:59:03 +00:00
if header == b ' BM ' :
2013-02-19 00:11:43 +00:00
2021-10-27 21:12:33 +00:00
( os_file_handle , temp_path ) = HydrusTemp . GetTempPath ( )
2013-02-19 00:11:43 +00:00
2015-03-25 22:04:19 +00:00
try :
with open ( path , ' rb ' ) as f_source :
with open ( temp_path , ' wb ' ) as f_dest :
2015-11-04 22:30:28 +00:00
HydrusPaths . CopyFileLikeToFileLike ( f_source , f_dest )
2015-03-25 22:04:19 +00:00
pil_image = GeneratePILImage ( temp_path )
pil_image . save ( path , ' PNG ' )
finally :
2021-10-27 21:12:33 +00:00
HydrusTemp . CleanUpTempPath ( os_file_handle , temp_path )
2015-03-25 22:04:19 +00:00
2013-02-19 00:11:43 +00:00
2016-06-29 19:55:46 +00:00
def Dequantize ( pil_image ) :
if pil_image . mode not in ( ' RGBA ' , ' RGB ' ) :
2019-01-09 22:59:03 +00:00
if pil_image . mode == ' LA ' or ( pil_image . mode == ' P ' and ' transparency ' in pil_image . info ) :
2016-06-29 19:55:46 +00:00
pil_image = pil_image . convert ( ' RGBA ' )
else :
pil_image = pil_image . convert ( ' RGB ' )
return pil_image
2019-05-08 21:06:42 +00:00
def GenerateNumPyImage ( path , mime , force_pil = False ) :
if HG . media_load_report_mode :
HydrusData . ShowText ( ' Loading media: ' + path )
if not OPENCV_OK :
force_pil = True
2019-05-15 20:35:00 +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 ' )
if mime == HC . IMAGE_JPEG :
flags = CV_IMREAD_FLAGS_SUPPORTS_EXIF_REORIENTATION
else :
flags = CV_IMREAD_FLAGS_SUPPORTS_ALPHA
numpy_image = cv2 . imread ( path , flags = flags )
if numpy_image is None : # doesn't support static gifs and some random other stuff
if HG . media_load_report_mode :
HydrusData . ShowText ( ' OpenCV Failed, loading with PIL ' )
pil_image = GeneratePILImage ( path )
numpy_image = GenerateNumPyImageFromPILImage ( pil_image )
else :
if numpy_image . dtype == ' uint16 ' :
numpy_image / / = 256
numpy_image = numpy . array ( numpy_image , dtype = ' uint8 ' )
shape = numpy_image . shape
if len ( shape ) == 2 :
# monochrome image
convert = cv2 . COLOR_GRAY2RGB
else :
( im_y , im_x , depth ) = shape
if depth == 4 :
convert = cv2 . COLOR_BGRA2RGBA
else :
convert = cv2 . COLOR_BGR2RGB
numpy_image = cv2 . cvtColor ( numpy_image , convert )
return numpy_image
2021-06-23 21:11:38 +00:00
def GenerateNumPyImageFromPILImage ( pil_image , dequantize = True ) :
2019-05-08 21:06:42 +00:00
2021-06-23 21:11:38 +00:00
if dequantize :
pil_image = Dequantize ( pil_image )
2019-05-08 21:06:42 +00:00
( w , h ) = pil_image . size
s = pil_image . tobytes ( )
return numpy . fromstring ( s , dtype = ' uint8 ' ) . reshape ( ( h , w , len ( s ) / / ( w * h ) ) )
2015-10-21 21:53:10 +00:00
def GeneratePILImage ( path ) :
2016-02-03 22:12:53 +00:00
try :
2019-07-03 22:49:27 +00:00
pil_image = PILImage . open ( path )
2016-02-03 22:12:53 +00:00
2019-05-08 21:06:42 +00:00
except Exception as e :
2016-02-03 22:12:53 +00:00
2020-05-27 21:27:52 +00:00
raise HydrusExceptions . DamagedOrUnusualFileException ( ' Could not load the image--it was likely malformed! ' )
2016-02-03 22:12:53 +00:00
2015-10-21 21:53:10 +00:00
2017-11-01 20:37:39 +00:00
if pil_image . format == ' JPEG ' and hasattr ( pil_image , ' _getexif ' ) :
2017-12-06 22:06:56 +00:00
try :
exif_dict = pil_image . _getexif ( )
except :
exif_dict = None
2017-11-01 20:37:39 +00:00
if exif_dict is not None :
EXIF_ORIENTATION = 274
if EXIF_ORIENTATION in exif_dict :
orientation = exif_dict [ EXIF_ORIENTATION ]
if orientation == 1 :
pass # normal
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 )
2015-10-21 21:53:10 +00:00
if pil_image is None :
raise Exception ( ' The file at ' + path + ' could not be rendered! ' )
return pil_image
2019-05-08 21:06:42 +00:00
def GeneratePILImageFromNumPyImage ( numpy_image ) :
2014-06-25 20:37:06 +00:00
( h , w , depth ) = numpy_image . shape
2016-10-12 21:52:50 +00:00
if depth == 3 :
format = ' RGB '
elif depth == 4 :
format = ' RGBA '
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
2019-05-08 21:06:42 +00:00
def GenerateThumbnailBytesFromStaticImagePath ( path , target_resolution , mime ) :
2019-05-15 20:35:00 +00:00
if OPENCV_OK :
2019-05-08 21:06:42 +00:00
numpy_image = GenerateNumPyImage ( path , mime )
thumbnail_numpy_image = ResizeNumPyImage ( numpy_image , target_resolution )
try :
thumbnail_bytes = GenerateThumbnailBytesNumPy ( thumbnail_numpy_image , mime )
return thumbnail_bytes
except HydrusExceptions . CantRenderWithCVException :
pass # fall back to pil
pil_image = GeneratePILImage ( path )
pil_image = Dequantize ( pil_image )
thumbnail_pil_image = pil_image . resize ( target_resolution , PILImage . ANTIALIAS )
thumbnail_bytes = GenerateThumbnailBytesPIL ( pil_image , mime )
return thumbnail_bytes
def GenerateThumbnailBytesNumPy ( numpy_image , mime ) :
if not OPENCV_OK :
pil_image = GeneratePILImageFromNumPyImage ( numpy_image )
return GenerateThumbnailBytesPIL ( pil_image , mime )
2021-05-12 20:49:20 +00:00
( im_height , im_width , depth ) = numpy_image . shape
2019-05-08 21:06:42 +00:00
if depth == 4 :
convert = cv2 . COLOR_RGBA2BGRA
else :
convert = cv2 . COLOR_RGB2BGR
numpy_image = cv2 . cvtColor ( numpy_image , convert )
if mime == HC . IMAGE_JPEG :
ext = ' .jpg '
params = CV_JPEG_THUMBNAIL_ENCODE_PARAMS
else :
ext = ' .png '
params = CV_PNG_THUMBNAIL_ENCODE_PARAMS
( 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! ' )
def GenerateThumbnailBytesPIL ( pil_image , mime ) :
f = io . BytesIO ( )
pil_image = Dequantize ( pil_image )
if mime == HC . IMAGE_PNG or pil_image . mode == ' RGBA ' :
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
2014-05-28 21:03:24 +00:00
def GetGIFFrameDurations ( path ) :
2015-10-21 21:53:10 +00:00
pil_image = GeneratePILImage ( path )
2014-05-07 22:42:30 +00:00
2020-02-26 22:28:52 +00:00
times_to_play_gif = GetTimesToPlayGIFFromPIL ( pil_image )
2014-05-07 22:42:30 +00:00
frame_durations = [ ]
i = 0
while True :
2019-03-20 21:22:10 +00:00
try :
pil_image . seek ( i )
except :
break
2014-05-07 22:42:30 +00:00
2015-10-21 21:53:10 +00:00
if ' duration ' not in pil_image . info :
2019-01-09 22:59:03 +00:00
duration = 83 # (83ms -- 1000 / 12) Set a 12 fps default when duration is missing or too funky to extract. most stuff looks ok at this.
2015-10-21 21:53:10 +00:00
2014-05-07 22:42:30 +00:00
else :
2015-10-21 21:53:10 +00:00
duration = pil_image . info [ ' duration ' ]
2014-05-07 22:42:30 +00:00
2015-10-21 21:53:10 +00:00
# In the gif frame header, 10 is stored as 1ms. This 1 is commonly as utterly wrong as 0.
if duration in ( 0 , 10 ) :
2019-01-09 22:59:03 +00:00
duration = 83
2015-10-21 21:53:10 +00:00
2014-05-07 22:42:30 +00:00
frame_durations . append ( duration )
i + = 1
2020-02-26 22:28:52 +00:00
return ( frame_durations , times_to_play_gif )
2014-05-07 22:42:30 +00:00
2019-07-24 21:39:02 +00:00
def GetImagePixelHash ( path , mime ) :
numpy_image = GenerateNumPyImage ( path , mime )
return hashlib . sha256 ( numpy_image . data . tobytes ( ) ) . digest ( )
2018-09-26 19:05:12 +00:00
def GetImageProperties ( path , mime ) :
2014-05-14 20:46:38 +00:00
2019-05-15 20:35:00 +00:00
if OPENCV_OK and mime not in PIL_ONLY_MIMETYPES : # webp here too maybe eventually, or offload it all to ffmpeg
2014-05-14 20:46:38 +00:00
2019-05-08 21:06:42 +00:00
numpy_image = GenerateNumPyImage ( path , mime )
2014-05-14 20:46:38 +00:00
2019-05-08 21:06:42 +00:00
( width , height ) = GetResolutionNumPy ( numpy_image )
2014-05-14 20:46:38 +00:00
duration = None
num_frames = None
2019-05-08 21:06:42 +00:00
else :
( ( width , height ) , num_frames ) = GetResolutionAndNumFramesPIL ( path , mime )
if num_frames > 1 :
2020-02-26 22:28:52 +00:00
( durations , times_to_play_gif ) = GetGIFFrameDurations ( path )
2019-05-08 21:06:42 +00:00
duration = sum ( durations )
else :
duration = None
num_frames = None
2014-05-14 20:46:38 +00:00
return ( ( width , height ) , duration , num_frames )
2019-05-15 20:35:00 +00:00
# bigger number is worse quality
# this is very rough and misses some finesse
def GetJPEGQuantizationQualityEstimate ( path ) :
pil_image = GeneratePILImage ( path )
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 )
2019-03-20 21:22:10 +00:00
def GetPSDResolution ( path ) :
with open ( path , ' rb ' ) as f :
f . seek ( 14 )
height_bytes = f . read ( 4 )
width_bytes = f . read ( 4 )
height = struct . unpack ( ' >L ' , height_bytes ) [ 0 ]
width = struct . unpack ( ' >L ' , width_bytes ) [ 0 ]
return ( width , height )
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 )
def GetResolutionAndNumFramesPIL ( path , mime ) :
2014-05-14 20:46:38 +00:00
pil_image = GeneratePILImage ( path )
( x , y ) = pil_image . size
2013-02-19 00:11:43 +00:00
2018-09-26 19:05:12 +00:00
if mime == HC . IMAGE_GIF : # some jpegs came up with 2 frames and 'duration' because of some embedded thumbnail in the metadata
2013-02-19 00:11:43 +00:00
2018-09-26 19:05:12 +00:00
try :
pil_image . seek ( 1 )
pil_image . seek ( 0 )
num_frames = 1
2014-05-14 20:46:38 +00:00
2018-09-26 19:05:12 +00:00
while True :
2014-05-14 20:46:38 +00:00
2018-09-26 19:05:12 +00:00
try :
pil_image . seek ( pil_image . tell ( ) + 1 )
num_frames + = 1
except : break
2014-05-14 20:46:38 +00:00
2018-09-26 19:05:12 +00:00
except :
num_frames = 1
2014-05-14 20:46:38 +00:00
2013-02-19 00:11:43 +00:00
2018-09-26 19:05:12 +00:00
else :
2017-08-23 21:34:25 +00:00
num_frames = 1
2014-05-14 20:46:38 +00:00
return ( ( x , y ) , num_frames )
2019-05-08 21:06:42 +00:00
def GetThumbnailResolution ( image_resolution , bounding_dimensions ) :
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
2019-04-03 22:45:57 +00:00
if bounding_width > = im_width and bounding_height > = im_height :
2016-06-01 20:04:15 +00:00
2019-04-03 22:45:57 +00:00
return ( 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
2019-04-03 22:45:57 +00:00
if width_ratio > height_ratio :
2016-06-01 20:04:15 +00:00
2019-04-03 22:45:57 +00:00
thumbnail_height = im_height / width_ratio
2016-06-01 20:04:15 +00:00
2019-04-03 22:45:57 +00:00
elif height_ratio > width_ratio :
2016-06-01 20:04:15 +00:00
2019-04-03 22:45:57 +00:00
thumbnail_width = im_width / height_ratio
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
2019-04-03 22:45:57 +00:00
return ( thumbnail_width , thumbnail_height )
2017-01-25 22:56:55 +00:00
2020-02-26 22:28:52 +00:00
def GetTimesToPlayGIF ( path ) :
pil_image = GeneratePILImage ( path )
return GetTimesToPlayGIFFromPIL ( pil_image )
def GetTimesToPlayGIFFromPIL ( pil_image ) :
if ' loop ' in pil_image . info :
times_to_play_gif = pil_image . info [ ' loop ' ]
else :
times_to_play_gif = 1
return times_to_play_gif
2017-10-04 17:51:58 +00:00
def IsDecompressionBomb ( path ) :
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
PILImage . MAX_IMAGE_PIXELS = ( 1024 * * 3 ) / / 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 :
GeneratePILImage ( path )
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
2019-05-08 21:06:42 +00:00
def ResizeNumPyImage ( numpy_image , target_resolution ) :
( 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