diff --git a/lib/image.ex b/lib/image.ex index b510d35..7639816 100644 --- a/lib/image.ex +++ b/lib/image.ex @@ -10759,11 +10759,15 @@ defmodule Image do * `:background` defines the color of any generated background pixels. This can be specified as a single integer which will be applied to all bands, or a list of integers representing - the color for each band. The color can also be supplied as a - CSS color name as a string or atom. For example: `:misty_rose`. - It can also be supplied as a hex string of the form `#rrggbb`. - Can also be set to `:average` in which case the background will - be the average color of the base image. The default is `:black`. + the color for each band (including an optional alpha band). The + color can also be supplied as a CSS color name as a string or + atom. For example: `:misty_rose`. It can also be supplied as a + hex string of the form `#rrggbb`. Can also be set to `:average` + in which case the background will be the average color of the + base image. + + When omitted, `libvips` uses its native fill: transparent for + images with an alpha band, black otherwise. See also `Image.Pixel.to_pixel/2`. @@ -10795,6 +10799,42 @@ defmodule Image do Image.height(image)]` keeps the output the same size as the input, anchored at the origin. + ## Partially-transparent backgrounds + + A fully opaque or fully transparent `:background` is reproduced + exactly. A *partially* transparent `:background` (an alpha band + value somehwere between fully opaque and fully transparent) is + not. To interpolate correctly `libvips` works in premultiplied-alpha + space, and it injects the fill color directly into that space and + then unpremultiplies the whole result on output. The fill therefore + sees only that one unpremultiply step, which scales its color bands + by `max / alpha`. For example, on an 8-bit image (where `max` is + `255`) a declared `[10, 20, 30, 40]` is filled as `[63, 127, 191, + 40]`: the alpha is preserved, but each color band is multiplied by + `255 / 40`. + + If you need an exact partially-transparent fill, don't pass it as + `:background`. Instead transform over a transparent background and + composite the result onto a canvas of the desired color. Here + `jose.png` is opaque (an alpha band is added so the exposed canvas + can be transparent), so the only transparency is the canvas the + transform exposes: + + iex> image = Image.add_alpha!(Image.open!("./test/support/images/jose.png"), 255) + iex> angle = :math.pi() / 4 + iex> matrix = [:math.cos(angle), -:math.sin(angle), :math.sin(angle), :math.cos(angle)] + iex> {:ok, rotated} = Image.affine(image, matrix, background: [0, 0, 0, 0]) + iex> canvas = Image.new!(rotated, color: [10, 20, 30, 40]) + iex> {:ok, _filled} = Image.compose(canvas, rotated) + + The color is applied by the composite rather than passed through the + transform, so it is reproduced exactly. Note that the composite backs + the *entire* image: it fills every transparent pixel, not just the + canvas exposed by the transform. Because the content here is opaque + the two coincide; for a source that carries its own transparency the + composite also fills those areas, which `:background` would leave + untouched. + ### Notes The output canvas is sized to the bounding box of the diff --git a/lib/image/options/affine.ex b/lib/image/options/affine.ex index 23021eb..c62e0ea 100644 --- a/lib/image/options/affine.ex +++ b/lib/image/options/affine.ex @@ -163,6 +163,6 @@ defmodule Image.Options.Affine do # `:background` blends the fringe toward the fill color, whereas `:black` # would leave a dark fringe on a non-black background. defp default_options do - [extend_mode: :background, background: :black, interpolate: :bilinear] + [extend_mode: :background, interpolate: :bilinear] end end diff --git a/test/affine_test.exs b/test/affine_test.exs index 73dd1d5..20d8f7f 100644 --- a/test/affine_test.exs +++ b/test/affine_test.exs @@ -104,13 +104,25 @@ defmodule Image.Affine.Test do end end - test "applies the documented default :extend_mode, :background and :interpolate" do + test "applies the documented default :extend_mode and :interpolate and leaves :background to libvips" do image = white_dot(20, 20, 2, 3) {:ok, options} = Image.Options.Affine.validate_options(image, []) assert Keyword.get(options, :extend) == :VIPS_EXTEND_BACKGROUND - assert Keyword.get(options, :background) == [0, 0, 0] assert %Vix.Vips.Interpolate{} = Keyword.get(options, :interpolate) + # No default :background is injected so libvips keeps its native + # fill (transparent for alpha images, black otherwise). + refute Keyword.has_key?(options, :background) + end + + test "preserves libvips' transparent fill for alpha images when :background is unset" do + # A translation vacates a strip of canvas. With no :background the + # exposed pixels must keep libvips' native transparent fill rather + # than being forced to opaque black. + image = Image.new!(20, 20, color: [0, 0, 0, 255]) + + {:ok, translated} = Image.translate(image, 10, 0) + assert Image.get_pixel!(translated, 2, 10) == [0, 0, 0, 0] end end