iiif_image_plug

An Elixir Plug implementing the IIIF image API specification.


Keywords
iiif, libvips
License
Apache-2.0

Documentation

CI status

IIIF Image Plug

An Elixir plug implementing the International Image Interoperability Framework (IIIF) image API specification.

  • The goal of IIIF is to define a standardised REST API for serving high resolution images (art, photographes or archival material published by museums, universities and similar institutions).
  • This plug library needs you to define a mapping between image identifier (used in the REST API) and file system path, and will then do the image transformations based on the other request parameters for you.
  • There exist several generic Javascript IIIF viewers that utilize this API to allow for optimized viewing (dynamic loading of parts of the image data based on zoom level/viewport).
  • WebGIS Javascript libraries like leaflet or OpenLayers support IIIF in one way or the other.
  • For the time beeing only (the current) Image API 3.0 is implemented, check out the IIIF documentation for its capabilities.
  • The IIIF image API implemented by this library is just one (the foundational) of currently six API standards the IIIF community defines for serving multimedia content and metadata.
  • The image processing is handled by libvips via Vix.

Installation

The package can be installed by adding iiif_image_plug to your list of dependencies in mix.exs:

def deps do
  [
    {:iiif_image_plug, "~> 0.7.0"}
  ]
end

Usage

Assuming you want to serve IIIF in your plug based server at "/iiif/v3", add a forward route like this:

  forward("/iiif/v3",
    to: MyApp.IIIFPlug,
    init_opts: %IIIFImagePlug.V3.Options{}
  )

For Phoenix it would look slightly different:

  forward("/iiif/v3", MyApp.IIIFPlug, %IIIFImagePlug.V3.Options{})

Example plug

A plug implementation may look something like this:

defmodule MyApp.IIIFPlug do
  use IIIFImagePlug.V3

  # There are two required callbacks you have to implement, plus 
  # several optional ones. See the `IIIFImagePlug.V3` 
  # documentation for more.

  @impl true
  def info_metadata(identifier) do
    # The first required callback lets you inject some metadata 
    # from your application into the plug when it is responding to
    # an information request (info.json) for a specific `identifier`. 
    # The only required field is `:path`, which tells the plug the 
    # file system path matching the given `identifier`.

    MyApp.ContextModule.get_image_metadata(identifier)
    |> case do
      %{path: path, rights_statement: rights} ->
        {
          :ok,
          %IIIFImagePlug.V3.InfoRequestMetadata{
            path: path,
            rights: rights
          }
        }
      {:error, :not_found} ->
        {
          :error,
          %IIIFImagePlug.V3.RequestError{
            status_code: 404,
            msg: :not_found
          }
        }
    end
  end

  @impl true
  def data_metadata(identifier) do
    # The second required callback lets you inject some metadata 
    # from your application into the plug when it is responding to
    # an actual image data request for a specific `identifier`. As 
    # with `info_metadata/1`, the only required field is `:path`, which 
    # tells the plug the file system path matching the given `identifier`.
    MyApp.ContextModule.get_image_path(identifier)
    |> case do
      {:ok, path} ->
        {
          :ok,
          %IIIFImagePlug.V3.DataRequestMetadata{
            path: path,      
            response_headers: [
              {"cache-control", "public, max-age=31536000, immutable"}
            ]
          }
        }
      {:error, :not_found} ->
        {
          :error,
          %IIIFImagePlug.V3.RequestError{
            status_code: 404,
            msg: :not_found
          }
        }
    end
  end

CORS

For your service to fully implement the API specification, you need to properly configure Cross-Origin Resource Sharing (CORS). You could either set the correct headers in your info_metadata/1 or data_metadata/1 implementation or configure the appropriate headers in a plug before this one (cors_plug was used in this example):

(..)
  plug(CORSPlug, origin: ["*"])
  plug(:match)
  plug(:dispatch)

  forward("/",
    to: MyApp.IIIFPlug,
    init_opts: (..)
  )
end
(..)

Testing your endpoint

Because this plug is just a library and only part of your overall application, you might want to test your service's IIIF compliance against the official validator:

Development

This repository comes with a minimalistic server, run the server with:

iex -S mix run

The metadata of the main sample file test/images/bentheim.jpg can now be accessed at http://localhost:4000/bentheim.jpg/info.json:

{
    "id": "http://localhost:4000/bentheim.jpg",
    "profile": "level2",
    "type": "ImageServer3",
    "protocol": "http://iiif.io/api/image",
    "rights": "https://creativecommons.org/publicdomain/zero/1.0/",
    "width": 3000,
    "height": 2279,
    "@context": "http://iiif.io/api/image/3/context.json",
    "maxHeight": 10000,
    "maxWidth": 10000,
    "maxArea": 100000000,
    "extra_features": [
        "mirroring",
        "regionByPct",
        "regionByPx",
        "regionSquare",
        "rotationArbitrary",
        "sizeByConfinedWh",
        "sizeByH",
        "sizeByPct",
        "sizeByW",
        "sizeByWh",
        "sizeUpscaling"
    ],
    "preferredFormat": [
        "jpg"
    ],
    "extraFormats": [
        "webp",
        "png",
        "tif"
    ],
    "extraQualities": [
        "color",
        "gray",
        "bitonal"
    ]
}

The sample image can be viewed at http://localhost:4000/bentheim.jpg/full/max/0/default.jpg and you can start experimenting with the IIIF API parameters.

Jacob van Ruisdael. Gezicht op kasteel Bentheim, circa 1653