"""This module responds for image transcoding.""" import io import os import uuid import numpy as np from PIL import Image from PIL import ImageCms from PIL import ImageOps from PIL.ImageCms import PyCMSError from constants import transcoding_configs def _generate_unique_sequence(): """Generate unique str sequence from uuid. Returns: str: Unique sequence. """ raw_uuid = str(uuid.uuid4()) result = raw_uuid.replace('-', '') return result def transcode_image(image, output_config, output_buffer, **params): """Open image and save in given format and size. Args: image (PIL.Image): Source image object. output_config (dict): Paths for tmp directory, s3 key and size. E.g.: { 'size': (2000, 2000), 'key': 'img/jpg/test_image_2000_2000.jpg', 'format': 'JPEG' } params (dict): extra parameters for PIL.Image.save. output_buffer (io.BytesIO): Buffer for output image. Returns: io.BytesIO: transcoded file object """ color_profile = image.info.get('icc_profile') if output_config['size']: resized_image = image.resize( output_config['size'], Image.LANCZOS) resized_image.save( output_buffer, icc_profile=color_profile, **output_config['pillow_image_save_params'], **params) resized_image.close() else: image.save( output_buffer, **output_config['pillow_image_save_params'], **params) output_buffer.seek(0) return output_buffer def generate_transcoding_configs(key, descriptors): """Generate output paths. Args: key (str): target S3 key. descriptors (list): Contains dictionaries of size and output format. Returns: list: Contains dictionaries describing configuration of output files. """ base_name = os.path.basename(key) file_name, ext = os.path.splitext(base_name) result_configs = [] for descriptor in descriptors: if descriptor['size']: size_mark = '{}_{}'.format(*descriptor['size']) else: size_mark = '' name_prefix = _generate_unique_sequence() s3_output_template = ( transcoding_configs.IMAGE_PREVIEW_OUTPUT_PATH_TEMPLATE if descriptor['preview'] else transcoding_configs.IMAGE_OUTPUT_PATH_TEMPLATE) s3_output_key = s3_output_template.format( base_path=transcoding_configs.BASE_IMAGE_OUTPUT_PATH, unique_prefix=name_prefix, file_name=file_name, size=size_mark, extension=descriptor['extension'], subtype=descriptor.get('asset_subtype') ) config = { 'size': descriptor['size'], 'key': s3_output_key, 'preview': descriptor['preview'], 'pillow_image_save_params': descriptor['pillow_image_save_params'], 'asset_type': descriptor['asset_type'], 'asset_subtype': descriptor.get('asset_subtype'), 'output_image_buffer': None } result_configs.append(config) return result_configs def is_opaque(image): """ Check if an image is fully opaque. Args: image (PIL.Image): Image object. Returns: bool: True if image is fully opaque, False otherwise. """ if image.mode not in {'RGBA', 'LA', 'PA'}: return True if image.mode in ['LA', 'PA']: image = image.convert('RGBA') np_img = np.array(image) return np.all(np_img[:, :, 3] == 255) def ensure_rgb_image_mode(image): """ Ensure an image is an 8-bit RGB image, converting it if necessary. Args: image (PIL.Image): Image object. Returns: PIL.Image: 8-bit RGB image. """ if image.mode not in {'RGB', 'RGBX', 'RGBA', 'L', 'LA', 'P', 'PA', '1', 'CMYK'}: raise ValueError(f'Unsupported image mode: {image.mode}. Image not processed.') if not is_opaque(image): raise ValueError(f'Unsupported image mode: {image.mode} with non-opaque pixels. Image not processed.') # Try to convert image to RGB mode using an embedded ICC profile to ensure correct colors. try: return ImageCms.profileToProfile( im=image, inputProfile=ImageCms.ImageCmsProfile(io.BytesIO(image.info.get('icc_profile'))), outputProfile=ImageCms.createProfile('sRGB'), outputMode='RGB', ) except (OSError, PyCMSError): # Not all images have embedded ICC profiles and some have incompatible embedded ICC profiles. pass if image.mode == 'CMYK': raise ValueError( 'Unsupported image mode: CMYK without a compatible embedded ICC profile. ' 'Converting from CMYK to RGB requires an ICC profile that matches the intended color output of the image. ' 'Using an incorrect ICC profile can result in inaccurate color representation. ' 'Image not processed.' ) # Convert remaining image modes to RGB mode without using an ICC profile. return image.convert('RGB') def convert(file_object, image_descriptors, output_buffers): """Convert image file to new images by received descriptors. Args: file_object (io.BytesIO): File object. image_descriptors (list): List of sizes and output formats. output_buffers (list): List of buffer for output files. Returns: list: image descriptors with file-like output objects. """ described_output = [] with Image.open(file_object) as image: image = ImageOps.exif_transpose(image) image = ensure_rgb_image_mode(image) for descriptor in zip(image_descriptors, output_buffers): transcoded_image = transcode_image( image, descriptor[0], descriptor[1], dpi=transcoding_configs.DEFAULT_DENSITY) updated_descriptor = dict.copy(descriptor[0]) updated_descriptor['output_image_buffer'] = transcoded_image described_output.append(updated_descriptor) return described_output