# `PdfElixide.Document.Image`
[🔗](https://github.com/r8/pdf_elixide/blob/v0.16.0/lib/pdf_elixide/document/image.ex#L1)

A raster image (photo, logo, scanned picture) extracted from a PDF page, with
its zero-based page index, on-page bounding box, and dimensions.

The pixel data is not carried on the struct; instead `:ref` is a handle to the
underlying image, and you encode it on demand with `to_binary/2` (bytes) or
`save/3` (to a file), choosing `:png` or `:jpeg`:

    {:ok, png} = PdfElixide.Document.Image.to_binary(image)              # PNG bytes
    {:ok, jpg} = PdfElixide.Document.Image.to_binary(image, format: :jpeg)
    :ok = PdfElixide.Document.Image.save(image, "out.png")              # format inferred
    :ok = PdfElixide.Document.Image.save(image, "out.jpg", format: :jpeg)

`:format` reports how the image was *stored* in the PDF — `:jpeg` (a JPEG blob)
or `:raw` (decoded pixels) — which tells you whether a JPEG encode is lossless:
for a `:jpeg` source the original bytes are passed through untouched (except
CMYK JPEGs, which must be re-encoded to RGB), while a `:raw` source is always
encoded fresh. The `:color_space` and `:bits_per_component` fields describe the
image as it was stored.

For the raw stored bytes (rather than an encoded PNG/JPEG), use `data/1`, which
returns `{:jpeg, bytes}` (the original JPEG blob) or `{:raw, bytes,
pixel_format}` (bare pixels — not a standalone file; pair them with `:width`,
`:height`, and `:color_space`, or reach for `to_binary/2` when you want an
encoded image).

`:matrix` is the transformation the image was drawn under (`t:matrix/0`). For
an image the page paints directly, `:bbox` is that matrix applied to the unit
square and squared up to the axes — so for a rotated or skewed image the box is
looser than the placement — and `:rotation_degrees` is the angle the matrix
turns through. An image painted inside a Form XObject is the exception, and
`t:matrix/0` says how the three disagree there.

`data/1`, `to_binary/2` and `save/3` take the handle's lock shared and
`close/1` takes it exclusively, so encoding one image from several processes
runs in parallel; see the [Concurrency](guides/concurrency.md) guide.

## Which images are extracted

**An image is left out of the list rather than reported as an error** when
it is under 8 pixels wide or tall, or when its stored encoding cannot be
decoded. Flate, LZW, run-length, CCITT fax, JPEG and JPEG 2000
(`/JPXDecode`) decode; JBIG2 does not, nor does a JPEG 2000 codestream whose
component count is anything but 1, 3 or 4. A JPEG 2000 image arrives as
`:raw` pixels whose `pixel_format` comes from its codestream rather than
from the colour space the PDF declares for it.

`PdfElixide.Document.images/1` and `PdfElixide.Document.images/2` return
`{:ok, list}` either way, so a page whose only picture was skipped is
indistinguishable from a page with none — nothing is raised and nothing is
logged.

**JPEG 2000 transparency is the one case that comes back wrong rather than
missing.** The alpha channel declared through `/SMaskInData` is ignored, so
a four-component codestream carrying RGB plus alpha is reported as `:cmyk`,
and `to_binary/2` and `save/3` *succeed* with wrong colours instead of
failing. To spot one, compare `:color_space` against `data/1`'s
`pixel_format`: a `:device_rgb` image whose pixels are `:cmyk` is one. (An
`:indexed` image reports `:rgb` pixels too, but correctly — its palette
really is expanded to RGB.)

A page that cannot be reached, or whose `/Resources` cannot be resolved,
does return `{:error, t:PdfElixide.Error.t/0}`. A page whose content stream
fails to parse does not: it returns `{:ok, []}`, like a page with no images.

# `color_space`

```elixir
@type color_space() ::
  :device_rgb
  | :device_gray
  | :device_cmyk
  | :indexed
  | :cal_gray
  | :cal_rgb
  | :lab
  | :icc_based
  | :separation
  | :device_n
  | :pattern
```

The stored color space, resolved to an atom. `:icc_based` covers any
ICC-profile-based space (the component count is dropped).

# `image_opts`

```elixir
@type image_opts() :: [{:format, :png | :jpeg}]
```

Options for `to_binary/2` and `save/3`.

An unknown key, or a `:format` other than `:png` or `:jpeg`, raises
`ArgumentError` naming the offending key; see the "Errors versus exceptions"
section of `PdfElixide.Error`.

# `matrix`

```elixir
@type matrix() :: {float(), float(), float(), float(), float(), float()}
```

The transformation in effect where the image is painted, `{a, b, c, d, e, f}`.

Every `cm` operator in scope multiplied together, not the operands of any one
of them: a page drawing under `2 0 0 2 10 10 cm` and then `3 0 0 3 1 1 cm`
reports their product, `{6.0, 0.0, 0.0, 6.0, 12.0, 12.0}`.

The image occupies the unit square mapped through it, so
`{w, 0.0, 0.0, h, x, y}` is an unrotated image `w` by `h` points with its
bottom-left corner at `(x, y)`. An image drawn under no transformation reports
the identity `{1.0, 0.0, 0.0, 1.0, 0.0, 0.0}`; it is never `nil`.

## What it does not include

The matrix is in default PDF user space, so a page's rotation is not folded
into it — read `PdfElixide.Document.Page.rotation/1` to place the image as a
viewer displays it.

**An image painted inside a Form XObject reports the transformation of the
form's own frame, not of the page.** The form's `/Matrix` and the `cm`
operators inside it are included; the `cm` that preceded the form's own
painting is not. `:bbox` does include it, so for such an image `:matrix` and
`:bbox` describe different frames and `:rotation_degrees` is the angle within
the form. `:bbox` is the one to trust for where the image lands on the page.

# `pixel_format`

```elixir
@type pixel_format() :: :rgb | :grayscale | :cmyk
```

The layout of raw (uncompressed) pixel data from `data/1`.

# `raw_data`

```elixir
@type raw_data() :: {:jpeg, binary()} | {:raw, binary(), pixel_format()}
```

The raw stored bytes of an image, from `data/1`: either the original JPEG blob
or bare decoded pixels with their layout.

# `t`

```elixir
@type t() :: %PdfElixide.Document.Image{
  bbox: PdfElixide.Geometry.Rect.t() | nil,
  bits_per_component: non_neg_integer(),
  color_space: color_space(),
  format: :jpeg | :raw,
  height: non_neg_integer(),
  matrix: matrix(),
  page: non_neg_integer(),
  ref: reference(),
  rotation_degrees: integer(),
  width: non_neg_integer()
}
```

# `close`

```elixir
@spec close(t()) :: :ok
```

Releases the image's pixel data immediately.

The bytes behind `:ref` are normally freed when the BEAM garbage-collects the
handle; `close/1` frees them now, which is worth doing when walking many large
images. Calling it is optional and idempotent. It takes the handle's lock
exclusively, so it waits for an in-flight `data/1`, `to_binary/2` or `save/3`
on the same image — *immediately* means as soon as the handle is idle, not
preemptively.

Afterwards `data/1`, `to_binary/2`, and `save/3` return
`{:error, %PdfElixide.Error{reason: :closed}}` (bang variants raise it); the
metadata fields on the struct keep working. An image's lifetime is independent
of the document it came from — closing either one leaves the other usable.

# `closed?`

```elixir
@spec closed?(t()) :: boolean()
```

Returns whether the image has been released with `close/1`.

# `data`

```elixir
@spec data(t()) :: {:ok, raw_data()} | {:error, PdfElixide.Error.t()}
```

Returns the image's raw stored bytes.

Gives `{:jpeg, bytes}` for a JPEG-stored image (the original DCTDecode blob) or
`{:raw, bytes, pixel_format}` for one stored as decoded pixels, where
`pixel_format` is `:rgb`, `:grayscale`, or `:cmyk`. The `:raw` bytes are bare
pixels, not a standalone image file — use `to_binary/2` when you need an encoded
PNG or JPEG.

# `data!`

```elixir
@spec data!(t()) :: raw_data()
```

Same as `data/1` but returns the raw data directly, raising on error.

# `save`

```elixir
@spec save(t(), Path.t(), image_opts()) :: :ok | {:error, PdfElixide.Error.t()}
```

Writes the image to a file at the given path.

The output format is taken from `opts[:format]` when given, otherwise inferred
from the path extension (`.png` → PNG, `.jpg`/`.jpeg` → JPEG). An unknown
extension with no `:format` option raises `ArgumentError`.

The path is handed to the operating system unchanged — see the "File paths"
section of `PdfElixide`.

# `save!`

```elixir
@spec save!(t(), Path.t(), image_opts()) :: :ok
```

Same as `save/3` but raises on error.

The path is handed to the operating system unchanged — see the "File paths"
section of `PdfElixide`.

# `to_binary`

```elixir
@spec to_binary(t(), image_opts()) :: {:ok, binary()} | {:error, PdfElixide.Error.t()}
```

Encodes the image to a binary in the requested format.

`opts[:format]` is `:png` (the default) or `:jpeg`. For a `:jpeg` source image
the original bytes are returned untouched (zero loss), except CMYK JPEGs which
are re-encoded to RGB.

That pass-through copies the stored blob once, straight into the returned
binary; the PNG and re-encoding paths hold the encoded image and the binary at
the same time, so they peak at roughly twice the output size.

Returns the bytes rather than writing them, so no path is involved; use
`save/3` to write a file.

# `to_binary!`

```elixir
@spec to_binary!(t(), image_opts()) :: binary()
```

Same as `to_binary/2` but returns the binary directly, raising on error.

Returns the bytes rather than writing them, so no path is involved; use
`save!/3` to write a file.

---

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