Merge pull request #7092 from radarhere/exif_transpose

Added in_place argument to ImageOps.exif_transpose()
This commit is contained in:
mergify[bot] 2023-06-14 07:24:47 +00:00 committed by GitHub
commit 561986ee71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 48 additions and 29 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

View File

@ -404,6 +404,18 @@ def test_exif_transpose():
assert 0x0112 not in transposed_im.getexif() 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(): def test_autocontrast_cutoff():
# Test the cutoff argument of autocontrast # Test the cutoff argument of autocontrast
with Image.open("Tests/images/bw_gradient.png") as img: with Image.open("Tests/images/bw_gradient.png") as img:

View File

@ -1433,12 +1433,12 @@ class Image:
self._exif.load(exif_info) self._exif.load(exif_info)
# XMP tags # 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") xmp_tags = self.info.get("XML:com.adobe.xmp")
if xmp_tags: if xmp_tags:
match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
if match: if match:
self._exif[0x0112] = int(match[2]) self._exif[ExifTags.Base.Orientation] = int(match[2])
return self._exif return self._exif

View File

@ -21,7 +21,7 @@ import functools
import operator import operator
import re import re
from . import Image, ImagePalette from . import ExifTags, Image, ImagePalette
# #
# helpers # helpers
@ -576,19 +576,20 @@ def solarize(image, threshold=128):
return _lut(image, lut) 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 If an image has an EXIF Orientation tag, other than 1, transpose the image
that is transposed accordingly. The new image will have the orientation accordingly, and remove the orientation data.
data removed.
Otherwise, return a copy of the image.
:param image: The image to transpose. :param image: The image to transpose.
:return: An image. :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.
""" """
exif = image.getexif() image_exif = image.getexif()
orientation = exif.get(0x0112) orientation = image_exif.get(ExifTags.Base.Orientation)
method = { method = {
2: Image.Transpose.FLIP_LEFT_RIGHT, 2: Image.Transpose.FLIP_LEFT_RIGHT,
3: Image.Transpose.ROTATE_180, 3: Image.Transpose.ROTATE_180,
@ -600,22 +601,28 @@ def exif_transpose(image):
}.get(orientation) }.get(orientation)
if method is not None: if method is not None:
transposed_image = image.transpose(method) transposed_image = image.transpose(method)
transposed_exif = transposed_image.getexif() if in_place:
if 0x0112 in transposed_exif: image.im = transposed_image.im
del transposed_exif[0x0112] image.pyaccess = None
if "exif" in transposed_image.info: image._size = transposed_image._size
transposed_image.info["exif"] = transposed_exif.tobytes() exif_image = image if in_place else transposed_image
elif "Raw profile type exif" in transposed_image.info:
transposed_image.info[ exif = exif_image.getexif()
"Raw profile type exif" if ExifTags.Base.Orientation in exif:
] = transposed_exif.tobytes().hex() del exif[ExifTags.Base.Orientation]
elif "XML:com.adobe.xmp" in transposed_image.info: 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 ( for pattern in (
r'tiff:Orientation="([0-9])"', r'tiff:Orientation="([0-9])"',
r"<tiff:Orientation>([0-9])</tiff:Orientation>", r"<tiff:Orientation>([0-9])</tiff:Orientation>",
): ):
transposed_image.info["XML:com.adobe.xmp"] = re.sub( exif_image.info["XML:com.adobe.xmp"] = re.sub(
pattern, "", transposed_image.info["XML:com.adobe.xmp"] pattern, "", exif_image.info["XML:com.adobe.xmp"]
) )
if not in_place:
return transposed_image return transposed_image
elif not in_place:
return image.copy() return image.copy()

View File

@ -49,7 +49,7 @@ from collections.abc import MutableMapping
from fractions import Fraction from fractions import Fraction
from numbers import Number, Rational 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 i16be as i16
from ._binary import i32be as i32 from ._binary import i32be as i32
from ._binary import o8 from ._binary import o8
@ -1185,7 +1185,7 @@ class TiffImageFile(ImageFile.ImageFile):
:returns: Photoshop "Image Resource Blocks" in a dictionary. :returns: Photoshop "Image Resource Blocks" in a dictionary.
""" """
blocks = {} blocks = {}
val = self.tag_v2.get(0x8649) val = self.tag_v2.get(ExifTags.Base.ImageResources)
if val: if val:
while val[:4] == b"8BIM": while val[:4] == b"8BIM":
id = i16(val[4:6]) id = i16(val[4:6])
@ -1550,7 +1550,7 @@ class TiffImageFile(ImageFile.ImageFile):
palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) 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)
# #