mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-06-10 00:07:33 +08:00
* feat(assets): extract image dimensions at ingest and emit on asset responses
Image assets now carry width/height under the existing `metadata` field on
asset responses, shaped as `{"kind": "image", "width": W, "height": H}`.
This lets consumers get original dimensions (e.g. for clients that render
server-side thumbnails and can't recover them from naturalWidth/Height)
without an extra round-trip.
Dimensions are written to AssetReference.system_metadata across three
ingest paths:
- Direct file ingest (upload, in-place registration): Pillow reads the
image header right after hashing, while the file is still in OS page
cache. Non-image MIME types are skipped without touching the file.
- From-hash registration: this path never reads the file bytes, so
dimensions are best-effort copied from any prior sibling reference of
the same asset that already carries kind=image metadata. Missing
siblings, non-image siblings, or absent dimension keys leave the new
reference's metadata unchanged.
- Scanner enrichment: extends the existing system_metadata write in
enrich_asset so scanner-registered images get the same treatment as
uploaded ones.
Existing system_metadata keys (e.g. safetensors fields written by the
enricher, download provenance) are preserved through merge. Existing
assets ingested before this change retain their current metadata — no
automatic backfill in this PR.
Tests cover image emission, non-image no-op, merge preservation, and the
from-hash sibling back-fill (including the no-sibling and non-image-sibling
cases).
* fix(assets): validate sibling dimensions before backfilling
Per CodeRabbit review on #13991: the previous loop accepted any sibling
with `kind == "image"` and copied whichever dimension keys happened to
be present, then returned. A partial sibling (kind set but missing or
invalid width/height) could persist incomplete metadata onto the new
reference even when a later sibling had valid dimensions.
Now we validate that the sibling has both width and height as positive
integers before adopting its dimensions, and continue scanning to the
next sibling otherwise.
* fix(assets): reject booleans in sibling dimension validation (use type-is)
Per CodeRabbit follow-up on #13991: bool is a subclass of int in Python,
so isinstance(True, int) is True. The previous strict-int gate would
have accepted width=True (truthy + > 0) as a valid dimension.
Realistic occurrence is low (extract_image_dimensions returns proper
ints, JSON doesn't serialize bools as numbers), but the validation gate
exists for defense-in-depth so it should be actually strict.
---------
Co-authored-by: guill <jacob.e.segal@gmail.com>
87 lines
2.7 KiB
Python
87 lines
2.7 KiB
Python
"""Tests for the image_dimensions service."""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from PIL import Image
|
|
|
|
from app.assets.services.image_dimensions import extract_image_dimensions
|
|
|
|
|
|
def _make_png(path: Path, size: tuple[int, int]) -> Path:
|
|
img = Image.new("RGB", size, color=(123, 45, 67))
|
|
img.save(path, format="PNG")
|
|
return path
|
|
|
|
|
|
def _make_jpeg(path: Path, size: tuple[int, int]) -> Path:
|
|
img = Image.new("RGB", size, color=(10, 20, 30))
|
|
img.save(path, format="JPEG", quality=80)
|
|
return path
|
|
|
|
|
|
class TestExtractImageDimensions:
|
|
def test_extracts_png_dimensions(self, tmp_path: Path):
|
|
f = _make_png(tmp_path / "rect.png", (320, 240))
|
|
|
|
result = extract_image_dimensions(str(f), mime_type="image/png")
|
|
|
|
assert result == {"kind": "image", "width": 320, "height": 240}
|
|
|
|
def test_extracts_jpeg_dimensions(self, tmp_path: Path):
|
|
f = _make_jpeg(tmp_path / "shot.jpg", (1920, 1080))
|
|
|
|
result = extract_image_dimensions(str(f), mime_type="image/jpeg")
|
|
|
|
assert result == {"kind": "image", "width": 1920, "height": 1080}
|
|
|
|
def test_works_when_mime_type_is_none(self, tmp_path: Path):
|
|
f = _make_png(tmp_path / "no_mime.png", (50, 100))
|
|
|
|
result = extract_image_dimensions(str(f), mime_type=None)
|
|
|
|
assert result == {"kind": "image", "width": 50, "height": 100}
|
|
|
|
def test_skips_non_image_mime_without_touching_file(self, tmp_path: Path):
|
|
# Path doesn't need to exist — non-image MIME short-circuits.
|
|
result = extract_image_dimensions(
|
|
str(tmp_path / "model.safetensors"),
|
|
mime_type="application/octet-stream",
|
|
)
|
|
|
|
assert result is None
|
|
|
|
@pytest.mark.parametrize(
|
|
"mime",
|
|
["application/json", "text/plain", "video/mp4", "audio/mpeg"],
|
|
)
|
|
def test_skips_all_non_image_mime_types(self, tmp_path: Path, mime: str):
|
|
f = tmp_path / "file.bin"
|
|
f.write_bytes(b"\x00\x01\x02")
|
|
|
|
assert extract_image_dimensions(str(f), mime_type=mime) is None
|
|
|
|
def test_returns_none_for_missing_file(self, tmp_path: Path):
|
|
result = extract_image_dimensions(
|
|
str(tmp_path / "does_not_exist.png"), mime_type="image/png"
|
|
)
|
|
|
|
assert result is None
|
|
|
|
def test_returns_none_for_corrupt_image(self, tmp_path: Path):
|
|
f = tmp_path / "corrupt.png"
|
|
f.write_bytes(b"not actually a png file")
|
|
|
|
result = extract_image_dimensions(str(f), mime_type="image/png")
|
|
|
|
assert result is None
|
|
|
|
def test_returns_none_for_empty_file(self, tmp_path: Path):
|
|
f = tmp_path / "empty.png"
|
|
f.write_bytes(b"")
|
|
|
|
result = extract_image_dimensions(str(f), mime_type="image/png")
|
|
|
|
assert result is None
|