Image.Components ships a fifth provider — :iiif — that emits URLs in the IIIF Image API 3.0 URL grammar. IIIF is different from the four CDN providers: it's an open standard published by the IIIF Consortium, not a single vendor's URL syntax, and dozens of cultural-heritage institutions and image servers (Cantaloupe, IIPImage, Loris, Hyrax, Goobi viewer, Universal Viewer's backends) implement it. Use :iiif when your image source is a IIIF-compliant server — Wellcome Collection, Library of Congress, Yale's IIIF deployments, Bodleian, an institutional Cantaloupe instance, or your own image_plug IIIF mount (the symmetric server-side parser, see "Use against a self-hosted image_plug IIIF mount" below).

URL shape

{host}{prefix}/{identifier}/{region}/{size}/{rotation}/{quality}.{format}

The five positional segments after the identifier are always present — IIIF servers reject URLs missing any segment.

Quick start

<.image
  src="/V0007727.jpg"
  provider={:iiif}
  host="https://iiif.wellcomecollection.org"
  iiif_prefix="/image"
  width={400}
/>

Renders to:

<img src="https://iiif.wellcomecollection.org/image/V0007727.jpg/full/^400,/0/default.jpg" />

The iiif_prefix here is "/image" because Wellcome's server publishes under that prefix; for a vanilla Image API 3.0 server the prefix is typically "/iiif/3" (the default).

What maps and what doesn't

Component attributeIIIF segmentNotes
src={identifier}Leading / stripped; embedded / percent-encoded as %2F
width, height, fit, dpr{size}fit: :contain!w,h; fit: :squeezew,h (distort); width-only → w,; height-only → ,h; Resize.upscale? toggles the ^ prefix
region={...}{region}:full, {:pixels, x, y, w, h}, or {:percent, x, y, w, h}
Rotate{angle: …}{rotation}Any 0..360 (3.0 allows arbitrary angles)
iiif_quality={quality}:default, :color, :gray, :bitonal
format{format}:jpegjpg, :webp, :png, :gif, :tifftif, :jp2, :pdf. :auto falls back to iiif_format= (default :jpeg)

Concepts that have no IIIF equivalent and are silently dropped:

  • Effectsblur, sharpen, vignette, tint. IIIF's spec deliberately scopes to geometric transforms and a small fixed quality vocabulary. Effects are out of scope.
  • fit: :cover — IIIF cannot express "scale-to-fill plus centred crop" in one URL. Use :contain or :squeeze, or supply an explicit region={...} for the sub-rectangle you want.
  • Adjust (non-grayscale) — brightness/contrast/saturation/gamma have no IIIF parameter. Only Adjust{saturation: 0.0} round-trips, via the gray quality token.
  • face_zoom, gravity — IIIF has no face-aware crop concept.

A pipeline that uses any of the dropped ops still produces a valid IIIF URL — the dropped ops just don't appear in it. If your app needs effects, render against a CDN provider; if your app needs IIIF compatibility, scope the IR to what IIIF can carry.

Region

region= is the IIIF-specific attribute that no other provider in this library carries. Three shapes:

<%!-- The whole image (the default; same as omitting region=) --%>
<.image src="/cat.jpg" provider={:iiif} region={:full} />

<%!-- Pixel rectangle: x, y, width, height --%>
<.image src="/cat.jpg" provider={:iiif} region={{:pixels, 100, 50, 400, 300}} />

<%!-- Percentage rectangle (0..100 each) --%>
<.image src="/cat.jpg" provider={:iiif} region={{:percent, 25.0, 25.0, 50.0, 50.0}} />

The pixel form maps to IIIF x,y,w,h; the percent form to pct:x,y,w,h; :full (or omitted) to full. The IR carries the rectangle as Image.Plug.Pipeline.Ops.Crop, which image_plug's interpreter applies before the resize step (matching the IIIF spec's region → size order).

The IIIF spec also defines a square region that crops the largest centered square. The library doesn't currently project to square — use {:percent, …} with explicit dimensions if you need a centered square.

Quality

IIIF has four named quality values: default, color, gray, bitonal. Most servers treat default and color identically; gray is luminance-only; bitonal is one-bit-per-pixel black-and-white.

