(medium) image_sanitizer.sanitize_image: EXIF orientation срезается без поворота пикселей → фото клиента лежат на боку #1526

Closed
opened 2026-06-15 05:59:16 +00:00 by bot-backend · 1 comment
Collaborator

Severity: medium · Категория: report · Файл: tradein-mvp/backend/app/services/image_sanitizer.py:51-63

image_sanitizer.py:51-63. sanitize_image ре-энкодит изображение через Pillow: convert('RGB'), thumbnail, save(format='JPEG') без exif= и без ImageOps.exif_transpose (комментарий стр.61 подтверждает, что EXIF не переносится). Для фото с телефона в портретной ориентации пиксели хранятся в landscape, а корректный поворот задаётся EXIF-тегом Orientation=6/8. После strip тега и сохранения поворот теряется безвозвратно → фото в клиентском отчёте/галерее повёрнуто на 90/270°. Импакт: визуальный брак в клиентских материалах (массовый кейс — телефонные фото квартир). Фикс: перед convert/save вызвать img = ImageOps.exif_transpose(img) (PIL.ImageOps), чтобы запечь ориентацию в пиксели до удаления EXIF.

Почему баг: Линии 60-62 сохраняют img как есть (после convert('RGB')/thumbnail), exif не передаётся. Grep по проекту: exif_transpose/ImageOps нигде не вызывается (единственный вызов sanitize_image — app/api/v1/trade_in.py:503, сохраняет в estimate_photos и отдаёт клиенту). Значит ориентация теряется безвозвратно: исходный EXIF выкинут, поворот не применён. Для квартирных фото с телефона (массовый кейс) изображение в клиентском отчёте/галерее будет повёрнуто.

Как чинить: Перед resize/convert вызвать img = ImageOps.exif_transpose(img) (из PIL import ImageOps), чтобы запечь ориентацию в пиксели до отбрасывания EXIF.


deep-audit «армия ботов 2» · verify+harden, confidence 0.9 · unit U127

**Severity:** medium · **Категория:** report · **Файл:** `tradein-mvp/backend/app/services/image_sanitizer.py:51-63` image_sanitizer.py:51-63. sanitize_image ре-энкодит изображение через Pillow: convert('RGB'), thumbnail, save(format='JPEG') без exif= и без ImageOps.exif_transpose (комментарий стр.61 подтверждает, что EXIF не переносится). Для фото с телефона в портретной ориентации пиксели хранятся в landscape, а корректный поворот задаётся EXIF-тегом Orientation=6/8. После strip тега и сохранения поворот теряется безвозвратно → фото в клиентском отчёте/галерее повёрнуто на 90/270°. Импакт: визуальный брак в клиентских материалах (массовый кейс — телефонные фото квартир). Фикс: перед convert/save вызвать img = ImageOps.exif_transpose(img) (PIL.ImageOps), чтобы запечь ориентацию в пиксели до удаления EXIF. **Почему баг:** Линии 60-62 сохраняют img как есть (после convert('RGB')/thumbnail), exif не передаётся. Grep по проекту: exif_transpose/ImageOps нигде не вызывается (единственный вызов sanitize_image — app/api/v1/trade_in.py:503, сохраняет в estimate_photos и отдаёт клиенту). Значит ориентация теряется безвозвратно: исходный EXIF выкинут, поворот не применён. Для квартирных фото с телефона (массовый кейс) изображение в клиентском отчёте/галерее будет повёрнуто. **Как чинить:** Перед resize/convert вызвать img = ImageOps.exif_transpose(img) (из PIL import ImageOps), чтобы запечь ориентацию в пиксели до отбрасывания EXIF. --- <sub>deep-audit «армия ботов 2» · verify+harden, confidence 0.9 · unit U127</sub>
bot-backend added the
week ревью 1
label 2026-06-15 05:59:16 +00:00
bot-backend added the
scope/backend
label 2026-06-15 11:02:18 +00:00
Owner

🔍 week ревью 1 — верификация (ПОДТВЕРЖДЕНО)

Текущее состояние кода: tradein-mvp/backend/app/services/image_sanitizer.py:51-63 (sanitize_image). Импорт from PIL import Image, UnidentifiedImageError (:19) — ImageOps НЕ импортируется.

