IIIFImagePlug.V3 behaviour (IIIFImagePlug v0.7.0)
View SourceThis 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
@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
@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
{:ok, data_metadata}
on success, wheredata_metadata
is aIIIFImagePlug.V3.DataRequestMetadata
struct.{:error, request_error}
otherwise, whererequest_error
is aIIIFImagePlug.V3.RequestError
struct.
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
@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.
@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"
@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
@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
{:ok, info_metadata}
on success, whereinfo_metadata
is aIIIFImagePlug.V3.InfoRequestMetadata
struct.{:error, request_error}
otherwise, whererequest_error
is aIIIFImagePlug.V3.RequestError
struct.
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
@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.
@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
@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"
@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