<.image src="/cat.jpg" provider={:iiif} iiif_quality={:gray} />
<.image src="/cat.jpg" provider={:iiif} iiif_quality={:bitonal} />

Behind the scenes:

  • iiif_quality={:gray} injects an Ops.Adjust{saturation: 0.0} op into the pipeline.
  • iiif_quality={:bitonal} injects an Ops.Posterize{levels: 2} op.

Both are detected by the projector when emitting the URL's quality segment. The same trick lets you achieve the same IIIF URL output by setting saturation or posterize directly on the IR — useful when your code already builds pipelines for one of the CDN providers and you want to render the same data via IIIF.

Don't confuse iiif_quality= (the IIIF quality token) with quality= (the integer 1..100 compression quality used by every other provider). They are unrelated; IIIF servers don't accept a numeric quality.

Rotation

IIIF Image API 3.0 accepts any rotation angle 0..360. We project Ops.Rotate.angle directly:

<.image src="/cat.jpg" provider={:iiif} width={400} />
<%!-- /iiif/3/cat.jpg/full/^400,/0/default.jpg --%>

<.image src="/cat.jpg" provider={:iiif} width={400} {%{rotate: 45}} />
<%!-- /iiif/3/cat.jpg/full/^400,/45/default.jpg (with Rotate{angle: 45} in the IR) --%>

IIIF servers process operations in spec-prescribed order: regionsizerotationqualityformat. So a width-then-rotate will resize first, then rotate the resized image. A 4:3 source resized to width=400 becomes 400×300, then a 90° rotation makes it 300×400. Plan dimensions accordingly.

The IIIF spec also defines !N (mirror-then-rotate). The library doesn't yet project a mirror op into the rotation prefix — if you need mirroring, drop down to the URL builder and emit it manually.

Server prefixes

iiif_prefix= is the segment between host= and the identifier. It varies by deployment:

ServerTypical prefix
Cantaloupe (default config), Loris, IIPImage/iiif/3
Wellcome Collection/image
Library of Congress IIIF/image-services/iiif
Custom (anything goes)whatever the operator chose

Always check the server's info.json document — its id field reveals the prefix the server uses.

Pre-computing pipelines

As with the CDN providers, you can call the URL builder directly when you don't need to render HTML:

alias Image.Components.URL
alias Image.Plug.Pipeline
alias Image.Plug.Pipeline.Ops

pipeline = %Pipeline{
  ops: [
    %Ops.Resize{width: 400, upscale?: false},
    %Ops.Rotate{angle: 90}
  ],
  output: %Ops.Format{type: :jpeg, quality: 80}
}

URL.iiif(pipeline,
  source_path: "/V0007727.jpg",
  host: "https://iiif.wellcomecollection.org",
  iiif_prefix: "/image"
)
# => "https://iiif.wellcomecollection.org/image/V0007727.jpg/full/400,/90/default.jpg"

URL.iiif/2's docstring lists the full options surface; the same builder powers <.image> and <.picture> internally.

Conformance level

The library targets IIIF Image API 3.0 Compliance Level 2, which is the level most production servers implement. URL forms outside Level 2 — IIIF authentication URLs, the info.json discovery document, the services extension — are out of scope on the client side. The companion image_plug provider is a server-side IIIF implementation; it ships a Level 2 parser plus an info.json endpoint.

The conformance gaps documented above (no :cover fit, effects dropped, no per-channel adjust) are deliberate semantic limits of IIIF as a standard, not omissions in this library.

Use against a self-hosted image_plug IIIF mount

image_plug ships an Image.Plug.Provider.IIIF parser — the symmetric inverse of the URL builder here. Mount it in your Phoenix app to serve your own IIIF endpoint:

forward "/iiif/3", Image.Plug,
  provider: {Image.Plug.Provider.IIIF, []},
  source_resolver: {Image.Plug.SourceResolver.File, root: "/var/lib/iiif"}

…and <.image provider={:iiif} host=""> resolves through your in-process server using the same URL grammar real IIIF servers use. The mount also serves info.json discovery documents at /iiif/3/<identifier>/info.json automatically. See image_plug's IIIF conformance guide for the per-segment compliance matrix and the deployment recipe.

The IIIF-specific component: <.iiif>

