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()