defmodule Image.Plug.Provider.IIIF.InfoJson do
@moduledoc """
Builds the IIIF Image API 3.0 [Image Information document](https://iiif.io/api/image/3.0/#5-image-information)
(`info.json`) for a given source.
The info document describes what the server can do with this
particular image: its full pixel dimensions, the qualities and
formats it can deliver, the spec features it supports, and the
compliance level. Clients fetch info.json before issuing any
image request to learn what URLs they're allowed to construct.
This module is invoked by `Image.Plug` when an IIIF provider
recognises an `info.json` request. The provider tags the request
with `:json` output; the plug routes to `build/2` instead of the
normal pipeline interpreter.
### Compliance
Targets [IIIF Image API 3.0 Compliance Level 2](https://iiif.io/api/image/3.0/compliance/).
The advertised `extraFeatures`, `extraQualities`, and `extraFormats`
reflect what `Image.Plug.Provider.IIIF.Options` can parse and what
`Image.Plug.Capabilities` confirms libvips can encode.
"""
alias Image.Plug.Capabilities
@context "http://iiif.io/api/image/3/context.json"
@protocol "http://iiif.io/api/image"
@profile "level2"
@type_value "ImageService3"
# Always-supported qualities — the bare minimum any Compliance Level
# 0 server promises. Additional qualities (`gray`, `bitonal`) are
# advertised under `extraQualities` based on what we can actually
# produce.
@base_qualities ["default", "color"]
@extra_qualities ["gray", "bitonal"]
# Always-supported formats per Compliance Level 1+. JPEG is required
# by Level 0; PNG is the standard alpha-friendly raster. Anything
# else is advertised under `extraFormats` based on capabilities.
@base_formats ["jpg"]
@level_2_formats ["png"]
# Always-supported features per Compliance Level 2. The full
# taxonomy lives at https://iiif.io/api/image/3.0/#57-extra-functionality.
@base_features [
"baseUriRedirect",
"canonicalLinkHeader",
"cors",
"jsonldMediaType",
"mirroring",
"profileLinkHeader",
"regionByPct",
"regionByPx",
"regionSquare",
"rotationArbitrary",
"rotationBy90s",
"sizeByConfinedWh",
"sizeByH",
"sizeByPct",
"sizeByW",
"sizeByWh",
"sizeUpscaling"
]
@doc """
Builds an info.json map for the given source dimensions.
### Arguments
* `id` is the absolute URL the info.json document is served from
*without* the trailing `/info.json` segment. Per the spec, this
is the image's canonical identifier URL.
* `dimensions` is `{width, height}` of the source image, in
pixels.
### Returns
* A plain Elixir map ready to be serialised with `:json.encode/1`.
### Examples
iex> info = Image.Plug.Provider.IIIF.InfoJson.build(
...> "https://iiif.example.org/iiif/3/cat.jpg",
...> {1024, 768}
...> )
iex> info["@context"]
"http://iiif.io/api/image/3/context.json"
iex> info["width"]
1024
iex> info["height"]
768
iex> info["protocol"]
"http://iiif.io/api/image"
"""
@spec build(String.t(), {pos_integer(), pos_integer()}) :: map()
def build(id, {width, height}) when is_binary(id) and width > 0 and height > 0 do
%{
"@context" => @context,
"id" => id,
"type" => @type_value,
"protocol" => @protocol,
"profile" => @profile,
"width" => width,
"height" => height,
"extraQualities" => available_extra_qualities(),
"extraFormats" => available_extra_formats(),
"extraFeatures" => @base_features
}
end
@doc false
def base_qualities, do: @base_qualities
@doc false
def base_formats, do: @base_formats ++ @level_2_formats
defp available_extra_qualities do
# Both gray and bitonal are pure pixel-domain transforms libvips
# can always produce; no capability probe needed.
@extra_qualities
end
defp available_extra_formats do
# Cull `tif` / `jp2` / `webp` based on what libvips can actually
# write. Always advertise WebP (libvips bundled support is
# ubiquitous), gate the rest on capability probes.
formats =
["webp"]
|> maybe_append("tif", Capabilities.tiff_write?())
|> maybe_append("jp2", Capabilities.jp2_write?())
|> maybe_append("avif", Capabilities.avif_write?())
formats
end
defp maybe_append(list, _value, false), do: list
defp maybe_append(list, value, true), do: list ++ [value]
end