Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,9 @@ IMAGE_PROCESS_COPY_EXIF_TAGS = True
Note that `exiftool` prior to version 12.46 cannot write WebP images, so if you work
with WebP images, you should use version 12.46 or later.

When copying EXIF tags, the plugin will update `ExifImageWidth` and
`ExifImageHeight` tags to reflect the dimensions of the transformed image.

#### Modifying the `class` Attribute of Processed Images

By default, *Image Process* adds the `image-process-<transform>`
Expand Down Expand Up @@ -520,9 +523,12 @@ IMAGE_PROCESS_ADD_CLASS = False

## Contributing

Contributions are welcome and much appreciated. Every little bit helps. You can contribute by improving the documentation, adding missing features, and fixing bugs. You can also help out by reviewing and commenting on [existing issues][].
Contributions are welcome and much appreciated. Every little bit helps. You can
contribute by improving the documentation, adding missing features, and fixing bugs.
You can also help out by reviewing and commenting on [existing issues][].

To start contributing to this plugin, review the [Contributing to Pelican][] documentation, beginning with the **Contributing Code** section.
To start contributing to this plugin, review the [Contributing to Pelican][]
documentation, beginning with the **Contributing Code** section.

[existing issues]: https://github.com/pelican-plugins/image-process/issues
[Contributing to Pelican]: https://docs.getpelican.com/en/latest/contribute.html
Expand Down
27 changes: 23 additions & 4 deletions pelican/plugins/image_process/image_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def __init__(self):
with contextlib.suppress(LookupError):
codecs.lookup_error("surrogateescape")

with open(os.devnull, "w") as devnull:
with open(os.devnull, "w"):
self.process = subprocess.Popen(
[
"exiftool",
Expand All @@ -89,7 +89,7 @@ def __init__(self):
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=devnull,
stderr=subprocess.STDOUT,
)

def __del__(self):
Expand All @@ -101,21 +101,40 @@ def _copy_tags(self, src, dst):
params = (
b"-TagsFromFile",
src.encode(self.encoding, ExifTool.errors),
b'"-all:all>all:all"',
dst.encode(self.encoding, ExifTool.errors),
)
self._send_command(params)
params = (
b"-ExifImageWidth<ImageWidth",
b"-if",
b'"$ExifImageWidth"',
dst.encode(self.encoding, ExifTool.errors),
)
self._send_command(params)
params = (
b"-ExifImageHeight<ImageHeight",
b"-if",
b'"$ExifImageHeight"',
dst.encode(self.encoding, ExifTool.errors),
)
self._send_command(params)
params = (b"-delete_original!", dst.encode(self.encoding, ExifTool.errors))
self._send_command(params)

def _send_command(self, params):
self.process.stdin.write(b"\n".join((*params, b"-j\n", b"-execute\n")))
joined_params = b"\n".join((*params, b"-j\n", b"-execute\n"))
self.process.stdin.write(joined_params)
self.process.stdin.flush()

output = b""
fd = self.process.stdout.fileno()
while not output.strip().endswith(ExifTool.sentinel):
output += os.read(fd, ExifTool.block_size)
exiftool_result = output.strip()[: -len(ExifTool.sentinel)]

logger.debug(
"{} exiftool command: {}".format(LOG_PREFIX, joined_params.decode("utf-8"))
)
logger.debug(
"{} exiftool result: {}".format(LOG_PREFIX, exiftool_result.decode("utf-8"))
)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
91 changes: 78 additions & 13 deletions pelican/plugins/image_process/test_image_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
TEST_DATA.joinpath("exif", f"pelican-bird.{ext}").resolve()
for ext in SUPPORTED_EXIF_IMAGE_FORMATS
]
NOEXIF_TEST_IMAGES = [
TEST_DATA.joinpath("noexif", f"pelican-bird.{ext}").resolve()
for ext in SUPPORTED_EXIF_IMAGE_FORMATS
]
TRANSFORM_RESULTS = TEST_DATA.joinpath("results").resolve()

# Register all supported transforms.
Expand Down Expand Up @@ -708,30 +712,35 @@ def test_copy_exif_tags(tmp_path, image_path, copy_tags):
return

