Handle more iPhone photo variants
This commit is contained in:
@@ -167,7 +167,8 @@ Older batch documents are still split at render time for backward compatibility.
|
|||||||
Metadata sidecars under `__MACOSX/` and `.DS_Store` are ignored. Ordinary API
|
Metadata sidecars under `__MACOSX/` and `.DS_Store` are ignored. Ordinary API
|
||||||
clients can also send repeated `photos` parts; send either `archive` or `photos`,
|
clients can also send repeated `photos` parts; send either `archive` or `photos`,
|
||||||
never both.
|
never both.
|
||||||
Select still photos; videos/Live Photo video components aren't supported. The
|
Select JPEG, PNG, WebP, HEIC, AVIF, or TIFF still photos; videos/Live Photo video
|
||||||
|
components aren't supported. The
|
||||||
service reads EXIF `DateTimeOriginal` automatically when it is present. Because
|
service reads EXIF `DateTimeOriginal` automatically when it is present. Because
|
||||||
iOS Shortcuts can blank that EXIF field while creating the ZIP, `photo_dates`
|
iOS Shortcuts can blank that EXIF field while creating the ZIP, `photo_dates`
|
||||||
supplies the same Photos-library metadata explicitly, one ISO 8601 value per
|
supplies the same Photos-library metadata explicitly, one ISO 8601 value per
|
||||||
|
|||||||
+27
-10
@@ -128,16 +128,21 @@ def public_text(text):
|
|||||||
|
|
||||||
def web_image(raw, destination):
|
def web_image(raw, destination):
|
||||||
with Image.open(io.BytesIO(raw)) as original:
|
with Image.open(io.BytesIO(raw)) as original:
|
||||||
if original.format not in {"JPEG", "PNG", "WEBP", "HEIF"}:
|
if original.format not in {"JPEG", "PNG", "WEBP", "HEIF", "AVIF", "TIFF"}:
|
||||||
raise ValueError("Use JPEG, PNG, WebP, or HEIC photos")
|
raise ValueError("Use JPEG, PNG, WebP, HEIC, AVIF, or TIFF photos")
|
||||||
taken_at = exif_photo_date(original.getexif())
|
taken_at = exif_photo_date(original.getexif())
|
||||||
image = ImageOps.exif_transpose(original)
|
image = ImageOps.exif_transpose(original)
|
||||||
image.thumbnail((GALLERY_MAX_EDGE, GALLERY_MAX_EDGE), Image.Resampling.LANCZOS)
|
image.thumbnail((GALLERY_MAX_EDGE, GALLERY_MAX_EDGE), Image.Resampling.LANCZOS)
|
||||||
if image.info.get("icc_profile"):
|
if image.info.get("icc_profile"):
|
||||||
image = ImageCms.profileToProfile(
|
try:
|
||||||
image, ImageCms.ImageCmsProfile(io.BytesIO(image.info["icc_profile"])),
|
image = ImageCms.profileToProfile(
|
||||||
ImageCms.createProfile("sRGB"), outputMode="RGB",
|
image, ImageCms.ImageCmsProfile(io.BytesIO(image.info["icc_profile"])),
|
||||||
)
|
ImageCms.createProfile("sRGB"), outputMode="RGB",
|
||||||
|
)
|
||||||
|
except (ImageCms.PyCMSError, OSError, ValueError, SyntaxError):
|
||||||
|
# Bad embedded color metadata should not block an otherwise
|
||||||
|
# decodable phone photo.
|
||||||
|
image = image.convert("RGB")
|
||||||
if image.mode in {"RGBA", "LA"} or "transparency" in image.info:
|
if image.mode in {"RGBA", "LA"} or "transparency" in image.info:
|
||||||
rgba = image.convert("RGBA")
|
rgba = image.convert("RGBA")
|
||||||
background = Image.new("RGB", image.size, "white")
|
background = Image.new("RGB", image.size, "white")
|
||||||
@@ -362,10 +367,22 @@ def create_app(data_dir=None, token=None):
|
|||||||
capture_dates.append(batch_date or
|
capture_dates.append(batch_date or
|
||||||
(supplied_dates[index] if supplied_dates else embedded_date) or
|
(supplied_dates[index] if supplied_dates else embedded_date) or
|
||||||
received_at)
|
received_at)
|
||||||
except (UnidentifiedImageError, OSError, ValueError, SyntaxError,
|
except (Image.DecompressionBombError, Image.DecompressionBombWarning) as error:
|
||||||
Image.DecompressionBombError, Image.DecompressionBombWarning,
|
app.logger.warning("Photo %d exceeded the pixel limit", index + 1,
|
||||||
ImageCms.PyCMSError):
|
exc_info=error)
|
||||||
abort(400, f"Photo {index + 1} is unsupported, damaged, or exceeds 60 megapixels")
|
abort(400, f"Photo {index + 1} exceeds the 60 megapixel limit")
|
||||||
|
except UnidentifiedImageError as error:
|
||||||
|
app.logger.warning("Photo %d could not be identified", index + 1,
|
||||||
|
exc_info=error)
|
||||||
|
abort(400, f"Photo {index + 1} is not a supported still-image file")
|
||||||
|
except ValueError as error:
|
||||||
|
app.logger.warning("Photo %d has an unsupported format", index + 1,
|
||||||
|
exc_info=error)
|
||||||
|
abort(400, f"Photo {index + 1}: {error}")
|
||||||
|
except (OSError, SyntaxError, ImageCms.PyCMSError) as error:
|
||||||
|
app.logger.warning("Photo %d could not be decoded", index + 1,
|
||||||
|
exc_info=error)
|
||||||
|
abort(400, f"Photo {index + 1} could not be decoded ({type(error).__name__})")
|
||||||
# Preserve existing IDs when omitted; named highlights participate in deduplication.
|
# Preserve existing IDs when omitted; named highlights participate in deduplication.
|
||||||
if highlight:
|
if highlight:
|
||||||
digest.update(b"\0highlight\0" + highlight.encode())
|
digest.update(b"\0highlight\0" + highlight.encode())
|
||||||
|
|||||||
@@ -157,6 +157,20 @@ def test_orientation_metadata_and_heic(service):
|
|||||||
assert image.height > image.width
|
assert image.height > image.width
|
||||||
|
|
||||||
|
|
||||||
|
def test_tiff_and_invalid_color_profile_are_accepted(service):
|
||||||
|
_, client, data = service
|
||||||
|
invalid_profile = io.BytesIO()
|
||||||
|
Image.new("RGB", (40, 20), "purple").save(
|
||||||
|
invalid_profile, "JPEG", icc_profile=b"not an ICC profile")
|
||||||
|
|
||||||
|
for raw in (photo(format="TIFF"), invalid_profile.getvalue()):
|
||||||
|
response = upload(client, [raw])
|
||||||
|
assert response.status_code == 202
|
||||||
|
output = data / "submissions" / response.json["id"] / "images/00.jpg"
|
||||||
|
with Image.open(output) as image:
|
||||||
|
assert image.format == "JPEG"
|
||||||
|
|
||||||
|
|
||||||
def test_photo_date_comes_from_original_metadata(service):
|
def test_photo_date_comes_from_original_metadata(service):
|
||||||
_, client, data = service
|
_, client, data = service
|
||||||
exif = Image.Exif()
|
exif = Image.Exif()
|
||||||
|
|||||||
Reference in New Issue
Block a user