Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 45 additions & 5 deletions lib/image.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/image/options/affine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 14 additions & 2 deletions test/affine_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down