Add images.Opacity filter

Fixes #11471
This commit is contained in:
Bjørn Erik Pedersen 2023-09-21 22:32:32 +02:00
parent 11fcda971c
commit f9b3c0f486
8 changed files with 76 additions and 14 deletions

View file

@ -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`). 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 ## Text
Using the `Text` filter, you can add text to an image. Using the `Text` filter, you can add text to an image.

View file

@ -63,7 +63,7 @@ var eq = qt.CmpEquals(
} }
return p1.Name() == p2.Name() && p1.Size() == p2.Size() && p1.IsDir() == p2.IsDir() 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 { cmp.Comparer(func(m1, m2 media.Type) bool {
return m1.Type == m2.Type return m1.Type == m2.Type
}), }),
@ -162,7 +162,6 @@ func TestImageTransformBasic(t *testing.T) {
croppedAgain, err := image.Crop("300x300 topRight") croppedAgain, err := image.Crop("300x300 topRight")
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
c.Assert(cropped, qt.Equals, croppedAgain) c.Assert(cropped, qt.Equals, croppedAgain)
} }
func TestImageTransformFormat(t *testing.T) { func TestImageTransformFormat(t *testing.T) {
@ -267,7 +266,6 @@ func TestImageBugs(t *testing.T) {
c.Assert(resized, qt.Not(qt.IsNil)) c.Assert(resized, qt.Not(qt.IsNil))
c.Assert(resized.Width(), qt.Equals, 100) c.Assert(resized.Width(), qt.Equals, 100)
c.Assert(resized.RelPermalink(), qt.Equals, "/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c876768085288f41211f768147ba2647.jpg") c.Assert(resized.RelPermalink(), qt.Equals, "/a/_hu59e56ffff1bc1d8d122b1403d34e039f_90587_c876768085288f41211f768147ba2647.jpg")
}) })
// Issue #6137 // Issue #6137
@ -278,7 +276,6 @@ func TestImageBugs(t *testing.T) {
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
c.Assert(resized, qt.Not(qt.IsNil)) c.Assert(resized, qt.Not(qt.IsNil))
c.Assert(resized.Width(), qt.Equals, 200) c.Assert(resized.Width(), qt.Equals, 200)
}) })
// Issue #7955 // Issue #7955
@ -307,9 +304,7 @@ func TestImageBugs(t *testing.T) {
c.Assert(resized.Width(), qt.Equals, test.targetWH) c.Assert(resized.Width(), qt.Equals, test.targetWH)
c.Assert(resized.Height(), 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") dir2 := filepath.FromSlash("testdata/golden_webp")
assetGoldenDirs(c, dir1, dir2) assetGoldenDirs(c, dir1, dir2)
} }
func TestImageOperationsGolden(t *testing.T) { func TestImageOperationsGolden(t *testing.T) {
@ -621,7 +615,7 @@ func TestImageOperationsGolden(t *testing.T) {
c.Parallel() c.Parallel()
// Note, if you're enabling this on a MacOS M1 (ARM) you need to run the test with GOARCH=amd64. // 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. // The above will print out a folder.
// Replace testdata/golden with resources/_gen/images in that folder. // Replace testdata/golden with resources/_gen/images in that folder.
devMode := false devMode := false
@ -644,6 +638,10 @@ func TestImageOperationsGolden(t *testing.T) {
gopher, err = gopher.Resize("30x") gopher, err = gopher.Resize("30x")
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
f := &images.Filters{}
sunset := fetchImageForSpec(spec, c, "sunset.jpg")
// Test PNGs with alpha channel. // Test PNGs with alpha channel.
for _, img := range []string{"gopher-hero8.png", "gradient-circle.png"} { for _, img := range []string{"gopher-hero8.png", "gradient-circle.png"} {
orig := fetchImageForSpec(spec, c, img) orig := fetchImageForSpec(spec, c, img)
@ -653,7 +651,15 @@ func TestImageOperationsGolden(t *testing.T) {
rel := resized.RelPermalink() rel := resized.RelPermalink()
c.Assert(rel, qt.Not(qt.Equals), "") 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). // A simple Gif file (no animation).
@ -699,8 +705,6 @@ func TestImageOperationsGolden(t *testing.T) {
c.Assert(rel, qt.Not(qt.Equals), "") c.Assert(rel, qt.Not(qt.Equals), "")
} }
f := &images.Filters{}
filters := []gift.Filter{ filters := []gift.Filter{
f.Grayscale(), f.Grayscale(),
f.GaussianBlur(6), f.GaussianBlur(6),
@ -746,11 +750,9 @@ func TestImageOperationsGolden(t *testing.T) {
dir2 := filepath.FromSlash("testdata/golden") dir2 := filepath.FromSlash("testdata/golden")
assetGoldenDirs(c, dir1, dir2) assetGoldenDirs(c, dir1, dir2)
} }
func assetGoldenDirs(c *qt.C, dir1, dir2 string) { func assetGoldenDirs(c *qt.C, dir1, dir2 string) {
// The two dirs above should now be the same. // The two dirs above should now be the same.
dirinfos1, err := os.ReadDir(dir1) dirinfos1, err := os.ReadDir(dir1)
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)

View file

@ -28,8 +28,7 @@ import (
// Increment for re-generation of images using these filters. // Increment for re-generation of images using these filters.
const filterAPIVersion = 0 const filterAPIVersion = 0
type Filters struct { type Filters struct{}
}
// Overlay creates a filter that overlays src at position x y. // Overlay creates a filter that overlays src at position x y.
func (*Filters) Overlay(src ImageSource, x, y any) gift.Filter { 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. // Text creates a filter that draws text with the given options.
func (*Filters) Text(text string, options ...any) gift.Filter { func (*Filters) Text(text string, options ...any) gift.Filter {
tf := textFilter{ tf := textFilter{

View file

@ -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())
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB