URI: 
       resources/images: Allow to set background fill colour - hugo - [fork] hugo port for 9front
  HTML git clone git@git.drkhsh.at/hugo.git
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
   DIR README
   DIR LICENSE
       ---
   DIR commit 4b286b9d2722909d0682e50eeecdfe16c1f47fd8
   DIR parent 689f647baf96af078186f0cdc45199f7d0995d22
  HTML Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
       Date:   Sun, 20 Oct 2019 10:39:00 +0200
       
       resources/images: Allow to set background fill colour
       
       Closes #6298
       
       Diffstat:
         M docs/content/en/content-management… |      45 +++++++++++++++++++++++++-------
         M hugolib/image_test.go               |       5 +++--
         M resources/image.go                  |      38 +++++++++++++++++++++++++++----
         M resources/image_test.go             |      16 ++++++++++++----
         A resources/images/color.go           |      85 +++++++++++++++++++++++++++++++
         A resources/images/color_test.go      |      90 +++++++++++++++++++++++++++++++
         M resources/images/config.go          |      56 +++++++++++++++++++++++++++----
         M resources/images/config_test.go     |      27 +++++++++++++++++----------
         M resources/images/image.go           |      39 +++++++++++++++++++++++--------
         A resources/testdata/golden/gopher-h… |       0 
         A resources/testdata/golden/gopher-h… |       0 
         A resources/testdata/golden/gradient… |       0 
         A resources/testdata/golden/gradient… |       0 
         A resources/testdata/gopher-hero8.png |       0 
         A resources/testdata/gradient-circle… |       0 
       
       15 files changed, 356 insertions(+), 45 deletions(-)
       ---
   DIR diff --git a/docs/content/en/content-management/image-processing/index.md b/docs/content/en/content-management/image-processing/index.md
       @@ -2,7 +2,6 @@
        title: "Image Processing"
        description: "Image Page resources can be resized and cropped."
        date: 2018-01-24T13:10:00-05:00
       -lastmod: 2018-01-26T15:59:07-05:00
        linktitle: "Image Processing"
        categories: ["content management"]
        keywords: [bundle,content,resources,images]
       @@ -72,31 +71,42 @@ Image operations in Hugo currently **do not preserve EXIF data** as this is not 
        
        In addition to the dimensions (e.g. `600x400`), Hugo supports a set of additional image options.
        
       +### Background Color
        
       -JPEG Quality
       -: Only relevant for JPEG images, values 1 to 100 inclusive, higher is better. Default is 75.
       +The background color to fill into the transparency layer. This is mostly useful when converting to a format that does not support transparency, e.g. `JPEG`.
       +
       +You can set the background color to use with a 3 or 6 digit hex code starting with `#`.
       +
       +```go
       +{{ $image.Resize "600x jpg #b31280" }}
       +```
       +
       +For color codes, see https://www.google.com/search?q=color+picker
       +
       +### JPEG Quality
       +Only relevant for JPEG images, values 1 to 100 inclusive, higher is better. Default is 75.
        
        ```go
        {{ $image.Resize "600x q50" }}
        ```
        
       -Rotate
       -: Rotates an image by the given angle counter-clockwise. The rotation will be performed first to get the dimensions correct. The main use of this is to be able to manually correct for [EXIF orientation](https://github.com/golang/go/issues/4341) of JPEG images.
       +### Rotate
       +Rotates an image by the given angle counter-clockwise. The rotation will be performed first to get the dimensions correct. The main use of this is to be able to manually correct for [EXIF orientation](https://github.com/golang/go/issues/4341) of JPEG images.
        
        ```go
        {{ $image.Resize "600x r90" }}
        ```
        
       -Anchor
       -: Only relevant for the `Fill` method. This is useful for thumbnail generation where the main motive is located in, say, the left corner. 
       +###  Anchor
       +Only relevant for the `Fill` method. This is useful for thumbnail generation where the main motive is located in, say, the left corner. 
        Valid are `Center`, `TopLeft`, `Top`, `TopRight`, `Left`, `Right`, `BottomLeft`, `Bottom`, `BottomRight`.
        
        ```go
        {{ $image.Fill "300x200 BottomLeft" }}
        ```
        
       -Resample Filter
       -: Filter used in resizing. Default is `Box`, a simple and fast resampling filter appropriate for downscaling. 
       +### Resample Filter
       +Filter used in resizing. Default is `Box`, a simple and fast resampling filter appropriate for downscaling. 
        
        Examples are: `Box`, `NearestNeighbor`, `Linear`, `Gaussian`.
        
       @@ -106,6 +116,16 @@ See https://github.com/disintegration/imaging for more. If you want to trade qua
        {{ $image.Resize "600x400 Gaussian" }}
        ```
        
       +### Target Format
       +
       +By default the images is encoded in the source format, but you can set the target format as an option.
       +
       +Valid values are `jpg`, `png`, `tif`, `bmp`, and `gif`.
       +
       +```go
       +{{ $image.Resize "600x jpg" }}
       +```
       +
        ## Image Processing Examples
        
        _The photo of the sunset used in the examples below is Copyright [Bjørn Erik Pedersen](https://commons.wikimedia.org/wiki/User:Bep) (Creative Commons Attribution-Share Alike 4.0 International license)_
       @@ -160,6 +180,13 @@ quality = 75
        # Valid values are Smart, Center, TopLeft, Top, TopRight, Left, Right, BottomLeft, Bottom, BottomRight
        anchor = "smart"
        
       +# Default background color. 
       +# Hugo will preserve transparency for target formats that supports it,
       +# but will fall back to this color for JPEG.
       +# Expects a standard HEX color string with 3 or 6 digits.
       +# See https://www.google.com/search?q=color+picker
       +bgColor = "#ffffff"
       +
        ```
        
        All of the above settings can also be set per image procecssing.
   DIR diff --git a/hugolib/image_test.go b/hugolib/image_test.go
       @@ -205,10 +205,11 @@ SUNSET2: {{ $resized2.RelPermalink }}/{{ $resized2.Width }}/Lat: {{ $resized2.Ex
        
                // Check the file cache
                b.AssertImage(200, 200, "resources/_gen/images/bundle/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x200_resize_q75_box.jpg")
       -        b.AssertFileContent("resources/_gen/images/bundle/sunset_17701188623491591036.json",
       +
       +        b.AssertFileContent("resources/_gen/images/bundle/sunset_7645215769587362592.json",
                        "DateTimeDigitized|time.Time", "PENTAX")
                b.AssertImage(123, 234, "resources/_gen/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_123x234_resize_q75_box.jpg")
       -        b.AssertFileContent("resources/_gen/images/sunset_17701188623491591036.json",
       +        b.AssertFileContent("resources/_gen/images/sunset_7645215769587362592.json",
                        "DateTimeDigitized|time.Time", "PENTAX")
        
                // TODO(bep) add this as a default assertion after Build()?
   DIR diff --git a/resources/image.go b/resources/image.go
       @@ -17,6 +17,7 @@ import (
                "encoding/json"
                "fmt"
                "image"
       +        "image/color"
                "image/draw"
                _ "image/gif"
                _ "image/png"
       @@ -254,10 +255,32 @@ func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src im
                                return nil, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
                        }
        
       +                hasAlpha := !images.IsOpaque(converted)
       +                shouldFill := conf.BgColor != nil && hasAlpha
       +                shouldFill = shouldFill || (!conf.TargetFormat.SupportsTransparency() && hasAlpha)
       +                var bgColor color.Color
       +
       +                if shouldFill {
       +                        bgColor = conf.BgColor
       +                        if bgColor == nil {
       +                                bgColor = i.Proc.Cfg.BgColor
       +                        }
       +                        tmp := image.NewRGBA(converted.Bounds())
       +                        draw.Draw(tmp, tmp.Bounds(), image.NewUniform(bgColor), image.Point{}, draw.Src)
       +                        draw.Draw(tmp, tmp.Bounds(), converted, converted.Bounds().Min, draw.Over)
       +                        converted = tmp
       +                }
       +
                        if conf.TargetFormat == images.PNG {
                                // Apply the colour palette from the source
                                if paletted, ok := src.(*image.Paletted); ok {
       -                                tmp := image.NewPaletted(converted.Bounds(), paletted.Palette)
       +                                palette := paletted.Palette
       +                                if bgColor != nil && len(palette) < 256 {
       +                                        palette = images.AddColorToPalette(bgColor, palette)
       +                                } else if bgColor != nil {
       +                                        images.ReplaceColorInPalette(bgColor, palette)
       +                                }
       +                                tmp := image.NewPaletted(converted.Bounds(), palette)
                                        draw.FloydSteinberg.Draw(tmp, tmp.Bounds(), converted, converted.Bounds().Min)
                                        converted = tmp
                                }
       @@ -273,7 +296,7 @@ func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src im
        }
        
        func (i *imageResource) decodeImageConfig(action, spec string) (images.ImageConfig, error) {
       -        conf, err := images.DecodeImageConfig(action, spec, i.Proc.Cfg)
       +        conf, err := images.DecodeImageConfig(action, spec, i.Proc.Cfg.Cfg)
                if err != nil {
                        return conf, err
                }
       @@ -285,7 +308,14 @@ func (i *imageResource) decodeImageConfig(action, spec string) (images.ImageConf
        
                if conf.Quality <= 0 && conf.TargetFormat.RequiresDefaultQuality() {
                        // We need a quality setting for all JPEGs
       -                conf.Quality = i.Proc.Cfg.Quality
       +                conf.Quality = i.Proc.Cfg.Cfg.Quality
       +        }
       +
       +        if conf.BgColor == nil && conf.TargetFormat != i.Format {
       +                if i.Format.SupportsTransparency() && !conf.TargetFormat.SupportsTransparency() {
       +                        conf.BgColor = i.Proc.Cfg.BgColor
       +                        conf.BgColorStr = i.Proc.Cfg.Cfg.BgColor
       +                }
                }
        
                return conf, nil
       @@ -325,7 +355,7 @@ func (i *imageResource) setBasePath(conf images.ImageConfig) {
        func (i *imageResource) getImageMetaCacheTargetPath() string {
                const imageMetaVersionNumber = 1 // Increment to invalidate the meta cache
        
       -        cfg := i.getSpec().imaging.Cfg
       +        cfg := i.getSpec().imaging.Cfg.Cfg
                df := i.getResourcePaths().relTargetDirFile
                if fi := i.getFileInfo(); fi != nil {
                        df.dir = filepath.Dir(fi.Meta().Path())
   DIR diff --git a/resources/image_test.go b/resources/image_test.go
       @@ -22,7 +22,6 @@ import (
                "os"
                "path"
                "path/filepath"
       -        "regexp"
                "runtime"
                "strconv"
                "sync"
       @@ -540,6 +539,18 @@ func TestImageOperationsGolden(t *testing.T) {
                        fmt.Println(workDir)
                }
        
       +        // Test PNGs with alpha channel.
       +        for _, img := range []string{"gopher-hero8.png", "gradient-circle.png"} {
       +                orig := fetchImageForSpec(spec, c, img)
       +                for _, resizeSpec := range []string{"200x #e3e615", "200x jpg #e3e615"} {
       +                        resized, err := orig.Resize(resizeSpec)
       +                        c.Assert(err, qt.IsNil)
       +                        rel := resized.RelPermalink()
       +                        c.Log("resize", rel)
       +                        c.Assert(rel, qt.Not(qt.Equals), "")
       +                }
       +        }
       +
                for _, img := range testImages {
        
                        orig := fetchImageForSpec(spec, c, img)
       @@ -618,9 +629,6 @@ func TestImageOperationsGolden(t *testing.T) {
                c.Assert(len(dirinfos1), qt.Equals, len(dirinfos2))
        
                for i, fi1 := range dirinfos1 {
       -                if regexp.MustCompile("gauss").MatchString(fi1.Name()) {
       -                        continue
       -                }
                        fi2 := dirinfos2[i]
                        c.Assert(fi1.Name(), qt.Equals, fi2.Name())
        
   DIR diff --git a/resources/images/color.go b/resources/images/color.go
       @@ -0,0 +1,85 @@
       +// Copyright 2019 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 (
       +        "encoding/hex"
       +        "image/color"
       +        "strings"
       +
       +        "github.com/pkg/errors"
       +)
       +
       +// AddColorToPalette adds c as the first color in p if not already there.
       +// Note that it does no additional checks, so callers must make sure
       +// that the palette is valid for the relevant format.
       +func AddColorToPalette(c color.Color, p color.Palette) color.Palette {
       +        var found bool
       +        for _, cc := range p {
       +                if c == cc {
       +                        found = true
       +                        break
       +                }
       +        }
       +
       +        if !found {
       +                p = append(color.Palette{c}, p...)
       +        }
       +
       +        return p
       +}
       +
       +// ReplaceColorInPalette will replace the color in palette p closest to c in Euclidean
       +// R,G,B,A space with c.
       +func ReplaceColorInPalette(c color.Color, p color.Palette) {
       +        p[p.Index(c)] = c
       +}
       +
       +func hexStringToColor(s string) (color.Color, error) {
       +        s = strings.TrimPrefix(s, "#")
       +
       +        if len(s) != 3 && len(s) != 6 {
       +                return nil, errors.Errorf("invalid color code: %q", s)
       +        }
       +
       +        s = strings.ToLower(s)
       +
       +        if len(s) == 3 {
       +                var v string
       +                for _, r := range s {
       +                        v += string(r) + string(r)
       +                }
       +                s = v
       +        }
       +
       +        // Standard colors.
       +        if s == "ffffff" {
       +                return color.White, nil
       +        }
       +
       +        if s == "000000" {
       +                return color.Black, nil
       +        }
       +
       +        // Set Alfa to white.
       +        s += "ff"
       +
       +        b, err := hex.DecodeString(s)
       +        if err != nil {
       +                return nil, err
       +        }
       +
       +        return color.RGBA{b[0], b[1], b[2], b[3]}, nil
       +
       +}
   DIR diff --git a/resources/images/color_test.go b/resources/images/color_test.go
       @@ -0,0 +1,90 @@
       +// Copyright 2019 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/color"
       +        "testing"
       +
       +        qt "github.com/frankban/quicktest"
       +)
       +
       +func TestHexStringToColor(t *testing.T) {
       +        c := qt.New(t)
       +
       +        for _, test := range []struct {
       +                arg    string
       +                expect interface{}
       +        }{
       +                {"f", false},
       +                {"#f", false},
       +                {"#fffffff", false},
       +                {"fffffff", false},
       +                {"#fff", color.White},
       +                {"fff", color.White},
       +                {"FFF", color.White},
       +                {"FfF", color.White},
       +                {"#ffffff", color.White},
       +                {"ffffff", color.White},
       +                {"#000", color.Black},
       +                {"#4287f5", color.RGBA{R: 0x42, G: 0x87, B: 0xf5, A: 0xff}},
       +                {"777", color.RGBA{R: 0x77, G: 0x77, B: 0x77, A: 0xff}},
       +        } {
       +
       +                test := test
       +                c.Run(test.arg, func(c *qt.C) {
       +                        c.Parallel()
       +
       +                        result, err := hexStringToColor(test.arg)
       +
       +                        if b, ok := test.expect.(bool); ok && !b {
       +                                c.Assert(err, qt.Not(qt.IsNil))
       +                                return
       +                        }
       +
       +                        c.Assert(err, qt.IsNil)
       +                        c.Assert(result, qt.DeepEquals, test.expect)
       +                })
       +
       +        }
       +}
       +
       +func TestAddColorToPalette(t *testing.T) {
       +        c := qt.New(t)
       +
       +        palette := color.Palette{color.White, color.Black}
       +
       +        c.Assert(AddColorToPalette(color.White, palette), qt.HasLen, 2)
       +
       +        blue1, _ := hexStringToColor("34c3eb")
       +        blue2, _ := hexStringToColor("34c3eb")
       +        white, _ := hexStringToColor("fff")
       +
       +        c.Assert(AddColorToPalette(white, palette), qt.HasLen, 2)
       +        c.Assert(AddColorToPalette(blue1, palette), qt.HasLen, 3)
       +        c.Assert(AddColorToPalette(blue2, palette), qt.HasLen, 3)
       +
       +}
       +
       +func TestReplaceColorInPalette(t *testing.T) {
       +        c := qt.New(t)
       +
       +        palette := color.Palette{color.White, color.Black}
       +        offWhite, _ := hexStringToColor("fcfcfc")
       +
       +        ReplaceColorInPalette(offWhite, palette)
       +
       +        c.Assert(palette, qt.HasLen, 2)
       +        c.Assert(palette[0], qt.Equals, offWhite)
       +}
   DIR diff --git a/resources/images/config.go b/resources/images/config.go
       @@ -16,6 +16,7 @@ package images
        import (
                "errors"
                "fmt"
       +        "image/color"
                "strconv"
                "strings"
        
       @@ -27,6 +28,7 @@ import (
        const (
                defaultJPEGQuality    = 75
                defaultResampleFilter = "box"
       +        defaultBgColor        = "ffffff"
        )
        
        var (
       @@ -87,16 +89,28 @@ func ImageFormatFromExt(ext string) (Format, bool) {
                return f, found
        }
        
       -func DecodeConfig(m map[string]interface{}) (Imaging, error) {
       +func DecodeConfig(m map[string]interface{}) (ImagingConfig, error) {
                var i Imaging
       +        var ic ImagingConfig
                if err := mapstructure.WeakDecode(m, &i); err != nil {
       -                return i, err
       +                return ic, err
                }
        
                if i.Quality == 0 {
                        i.Quality = defaultJPEGQuality
                } else if i.Quality < 0 || i.Quality > 100 {
       -                return i, errors.New("JPEG quality must be a number between 1 and 100")
       +                return ic, errors.New("JPEG quality must be a number between 1 and 100")
       +        }
       +
       +        if i.BgColor != "" {
       +                i.BgColor = strings.TrimPrefix(i.BgColor, "#")
       +        } else {
       +                i.BgColor = defaultBgColor
       +        }
       +        var err error
       +        ic.BgColor, err = hexStringToColor(i.BgColor)
       +        if err != nil {
       +                return ic, err
                }
        
                if i.Anchor == "" || strings.EqualFold(i.Anchor, smartCropIdentifier) {
       @@ -104,7 +118,7 @@ func DecodeConfig(m map[string]interface{}) (Imaging, error) {
                } else {
                        i.Anchor = strings.ToLower(i.Anchor)
                        if _, found := anchorPositions[i.Anchor]; !found {
       -                        return i, errors.New("invalid anchor value in imaging config")
       +                        return ic, errors.New("invalid anchor value in imaging config")
                        }
                }
        
       @@ -114,7 +128,7 @@ func DecodeConfig(m map[string]interface{}) (Imaging, error) {
                        filter := strings.ToLower(i.ResampleFilter)
                        _, found := imageFilters[filter]
                        if !found {
       -                        return i, fmt.Errorf("%q is not a valid resample filter", filter)
       +                        return ic, fmt.Errorf("%q is not a valid resample filter", filter)
                        }
                        i.ResampleFilter = filter
                }
       @@ -124,7 +138,9 @@ func DecodeConfig(m map[string]interface{}) (Imaging, error) {
                        i.Exif.ExcludeFields = "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance"
                }
        
       -        return i, nil
       +        ic.Cfg = i
       +
       +        return ic, nil
        }
        
        func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, error) {
       @@ -151,6 +167,12 @@ func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, er
                        } else if filter, ok := imageFilters[part]; ok {
                                c.Filter = filter
                                c.FilterStr = part
       +                } else if part[0] == '#' {
       +                        c.BgColorStr = part[1:]
       +                        c.BgColor, err = hexStringToColor(c.BgColorStr)
       +                        if err != nil {
       +                                return c, err
       +                        }
                        } else if part[0] == 'q' {
                                c.Quality, err = strconv.Atoi(part[1:])
                                if err != nil {
       @@ -230,6 +252,14 @@ type ImageConfig struct {
                // The rotation will be performed first.
                Rotate int
        
       +        // Used to fill any transparency.
       +        // When set in site config, it's used when converting to a format that does
       +        // not support transparency.
       +        // When set per image operation, it's used even for formats that does support
       +        // transparency.
       +        BgColor    color.Color
       +        BgColorStr string
       +
                Width  int
                Height int
        
       @@ -255,6 +285,10 @@ func (i ImageConfig) GetKey(format Format) string {
                if i.Rotate != 0 {
                        k += "_r" + strconv.Itoa(i.Rotate)
                }
       +        if i.BgColorStr != "" {
       +                k += "_bg" + i.BgColorStr
       +        }
       +
                anchor := i.AnchorStr
                if anchor == smartCropIdentifier {
                        anchor = anchor + strconv.Itoa(smartCropVersionNumber)
       @@ -277,6 +311,13 @@ func (i ImageConfig) GetKey(format Format) string {
                return k
        }
        
       +type ImagingConfig struct {
       +        BgColor color.Color
       +
       +        // Config as provided by the user.
       +        Cfg Imaging
       +}
       +
        // Imaging contains default image processing configuration. This will be fetched
        // from site (or language) config.
        type Imaging struct {
       @@ -289,6 +330,9 @@ type Imaging struct {
                // The anchor to use in Fill. Default is "smart", i.e. Smart Crop.
                Anchor string
        
       +        // Default color used in fill operations (e.g. "fff" for white).
       +        BgColor string
       +
                Exif ExifConfig
        }
        
   DIR diff --git a/resources/images/config_test.go b/resources/images/config_test.go
       @@ -29,17 +29,19 @@ func TestDecodeConfig(t *testing.T) {
                        "anchor":         "topLeft",
                }
        
       -        imaging, err := DecodeConfig(m)
       +        imagingConfig, err := DecodeConfig(m)
        
                c.Assert(err, qt.IsNil)
       +        imaging := imagingConfig.Cfg
                c.Assert(imaging.Quality, qt.Equals, 42)
                c.Assert(imaging.ResampleFilter, qt.Equals, "nearestneighbor")
                c.Assert(imaging.Anchor, qt.Equals, "topleft")
        
                m = map[string]interface{}{}
        
       -        imaging, err = DecodeConfig(m)
       +        imagingConfig, err = DecodeConfig(m)
                c.Assert(err, qt.IsNil)
       +        imaging = imagingConfig.Cfg
                c.Assert(imaging.Quality, qt.Equals, defaultJPEGQuality)
                c.Assert(imaging.ResampleFilter, qt.Equals, "box")
                c.Assert(imaging.Anchor, qt.Equals, "smart")
       @@ -59,18 +61,20 @@ func TestDecodeConfig(t *testing.T) {
                })
                c.Assert(err, qt.Not(qt.IsNil))
        
       -        imaging, err = DecodeConfig(map[string]interface{}{
       +        imagingConfig, err = DecodeConfig(map[string]interface{}{
                        "anchor": "Smart",
                })
       +        imaging = imagingConfig.Cfg
                c.Assert(err, qt.IsNil)
                c.Assert(imaging.Anchor, qt.Equals, "smart")
        
       -        imaging, err = DecodeConfig(map[string]interface{}{
       +        imagingConfig, err = DecodeConfig(map[string]interface{}{
                        "exif": map[string]interface{}{
                                "disableLatLong": true,
                        },
                })
                c.Assert(err, qt.IsNil)
       +        imaging = imagingConfig.Cfg
                c.Assert(imaging.Exif.DisableLatLong, qt.Equals, true)
                c.Assert(imaging.Exif.ExcludeFields, qt.Equals, "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance")
        
       @@ -81,11 +85,12 @@ func TestDecodeImageConfig(t *testing.T) {
                        in     string
                        expect interface{}
                }{
       -                {"300x400", newImageConfig(300, 400, 0, 0, "", "")},
       -                {"100x200 bottomRight", newImageConfig(100, 200, 0, 0, "", "BottomRight")},
       -                {"10x20 topleft Lanczos", newImageConfig(10, 20, 0, 0, "Lanczos", "topleft")},
       -                {"linear left 10x r180", newImageConfig(10, 0, 0, 180, "linear", "left")},
       -                {"x20 riGht Cosine q95", newImageConfig(0, 20, 95, 0, "cosine", "right")},
       +                {"300x400", newImageConfig(300, 400, 0, 0, "", "", "")},
       +                {"300x400 #fff", newImageConfig(300, 400, 0, 0, "", "", "fff")},
       +                {"100x200 bottomRight", newImageConfig(100, 200, 0, 0, "", "BottomRight", "")},
       +                {"10x20 topleft Lanczos", newImageConfig(10, 20, 0, 0, "Lanczos", "topleft", "")},
       +                {"linear left 10x r180", newImageConfig(10, 0, 0, 180, "linear", "left", "")},
       +                {"x20 riGht Cosine q95", newImageConfig(0, 20, 95, 0, "cosine", "right", "")},
        
                        {"", false},
                        {"foo", false},
       @@ -107,13 +112,15 @@ func TestDecodeImageConfig(t *testing.T) {
                }
        }
        
       -func newImageConfig(width, height, quality, rotate int, filter, anchor string) ImageConfig {
       +func newImageConfig(width, height, quality, rotate int, filter, anchor, bgColor string) ImageConfig {
                var c ImageConfig
                c.Action = "resize"
                c.Width = width
                c.Height = height
                c.Quality = quality
                c.Rotate = rotate
       +        c.BgColorStr = bgColor
       +        c.BgColor, _ = hexStringToColor(bgColor)
        
                if filter != "" {
                        filter = strings.ToLower(filter)
   DIR diff --git a/resources/images/image.go b/resources/images/image.go
       @@ -51,11 +51,8 @@ func NewImage(f Format, proc *ImageProcessor, img image.Image, s Spec) *Image {
        
        type Image struct {
                Format Format
       -
       -        Proc *ImageProcessor
       -
       -        Spec Spec
       -
       +        Proc   *ImageProcessor
       +        Spec   Spec
                *imageConfig
        }
        
       @@ -158,8 +155,8 @@ func (i *Image) initConfig() error {
                return nil
        }
        
       -func NewImageProcessor(cfg Imaging) (*ImageProcessor, error) {
       -        e := cfg.Exif
       +func NewImageProcessor(cfg ImagingConfig) (*ImageProcessor, error) {
       +        e := cfg.Cfg.Exif
                exifDecoder, err := exif.NewDecoder(
                        exif.WithDateDisabled(e.DisableDate),
                        exif.WithLatLongDisabled(e.DisableLatLong),
       @@ -179,7 +176,7 @@ func NewImageProcessor(cfg Imaging) (*ImageProcessor, error) {
        }
        
        type ImageProcessor struct {
       -        Cfg         Imaging
       +        Cfg         ImagingConfig
                exifDecoder *exif.Decoder
        }
        
       @@ -218,7 +215,12 @@ func (p *ImageProcessor) ApplyFiltersFromConfig(src image.Image, conf ImageConfi
                        return nil, errors.Errorf("unsupported action: %q", conf.Action)
                }
        
       -        return p.Filter(src, filters...)
       +        img, err := p.Filter(src, filters...)
       +        if err != nil {
       +                return nil, err
       +        }
       +
       +        return img, nil
        }
        
        func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.Image, error) {
       @@ -231,7 +233,7 @@ func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.
        func (p *ImageProcessor) GetDefaultImageConfig(action string) ImageConfig {
                return ImageConfig{
                        Action:  action,
       -                Quality: p.Cfg.Quality,
       +                Quality: p.Cfg.Cfg.Quality,
                }
        }
        
       @@ -256,6 +258,11 @@ func (f Format) RequiresDefaultQuality() bool {
                return f == JPEG
        }
        
       +// SupportsTransparency reports whether it supports transparency in any form.
       +func (f Format) SupportsTransparency() bool {
       +        return f != JPEG
       +}
       +
        // DefaultExtension returns the default file extension of this format, starting with a dot.
        // For example: .jpg for JPEG
        func (f Format) DefaultExtension() string {
       @@ -307,3 +314,15 @@ func ToFilters(in interface{}) []gift.Filter {
                        panic(fmt.Sprintf("%T is not an image filter", in))
                }
        }
       +
       +// IsOpaque returns false if the image has alpha channel and there is at least 1
       +// pixel that is not (fully) opaque.
       +func IsOpaque(img image.Image) bool {
       +        if oim, ok := img.(interface {
       +                Opaque() bool
       +        }); ok {
       +                return oim.Opaque()
       +        }
       +
       +        return false
       +}
   DIR diff --git a/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_200x0_resize_bge3e615_box_2.png b/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_200x0_resize_bge3e615_box_2.png
       Binary files differ.
   DIR diff --git a/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_200x0_resize_q75_bge3e615_box_2.jpg b/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_200x0_resize_q75_bge3e615_box_2.jpg
       Binary files differ.
   DIR diff --git a/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_200x0_resize_bge3e615_box_2.png b/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_200x0_resize_bge3e615_box_2.png
       Binary files differ.
   DIR diff --git a/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_200x0_resize_q75_bge3e615_box_2.jpg b/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_200x0_resize_q75_bge3e615_box_2.jpg
       Binary files differ.
   DIR diff --git a/resources/testdata/gopher-hero8.png b/resources/testdata/gopher-hero8.png
       Binary files differ.
   DIR diff --git a/resources/testdata/gradient-circle.png b/resources/testdata/gradient-circle.png
       Binary files differ.