Files
2026-moving-helper/app/images.py
T
2026-04-19 12:54:25 +02:00

74 lines
2.1 KiB
Python

from dataclasses import dataclass
from io import BytesIO
from fastapi import HTTPException, UploadFile
from PIL import Image, UnidentifiedImageError
MAX_IMAGE_SIDE = 1600
JPEG_QUALITY = 80
JPEG_MIME_TYPE = "image/jpeg"
@dataclass(slots=True)
class ProcessedImage:
blob: bytes
mime_type: str
width: int
height: int
def process_upload(file: UploadFile | None) -> ProcessedImage | None:
if file is None or not file.filename:
return None
try:
raw_bytes = file.file.read()
if not raw_bytes:
raise HTTPException(status_code=400, detail="上传的图片内容为空")
with Image.open(BytesIO(raw_bytes)) as source_image:
processed_image = _prepare_image(source_image)
except UnidentifiedImageError as exc:
raise HTTPException(status_code=400, detail="上传的文件不是合法图片") from exc
except HTTPException:
raise
except Exception as exc:
raise HTTPException(status_code=400, detail="图片处理失败,请尝试更换图片") from exc
finally:
file.file.close()
return processed_image
def _prepare_image(source_image: Image.Image) -> ProcessedImage:
prepared = _strip_metadata_and_convert(source_image)
prepared.thumbnail((MAX_IMAGE_SIDE, MAX_IMAGE_SIDE))
output = BytesIO()
prepared.save(output, format="JPEG", quality=JPEG_QUALITY, optimize=True)
blob = output.getvalue()
return ProcessedImage(
blob=blob,
mime_type=JPEG_MIME_TYPE,
width=prepared.width,
height=prepared.height,
)
def _strip_metadata_and_convert(source_image: Image.Image) -> Image.Image:
if source_image.mode in ("RGBA", "LA"):
background = Image.new("RGB", source_image.size, (255, 255, 255))
alpha = source_image.getchannel("A")
background.paste(source_image.convert("RGBA"), mask=alpha)
return background
if source_image.mode == "P":
return source_image.convert("RGB")
if source_image.mode != "RGB":
return source_image.convert("RGB")
return source_image.copy()