hydrus/hydrus/core/HydrusImageHandling.py

1252 lines
32 KiB
Python
Raw Normal View History

2019-07-24 21:39:02 +00:00
import hashlib
2019-05-08 21:06:42 +00:00
import io
import os
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
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
from hydrus.core import HydrusTemp
from hydrus.core import HydrusPSDHandling
2017-10-04 17:51:58 +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 )
if mime == HC.APPLICATION_PSD:
if HG.media_load_report_mode:
HydrusData.ShowText( 'Loading PSD' )
2023-09-13 18:26:31 +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
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
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
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
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 )
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 )
thumbnail_pil_image = pil_image.resize( target_resolution, PILImage.LANCZOS )
2019-05-08 21:06:42 +00:00
thumbnail_numpy_image = GenerateNumPyImageFromPILImage(thumbnail_pil_image)
2019-05-08 21:06:42 +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
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!' )
def GetEXIFDict( pil_image: PILImage.Image ) -> typing.Optional[ dict ]:
if pil_image.format in ( 'JPEG', 'TIFF', 'PNG', 'WEBP', 'HEIF', 'AVIF' ):
try:
exif_dict = pil_image.getexif()._get_merged_dict()
if len( exif_dict ) > 0:
return exif_dict
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 )
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' )
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 )
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
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
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
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
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
def RotateEXIFPILImage( pil_image: PILImage.Image )-> PILImage.Image:
2021-12-01 22:12:16 +00:00
exif_dict = GetEXIFDict( pil_image )
if exif_dict is not None:
2021-12-01 22:12:16 +00:00
EXIF_ORIENTATION = 274
2021-12-01 22:12:16 +00:00
if EXIF_ORIENTATION in exif_dict:
2021-12-01 22:12:16 +00:00
orientation = exif_dict[ EXIF_ORIENTATION ]
2021-12-01 22:12:16 +00:00
if orientation == 1:
2021-12-01 22:12:16 +00:00
pass # normal
2021-12-01 22:12:16 +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
def GetImageBlurHashNumPy( numpy_image, components_x = 4, components_y = 4 ):
return blurhash.blurhash_encode( numpy_image, components_x, components_y )