diff --git a/backend/modules/fleet/views.py b/backend/modules/fleet/views.py index 8b53395..9740c84 100644 --- a/backend/modules/fleet/views.py +++ b/backend/modules/fleet/views.py @@ -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: