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
2021-12-01 22:12:16 +00:00
from PIL import ImageCms as PILImageCms
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
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
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 ' ) :
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
CV_IMREAD_FLAGS_WEIRD = cv2 . IMREAD_ANYDEPTH | cv2 . IMREAD_ANYCOLOR
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
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 ]
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 ) )
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
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 )
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
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 :
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-02-02 22:14:01 +00:00
if NumPyImageHasOpaqueAlphaChannel ( numpy_image ) :
convert = cv2 . COLOR_RGBA2RGB
numpy_image = cv2 . cvtColor ( numpy_image , convert )
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
( 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 ) )
2015-10-21 21:53:10 +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
2021-12-01 22:12:16 +00:00
RotateEXIFPILImage ( pil_image )
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
2022-02-02 22:14:01 +00:00
def GenerateThumbnailBytesFromStaticImagePath ( path , target_resolution , mime , clip_rect = None ) - > bytes :
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 )
try :
thumbnail_bytes = GenerateThumbnailBytesNumPy ( thumbnail_numpy_image , mime )
return thumbnail_bytes
except HydrusExceptions . CantRenderWithCVException :
2021-12-01 22:12:16 +00:00
pass # fallback to PIL
2019-05-08 21:06:42 +00:00
pil_image = GeneratePILImage ( path )
2022-02-02 22:14:01 +00:00
if clip_rect is None :
pil_image = ClipPILImage ( pil_image , clip_rect )
2019-05-08 21:06:42 +00:00
thumbnail_pil_image = pil_image . resize ( target_resolution , PILImage . ANTIALIAS )
thumbnail_bytes = GenerateThumbnailBytesPIL ( pil_image , mime )
return thumbnail_bytes
2021-12-01 22:12:16 +00:00
def GenerateThumbnailBytesNumPy ( numpy_image , mime ) - > 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
if depth == 4 :
convert = cv2 . COLOR_RGBA2BGRA
else :
convert = cv2 . COLOR_RGB2BGR
numpy_image = cv2 . cvtColor ( numpy_image , convert )
2021-12-01 22:12:16 +00:00
if mime == HC . IMAGE_PNG or 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! ' )
2021-12-01 22:12:16 +00:00
def GenerateThumbnailBytesPIL ( pil_image : PILImage . Image , mime ) - > bytes :
2019-05-08 21:06:42 +00:00
f = io . BytesIO ( )
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 ) :
2021-12-01 22:12:16 +00:00
pil_image = RawOpenPILImage ( 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
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 )
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 ) :
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 )
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
2021-12-01 22:12:16 +00:00
pil_image = GeneratePILImage ( path , dequantize = False )
2014-05-14 20:46:38 +00:00
( 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
2021-12-01 22:12:16 +00:00
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 )
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 '
}
def GetThumbnailResolutionAndClipRegion ( image_resolution , bounding_dimensions , thumbnail_scale_type : int ) :
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-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
2021-12-01 22:12:16 +00:00
def GetTimesToPlayGIF ( path ) - > int :
2020-02-26 22:28:52 +00:00
2021-12-22 22:31:23 +00:00
try :
pil_image = RawOpenPILImage ( path )
except HydrusExceptions . UnsupportedFileException :
return 1
2020-02-26 22:28:52 +00:00
return GetTimesToPlayGIFFromPIL ( pil_image )
2021-12-01 22:12:16 +00:00
def GetTimesToPlayGIFFromPIL ( pil_image : PILImage . Image ) - > int :
2020-02-26 22:28:52 +00:00
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
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
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
2021-12-01 22:12:16 +00:00
def NormaliseICCProfilePILImageToSRGB ( pil_image : PILImage . Image ) :
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 :
2021-12-22 22:31:23 +00:00
if PILImageHasAlpha ( pil_image ) :
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
def NormalisePILImageToRGB ( pil_image : PILImage . Image ) :
if PILImageHasAlpha ( pil_image ) :
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-02-02 22:14:01 +00:00
def NumPyImageHasOpaqueAlphaChannel ( numpy_image : numpy . array ) :
shape = numpy_image . shape
if len ( shape ) == 2 :
return False
if shape [ 2 ] == 4 :
# RGBA image
# if the alpha channel is all opaque, there is no use storing that info in our pixel hash
# opaque means 255
alpha_channel = numpy_image [ : , : , 3 ]
if ( alpha_channel == numpy . full ( ( shape [ 0 ] , shape [ 1 ] ) , 255 , dtype = ' uint8 ' ) ) . all ( ) :
return True
return False
2021-12-01 22:12:16 +00:00
def PILImageHasAlpha ( pil_image : PILImage . Image ) :
return pil_image . mode in ( ' LA ' , ' RGBA ' ) or ( pil_image . mode == ' P ' and ' transparency ' in pil_image . info )
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
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
2021-12-01 22:12:16 +00:00
def RotateEXIFPILImage ( pil_image : PILImage . Image ) :
if pil_image . format == ' JPEG ' and hasattr ( pil_image , ' _getexif ' ) :
try :
exif_dict = pil_image . _getexif ( )
except :
exif_dict = None
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 )
return pil_image