fix: apply EXIF orientation to service record images in PDF/DOCX
Some checks failed
ERP CI/CD Pipeline / test (push) Has been cancelled
ERP CI/CD Pipeline / Deploy (server git pull + compose) (push) Has been cancelled

Smartphone photos often have EXIF orientation metadata (tags 6, 8, 3)
that rotates the display but doesn't transform the pixel data. When
PDFs/DOCX embedded images without applying this metadata, they appear
rotated 90/180/270 degrees.

Use ImageOps.exif_transpose(img) before resize/convert in both
_compress_image_for_pdf() and _compress_image_for_docx() to read EXIF
orientation and transpose the actual pixel data accordingly. This is
a standard Pillow function and is a no-op for images without EXIF.

Add regression test test_compress_image_for_docx_handles_exif_orientation
to verify images with EXIF tag 0x0112 (orientation=6) are transposed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
mariomitte
2026-08-31 11:08:16 +02:00
parent aa690d4ac4
commit b985c285f9
2 changed files with 17 additions and 1 deletions

View File

@@ -252,6 +252,20 @@ class WorkOrderImagesEndpointTests(TestCase):
self.assertIsNotNone(result) self.assertIsNotNone(result)
self.assertGreater(len(result.getvalue()), 0) self.assertGreater(len(result.getvalue()), 0)
def test_compress_image_for_docx_handles_exif_orientation(self):
exif = Image.Exif()
exif[0x0112] = 6
file = BytesIO()
Image.new('RGB', (3000, 2000), color='green').save(file, format='JPEG', quality=85, exif=exif.tobytes())
file.seek(0)
uploaded = SimpleUploadedFile('rotated-phone.jpg', file.getvalue(), content_type='image/jpeg')
result = _compress_image_for_docx(uploaded)
self.assertIsNotNone(result)
self.assertGreater(len(result.getvalue()), 0)
def test_work_order_related_tasks_queryset_excludes_other_work_orders_for_same_vehicle(self): def test_work_order_related_tasks_queryset_excludes_other_work_orders_for_same_vehicle(self):
other_work_order = WorkOrder.objects.create( other_work_order = WorkOrder.objects.create(
vehicle=self.vehicle, vehicle=self.vehicle,

View File

@@ -14,7 +14,7 @@ from pathlib import Path
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from PIL import Image, UnidentifiedImageError from PIL import Image, ImageOps, UnidentifiedImageError
from django.conf import settings from django.conf import settings
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
@@ -479,6 +479,7 @@ def _compress_image_for_pdf(image_field, max_width=800, quality=75):
try: try:
image_field.open('rb') image_field.open('rb')
with Image.open(image_field) as img: with Image.open(image_field) as img:
img = ImageOps.exif_transpose(img)
w, h = img.size w, h = img.size
if w * h > _COMPRESS_IMAGE_MAX_MEGAPIXELS * 1_000_000: if w * h > _COMPRESS_IMAGE_MAX_MEGAPIXELS * 1_000_000:
return None return None
@@ -508,6 +509,7 @@ def _compress_image_for_docx(image_field, max_width=800, quality=80):
try: try:
image_field.open('rb') image_field.open('rb')
with Image.open(image_field) as img: with Image.open(image_field) as img:
img = ImageOps.exif_transpose(img)
w, h = img.size w, h = img.size
if w * h > _COMPRESS_IMAGE_MAX_MEGAPIXELS * 1_000_000: if w * h > _COMPRESS_IMAGE_MAX_MEGAPIXELS * 1_000_000:
return None return None