Repository: gohugoio/hugo (language: go)

Issue:
Add images.Opacity filter



Discussion / hints:

Fix patch:
diff --git a/docs/content/en/functions/images/index.md b/docs/content/en/functions/images/index.md
index e42f4cdd8ab..fcd0796e3fe 100644
--- a/docs/content/en/functions/images/index.md
+++ b/docs/content/en/functions/images/index.md
@@ -34,6 +34,19 @@ A shorter version of the above, if you only need to apply the filter once:
 
 The above will overlay `$logo` in the upper left corner of `$img` (at position `x=50, y=50`).
 
+## Opacity
+
+{{% funcsig %}}
+images.Opacity SRC OPACITY
+{{% /funcsig %}}
+
+Opacity creates a filter that changes the opacity of an image.
+The OPACITY parameter must be in range (0, 1).
+
+```go-html-template
+{{ $img := $img.Filter (images.Opacity 0.5 )}}
+```
+
 ## Text
 
 Using the `Text` filter, you can add text to an image.
diff --git a/resources/images/filters.go b/resources/images/filters.go
index 90667af7c90..63e90d2ada9 100644
--- a/resources/images/filters.go
+++ b/resources/images/filters.go
@@ -28,8 +28,7 @@ import (
 // Increment for re-generation of images using these filters.
 const filterAPIVersion = 0
 
-type Filters struct {
-}
+type Filters struct{}
 
 // Overlay creates a filter that overlays src at position x y.
 func (*Filters) Overlay(src ImageSource, x, y any) gift.Filter {
@@ -39,6 +38,15 @@ func (*Filters) Overlay(src ImageSource, x, y any) gift.Filter {
 	}
 }
 
+// Opacity creates a filter that changes the opacity of an image.
+// The opacity parameter must be in range (0, 1).
+func (*Filters) Opacity(opacity any) gift.Filter {
+	return filter{
+		Options: newFilterOpts(opacity),
+		Filter:  opacityFilter{opacity: cast.ToFloat32(opacity)},
+	}
+}
+
 // Text creates a filter that draws text with the given options.
 func (*Filters) Text(text string, options ...any) gift.Filter {
 	tf := textFilter{
diff --git a/resources/images/opacity.go b/resources/images/opacity.go
new file mode 100644
index 00000000000..4b60e30a4c3
--- /dev/null
+++ b/resources/images/opacity.go
@@ -0,0 +1,39 @@
+// Copyright 2023 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package images
+
+import (
+	"image"
+	"image/color"
+	"image/draw"
+
+	"github.com/disintegration/gift"
+)
+
+var _ gift.Filter = (*opacityFilter)(nil)
+
+type opacityFilter struct {
+	opacity float32
+}
+
+func (f opacityFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
+	// 0 is fully transparent and 255 is opaque.
+	alpha := uint8(f.opacity * 255)
+	mask := image.NewUniform(color.Alpha{alpha})
+	draw.DrawMask(dst, dst.Bounds(), src, image.Point{}, mask, image.Point{}, draw.Over)
+}
+
+func (f opacityFilter) Bounds(srcBounds image.Rectangle) image.Rectangle {
+	return image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
+}


Test patch:
diff --git a/resources/image_test.go b/resources/image_test.go
index de32f06282c..751ef3f5d18 100644
--- a/resources/image_test.go
+++ b/resources/image_test.go
@@ -63,7 +63,7 @@ var eq = qt.CmpEquals(
 		}
 		return p1.Name() == p2.Name() && p1.Size() == p2.Size() && p1.IsDir() == p2.IsDir()
 	}),
-	//cmp.Comparer(func(p1, p2 *genericResource) bool { return p1 == p2 }),
+	// cmp.Comparer(func(p1, p2 *genericResource) bool { return p1 == p2 }),
 	cmp.Comparer(func(m1, m2 media.Type) bool {
 		return m1.Type == m2.Type
 	}),
@@ -162,7 +162,6 @@ func TestImageTransformBasic(t *testing.T) {
 	croppedAgain, err := image.Crop("300x300 topRight")
 	c.Assert(err, qt.IsNil)
 	c.Assert(cropped, qt.Equals, croppedAgain)
-
 }
 
 func TestImageTransformFormat(t *testing.T) {
@@ -267,7 +266,6 @@ func TestImageBugs(t *testing.T) {
 		c.Assert(resized, qt.Not(qt.IsNil))
 		c.Assert(resized.Width(), qt.Equals, 100)
 		c.Assert(resized.RelPermalink(), qt.Equals, "/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c876768085288f41211f768147ba2647.jpg")
-
 	})
 
 	// Issue #6137
@@ -278,7 +276,6 @@ func TestImageBugs(t *testing.T) {
 		c.Assert(err, qt.IsNil)
 		c.Assert(resized, qt.Not(qt.IsNil))
 		c.Assert(resized.Width(), qt.Equals, 200)
-
 	})
 
 	// Issue #7955