<.image provider={:iiif}> covers the static-thumbnail case. Image.Components.IIIF.iiif/1 is a dedicated IIIF component that adds two IIIF fundamentals nothing else exposes: tiled rendering and deep-zoom viewer mounting.

import Image.Components.IIIF

It has three modes selected by the mode= attribute.

mode={:static} — the default

Equivalent to <.image provider={:iiif} src="…">. Lives here for symmetry; if all you need is a thumbnail, the cross-provider <.image> is the simpler call.

<.iiif src="/cat.jpg" host="https://iiif.example.org" width={400} />

mode={:tiles} — static tile grid

Computes the IIIF tile URLs that cover the source image at a chosen scale factor and emits a CSS-grid container of <img> elements. No JavaScript. Useful for high-resolution static layouts (atlases, scanned manuscripts, posters) where each tile is just one more cacheable HTTP request.

<.iiif
  src="/atlas.jpg"
  host="https://iiif.example.org"
  mode={:tiles}
  source_width={4096}
  source_height={4096}
  tile_width={512}
  scale_factor={2}
/>

Renders a 4×4 grid (16 tiles) where each tile covers 512 * 2 = 1024 source pixels rendered at 512 — i.e. a 2048×2048 zoomed-out view of the 4096×4096 original. Each <img> is a separate HTTP request to a URL like …/0,0,1024,1024/512,512/0/default.jpg, so the browser can parallelise the fetches and the CDN can cache each tile independently.

The component does not auto-discover the source dimensions — you supply them as source_width= and source_height=. Use Image.Components.URL.iiif_info_url/1 to build the info.json URL, fetch it (e.g. in mount/3), and pass the resulting width / height values down. The discovery step is deliberately out of scope for the component to keep it pure render-time logic.

The bottom and right edges of the grid clip to the remaining source pixels — a 1500×1024 source with tile_width={512} produces a 3×2 grid where the rightmost column's <img> tiles are 476px wide instead of 512.

mode={:viewer} — deep-zoom viewer mount

For real interactive deep-zoom (pan, pinch-zoom, full-resolution streaming) you need a JavaScript viewer — OpenSeadragon, Mirador, or Leaflet-IIIF are the canonical choices. This mode emits the markup those viewers consume.

<.iiif
  src="/portrait.jpg"
  host="https://iiif.example.org"
  mode={:viewer}
  width={1200}
  height={900}
  viewer={:openseadragon}
  id="viewer-portrait"
  phx-hook="OpenSeadragon"
/>

Renders:

<div
  data-iiif-info-url="https://iiif.example.org/iiif/3/portrait.jpg/info.json"
  data-iiif-viewer="openseadragon"
  style="width:1200px;height:900px;"
  id="viewer-portrait"
  phx-hook="OpenSeadragon"
>
  <img src="https://iiif.example.org/iiif/3/portrait.jpg/full/800,/0/default.jpg"
       style="width:100%;height:100%;object-fit:contain;" />
</div>

The fallback <img> is what the user sees while the JS loads (or permanently if JS is off / fails). The JS hook reads data-iiif-info-url, fetches info.json, and replaces the <img> with a tile-rendering canvas.

A minimal OpenSeadragon LiveView hook on the JS side:

const Hooks = {
  OpenSeadragon: {
    mounted() {
      const tileSources = this.el.dataset.iiifInfoUrl
      this.viewer = OpenSeadragon({ element: this.el, tileSources, prefixUrl: "/openseadragon/" })
    },
    destroyed() { this.viewer && this.viewer.destroy() }
  }
}

The component does not bundle the viewer JS — that's your application's choice. Each viewer has its own license, footprint, and API; baking one in would force a decision that doesn't apply to every consumer.

info.json discovery

Use Image.Components.URL.iiif_info_url/1 when you need the URL of the info.json document — e.g. for a <link rel="alternate"> in your page head, for a JS viewer config, or for fetching the document server-side to discover source dimensions:

alias Image.Components.URL

URL.iiif_info_url(source_path: "/cat.jpg", host: "https://iiif.example.org")
# => "https://iiif.example.org/iiif/3/cat.jpg/info.json"

Same identifier-encoding rules as the regular IIIF builder: leading / stripped, embedded / percent-encoded.