Skip to main content

lib/image/plug/capabilities.ex

defmodule Image.Plug.Capabilities do
  @moduledoc """
  Probes the local libvips build for optional encoder capabilities.

  Run once at application boot. Results are cached in
  `:persistent_term` so per-request reads are free.

  Currently probes AVIF, JP2, and TIFF write support. Adding a
  probe for another capability is one new clause in `probe/0` and
  one new accessor.
  """

  require Logger

  @probe_key {__MODULE__, :avif_write?}

  @doc """
  Performs every capability probe and caches the results.

  Idempotent: calling `probe/0` twice in the same VM lifetime returns
  the same booleans without re-probing.

  ### Returns

  * `:ok`.

  """
  @spec probe() :: :ok
  def probe do
    case :persistent_term.get(@probe_key, :unprobed) do
      :unprobed ->
        avif? = probe_avif_write()
        :persistent_term.put(@probe_key, avif?)

        unless avif? do
          Logger.warning(
            "image_plug: libvips lacks AVIF write support — `format=avif` " <>
              "requests will be served as WebP and tagged with " <>
              "`x-image-plug-format-fallback: avif->webp`."
          )
        end

        :ok

      _cached ->
        :ok
    end
  end

  @doc """
  Returns whether the local libvips can encode AVIF.

  Falls back to a one-off probe if `probe/0` (in this module) has
  not been called — useful in tests that exercise the encoder
  without booting the application.

  ### Returns

  * `true` if libvips can write AVIF.

  * `false` otherwise.

  ### Examples

      iex> is_boolean(Image.Plug.Capabilities.avif_write?())
      true

  """
  @spec avif_write?() :: boolean()
  def avif_write? do
    case :persistent_term.get(@probe_key, :unprobed) do
      :unprobed ->
        result = probe_avif_write()
        :persistent_term.put(@probe_key, result)
        result

      result when is_boolean(result) ->
        result
    end
  end

  defp probe_avif_write, do: probe_format_write(".avif")

  @doc """
  Returns whether the local libvips can encode TIFF.

  Required by IIIF Image API 3.0 Compliance Level 2 deployments
  that promise TIFF support.

  ### Returns

  * `true` if libvips can write TIFF.

  * `false` otherwise.

  """
  @spec tiff_write?() :: boolean()
  def tiff_write?, do: cached_probe({__MODULE__, :tiff_write?}, ".tif")

  @doc """
  Returns whether the local libvips can encode JPEG 2000 (JP2).

  Required by IIIF Image API 3.0 Compliance Level 2 deployments
  that promise JP2 support. Available only when libvips was built
  with `libopenjp2`.

  ### Returns

  * `true` if libvips can write JP2.

  * `false` otherwise.

  """
  @spec jp2_write?() :: boolean()
  def jp2_write?, do: cached_probe({__MODULE__, :jp2_write?}, ".jp2")

  defp cached_probe(key, suffix) do
    case :persistent_term.get(key, :unprobed) do
      :unprobed ->
        result = probe_format_write(suffix)
        :persistent_term.put(key, result)
        result

      result when is_boolean(result) ->
        result
    end
  end

  defp probe_format_write(suffix) do
    case Image.new(1, 1, color: [0, 0, 0]) do
      {:ok, image} ->
        case Image.write(image, :memory, suffix: suffix, quality: 50) do
          {:ok, _bytes} -> true
          {:error, _reason} -> false
        end

      {:error, _reason} ->
        false
    end
  rescue
    # `Image.write/3` can raise (e.g. via `stream!/2`) for some
    # invalid suffix configurations on builds without the encoder.
    _ -> false
  end
end