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