From 7f2aefc924d3d78ae4b4875c29a2ce57ec7a1576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Wed, 29 Jul 2026 23:36:44 +0200 Subject: [PATCH] Add shared Image.Pixel.strip_alpha/2 helper --- CHANGELOG.md | 2 ++ lib/image.ex | 5 ++-- lib/image/options/trim.ex | 10 +------- lib/image/options/write.ex | 10 +------- lib/image/pixel.ex | 47 ++++++++++++++++++++++++++++++++++++++ test/pixel_test.exs | 20 ++++++++++++++++ 6 files changed, 73 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69478aa9..892bf9e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ * Adds `Image.chroma_color!/1` as a companion to `Image.chroma_color/1`. ([#219](https://github.com/elixir-image/image/pull/219)) +* Adds `Image.Pixel.strip_alpha/2` which returns a resolved pixel without its alpha component. It consolidates the truncation that `Image.flatten/2`, `Image.chroma_mask/2`, `Image.Options.Trim` and `Image.Options.Write` each did separately before. ([#222](https://github.com/elixir-image/image/pull/222)) + ### Changed * **Breaking:** `Image.average/1` and `Image.chroma_color/1` now return `{:ok, [number()]} | {:error, Image.Error.t()}` instead of a bare list on success. The previous success type was documented as `Pixel.t()` but was always a list of numbers. ([#219](https://github.com/elixir-image/image/pull/219)) diff --git a/lib/image.ex b/lib/image.ex index c774759e..f2c4e0e7 100644 --- a/lib/image.ex +++ b/lib/image.ex @@ -2390,7 +2390,7 @@ defmodule Image do {color_image, _alpha} = split_alpha(image) with {:ok, color} <- maybe_calculate_color(image, color) do - color = Enum.take(color, bands(color_image)) + color = Pixel.strip_alpha(color, image) color_image |> Math.subtract!(color) @@ -6377,8 +6377,7 @@ defmodule Image do with {:ok, resolved} <- Image.BackgroundColor.resolve(image, background) do # Flatten strips alpha and replaces it with the background, so the # background must be the opaque color part only. - bands = Vix.Vips.Image.bands(image) - 1 - Operation.flatten(image, background: Enum.take(resolved, bands)) + Operation.flatten(image, background: Pixel.strip_alpha(resolved, image)) end end end diff --git a/lib/image/options/trim.ex b/lib/image/options/trim.ex index 1832e68e..da05a70b 100644 --- a/lib/image/options/trim.ex +++ b/lib/image/options/trim.ex @@ -43,7 +43,7 @@ defmodule Image.Options.Trim do {:ok, pixel} -> # libvips find_trim compares against the color bands only, so # any alpha band value is dropped from the resolved pixel. - {:cont, Keyword.put(options, :background, strip_alpha(pixel, image))} + {:cont, Keyword.put(options, :background, Pixel.strip_alpha(pixel, image))} _other -> {:halt, {:error, invalid_option(option)}} @@ -59,14 +59,6 @@ defmodule Image.Options.Trim do {:halt, {:error, invalid_option(option)}} end - defp strip_alpha(pixel, image) do - if Image.has_alpha?(image) and length(pixel) == Image.bands(image) do - Enum.take(pixel, length(pixel) - 1) - else - pixel - end - end - defp invalid_option(option) do %Image.Error{ reason: :invalid_option, diff --git a/lib/image/options/write.ex b/lib/image/options/write.ex index 8ce5599c..3d268cfa 100644 --- a/lib/image/options/write.ex +++ b/lib/image/options/write.ex @@ -314,7 +314,7 @@ defmodule Image.Options.Write do defp validate_option({:background, background}, options, image, _image_type) do case BackgroundColor.resolve(image, background) do {:ok, pixel} -> - {:cont, Keyword.put(options, :background, strip_alpha(pixel, image))} + {:cont, Keyword.put(options, :background, Pixel.strip_alpha(pixel, image))} # The resolve error is already an %Image.Error{} with a more # specific message than invalid_option/1 would produce. @@ -442,14 +442,6 @@ defmodule Image.Options.Write do Enum.reduce(@suffix_values, options, &Keyword.delete(&2, &1)) end - defp strip_alpha(pixel, image) do - if Image.has_alpha?(image) do - Enum.take(pixel, length(pixel) - 1) - else - pixel - end - end - # Range 1..10 defp conform_effort(effort, ".png"), do: effort diff --git a/lib/image/pixel.ex b/lib/image/pixel.ex index 3f4979fe..0852b982 100644 --- a/lib/image/pixel.ex +++ b/lib/image/pixel.ex @@ -287,6 +287,53 @@ defmodule Image.Pixel do end end + @doc """ + Returns a resolved pixel without its alpha component, if it + has one. + + ### Arguments + + * `pixel` is a list of numbers in `image`'s band layout, such as + the output of `to_pixel/3`. + + * `image` is any `t:Vix.Vips.Image.t/0`. + + ### 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] + + """ + @spec strip_alpha(pixel :: [number()], image :: Vimage.t()) :: [number()] + def strip_alpha(pixel, %Vimage{} = image) when is_list(pixel) do + if Vimage.has_alpha?(image) and length(pixel) == Vimage.bands(image) do + Enum.take(pixel, length(pixel) - 1) + else + pixel + end + end + @doc """ Resolves a color input to an sRGB pixel `[r, g, b]` (or `[r, g, b, a]`) with channels in `0..255`, regardless of any diff --git a/test/pixel_test.exs b/test/pixel_test.exs index d3ac1f1d..4bbfcf54 100644 --- a/test/pixel_test.exs +++ b/test/pixel_test.exs @@ -205,6 +205,26 @@ defmodule Image.PixelTest do end end + describe "strip_alpha/2" do + test "drops the last band of a full pixel on an alpha image" do + {:ok, image} = Image.new(2, 2, color: [0, 0, 0, 255]) + + assert [255, 0, 0] == Pixel.strip_alpha([255, 0, 0, 128], image) + end + + test "returns the pixel unchanged on an image without alpha" do + {:ok, image} = Image.new(2, 2, color: [0, 0, 0]) + + assert [255, 0, 0] == Pixel.strip_alpha([255, 0, 0], image) + end + + test "returns a pixel shorter than the image's bands unchanged" do + {:ok, image} = Image.new(2, 2, color: [0, 0, 0, 255]) + + assert [255, 0, 0] == Pixel.strip_alpha([255, 0, 0], image) + end + end + describe "transparency/1" do test "atoms" do assert {:ok, 0} = Pixel.transparency(:none)