# `Color.XYZ`

CIE 1931 XYZ tristimulus color space.

The struct carries the `:illuminant` and `:observer_angle` of the
reference white under which the `{x, y, z}` values are expressed.
`Y` is typically on the `[0, 1]` scale, with `Y = 1.0` at the
reference white.

Chromatic adaptation between illuminants is provided by `adapt/3`,
which applies one of the transforms from `Color.ChromaticAdaptation`.

# `t`

```elixir
@type t() :: %Color.XYZ{
  alpha: number() | nil,
  illuminant: atom() | nil,
  observer_angle: 2 | 10 | nil,
  x: number() | nil,
  y: number() | nil,
  z: number() | nil
}
```

# `adapt`

```elixir
@spec adapt(t(), atom(), keyword()) :: {:ok, t()}
```

Chromatically adapts an `XYZ` color from its current reference white
to a new reference white.

### Arguments

* `xyz` is a `Color.XYZ` struct. Its `:illuminant` and
  `:observer_angle` fields identify the source reference white.

* `dest_illuminant` is the target illuminant atom (for example
  `:D65`).

* `options` is a keyword list.

### Options

* `:observer_angle` is the target observer angle (`2` or `10`).
  Defaults to `2`.

* `:method` is the chromatic adaptation transform. One of
  `:bradford` (default), `:xyz_scaling`, `:von_kries`, `:sharp`,
  `:cmccat2000`, or `:cat02`.

### Returns

* `{:ok, %Color.XYZ{}}` tagged with the new illuminant and observer
  angle.

### Examples

    iex> d50 = %Color.XYZ{x: 0.96422, y: 1.0, z: 0.82521, illuminant: :D50, observer_angle: 2}
    iex> {:ok, d65} = Color.XYZ.adapt(d50, :D65)
    iex> {Float.round(d65.x, 5), Float.round(d65.y, 5), Float.round(d65.z, 5)}
    {0.95047, 1.0, 1.08883}

# `apply_bpc`

```elixir
@spec apply_bpc(t(), number(), number()) :: t()
```

Applies black point compensation (BPC) to an `XYZ` color.

BPC rescales the XYZ such that the source's darkest reproducible
black maps to the destination's darkest reproducible black, so
that shadow detail is preserved across profiles with different
minimum luminances. Without BPC, converting from a printer profile
(whose darkest achievable black might be 3% of white) to a display
profile (which can produce pure black) leaves shadows visibly
lifted.

The rescale is a linear map along the achromatic axis:

    Y_out = (Y_in - k_src) · (1 - k_dst) / (1 - k_src) + k_dst

and `X` / `Z` are rescaled by the same factor so chromaticity is
preserved.

This library does not currently read ICC profiles, so in most
workflows both black points default to `0.0` and `apply_bpc/3`
becomes an identity. It is provided for explicit use in
ICC-aware pipelines and for completeness of the rendering-intent
API (see `Color.convert/3` with `bpc: true`).

### Arguments

* `xyz` is a `Color.XYZ` struct.

* `source_bp` is the source black point as a relative luminance
  (`Y`) in `[0.0, 1.0]`.

* `dest_bp` is the destination black point as a relative luminance.

### Returns

* A new `Color.XYZ` struct with the compensation applied.

### Examples

    iex> xyz = %Color.XYZ{x: 0.5, y: 0.5, z: 0.5, illuminant: :D65}
    iex> Color.XYZ.apply_bpc(xyz, 0.0, 0.0) == xyz
    true

    iex> xyz = %Color.XYZ{x: 0.1, y: 0.1, z: 0.1, illuminant: :D65}
    iex> out = Color.XYZ.apply_bpc(xyz, 0.05, 0.0)
    iex> Float.round(out.y, 6)
    0.052632

# `from_xyz`

Identity conversion from `Color.XYZ` to `Color.XYZ`.

### Arguments

* `xyz` is a `Color.XYZ` struct.

### Returns

* `{:ok, xyz}`.

# `to_xyz`

Identity conversion — returns the struct wrapped in an `:ok` tuple.

### Arguments

* `xyz` is a `Color.XYZ` struct.

### Returns

* `{:ok, xyz}`.

---

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