From 7cfd3d9ef786a2e538ca30df700325e72e85b5f5 Mon Sep 17 00:00:00 2001 From: LI <2484593937@qq.com> Date: Mon, 12 Oct 2026 17:06:01 -0700 Subject: [PATCH] fix(python): honor EXIF orientation for bytes and PIL images (#755) --- python/rapidocr/utils/load_image.py | 7 ++-- python/tests/test_load_image_exif.py | 58 ++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 python/tests/test_load_image_exif.py diff --git a/python/rapidocr/utils/load_image.py b/python/rapidocr/utils/load_image.py index 802c93a..e740459 100644 --- a/python/rapidocr/utils/load_image.py +++ b/python/rapidocr/utils/load_image.py @@ -81,14 +81,15 @@ class LoadImage: return img if isinstance(img, bytes): - img = self.img_to_ndarray(Image.open(BytesIO(img))) + img = self.exif_transpose(Image.open(BytesIO(img))) + img = self.img_to_ndarray(img) return img if isinstance(img, np.ndarray): return img if isinstance(img, Image.Image): - return self.img_to_ndarray(img) + return self.img_to_ndarray(self.exif_transpose(img)) if isinstance(img, MemoryImage): try: @@ -116,7 +117,7 @@ class LoadImage: if img_corrected is None: return img return img_corrected - except Exception as e: + except Exception: return img def img_to_ndarray(self, img: Image.Image) -> np.ndarray: diff --git a/python/tests/test_load_image_exif.py b/python/tests/test_load_image_exif.py new file mode 100644 index 0000000..3a19964 --- /dev/null +++ b/python/tests/test_load_image_exif.py @@ -0,0 +1,58 @@ +from pathlib import Path + +import numpy as np +import pytest +from PIL import Image +from rapidocr.utils.load_image import LoadImage + + +@pytest.mark.parametrize("input_type", ["bytes", "pil"]) +@pytest.mark.parametrize("orientation", [1, 3, 6, 8]) +def test_exif_input_types_match_path(tmp_path, input_type, orientation): + pixels = np.arange(18, dtype=np.uint8).reshape(2, 3, 3) + image = Image.fromarray(pixels) + exif = image.getexif() + exif[274] = orientation + path = tmp_path / "oriented.png" + image.save(path, exif=exif) + loader = LoadImage() + rotations = {1: 0, 3: 2, 6: 3, 8: 1} + expected = np.rot90(pixels, rotations[orientation])[..., ::-1] + np.testing.assert_array_equal(loader(path), expected) + + if input_type == "bytes": + actual = loader(path.read_bytes()) + else: + with Image.open(path) as pil_image: + original_pixels = np.array(pil_image) + actual = loader(pil_image) + assert pil_image.getexif()[274] == orientation + np.testing.assert_array_equal(np.array(pil_image), original_pixels) + np.testing.assert_array_equal(actual, expected) + + +@pytest.mark.parametrize("input_type", ["bytes", "pil"]) +def test_exif_existing_jpeg_matches_path(input_type): + path = Path(__file__).parent / "test_files" / "img_exif_orientation.jpg" + loader = LoadImage() + expected = loader(path) + if input_type == "bytes": + actual = loader(path.read_bytes()) + else: + with Image.open(path) as image: + actual = loader(image) + np.testing.assert_array_equal(actual, expected) + + +@pytest.mark.parametrize("input_type", ["bytes", "pil"]) +def test_input_without_exif_preserves_pixels(tmp_path, input_type): + pixels = np.arange(18, dtype=np.uint8).reshape(2, 3, 3) + image = Image.fromarray(pixels) + path = tmp_path / "unoriented.png" + image.save(path) + loader = LoadImage() + if input_type == "bytes": + actual = loader(path.read_bytes()) + else: + actual = loader(image) + np.testing.assert_array_equal(actual, pixels[..., ::-1])