diff --git a/docs/content/en/functions/images/index.md b/docs/content/en/functions/images/index.md index e42f4cdd8..fcd0796e3 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/image_test.go b/resources/image_test.go index de32f0628..751ef3f5d 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/images/filters.go b/resources/images/filters.go index 90667af7c..63e90d2ad 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 000000000..4b60e30a4 --- /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()) +} 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 000000000..935c2391b 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 000000000..fe39286bd 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 000000000..bc43f4ef9 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 000000000..80f06bf66 Binary files /dev/null and b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_filter_18171945436439920693.jpg differ