# `ALLM.Image`
[🔗](https://github.com/cykod/ALLM/blob/v0.5.0/lib/allm/image.ex#L1)

A serializable image value used by image generation, editing, and vision
inputs — Layer A serializable data.

The `:source` is one of four tagged tuples and is the
only required field. All other fields are nilable to support both inputs
(vision: only `:source`/`:mime_type`) and outputs (generated:
`:prompt`/`:revised_prompt`/`:width`/`:height` populated by the adapter).

## Source variants

- `{:binary, bytes}` — raw image bytes; an explicit `:mime_type` MUST be
  supplied via `from_binary/2`.
- `{:base64, encoded}` — standard base64-encoded bytes (with padding) plus
  explicit `:mime_type`. Constructed via `from_base64/2` or
  `from_data_uri/1` (the latter parses a `data:<mime>;base64,<encoded>`
  string). Pure data, no decode validation. Callers passing URL-safe
  (`-`/`_`) or unpadded base64 will round-trip cleanly through ETF / JSON
  but `to_data_uri/1`'s fast-path (which forwards the encoded string
  verbatim) may emit a `data:` URI a consumer rejects.
- `{:url, url}` — public HTTP/HTTPS URL. `from_url/1` does NOT inspect or
  fetch the URL; `:mime_type` is `nil` and the adapter resolves it. URLs are
  NEVER fetched in Layer A — see `to_binary/1` and `to_data_uri/1` for the
  `:remote_source` error.
- `{:file, path}` — local filesystem path. `from_file/1` does NOT call
  `File.read/1`; only the path is stored. MIME type is inferred from the
  extension (lowercase) and may be `nil` when the extension is missing or
  unknown. Filesystem I/O happens only in `to_binary/1`/`to_data_uri/1`.

## Serializability

ETF round-trip via `:erlang.term_to_binary/1` preserves every legal source
shape verbatim. JSON round-trip via `ALLM.Serializer` represents `:source`
as `%{"type" => "<kind>", "value" => <value>}`; the `{:binary, _}` variant
Base64-encodes its bytes on the wire so JSON stays text-safe. Decoding
dispatches on `data["source"]["type"]` against the closed set
`~w[binary base64 url file]`; an unknown type falls through to the
`{:_unknown, :atom_decode_failed}` field-error path. A `"binary"` source
whose `"value"` is not valid base64 surfaces as
`{[:source], :invalid_base64}` (raised pre-emptively as a
`ValidationError` so the field-error survives `Serializer.from_json/1`'s
`ArgumentError` rescue).

# `source`

```elixir
@type source() ::
  {:binary, binary()}
  | {:base64, String.t()}
  | {:url, String.t()}
  | {:file, Path.t()}
```

# `t`

```elixir
@type t() :: %ALLM.Image{
  height: non_neg_integer() | nil,
  metadata: map(),
  mime_type: String.t() | nil,
  prompt: String.t() | nil,
  revised_prompt: String.t() | nil,
  source: source(),
  width: non_neg_integer() | nil
}
```

# `from_base64`

```elixir
@spec from_base64(String.t(), String.t()) :: t()
```

Build an `%Image{}` from a base64-encoded string plus an explicit MIME type.

Pure data — does NOT validate that `encoded` is well-formed base64. Standard
base64 with padding is the documented contract; URL-safe base64 (`-`/`_`)
or unpadded variants may produce a struct whose `to_data_uri/1` fast-path
emits a `data:` URI a downstream consumer rejects. `mime_type: nil` raises
`FunctionClauseError`.

## Examples

    iex> img = ALLM.Image.from_base64("aGk=", "image/png")
    iex> img.source
    {:base64, "aGk="}
    iex> img.mime_type
    "image/png"

# `from_binary`

```elixir
@spec from_binary(binary(), String.t()) :: t()
```

Build an `%Image{}` from raw bytes plus an explicit MIME type.

