Skip to content

Commit 1f3e614

Browse files
author
Project Team
committed
Replace PNG support with TIFF; JPEG and TIFF are the only accepted formats
- api.py: validate_image_upload now accepts image/jpeg and image/tiff (.jpg, .jpeg, .tif, .tiff); batch ZIP scanner uses same extensions - ui_routes.py: single-image upload check updated to match; TIFF images are converted to JPEG via Pillow before base64-encoding for display (browsers cannot render TIFF); raw image endpoint serves image/tiff - templates/index.html: file input accept attr and help text updated - templates/batch.html: instructions and example updated - tests: replaced test_verify_with_png_image with test_verify_with_tiff_image and added test_verify_png_rejected to confirm PNG is now rejected
1 parent 6f9c192 commit 1f3e614

5 files changed

Lines changed: 57 additions & 31 deletions

File tree

app/api.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -287,25 +287,25 @@ def validate_image_file(upload_file: UploadFile, correlation_id: str) -> None:
287287
HTTPException: If validation fails
288288
"""
289289
# Check content type
290-
if upload_file.content_type not in ["image/jpeg", "image/png", "image/jpg"]:
290+
if upload_file.content_type not in ["image/jpeg", "image/jpg", "image/tiff"]:
291291
logger.warning(
292292
f"[{correlation_id}] Invalid file type: {upload_file.content_type}"
293293
)
294294
raise HTTPException(
295295
status_code=status.HTTP_400_BAD_REQUEST,
296-
detail=f"Invalid file type. Expected image/jpeg or image/png, got {upload_file.content_type}"
296+
detail=f"Invalid file type. Expected image/jpeg or image/tiff, got {upload_file.content_type}"
297297
)
298298

299299
# Check file extension
300300
if upload_file.filename:
301301
ext = Path(upload_file.filename).suffix.lower()
302-
if ext not in [".jpg", ".jpeg", ".png"]:
302+
if ext not in [".jpg", ".jpeg", ".tif", ".tiff"]:
303303
logger.warning(
304304
f"[{correlation_id}] Invalid file extension: {ext}"
305305
)
306306
raise HTTPException(
307307
status_code=status.HTTP_400_BAD_REQUEST,
308-
detail=f"Invalid file extension. Expected .jpg, .jpeg, or .png, got {ext}"
308+
detail=f"Invalid file extension. Expected .jpg, .jpeg, .tif, or .tiff, got {ext}"
309309
)
310310

311311
# Verify file is actually a valid image by trying to open it
@@ -417,7 +417,7 @@ async def extract_zip_file(
417417
)
418418

419419
# Find all image files
420-
image_extensions = {'.jpg', '.jpeg', '.png'}
420+
image_extensions = {'.jpg', '.jpeg', '.tif', '.tiff'}
421421
image_files = []
422422

423423
for ext in image_extensions:
@@ -637,7 +637,7 @@ async def verify_label(
637637
(Tier 2) if ground truth is provided.
638638
639639
**Request:**
640-
- `image`: Label image file (JPEG or PNG, max 10MB)
640+
- `image`: Label image file (JPEG or TIFF, max 10MB)
641641
- `ground_truth`: Optional JSON with expected values
642642
- `timeout`: Optional timeout in seconds (default: 60s)
643643
@@ -956,7 +956,7 @@ async def submit_async_verify(
956956
(~10s) happens in the separate worker container.
957957
958958
**Request:**
959-
- ``image``: Label image (JPEG or PNG, max 10MB)
959+
- ``image``: Label image (JPEG or TIFF, max 10MB)
960960
- ``ground_truth``: Optional JSON with expected values
961961
962962
**Response:**

app/templates/batch.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ <h6 class="alert-heading">
4949
</h6>
5050
<p class="mb-2">Your ZIP file should contain:</p>
5151
<ul class="mb-0 small">
52-
<li><strong>Images:</strong> JPEG or PNG files</li>
52+
<li><strong>Images:</strong> JPEG or TIFF files</li>
5353
<li><strong>Ground Truth (optional):</strong> JSON files with same name as images
5454
<ul>
5555
<li>Example: <code>label_001.jpg</code> + <code>label_001.json</code></li>
@@ -95,7 +95,7 @@ <h6 class="card-title">
9595
├── label_001.json (optional)
9696
├── label_002.jpg
9797
├── label_002.json (optional)
98-
└── label_003.png</code></pre>
98+
└── label_003.tiff</code></pre>
9999
</div>
100100
</div>
101101
</div>

app/templates/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ <h1 class="mb-4">
2929
class="form-control {% if error_field == 'image' %}is-invalid{% endif %}"
3030
id="image"
3131
name="image"
32-
accept="image/jpeg,image/png"
32+
accept="image/jpeg,image/tiff,.tif,.tiff"
3333
required
3434
>
3535
<div class="form-text">
36-
Upload a clear image of the alcohol label (JPEG or PNG, max 10MB)
36+
Upload a clear image of the alcohol label (JPEG or TIFF, max 10MB)
3737
</div>
3838
{% if error_field == 'image' %}
3939
<div class="invalid-feedback d-block">

app/tests/test_api/test_fastapi_endpoints.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -467,22 +467,36 @@ def test_error_response_structure(authenticated_client):
467467
# Edge Cases
468468
# ============================================================================
469469

470-
def test_verify_with_png_image(authenticated_client, samples_dir):
471-
"""Test verification with PNG image (if available)."""
472-
# Try to find a PNG sample or skip
473-
png_files = list(samples_dir.glob("*.png"))
474-
if not png_files:
475-
pytest.skip("No PNG samples available")
476-
477-
with open(png_files[0], 'rb') as f:
478-
png_bytes = f.read()
479-
470+
def test_verify_with_tiff_image(authenticated_client):
471+
"""Test that TIFF images are accepted."""
472+
# Minimal valid TIFF bytes (little-endian, no actual image data needed for format check)
473+
import struct
474+
# TIFF magic: II (little-endian) + 42 + offset 8
475+
tiff_bytes = b'II' + struct.pack('<HI', 42, 8) + b'\x00' * 32
476+
480477
response = authenticated_client.post(
481-
"/verify",
482-
files={"image": ("label.png", png_bytes, "image/png")}
478+
"/verify/async",
479+
files={"image": ("label.tiff", tiff_bytes, "image/tiff")}
483480
)
484-
485-
assert response.status_code == 200
481+
482+
# Should be accepted (200/202), not rejected as invalid type
483+
assert response.status_code in (200, 202, 400), \
484+
f"Unexpected status {response.status_code} — TIFF should not be rejected as invalid type"
485+
if response.status_code == 400:
486+
data = response.json()
487+
assert "Invalid file type" not in data.get("detail", ""), \
488+
"TIFF was incorrectly rejected as an invalid file type"
489+
490+
491+
def test_verify_png_rejected(authenticated_client, sample_image_bytes):
492+
"""Test that PNG images are no longer accepted."""
493+
response = authenticated_client.post(
494+
"/verify/async",
495+
files={"image": ("label.png", sample_image_bytes, "image/png")}
496+
)
497+
assert response.status_code == 400
498+
data = response.json()
499+
assert "Invalid file type" in data["detail"]
486500

487501

488502
def test_batch_with_custom_timeout(authenticated_client, sample_batch_zip):

app/ui_routes.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,13 @@ async def ui_verify_submit(
215215
from api import verify_queue
216216

217217
# Validate image file
218-
if image.content_type not in ["image/jpeg", "image/png", "image/jpg"]:
218+
if image.content_type not in ["image/jpeg", "image/jpg", "image/tiff"]:
219219
return templates.TemplateResponse(
220220
"index.html",
221221
{
222222
"request": request,
223223
"username": username,
224-
"error": f"Invalid file type: {image.content_type}. Please upload JPEG or PNG.",
224+
"error": f"Invalid file type: {image.content_type}. Please upload JPEG or TIFF.",
225225
"error_field": "image",
226226
"form_data": {
227227
"brand_name": brand_name,
@@ -493,12 +493,21 @@ async def ui_verify_result(
493493
image_path = Path(job["image_path"])
494494
filename = image_path.name
495495

496-
# Re-encode the saved image as a base64 data URL for display in the template
496+
# Re-encode the saved image as a base64 data URL for display in the template.
497+
# TIFFs are not renderable in most browsers, so convert them to JPEG first.
497498
image_data = None
498499
try:
499-
raw = image_path.read_bytes()
500+
from io import BytesIO
500501
suffix = image_path.suffix.lower()
501-
mime = "image/png" if suffix == ".png" else "image/jpeg"
502+
if suffix in (".tif", ".tiff"):
503+
with Image.open(image_path) as img:
504+
buf = BytesIO()
505+
img.convert("RGB").save(buf, format="JPEG", quality=90)
506+
raw = buf.getvalue()
507+
mime = "image/jpeg"
508+
else:
509+
raw = image_path.read_bytes()
510+
mime = "image/jpeg"
502511
image_data = f"data:{mime};base64,{base64.b64encode(raw).decode()}"
503512
except Exception as e:
504513
logger.warning(f"Could not encode image for results page: {e}")
@@ -587,7 +596,10 @@ async def ui_verify_image(
587596
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Image not found")
588597

589598
suffix = image_path.suffix.lower()
590-
media_type = "image/png" if suffix == ".png" else "image/jpeg"
599+
if suffix in (".tif", ".tiff"):
600+
media_type = "image/tiff"
601+
else:
602+
media_type = "image/jpeg"
591603
return Response(content=image_path.read_bytes(), media_type=media_type)
592604

593605

0 commit comments

Comments
 (0)