Image.Pixel (image v0.73.0)

Copy Markdown View Source

Bridges the Color library and libvips pixel arguments.

Every libvips operation that takes a color (background, fill, draw, flatten, embed, …) ultimately wants a flat list of numbers in the interpretation, value range, and band layout of the image it is operating on. This module owns that conversion so that callers can pass user-friendly inputs (atoms, hex strings, Color.* structs, numeric lists) without worrying about whether the target image is sRGB, Lab, scRGB, CMYK, or 16-bit.

Opacity, alpha and color

An opacity (opacity/0) is the value a caller supplies, expressed relative to full opacity rather than to any image: :transparent, :opaque, a float in 0.0..1.0, or an integer in 0..255 as the same value in 8-bit notation.

An alpha is what an opacity becomes, the content of an image's alpha band. alpha_for/2 scales an opacity to a given image's band, which runs to 65535 for 16-bit and 1.0 for scRGB, and put_alpha/3 sets it on an existing pixel. Prefer a float when the alpha band is not 8-bit: an integer is a fraction of 255, so it cannot reach every value of a 16-bit or scRGB band.

A color may be :none, :transparent or :opaque on top of everything Color.new/2 accepts. All three resolve to black, the first two fully transparent.

Example

iex> {:ok, image} = Image.new(2, 2, color: :black)
iex> Image.Pixel.to_pixel(image, :red)
{:ok, [255, 0, 0]}

iex> {:ok, image} = Image.new(2, 2, color: :black)
iex> {:ok, lab_image} = Image.to_colorspace(image, :lab)
iex> {:ok, [l, a, b]} = Image.Pixel.to_pixel(lab_image, :red)
iex> {Float.round(l, 2), Float.round(a, 2), Float.round(b, 2)}
{53.24, 80.09, 67.2}

Summary

Types

How opaque something should be, expressed relative to full opacity rather than to any particular image's alpha band.

t()

Anything that to_pixel/3 knows how to turn into a pixel.

Functions

Returns an alpha value scaled to the alpha band of image.

Same as alpha_for/2, but raises on error.

A defguard that loosely matches things that look like a pixel input.

Returns an opacity as a fraction of full opacity.

Same as opacity_fraction/1, but raises on error.

Returns pixel with its alpha component set to opacity, scaled to the alpha band of image.

Same as put_alpha/3, but raises on error.

Returns a resolved pixel without its alpha component, if it has one.

Converts a color to a pixel matching the interpretation and band layout of image.

Same as to_pixel/3, but raises on error.

Resolves a color input to an sRGB pixel [r, g, b] (or [r, g, b, a]) with channels in 0..255, regardless of any image context.

Same as to_srgb/1, but raises on error.

Types

opacity()

@type opacity() :: :transparent | :opaque | 0..255 | float()

How opaque something should be, expressed relative to full opacity rather than to any particular image's alpha band.

A float in 0.0..1.0 is the canonical form, being a fraction of full opacity. An integer in 0..255 is that same value in 8-bit notation, normalized as n / 255. The integer form does not imply that the target's alpha band is 8-bit: 128 means 128/255 on a 16-bit image too, not 128/65535.

1 and 1.0 differ

1 is 8-bit notation for 1/255, which is very nearly transparent. Fully opaque is 1.0 or :opaque.

:transparent is 0.0 and :opaque is 1.0. :none is not an opacity, only a color meaning no color at all.

alpha_for/2 scales an opacity to the alpha band of a given image, which is the range 0..65535 for a 16-bit image and 0.0..1.0 for an scRGB one.

t()

@type t() :: struct() | [number()] | String.t() | atom()

Anything that to_pixel/3 knows how to turn into a pixel.

This includes any input accepted by Color.new/2 (a Color.* struct, a numeric list of length 3..5, a hex string, a CSS named color string or atom), plus :none, :transparent and :opaque.

Functions

alpha_for(image, opacity)

@spec alpha_for(image :: Vix.Vips.Image.t(), opacity :: opacity()) ::
  {:ok, number()} | {:error, Image.Error.t()}

Returns an alpha value scaled to the alpha band of image.

The result is in the range image actually uses: 0..65535 for 16-bit images, 0.0..1.0 for scRGB, and 0..255 for everything else, including Lab and LCH despite their float color bands.

The result is a band value, to be written into a pixel. It is not an opacity and must not be passed back where one is expected, such as Image.add_alpha/2 or the :opacity option of to_pixel/3, which would scale it a second time.

Arguments

Returns

  • {:ok, number} or

  • {:error, reason}.

Examples

iex> image = Image.new!(2, 2, color: :black)
iex> Image.Pixel.alpha_for(image, :opaque)
{:ok, 255}

