images: Add images.Overlay filter
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 16 Dec 2020 12:52:47 +0000 (13:52 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 17 Dec 2020 08:14:18 +0000 (09:14 +0100)
This allows for constructs ala:

```
{{ $overlay := $img.Filter (images.Overlay $logo 50 50 )}}
```
Or:

```
{{ $logoFilter := (images.Overlay $logo 50 50 ) }}
{{ $overlay := $img | images.Filter $logoFilter }}
```

Which will overlay the logo in the top left corner (x=50, y=50) of `$img`.

Fixes #8057
Fixes #4595
Updates #6731

12 files changed:
docs/content/en/functions/images/index.md
resources/image.go
resources/image_test.go
resources/images/filters.go
resources/images/image.go
resources/images/overlay.go [new file with mode: 0644]
resources/resource/resourcetypes.go
resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_3ad578dd67cd055b4382e4062918d0a2.png [new file with mode: 0644]
resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_b4afd8d32218a87ed1f7e351368501c3.png [new file with mode: 0644]
resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_30x0_resize_box_2.png [new file with mode: 0644]
resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_da536a2a5436e387d0d482675e08ad48.jpg [new file with mode: 0644]
resources/transform.go

index e83d41154b9c60f75f0e1b6853ed671a48297efc..e61a10916b10bdd204f37858c82b4850d4e7af7f 100644 (file)
@@ -17,6 +17,30 @@ toc: true
 
 See [images.Filter](#filter) for how to apply these filters to an image.
 
+### Overlay
+
+{{< new-in "0.80.0" >}}
+
+{{% funcsig %}}
+images.Overlay SRC X Y
+{{% /funcsig %}}
+
+Overlay creates a filter that overlays the source image at position x y, e.g:
+
+
+```go-html-template
+{{ $logoFilter := (images.Overlay $logo 50 50 ) }}
+{{ $img := $img | images.Filter $logoFilter }}
+```
+
+A shorter version of the above, if you only need to apply the filter once:
+
+```go-html-template
+{{ $img := $img.Filter (images.Overlay $logo 50 50 )}}
+```
+
+The above will overlay `$logo` in the upper left corner of `$img` (at position `x=50, y=50`).
+
 ### Brightness
 
 {{% funcsig %}}
index ed303613f797af708baee7ea7d366ebcf21d50e3..0396c2208e8768a68636013787e4f48252c71f2e 100644 (file)
@@ -242,7 +242,7 @@ func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src im
                errOp := conf.Action
                errPath := i.getSourceFilename()
 
-               src, err := i.decodeSource()
+               src, err := i.DecodeImage()
                if err != nil {
                        return nil, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
                }
@@ -324,7 +324,9 @@ func (i *imageResource) decodeImageConfig(action, spec string) (images.ImageConf
        return conf, nil
 }
 
-func (i *imageResource) decodeSource() (image.Image, error) {
+// DecodeImage decodes the image source into an Image.
+// This an internal method and may change.
+func (i *imageResource) DecodeImage() (image.Image, error) {
        f, err := i.ReadSeekCloser()
        if err != nil {
                return nil, _errors.Wrap(err, "failed to open image for decode")
index 9c186196002621978b0c3c6a9590ffb9218c42df..8c69591cf32caf9ff808f1e8b214f03bae3be55a 100644 (file)
@@ -533,6 +533,11 @@ func TestImageOperationsGolden(t *testing.T) {
                fmt.Println(workDir)
        }
 
+       gopher := fetchImageForSpec(spec, c, "gopher-hero8.png")
+       var err error
+       gopher, err = gopher.Resize("30x")
+       c.Assert(err, qt.IsNil)
+
        // Test PNGs with alpha channel.
        for _, img := range []string{"gopher-hero8.png", "gradient-circle.png"} {
                orig := fetchImageForSpec(spec, c, img)
@@ -589,6 +594,7 @@ func TestImageOperationsGolden(t *testing.T) {
                        f.Invert(),
                        f.Hue(22),
                        f.Contrast(32.5),
+                       f.Overlay(gopher.(images.ImageSource), 20, 30),
                }
 
                resized, err := orig.Fill("400x200 center")
index dd7b5834563dcf2fd9ec5239ab1435495be99b07..74c50363e5d65e443424d7950d59612b54bdb10c 100644 (file)
@@ -25,6 +25,14 @@ const filterAPIVersion = 0
 type Filters struct {
 }
 
+// Overlay creates a filter that overlays src at position x y.
+func (*Filters) Overlay(src ImageSource, x, y interface{}) gift.Filter {
+       return filter{
+               Options: newFilterOpts(src.Key(), x, y),
+               Filter:  overlayFilter{src: src, x: cast.ToInt(x), y: cast.ToInt(y)},
+       }
+}
+
 // Brightness creates a filter that changes the brightness of an image.
 // The percentage parameter must be in range (-100, 100).
 func (*Filters) Brightness(percentage interface{}) gift.Filter {
index 88eed2f7eb7258ef51e22b7ada166174fac67cff..3d28263c0cda7313ed88a79c530ad3ac75696b9f 100644 (file)
@@ -325,3 +325,9 @@ func IsOpaque(img image.Image) bool {
 
        return false
 }
+
+// ImageSource identifies and decodes an image.
+type ImageSource interface {
+       DecodeImage() (image.Image, error)
+       Key() string
+}
diff --git a/resources/images/overlay.go b/resources/images/overlay.go
new file mode 100644 (file)
index 0000000..780e28f
--- /dev/null
@@ -0,0 +1,43 @@
+// Copyright 2020 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 (
+       "fmt"
+       "image"
+       "image/draw"
+
+       "github.com/disintegration/gift"
+)
+
+var _ gift.Filter = (*overlayFilter)(nil)
+
+type overlayFilter struct {
+       src  ImageSource
+       x, y int
+}
+
+func (f overlayFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
+       overlaySrc, err := f.src.DecodeImage()
+       if err != nil {
+               panic(fmt.Sprintf("failed to decode image: %s", err))
+       }
+
+       gift.New().Draw(dst, src)
+       gift.New().DrawAt(dst, overlaySrc, image.Pt(f.x, f.y), gift.OverOperator)
+}
+
+func (f overlayFilter) Bounds(srcBounds image.Rectangle) image.Rectangle {
+       return image.Rect(0, 0, srcBounds.Dx(), srcBounds.Dy())
+}
index f42372fa3960996033382fa305fcc4ff0ea97837..206ce8de8d041a1258c74be22a863973e8eb4c0f 100644 (file)
@@ -14,6 +14,8 @@
 package resource
 
 import (
+       "image"
+
        "github.com/gohugoio/hugo/common/maps"
        "github.com/gohugoio/hugo/langs"
        "github.com/gohugoio/hugo/media"
@@ -59,6 +61,9 @@ type ImageOps interface {
        Resize(spec string) (Image, error)
        Filter(filters ...interface{}) (Image, error)
        Exif() *exif.Exif
+
+       // Internal
+       DecodeImage() (image.Image, error)
 }
 
 type ResourceTypeProvider interface {
diff --git a/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_3ad578dd67cd055b4382e4062918d0a2.png b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_3ad578dd67cd055b4382e4062918d0a2.png
new file mode 100644 (file)
index 0000000..3a267fb
Binary files /dev/null and b/resources/testdata/golden/gohugoio24_huc57dd738f4724f4b341121e66fd85555_267952_3ad578dd67cd055b4382e4062918d0a2.png differ
diff --git a/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_b4afd8d32218a87ed1f7e351368501c3.png b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_b4afd8d32218a87ed1f7e351368501c3.png
new file mode 100644 (file)
index 0000000..86896ef
Binary files /dev/null and b/resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_b4afd8d32218a87ed1f7e351368501c3.png differ
diff --git a/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_30x0_resize_box_2.png b/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_30x0_resize_box_2.png
new file mode 100644 (file)
index 0000000..5b87483
Binary files /dev/null and b/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_30x0_resize_box_2.png differ
diff --git a/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_da536a2a5436e387d0d482675e08ad48.jpg b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_da536a2a5436e387d0d482675e08ad48.jpg
new file mode 100644 (file)
index 0000000..15f2ef1
Binary files /dev/null and b/resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_da536a2a5436e387d0d482675e08ad48.jpg differ
index a9ec8467137cb11b36cd3b4ce9803891e8df1436..9007ead180fb68f966256984d8b333de520f4229 100644 (file)
@@ -16,6 +16,7 @@ package resources
 import (
        "bytes"
        "fmt"
+       "image"
        "io"
        "path"
        "strings"
@@ -264,6 +265,10 @@ func (r *resourceAdapter) Width() int {
        return r.getImageOps().Width()
 }
 
+func (r *resourceAdapter) DecodeImage() (image.Image, error) {
+       return r.getImageOps().DecodeImage()
+}
+
 func (r *resourceAdapter) getImageOps() resource.ImageOps {
        img, ok := r.target.(resource.ImageOps)
        if !ok {