Skip to content

Commit

Permalink
Merge pull request #7092 from radarhere/exif_transpose
Browse files Browse the repository at this point in the history
Added in_place argument to ImageOps.exif_transpose()
  • Loading branch information
mergify[bot] committed Jun 14, 2023
2 parents 2aacab1 + 7d97fa8 commit 561986e
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 30 deletions.
Binary file added Tests/images/orientation_rectangle.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions Tests/test_imageops.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,18 @@ def check(orientation_im):
assert 0x0112 not in transposed_im.getexif()


def test_exif_transpose_in_place():
with Image.open("Tests/images/orientation_rectangle.jpg") as im:
assert im.size == (2, 1)
assert im.getexif()[0x0112] == 8
expected = im.rotate(90, expand=True)

ImageOps.exif_transpose(im, in_place=True)
assert im.size == (1, 2)
assert 0x0112 not in im.getexif()
assert_image_equal(im, expected)


def test_autocontrast_cutoff():
# Test the cutoff argument of autocontrast
with Image.open("Tests/images/bw_gradient.png") as img:
Expand Down
4 changes: 2 additions & 2 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -1433,12 +1433,12 @@ def getexif(self):
self._exif.load(exif_info)

# XMP tags
if 0x0112 not in self._exif:
if ExifTags.Base.Orientation not in self._exif:
xmp_tags = self.info.get("XML:com.adobe.xmp")
if xmp_tags:
match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
if match:
self._exif[0x0112] = int(match[2])
self._exif[ExifTags.Base.Orientation] = int(match[2])

return self._exif

Expand Down
57 changes: 32 additions & 25 deletions src/PIL/ImageOps.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import operator
import re

from . import Image, ImagePalette
from . import ExifTags, Image, ImagePalette

#
# helpers
Expand Down Expand Up @@ -576,19 +576,20 @@ def solarize(image, threshold=128):
return _lut(image, lut)


def exif_transpose(image):
def exif_transpose(image, *, in_place=False):
"""
If an image has an EXIF Orientation tag, other than 1, return a new image
that is transposed accordingly. The new image will have the orientation
data removed.
Otherwise, return a copy of the image.
If an image has an EXIF Orientation tag, other than 1, transpose the image
accordingly, and remove the orientation data.
:param image: The image to transpose.
:return: An image.
"""
exif = image.getexif()
orientation = exif.get(0x0112)
:param in_place: Boolean. Keyword-only argument.
If ``True``, the original image is modified in-place, and ``None`` is returned.
If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned
with the transposition applied. If there is no transposition, a copy of the
image will be returned.
"""
image_exif = image.getexif()
orientation = image_exif.get(ExifTags.Base.Orientation)
method = {
2: Image.Transpose.FLIP_LEFT_RIGHT,
3: Image.Transpose.ROTATE_180,
Expand All @@ -600,22 +601,28 @@ def exif_transpose(image):
}.get(orientation)
if method is not None:
transposed_image = image.transpose(method)
transposed_exif = transposed_image.getexif()
if 0x0112 in transposed_exif:
del transposed_exif[0x0112]
if "exif" in transposed_image.info:
transposed_image.info["exif"] = transposed_exif.tobytes()
elif "Raw profile type exif" in transposed_image.info:
transposed_image.info[
"Raw profile type exif"
] = transposed_exif.tobytes().hex()
elif "XML:com.adobe.xmp" in transposed_image.info:
if in_place:
image.im = transposed_image.im
image.pyaccess = None
image._size = transposed_image._size
exif_image = image if in_place else transposed_image

exif = exif_image.getexif()
if ExifTags.Base.Orientation in exif:
del exif[ExifTags.Base.Orientation]
if "exif" in exif_image.info:
exif_image.info["exif"] = exif.tobytes()
elif "Raw profile type exif" in exif_image.info:
exif_image.info["Raw profile type exif"] = exif.tobytes().hex()
elif "XML:com.adobe.xmp" in exif_image.info:
for pattern in (
r'tiff:Orientation="([0-9])"',
r"<tiff:Orientation>([0-9])</tiff:Orientation>",
):
transposed_image.info["XML:com.adobe.xmp"] = re.sub(
pattern, "", transposed_image.info["XML:com.adobe.xmp"]
exif_image.info["XML:com.adobe.xmp"] = re.sub(
pattern, "", exif_image.info["XML:com.adobe.xmp"]
)
return transposed_image
return image.copy()
if not in_place:
return transposed_image
elif not in_place:
return image.copy()
6 changes: 3 additions & 3 deletions src/PIL/TiffImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
from fractions import Fraction
from numbers import Number, Rational

from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16
from ._binary import i32be as i32
from ._binary import o8
Expand Down Expand Up @@ -1185,7 +1185,7 @@ def get_photoshop_blocks(self):
:returns: Photoshop "Image Resource Blocks" in a dictionary.
"""
blocks = {}
val = self.tag_v2.get(0x8649)
val = self.tag_v2.get(ExifTags.Base.ImageResources)
if val:
while val[:4] == b"8BIM":
id = i16(val[4:6])
Expand Down Expand Up @@ -1550,7 +1550,7 @@ def _setup(self):
palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
self.palette = ImagePalette.raw("RGB;L", b"".join(palette))

self._tile_orientation = self.tag_v2.get(0x0112)
self._tile_orientation = self.tag_v2.get(ExifTags.Base.Orientation)


#
Expand Down

0 comments on commit 561986e

Please sign in to comment.