Both arguments are required and must be binaries — `mime_type: nil` raises
`FunctionClauseError` (the `is_binary(mime_type)` runtime guard backs the
`@spec` so callers don't get silently-`nil`-mime structs).

## Examples

    iex> img = ALLM.Image.from_binary(<<137, 80>>, "image/png")
    iex> img.source
    {:binary, <<137, 80>>}
    iex> img.mime_type
    "image/png"

# `from_data_uri`

```elixir
@spec from_data_uri(String.t()) :: t()
```

Build an `%Image{}` from a `data:<mime>;base64,<encoded>` URI string.

Parses the URI scheme into `{:base64, encoded}` source + explicit
`:mime_type` — no base64 decode, no MIME validation. The output is
equivalent to `from_base64(encoded, mime)` so the `to_data_uri/1`
fast-path round-trips the input string verbatim.

Only the standard `;base64,<payload>` form is supported. URL-encoded
payloads (`data:<mime>,<urlencoded>` without `;base64`) raise
`ArgumentError` — adapters that consume `data:` URIs expect base64.

Media-type parameters (`data:image/svg+xml;charset=utf-8;base64,...`)
are accepted: the leading `<type>/<subtype>` segment is kept and any
trailing `;`-delimited parameters are dropped before storage so the
resulting `%Image{}.mime_type` is a bare type/subtype.

Raises `ArgumentError` when the input is not a `data:` URI, when the
`;base64,` segment is missing, or when the MIME segment is empty.

## Examples

    iex> img = ALLM.Image.from_data_uri("data:image/png;base64,aGk=")
    iex> img.source
    {:base64, "aGk="}
    iex> img.mime_type
    "image/png"

    iex> ALLM.Image.to_data_uri(ALLM.Image.from_data_uri("data:image/jpeg;base64,QUJDRA=="))
    {:ok, "data:image/jpeg;base64,QUJDRA=="}

    iex> ALLM.Image.from_data_uri("data:image/svg+xml;charset=utf-8;base64,PHN2Zy8+").mime_type
    "image/svg+xml"

# `from_file`

```elixir
@spec from_file(Path.t()) :: t()
```

Build an `%Image{}` from a local filesystem path.

Pure data — does NOT call `File.read/1`. The path is stored verbatim and the
filesystem is only touched at `to_binary/1` / `to_data_uri/1` time. The
`:mime_type` is inferred from the lowercased extension against the closed
set `[".png", ".jpg", ".jpeg", ".webp", ".gif"]`; unknown or missing
extensions leave `:mime_type` as `nil`.

## Examples

    iex> img = ALLM.Image.from_file("/tmp/cat.png")
    iex> img.source
    {:file, "/tmp/cat.png"}
    iex> img.mime_type
    "image/png"

    iex> ALLM.Image.from_file("/tmp/noext").mime_type
    nil

# `from_url`

```elixir
@spec from_url(String.t()) :: t()
```

Build an `%Image{}` from a URL string.

Pure data — does NOT inspect or fetch the URL. The `:mime_type` is `nil`;
the adapter that consumes the image resolves the type at request-build
time. URL fetching is banned in Layer A — adapters fetch at
request-build time.

## Examples

    iex> img = ALLM.Image.from_url("https://example.com/x.png")
    iex> img.source
    {:url, "https://example.com/x.png"}
    iex> img.mime_type
    nil

# `to_binary`

```elixir
@spec to_binary(t()) ::
  {:ok, binary()} | {:error, :remote_source | :invalid_base64 | File.posix()}
```

Resolve the image's `:source` to raw bytes.

- `{:binary, b}` returns `{:ok, b}` verbatim.
- `{:base64, s}` decodes via `Base.decode64/1`; invalid base64 returns
  `{:error, :invalid_base64}`.
- `{:url, _}` returns `{:error, :remote_source}` — Layer A NEVER fetches
  URLs. Adapters fetch at request-build time.
- `{:file, path}` reads the file via `File.read/1` and returns its
  `{:ok, _} | {:error, posix}` shape directly (`:enoent` on missing,
  etc.).

## Examples

    iex> ALLM.Image.to_binary(ALLM.Image.from_binary(<<1, 2>>, "image/png"))
    {:ok, <<1, 2>>}

    iex> ALLM.Image.to_binary(ALLM.Image.from_base64("aGVsbG8=", "image/png"))
    {:ok, "hello"}

    iex> ALLM.Image.to_binary(ALLM.Image.from_url("https://example.com/x.png"))
    {:error, :remote_source}

# `to_data_uri`

```elixir
@spec to_data_uri(t()) ::
  {:ok, String.t()}
  | {:error,
     :remote_source | :missing_mime_type | :invalid_base64 | File.posix()}
```

Resolve the image to a `data:<mime>;base64,<...>` URI.

- `{:binary, b}` with a `:mime_type` Base64-encodes the bytes.
- `{:base64, s}` with a `:mime_type` forwards `s` verbatim — fast path,
  no decode-then-re-encode.
- `{:file, path}` with a `:mime_type` reads + Base64-encodes.
- `{:url, _}` returns `{:error, :remote_source}` — a `data:` URI and an
  `https:` URI are different addressing schemes; passing the URL
  through verbatim would surprise.
- Missing `:mime_type` returns `{:error, :missing_mime_type}` — there is
  no default like `application/octet-stream`.

## Examples

    iex> ALLM.Image.to_data_uri(ALLM.Image.from_binary("hi", "image/png"))
    {:ok, "data:image/png;base64,aGk="}

    iex> ALLM.Image.to_data_uri(ALLM.Image.from_base64("aGk=", "image/png"))
    {:ok, "data:image/png;base64,aGk="}

    iex> ALLM.Image.to_data_uri(ALLM.Image.from_url("https://example.com/x.png"))
    {:error, :remote_source}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
