defmodule Ssimulacra2 do @moduledoc """ SSIMULACRA2 perceptual image-quality metric for Elixir, backed by the `fast-ssim2` Rust crate. Inputs are packed binaries whose layout is chosen with the `:format` option (default `:rgb888`). The score is on the native SSIMULACRA2 0–100 scale: 100 is pixel-identical, ~90+ is visually lossless, and low/negative values indicate large perceptual differences. ## Formats | format | element | channels | bytes/pixel | color space | | --- | --- | --- | --- | --- | | `:rgb888` (default) | `u8` | 3 | 3 | sRGB (gamma) | | `:rgb16` | `u16` | 3 | 6 | sRGB (gamma) | | `:linear_rgb` | `f32` | 3 | 12 | linear RGB | | `:gray8` | `u8` | 1 | 1 | sRGB grayscale | | `:linear_gray` | `f32` | 1 | 4 | linear grayscale | Convention: integer elements are sRGB (gamma-encoded); float elements are linear RGB. Grayscale is expanded to RGB (R=G=B). Multi-byte elements (`u16`, `f32`) are **native-endian** — e.g. `<>` / `<>`. A binary's size must equal `width * height * channels * bytes_per_element` for its format. ## Cancellation `compare/5` and `Ssimulacra2.Reference.compare/3` accept `cancel:` (an `Ssimulacra2.CancelRef`) and `timeout:` (milliseconds) to abort a long comparison mid-computation, returning `{:error, :cancelled}` or `{:error, :timeout}`. Create a ref with `Ssimulacra2.CancelRef.new/0` and trip it with `cancel/1`. """ alias Ssimulacra2.{Cancellation, CancelRef, Native, Validate} @type image_data :: binary() @type reason :: :invalid_dimensions | :size_mismatch | :dimension_mismatch | :unknown_format | :invalid_cancel | :invalid_timeout | :cancelled | :timeout | {:ssimulacra2, String.t()} @doc """ Compare a reference and distorted image of the same dimensions. Pass `format:` to select the packed pixel layout; it defaults to `:rgb888` (packed 8-bit sRGB, `byte_size == width * height * 3`). Other supported formats are `:rgb16`, `:linear_rgb`, `:gray8`, and `:linear_gray`. Returns `{:ok, score}` or `{:error, reason}`. ## Cancellation Pass `cancel:` an `Ssimulacra2.CancelRef` to abort the comparison from another process (e.g. on client disconnect) — the call returns `{:error, :cancelled}`. Pass `timeout:` a positive integer of milliseconds to bound the wall-clock time — the call returns `{:error, :timeout}` if it exceeds that. Both may be combined; cancellation is checked at strip boundaries, so the CPU is freed promptly without leaving the dirty scheduler. Invalid options return `{:error, :invalid_cancel}` / `{:error, :invalid_timeout}`. """ @spec compare(image_data(), image_data(), pos_integer(), pos_integer(), keyword()) :: {:ok, float()} | {:error, reason()} def compare(reference, distorted, width, height, opts \\ []) when is_binary(reference) and is_binary(distorted) do format = Keyword.get(opts, :format, :rgb888) cancel = Keyword.get(opts, :cancel) timeout = Keyword.get(opts, :timeout) with :ok <- Validate.format(format), :ok <- Validate.dims(width, height), :ok <- Validate.size(reference, width, height, format), :ok <- Validate.size(distorted, width, height, format), :ok <- Validate.cancel(cancel), :ok <- Validate.timeout(timeout) do Cancellation.run(cancel, timeout, fn resource -> Native.compare(reference, distorted, width, height, format, resource) end) end end @doc """ Like `compare/5` but returns the bare score and raises `Ssimulacra2.Error` on failure. Accepts the same `format:` option, defaulting to `:rgb888`. """ @spec compare!(image_data(), image_data(), pos_integer(), pos_integer(), keyword()) :: float() def compare!(reference, distorted, width, height, opts \\ []) do case compare(reference, distorted, width, height, opts) do {:ok, score} -> score {:error, reason} -> raise Ssimulacra2.Error, reason: reason end end @doc """ Trip an `Ssimulacra2.CancelRef`, aborting any comparison that uses it. Call from any process to cancel an in-flight `compare/5` or `Ssimulacra2.Reference.compare/3` that was passed this ref as `cancel:`. Returns `:ok` and is safe to call more than once. """ @spec cancel(CancelRef.t()) :: :ok def cancel(%CancelRef{resource: r}), do: Native.token_cancel(r) end