66 lines
2.3 KiB
Python
66 lines
2.3 KiB
Python
"""platform/images — pure decode + resolution bar + WebP renditions (SD-0002 §6.2)."""
|
|
import io
|
|
|
|
from PIL import Image
|
|
|
|
from app.platform import images
|
|
|
|
|
|
def _png(width: int, height: int, color=(120, 80, 200)) -> bytes:
|
|
buf = io.BytesIO()
|
|
Image.new("RGB", (width, height), color).save(buf, format="PNG")
|
|
return buf.getvalue()
|
|
|
|
|
|
def _jpeg(width: int, height: int) -> bytes:
|
|
buf = io.BytesIO()
|
|
Image.new("RGB", (width, height), (10, 200, 90)).save(buf, format="JPEG")
|
|
return buf.getvalue()
|
|
|
|
|
|
def test_good_image_produces_three_webp_renditions():
|
|
result = images.process(_png(1200, 900))
|
|
assert isinstance(result, images.Processed)
|
|
assert result.source_format == "PNG"
|
|
assert set(result.renditions) == {"thumb", "card", "detail"}
|
|
for name, blob in result.renditions.items():
|
|
with Image.open(io.BytesIO(blob)) as im:
|
|
assert im.format == "WEBP"
|
|
with Image.open(io.BytesIO(result.renditions["thumb"])) as im:
|
|
assert max(im.size) <= images.RENDITIONS["thumb"]
|
|
with Image.open(io.BytesIO(result.renditions["detail"])) as im:
|
|
assert max(im.size) <= images.RENDITIONS["detail"]
|
|
|
|
|
|
def test_downscale_only_never_upscales_small_source():
|
|
result = images.process(_png(520, 520))
|
|
with Image.open(io.BytesIO(result.renditions["detail"])) as im:
|
|
assert im.size == (520, 520)
|
|
|
|
|
|
def test_below_resolution_bar_rejected_low_res():
|
|
result = images.process(_png(300, 1200)) # shorter side 300 < 500
|
|
assert isinstance(result, images.Rejected)
|
|
assert result.reason == "rejected_low_res"
|
|
|
|
|
|
def test_not_an_image_rejected_not_image():
|
|
result = images.process(b"this is not an image")
|
|
assert isinstance(result, images.Rejected)
|
|
assert result.reason == "rejected_not_image"
|
|
|
|
|
|
def test_jpeg_source_format_preserved_in_result():
|
|
result = images.process(_jpeg(800, 800))
|
|
assert result.source_format == "JPEG"
|
|
|
|
|
|
def test_decompression_bomb_rejected_not_image(monkeypatch):
|
|
# A small file whose pixel count exceeds Pillow's bomb threshold must be a
|
|
# typed rejection, never an escaping exception.
|
|
from PIL import Image as PILImage
|
|
monkeypatch.setattr(PILImage, "MAX_IMAGE_PIXELS", 100) # 10x10 exceeds it
|
|
result = images.process(_png(800, 800))
|
|
assert isinstance(result, images.Rejected)
|
|
assert result.reason == "rejected_not_image"
|