From 5afc9b02215ea43720c69665e4569cf44cde36e3 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 24 Oct 2025 22:08:23 +1100 Subject: [PATCH] Automatically transpose images using EXIF data (#6975) * Automatically transpose images using EXIF data * Added test --------- Co-authored-by: Andrew Murray --- scrapy/pipelines/images.py | 8 +++++--- tests/test_pipeline_images.py | 22 +++++++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/scrapy/pipelines/images.py b/scrapy/pipelines/images.py index 450311e18..124a7f706 100644 --- a/scrapy/pipelines/images.py +++ b/scrapy/pipelines/images.py @@ -63,9 +63,10 @@ class ImagesPipeline(FilesPipeline): crawler: Crawler | None = None, ): try: - from PIL import Image # noqa: PLC0415 + from PIL import Image, ImageOps # noqa: PLC0415 self._Image = Image + self._ImageOps = ImageOps except ImportError: raise NotConfigured( "ImagesPipeline requires installing Pillow 8.0.0 or later" @@ -180,8 +181,9 @@ class ImagesPipeline(FilesPipeline): ) -> Iterable[tuple[str, Image.Image, BytesIO]]: path = self.file_path(request, response=response, info=info, item=item) orig_image = self._Image.open(BytesIO(response.body)) + transposed_image = self._ImageOps.exif_transpose(orig_image) - width, height = orig_image.size + width, height = transposed_image.size if width < self.min_width or height < self.min_height: raise ImageException( "Image too small " @@ -190,7 +192,7 @@ class ImagesPipeline(FilesPipeline): ) image, buf = self.convert_image( - orig_image, response_body=BytesIO(response.body) + transposed_image, response_body=BytesIO(response.body) ) yield path, image, buf diff --git a/tests/test_pipeline_images.py b/tests/test_pipeline_images.py index 3acb9bf7f..c633e867a 100644 --- a/tests/test_pipeline_images.py +++ b/tests/test_pipeline_images.py @@ -169,13 +169,33 @@ class TestImagesPipeline: path, new_im, new_buf = next(get_images_gen) assert path == "full/3fd165099d8e71b8a48b2683946e64dbfad8b52d.jpg" - assert orig_im == new_im + assert orig_im.copy() == new_im assert buf.getvalue() == new_buf.getvalue() thumb_path, thumb_img, thumb_buf = next(get_images_gen) assert thumb_path == "thumbs/small/3fd165099d8e71b8a48b2683946e64dbfad8b52d.jpg" assert orig_thumb_buf.getvalue() == thumb_buf.getvalue() + def test_get_transposed_images(self): + orig_im = Image.new("RGB", (2, 2), (0, 0, 0)) + orig_im.putpixel((1, 1), (255, 0, 0)) + exif = orig_im.getexif() + exif[274] = 3 + buf = io.BytesIO() + orig_im.save(buf, "PNG", exif=exif) + buf.seek(0) + + resp = Response(url="https://dev.mydeco.com/mydeco.gif", body=buf.getvalue()) + req = Request(url="https://dev.mydeco.com/mydeco.gif") + + get_images_gen = self.pipeline.get_images( + response=resp, request=req, info=object() + ) + + path, new_im, _ = next(get_images_gen) + assert path == "full/3fd165099d8e71b8a48b2683946e64dbfad8b52d.jpg" + assert new_im.getpixel((0, 0)) == (255, 0, 0) + def test_convert_image(self): SIZE = (100, 100) # straight forward case: RGB and JPEG