iex> image = Image.new!(2, 2, color: :black)
iex> Image.Pixel.alpha_for(Image.to_colorspace!(image, :rgb16), 0.5)
{:ok, 32768}

iex> image = Image.new!(2, 2, color: :black)
iex> Image.Pixel.alpha_for(Image.to_colorspace!(image, :scrgb), :opaque)
{:ok, 1.0}

alpha_for!(image, opacity)

@spec alpha_for!(image :: Vix.Vips.Image.t(), opacity :: opacity()) :: number()

Same as alpha_for/2, but raises on error.

Examples

iex> image = Image.new!(2, 2, color: :black)
iex> Image.Pixel.alpha_for!(image, :opaque)
255

is_pixel(value)

(macro)

A defguard that loosely matches things that look like a pixel input.

This is intentionally permissive — it accepts anything that might be a color (struct, numeric list, atom that isn't a boolean, binary). Actual validation happens in to_pixel/3.

Use this in function-head guards where you need to dispatch a color argument away from an image argument (the Image.if_then_else/4 pattern).

opacity_fraction(int)

@spec opacity_fraction(opacity :: opacity()) ::
  {:ok, float()} | {:error, Image.Error.t()}

Returns an opacity as a fraction of full opacity.

The 0.0..1.0 float is the canonical form of an opacity, so this is the identity for a float and n / 255 for an integer. Unlike alpha_for/2, the result belongs to no particular image.

Arguments

Returns

  • {:ok, float} in 0.0..1.0, or

  • {:error, t:Image.Error.t/0}.

Examples

iex> Image.Pixel.opacity_fraction(:opaque)
{:ok, 1.0}

iex> Image.Pixel.opacity_fraction(0.5)
{:ok, 0.5}

iex> Image.Pixel.opacity_fraction(128)
{:ok, 0.5019607843137255}

opacity_fraction!(opacity)

@spec opacity_fraction!(opacity :: opacity()) :: float()

Same as opacity_fraction/1, but raises on error.

Examples

iex> Image.Pixel.opacity_fraction!(:transparent)
0.0

put_alpha(pixel, image, opacity)

@spec put_alpha(
  pixel :: [number()],
  image :: Vix.Vips.Image.t(),
  opacity :: opacity()
) ::
  {:ok, [number()]} | {:error, Image.Error.t()}

Returns pixel with its alpha component set to opacity, scaled to the alpha band of image.

The inverse of strip_alpha/2, and like it a no-op on an image with no alpha band, since there is no component to set. opacity is validated either way.

Arguments

  • pixel is a list of numbers already in image's interpretation.

  • image is any Vix.Vips.Image.t/0. Its interpretation determines the alpha the opacity scales to.

  • opacity is any opacity/0.

Returns

  • {:ok, pixel} with its last component replaced, or with pixel unchanged if image has no alpha band, or

  • {:error, reason}.

Examples

iex> {:ok, image} = Image.new(2, 2, color: [0, 0, 0, 255])
iex> Image.Pixel.put_alpha([255, 0, 0, 255], image, 0.5)
{:ok, [255, 0, 0, 128]}

iex> {:ok, image} = Image.new(2, 2, color: :black)
iex> Image.Pixel.put_alpha([255, 0, 0], image, 0.5)
{:ok, [255, 0, 0]}

put_alpha!(pixel, image, opacity)

@spec put_alpha!(
  pixel :: [number()],
  image :: Vix.Vips.Image.t(),
  opacity :: opacity()
) :: [number()]

Same as put_alpha/3, but raises on error.

Examples

iex> image = Image.new!(2, 2, color: [0, 0, 0, 255])
iex> Image.Pixel.put_alpha!([255, 0, 0, 255], image, 0.5)
[255, 0, 0, 128]

strip_alpha(pixel, image)

@spec strip_alpha(pixel :: [number()], image :: Vix.Vips.Image.t()) :: [number()]

Returns a resolved pixel without its alpha component, if it has one.

Arguments

Returns

  • pixel without its last element if image has an alpha band and pixel spans all of image's bands, or

  • pixel unchanged. A pixel that does not span the image's bands exactly has no identifiable alpha component, so it is left alone.

Notes

  • The band count is the only check, so a pixel resolved against another image of the same band count is truncated just the same.

Examples

iex> {:ok, image} = Image.new(2, 2, color: [0, 0, 0, 255])
iex> {:ok, pixel} = Image.Pixel.to_pixel(image, :red)
iex> Image.Pixel.strip_alpha(pixel, image)
[255, 0, 0]

iex> {:ok, image} = Image.new(2, 2, color: :black)
iex> {:ok, pixel} = Image.Pixel.to_pixel(image, :red)
iex> Image.Pixel.strip_alpha(pixel, image)
[255, 0, 0]

to_pixel(image, color, options \\ [])

@spec to_pixel(
  image :: Vix.Vips.Image.t() | Vix.Vips.MutableImage.t(),
  color :: t(),
  options :: Keyword.t()
) :: {:ok, [number()]} | {:error, Image.Error.t()}

Converts a color to a pixel matching the interpretation and band layout of image.

Arguments

  • image is the target Vix.Vips.Image.t/0. Its interpretation and band count determine the output shape and value range.

  • color is anything Color.new/2 accepts: a Color.* struct, a list of 3/4/5 numbers, a hex string ("#ff0000", "#f80", "#ff000080"), a CSS named color ("rebeccapurple", :misty_rose), or :none, :transparent or :opaque.

  • options is a keyword list — see below.

Options

  • :opacity — if the target image has an alpha band, force this opacity. Accepts any opacity/0. If unset, the input color's own alpha is used, or full opacity if it has none.

  • :intent — passed through to Color.convert/3. One of :relative_colorimetric (default), :absolute_colorimetric, :perceptual, or :saturation.

Returns

  • {:ok, [number(), ...]} — a flat list of numbers in the band order and pixel range that the image's interpretation expects.

  • {:error, reason}.

Notes

  • For 8-bit interpretations (:srgb, :rgb, :cmyk, :hsv, :bw) the output is integers in 0..255.

  • For 16-bit interpretations (:rgb16, :grey16) the output is integers in 0..65535.

  • For :scrgb, :lab and :lch the color bands are floats in the natural range of that space, and :labs uses 16-bit integers. The alpha component is 0.0..1.0 for :scrgb and 0..255 for the other three.

  • :scrgb is linear light: mid grey "#808080" encodes as 0.216, not 0.502.

  • The output band count matches Vix.Vips.Image.bands/1 exactly. Alpha is appended when the image has an alpha band, and stripped when it does not.

  • 1-band :bw and :grey16 images receive a single luminance channel computed from the perceptually-uniform Color.Lab L*, as does any other interpretation that arrives with one band.

  • 1-band :scrgb is the exception: it holds linear light, so it receives relative luminance computed from Color.XYZ Y.

Examples

iex> {:ok, image} = Image.new(2, 2, color: :black)
iex> Image.Pixel.to_pixel(image, :red)
{:ok, [255, 0, 0]}

iex> {:ok, image} = Image.new(2, 2, color: [0, 0, 0, 255])
iex> Image.Pixel.to_pixel(image, :red)
{:ok, [255, 0, 0, 255]}

iex> {:ok, image} = Image.new(2, 2, color: [0, 0, 0, 255])
iex> Image.Pixel.to_pixel(image, :red, opacity: 0.5)
{:ok, [255, 0, 0, 128]}

iex> {:ok, image} = Image.new(2, 2, color: :black)
iex> Image.Pixel.to_pixel(image, "#ff000080")
{:ok, [255, 0, 0]}

to_pixel!(image, color, options \\ [])

@spec to_pixel!(image :: Vix.Vips.Image.t(), color :: t(), options :: Keyword.t()) ::
  [number()]

Same as to_pixel/3, but raises on error.

Examples

iex> image = Image.new!(2, 2, color: :black)
iex> Image.Pixel.to_pixel!(image, :red)
[255, 0, 0]

iex> image = Image.new!(2, 2, color: [0, 0, 0, 255])
iex> Image.Pixel.to_pixel!(image, :red, opacity: 0.5)
[255, 0, 0, 128]

to_srgb(color)

@spec to_srgb(color :: t()) :: {:ok, [0..255]} | {:error, Image.Error.t()}

Resolves a color input to an sRGB pixel [r, g, b] (or [r, g, b, a]) with channels in 0..255, regardless of any image context.

Use this for callers that need sRGB output specifically — for example SVG renderers — rather than the interpretation of an image. For image-aware encoding use to_pixel/3.

Arguments

  • color is anything Color.new/2 accepts, plus :none, :transparent and :opaque.

Returns

  • {:ok, [0..255, 0..255, 0..255]} or {:ok, [0..255, 0..255, 0..255, 0..255]} if the source had an alpha channel.

  • {:error, reason}.

Examples

iex> Image.Pixel.to_srgb(:red)
{:ok, [255, 0, 0]}

iex> Image.Pixel.to_srgb("#ff000080")
{:ok, [255, 0, 0, 128]}

iex> Image.Pixel.to_srgb(%Color.Lab{l: 53.24, a: 80.09, b: 67.20})
{:ok, [255, 0, 0]}

to_srgb!(color)

@spec to_srgb!(color :: t()) :: [0..255]

Same as to_srgb/1, but raises on error.

Examples

iex> Image.Pixel.to_srgb!(:red)
[255, 0, 0]

iex> Image.Pixel.to_srgb!("#ff000080")
[255, 0, 0, 128]