Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/gohugoio/hugo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-10-20 11:39:00 +0300
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-10-20 23:06:58 +0300
commit4b286b9d2722909d0682e50eeecdfe16c1f47fd8 (patch)
tree3efb5cd01bc95bc4eada991d01e5a3a84adee28c /resources/images
parent689f647baf96af078186f0cdc45199f7d0995d22 (diff)
resources/images: Allow to set background fill colour
Closes #6298
Diffstat (limited to 'resources/images')
-rw-r--r--resources/images/color.go85
-rw-r--r--resources/images/color_test.go90
-rw-r--r--resources/images/config.go56
-rw-r--r--resources/images/config_test.go27
-rw-r--r--resources/images/image.go39
5 files changed, 271 insertions, 26 deletions
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
+}