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

A serializable image value used by image generation, editing, and vision
inputs. See spec §35.2.1.

Layer A — pure 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` — 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_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 (Decision #2 / phasing principle
#8).

## 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 (Decision #2). 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}` (Decision #6) — 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*
