From 48b312c01f919733a6087131afd232b1e79be96a Mon Sep 17 00:00:00 2001 From: Patrick Fournier Date: Tue, 11 Nov 2025 13:58:27 -0500 Subject: [PATCH 01/11] Add metadata processing for image URLs. --- README.md | 28 +++++++++ .../plugins/image_process/image_process.py | 52 ++++++++++++++-- .../image_process/test_image_process.py | 61 ++++++++++++++++++- 3 files changed, 134 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a4809c2..3a967a6 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,9 @@ classes. It then maps the classes to a set of image processing instructions, computes new images, and modifies HTML code according to the instructions. +It can optionally scan the metadata of your content to update image URLs +there. + ### Define Transformations The first step in using this module is to define some image @@ -517,6 +520,31 @@ IMAGE_PROCESS_CLASS_PREFIX = "custom-prefix-" IMAGE_PROCESS_ADD_CLASS = False ``` +#### Updating Image URLs in Metadata + +If you want *Image Process* to process and update image URLs in the metadata +of your content (for example, in the `og_image` field used by the `pelican-open_graph` plugin), +you can set the `IMAGE_PROCESS_UPDATE_METADATA` setting to a dictionary mapping +metadata fields to transformation names. For example: + +```python +IMAGE_PROCESS_UPDATE_METADATA = { + "og_image": "og-image-transform", +} +``` + +The transformation must be defined in the `IMAGE_PROCESS` setting as usual, and it must be +an image replacement transformation (i.e., of type `image`). + +It is possible to override the transformation applied to a metadata field by prefixing +the metadata value with `{transform-name}`. For example, if you have defined +`IMAGE_PROCESS_UPDATE_METADATA` as above, you can override the transformation for a specific article +by setting the `og_image` metadata field to `{other-transform}/path/to/image.jpg`. + +If you only want to process metadata in some articles, you can set the transformation to `None` +in `IMAGE_PROCESS_UPDATE_METADATA` and then specify the desired transformation in the metadata +field using the `{transform-name}` prefix. + ## Known Issues * Pillow, when resizing animated GIF files, [does not return an animated file](https://github.com/pelican-plugins/image-process/issues/11). diff --git a/pelican/plugins/image_process/image_process.py b/pelican/plugins/image_process/image_process.py index 520e727..d8c4e36 100644 --- a/pelican/plugins/image_process/image_process.py +++ b/pelican/plugins/image_process/image_process.py @@ -389,15 +389,15 @@ def harvest_images_in_fragment(fragment, settings): return str(soup) -def compute_paths(img, settings, derivative): +def compute_paths(image_url, settings, derivative): process_dir = settings["IMAGE_PROCESS_DIR"] - img_src = urlparse(img["src"]) + img_src = urlparse(image_url) img_src_path = url2pathname(img_src.path.lstrip("/")) _img_src_dirname, filename = os.path.split(img_src_path) derivative_path = os.path.join(process_dir, derivative) # urljoin truncates leading ../ elements base_url = posixpath.join( - posixpath.dirname(img["src"]), pathname2url(str(derivative_path)) + posixpath.dirname(image_url), pathname2url(str(derivative_path)) ) PELICAN_V4 = 4 @@ -439,7 +439,7 @@ def compute_paths(img, settings, derivative): def process_img_tag(img, settings, derivative): - path = compute_paths(img, settings, derivative) + path = compute_paths(img["src"], settings, derivative) process = settings["IMAGE_PROCESS"][derivative] img["src"] = posixpath.join(path.base_url, path.filename) @@ -465,7 +465,7 @@ def format_srcset_element(path, condition): def build_srcset(img, settings, derivative): - path = compute_paths(img, settings, derivative) + path = compute_paths(img["src"], settings, derivative) process = settings["IMAGE_PROCESS"][derivative] default = process["default"] @@ -768,6 +768,47 @@ def process_image(image, settings): return i.width, i.height +def process_metadata(generator, metadata): + set_default_settings(generator.context) + metadata_to_process = generator.context.get("IMAGE_PROCESS_METADATA", {}).keys() + + for key, value in metadata.items(): + if key in metadata_to_process: + derivative = generator.context["IMAGE_PROCESS_METADATA"][key] + # If value starts with {some-other-derivative}, override derivative + if value.startswith("{") and "}" in value: + end_brace = value.index("}") + derivative = value[1:end_brace] + value = value[end_brace + 1 :].lstrip() # noqa: PLW2901 + + if derivative is None: + continue + + try: + process = generator.context["IMAGE_PROCESS"][derivative] + except KeyError as e: + raise RuntimeError(f"Derivative {derivative} undefined.") from e + + if not ( + isinstance(process, list) + or (isinstance(process, dict) and process.type == "image") + ): + raise RuntimeError( + f'IMAGE_PROCESS_METADATA "{key}" must reference a transformation ' + 'of type "image".' + ) + + path = compute_paths(value, generator.context, derivative) + + metadata[key] = posixpath.join(path.base_url, path.filename) + destination = os.path.join(str(path.base_path), path.filename) + + if not isinstance(process, list): + process = process["ops"] + + process_image((path.source, destination, process), generator.context) + + def dump_config(pelican): set_default_settings(pelican.settings) @@ -779,6 +820,7 @@ def dump_config(pelican): def register(): + signals.article_generator_context.connect(process_metadata) signals.content_written.connect(harvest_images) signals.feed_written.connect(harvest_feed_images) signals.finalized.connect(dump_config) diff --git a/pelican/plugins/image_process/test_image_process.py b/pelican/plugins/image_process/test_image_process.py index fb67f47..d5e262a 100644 --- a/pelican/plugins/image_process/test_image_process.py +++ b/pelican/plugins/image_process/test_image_process.py @@ -14,6 +14,7 @@ compute_paths, harvest_images_in_fragment, process_image, + process_metadata, set_default_settings, try_open_image, ) @@ -836,9 +837,9 @@ def test_try_open_image(): assert not try_open_image(TEST_DATA.joinpath("folded_puzzle.png")) assert not try_open_image(TEST_DATA.joinpath("minimal.svg")) - img = {"src": "https://upload.wikimedia.org/wikipedia/commons/3/34/Exemple.png"} + img_path = "https://upload.wikimedia.org/wikipedia/commons/3/34/Exemple.png" settings = get_settings(IMAGE_PROCESS_DIR="derivatives") - path = compute_paths(img, settings, derivative="thumb") + path = compute_paths(img_path, settings, derivative="thumb") with pytest.raises(FileNotFoundError): assert not try_open_image(path.source) @@ -901,6 +902,62 @@ def test_class_settings(mocker, orig_tag, new_tag, setting_overrides): assert harvest_images_in_fragment(orig_tag, settings) == new_tag +@pytest.mark.parametrize( + "orig_metadata, new_metadata, setting_overrides, should_process, transform_id", + [ + ( + {"title": "Test Article"}, + {"title": "Test Article"}, + {"IMAGE_PROCESS_METADATA": {"og_image": "crop"}}, + False, + None, + ), + ( + {"og_image": "/photos/test-image.jpg"}, + {"og_image": "/photos/derivatives/crop/test-image.jpg"}, + {"IMAGE_PROCESS_METADATA": {"og_image": "crop"}}, + True, + "crop", + ), + ( + {"og_image": "{resize}/photos/test-image.jpg"}, + {"og_image": "/photos/derivatives/resize/test-image.jpg"}, + {"IMAGE_PROCESS_METADATA": {"og_image": "crop"}}, + True, + "resize", + ), + ], +) +def test_process_metadata_image( # noqa: PLR0913 + mocker, orig_metadata, new_metadata, setting_overrides, should_process, transform_id +): + # Silence image transforms. + process = mocker.patch("pelican.plugins.image_process.image_process.process_image") + + settings = get_settings(**setting_overrides) + + fake_generator = mocker.MagicMock() + fake_generator.context = settings + processed_metadata = orig_metadata.copy() + process_metadata(fake_generator, processed_metadata) + + assert processed_metadata == new_metadata + + if should_process: + path = orig_metadata["og_image"] + if path.startswith("{") and "}" in path: + path = path.split("}", 1)[1] + + process.assert_called_once_with( + ( + os.path.join(settings["PATH"], path[1:]), + os.path.join(settings["OUTPUT_PATH"], new_metadata["og_image"][1:]), + SINGLE_TRANSFORMS[transform_id], + ), + settings, + ) + + def generate_test_images(): settings = get_settings() image_count = 0 From 3dcf90f9dd81cf3c40971dc68b3ef2355ac64d75 Mon Sep 17 00:00:00 2001 From: Patrick Fournier Date: Tue, 11 Nov 2025 22:38:47 -0500 Subject: [PATCH 02/11] Save original values. --- README.md | 3 +++ pelican/plugins/image_process/image_process.py | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a967a6..3d83660 100644 --- a/README.md +++ b/README.md @@ -545,6 +545,9 @@ If you only want to process metadata in some articles, you can set the transform in `IMAGE_PROCESS_UPDATE_METADATA` and then specify the desired transformation in the metadata field using the `{transform-name}` prefix. +The original metadata values are placed in the `image_process_original_metadata` dictionary +of the content object, so that you can access them later if needed. + ## Known Issues * Pillow, when resizing animated GIF files, [does not return an animated file](https://github.com/pelican-plugins/image-process/issues/11). diff --git a/pelican/plugins/image_process/image_process.py b/pelican/plugins/image_process/image_process.py index d8c4e36..7879f77 100644 --- a/pelican/plugins/image_process/image_process.py +++ b/pelican/plugins/image_process/image_process.py @@ -772,6 +772,8 @@ def process_metadata(generator, metadata): set_default_settings(generator.context) metadata_to_process = generator.context.get("IMAGE_PROCESS_METADATA", {}).keys() + original_values = {} + for key, value in metadata.items(): if key in metadata_to_process: derivative = generator.context["IMAGE_PROCESS_METADATA"][key] @@ -791,7 +793,7 @@ def process_metadata(generator, metadata): if not ( isinstance(process, list) - or (isinstance(process, dict) and process.type == "image") + or (isinstance(process, dict) and process["type"] == "image") ): raise RuntimeError( f'IMAGE_PROCESS_METADATA "{key}" must reference a transformation ' @@ -800,6 +802,7 @@ def process_metadata(generator, metadata): path = compute_paths(value, generator.context, derivative) + original_values[key] = value metadata[key] = posixpath.join(path.base_url, path.filename) destination = os.path.join(str(path.base_path), path.filename) @@ -808,6 +811,9 @@ def process_metadata(generator, metadata): process_image((path.source, destination, process), generator.context) + if original_values: + metadata["image_process_original_metadata"] = original_values + def dump_config(pelican): set_default_settings(pelican.settings) From e9773df2ddfddf67779c751e6f68ba8dc0511bc4 Mon Sep 17 00:00:00 2001 From: Patrick Fournier Date: Wed, 12 Nov 2025 09:05:23 -0500 Subject: [PATCH 03/11] Make sure the image URL is absolute. --- pelican/plugins/image_process/image_process.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pelican/plugins/image_process/image_process.py b/pelican/plugins/image_process/image_process.py index 7879f77..286f639 100644 --- a/pelican/plugins/image_process/image_process.py +++ b/pelican/plugins/image_process/image_process.py @@ -18,7 +18,7 @@ import subprocess import sys import urllib -from urllib.parse import unquote, urlparse +from urllib.parse import unquote, urlparse, urljoin from urllib.request import pathname2url, url2pathname from bs4 import BeautifulSoup @@ -771,6 +771,7 @@ def process_image(image, settings): def process_metadata(generator, metadata): set_default_settings(generator.context) metadata_to_process = generator.context.get("IMAGE_PROCESS_METADATA", {}).keys() + site_url = generator.context.get("SITEURL", "") original_values = {} @@ -803,7 +804,7 @@ def process_metadata(generator, metadata): path = compute_paths(value, generator.context, derivative) original_values[key] = value - metadata[key] = posixpath.join(path.base_url, path.filename) + metadata[key] = urljoin(site_url, posixpath.join(path.base_url, path.filename)) destination = os.path.join(str(path.base_path), path.filename) if not isinstance(process, list): From c5b9ca23aaea2473bd6272b366c73c7a427ad266 Mon Sep 17 00:00:00 2001 From: Patrick Fournier Date: Wed, 12 Nov 2025 09:26:11 -0500 Subject: [PATCH 04/11] Update doc --- README.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3d83660..024199a 100644 --- a/README.md +++ b/README.md @@ -520,12 +520,12 @@ IMAGE_PROCESS_CLASS_PREFIX = "custom-prefix-" IMAGE_PROCESS_ADD_CLASS = False ``` -#### Updating Image URLs in Metadata +#### Converting Image Paths to URLs in Metadata -If you want *Image Process* to process and update image URLs in the metadata +If you want *Image Process* to process images in the metadata of your content (for example, in the `og_image` field used by the `pelican-open_graph` plugin), you can set the `IMAGE_PROCESS_UPDATE_METADATA` setting to a dictionary mapping -metadata fields to transformation names. For example: +metadata field names to transformation names. For example: ```python IMAGE_PROCESS_UPDATE_METADATA = { @@ -534,18 +534,21 @@ IMAGE_PROCESS_UPDATE_METADATA = { ``` The transformation must be defined in the `IMAGE_PROCESS` setting as usual, and it must be -an image replacement transformation (i.e., of type `image`). +an image replacement transformation (i.e., of type `image`). *Image Process* will look for the specified +metadata fields in your content and will apply the specified transformation +to the image path found in the metadata value. -It is possible to override the transformation applied to a metadata field by prefixing +It is possible to override the transformation applied to a specific instance of a metadata field by prefixing the metadata value with `{transform-name}`. For example, if you have defined `IMAGE_PROCESS_UPDATE_METADATA` as above, you can override the transformation for a specific article -by setting the `og_image` metadata field to `{other-transform}/path/to/image.jpg`. +by setting its `og_image` metadata value to `{other-transform}/path/to/image.jpg`. -If you only want to process metadata in some articles, you can set the transformation to `None` -in `IMAGE_PROCESS_UPDATE_METADATA` and then specify the desired transformation in the metadata -field using the `{transform-name}` prefix. +If you only want to process metadata fields for some articles, you can set the transformation to `None` +in `IMAGE_PROCESS_UPDATE_METADATA` and add the `{transform-name}` prefix to the metadata value of +selected articles. -The original metadata values are placed in the `image_process_original_metadata` dictionary +*Image Process* will update the metadata field to contain the URL of the transformed image. +The original metadata values are saved in the `image_process_original_metadata` dictionary of the content object, so that you can access them later if needed. ## Known Issues From 824407b1bea644a623805870112dfd5cffe2ef0c Mon Sep 17 00:00:00 2001 From: Patrick Fournier Date: Wed, 12 Nov 2025 09:39:57 -0500 Subject: [PATCH 05/11] Fix format and test. --- pelican/plugins/image_process/image_process.py | 6 ++++-- .../plugins/image_process/test_image_process.py | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/pelican/plugins/image_process/image_process.py b/pelican/plugins/image_process/image_process.py index 286f639..542275a 100644 --- a/pelican/plugins/image_process/image_process.py +++ b/pelican/plugins/image_process/image_process.py @@ -18,7 +18,7 @@ import subprocess import sys import urllib -from urllib.parse import unquote, urlparse, urljoin +from urllib.parse import unquote, urljoin, urlparse from urllib.request import pathname2url, url2pathname from bs4 import BeautifulSoup @@ -804,7 +804,9 @@ def process_metadata(generator, metadata): path = compute_paths(value, generator.context, derivative) original_values[key] = value - metadata[key] = urljoin(site_url, posixpath.join(path.base_url, path.filename)) + metadata[key] = urljoin( + site_url, posixpath.join(path.base_url, path.filename) + ) destination = os.path.join(str(path.base_path), path.filename) if not isinstance(process, list): diff --git a/pelican/plugins/image_process/test_image_process.py b/pelican/plugins/image_process/test_image_process.py index d5e262a..e5a2974 100644 --- a/pelican/plugins/image_process/test_image_process.py +++ b/pelican/plugins/image_process/test_image_process.py @@ -914,14 +914,24 @@ def test_class_settings(mocker, orig_tag, new_tag, setting_overrides): ), ( {"og_image": "/photos/test-image.jpg"}, - {"og_image": "/photos/derivatives/crop/test-image.jpg"}, + { + "og_image": "/photos/derivatives/crop/test-image.jpg", + "image_process_original_metadata": { + "og_image": "/photos/test-image.jpg" + }, + }, {"IMAGE_PROCESS_METADATA": {"og_image": "crop"}}, True, "crop", ), ( {"og_image": "{resize}/photos/test-image.jpg"}, - {"og_image": "/photos/derivatives/resize/test-image.jpg"}, + { + "og_image": "/photos/derivatives/resize/test-image.jpg", + "image_process_original_metadata": { + "og_image": "/photos/test-image.jpg" + }, + }, {"IMAGE_PROCESS_METADATA": {"og_image": "crop"}}, True, "resize", From 1a6a75a5eb85cb44150906c5ea7e5203e0a42861 Mon Sep 17 00:00:00 2001 From: Patrick Fournier Date: Wed, 12 Nov 2025 23:47:53 -0500 Subject: [PATCH 06/11] Avoid using exotic `//` as SITEURL. In Python 3.14, `urllib.parse.urljoin` handles `//` differently than in previous versions. --- .../image_process/test_image_process.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/pelican/plugins/image_process/test_image_process.py b/pelican/plugins/image_process/test_image_process.py index e5a2974..f06fb6a 100644 --- a/pelican/plugins/image_process/test_image_process.py +++ b/pelican/plugins/image_process/test_image_process.py @@ -91,7 +91,7 @@ def get_settings(**kwargs): "OUTPUT_PATH": "output", "static_content": {}, "filenames": {}, - "SITEURL": "//", + "SITEURL": "https://www.example.com", "IMAGE_PROCESS": SINGLE_TRANSFORMS, } settings = DEFAULT_CONFIG.copy() @@ -903,7 +903,8 @@ def test_class_settings(mocker, orig_tag, new_tag, setting_overrides): @pytest.mark.parametrize( - "orig_metadata, new_metadata, setting_overrides, should_process, transform_id", + "orig_metadata, new_metadata, setting_overrides, should_process, transform_id, " + "expected_output_path", [ ( {"title": "Test Article"}, @@ -911,11 +912,12 @@ def test_class_settings(mocker, orig_tag, new_tag, setting_overrides): {"IMAGE_PROCESS_METADATA": {"og_image": "crop"}}, False, None, + None, ), ( {"og_image": "/photos/test-image.jpg"}, { - "og_image": "/photos/derivatives/crop/test-image.jpg", + "og_image": "https://www.example.com/photos/derivatives/crop/test-image.jpg", "image_process_original_metadata": { "og_image": "/photos/test-image.jpg" }, @@ -923,11 +925,12 @@ def test_class_settings(mocker, orig_tag, new_tag, setting_overrides): {"IMAGE_PROCESS_METADATA": {"og_image": "crop"}}, True, "crop", + "photos/derivatives/crop/test-image.jpg", ), ( {"og_image": "{resize}/photos/test-image.jpg"}, { - "og_image": "/photos/derivatives/resize/test-image.jpg", + "og_image": "https://www.example.com/photos/derivatives/resize/test-image.jpg", "image_process_original_metadata": { "og_image": "/photos/test-image.jpg" }, @@ -935,11 +938,18 @@ def test_class_settings(mocker, orig_tag, new_tag, setting_overrides): {"IMAGE_PROCESS_METADATA": {"og_image": "crop"}}, True, "resize", + "photos/derivatives/resize/test-image.jpg", ), ], ) def test_process_metadata_image( # noqa: PLR0913 - mocker, orig_metadata, new_metadata, setting_overrides, should_process, transform_id + mocker, + orig_metadata, + new_metadata, + setting_overrides, + should_process, + transform_id, + expected_output_path, ): # Silence image transforms. process = mocker.patch("pelican.plugins.image_process.image_process.process_image") @@ -961,7 +971,7 @@ def test_process_metadata_image( # noqa: PLR0913 process.assert_called_once_with( ( os.path.join(settings["PATH"], path[1:]), - os.path.join(settings["OUTPUT_PATH"], new_metadata["og_image"][1:]), + os.path.join(settings["OUTPUT_PATH"], expected_output_path), SINGLE_TRANSFORMS[transform_id], ), settings, From 1e0a0c8253969093d59a13ebdbf09093682b52a0 Mon Sep 17 00:00:00 2001 From: Patrick Fournier Date: Wed, 12 Nov 2025 23:51:28 -0500 Subject: [PATCH 07/11] Test that original metadata is preserved. --- pelican/plugins/image_process/test_image_process.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pelican/plugins/image_process/test_image_process.py b/pelican/plugins/image_process/test_image_process.py index f06fb6a..c39a759 100644 --- a/pelican/plugins/image_process/test_image_process.py +++ b/pelican/plugins/image_process/test_image_process.py @@ -977,6 +977,8 @@ def test_process_metadata_image( # noqa: PLR0913 settings, ) + assert processed_metadata["image_process_original_metadata"]["og_image"] == path + def generate_test_images(): settings = get_settings() From 8421835b822960fa75f69a7eb76a7f27e5d86fa0 Mon Sep 17 00:00:00 2001 From: Patrick Fournier Date: Thu, 13 Nov 2025 00:02:03 -0500 Subject: [PATCH 08/11] Apply Copilot suggestions --- README.md | 8 ++++---- pelican/plugins/image_process/image_process.py | 8 +++++++- pelican/plugins/image_process/test_image_process.py | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 024199a..fa60de5 100644 --- a/README.md +++ b/README.md @@ -524,11 +524,11 @@ IMAGE_PROCESS_ADD_CLASS = False If you want *Image Process* to process images in the metadata of your content (for example, in the `og_image` field used by the `pelican-open_graph` plugin), -you can set the `IMAGE_PROCESS_UPDATE_METADATA` setting to a dictionary mapping +you can set the `IMAGE_PROCESS_METADATA` setting to a dictionary mapping metadata field names to transformation names. For example: ```python -IMAGE_PROCESS_UPDATE_METADATA = { +IMAGE_PROCESS_METADATA = { "og_image": "og-image-transform", } ``` @@ -540,11 +540,11 @@ to the image path found in the metadata value. It is possible to override the transformation applied to a specific instance of a metadata field by prefixing the metadata value with `{transform-name}`. For example, if you have defined -`IMAGE_PROCESS_UPDATE_METADATA` as above, you can override the transformation for a specific article +`IMAGE_PROCESS_METADATA` as above, you can override the transformation for a specific article by setting its `og_image` metadata value to `{other-transform}/path/to/image.jpg`. If you only want to process metadata fields for some articles, you can set the transformation to `None` -in `IMAGE_PROCESS_UPDATE_METADATA` and add the `{transform-name}` prefix to the metadata value of +in `IMAGE_PROCESS_METADATA` and add the `{transform-name}` prefix to the metadata value of selected articles. *Image Process* will update the metadata field to contain the URL of the transformed image. diff --git a/pelican/plugins/image_process/image_process.py b/pelican/plugins/image_process/image_process.py index 542275a..1c757d1 100644 --- a/pelican/plugins/image_process/image_process.py +++ b/pelican/plugins/image_process/image_process.py @@ -390,6 +390,12 @@ def harvest_images_in_fragment(fragment, settings): def compute_paths(image_url, settings, derivative): + # Backwards compatibility: accept either a string (image_url) or + # a dict (img with "src" key) + if isinstance(image_url, dict): + image_url = image_url.get("src", "") + logger.warning(f"{LOG_PREFIX} Deprecated use of dict for image_url.") + process_dir = settings["IMAGE_PROCESS_DIR"] img_src = urlparse(image_url) img_src_path = url2pathname(img_src.path.lstrip("/")) @@ -776,7 +782,7 @@ def process_metadata(generator, metadata): original_values = {} for key, value in metadata.items(): - if key in metadata_to_process: + if isinstance(value, str) and key in metadata_to_process: derivative = generator.context["IMAGE_PROCESS_METADATA"][key] # If value starts with {some-other-derivative}, override derivative if value.startswith("{") and "}" in value: diff --git a/pelican/plugins/image_process/test_image_process.py b/pelican/plugins/image_process/test_image_process.py index c39a759..eb352b8 100644 --- a/pelican/plugins/image_process/test_image_process.py +++ b/pelican/plugins/image_process/test_image_process.py @@ -966,7 +966,7 @@ def test_process_metadata_image( # noqa: PLR0913 if should_process: path = orig_metadata["og_image"] if path.startswith("{") and "}" in path: - path = path.split("}", 1)[1] + path = path.split("}", 1)[1].lstrip() process.assert_called_once_with( ( From 0ded056177658272428a4004ec251ff9f37a1d36 Mon Sep 17 00:00:00 2001 From: Patrick Fournier Date: Thu, 13 Nov 2025 09:57:17 -0500 Subject: [PATCH 09/11] Apply @justinmayer review suggestion. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa60de5..b721cc0 100644 --- a/README.md +++ b/README.md @@ -523,7 +523,7 @@ IMAGE_PROCESS_ADD_CLASS = False #### Converting Image Paths to URLs in Metadata If you want *Image Process* to process images in the metadata -of your content (for example, in the `og_image` field used by the `pelican-open_graph` plugin), +of your content (for example, in the `og_image` field used by the `seo` and `pelican-open_graph` plugins), you can set the `IMAGE_PROCESS_METADATA` setting to a dictionary mapping metadata field names to transformation names. For example: From dc4ec42b0cf27e9f0699936ba2a4fe885dc15279 Mon Sep 17 00:00:00 2001 From: Patrick Fournier Date: Mon, 17 Nov 2025 14:59:32 -0500 Subject: [PATCH 10/11] Apply review suggestions from @minchinweb. - Clarify the feature documentation in README.md. - Ignore Pelican special linking directives. - Add a test for Pelican special linking directives. --- README.md | 39 +++++++++++++++---- .../plugins/image_process/image_process.py | 18 +++++++++ .../image_process/test_image_process.py | 9 +++++ 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b721cc0..b424f19 100644 --- a/README.md +++ b/README.md @@ -523,31 +523,54 @@ IMAGE_PROCESS_ADD_CLASS = False #### Converting Image Paths to URLs in Metadata If you want *Image Process* to process images in the metadata -of your content (for example, in the `og_image` field used by the `seo` and `pelican-open_graph` plugins), +of your content (for example, in the `og_image` field used by the `seo` and `pelican-open_graph` plugins), you can set the `IMAGE_PROCESS_METADATA` setting to a dictionary mapping -metadata field names to transformation names. For example: +metadata field names to transformation names. The transformation must be defined +in the `IMAGE_PROCESS` setting as usual, and it must be +an image replacement transformation (i.e., of type `image`). +For example: ```python +# pelicanconf.py + IMAGE_PROCESS_METADATA = { "og_image": "og-image-transform", } + +IMAGE_PROCESS = { + 'og-image-transform': {"type": "image", + "ops": ["scale_in 800 640 True"], + }, + # ... possibly other transformations ... +} ``` -The transformation must be defined in the `IMAGE_PROCESS` setting as usual, and it must be -an image replacement transformation (i.e., of type `image`). *Image Process* will look for the specified +*Image Process* will look for the specified metadata fields in your content and will apply the specified transformation to the image path found in the metadata value. It is possible to override the transformation applied to a specific instance of a metadata field by prefixing -the metadata value with `{transform-name}`. For example, if you have defined +the metadata value with `{transformation-name}`, where `transformation-name` is the name +of a transformation in the `IMAGE_PROCESS` dictionary. For example, if you have defined `IMAGE_PROCESS_METADATA` as above, you can override the transformation for a specific article -by setting its `og_image` metadata value to `{other-transform}/path/to/image.jpg`. +by setting its `og_image` metadata value to `{some-special-transformation}/path/to/image.jpg`, +where `some-special-transformation` is a transformation defined in the `IMAGE_PROCESS` +dictionary. Here is an example article using this feature: + +```markdown +# Example article +Title: Example Article +Date: 2024-06-01 +og_image: {some-special-transformation}/images/special-image.jpg + +This article uses a special image for Open Graph. +``` If you only want to process metadata fields for some articles, you can set the transformation to `None` -in `IMAGE_PROCESS_METADATA` and add the `{transform-name}` prefix to the metadata value of +in `IMAGE_PROCESS_METADATA` and add a `{transform-name}` prefix to the metadata value of selected articles. -*Image Process* will update the metadata field to contain the URL of the transformed image. +*Image Process* will update the metadata field to the URL of the transformed image. The original metadata values are saved in the `image_process_original_metadata` dictionary of the content object, so that you can access them later if needed. diff --git a/pelican/plugins/image_process/image_process.py b/pelican/plugins/image_process/image_process.py index 1c757d1..644fc5e 100644 --- a/pelican/plugins/image_process/image_process.py +++ b/pelican/plugins/image_process/image_process.py @@ -793,6 +793,24 @@ def process_metadata(generator, metadata): if derivative is None: continue + # Ignore Pelican special linking directives to avoid conflicts. + # Extracted from Pelican function _link_replacer() in contents.py + special_file_locations = { + "filename", + "attach", + "static", + "category", + "tag", + "author", + "index", + } + if derivative in special_file_locations: + logger.warning( + f"{LOG_PREFIX} Skipping metadata key '{key}' " + f"because it uses Pelican linking directive '{derivative}'." + ) + continue + try: process = generator.context["IMAGE_PROCESS"][derivative] except KeyError as e: diff --git a/pelican/plugins/image_process/test_image_process.py b/pelican/plugins/image_process/test_image_process.py index eb352b8..cfc2868 100644 --- a/pelican/plugins/image_process/test_image_process.py +++ b/pelican/plugins/image_process/test_image_process.py @@ -940,6 +940,15 @@ def test_class_settings(mocker, orig_tag, new_tag, setting_overrides): "resize", "photos/derivatives/resize/test-image.jpg", ), + # Ignore Pelican special linking directives like {static} and {attach}. + ( + {"og_image": "{static}/photos/test-image.jpg"}, + {"og_image": "{static}/photos/test-image.jpg"}, + {"IMAGE_PROCESS_METADATA": {"og_image": "crop"}}, + False, + None, + None, + ), ], ) def test_process_metadata_image( # noqa: PLR0913 From 81fd081205660d35a41408ad9f9cdafff49300ed Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Tue, 18 Nov 2025 09:45:14 +0100 Subject: [PATCH 11/11] Prepare release --- RELEASE.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..a461a08 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,3 @@ +Release type: minor + +- Process images in content metadata via new `IMAGE_PROCESS_METADATA` setting