1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-24 21:06:19 +00:00

Detect image type from magic numbers in image component (#157190)

This commit is contained in:
Manu
2025-11-25 17:38:16 +01:00
committed by GitHub
parent eb9fc66ca9
commit f96996b27f
2 changed files with 67 additions and 1 deletions

View File

@@ -70,6 +70,20 @@ LAST_FRAME_MARKER = bytes(f"\r\n--{FRAME_BOUNDARY}--\r\n", "utf-8")
IMAGE_SERVICE_SNAPSHOT: VolDictType = {vol.Required(ATTR_FILENAME): cv.string}
MAP_MAGIC_NUMBERS_TO_CONTENT_TYPE = {
b"\x89PNG": "image/png",
b"GIF8": "image/gif",
b"RIFF": "image/webp",
b"\x49\x49\x2a\x00": "image/tiff",
b"\x4d\x4d\x00\x2a": "image/tiff",
b"\xff\xd8\xff\xdb": "image/jpeg",
b"\xff\xd8\xff\xe0": "image/jpeg",
b"\xff\xd8\xff\xed": "image/jpeg",
b"\xff\xd8\xff\xee": "image/jpeg",
b"\xff\xd8\xff\xe1": "image/jpeg",
b"\xff\xd8\xff\xe2": "image/jpeg",
}
class ImageEntityDescription(EntityDescription, frozen_or_thawed=True):
"""A class that describes image entities."""
@@ -94,6 +108,11 @@ def valid_image_content_type(content_type: str | None) -> str:
return content_type
def infer_image_type(content: bytes) -> str | None:
"""Infer image type from first 4 bytes (magic number)."""
return MAP_MAGIC_NUMBERS_TO_CONTENT_TYPE.get(content[:4])
async def _async_get_image(image_entity: ImageEntity, timeout: int) -> Image:
"""Fetch image from an image entity."""
with suppress(asyncio.CancelledError, TimeoutError, ImageContentTypeError):
@@ -242,7 +261,9 @@ class ImageEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
async def _async_load_image_from_url(self, url: str) -> Image | None:
"""Load an image by url."""
if response := await self._fetch_url(url):
content_type = response.headers.get("content-type")
content_type = response.headers.get("content-type") or infer_image_type(
response.content
)
try:
return Image(
content=response.content,

View File

@@ -348,6 +348,51 @@ async def test_fetch_image_url_wrong_content_type(
assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR
@respx.mock
@pytest.mark.parametrize(
("content", "content_type"),
[
(b"\x89PNG", "image/png"),
(b"\xff\xd8\xff\xdb", "image/jpeg"),
(b"\xff\xd8\xff\xe0", "image/jpeg"),
(b"\xff\xd8\xff\xed", "image/jpeg"),
(b"\xff\xd8\xff\xee", "image/jpeg"),
(b"\xff\xd8\xff\xe1", "image/jpeg"),
(b"\xff\xd8\xff\xe2", "image/jpeg"),
(b"GIF89a", "image/gif"),
(b"GIF87a", "image/gif"),
(b"RIFF", "image/webp"),
(b"\x49\x49\x2a\x00", "image/tiff"),
(b"\x4d\x4d\x00\x2a", "image/tiff"),
],
)
async def test_fetch_image_url_infer_content_type_from_magic_number(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
content: bytes,
content_type: str,
) -> None:
"""Test fetching an image and inferring content-type from magic number."""
respx.get("https://example.com/myimage.jpg").respond(
status_code=HTTPStatus.OK, content=content
)
mock_integration(hass, MockModule(domain="test"))
mock_platform(hass, "test.image", MockImagePlatform([MockURLImageEntity(hass)]))
assert await async_setup_component(
hass, image.DOMAIN, {"image": {"platform": "test"}}
)
await hass.async_block_till_done()
client = await hass_client()
resp = await client.get("/api/image_proxy/image.test")
assert resp.status == HTTPStatus.OK
body = await resp.read()
assert body == content
assert resp.content_type == content_type
async def test_image_stream(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,