"""Parse an Image API ``info.json`` into a loaded :class:`~iiisight.model.ImageService`.
Handles both v2 and v3 ``info.json`` shapes: v3 carries limits and features at the
top level with a plain ``profile`` string, while v2 packs the compliance-level URI
and the limits/formats/qualities/supports into a ``profile`` array. Both fold into
the same unified :class:`ImageService`, with ``loaded=True`` and the tile/size
pyramid populated so the URL and tile helpers can run.
"""
from collections.abc import Mapping
from typing import Any
from .model import ImageService, Size, TileSet
[docs]
def parse_info(info: Mapping[str, Any]) -> ImageService:
"""Build a loaded :class:`ImageService` from a parsed ``info.json`` document."""
service_id = str(info.get("id") or info.get("@id") or "")
api_version, level, profile_extras = _version_level_extras(info)
tiles = tuple(
TileSet(
width=int(entry["width"]),
height=int(entry["height"]) if "height" in entry else None,
scale_factors=tuple(int(factor) for factor in entry.get("scaleFactors", [1])),
)
for entry in info.get("tiles", [])
if isinstance(entry, Mapping) and "width" in entry
)
sizes = tuple(
Size(width=int(entry["width"]), height=int(entry["height"]))
for entry in info.get("sizes", [])
if isinstance(entry, Mapping) and "width" in entry and "height" in entry
)
return ImageService(
id=service_id,
api_version=api_version,
compliance_level=level,
loaded=True,
protocol=info.get("protocol"),
width=_maybe_int(info.get("width")),
height=_maybe_int(info.get("height")),
max_width=_maybe_int(info.get("maxWidth", profile_extras.get("maxWidth"))),
max_height=_maybe_int(info.get("maxHeight", profile_extras.get("maxHeight"))),
max_area=_maybe_int(info.get("maxArea", profile_extras.get("maxArea"))),
sizes=sizes,
tiles=tiles,
preferred_formats=tuple(info.get("preferredFormats", [])),
extra_formats=tuple(info.get("extraFormats", profile_extras.get("formats", []))),
extra_qualities=tuple(info.get("extraQualities", profile_extras.get("qualities", []))),
extra_features=tuple(info.get("extraFeatures", profile_extras.get("supports", []))),
)
def _version_level_extras(info: Mapping[str, Any]) -> tuple[int, str | None, Mapping[str, Any]]:
"""Return ``(api_version, compliance_level, v2_profile_extras)`` from info.json."""
context = info.get("@context")
context_text = " ".join(context if isinstance(context, list) else [context or ""])
profile = info.get("profile")
level: str | None = None
extras: Mapping[str, Any] = {}
if isinstance(profile, list):
for entry in profile:
if isinstance(entry, str) and level is None:
level = _level_from(entry)
elif isinstance(entry, Mapping):
extras = entry
elif isinstance(profile, str):
level = _level_from(profile)
if "/image/3" in context_text or info.get("type") == "ImageService3":
version = 3
elif "/image/2" in context_text or isinstance(profile, list):
version = 2
elif isinstance(profile, str) and "/image/" in profile:
version = 2
else:
version = 3
return version, level, extras
def _level_from(profile: str) -> str | None:
"""Extract ``levelN`` from a plain ``levelN`` string or a v2 profile URI."""
if profile.startswith("level"):
return profile
tail = profile.rstrip("/").rsplit("/", 1)[-1].split(".", 1)[0]
return tail if tail.startswith("level") else None
def _maybe_int(value: Any) -> int | None:
if value is None:
return None
try:
return int(value)
except (TypeError, ValueError):
return None