ChristopherJKoen commited on
Commit
b434fc2
·
1 Parent(s): 1ebea81

Normalize uploaded photos' EXIF orientation

Browse files

Add image normalization for uploaded photos using Pillow. Import Image, ImageOps and UnidentifiedImageError; introduce EXIF_NORMALIZE_EXTS and a _normalize_uploaded_photo(path) helper that applies ImageOps.exif_transpose, converts incompatible modes for JPEG, and saves with reasonable JPEG options. Call the normalizer for files categorized as "photos" and update the stored file size afterward. Errors decoding the image are caught and the original bytes are preserved.

server/app/services/session_store.py CHANGED
@@ -11,6 +11,7 @@ from typing import Iterable, List, Optional
11
  from uuid import uuid4
12
 
13
  from fastapi import UploadFile
 
14
 
15
  from ..core.config import get_settings
16
 
@@ -18,6 +19,7 @@ from ..core.config import get_settings
18
  IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
19
  DOC_EXTS = {".pdf", ".doc", ".docx"}
20
  DATA_EXTS = {".csv", ".xls", ".xlsx"}
 
21
  SESSION_ID_RE = re.compile(r"^[0-9a-f]{32}$")
22
  BUILTIN_PAGE_TEMPLATES = [
23
  {
@@ -82,6 +84,27 @@ def _category_for(filename: str) -> str:
82
  return "documents"
83
 
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  def _validate_session_id(session_id: str) -> str:
86
  if not session_id:
87
  raise ValueError("Invalid session id.")
@@ -585,6 +608,9 @@ class SessionStore:
585
  handle.write(chunk)
586
 
587
  category = _category_for(filename)
 
 
 
588
  return StoredFile(
589
  id=file_id,
590
  name=filename,
 
11
  from uuid import uuid4
12
 
13
  from fastapi import UploadFile
14
+ from PIL import Image, ImageOps, UnidentifiedImageError
15
 
16
  from ..core.config import get_settings
17
 
 
19
  IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
20
  DOC_EXTS = {".pdf", ".doc", ".docx"}
21
  DATA_EXTS = {".csv", ".xls", ".xlsx"}
22
+ EXIF_NORMALIZE_EXTS = {".jpg", ".jpeg", ".png", ".webp"}
23
  SESSION_ID_RE = re.compile(r"^[0-9a-f]{32}$")
24
  BUILTIN_PAGE_TEMPLATES = [
25
  {
 
84
  return "documents"
85
 
86
 
87
+ def _normalize_uploaded_photo(path: Path) -> None:
88
+ ext = path.suffix.lower()
89
+ if ext not in EXIF_NORMALIZE_EXTS:
90
+ return
91
+
92
+ try:
93
+ with Image.open(path) as image:
94
+ normalized = ImageOps.exif_transpose(image)
95
+ format_name = image.format
96
+ if normalized.mode in ("RGBA", "LA", "P") and format_name in {"JPEG", "JPG"}:
97
+ normalized = normalized.convert("RGB")
98
+ save_kwargs = {"exif": b""}
99
+ if format_name in {"JPEG", "JPG"}:
100
+ save_kwargs["quality"] = 95
101
+ save_kwargs["optimize"] = True
102
+ normalized.save(path, format=format_name, **save_kwargs)
103
+ except (UnidentifiedImageError, OSError, ValueError, TypeError):
104
+ # Keep original bytes if the file cannot be decoded by Pillow.
105
+ return
106
+
107
+
108
  def _validate_session_id(session_id: str) -> str:
109
  if not session_id:
110
  raise ValueError("Invalid session id.")
 
608
  handle.write(chunk)
609
 
610
  category = _category_for(filename)
611
+ if category == "photos":
612
+ _normalize_uploaded_photo(dest)
613
+ size = dest.stat().st_size
614
  return StoredFile(
615
  id=file_id,
616
  name=filename,