Корень: sanitize_image ре-энкодит: Image.open (:52) → img.load() (:53) → img.convert("RGB") (:55-56) → img.thumbnail(...) при oversize (:58-59) → img.save(buf, format="JPEG", ...) (:62) без exif= и без ImageOps.exif_transpose. Комментарий :61 явно подтверждает: «save() does NOT carry over .info / EXIF unless we pass exif=...; we don't». Для портретных телефонных фото пиксели хранятся в landscape, корректный поворот задаётся EXIF-тегом Orientation=6/8. После strip EXIF (что делается намеренно, ради privacy — см. docstring :3-12) и без exif_transpose поворот теряется безвозвратно → фото в клиентском отчёте/галерее лежит на боку (90/270°). Единственный вызов — api/v1/trade_in.py (сохранение в estimate_photos, отдаётся клиенту).

Что править:

  • Добавить from PIL import ImageOps и перед convert/thumbnail/save вызвать img = ImageOps.exif_transpose(img) — это запекает ориентацию в пиксели ДО отбрасывания EXIF. Privacy-эффект сохраняется (EXIF всё равно не переносится в выходной JPEG), но картинка ориентирована верно.
  • NB: exif_transpose без аргумента возвращает новый Image; присвоить результат обратно (img = ImageOps.exif_transpose(img)), иначе no-op.

Что посмотреть:

  • Код: image_sanitizer.py:51-63 (вставить exif_transpose первым шагом после img.load()), :19 (импорт), call-site api/v1/trade_in.py (sanitize_image → estimate_photos). Тест: прогнать JPEG с EXIF Orientation=6 → выходной JPEG имеет корректные width/height (повёрнутые) и нет EXIF.
  • Vault: релевантных заметок не найдено (image sanitizer EXIF orientation exif_transpose photo — пусто).
**🔍 week ревью 1 — верификация (ПОДТВЕРЖДЕНО)** **Текущее состояние кода:** `tradein-mvp/backend/app/services/image_sanitizer.py:51-63` (`sanitize_image`). Импорт `from PIL import Image, UnidentifiedImageError` (:19) — `ImageOps` НЕ импортируется. **Корень:** `sanitize_image` ре-энкодит: `Image.open` (:52) → `img.load()` (:53) → `img.convert("RGB")` (:55-56) → `img.thumbnail(...)` при oversize (:58-59) → `img.save(buf, format="JPEG", ...)` (:62) без `exif=` и без `ImageOps.exif_transpose`. Комментарий :61 явно подтверждает: «save() does NOT carry over .info / EXIF unless we pass exif=...; we don't». Для портретных телефонных фото пиксели хранятся в landscape, корректный поворот задаётся EXIF-тегом Orientation=6/8. После strip EXIF (что делается намеренно, ради privacy — см. docstring :3-12) и без `exif_transpose` поворот теряется безвозвратно → фото в клиентском отчёте/галерее лежит на боку (90/270°). Единственный вызов — `api/v1/trade_in.py` (сохранение в estimate_photos, отдаётся клиенту). **Что править:** - Добавить `from PIL import ImageOps` и перед `convert`/`thumbnail`/`save` вызвать `img = ImageOps.exif_transpose(img)` — это запекает ориентацию в пиксели ДО отбрасывания EXIF. Privacy-эффект сохраняется (EXIF всё равно не переносится в выходной JPEG), но картинка ориентирована верно. - NB: `exif_transpose` без аргумента возвращает новый Image; присвоить результат обратно (`img = ImageOps.exif_transpose(img)`), иначе no-op. **Что посмотреть:** - Код: `image_sanitizer.py:51-63` (вставить exif_transpose первым шагом после `img.load()`), `:19` (импорт), call-site `api/v1/trade_in.py` (sanitize_image → estimate_photos). Тест: прогнать JPEG с EXIF Orientation=6 → выходной JPEG имеет корректные width/height (повёрнутые) и нет EXIF. - Vault: релевантных заметок не найдено (`image sanitizer EXIF orientation exif_transpose photo` — пусто).
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#1526
No description provided.