diff options
author | Bjørn Erik Pedersen <[email protected]> | 2019-10-20 10:39:00 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2019-10-20 22:06:58 +0200 |
commit | 4b286b9d2722909d0682e50eeecdfe16c1f47fd8 (patch) | |
tree | 3efb5cd01bc95bc4eada991d01e5a3a84adee28c | |
parent | 689f647baf96af078186f0cdc45199f7d0995d22 (diff) | |
download | hugo-4b286b9d2722909d0682e50eeecdfe16c1f47fd8.tar.gz hugo-4b286b9d2722909d0682e50eeecdfe16c1f47fd8.zip |
resources/images: Allow to set background fill colour
Closes #6298
15 files changed, 356 insertions, 45 deletions
diff --git a/docs/content/en/content-management/image-processing/index.md b/docs/content/en/content-management/image-processing/index.md index b83a6c103..f03c5bee6 100644 --- 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. diff --git a/hugolib/image_test.go b/hugolib/image_test.go index a13338afc..d0bff75a2 100644 --- 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()? diff --git a/resources/image.go b/resources/image.go index bb9c987a5..1991e65f5 100644 --- 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()) diff --git a/resources/image_test.go b/resources/image_test.go index 4b88b7aa1..89e686ed1 100644 --- 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()) diff --git a/resources/images/color.go b/resources/images/color.go new file mode 100644 index 000000000..b17173e26 --- /dev/null +++ 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 + +} diff --git a/resources/images/color_test.go b/resources/images/color_test.go new file mode 100644 index 000000000..3ef9f76cc --- /dev/null +++ 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) +} diff --git a/resources/images/config.go b/resources/images/config.go index 6bc701bfe..7b2ade29f 100644 --- 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 } diff --git a/resources/images/config_test.go b/resources/images/config_test.go index 46b0c9858..f60cce9ef 100644 --- 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) diff --git a/resources/images/image.go b/resources/images/image.go index bd7500c28..bac05ab70 100644 --- 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 +} 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 differnew file mode 100644 index 000000000..830ee906b --- /dev/null +++ b/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_200x0_resize_bge3e615_box_2.png 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 differnew file mode 100644 index 000000000..4ae6f5173 --- /dev/null +++ b/resources/testdata/golden/gopher-hero8_huaa0cd7d2cfc14ff32a57f171896f2285_13327_200x0_resize_q75_bge3e615_box_2.jpg 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 differnew file mode 100644 index 000000000..3c861e922 --- /dev/null +++ b/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_200x0_resize_bge3e615_box_2.png 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 differnew file mode 100644 index 000000000..beb80bb12 --- /dev/null +++ b/resources/testdata/golden/gradient-circle_huf3d35257a40a8d6f525263a856c5ecfd_20069_200x0_resize_q75_bge3e615_box_2.jpg diff --git a/resources/testdata/gopher-hero8.png b/resources/testdata/gopher-hero8.png Binary files differnew file mode 100644 index 000000000..08ae570d2 --- /dev/null +++ b/resources/testdata/gopher-hero8.png diff --git a/resources/testdata/gradient-circle.png b/resources/testdata/gradient-circle.png Binary files differnew file mode 100644 index 000000000..a4ace53a1 --- /dev/null +++ b/resources/testdata/gradient-circle.png |