# A few EXIF tags to test for.
exif_tags = [
"Artist",
"Creator",
"Title",
"Description",
"Subject",
"Rating",
exif_tags = ["Artist", "Creator", "Title", "Description", "Subject", "Rating"]

size_tags = [
"ExifImageWidth",
"ExifImageHeight",
"ImageWidth",
"ImageHeight",
]

settings = get_settings(IMAGE_PROCESS_COPY_EXIF_TAGS=copy_tags)

transform_id = "grayscale"
transform_params = ["grayscale"]
# Use a transform that changes image dimensions.
transform_id = "scale_in"
transform_params = ["scale_in 200 250 False"]
image_name = image_path.name
destination_path = tmp_path.joinpath(transform_id, image_name)

expected_results = subprocess.run(
["exiftool", "-json", image_path], stdout=subprocess.PIPE, check=False
)
expected_tags = json.loads(expected_results.stdout)[0]
for tag in exif_tags:
for tag in exif_tags + size_tags:
assert tag in expected_tags

assert "ExifImageWidth" in expected_tags
assert "ExifImageHeight" in expected_tags
assert expected_tags["ExifImageWidth"] == expected_tags["ImageWidth"]
assert expected_tags["ExifImageHeight"] == expected_tags["ImageHeight"]

if copy_tags:
ExifTool.start_exiftool()
process_image((str(image_path), str(destination_path), transform_params), settings)
Expand All @@ -745,12 +754,68 @@ def test_copy_exif_tags(tmp_path, image_path, copy_tags):
assert actual_results.returncode == 0

actual_tags = json.loads(actual_results.stdout)[0]
for tag in exif_tags:
for tag in exif_tags + size_tags:
if copy_tags:
assert tag in actual_tags
assert expected_tags[tag] == actual_tags[tag]
else:
assert tag not in actual_tags
assert tag in {"ImageWidth", "ImageHeight"} or tag not in actual_tags

if copy_tags:
for tag in exif_tags:
assert expected_tags[tag] == actual_tags[tag]

# Dimensions should differ due to the transform.
assert expected_tags["ExifImageWidth"] != actual_tags["ExifImageWidth"]
assert expected_tags["ImageWidth"] != actual_tags["ImageWidth"]
assert expected_tags["ExifImageHeight"] != actual_tags["ExifImageHeight"]
assert expected_tags["ImageHeight"] != actual_tags["ImageHeight"]

# Check that the exif size tags are equal in the real size tags.
assert actual_tags["ExifImageWidth"] == actual_tags["ImageWidth"]
assert actual_tags["ExifImageHeight"] == actual_tags["ImageHeight"]


@pytest.mark.parametrize("image_path", NOEXIF_TEST_IMAGES)
def test_copy_exif_tags_does_not_add_exif_dims_tags(tmp_path, image_path):
if shutil.which("exiftool") is None:
warnings.warn(
"EXIF tags copying will not be tested because the exiftool program could "
"not be found. Please install exiftool and make sure it is in your path.",
stacklevel=2,
)
return

settings = get_settings(IMAGE_PROCESS_COPY_EXIF_TAGS=True)

transform_id = "scale_in"
transform_params = ["scale_in 200 250 False"]
image_name = image_path.name
destination_path = tmp_path.joinpath(transform_id, image_name)

expected_results = subprocess.run(
["exiftool", "-json", image_path], stdout=subprocess.PIPE, check=False
)
expected_tags = json.loads(expected_results.stdout)[0]

# We want to verify that dimension tags are not added
# if they are not initially present.
assert "ExifImageWidth" not in expected_tags
assert "ExifImageHeight" not in expected_tags

ExifTool.start_exiftool()
process_image((str(image_path), str(destination_path), transform_params), settings)
ExifTool.stop_exiftool()

actual_results = subprocess.run(
["exiftool", "-json", destination_path], stdout=subprocess.PIPE, check=False
)

assert actual_results.returncode == 0

actual_tags = json.loads(actual_results.stdout)[0]

assert "ExifImageWidth" not in actual_tags
assert "ExifImageHeight" not in actual_tags


def test_try_open_image():
Expand Down