resources/images: Allow to set background fill colour
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 20 Oct 2019 08:39:00 +0000 (10:39 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 20 Oct 2019 20:06:58 +0000 (22:06 +0200)
Closes #6298

15 files changed:
docs/content/en/content-management/image-processing/index.md
hugolib/image_test.go
resources/image.go
resources/image_test.go
resources/images/color.go [new file with mode: 0644]
resources/images/color_test.go [new file with mode: 0644]
resources/images/config.go
resources/images/config_test.go
resources/images/image.go
resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_200x0_resize_bge3e615_box_2.png [new file with mode: 0644]
resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_200x0_resize_q75_bge3e615_box_2.jpg [new file with mode: 0644]
resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_200x0_resize_bge3e615_box_2.png [new file with mode: 0644]
resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_200x0_resize_q75_bge3e615_box_2.jpg [new file with mode: 0644]
resources/testdata/gopher-hero8.png [new file with mode: 0644]
resources/testdata/gradient-circle.png [new file with mode: 0644]

index b83a6c103eaf5ffd7bebaf34ff4540260b0957a5..f03c5bee6ad03e34bf1067bb3ccbd6e792e48ad5 100644 (file)
@@ -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.
index a13338afc0f86b81f6b6d70613b4c1305e12529c..d0bff75a2ffb75d58483433abc26a8e944900a7a 100644 (file)
@@ -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()?
index bb9c987a5c530a29c456dec612e1333e085463b1..1991e65f51104eb4ec4585284426a8085a72bc40 100644 (file)
@@ -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())
index 4b88b7aa1213032eb84dc1c64127b7c30abfad04..89e686ed18d500a38470c36f969967858891b28a 100644 (file)
@@ -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())
 
diff --git a/resources/images/color.go b/resources/images/color.go
new file mode 100644 (file)
index 0000000..b17173e
--- /dev/null
@@ -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
+
+}
diff --git a/resources/images/color_test.go b/resources/images/color_test.go
new file mode 100644 (file)
index 0000000..3ef9f76
--- /dev/null
@@ -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)
+}
index 6bc701bfe28cfa140f0deee0a4bdb7fc38123b8e..7b2ade29f6a64ee892bd409cbf5788472e45372b 100644 (file)
@@ -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
 }
 
index 46b0c9858a9ba9b3d9203da012226a439333de2e..f60cce9ef0bc344abe7e8f55d2edb22e6b01f363 100644 (file)
@@ -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)
index bd7500c28389dd105955190e752f5df0e2398153..bac05ab708fa89f77d11494de211cf184c0d0909 100644 (file)
@@ -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
+}
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
new file mode 100644 (file)
index 0000000..830ee90
Binary files /dev/null and b/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_200x0_resize_bge3e615_box_2.png differ
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
new file mode 100644 (file)
index 0000000..4ae6f51
Binary files /dev/null and b/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_200x0_resize_q75_bge3e615_box_2.jpg differ
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
new file mode 100644 (file)
index 0000000..3c861e9
Binary files /dev/null and b/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_200x0_resize_bge3e615_box_2.png differ
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
new file mode 100644 (file)
index 0000000..beb80bb
Binary files /dev/null and b/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_200x0_resize_q75_bge3e615_box_2.jpg differ
diff --git a/resources/testdata/gopher-hero8.png b/resources/testdata/gopher-hero8.png
new file mode 100644 (file)
index 0000000..08ae570
Binary files /dev/null and b/resources/testdata/gopher-hero8.png differ
diff --git a/resources/testdata/gradient-circle.png b/resources/testdata/gradient-circle.png
new file mode 100644 (file)
index 0000000..a4ace53
Binary files /dev/null and b/resources/testdata/gradient-circle.png differ