From 8e9d23fcfa60eb327954805a51de8a8937eb697d Mon Sep 17 00:00:00 2001 From: mariomitte Date: Mon, 31 Aug 2026 10:10:01 +0200 Subject: [PATCH] fix: thumbnail-first + megapixel guard to prevent image OOM in PDF/DOCX Previous draft()+convert()+thumbnail() ordering still caused OOM for non-JPEG formats (PNG/HEIC) because draft() is a no-op for those formats, and convert('RGB') forces a full pixel decode regardless. Replace with thumbnail()-first ordering: - thumbnail() internally calls draft() for JPEG before decoding - thumbnail() performs in-place resize without allocating a second full-resolution buffer - convert('RGB') is then called on the already-small image (safe for any format) Add _COMPRESS_IMAGE_MAX_MEGAPIXELS guard (20 MP): read image dimensions from headers only (no pixel decode) and return None for images that exceed the limit. This prevents OOM even for pathologically large files where draft() provides no benefit (e.g. PNG, TIFF, HEIC). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- backend/modules/fleet/views.py | 37 +++++++++++++++++++++------------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c52603b..b612cce 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Nakon što je gitea webhook i testirana skripta za automatsko povlačenje promje ```bash cd /opt/erp/app git fetch origin -git reset --hard origin/main +git reset --hard origin/main ili git pull --ff-only origin main # rebuild backend + worker + beat + frontend docker compose -f docker-compose.yml -f docker-compose.prod.yml build backend worker beat frontend diff --git a/backend/modules/fleet/views.py b/backend/modules/fleet/views.py index 2ef0abc..8b53395 100644 --- a/backend/modules/fleet/views.py +++ b/backend/modules/fleet/views.py @@ -464,24 +464,29 @@ def _resolve_service_report_tasks(work_order, task_id): return [selected_task], selected_task +_COMPRESS_IMAGE_MAX_MEGAPIXELS = 20 # preskači slike iznad 20 MP kako bi se izbjegao OOM + + def _compress_image_for_pdf(image_field, max_width=1280, quality=75): """ Otvori image_field (Django FileField), kompresiraj na max_width JPEG u memoriji, vrati ImageReader spreman za reportlab. Vraća None ako slika nije dostupna. - Koristi Image.draft() + thumbnail() kako bi se izbjeglo učitavanje pune rezolucije - velikih fotografija u RAM (sprječava OOM kill Gunicorn workera). + Redoslijed thumbnail() → convert() je bitan: thumbnail() interno poziva draft() + za JPEG (smanjuje rezoluciju JPEG dekodera na serveru) te radi in-place resize + bez alokacije full-resolution kopije. convert() se poziva tek na već maloj slici. + Pixel-count guard preskači slike koje bi i uz draft() prekoračile RAM. """ try: image_field.open('rb') - with Image.open(image_field) as src: - # draft() smanjuje dekodiranu rezoluciju za JPEG na serveru (hint, ne garantija) - src.draft('RGB', (max_width, max_width * 4)) - img = src.convert('RGB') - # thumbnail() radi in-place i ne alocira novu sliku pune veličine + with Image.open(image_field) as img: + w, h = img.size # čita samo header, bez dekodiranja piksela + if w * h > _COMPRESS_IMAGE_MAX_MEGAPIXELS * 1_000_000: + return None img.thumbnail((max_width, max_width * 4), Image.LANCZOS) + rgb = img.convert('RGB') buf = BytesIO() - img.save(buf, format='JPEG', quality=quality, optimize=True) + rgb.save(buf, format='JPEG', quality=quality, optimize=True) buf.seek(0) return ImageReader(buf) except Exception: @@ -497,17 +502,21 @@ def _compress_image_for_docx(image_field, max_width=1600, quality=80): """ Pripremi sliku za python-docx kao JPEG stream razumne veličine. - Koristi Image.draft() + thumbnail() kako bi se izbjeglo učitavanje pune rezolucije - velikih fotografija u RAM (sprječava OOM kill Gunicorn workera). + Redoslijed thumbnail() → convert() je bitan: thumbnail() interno poziva draft() + za JPEG (smanjuje rezoluciju JPEG dekodera na serveru) te radi in-place resize + bez alokacije full-resolution kopije. convert() se poziva tek na već maloj slici. + Pixel-count guard preskači slike koje bi i uz draft() prekoračile RAM. """ try: image_field.open('rb') - with Image.open(image_field) as src: - src.draft('RGB', (max_width, max_width * 4)) - img = src.convert('RGB') + with Image.open(image_field) as img: + w, h = img.size # čita samo header, bez dekodiranja piksela + if w * h > _COMPRESS_IMAGE_MAX_MEGAPIXELS * 1_000_000: + return None img.thumbnail((max_width, max_width * 4), Image.LANCZOS) + rgb = img.convert('RGB') buf = BytesIO() - img.save(buf, format='JPEG', quality=quality, optimize=True) + rgb.save(buf, format='JPEG', quality=quality, optimize=True) buf.seek(0) return buf except Exception: