IIIF compliance¶
iiiris targets IIIF Image API 3.0 Level 2 with 2.x compatibility for migration from existing deployments. Both versions are first-class: distinct parsers, distinct info.json shapes, validated against their respective spec grammars.
References: - IIIF Image API 3.0 - IIIF Image API 2.1
URL routes¶
| Method | Path | Purpose |
|---|---|---|
| GET | /iiif/3/{identifier}/info.json |
3.0 image information |
| GET | /iiif/3/{identifier}/{region}/{size}/{rotation}/{quality.format} |
3.0 image request |
| GET | /iiif/2/{identifier}/info.json |
2.x image information |
| GET | /iiif/2/{identifier}/{region}/{size}/{rotation}/{quality.format} |
2.x image request |
{identifier} may contain forward slashes — e.g.
/iiif/3/photos/cat.jpg/full/max/0/default.jpg resolves to the identifier
photos/cat.jpg. The router treats the last four URL segments of an image
request as region/size/rotation/quality.format; everything before is the
identifier. For info.json, everything before the trailing info.json is
the identifier.
Parameter coverage¶
Region (both versions)¶
full— entire imagesquare— largest centred square (2.1.1+; supported in v2 too)x,y,w,h— pixel coordinatespct:x,y,w,h— percentage coordinates
Size¶
| Token | v3 | v2 |
|---|---|---|
max |
✓ | ✓ |
full |
— | ✓ (v2 spelling, equivalent to max) |
^max |
✓ (upscaling) | rejected (v3-only ^ prefix) |
w, |
✓ | ✓ |
,h |
✓ | ✓ |
pct:n |
✓ | ✓ |
w,h |
✓ (forced; may distort) | ✓ |
!w,h |
✓ (confined; preserves aspect) | ✓ |
^… (upscale) |
✓ on any of the above | rejected |
Without ^, requests that would require upscaling return 400 with a
"use ^ to upscale" message.
Rotation¶
n(degrees),!n(mirror then rotate)- v3:
n ∈ [0, 360), decimal allowed - v2:
n ∈ [0, 360](closed range; 360 normalised to 0 internally)
Quality¶
| Token | v3 | v2 | Pipeline behavior |
|---|---|---|---|
default |
✓ | ✓ | no-op |
color |
✓ | ✓ | no-op |
gray |
✓ | ✓ | vips_colourspace(BW) |
native |
rejected | ✓ | normalised to default |
bitonal |
✓ | ✓ | ToColorSpace(BW) → Linear(a=10000, b=-1.28e6) → Cast(UChar) (clipping does the threshold) |
Format¶
jpg, png, tif, webp for both v2 and v3. (Source ingestion is
broader: libvips reads JP2, GIF, TIFF, etc.)
JPEG output is quality 90, baseline (sequential — ~3× faster to encode
than progressive). Set image.jpeg_progressive: true for progressive
(interlaced) output; see configuration.md.
Metadata, colour, and orientation¶
Governed by image.metadata (default strip; see
configuration.md).
| Aspect | strip (default) |
preserve |
|---|---|---|
| EXIF / XMP / IPTC (camera, serial, GPS, captions) | removed | kept |
| Colour space | normalized to sRGB (colour) / sGray (grayscale, no RGB promotion); CMYK → RGB; ~0.5 KB micro ICC profile embedded | original profile kept verbatim; no colour transform |
| EXIF orientation | baked into the pixels (autorotate); tag cleared | kept as a tag; pixels not rotated |
info.json width/height |
oriented dimensions | raw header dimensions |
The IIIF rotation parameter (the /{rotation}/ path segment) is
independent of EXIF-orientation autorotate: in strip mode the image is
first made upright, then the requested rotation is applied on top.
info.json¶
v3 shape (flat fields)¶
{
"@context": "http://iiif.io/api/image/3/context.json",
"id": "https://host/iiif/3/sample.jpg",
"type": "ImageService3",
"protocol": "http://iiif.io/api/image",
"profile": "level2",
"width": 1024,
"height": 768,
"extraFormats": ["png", "tif", "webp"],
"extraQualities": ["color", "gray"],
"extraFeatures": ["sizeUpscaling", "rotationArbitrary", "jsonldMediaType", "cors"],
"preferredFormats": ["jpg"]
}
v2 shape (profile array)¶
{
"@context": "http://iiif.io/api/image/2/context.json",
"@id": "https://host/iiif/2/sample.jpg",
"@type": "iiif:Image",
"protocol": "http://iiif.io/api/image",
"width": 1024,
"height": 768,
"profile": [
"http://iiif.io/api/image/2/level2.json",
{
"formats": ["jpg", "png", "tif", "webp"],
"qualities": ["default", "native", "color", "gray"],
"supports": ["regionSquare", "rotationArbitrary", "mirroring", "jsonldMediaType", "cors"]
}
]
}
Single source of truth¶
Per-version Supported* slices in internal/iiif/v{2,3}/ are consumed by
both the parsers (for validation) and the info.json builders (for
advertising). The advertised feature set therefore cannot drift from what
the pipeline actually implements — change the slice, both sides update.
HTTP status codes¶
The full transport contract (routes, middleware, cache/CORS semantics) is
specs/http-interface.md; the parameter-level
detail behind a 400 is below.
| Condition | Status |
|---|---|
| Parser rejects path syntax (bad region, unknown quality, missing format, etc.) | 400 |
Pipeline rejects values (region origin outside source, size requires upscale without ^) |
400 |
image.max_pixel_area exceeded by source |
400 |
image.max_output_area exceeded by output |
400 |
| Identifier not found in the configured source | 404 |
| Auth denied | 401 (or as supplied by the Authorizer) |
| vips decode failure, encode failure, source I/O error | 500 |
If-None-Match / If-Modified-Since matches cached entry |
304 |
info.json fields¶
iiiris emits the following info.json fields when configured. Zero-valued limits are omitted (advertisement matches what the pipeline enforces, in both directions):
| Field | Source | Notes |
|---|---|---|
maxWidth / maxHeight / maxArea (v3 flat; v2 in profile-extras) |
image.max_output_width / max_output_height / max_output_area |
Advertised when > 0 and enforced by the pipeline output guard. |
tiles (single entry, square) |
image.tile_size (default 512) |
scaleFactors computed from source dims. Hint only — iiiris generates any size on demand. |
extraFormats / extraQualities / extraFeatures |
per-version Supported* slices |
Single source of truth shared with the parsers. |
service (auth trees: 2.0 + 1.0 side-by-side) |
auth.BuildAdvertisement + auth.BuildAdvertisementV1 per matched profile |
Emitted only when the identifier matches an auth.rules entry. 2.0 entry first (AuthProbeService2 → AuthAccessService2 → AuthAccessTokenService2 + AuthLogoutService2); 1.0 entry second (http://iiif.io/api/auth/1/{clickthrough,login,kiosk,external} access service with nested token + logout). header-backend profiles emit the 2.0 probe service only — no 2.0 access service (no interactive flow) and no 1.0 entry (1.0 has no probe-only mode). See iiif-auth.md. |
IIIF Authorization Flow conformance¶
iiiris implements the IIIF Authorization Flow API 2.0 and
advertises the older IIIF Authentication API 1.0 service tree
side-by-side for migration compatibility. The four 2.0 services
(probe, access, access-token, logout) are mounted under /iiif/auth/;
the smaller 1.0 surface (token, login, logout — 1.0 has no probe) is
mounted under /iiif/auth/v1/. Both versions share the same cookie
and the same session store; the login and logout handlers are reused
verbatim. Only the advertised service tree and the token-response
shape differ. Both info.json versions (v2 and v3) carry both auth
service trees.
| Auth 2.0 feature | Status |
|---|---|
AuthProbeService2 + AuthProbeResult2 envelope |
✓ |
AuthAccessService2 (clickthrough / active / kiosk / external profiles) |
✓ |
AuthAccessTokenService2 (postMessage iframe flow, opaque tokens) |
✓ |
AuthLogoutService2 |
✓ |
substitute ref on denied probe (degraded-size variant or hook-supplied alternate identifier) |
✓ |
Service-tree advertisement on info.json |
✓ (v2 and v3) |
| OIDC, external-redirect, header-trust, and built-in access services | ✓ (per-profile access_service.backend) |
| Auth 1.0 feature | Status |
|---|---|
Access service (clickthrough / login / kiosk / external profiles) |
✓ shares the v2 login handler under /iiif/auth/v1/login/... |
| Token service (postMessage payload, 1.0 success + error shapes) | ✓ /iiif/auth/v1/token |
| Logout service | ✓ shares the v2 logout handler under /iiif/auth/v1/logout |
Service-tree advertisement on info.json (alongside the 2.0 tree) |
✓ BuildAdvertisementV1; second entry in the service array |
No community-blessed Auth 2.0 validator exists today. In its place the
iiif-auth-validate CI stage runs a project-local conformance harness
(tools/iiif-auth-conformance) against a sample auth-enabled
iiirisd. The harness drives the spec-defined endpoints (probe,
access, access-token, logout) and asserts the response shapes
(@context, type, status, payload fields). The stage is
load-bearing — a regression in any of the four interaction patterns
fails the pipeline. See
iiif-auth.md.
URL path normalisation¶
Requests whose URL path contains runs of / (e.g. host//iiif/3/{id}/info.json,
which naive server + path concatenation in clients produces) are folded to a
single / by the slash-collapse middleware before routing. iiiris matches the
behaviour of nginx, Apache, and most reverse proxies; without it, Go's
stdlib ServeMux returns a 301 redirect, which trips clients that don't follow.
Content negotiation for info.json¶
When a client sends Accept: application/ld+json, iiiris serves info.json with
Content-Type: application/ld+json;profile="http://iiif.io/api/image/{2,3}/context.json".
Without that Accept header the default is application/json. The cors and
jsonldMediaType entries in v3 extraFeatures (and the v2 profile's supports
list) match this behaviour.
CORS¶
All IIIF responses (info.json and image bodies) emit
Access-Control-Allow-Origin: *. This matches the cors advertisement in
both v3 extraFeatures and the v2 profile.
IIIF validator coverage¶
The CI iiif-validate job runs the Python iiif-validator against a
purpose-built fixture (testdata/validator-squares.jpg — a 1000×1000
colored grid that matches the palette hardcoded in
iiif_validator.validator; regenerate with make testdata-validator).
Versions pinned in CI:
iiif-validator==1.0.5— the only published PyPI version (since 2019).Pillow<12— defensive pin. iiif-validator has no release-time pin on its transitive deps, so a future Pillow major could re-break the decode path without warning. Holding Pillow on 11.x is the cheapest protection.
The iiirisd HTTP listener in the validator jobs is bound to :18180
(not the default :8080) because shell-executor runners on macOS
Docker Desktop frequently have a developer service squatting on
:8080; --network host would otherwise let iiirisd silently lose
the bind race and the validator would talk to that other service.
Against iiirisd on main with these pins, the validator reports:
- v2.0 (21 tests, 2 fail) —
baseurl_redirect(validator's Python raisesNameErrorconstructing the result),info_json(validator assertsidmatches the requested URL byte-for-byte; iiiris emits the canonical service URL per IIIF spec § 5.3). - v3.0 (24 tests, 3 fail) — same two, plus
size_noup(validator'sValidatorError.__init__is called with the wrong positional-argument count when iiirisd correctly rejects an upscale-without-^per v3 spec).
All three are upstream iiif-validator bugs / spec-disagreements, not
iiirisd issues. The stage stays allow_failure: true; the
v{2,3}-validation.log artifacts capture full output for reviewers.
1.0 decision: this posture is accepted for 1.0 — the validator is
unmaintained (pinned v1.0.5, no release since 2019), so blocking on an
upstream fix for non-iiiris failures is an indefinite wait. Recorded in the
1.0 checklist in ROADMAP.md.
Standards-compliance gaps¶
- IIIF Image API 3.0 validator stage in CI is
allow_failure: true— the Pythoniiif-validatortool's v3 coverage is in flux (see above for the three known-bad tests). v2 stage previously claimed strict but ran zero tests because the validator has no tests tagged2.1; we now use--version=2.0to actually exercise the v2 surface. sizesarray (pre-rendered size hints) is not emitted; iiiris generates any size on demand so it would be a hint only.rights/servicesblocks are not generated.- JPEG 2000 / HTJ2K encode is Part 1 only. OpenJPEG (the JP2 codec
iiiris uses) implements HTJ2K decode in 2.5.0+ but has no HTJ2K
encoder (upstream issue #1487).
iiiris can therefore consume HTJ2K (
.jph) sources but if asked to produce a JP2 output (/iiif/3/{id}/full/max/0/default.jp2) the response is classic Part 1, not HTJ2K. In practice IIIF clients almost never requestjp2output; the typical flow consumes JP2/JPH and emits JPEG/PNG/WebP.
iiiris transparently accepts both forms of JP2K source content: the
JP2 box-wrapped form (00 00 00 0C 6A 50… signature) and the raw
codestream form (SOC marker FF 4F FF 51…, the default from
OpenJPH's ojph_compress). govips's Go-side magic-byte gate only
recognises the boxed form; for raw codestreams the internal/image
package parses the SIZ marker and synthesises a minimal JP2 envelope
(signature + ftyp + jp2h(ihdr) + jp2c) so the buffer reaches
vips_jp2kload_buffer, which then decodes either Part 1 or HTJ2K
uniformly.