"""Image encoding module unit test.""" from contextlib import nullcontext as does_not_raise from pathlib import Path from unittest.mock import MagicMock from unittest.mock import patch from PIL import Image from PIL import ImageCms import pytest import numpy as np from constants import transcoding_configs from src import images_encoding @pytest.fixture def transcode_image_inputs(): """Input parameters for transcode_image calling.""" return { 'size': (2000, 2000), 'key': 'img/jpeg/test_image_2000_2000.jpeg', 'pillow_image_save_params': { 'format': 'JPEG', }, } @pytest.fixture def input_image_descriptors_data(): """Input image descriptors data.""" return [ { 'pillow_image_save_params': { 'format': 'JPEG', }, 'extension': 'jpeg', 'size': (110, 110), 'preview': True, 'asset_type': 'JPG', 'asset_subtype': 'cover' }, { 'pillow_image_save_params': { 'format': 'JPEG', }, 'extension': 'jpeg', 'size': (360, 360), 'preview': True, 'asset_type': 'JPG', 'asset_subtype': 'large_cover' }, { 'pillow_image_save_params': { 'format': 'JPEG', }, 'extension': 'jpeg', 'size': (2000, 2000), 'preview': True, 'asset_type': 'JPG', 'asset_subtype': 'xlarge_cover' }, { 'pillow_image_save_params': { 'format': 'TIFF', }, 'extension': 'tif', 'size': None, 'preview': False, 'asset_type': 'TIF' } ] @pytest.fixture def uuid_mock(): """UUID mock value.""" return 'unique-mock-uuid' @pytest.fixture def image_name_mock(): """Image name mock value.""" return 'image_name_mock' @pytest.fixture def expected_generate_transcoding_configs_result(uuid_mock, image_name_mock): """Fixture with expected result for generate_transcoding_configs call.""" cover = 'images/v2/product/cover/uuid4_{image_name}.jpeg'.format( image_name=image_name_mock) lg_cover = 'images/v2/product/large_cover/uuid4_{image_name}.jpeg'.format( image_name=image_name_mock) xl_cover = 'images/v2/product/xlarge_cover/uuid4_{image_name}.jpeg'.format( image_name=image_name_mock) return [ { 'size': (110, 110), 'key': cover, 'pillow_image_save_params': { 'format': 'JPEG', }, 'preview': True, 'asset_type': 'JPG', 'asset_subtype': 'cover', 'output_image_buffer': None }, { 'size': (360, 360), 'key': lg_cover, 'pillow_image_save_params': { 'format': 'JPEG', }, 'preview': True, 'asset_type': 'JPG', 'asset_subtype': 'large_cover', 'output_image_buffer': None }, { 'size': (2000, 2000), 'key': xl_cover, 'pillow_image_save_params': { 'format': 'JPEG', }, 'preview': True, 'asset_type': 'JPG', 'asset_subtype': 'xlarge_cover', 'output_image_buffer': None }, { 'size': None, 'key': 'images/tif/uuid4_{image_name}.tif'.format( image_name=image_name_mock), 'pillow_image_save_params': { 'format': 'TIFF', }, 'preview': False, 'asset_type': 'TIF', 'asset_subtype': None, 'output_image_buffer': None } ] @pytest.fixture def image_output_configs_mock_data(): """Correct image configs mock data.""" return [ { 'size': (2000, 2000), 'key': 'img/jpeg/uuid4_test_image_2000_2000.jpeg', 'pillow_image_save_params': { 'format': 'JPEG', }, }, { 'size': (110, 110), 'key': 'img/jpeg/uuid4_test_image_110_110.jpeg', 'pillow_image_save_params': { 'format': 'JPEG', }, }, { 'size': (360, 360), 'key': 'img/jpeg/uuid4_test_image_360_360.jpeg', 'pillow_image_save_params': { 'format': 'JPEG', }, }, { 'size': None, 'key': 'img/tif/uuid4_test_image.tif', 'pillow_image_save_params': { 'format': 'TIFF', }, } ] def test_transcode_image_with_resize_success(transcode_image_inputs): """Test transcode_image function success call with resize.""" # mocking icc_profile = 'ProPhoto RGB' resized_image = MagicMock() input_image = MagicMock() output_buffer_mock = MagicMock() input_image.info = {'icc_profile': icc_profile} input_image.resize.return_value = resized_image # test function call result = images_encoding.transcode_image( input_image, transcode_image_inputs, output_buffer_mock) # checking (input_image.resize.assert_any_call( transcode_image_inputs['size'], Image.LANCZOS)) resized_image.save.assert_any_call( output_buffer_mock, icc_profile=icc_profile, **transcode_image_inputs['pillow_image_save_params']) assert resized_image.close.called assert result == output_buffer_mock output_buffer_mock.seek.assert_called_once_with(0) def test_transcode_image_without_resize_success(transcode_image_inputs): """Test transcode_image function success call without resize.""" # mocking input_image = MagicMock() transcode_image_inputs['size'] = None output_buffer_mock = MagicMock() # test function call result = images_encoding.transcode_image( input_image, transcode_image_inputs, output_buffer_mock) # checking input_image.save.assert_any_call( output_buffer_mock, **transcode_image_inputs['pillow_image_save_params']) assert result == output_buffer_mock output_buffer_mock.seek.assert_called_once_with(0) @patch('PIL.ImageOps.exif_transpose') @patch('src.images_encoding.transcode_image') @patch('src.images_encoding.ensure_rgb_image_mode') @patch('src.images_encoding.Image') def test_convert( pil_image_class, ensure_rgb_image_mode, transcode_image, exif_transpose, image_output_configs_mock_data, ): """Test convert function call.""" # presetting file_object = MagicMock() output_buffers = [] for _ in image_output_configs_mock_data: output_buffers.append(MagicMock()) transcode_image.side_effect = output_buffers convertion_result = [] for output in zip(output_buffers, image_output_configs_mock_data): result = dict.copy(output[1]) result['output_image_buffer'] = output[0] convertion_result.append(result) # test function call result = images_encoding.convert( file_object, image_output_configs_mock_data, output_buffers) # checking pil_image_class.open.assert_called_once_with(file_object) exif_transpose.assert_called_once_with(pil_image_class.open.return_value.__enter__.return_value) ensure_rgb_image_mode.assert_called_once_with(exif_transpose.return_value) assert result == convertion_result for configuration in zip(output_buffers, image_output_configs_mock_data): transcode_image.assert_any_call( ensure_rgb_image_mode.return_value, configuration[1], configuration[0], dpi=transcoding_configs.DEFAULT_DENSITY) def _attach_icc_profile_to_image(image, icc_profile_name): """Attach ICC profile to image.""" icc_profiles_dir = Path(__file__).resolve().parent.parent / 'icc_profiles' if icc_profile_name == 'srgb': icc_profile_data = ImageCms.ImageCmsProfile(ImageCms.createProfile('sRGB')).tobytes() elif icc_profile_name == 'cmyk': with open(icc_profiles_dir / 'Generic CMYK Profile.icc', 'rb') as f: icc_profile_data = f.read() image.info['icc_profile'] = icc_profile_data return image @pytest.mark.parametrize(( 'description', 'image', 'expected_raise', ), [ ( 'ASDF mode', MagicMock(mode='ASDF'), pytest.raises(ValueError, match='^Unsupported image mode: ASDF. Image not processed.$'), ), ( 'RGBA mode with transparency used', Image.fromarray(np.full((100, 100, 4), [0, 0, 0, 254], dtype=np.uint8), mode='RGBA'), pytest.raises(ValueError, match='^Unsupported image mode: RGBA with non-opaque pixels. Image not processed.$'), ), ( 'RGBA mode with transparency not used', Image.fromarray(np.full((100, 100, 4), [0, 0, 0, 255], dtype=np.uint8), mode='RGBA'), does_not_raise(), ), ( 'LA mode with transparency used', Image.fromarray(np.full((100, 100, 2), [0, 254], dtype=np.uint8), mode='LA'), pytest.raises(ValueError, match='^Unsupported image mode: LA with non-opaque pixels. Image not processed.$'), ), ( 'PA mode with transparency used', Image.fromarray(np.full((100, 100, 2), [0, 254], dtype=np.uint8), mode='PA'), pytest.raises(ValueError, match='^Unsupported image mode: PA with non-opaque pixels. Image not processed.$'), ), ( 'RGB mode with no embedded ICC profile.', Image.fromarray(np.full((100, 100, 3), [0, 0, 0], dtype=np.uint8), mode='RGB'), does_not_raise(), ), ( 'RGB mode with incompatible embedded ICC profile.', _attach_icc_profile_to_image( Image.fromarray(np.full((100, 100, 3), [0, 0, 0], dtype=np.uint8), mode='RGB'), 'cmyk' ), does_not_raise(), ), ( 'RGB mode with compatible embedded ICC profile.', _attach_icc_profile_to_image( Image.fromarray(np.full((100, 100, 3), [0, 0, 0], dtype=np.uint8), mode='RGB'), 'srgb' ), does_not_raise(), ), ( 'CMYK mode with no embedded ICC profile.', Image.fromarray(np.full((100, 100, 4), [0, 0, 0, 0], dtype=np.uint8), mode='CMYK'), pytest.raises( ValueError, match=( '^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.$' ) ), ), ( 'CMYK mode with incompatible embedded ICC profile.', _attach_icc_profile_to_image( Image.fromarray(np.full((100, 100, 4), [0, 0, 0, 0], dtype=np.uint8), mode='CMYK'), 'srgb' ), pytest.raises( ValueError, match=( '^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.$' ) ), ), ( 'CMYK mode with compatible embedded ICC profile.', _attach_icc_profile_to_image( Image.fromarray(np.full((100, 100, 4), [0, 0, 0, 0], dtype=np.uint8), mode='CMYK'), 'cmyk' ), does_not_raise(), ), ( 'L mode with no embedded ICC profile.', Image.fromarray(np.full((100, 100), 0, dtype=np.uint8), mode='L'), does_not_raise(), ), ( 'P mode with no embedded ICC profile.', Image.fromarray(np.full((100, 100), 0, dtype=np.uint8), mode='P'), does_not_raise(), ), ( '1 mode', Image.fromarray(np.full((100, 100), 0, dtype=np.uint8), mode='1'), does_not_raise(), ) ]) def test_ensure_rgb_image_mode( description, image, expected_raise, ): """Test ensure_rgb_image_mode.""" with expected_raise: result = images_encoding.ensure_rgb_image_mode(image) assert result.mode == 'RGB' @patch('uuid.uuid4') def test_generate_transcoding_configs( uuid4_mock, input_image_descriptors_data, image_name_mock, expected_generate_transcoding_configs_result): """Test generate_transcoding_configs function call.""" # mocking input_key = 'input/img/{image_name}.jpeg'.format( image_name=image_name_mock) uuid4_mock.return_value = 'uuid-4' # test function call result = images_encoding.generate_transcoding_configs( input_key, input_image_descriptors_data) assert result == expected_generate_transcoding_configs_result @patch('uuid.uuid4') def test_generate_unique_sequence(uuid4_mock): """Test _generate_unique_sequence function call.""" # mocking uuid4_mock.return_value = 'uuid-4' expected_result = 'uuid4' # test function call result = images_encoding._generate_unique_sequence() # checking assert result == expected_result