diff --git a/beeref/fileio/image.py b/beeref/fileio/image.py index 6afb181..a43ebed 100644 --- a/beeref/fileio/image.py +++ b/beeref/fileio/image.py @@ -21,17 +21,63 @@ from urllib import request from PyQt6 import QtGui +import exif + logger = logging.getLogger(__name__) +def exif_rotated_image(path=None): + """Returns a QImage that is transformed according to the source's + orientation EXIF data. + """ + + img = QtGui.QImage(path) + if img.isNull(): + return img + + with open(path, 'rb') as f: + exifimg = exif.Image(f) + + if 'orientation' in exifimg.list_all(): + orientation = exifimg.orientation + else: + return img + + transform = QtGui.QTransform() + + if orientation == exif.Orientation.TOP_RIGHT: + return img.mirrored(horizontal=True, vertical=False) + if orientation == exif.Orientation.BOTTOM_RIGHT: + transform.rotate(180) + return img.transformed(transform) + if orientation == exif.Orientation.BOTTOM_LEFT: + return img.mirrored(horizontal=False, vertical=True) + if orientation == exif.Orientation.LEFT_TOP: + transform.rotate(90) + return img.transformed(transform).mirrored( + horizontal=True, vertical=False) + if orientation == exif.Orientation.RIGHT_TOP: + transform.rotate(90) + return img.transformed(transform) + if orientation == exif.Orientation.RIGHT_BOTTOM: + transform.rotate(270) + return img.transformed(transform).mirrored( + horizontal=True, vertical=False) + if orientation == exif.Orientation.LEFT_BOTTOM: + transform.rotate(270) + return img.transformed(transform) + + return img + + def load_image(path): if isinstance(path, str): - return (QtGui.QImage(path), path) + return (exif_rotated_image(path), path) if path.isLocalFile(): - return (QtGui.QImage(path.path()), path.path()) + return (exif_rotated_image(path.path()), path.path()) - img = QtGui.QImage() + img = exif_rotated_image() try: imgdata = request.urlopen(path.url()).read() except URLError as e: @@ -42,5 +88,5 @@ def load_image(path): with open(fname, 'wb') as f: f.write(imgdata) logger.debug(f'Temporarily saved in: {fname}') - img = QtGui.QImage(fname) + img = exif_rotated_image(fname) return (img, path.url()) diff --git a/setup.py b/setup.py index 3ee19fd..18887d5 100644 --- a/setup.py +++ b/setup.py @@ -11,6 +11,7 @@ setup( install_requires=[ 'pyQt6>=6.1', 'rectangle-packer>=2.0.1', + 'exif', ], packages=['beeref'], entry_points={ diff --git a/tests/assets/test3x3.jpg b/tests/assets/test3x3.jpg new file mode 100644 index 0000000..58a688e Binary files /dev/null and b/tests/assets/test3x3.jpg differ diff --git a/tests/assets/test3x3.png b/tests/assets/test3x3.png index 0009dac..233d48d 100644 Binary files a/tests/assets/test3x3.png and b/tests/assets/test3x3.png differ diff --git a/tests/assets/test3x3_orientation1.jpg b/tests/assets/test3x3_orientation1.jpg new file mode 100644 index 0000000..d01928e Binary files /dev/null and b/tests/assets/test3x3_orientation1.jpg differ diff --git a/tests/assets/test3x3_orientation2.jpg b/tests/assets/test3x3_orientation2.jpg new file mode 100644 index 0000000..7cb2089 Binary files /dev/null and b/tests/assets/test3x3_orientation2.jpg differ diff --git a/tests/assets/test3x3_orientation3.jpg b/tests/assets/test3x3_orientation3.jpg new file mode 100644 index 0000000..fe125cf Binary files /dev/null and b/tests/assets/test3x3_orientation3.jpg differ diff --git a/tests/assets/test3x3_orientation4.jpg b/tests/assets/test3x3_orientation4.jpg new file mode 100644 index 0000000..6e64a06 Binary files /dev/null and b/tests/assets/test3x3_orientation4.jpg differ diff --git a/tests/assets/test3x3_orientation5.jpg b/tests/assets/test3x3_orientation5.jpg new file mode 100644 index 0000000..8156dab Binary files /dev/null and b/tests/assets/test3x3_orientation5.jpg differ diff --git a/tests/assets/test3x3_orientation6.jpg b/tests/assets/test3x3_orientation6.jpg new file mode 100644 index 0000000..20b87aa Binary files /dev/null and b/tests/assets/test3x3_orientation6.jpg differ diff --git a/tests/assets/test3x3_orientation7.jpg b/tests/assets/test3x3_orientation7.jpg new file mode 100644 index 0000000..66e4bbd Binary files /dev/null and b/tests/assets/test3x3_orientation7.jpg differ diff --git a/tests/assets/test3x3_orientation8.jpg b/tests/assets/test3x3_orientation8.jpg new file mode 100644 index 0000000..1195564 Binary files /dev/null and b/tests/assets/test3x3_orientation8.jpg differ diff --git a/tests/fileio/test_image.py b/tests/fileio/test_image.py index 0210d7a..2b5f84e 100644 --- a/tests/fileio/test_image.py +++ b/tests/fileio/test_image.py @@ -1,8 +1,52 @@ +import math +import os.path + import httpretty +import pytest -from PyQt6 import QtCore +from PyQt6 import QtCore, QtGui -from beeref.fileio.image import load_image +from beeref.fileio.image import exif_rotated_image, load_image + + +def test_exif_rotated_image_without_path(qapp): + img = exif_rotated_image() + assert img.isNull() is True + + +def test_exif_rotated_image_not_a_file(qapp): + img = exif_rotated_image('foo') + assert img.isNull() is True + + +@pytest.mark.parametrize('path,expected', + [('test3x3.png', 'test3x3.png'), + ('test3x3_orientation1.jpg', 'test3x3.jpg'), + ('test3x3_orientation2.jpg', 'test3x3.jpg'), + ('test3x3_orientation3.jpg', 'test3x3.jpg'), + ('test3x3_orientation4.jpg', 'test3x3.jpg'), + ('test3x3_orientation5.jpg', 'test3x3.jpg'), + ('test3x3_orientation6.jpg', 'test3x3.jpg'), + ('test3x3_orientation7.jpg', 'test3x3.jpg'), + ('test3x3_orientation8.jpg', 'test3x3.jpg')]) +def test_exif_rotated_image(path, expected, qapp): + def get_fname(p): + root = os.path.dirname(__file__) + return os.path.join(root, '..', 'assets', p) + + img = exif_rotated_image(get_fname(path)) + assert img.isNull() is False + expected = QtGui.QImage(get_fname(expected)) + assert expected.isNull() is False + + # The JPEG format isn't pixel perfect, so we have to check whether + # pixels are approximately the same: + for x in range(3): + for y in range(3): + col_img = img.pixelColor(x, y).getRgb() + col_expected = expected.pixelColor(x, y).getRgb() + diff = [(col_img[i] - col_expected[i])**2 for i in range(4)] + assert math.sqrt(sum(diff)) < 3 def test_load_image_loads_from_filename(view, imgfilename3x3):