IIIFImagePlug.V3 behaviour (IIIFImagePlug v0.7.0)

View Source

This plug implements the IIIF Image API version 3 (see also https://iiif.io/api/image/3.0).

Summary

Callbacks

Optional callback function that gets triggered at the start of each image data request, before any processing is done.

Required callback function triggered on image data requests, that maps the given identifier to an image file.

Optional callback function that is triggered right before the final image gets rendered and sent.

Optional callback function to override the :host evaluated from the Plug.Conn, useful if your Elixir app runs behind a proxy.

Optional callback function that gets triggered at the start of an image information request, before any further evaluation is done.

Required callback function triggered on information requests (info.json), that maps the given identifier to an image file.

Optional callback function that gets triggered right before the info.json gets sent.

Optional callback function to override the :port evaluated from the Plug.Conn, useful if your Elixir app runs behind a proxy.

Optional callback function to override the :scheme ("http" or "https") evaluated from the Plug.Conn, useful if your Elixir app runs behind a proxy.

Optional callback function that lets you override the default plug error response.

Callbacks

data_call(conn)

(optional)
@callback data_call(conn :: Plug.Conn.t()) ::
  {:continue, Plug.Conn.t()} | {:stop, Plug.Conn.t()}

Optional callback function that gets triggered at the start of each image data request, before any processing is done.

If you want the plug to continue processing the data request, return {:continue, conn}, otherwise you might instruct the plug to stop further processing by returning {:stop, conn}.

This could be used in conjunction with IIIFImagePlug.V3.data_response/3 to implement your own caching strategy.

(naive!) Example

@impl true
def data_call(conn) do
  path = construct_cache_path(conn)

  if File.exists?(path) do
    {:stop, Plug.Conn.send_file(conn, 200, path)}
  else
    {:continue, conn}
  end
end

@impl true
def data_response(%Plug.Conn{} = conn, %Vix.Vips.Image{} = image, _format) do
  path = construct_cache_path(conn)

  path
  |> Path.dirname()
  |> File.mkdir_p!()

  Vix.Vips.Image.write_to_file(image, path)

  {:stop, send_file(conn, 200, path)}
end

data_metadata(identifier)

@callback data_metadata(identifier :: String.t()) ::
  {:ok, IIIFImagePlug.V3.DataRequestMetadata.t()}
  | {:error, IIIFImagePlug.V3.RequestError.t()}

Required callback function triggered on image data requests, that maps the given identifier to an image file.

Returns

Example

def data_metadata(identifier) do
  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

data_response(conn, image, format)

(optional)
@callback data_response(
  conn :: Plug.Conn.t(),
  image :: Vix.Vips.Image.t(),
  format :: atom()
) ::
  {:continue, Plug.Conn.t()} | {:stop, Plug.Conn.t()}

Optional callback function that is triggered right before the final image gets rendered and sent.

This could be used in conjunction with IIIFImagePlug.V3.data_call/1 to implement your own caching strategy.

host()

@callback host() :: String.t() | nil

Optional callback function to override the :host evaluated from the Plug.Conn, useful if your Elixir app runs behind a proxy.

Example

def host(), do: "images.example.org"

info_call(conn)

(optional)
@callback info_call(conn :: Plug.Conn.t()) ::
  {:continue, Plug.Conn.t()} | {:stop, Plug.Conn.t()}

Optional callback function that gets triggered at the start of an image information request, before any further evaluation is done.

If you want the plug to continue processing the information request, return {:continue, conn}, otherwise you might instruct the plug to stop further processing by returning {:stop, conn}. This can be used in conjunction with IIIFImagePlug.V3.info_response/2 to implement your own caching strategy.

(naive!) Example

@impl true
def info_call(conn) do
  path = construct_cache_path(conn)

  if File.exists?(path) do
    {:stop, Plug.Conn.send_file(conn, 200, path)}
  else
    {:continue, conn}
  end
end

@impl true
def info_response(conn, info) do
  path = construct_cache_path(conn)

  path
  |> Path.dirname()
  |> File.mkdir_p!()

  File.write!(path, Jason.encode!(data))

  {:stop, send_file(conn, 200, path)}
end

defp construct_cache_path(conn) do
  "/tmp/#{Path.join(conn.path_info)}"
end

info_metadata(identifier)

@callback info_metadata(identifier :: String.t()) ::
  {:ok, IIIFImagePlug.V3.InfoRequestMetadata.t()}
  | {:error, IIIFImagePlug.V3.RequestError.t()}

Required callback function triggered on information requests (info.json), that maps the given identifier to an image file.

Returns

Example

def info_metadata(identifier) do
  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

info_response(conn, info)

(optional)
@callback info_response(conn :: Plug.Conn.t(), info :: map()) ::
  {:continue, Plug.Conn.t()} | {:stop, Plug.Conn.t()}

Optional callback function that gets triggered right before the info.json gets sent.

This could be used in conjunction with IIIFImagePlug.V3.info_call/1 to implement your own caching strategy.

port()

@callback port() :: pos_integer() | nil

Optional callback function to override the :port evaluated from the Plug.Conn, useful if your Elixir app runs behind a proxy.

Example

def port(), do: 1337

scheme()

@callback scheme() :: String.t() | nil

Optional callback function to override the :scheme ("http" or "https") evaluated from the Plug.Conn, useful if your Elixir app runs behind a proxy.

Example

def scheme(), do: "https"

send_error(conn, status_code, msg)

@callback send_error(
  conn :: Plug.Conn.t(),
  status_code :: number(),
  msg :: atom()
) :: Plug.Conn.t()

Optional callback function that lets you override the default plug error response.

Examples

Default implementation

The default response for all errors is defined as follows:

def send_error(conn, status_code, msg) do
  conn
  |> Plug.Conn.put_resp_content_type("application/json")
  |> Plug.Conn.send_resp(
    status_code,
    Jason.encode!(%{error: msg})
  )
end

Rewriting 404 for data requests to serve a fallback image

You can pattern match on specific conn, status_code or msg to overwrite specific cases.

One use case might be sending your own placeholder image instead of the JSON for failed data requests.

First customize your data_metadata/1 implementation with a specific message (you do not want to return an image on a failed info.json request):

def data_metadata(identifier) do
  MyApp.ContextModule.get_image_path(identifier)
  |> case do
    {:ok, path} ->
      (...)
    {:error, :not_found} ->
      {
        :error,
        %IIIFImagePlug.V3.RequestError{
          status_code: 404,
          msg: :data_metadata_not_found
        }
      }
  end
end

Then add a custom send_error/3 that picks up on the status code and message you defined:

def send_error(conn, 404, :data_metadata_not_found) do
  Plug.Conn.send_file(conn, 404, "#{Application.app_dir(:my_app)}/images/not_found.webp")
end

For all errors that do not match the pattern, the plug will be falling back to the default implementation shown above.

Rewriting errors generated by the plug

This also works for errors the plug generates interally:

def send_error(conn, 400, :invalid_rotation) do
  requested_rotation = MyApp.extract_iiif_parameter(conn, :rotation)

  conn
  |> Plug.Conn.put_resp_content_type("application/json")
  |> Plug.Conn.send_resp(
    400,
    Jason.encode!(%{error: "Your rotation parameter '#{requested_rotation}' is invalid!"})
  )
end