Skip to content

Commit a26c5f8

Browse files
authored
Merge pull request #51 from Lucas-C/hardening
2 parents a8ba5a5 + 4aaa8b0 commit a26c5f8

File tree

5 files changed

+63
-7
lines changed

5 files changed

+63
-7
lines changed

RELEASE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Release type: patch
2+
3+
*bug*: handling edge cases where image is location is non-local to the Pelican site source,
4+
does not exist, or cannot be identified by Pillow.

pelican/plugins/image_process/image_process.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import subprocess
1818
import sys
1919

20-
from PIL import Image, ImageFilter
20+
from PIL import Image, ImageFilter, UnidentifiedImageError
2121
from bs4 import BeautifulSoup
2222
import six
2323
from six.moves.urllib_parse import unquote, urlparse
@@ -46,8 +46,9 @@ class ExifTool(object):
4646
def start_exiftool():
4747
if shutil.which("exiftool") is None:
4848
log.warning(
49-
"EXIF tags will not be copied because the exiftool program could not "
50-
"be found. Please install exiftool and make sure it is in your path."
49+
"[image_process] EXIF tags will not be copied because "
50+
"the exiftool program could not be found. "
51+
"Please install exiftool and make sure it is in your path."
5152
)
5253
else:
5354
ExifTool._instance = ExifTool()
@@ -110,7 +111,9 @@ def _send_command(self, params):
110111
output += os.read(fd, ExifTool.block_size)
111112
exiftool_result = output.strip()[: -len(ExifTool.sentinel)]
112113
log.debug(
113-
"image_process: exiftool result: {}".format(exiftool_result.decode("utf-8"))
114+
"[image_process] exiftool result: {}".format(
115+
exiftool_result.decode("utf-8")
116+
)
114117
)
115118

116119

@@ -260,7 +263,7 @@ def apply_filter(i, f):
260263

261264

262265
def harvest_images(path, context):
263-
log.debug("process_images: harvest %r", path)
266+
log.debug("[image_process] harvesting %r", path)
264267
# Set default value for 'IMAGE_PROCESS_DIR'.
265268
if "IMAGE_PROCESS_DIR" not in context:
266269
context["IMAGE_PROCESS_DIR"] = "derivatives"
@@ -412,6 +415,12 @@ def compute_paths(img, settings, derivative):
412415

413416
def process_img_tag(img, settings, derivative):
414417
path = compute_paths(img, settings, derivative)
418+
if not is_img_identifiable(path.source):
419+
log.warn(
420+
"[image_process] Skipping image %s that could not be identified by Pillow",
421+
path.source,
422+
)
423+
return
415424
process = settings["IMAGE_PROCESS"][derivative]
416425

417426
img["src"] = posixpath.join(path.base_url, path.filename)
@@ -423,16 +432,30 @@ def process_img_tag(img, settings, derivative):
423432
process_image((path.source, destination, process), settings)
424433

425434

435+
def is_img_identifiable(img_filepath):
436+
try:
437+
Image.open(img_filepath)
438+
return True
439+
except (FileNotFoundError, UnidentifiedImageError):
440+
return False
441+
442+
426443
def build_srcset(img, settings, derivative):
427444
path = compute_paths(img, settings, derivative)
445+
if not is_img_identifiable(path.source):
446+
log.warn(
447+
"[image_process] Skipping image %s that could not be identified by Pillow",
448+
path.source,
449+
)
450+
return
428451
process = settings["IMAGE_PROCESS"][derivative]
429452

430453
default = process["default"]
431454
if isinstance(default, six.string_types):
432455
breakpoints = {i for i, _ in process["srcset"]}
433456
if default not in breakpoints:
434457
log.error(
435-
'image_process: srcset "%s" does not define default "%s"',
458+
'[image_process] srcset "%s" does not define default "%s"',
436459
derivative,
437460
default,
438461
)
@@ -672,7 +695,7 @@ def process_image(image, settings):
672695
image[1] = unquote(image[1])
673696
# image[2] is the transformation
674697

675-
log.debug("image_process: {} -> {}".format(image[0], image[1]))
698+
log.debug("[image_process] {} -> {}".format(image[0], image[1]))
676699

677700
os.makedirs(os.path.dirname(image[1]), exist_ok=True)
678701

319 KB
Loading
Lines changed: 1 addition & 0 deletions
Loading

pelican/plugins/image_process/test_image_process.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
from pelican.plugins.image_process import (
1212
ExifTool,
13+
compute_paths,
1314
harvest_images_in_fragment,
15+
is_img_identifiable,
1416
process_image,
1517
)
1618

@@ -87,6 +89,7 @@ def test_all_transforms(tmp_path, transform_id, transform_params, image_path):
8789

8890
transformed = Image.open(destination_path)
8991
expected = Image.open(expected_path)
92+
# Image.getbbox() returns None if there are only black pixels in the image:
9093
image_diff = ImageChops.difference(transformed, expected).getbbox()
9194
assert image_diff is None
9295

@@ -115,6 +118,11 @@ def test_all_transforms(tmp_path, transform_id, transform_params, image_path):
115118
],
116119
)
117120
def test_path_normalization(mocker, orig_src, orig_img, new_src, new_img):
121+
# Allow non-existing images to be processed:
122+
mocker.patch(
123+
"pelican.plugins.image_process.image_process.is_img_identifiable",
124+
lambda img_filepath: True,
125+
)
118126
# Silence image transforms.
119127
process = mocker.patch("pelican.plugins.image_process.image_process.process_image")
120128

@@ -431,6 +439,11 @@ def test_path_normalization(mocker, orig_src, orig_img, new_src, new_img):
431439
],
432440
)
433441
def test_picture_generation(mocker, orig_tag, new_tag, call_args):
442+
# Allow non-existing images to be processed:
443+
mocker.patch(
444+
"pelican.plugins.image_process.image_process.is_img_identifiable",
445+
lambda img_filepath: True,
446+
)
434447
process = mocker.patch("pelican.plugins.image_process.image_process.process_image")
435448

436449
settings = get_settings(
@@ -543,3 +556,18 @@ def test_copy_exif_tags(tmp_path, image_path, copy_tags):
543556
assert expected_tags[tag] == actual_tags[tag]
544557
else:
545558
assert tag not in actual_tags.keys()
559+
560+
561+
def test_is_img_identifiable():
562+
for test_image in TEST_IMAGES:
563+
assert is_img_identifiable(test_image)
564+
565+
assert not is_img_identifiable("image/that/does/not/exist.png")
566+
567+
assert not is_img_identifiable(TEST_DATA.joinpath("folded_puzzle.png"))
568+
assert not is_img_identifiable(TEST_DATA.joinpath("minimal.svg"))
569+
570+
img = {"src": "https://upload.wikimedia.org/wikipedia/commons/3/34/Exemple.png"}
571+
settings = get_settings(IMAGE_PROCESS_DIR="derivatives")
572+
path = compute_paths(img, settings, derivative="thumb")
573+
assert not is_img_identifiable(path.source)

0 commit comments

Comments
 (0)