fix: BILINEAR+BaseException catch to fix image worker timeout 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

Root cause was WORKER TIMEOUT, not OOM:
- Gunicorn sends SIGABRT to workers that exceed 60s timeout
- SIGABRT handler calls sys.exit(1) raising SystemExit(BaseException)
- except Exception does NOT catch SystemExit, so the worker crashes

Two fixes:
1. except BaseException — catches SystemExit so the worker survives
   and gracefully skips the image (returns None) instead of dying
2. Image.BILINEAR instead of LANCZOS — orders of magnitude faster
   for large images, ensures processing completes well within 60s
3. Reduce max_width 1600->800, megapixel limit 20->8 MP, optimize=True removed
   (these reduce processing time further)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
mariomitte
2026-08-31 10:43:27 +02:00
parent 8e9d23fcfa
commit 52408bc5a9

View File

@@ -464,32 +464,31 @@ 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
_COMPRESS_IMAGE_MAX_MEGAPIXELS = 8 # preskači slike iznad 8 MP; sprječava timeout i OOM
def _compress_image_for_pdf(image_field, max_width=1280, quality=75):
def _compress_image_for_pdf(image_field, max_width=800, 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.
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.
BILINEAR umjesto LANCZOS: višestruko brže za velike slike (izbjeći Gunicorn timeout).
Megapixel guard čita samo header i preskače slike > 8 MP bez dekodiranja piksela.
except BaseException hvata i SystemExit koji Gunicorn diže na SIGABRT (worker timeout).
"""
try:
image_field.open('rb')
with Image.open(image_field) as img:
w, h = img.size # čita samo header, bez dekodiranja piksela
w, h = img.size
if w * h > _COMPRESS_IMAGE_MAX_MEGAPIXELS * 1_000_000:
return None
img.thumbnail((max_width, max_width * 4), Image.LANCZOS)
img.thumbnail((max_width, max_width * 2), Image.BILINEAR)
rgb = img.convert('RGB')
buf = BytesIO()
rgb.save(buf, format='JPEG', quality=quality, optimize=True)
rgb.save(buf, format='JPEG', quality=quality)
buf.seek(0)
return ImageReader(buf)
except Exception:
except BaseException:
return None
finally:
try:
@@ -498,28 +497,27 @@ def _compress_image_for_pdf(image_field, max_width=1280, quality=75):
pass
def _compress_image_for_docx(image_field, max_width=1600, quality=80):
def _compress_image_for_docx(image_field, max_width=800, quality=80):
"""
Pripremi sliku za python-docx kao JPEG stream razumne veličine.
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.
BILINEAR umjesto LANCZOS: višestruko brže za velike slike (izbjeći Gunicorn timeout).
Megapixel guard čita samo header i preskače slike > 8 MP bez dekodiranja piksela.
except BaseException hvata i SystemExit koji Gunicorn diže na SIGABRT (worker timeout).
"""
try:
image_field.open('rb')
with Image.open(image_field) as img:
w, h = img.size # čita samo header, bez dekodiranja piksela
w, h = img.size
if w * h > _COMPRESS_IMAGE_MAX_MEGAPIXELS * 1_000_000:
return None
img.thumbnail((max_width, max_width * 4), Image.LANCZOS)
img.thumbnail((max_width, max_width * 2), Image.BILINEAR)
rgb = img.convert('RGB')
buf = BytesIO()
rgb.save(buf, format='JPEG', quality=quality, optimize=True)
rgb.save(buf, format='JPEG', quality=quality)
buf.seek(0)
return buf
except Exception:
except BaseException:
return None
finally:
try: