Merge pull request #3806 from rudycelekli/fix/compressed-image-orientation-20261021

fix(images): preserve display orientation when compressing images
This commit is contained in:
zhayujie authored and GitHub committed 2026-10-25 10:43:18 +08:00
commit 5b95b3d1c6
2 files changed
+76 -2

No files matched your search

+2 -2
View File
@@ -25,10 +25,10 @@ def fsize(file):
def compress_imgfile(file, max_size):
if fsize(file) <= max_size:
return file
from PIL import Image
from PIL import Image, ImageOps
file.seek(0)
img = Image.open(file)
rgb_image = img.convert("RGB")
rgb_image = ImageOps.exif_transpose(img).convert("RGB")
quality = 95
min_quality = 10
while True:
@@ -0,0 +1,74 @@
"""Image compression must preserve the displayed orientation of real EXIF images."""
import io
import pytest
from common.utils import compress_imgfile
Image = pytest.importorskip("PIL.Image")
COLORS = ((240, 20, 20), (20, 220, 20), (20, 20, 240), (230, 220, 20))
# Corners TL, TR, BL, BR after the EXIF display transformation, hand-derived.
DISPLAY_CORNERS = {
1: (0, 1, 2, 3),
2: (1, 0, 3, 2),
3: (3, 2, 1, 0),
4: (2, 3, 0, 1),
5: (0, 2, 1, 3),
6: (2, 0, 3, 1),
7: (3, 1, 2, 0),
8: (1, 3, 0, 2),
}
def oriented_jpeg(orientation):
image = Image.new("RGB", (120, 80))
for index, color in enumerate(COLORS):
left, top = (index % 2) * 60, (index // 2) * 40
image.paste(color, (left, top, left + 60, top + 40))
exif = Image.Exif()
exif[274] = orientation
output = io.BytesIO()
image.save(output, "JPEG", quality=95, exif=exif)
output.seek(0)
return output
def corners(image):
width, height = image.size
points = (
(width // 4, height // 4),
(3 * width // 4, height // 4),
(width // 4, 3 * height // 4),
(3 * width // 4, 3 * height // 4),
)
return tuple(
min(
range(4),
key=lambda i: sum(
(a - b) ** 2 for a, b in zip(image.getpixel(point), COLORS[i])
),
)
for point in points
)
@pytest.mark.parametrize("orientation", range(1, 9))
def test_compression_preserves_exif_display_geometry_and_corners(orientation):
source = oriented_jpeg(orientation)
# Force real JPEG re-encoding through the public byte-budget helper.
result = compress_imgfile(source, len(source.getvalue()) - 1)
result.seek(0)
with Image.open(result) as decoded:
expected_size = (80, 120) if orientation >= 5 else (120, 80)
assert decoded.size == expected_size
assert corners(decoded) == DISPLAY_CORNERS[orientation]
assert decoded.getexif().get(274, 1) == 1
def test_under_budget_image_is_returned_unchanged():
source = oriented_jpeg(6)
original = source.getvalue()
assert compress_imgfile(source, len(original)) is source
assert source.getvalue() == original