@@ -6588,36 +6588,12 @@ defmodule Image do
65886588
65896589 See also `Image.Pixel.to_pixel/2`.
65906590
6591- ## Partially-transparent backgrounds
6592-
6593- A fully opaque or fully transparent `:background` is reproduced
6594- exactly. A *partially* transparent `:background` (an alpha band
6595- value somewhere between fully opaque and fully transparent) is
6596- not. To interpolate correctly `libvips` works in premultiplied-alpha
6597- space, and it injects the fill color directly into that space and
6598- then unpremultiplies the whole result on output. The fill therefore
6599- sees only that one unpremultiply step, which scales its color bands
6600- by `max / alpha`. For example, on an 8-bit image (where `max` is
6601- `255`) a declared `[10, 20, 30, 40]` is filled as `[63, 127, 191,
6602- 40]`: the alpha is preserved, but each color band is multiplied by
6603- `255 / 40`.
6604-
6605- If you need an exact partially-transparent fill, don't pass it as
6606- `:background`. Instead rotate over a transparent background and
6607- composite the result onto a canvas of the desired color:
6608-
6609- iex> image = Image.open!("./test/support/images/dice_transparent.png")
6610- iex> {:ok, rotated} = Image.rotate(image, 45, background: [0, 0, 0, 0])
6611- iex> canvas = Image.new!(rotated, color: [10, 20, 30, 40])
6612- iex> {:ok, _filled} = Image.compose(canvas, rotated)
6613-
6614- The color is applied by the composite rather than passed through the
6615- rotate, so it is reproduced exactly. Note that this backs the
6616- *entire* image: it fills every transparent pixel, not just the canvas
6617- exposed by the rotate. When the source content is fully opaque the
6618- two are the same region; for a source that carries its own
6619- transparency (such as the dice image above) the composite also fills
6620- those areas, which `:background` would leave untouched.
6591+ ## Transparent backgrounds
6592+
6593+ A partially transparent `:background` is reproduced exactly. The one
6594+ exception is a *fully* transparent fill (`alpha: 0`) with non-zero
6595+ color bands: color cannot be recovered from under zero alpha, so it
6596+ is rendered as transparent black rather than the declared color.
66216597
66226598 ## Discrete rotation
66236599
@@ -6640,13 +6616,20 @@ defmodule Image do
66406616
66416617 * `{:error, reason}`
66426618
6643- ### Example
6619+ ### Examples
66446620
66456621 iex> image = Image.new!(20, 10, color: :blue)
66466622 iex> {:ok, rotated} = Image.rotate(image, 90)
66476623 iex> Image.shape(rotated)
66486624 {10, 20, 3}
66496625
6626+ A partially transparent `:background` fills the exposed canvas exactly:
6627+
6628+ iex> image = Image.new!(20, 20, color: [255, 0, 0, 255])
6629+ iex> {:ok, rotated} = Image.rotate(image, 45, background: [10, 20, 30, 40])
6630+ iex> Image.get_pixel!(rotated, 1, 1)
6631+ [10, 20, 30, 40]
6632+
66506633 """
66516634 @ doc subject: "Operation"
66526635
@@ -6661,14 +6644,26 @@ defmodule Image do
66616644 with { :ok , options } <- Options.Rotate . validate_options ( image , options ) do
66626645 rot_angle = rot_angle ( angle , options )
66636646
6664- if rot_angle do
6665- Operation . rot ( image , rot_angle )
6666- else
6667- Operation . rotate ( image , angle , options )
6647+ cond do
6648+ rot_angle ->
6649+ Operation . rot ( image , rot_angle )
6650+
6651+ has_alpha? ( image ) ->
6652+ # `vips_rotate` doesn't expose the `:premultiplied` option, so alpha images
6653+ # run the equivalent affine through the premultiplied pipeline instead
6654+ premultiplied_affine ( image , rotation_matrix ( angle ) , options )
6655+
6656+ true ->
6657+ Operation . rotate ( image , angle , options )
66686658 end
66696659 end
66706660 end
66716661
6662+ defp rotation_matrix ( angle ) do
6663+ radians = angle * :math . pi ( ) / 180.0
6664+ [ :math . cos ( radians ) , - :math . sin ( radians ) , :math . sin ( radians ) , :math . cos ( radians ) ]
6665+ end
6666+
66726667 defp rot_angle ( angle , options ) do
66736668 if Options.Rotate . no_displacement? ( options ) do
66746669 to_rot_angle ( angle )
@@ -11911,41 +11906,12 @@ defmodule Image do
1191111906 Image.height(image)]` keeps the output the same size as the
1191211907 input, anchored at the origin.
1191311908
11914- ## Partially-transparent backgrounds
11915-
11916- A fully opaque or fully transparent `:background` is reproduced
11917- exactly. A *partially* transparent `:background` (an alpha band
11918- value somehwere between fully opaque and fully transparent) is
11919- not. To interpolate correctly `libvips` works in premultiplied-alpha
11920- space, and it injects the fill color directly into that space and
11921- then unpremultiplies the whole result on output. The fill therefore
11922- sees only that one unpremultiply step, which scales its color bands
11923- by `max / alpha`. For example, on an 8-bit image (where `max` is
11924- `255`) a declared `[10, 20, 30, 40]` is filled as `[63, 127, 191,
11925- 40]`: the alpha is preserved, but each color band is multiplied by
11926- `255 / 40`.
11927-
11928- If you need an exact partially-transparent fill, don't pass it as
11929- `:background`. Instead transform over a transparent background and
11930- composite the result onto a canvas of the desired color. Here
11931- `jose.png` is opaque (an alpha band is added so the exposed canvas
11932- can be transparent), so the only transparency is the canvas the
11933- transform exposes:
11934-
11935- iex> image = Image.add_alpha!(Image.open!("./test/support/images/jose.png"), 255)
11936- iex> angle = :math.pi() / 4
11937- iex> matrix = [:math.cos(angle), -:math.sin(angle), :math.sin(angle), :math.cos(angle)]
11938- iex> {:ok, rotated} = Image.affine(image, matrix, background: [0, 0, 0, 0])
11939- iex> canvas = Image.new!(rotated, color: [10, 20, 30, 40])
11940- iex> {:ok, _filled} = Image.compose(canvas, rotated)
11941-
11942- The color is applied by the composite rather than passed through the
11943- transform, so it is reproduced exactly. Note that the composite backs
11944- the *entire* image: it fills every transparent pixel, not just the
11945- canvas exposed by the transform. Because the content here is opaque
11946- the two coincide; for a source that carries its own transparency the
11947- composite also fills those areas, which `:background` would leave
11948- untouched.
11909+ ## Transparent backgrounds
11910+
11911+ A partially transparent `:background` is reproduced exactly. The one
11912+ exception is a *fully* transparent fill (`alpha: 0`) with non-zero
11913+ color bands: color cannot be recovered from under zero alpha, so it
11914+ is rendered as transparent black rather than the declared color.
1194911915
1195011916 ### Notes
1195111917
@@ -11978,6 +11944,15 @@ defmodule Image do
1197811944 iex> image = Image.open!("./test/support/images/jose.png")
1197911945 iex> {:ok, _stretched} = Image.affine(image, [2, 0, 0, 1])
1198011946
11947+ A partially transparent `:background` fills the exposed canvas exactly:
11948+
11949+ iex> image = Image.new!(20, 20, color: [255, 0, 0, 255])
11950+ iex> angle = :math.pi() / 4
11951+ iex> matrix = [:math.cos(angle), -:math.sin(angle), :math.sin(angle), :math.cos(angle)]
11952+ iex> {:ok, transformed} = Image.affine(image, matrix, background: [10, 20, 30, 40])
11953+ iex> Image.get_pixel!(transformed, 1, 1)
11954+ [10, 20, 30, 40]
11955+
1198111956 """
1198211957 @ doc subject: "Distortion"
1198311958
@@ -11993,7 +11968,7 @@ defmodule Image do
1199311968 def affine ( % Vimage { } = image , [ a , b , c , d ] , options )
1199411969 when is_number ( a ) and is_number ( b ) and is_number ( c ) and is_number ( d ) do
1199511970 with { :ok , options } <- Options.Affine . validate_options ( image , options ) do
11996- Operation . affine ( image , [ a , b , c , d ] , options )
11971+ premultiplied_affine ( image , [ a , b , c , d ] , options )
1199711972 end
1199811973 end
1199911974
@@ -12007,6 +11982,44 @@ defmodule Image do
1200711982 } }
1200811983 end
1200911984
11985+ # `libvips` resamples alpha images in premultiplied-alpha space and
11986+ # injects the `:background` fill raw into that space, so the fill's
11987+ # color bands come back scaled by `max_alpha / alpha`. Premultiplying
11988+ # the image and the background ourselves (with `premultiplied: true`)
11989+ # makes the fill round-trip exactly.
11990+ defp premultiplied_affine ( % Vimage { } = image , matrix , options ) do
11991+ if has_alpha? ( image ) do
11992+ band_format = Vix.Vips.Image . format ( image )
11993+
11994+ options =
11995+ options
11996+ |> Keyword . replace_lazy ( :background , & premultiply_pixel ( image , & 1 ) )
11997+ |> Keyword . put ( :premultiplied , true )
11998+
11999+ with { :ok , premultiplied } <- Operation . premultiply ( image ) ,
12000+ { :ok , transformed } <- Operation . affine ( premultiplied , matrix , options ) ,
12001+ { :ok , unpremultiplied } <- Operation . unpremultiply ( transformed ) do
12002+ Operation . cast ( unpremultiplied , band_format )
12003+ end
12004+ else
12005+ Operation . affine ( image , matrix , options )
12006+ end
12007+ end
12008+
12009+ defp premultiply_pixel ( image , pixel ) do
12010+ # The background premultiplication must use the same alpha scale as the
12011+ # operations above. `Image.Pixel` encodes that scale as the opaque alpha
12012+ # value, so read it from there.
12013+ max_alpha =
12014+ case Pixel . to_pixel ( image , :black , alpha: :opaque ) do
12015+ { :ok , pixel } -> List . last ( pixel )
12016+ { :error , _reason } -> 255
12017+ end
12018+
12019+ { color_bands , [ alpha ] } = Enum . split ( pixel , - 1 )
12020+ Enum . map ( color_bands , & ( & 1 * alpha / max_alpha ) ) ++ [ alpha ]
12021+ end
12022+
1201012023 @ doc """
1201112024 Applies an affine transformation to an image or raises
1201212025 an exception.
0 commit comments