"""S3 Image methods.""" # Based on: https://gist.github.com/ghandic/a48f450f3c011f44d42eea16a0c7014d from io import BytesIO import mimetypes import os import boto3 from PIL import Image from PIL.TiffImagePlugin import ImageFileDirectory_v2, TiffTags from config import get_current_logger from constants.artwork import ( MAX_ARTWORK_IMAGE_SIZE, MIN_ARTWORK_IMAGE_SIZE, RESIZED_ARTWORK_QUALITY ) # Fix from https://github.com/python-pillow/Pillow/issues/2278 def _monkey_write_undefined(_, value): if isinstance(value, tuple) and len(value) == 1: return value[0] else: return value ImageFileDirectory_v2._write_dispatch[ TiffTags.UNDEFINED] = _monkey_write_undefined class S3ImageInvalidExtension(Exception): """Error for invalid extension.""" pass class S3ImageUploadFailed(Exception): """Error for failed upload.""" pass def repair_image(image_name, image_object, correlation_id=None): """Transform a Pillow image to match ripper spec.""" logger = get_current_logger(correlation_id) # if the image is not square if image_object.width != image_object.height: # Check how much longer the long side is long_edge = max(image_object.width, image_object.height) logger.info('Correcting non-square image to {}x{}px.'.format( long_edge, long_edge)) # Reshape image_object = image_object.resize((long_edge, long_edge)) # Convert Mode if image_object.mode != 'RGB': logger.info('Converting {} to RGB.'.format(image_name)) image_object = image_object.convert('RGB') # Upscale dimensions if image_object.width < MIN_ARTWORK_IMAGE_SIZE: logger.info( f'Upscaling image {image_name} to ' f'{MIN_ARTWORK_IMAGE_SIZE}x{MIN_ARTWORK_IMAGE_SIZE}px.') image_object = image_object.resize((MIN_ARTWORK_IMAGE_SIZE, MIN_ARTWORK_IMAGE_SIZE)) # Downscale dimensions elif image_object.width > MAX_ARTWORK_IMAGE_SIZE: logger.info( f'Downscaling image {image_name} to ' f'{MAX_ARTWORK_IMAGE_SIZE}x{MAX_ARTWORK_IMAGE_SIZE}px.') image_object = image_object.resize((MAX_ARTWORK_IMAGE_SIZE, MAX_ARTWORK_IMAGE_SIZE)) return image_object def get_image_from_s3(bucket, key, correlation_id=None): """Get an image from an S3 source as a Pillow object.""" logger = get_current_logger(correlation_id) # Get S3 object s3_client = boto3.client('s3') file_byte_string = s3_client.get_object(Bucket=bucket, Key=key)[ 'Body'].read() logger.info('Converting object to Image') return Image.open(BytesIO(file_byte_string)) def save_image_to_s3(img, bucket, key, dpi=None, correlation_id=None): """Save a Pillow image object to an S3 target from memory. Args: img: bucket: key: dpi: correlation_id: """ logger = get_current_logger(correlation_id) if not dpi: dpi = (72, 72) print('Using dpi: {}'.format(dpi)) file_name = os.path.split(key)[1] orig_file_name = file_name buffer = BytesIO() ext = __get_safe_ext(file_name) print('Extension is: {}'.format(ext)) if ext == 'JPEG': img.save(buffer, format=ext, dpi=dpi, quality=RESIZED_ARTWORK_QUALITY) elif ext == 'TIFF': # Convert to JPG to prevent multi-layer TIFFs from going through file_name = os.path.splitext(file_name)[0] + '.jpg' # Change key to reflect .jpg extension key = os.path.join( os.path.split(key)[0], os.path.splitext( os.path.split(key)[1])[0] + '.jpg' ) img.save(buffer, format='JPEG', dpi=dpi) logger.info('Converted: {} to {}'.format(orig_file_name, file_name)) else: img.save(buffer, format=ext, dpi=dpi) logger.info('Saving: {} as {}'.format(orig_file_name, key)) buffer.seek(0) # Rewind buffer for saving to S3 logger.info('Putting {} to {}'.format(key, bucket)) s3_client = boto3.client('s3') sent_data = s3_client.put_object( Body=buffer, Key=key, Bucket=bucket, ContentType= mimetypes.guess_type(file_name)[0]) if sent_data['ResponseMetadata']['HTTPStatusCode'] != 200: raise S3ImageUploadFailed( 'Failed to upload image {} to bucket {}'.format(key, bucket)) return key def __get_safe_ext(key): ext = os.path.splitext(key)[-1].strip('.').upper() if ext in ['JPG', 'JPEG']: return 'JPEG' elif ext in ['PNG']: return 'PNG' elif ext in ['TIF', 'TIFF']: return 'TIFF' else: raise S3ImageInvalidExtension('Extension is invalid')