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: