aboutsummaryrefslogtreecommitdiffhomepage
path: root/resources/images
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <[email protected]>2021-04-07 16:49:34 +0200
committerBjørn Erik Pedersen <[email protected]>2021-04-15 17:22:55 +0200
commit33d5f805923eb50dfb309d024f6555c59a339846 (patch)
treecba8fde34849640bc09bd68d85c5d7a6ba042add /resources/images
parent509d39fa6ddbba106c127b7923a41b0dcaea9381 (diff)
downloadhugo-33d5f805923eb50dfb309d024f6555c59a339846.tar.gz
hugo-33d5f805923eb50dfb309d024f6555c59a339846.zip
Add webp image encoding support
Fixes #5924
Diffstat (limited to 'resources/images')
-rw-r--r--resources/images/config.go182
-rw-r--r--resources/images/config_test.go33
-rw-r--r--resources/images/image.go25
-rw-r--r--resources/images/webp/webp.go30
-rw-r--r--resources/images/webp/webp_notavailable.go30
5 files changed, 230 insertions, 70 deletions
diff --git a/resources/images/config.go b/resources/images/config.go
index 7b2ade29f..a4942688b 100644
--- a/resources/images/config.go
+++ b/resources/images/config.go
@@ -14,23 +14,22 @@
package images
import (
- "errors"
"fmt"
"image/color"
"strconv"
"strings"
+ "github.com/gohugoio/hugo/helpers"
+
+ "github.com/pkg/errors"
+
+ "github.com/bep/gowebp/libwebp/webpoptions"
+
"github.com/disintegration/gift"
"github.com/mitchellh/mapstructure"
)
-const (
- defaultJPEGQuality = 75
- defaultResampleFilter = "box"
- defaultBgColor = "ffffff"
-)
-
var (
imageFormats = map[string]Format{
".jpg": JPEG,
@@ -40,6 +39,7 @@ var (
".tiff": TIFF,
".bmp": BMP,
".gif": GIF,
+ ".webp": WEBP,
}
// Add or increment if changes to an image format's processing requires
@@ -65,6 +65,15 @@ var anchorPositions = map[string]gift.Anchor{
strings.ToLower("BottomRight"): gift.BottomRightAnchor,
}
+// These encoding hints are currently only relevant for Webp.
+var hints = map[string]webpoptions.EncodingPreset{
+ "picture": webpoptions.EncodingPresetPicture,
+ "photo": webpoptions.EncodingPresetPhoto,
+ "drawing": webpoptions.EncodingPresetDrawing,
+ "icon": webpoptions.EncodingPresetIcon,
+ "text": webpoptions.EncodingPresetText,
+}
+
var imageFilters = map[string]gift.Resampling{
strings.ToLower("NearestNeighbor"): gift.NearestNeighborResampling,
@@ -89,63 +98,71 @@ func ImageFormatFromExt(ext string) (Format, bool) {
return f, found
}
+const (
+ defaultJPEGQuality = 75
+ defaultResampleFilter = "box"
+ defaultBgColor = "ffffff"
+ defaultHint = "photo"
+)
+
+var defaultImaging = Imaging{
+ ResampleFilter: defaultResampleFilter,
+ BgColor: defaultBgColor,
+ Hint: defaultHint,
+ Quality: defaultJPEGQuality,
+}
+
func DecodeConfig(m map[string]interface{}) (ImagingConfig, error) {
- var i Imaging
- var ic ImagingConfig
- if err := mapstructure.WeakDecode(m, &i); err != nil {
- return ic, err
+ if m == nil {
+ m = make(map[string]interface{})
}
- if i.Quality == 0 {
- i.Quality = defaultJPEGQuality
- } else if i.Quality < 0 || i.Quality > 100 {
- return ic, errors.New("JPEG quality must be a number between 1 and 100")
+ i := ImagingConfig{
+ Cfg: defaultImaging,
+ CfgHash: helpers.HashString(m),
}
- if i.BgColor != "" {
- i.BgColor = strings.TrimPrefix(i.BgColor, "#")
- } else {
- i.BgColor = defaultBgColor
+ if err := mapstructure.WeakDecode(m, &i.Cfg); err != nil {
+ return i, err
+ }
+
+ if err := i.Cfg.init(); err != nil {
+ return i, err
}
+
var err error
- ic.BgColor, err = hexStringToColor(i.BgColor)
+ i.BgColor, err = hexStringToColor(i.Cfg.BgColor)
if err != nil {
- return ic, err
+ return i, err
}
- if i.Anchor == "" || strings.EqualFold(i.Anchor, smartCropIdentifier) {
- i.Anchor = smartCropIdentifier
- } else {
- i.Anchor = strings.ToLower(i.Anchor)
- if _, found := anchorPositions[i.Anchor]; !found {
- return ic, errors.New("invalid anchor value in imaging config")
+ if i.Cfg.Anchor != "" && i.Cfg.Anchor != smartCropIdentifier {
+ anchor, found := anchorPositions[i.Cfg.Anchor]
+ if !found {
+ return i, errors.Errorf("invalid anchor value %q in imaging config", i.Anchor)
}
+ i.Anchor = anchor
+ } else {
+ i.Cfg.Anchor = smartCropIdentifier
}
- if i.ResampleFilter == "" {
- i.ResampleFilter = defaultResampleFilter
- } else {
- filter := strings.ToLower(i.ResampleFilter)
- _, found := imageFilters[filter]
- if !found {
- return ic, fmt.Errorf("%q is not a valid resample filter", filter)
- }
- i.ResampleFilter = filter
+ filter, found := imageFilters[i.Cfg.ResampleFilter]
+ if !found {
+ return i, fmt.Errorf("%q is not a valid resample filter", filter)
}
+ i.ResampleFilter = filter
- if strings.TrimSpace(i.Exif.IncludeFields) == "" && strings.TrimSpace(i.Exif.ExcludeFields) == "" {
+ if strings.TrimSpace(i.Cfg.Exif.IncludeFields) == "" && strings.TrimSpace(i.Cfg.Exif.ExcludeFields) == "" {
// Don't change this for no good reason. Please don't.
- i.Exif.ExcludeFields = "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance"
+ i.Cfg.Exif.ExcludeFields = "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance"
}
- ic.Cfg = i
-
- return ic, nil
+ return i, nil
}
-func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, error) {
+func DecodeImageConfig(action, config string, defaults ImagingConfig, sourceFormat Format) (ImageConfig, error) {
var (
- c ImageConfig
+ c ImageConfig = GetDefaultImageConfig(action, defaults)
err error
)
@@ -167,6 +184,8 @@ func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, er
} else if filter, ok := imageFilters[part]; ok {
c.Filter = filter
c.FilterStr = part
+ } else if hint, ok := hints[part]; ok {
+ c.Hint = hint
} else if part[0] == '#' {
c.BgColorStr = part[1:]
c.BgColor, err = hexStringToColor(c.BgColorStr)
@@ -181,6 +200,7 @@ func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, er
if c.Quality < 1 || c.Quality > 100 {
return c, errors.New("quality ranges from 1 to 100 inclusive")
}
+ c.qualitySetForImage = true
} else if part[0] == 'r' {
c.Rotate, err = strconv.Atoi(part[1:])
if err != nil {
@@ -219,14 +239,33 @@ func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, er
}
if c.FilterStr == "" {
- c.FilterStr = defaults.ResampleFilter
- c.Filter = imageFilters[c.FilterStr]
+ c.FilterStr = defaults.Cfg.ResampleFilter
+ c.Filter = defaults.ResampleFilter
+ }
+
+ if c.Hint == 0 {
+ c.Hint = webpoptions.EncodingPresetPhoto
}
if c.AnchorStr == "" {
- c.AnchorStr = defaults.Anchor
- if !strings.EqualFold(c.AnchorStr, smartCropIdentifier) {
- c.Anchor = anchorPositions[c.AnchorStr]
+ c.AnchorStr = defaults.Cfg.Anchor
+ c.Anchor = defaults.Anchor
+ }
+
+ // default to the source format
+ if c.TargetFormat == 0 {
+ c.TargetFormat = sourceFormat
+ }
+
+ if c.Quality <= 0 && c.TargetFormat.RequiresDefaultQuality() {
+ // We need a quality setting for all JPEGs and WEBPs.
+ c.Quality = defaults.Cfg.Quality
+ }
+
+ if c.BgColor == nil && c.TargetFormat != sourceFormat {
+ if sourceFormat.SupportsTransparency() && !c.TargetFormat.SupportsTransparency() {
+ c.BgColor = defaults.BgColor
+ c.BgColorStr = defaults.Cfg.BgColor
}
}
@@ -235,7 +274,7 @@ func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, er
// ImageConfig holds configuration to create a new image from an existing one, resize etc.
type ImageConfig struct {
- // This defines the output format of the output image. It defaults to the source format
+ // This defines the output format of the output image. It defaults to the source format.
TargetFormat Format
Action string
@@ -244,9 +283,10 @@ type ImageConfig struct {
Key string
// Quality ranges from 1 to 100 inclusive, higher is better.
- // This is only relevant for JPEG images.
+ // This is only relevant for JPEG and WEBP images.
// Default is 75.
- Quality int
+ Quality int
+ qualitySetForImage bool // Whether the above is set for this image.
// Rotate rotates an image by the given angle counter-clockwise.
// The rotation will be performed first.
@@ -260,6 +300,10 @@ type ImageConfig struct {
BgColor color.Color
BgColorStr string
+ // Hint about what type of picture this is. Used to optimize encoding
+ // when target is set to webp.
+ Hint webpoptions.EncodingPreset
+
Width int
Height int
@@ -279,7 +323,8 @@ func (i ImageConfig) GetKey(format Format) string {
if i.Action != "" {
k += "_" + i.Action
}
- if i.Quality > 0 {
+ // This slightly odd construct is here to preserve the old image keys.
+ if i.qualitySetForImage || i.TargetFormat.RequiresDefaultQuality() {
k += "_q" + strconv.Itoa(i.Quality)
}
if i.Rotate != 0 {
@@ -289,6 +334,10 @@ func (i ImageConfig) GetKey(format Format) string {
k += "_bg" + i.BgColorStr
}
+ if i.TargetFormat == WEBP {
+ k += "_h" + strconv.Itoa(int(i.Hint))
+ }
+
anchor := i.AnchorStr
if anchor == smartCropIdentifier {
anchor = anchor + strconv.Itoa(smartCropVersionNumber)
@@ -312,10 +361,16 @@ func (i ImageConfig) GetKey(format Format) string {
}
type ImagingConfig struct {
- BgColor color.Color
+ BgColor color.Color
+ Hint webpoptions.EncodingPreset
+ ResampleFilter gift.Resampling
+ Anchor gift.Anchor
// Config as provided by the user.
Cfg Imaging
+
+ // Hash of the config map provided by the user.
+ CfgHash string
}
// Imaging contains default image processing configuration. This will be fetched
@@ -324,9 +379,15 @@ type Imaging struct {
// Default image quality setting (1-100). Only used for JPEG images.
Quality int
- // Resample filter to use in resize operations..
+ // Resample filter to use in resize operations.
ResampleFilter string
+ // Hint about what type of image this is.
+ // Currently only used when encoding to Webp.
+ // Default is "photo".
+ // Valid values are "picture", "photo", "drawing", "icon", or "text".
+ Hint string
+
// The anchor to use in Fill. Default is "smart", i.e. Smart Crop.
Anchor string
@@ -336,6 +397,19 @@ type Imaging struct {
Exif ExifConfig
}
+func (cfg *Imaging) init() error {
+ if cfg.Quality < 0 || cfg.Quality > 100 {
+ return errors.New("image quality must be a number between 1 and 100")
+ }
+
+ cfg.BgColor = strings.ToLower(strings.TrimPrefix(cfg.BgColor, "#"))
+ cfg.Anchor = strings.ToLower(cfg.Anchor)
+ cfg.ResampleFilter = strings.ToLower(cfg.ResampleFilter)
+ cfg.Hint = strings.ToLower(cfg.Hint)
+
+ return nil
+}
+
type ExifConfig struct {
// Regexp matching the Exif fields you want from the (massive) set of Exif info
diff --git a/resources/images/config_test.go b/resources/images/config_test.go
index 2a0de9ec0..7b2459250 100644
--- a/resources/images/config_test.go
+++ b/resources/images/config_test.go
@@ -42,7 +42,6 @@ func TestDecodeConfig(t *testing.T) {
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")
@@ -84,18 +83,22 @@ func TestDecodeImageConfig(t *testing.T) {
in string
expect interface{}
}{
- {"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", "")},
+ {"300x400", newImageConfig(300, 400, 75, 0, "box", "smart", "")},
+ {"300x400 #fff", newImageConfig(300, 400, 75, 0, "box", "smart", "fff")},
+ {"100x200 bottomRight", newImageConfig(100, 200, 75, 0, "box", "BottomRight", "")},
+ {"10x20 topleft Lanczos", newImageConfig(10, 20, 75, 0, "Lanczos", "topleft", "")},
+ {"linear left 10x r180", newImageConfig(10, 0, 75, 180, "linear", "left", "")},
{"x20 riGht Cosine q95", newImageConfig(0, 20, 95, 0, "cosine", "right", "")},
{"", false},
{"foo", false},
} {
- result, err := DecodeImageConfig("resize", this.in, Imaging{})
+ cfg, err := DecodeConfig(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ result, err := DecodeImageConfig("resize", this.in, cfg, PNG)
if b, ok := this.expect.(bool); ok && !b {
if err == nil {
t.Errorf("[%d] parseImageConfig didn't return an expected error", i)
@@ -112,11 +115,13 @@ func TestDecodeImageConfig(t *testing.T) {
}
func newImageConfig(width, height, quality, rotate int, filter, anchor, bgColor string) ImageConfig {
- var c ImageConfig
- c.Action = "resize"
+ var c ImageConfig = GetDefaultImageConfig("resize", ImagingConfig{})
+ c.TargetFormat = PNG
+ c.Hint = 2
c.Width = width
c.Height = height
c.Quality = quality
+ c.qualitySetForImage = quality != 75
c.Rotate = rotate
c.BgColorStr = bgColor
c.BgColor, _ = hexStringToColor(bgColor)
@@ -130,10 +135,14 @@ func newImageConfig(width, height, quality, rotate int, filter, anchor, bgColor
}
if anchor != "" {
- anchor = strings.ToLower(anchor)
- if v, ok := anchorPositions[anchor]; ok {
- c.Anchor = v
+ if anchor == smartCropIdentifier {
c.AnchorStr = anchor
+ } else {
+ anchor = strings.ToLower(anchor)
+ if v, ok := anchorPositions[anchor]; ok {
+ c.Anchor = v
+ c.AnchorStr = anchor
+ }
}
}
diff --git a/resources/images/image.go b/resources/images/image.go
index b71321244..db7d566a7 100644
--- a/resources/images/image.go
+++ b/resources/images/image.go
@@ -23,6 +23,9 @@ import (
"io"
"sync"
+ "github.com/bep/gowebp/libwebp/webpoptions"
+ "github.com/gohugoio/hugo/resources/images/webp"
+
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources/images/exif"
@@ -89,6 +92,15 @@ func (i *Image) EncodeTo(conf ImageConfig, img image.Image, w io.Writer) error {
case BMP:
return bmp.Encode(w, img)
+ case WEBP:
+ return webp.Encode(
+ w,
+ img, webpoptions.EncodingOptions{
+ Quality: conf.Quality,
+ EncodingPreset: webpoptions.EncodingPreset(conf.Hint),
+ UseSharpYuv: true,
+ },
+ )
default:
return errors.New("format not supported")
}
@@ -229,10 +241,11 @@ func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.
return dst, nil
}
-func (p *ImageProcessor) GetDefaultImageConfig(action string) ImageConfig {
+func GetDefaultImageConfig(action string, defaults ImagingConfig) ImageConfig {
return ImageConfig{
Action: action,
- Quality: p.Cfg.Cfg.Quality,
+ Hint: defaults.Hint,
+ Quality: defaults.Cfg.Quality,
}
}
@@ -250,11 +263,13 @@ const (
GIF
TIFF
BMP
+ WEBP
)
-// RequiresDefaultQuality returns if the default quality needs to be applied to images of this format
+// RequiresDefaultQuality returns if the default quality needs to be applied to
+// images of this format.
func (f Format) RequiresDefaultQuality() bool {
- return f == JPEG
+ return f == JPEG || f == WEBP
}
// SupportsTransparency reports whether it supports transparency in any form.
@@ -281,6 +296,8 @@ func (f Format) MediaType() media.Type {
return media.TIFFType
case BMP:
return media.BMPType
+ case WEBP:
+ return media.WEBPType
default:
panic(fmt.Sprintf("%d is not a valid image format", f))
}
diff --git a/resources/images/webp/webp.go b/resources/images/webp/webp.go
new file mode 100644
index 000000000..d7443ff23
--- /dev/null
+++ b/resources/images/webp/webp.go
@@ -0,0 +1,30 @@
+// Copyright 2021 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.
+
+// +build extended
+
+package webp
+
+import (
+ "image"
+ "io"
+
+ "github.com/bep/gowebp/libwebp"
+ "github.com/bep/gowebp/libwebp/webpoptions"
+)
+
+// Encode writes the Image m to w in Webp format with the given
+// options.
+func Encode(w io.Writer, m image.Image, o webpoptions.EncodingOptions) error {
+ return libwebp.Encode(w, m, o)
+}
diff --git a/resources/images/webp/webp_notavailable.go b/resources/images/webp/webp_notavailable.go
new file mode 100644
index 000000000..4209eb41a
--- /dev/null
+++ b/resources/images/webp/webp_notavailable.go
@@ -0,0 +1,30 @@
+// Copyright 2021 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.
+
+// +build !extended
+
+package webp
+
+import (
+ "image"
+ "io"
+
+ "github.com/gohugoio/hugo/common/herrors"
+
+ "github.com/bep/gowebp/libwebp/webpoptions"
+)
+
+// Encode is only available in the extended version.
+func Encode(w io.Writer, m image.Image, o webpoptions.EncodingOptions) error {
+ return herrors.ErrFeatureNotAvailable
+}