From 6dedfa71f73b6ad9062438cd3be2ce792a6e5765 Mon Sep 17 00:00:00 2001 From: Patrick Fournier Date: Fri, 31 Oct 2025 23:34:55 -0400 Subject: [PATCH 1/2] Add width and height attributes on simple images. For images with a single source, add the width and height attributes to the img tag in order to help browsers compute the page layout more quickly. If the width and height attributes were already present on the img tag, they are left untouched. --- .../plugins/image_process/image_process.py | 16 ++++-- .../image_process/test_image_process.py | 51 +++++++++++-------- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/pelican/plugins/image_process/image_process.py b/pelican/plugins/image_process/image_process.py index 82b7c93..fac356d 100644 --- a/pelican/plugins/image_process/image_process.py +++ b/pelican/plugins/image_process/image_process.py @@ -429,7 +429,12 @@ def process_img_tag(img, settings, derivative): if not isinstance(process, list): process = process["ops"] - process_image((path.source, destination, process), settings) + image_size = process_image((path.source, destination, process), settings) + if image_size: + if "width" not in img.attrs: + img["width"] = image_size[0] + if "height" not in img.attrs: + img["height"] = image_size[1] def format_srcset_element(path, condition): @@ -714,7 +719,7 @@ def process_image(image, settings): try: i = try_open_image(image[0]) except (UnidentifiedImageError, FileNotFoundError): - return + return None for step in image[2]: if callable(step): @@ -731,8 +736,11 @@ def process_image(image, settings): i.save(image[1], progressive=True) ExifTool.copy_tags(image[0], image[1]) - else: - logger.debug(f"{LOG_PREFIX} Skipping {image[0]} -> {image[1]}") + return i.width, i.height + + logger.debug(f"{LOG_PREFIX} Skipping {image[0]} -> {image[1]}") + i = Image.open(image[1]) + return i.width, i.height def dump_config(pelican): diff --git a/pelican/plugins/image_process/test_image_process.py b/pelican/plugins/image_process/test_image_process.py index d238363..f3cab2b 100644 --- a/pelican/plugins/image_process/test_image_process.py +++ b/pelican/plugins/image_process/test_image_process.py @@ -5,6 +5,7 @@ import subprocess import warnings +from bs4 import BeautifulSoup from PIL import Image, UnidentifiedImageError import pytest @@ -196,9 +197,8 @@ def test_path_normalization(mocker, orig_src, orig_img, new_src, new_img): img_tag_processed = harvest_images_in_fragment(img_tag_orig, settings) - assert img_tag_processed == ( - f'' - ) + soup = BeautifulSoup(img_tag_processed, "html.parser") + assert soup.img["src"] == new_src process.assert_called_once_with( ( @@ -299,8 +299,10 @@ def test_path_normalization(mocker, orig_src, orig_img, new_src, new_img): "orig_tag, new_tag, call_args", [ ( - '', - '', + '', + # Mock height and width added after processing. + '', [ ( "tmp/test.jpg", @@ -310,9 +312,11 @@ def test_path_normalization(mocker, orig_src, orig_img, new_src, new_img): ], ), ( - '', - '', + '', + # Pre-existing height and width unchanged after processing. + '', [ ( "tmp/test.jpg", @@ -502,6 +506,7 @@ def test_path_normalization(mocker, orig_src, orig_img, new_src, new_img): def test_html_and_pictures_generation(mocker, orig_tag, new_tag, call_args): """Tests that the generated html is as expected and the images are processed.""" process = mocker.patch("pelican.plugins.image_process.image_process.process_image") + process.return_value = (512, 384) settings = get_settings( IMAGE_PROCESS=COMPLEX_TRANSFORMS, IMAGE_PROCESS_DIR="derivs" @@ -528,22 +533,23 @@ def test_html_and_pictures_generation(mocker, orig_tag, new_tag, call_args): # src attribute with no quotes, spaces or commas. ( '', - '', + '', ), # src attribute with double quotes, spaces and commas. ( '', - '", + '', ), # src attribute with single and double quotes, spaces and commas. ( '', - '', + '', ), # srcset attribute with no quotes, spaces or commas. ( @@ -647,7 +653,8 @@ def test_special_chars_in_image_path_are_handled_properly(mocker, orig_tag, new_ Related to issue #78 https://github.com/pelican-plugins/image-process/issues/78 """ - mocker.patch("pelican.plugins.image_process.image_process.process_image") + process = mocker.patch("pelican.plugins.image_process.image_process.process_image") + process.return_value = (512, 384) settings = get_settings( IMAGE_PROCESS=COMPLEX_TRANSFORMS, IMAGE_PROCESS_DIR="derivs" @@ -776,7 +783,8 @@ def test_try_open_image(): [ ( '', - '', + '', [ # Default settings. {}, {"IMAGE_PROCESS_ADD_CLASS": True}, @@ -789,7 +797,8 @@ def test_try_open_image(): ), ( '', - '', + '', [ # Custom class prefix. {"IMAGE_PROCESS_CLASS_PREFIX": "custom-prefix-"}, { @@ -800,14 +809,15 @@ def test_try_open_image(): ), ( '', - '', + '', [ # Special case: empty string as class prefix. {"IMAGE_PROCESS_CLASS_PREFIX": ""}, ], ), ( '', - '', + '', [ # No class added. {"IMAGE_PROCESS_ADD_CLASS": False}, {"IMAGE_PROCESS_ADD_CLASS": False, "IMAGE_PROCESS_CLASS_PREFIX": ""}, @@ -818,7 +828,8 @@ def test_try_open_image(): def test_class_settings(mocker, orig_tag, new_tag, setting_overrides): """Test the IMAGE_PROCESS_ADD_CLASS and IMAGE_PROCESS_CLASS_PREFIX settings.""" # Silence image transforms. - mocker.patch("pelican.plugins.image_process.image_process.process_image") + process = mocker.patch("pelican.plugins.image_process.image_process.process_image") + process.return_value = (512, 384) for override in setting_overrides: settings = get_settings(**override) From f4484d561fb24443df115c54ba9460ad1f1bea2f Mon Sep 17 00:00:00 2001 From: MinchinWeb Date: Mon, 3 Nov 2025 10:25:57 -0700 Subject: [PATCH 2/2] Add doc string for `process_image()` --- pelican/plugins/image_process/image_process.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pelican/plugins/image_process/image_process.py b/pelican/plugins/image_process/image_process.py index fac356d..9dac200 100644 --- a/pelican/plugins/image_process/image_process.py +++ b/pelican/plugins/image_process/image_process.py @@ -700,6 +700,12 @@ def try_open_image(path): def process_image(image, settings): + """Actually process the image. + + Copies over the Exif tags, if ExifTool is available. + + Returns (int, int): tuple of the width and height of the resulting image. + """ # remove URL encoding to get to physical filenames image = list(image) image[0] = unquote(image[0])