upload: Replace exif_rotate with Pillow exif_transpose.

Fixes #18599.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg
2021-08-09 14:42:01 -07:00
committed by Tim Abbott
parent 6289803368
commit 14f0594795
6 changed files with 4 additions and 60 deletions

View File

@@ -25,7 +25,7 @@ from django.http import HttpRequest
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from jinja2.utils import Markup as mark_safe from jinja2.utils import Markup as mark_safe
from PIL import ExifTags, Image, ImageOps from PIL import Image, ImageOps
from PIL.GifImagePlugin import GifImageFile from PIL.GifImagePlugin import GifImageFile
from PIL.Image import DecompressionBombError from PIL.Image import DecompressionBombError
@@ -103,33 +103,10 @@ class BadImageError(JsonableError):
code = ErrorCode.BAD_IMAGE code = ErrorCode.BAD_IMAGE
name_to_tag_num = {name: num for num, name in ExifTags.TAGS.items()}
# https://stackoverflow.com/a/6218425
def exif_rotate(image: Image) -> Image:
if not hasattr(image, "_getexif"):
return image
exif_data = image._getexif()
if exif_data is None:
return image
exif_dict = dict(exif_data.items())
orientation = exif_dict.get(name_to_tag_num["Orientation"])
if orientation == 3:
return image.rotate(180, expand=True)
elif orientation == 6:
return image.rotate(270, expand=True)
elif orientation == 8:
return image.rotate(90, expand=True)
return image
def resize_avatar(image_data: bytes, size: int = DEFAULT_AVATAR_SIZE) -> bytes: def resize_avatar(image_data: bytes, size: int = DEFAULT_AVATAR_SIZE) -> bytes:
try: try:
im = Image.open(io.BytesIO(image_data)) im = Image.open(io.BytesIO(image_data))
im = exif_rotate(im) im = ImageOps.exif_transpose(im)
im = ImageOps.fit(im, (size, size), Image.ANTIALIAS) im = ImageOps.fit(im, (size, size), Image.ANTIALIAS)
except OSError: except OSError:
raise BadImageError(_("Could not decode image; did you upload an image file?")) raise BadImageError(_("Could not decode image; did you upload an image file?"))
@@ -145,7 +122,7 @@ def resize_avatar(image_data: bytes, size: int = DEFAULT_AVATAR_SIZE) -> bytes:
def resize_logo(image_data: bytes) -> bytes: def resize_logo(image_data: bytes) -> bytes:
try: try:
im = Image.open(io.BytesIO(image_data)) im = Image.open(io.BytesIO(image_data))
im = exif_rotate(im) im = ImageOps.exif_transpose(im)
im.thumbnail((8 * DEFAULT_AVATAR_SIZE, DEFAULT_AVATAR_SIZE), Image.ANTIALIAS) im.thumbnail((8 * DEFAULT_AVATAR_SIZE, DEFAULT_AVATAR_SIZE), Image.ANTIALIAS)
except OSError: except OSError:
raise BadImageError(_("Could not decode image; did you upload an image file?")) raise BadImageError(_("Could not decode image; did you upload an image file?"))
@@ -202,7 +179,7 @@ def resize_emoji(image_data: bytes, size: int = DEFAULT_EMOJI_SIZE) -> bytes:
) )
return resize_gif(im, size) if should_resize else image_data return resize_gif(im, size) if should_resize else image_data
else: else:
im = exif_rotate(im) im = ImageOps.exif_transpose(im)
im = ImageOps.fit(im, (size, size), Image.ANTIALIAS) im = ImageOps.fit(im, (size, size), Image.ANTIALIAS)
out = io.BytesIO() out = io.BytesIO()
im.save(out, format=image_format) im.save(out, format=image_format)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -51,7 +51,6 @@ from zerver.lib.upload import (
ZulipUploadBackend, ZulipUploadBackend,
delete_export_tarball, delete_export_tarball,
delete_message_image, delete_message_image,
exif_rotate,
resize_avatar, resize_avatar,
resize_emoji, resize_emoji,
sanitize_name, sanitize_name,
@@ -2153,38 +2152,6 @@ class UploadSpaceTests(UploadSerializeMixin, ZulipTestCase):
self.assert_length(data2, self.realm.currently_used_upload_space_bytes()) self.assert_length(data2, self.realm.currently_used_upload_space_bytes())
class ExifRotateTests(ZulipTestCase):
def test_image_do_not_rotate(self) -> None:
# Image does not have _getexif method.
with get_test_image_file("img.png") as f, Image.open(f) as img:
result = exif_rotate(img)
self.assertEqual(result, img)
# Image with no exif data.
with get_test_image_file("img_no_exif.jpg") as f, Image.open(f) as img:
result = exif_rotate(img)
self.assertEqual(result, img)
# Orientation of the image is 1.
with get_test_image_file("img.jpg") as f, Image.open(f) as img:
result = exif_rotate(img)
self.assertEqual(result, img)
def test_image_rotate(self) -> None:
with mock.patch("PIL.Image.Image.rotate") as rotate:
with get_test_image_file("img_orientation_3.jpg") as f, Image.open(f) as img:
exif_rotate(img)
rotate.assert_called_with(180, expand=True)
with get_test_image_file("img_orientation_6.jpg") as f, Image.open(f) as img:
exif_rotate(img)
rotate.assert_called_with(270, expand=True)
with get_test_image_file("img_orientation_8.jpg") as f, Image.open(f) as img:
exif_rotate(img)
rotate.assert_called_with(90, expand=True)
class DecompressionBombTests(ZulipTestCase): class DecompressionBombTests(ZulipTestCase):
def setUp(self) -> None: def setUp(self) -> None:
super().setUp() super().setUp()