Skip to content

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 image
  • square — largest centred square (2.1.1+; supported in v2 too)
  • x,y,w,h — pixel coordinates
  • pct: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 (AuthProbeService2AuthAccessService2AuthAccessTokenService2 + 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 raises NameError constructing the result), info_json (validator asserts id matches 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's ValidatorError.__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 Python iiif-validator tool'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 tagged 2.1; we now use --version=2.0 to actually exercise the v2 surface.
  • sizes array (pre-rendered size hints) is not emitted; iiiris generates any size on demand so it would be a hint only.
  • rights / services blocks 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 request jp2 output; 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.