From b985c285f9a488a8b028772702e84e20324052fd Mon Sep 17 00:00:00 2001 From: mariomitte Date: Mon, 31 Aug 2026 11:08:16 +0200 Subject: [PATCH] fix: apply EXIF orientation to service record images in PDF/DOCX 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> --- .../fleet/tests/test_work_order_images_endpoint.py | 14 ++++++++++++++ backend/modules/fleet/views.py | 4 +++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/modules/fleet/tests/test_work_order_images_endpoint.py b/backend/modules/fleet/tests/test_work_order_images_endpoint.py index cbfd21d..1c5ba37 100644 --- a/backend/modules/fleet/tests/test_work_order_images_endpoint.py +++ b/backend/modules/fleet/tests/test_work_order_images_endpoint.py @@ -252,6 +252,20 @@ class WorkOrderImagesEndpointTests(TestCase): self.assertIsNotNone(result) 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): other_work_order = WorkOrder.objects.create( vehicle=self.vehicle, diff --git a/backend/modules/fleet/views.py b/backend/modules/fleet/views.py index d39fb6b..24e77f7 100644 --- a/backend/modules/fleet/views.py +++ b/backend/modules/fleet/views.py @@ -14,7 +14,7 @@ from pathlib import Path from decimal import Decimal, InvalidOperation 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.core.mail import EmailMessage 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: image_field.open('rb') with Image.open(image_field) as img: + img = ImageOps.exif_transpose(img) w, h = img.size if w * h > _COMPRESS_IMAGE_MAX_MEGAPIXELS * 1_000_000: return None @@ -508,6 +509,7 @@ def _compress_image_for_docx(image_field, max_width=800, quality=80): try: image_field.open('rb') with Image.open(image_field) as img: + img = ImageOps.exif_transpose(img) w, h = img.size if w * h > _COMPRESS_IMAGE_MAX_MEGAPIXELS * 1_000_000: return None