Skip to main content

lib/image/plug/provider/iiif/info_json.ex

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