import json import os from pathlib import Path import shutil import subprocess import warnings from PIL import Image, ImageChops import pytest from pelican.plugins.image_process import ( ExifTool, compute_paths, harvest_images_in_fragment, is_img_identifiable, process_image, ) # Prepare test image constants. HERE = Path(__file__).resolve().parent TEST_DATA = HERE.joinpath("test_data").resolve() TEST_IMAGES = [ TEST_DATA.joinpath(f"pelican-bird.{ext}").resolve() for ext in ["jpg", "png"] ] EXIF_TEST_IMAGES = [ TEST_DATA.joinpath("exif", f"pelican-bird.{ext}").resolve() for ext in ["jpg", "png"] ] TRANSFORM_RESULTS = TEST_DATA.joinpath("results").resolve() # Register all supported transforms. SINGLE_TRANSFORMS = { "crop": ["crop 10 20 100 200"], "flip_horizontal": ["flip_horizontal"], "flip_vertical": ["flip_vertical"], "grayscale": ["grayscale"], "resize": ["resize 200 250"], "rotate": ["rotate 20"], "scale_in": ["scale_in 200 250 False"], "scale_out": ["scale_out 200 250 False"], "blur": ["blur"], "contour": ["contour"], "detail": ["detail"], "edge_enhance": ["edge_enhance"], "edge_enhance_more": ["edge_enhance_more"], "emboss": ["emboss"], "find_edges": ["find_edges"], "smooth": ["smooth"], "smooth_more": ["smooth_more"], "sharpen": ["sharpen"], } def get_settings(**kwargs): """Provide tweaked setting dictionaries for testing Set keyword arguments to override specific settings. """ DEFAULT_CONFIG = { "PATH": HERE, "OUTPUT_PATH": "output", "static_content": {}, "filenames": {}, "SITEURL": "//", "IMAGE_PROCESS": SINGLE_TRANSFORMS, } settings = DEFAULT_CONFIG.copy() settings.update(kwargs) return settings def test_undefined_transform(): settings = get_settings() tag = "" with pytest.raises(RuntimeError): harvest_images_in_fragment(tag, settings) @pytest.mark.parametrize("transform_id, transform_params", SINGLE_TRANSFORMS.items()) @pytest.mark.parametrize("image_path", TEST_IMAGES) def test_all_transforms(tmp_path, transform_id, transform_params, image_path): """Test the raw transform and their results on images.""" settings = get_settings() image_name = image_path.name destination_path = tmp_path.joinpath(transform_id, image_name) expected_path = TRANSFORM_RESULTS.joinpath(transform_id, image_name) process_image((str(image_path), str(destination_path), transform_params), settings) transformed = Image.open(destination_path) expected = Image.open(expected_path) # Image.getbbox() returns None if there are only black pixels in the image: image_diff = ImageChops.difference(transformed, expected).getbbox() assert image_diff is None @pytest.mark.parametrize( "orig_src, orig_img, new_src, new_img", [ ( "/tmp/test.jpg", "tmp/test.jpg", "/tmp/derivatives/crop/test.jpg", "tmp/derivatives/crop/test.jpg", ), ( "../tmp/test.jpg", "../tmp/test.jpg", "../tmp/derivatives/crop/test.jpg", "../tmp/derivatives/crop/test.jpg", ), ( "http://xxx/tmp/test.jpg", "tmp/test.jpg", "http://xxx/tmp/derivatives/crop/test.jpg", "tmp/derivatives/crop/test.jpg", ), ], ) def test_path_normalization(mocker, orig_src, orig_img, new_src, new_img): # Allow non-existing images to be processed: mocker.patch( "pelican.plugins.image_process.image_process.is_img_identifiable", lambda img_filepath: True, ) # Silence image transforms. process = mocker.patch("pelican.plugins.image_process.image_process.process_image") settings = get_settings(IMAGE_PROCESS_DIR="derivatives") img_tag_orig = ( '' ) img_tag_processed = harvest_images_in_fragment(img_tag_orig, settings) assert img_tag_processed == ( f'' ) process.assert_called_once_with( ( os.path.join(settings["PATH"], orig_img), os.path.join(settings["OUTPUT_PATH"], new_img), SINGLE_TRANSFORMS["crop"], ), settings, ) COMPLEX_TRANSFORMS = { "thumb": ["crop 0 0 50% 50%", "scale_out 150 150", "crop 0 0 150 150"], "article-image": {"type": "image", "ops": ["scale_in 300 300"]}, "crisp": { "type": "responsive-image", "srcset": [ ("1x", ["scale_in 800 600 True"]), ("2x", ["scale_in 1600 1200 True"]), ("4x", ["scale_in 3200 2400 True"]), ], "default": "1x", }, "crisp2": { "type": "responsive-image", "srcset": [ ("1x", ["scale_in 800 600 True"]), ("2x", ["scale_in 1600 1200 True"]), ("4x", ["scale_in 3200 2400 True"]), ], "default": ["scale_in 400 300 True"], }, "large-photo": { "type": "responsive-image", "sizes": ( "(min-width: 1200px) 800px, " "(min-width: 992px) 650px, " "(min-width: 768px) 718px, " "100vw" ), "srcset": [ ("600w", ["scale_in 600 450 True"]), ("800w", ["scale_in 800 600 True"]), ("1600w", ["scale_in 1600 1200 True"]), ], "default": "800w", }, "pict": { "type": "picture", "sources": [ { "name": "default", "media": "(min-width: 640px)", "srcset": [ ("640w", ["scale_in 640 480 True"]), ("1024w", ["scale_in 1024 683 True"]), ("1600w", ["scale_in 1600 1200 True"]), ], "sizes": "100vw", }, { "name": "source-1", "srcset": [ ("1x", ["crop 100 100 200 200"]), ("2x", ["crop 100 100 300 300"]), ], }, ], "default": ("default", "640w"), }, "pict2": { "type": "picture", "sources": [ { "name": "default", "media": "(min-width: 640px)", "srcset": [ ("640w", ["scale_in 640 480 True"]), ("1024w", ["scale_in 1024 683 True"]), ("1600w", ["scale_in 1600 1200 True"]), ], "sizes": "100vw", }, { "name": "source-2", "srcset": [ ("1x", ["crop 100 100 200 200"]), ("2x", ["crop 100 100 300 300"]), ], }, ], "default": ("source-2", ["scale_in 800 600 True"]), }, } @pytest.mark.parametrize( "orig_tag, new_tag, call_args", [ ( '', '', [ ( "tmp/test.jpg", "tmp/derivs/thumb/test.jpg", ["crop 0 0 50% 50%", "scale_out 150 150", "crop 0 0 150 150"], ), ], ), ( '', '', [ ( "tmp/test.jpg", "tmp/derivs/article-image/test.jpg", ["scale_in 300 300"], ), ], ), ( '', '', [ ( "tmp/test.jpg", "tmp/derivs/crisp/1x/test.jpg", ["scale_in 800 600 True"], ), ( "tmp/test.jpg", "tmp/derivs/crisp/2x/test.jpg", ["scale_in 1600 1200 True"], ), ( "tmp/test.jpg", "tmp/derivs/crisp/4x/test.jpg", ["scale_in 3200 2400 True"], ), ], ), ( '', '', # default must be first, because the process execute it first [ ( "tmp/test.jpg", "tmp/derivs/crisp2/default/test.jpg", ["scale_in 400 300 True"], ), ( "tmp/test.jpg", "tmp/derivs/crisp2/1x/test.jpg", ["scale_in 800 600 True"], ), ( "tmp/test.jpg", "tmp/derivs/crisp2/2x/test.jpg", ["scale_in 1600 1200 True"], ), ( "tmp/test.jpg", "tmp/derivs/crisp2/4x/test.jpg", ["scale_in 3200 2400 True"], ), ], ), ( '', '', [ ( "tmp/test.jpg", "tmp/derivs/large-photo/600w/test.jpg", ["scale_in 600 450 True"], ), ( "tmp/test.jpg", "tmp/derivs/large-photo/800w/test.jpg", ["scale_in 800 600 True"], ), ( "tmp/test.jpg", "tmp/derivs/large-photo/1600w/test.jpg", ["scale_in 1600 1200 True"], ), ], ), ( '", '", [ ( "images/pelican.jpg", "images/derivs/pict/default/640w/pelican.jpg", ["scale_in 640 480 True"], ), ( "images/pelican.jpg", "images/derivs/pict/default/1024w/pelican.jpg", ["scale_in 1024 683 True"], ), ( "images/pelican.jpg", "images/derivs/pict/default/1600w/pelican.jpg", ["scale_in 1600 1200 True"], ), ( "images/pelican-closeup.jpg", "images/derivs/pict/source-1/1x/pelican-closeup.jpg", ["crop 100 100 200 200"], ), ( "images/pelican-closeup.jpg", "images/derivs/pict/source-1/2x/pelican-closeup.jpg", ["crop 100 100 300 300"], ), ], ), ( '