@@ -307,9 +304,7 @@ func TestImageBugs(t *testing.T) {
 				c.Assert(resized.Width(), qt.Equals, test.targetWH)
 				c.Assert(resized.Height(), qt.Equals, test.targetWH)
 			})
-
 		}
-
 	})
 }
 
@@ -613,7 +608,6 @@ func TestImageOperationsGoldenWebp(t *testing.T) {
 	dir2 := filepath.FromSlash("testdata/golden_webp")
 
 	assetGoldenDirs(c, dir1, dir2)
-
 }
 
 func TestImageOperationsGolden(t *testing.T) {
@@ -621,7 +615,7 @@ func TestImageOperationsGolden(t *testing.T) {
 	c.Parallel()
 
 	// Note, if you're enabling this on a MacOS M1 (ARM) you need to run the test with GOARCH=amd64.
-	// GOARCH=amd64 go test -timeout 30s -run "^TestImageOperationsGolden$" ./resources -v
+	// GOARCH=amd64 go test -count 1 -timeout 30s -run "^TestImageOperationsGolden$" ./resources -v
 	// The above will print out a folder.
 	// Replace testdata/golden with resources/_gen/images in that folder.
 	devMode := false
@@ -644,6 +638,10 @@ func TestImageOperationsGolden(t *testing.T) {
 	gopher, err = gopher.Resize("30x")
 	c.Assert(err, qt.IsNil)
 
+	f := &images.Filters{}
+
+	sunset := fetchImageForSpec(spec, c, "sunset.jpg")
+
 	// Test PNGs with alpha channel.
 	for _, img := range []string{"gopher-hero8.png", "gradient-circle.png"} {
 		orig := fetchImageForSpec(spec, c, img)
@@ -653,7 +651,15 @@ func TestImageOperationsGolden(t *testing.T) {
 			rel := resized.RelPermalink()
 
 			c.Assert(rel, qt.Not(qt.Equals), "")
+
 		}
+
+		// Check the Opacity filter.
+		opacity30, err := orig.Filter(f.Opacity(30))
+		c.Assert(err, qt.IsNil)
+		overlay, err := sunset.Filter(f.Overlay(opacity30.(images.ImageSource), 20, 20))
+		rel := overlay.RelPermalink()
+		c.Assert(rel, qt.Not(qt.Equals), "")
 	}
 
 	// A simple Gif file (no animation).
@@ -699,8 +705,6 @@ func TestImageOperationsGolden(t *testing.T) {
 			c.Assert(rel, qt.Not(qt.Equals), "")
 		}
 
-		f := &images.Filters{}
-
 		filters := []gift.Filter{
 			f.Grayscale(),
 			f.GaussianBlur(6),
@@ -746,11 +750,9 @@ func TestImageOperationsGolden(t *testing.T) {
 	dir2 := filepath.FromSlash("testdata/golden")
 
 	assetGoldenDirs(c, dir1, dir2)
-
 }
 
 func assetGoldenDirs(c *qt.C, dir1, dir2 string) {
-
 	// The two dirs above should now be the same.
 	dirinfos1, err := os.ReadDir(dir1)
 	c.Assert(err, qt.IsNil)
diff --git a/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_filter_10590793696706257122.png b/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_filter_10590793696706257122.png
new file mode 100644
index 00000000000..935c2391b22
Binary files /dev/null and b/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_filter_10590793696706257122.png differ
diff --git a/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_filter_10590793696706257122.png b/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_filter_10590793696706257122.png
new file mode 100644
index 00000000000..fe39286bd1a
Binary files /dev/null and b/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_filter_10590793696706257122.png differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_filter_16531506165985954191.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_filter_16531506165985954191.jpg
new file mode 100644
index 00000000000..bc43f4ef9de
Binary files /dev/null and b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_filter_16531506165985954191.jpg differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_filter_18171945436439920693.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_filter_18171945436439920693.jpg
new file mode 100644
index 00000000000..80f06bf666e
Binary files /dev/null and b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_filter_18171945436439920693.jpg differ