Added HEIC, HEIF and HIF support v0.6
This commit is contained in:
1 parent
3acf6bb7ec
commit
47b8d42295
9 files changed
+83
-32
No files matched your search
+42
-10
@@ -24,24 +24,32 @@ from pydantic import BaseModel, Field
|
||||
from fastapi.responses import FileResponse, Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from PIL import Image, ImageOps, UnidentifiedImageError
|
||||
from pillow_heif import register_heif_opener
|
||||
|
||||
register_heif_opener()
|
||||
|
||||
APP_NAME = os.getenv("APP_NAME", "Photo Date Editor")
|
||||
APP_URL = os.getenv("APP_URL", "http://localhost:8080")
|
||||
APP_VERSION = "0.5.0"
|
||||
APP_VERSION = "0.6.0"
|
||||
MAX_UPLOAD_MB = int(os.getenv("MAX_UPLOAD_MB", "150"))
|
||||
MAX_UPLOAD_BYTES = MAX_UPLOAD_MB * 1024 * 1024
|
||||
PREVIEW_MAX_EDGE = int(os.getenv("PREVIEW_MAX_EDGE", "2400"))
|
||||
PREVIEW_MAX_PIXELS = int(os.getenv("PREVIEW_MAX_PIXELS", "300000000"))
|
||||
Image.MAX_IMAGE_PIXELS = PREVIEW_MAX_PIXELS
|
||||
|
||||
SUPPORTED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".tif", ".tiff"}
|
||||
SUPPORTED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".tif", ".tiff", ".heic", ".heif", ".hif"}
|
||||
TIFF_EXTENSIONS = {".tif", ".tiff"}
|
||||
HEIF_EXTENSIONS = {".heic", ".heif", ".hif"}
|
||||
SERVER_PREVIEW_EXTENSIONS = TIFF_EXTENSIONS | HEIF_EXTENSIONS
|
||||
MEDIA_TYPES = {
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".tif": "image/tiff",
|
||||
".tiff": "image/tiff",
|
||||
".heic": "image/heic",
|
||||
".heif": "image/heif",
|
||||
".hif": "image/heif",
|
||||
}
|
||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
||||
|
||||
@@ -389,12 +397,14 @@ def _format_name(suffix: str) -> str:
|
||||
return "PNG"
|
||||
if suffix in TIFF_EXTENSIONS:
|
||||
return "TIFF"
|
||||
if suffix in HEIF_EXTENSIONS:
|
||||
return "HEIF"
|
||||
return suffix.lstrip(".").upper() or "image"
|
||||
|
||||
|
||||
def _validate_image_signature(path: Path, suffix: str) -> None:
|
||||
with path.open("rb") as source:
|
||||
signature = source.read(16)
|
||||
signature = source.read(64)
|
||||
|
||||
valid = False
|
||||
if suffix in {".jpg", ".jpeg"}:
|
||||
@@ -403,6 +413,15 @@ def _validate_image_signature(path: Path, suffix: str) -> None:
|
||||
valid = signature.startswith(b"\x89PNG\r\n\x1a\n")
|
||||
elif suffix in TIFF_EXTENSIONS:
|
||||
valid = signature.startswith((b"II*\x00", b"MM\x00*", b"II+\x00", b"MM\x00+"))
|
||||
elif suffix in HEIF_EXTENSIONS:
|
||||
hevc_brands = {
|
||||
b"heic", b"heix", b"hevc", b"hevx",
|
||||
b"heim", b"heis", b"hevm", b"hevs",
|
||||
}
|
||||
valid = len(signature) >= 16 and signature[4:8] == b"ftyp" and any(
|
||||
signature[offset:offset + 4] in hevc_brands
|
||||
for offset in range(8, len(signature) - 3, 4)
|
||||
)
|
||||
|
||||
if not valid:
|
||||
raise HTTPException(
|
||||
@@ -549,11 +568,14 @@ def _metadata_values(metadata: dict[str, Any]) -> tuple[bool, bool, dict[str, An
|
||||
|
||||
@app.post("/api/preview")
|
||||
async def create_preview(file: Annotated[UploadFile, File(...)]) -> Response:
|
||||
"""Generate a temporary browser-friendly preview for TIFF images."""
|
||||
"""Generate a temporary browser-friendly preview for TIFF and HEIF images."""
|
||||
filename = file.filename or "photo.tiff"
|
||||
suffix = Path(filename).suffix.lower()
|
||||
if suffix not in TIFF_EXTENSIONS:
|
||||
raise HTTPException(status_code=415, detail="Server-generated previews are only required for TIFF files.")
|
||||
if suffix not in SERVER_PREVIEW_EXTENSIONS:
|
||||
raise HTTPException(
|
||||
status_code=415,
|
||||
detail="Server-generated previews are only required for TIFF and HEIF files.",
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="photo-date-editor-preview-") as temp_dir:
|
||||
temp_path = Path(temp_dir) / f"source{suffix}"
|
||||
@@ -575,8 +597,12 @@ async def create_preview(file: Annotated[UploadFile, File(...)]) -> Response:
|
||||
output = io.BytesIO()
|
||||
preview.save(output, format="JPEG", quality=88, optimize=True)
|
||||
except (Image.DecompressionBombError, UnidentifiedImageError, OSError, ValueError) as exc:
|
||||
logger.warning("TIFF preview generation failed for %s: %s", filename, exc)
|
||||
raise HTTPException(status_code=422, detail="The TIFF could not be rendered for preview.") from exc
|
||||
format_name = _format_name(suffix)
|
||||
logger.warning("%s preview generation failed for %s: %s", format_name, filename, exc)
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"The {format_name} image could not be rendered for preview.",
|
||||
) from exc
|
||||
|
||||
return Response(
|
||||
content=output.getvalue(),
|
||||
@@ -590,7 +616,10 @@ async def read_metadata(file: Annotated[UploadFile, File(...)]) -> dict[str, boo
|
||||
filename = file.filename or "photo.jpg"
|
||||
suffix = Path(filename).suffix.lower()
|
||||
if suffix not in SUPPORTED_EXTENSIONS:
|
||||
raise HTTPException(status_code=415, detail="Supported formats are JPG, JPEG, PNG, TIF, and TIFF.")
|
||||
raise HTTPException(
|
||||
status_code=415,
|
||||
detail="Supported formats are JPG, JPEG, PNG, TIF, TIFF, HEIC, HEIF, and HIF.",
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="photo-date-editor-read-") as temp_dir:
|
||||
temp_path = Path(temp_dir) / f"source{suffix}"
|
||||
@@ -656,7 +685,10 @@ async def process_photo(
|
||||
filename = file.filename or "photo.jpg"
|
||||
suffix = Path(filename).suffix.lower()
|
||||
if suffix not in SUPPORTED_EXTENSIONS:
|
||||
raise HTTPException(status_code=415, detail="Supported formats are JPG, JPEG, PNG, TIF, and TIFF.")
|
||||
raise HTTPException(
|
||||
status_code=415,
|
||||
detail="Supported formats are JPG, JPEG, PNG, TIF, TIFF, HEIC, HEIF, and HIF.",
|
||||
)
|
||||
|
||||
exif_datetime, xmp_date, precision_label = _normalise_datetime(
|
||||
precision=precision,
|
||||
|
||||
Reference in new issue
Block a user