fix: use draft()+thumbnail() to prevent OOM kill on large image resize
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

PIL img.resize() on high-resolution photos was exhausting worker RAM,
causing Gunicorn to SIGKILL the worker mid-request (production OOM crash).

Replace the explicit resize() path in both _compress_image_for_pdf() and
_compress_image_for_docx() with:
  - Image.draft() — hints the JPEG decoder to decode at a lower resolution
  - Image.thumbnail() — in-place resize that avoids allocating a second
    full-resolution buffer

This keeps peak memory proportional to the output size instead of the
original file size, preventing the worker from being killed when
processing multi-megapixel service-record photos.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
mariomitte
2026-08-31 09:51:21 +02:00
parent 1e68c663f2
commit a7220d27f5

View File

@@ -468,15 +468,18 @@ def _compress_image_for_pdf(image_field, max_width=1280, quality=75):
""" """
Otvori image_field (Django FileField), kompresiraj na max_width JPEG u memoriji, Otvori image_field (Django FileField), kompresiraj na max_width JPEG u memoriji,
vrati ImageReader spreman za reportlab. Vraća None ako slika nije dostupna. 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).
""" """
try: try:
image_field.open('rb') image_field.open('rb')
with Image.open(image_field) as src: 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') img = src.convert('RGB')
if img.width > max_width: # thumbnail() radi in-place i ne alocira novu sliku pune veličine
ratio = max_width / float(img.width) img.thumbnail((max_width, max_width * 4), Image.LANCZOS)
new_h = max(1, int(img.height * ratio))
img = img.resize((max_width, new_h), Image.LANCZOS)
buf = BytesIO() buf = BytesIO()
img.save(buf, format='JPEG', quality=quality, optimize=True) img.save(buf, format='JPEG', quality=quality, optimize=True)
buf.seek(0) buf.seek(0)
@@ -493,15 +496,16 @@ def _compress_image_for_pdf(image_field, max_width=1280, quality=75):
def _compress_image_for_docx(image_field, max_width=1600, quality=80): def _compress_image_for_docx(image_field, max_width=1600, quality=80):
""" """
Pripremi sliku za python-docx kao JPEG stream razumne veličine. 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).
""" """
try: try:
image_field.open('rb') image_field.open('rb')
with Image.open(image_field) as src: with Image.open(image_field) as src:
src.draft('RGB', (max_width, max_width * 4))
img = src.convert('RGB') img = src.convert('RGB')
if img.width > max_width: img.thumbnail((max_width, max_width * 4), Image.LANCZOS)
ratio = max_width / float(img.width)
new_h = max(1, int(img.height * ratio))
img = img.resize((max_width, new_h), Image.LANCZOS)
buf = BytesIO() buf = BytesIO()
img.save(buf, format='JPEG', quality=quality, optimize=True) img.save(buf, format='JPEG', quality=quality, optimize=True)
buf.seek(0) buf.seek(0)