hugo

Unnamed repository; edit this file 'description' to name the repository.

git clone git://git.shimmy1996.com/hugo.git
commit 33d5f805923eb50dfb309d024f6555c59a339846
parent 509d39fa6ddbba106c127b7923a41b0dcaea9381
Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Date:   Wed,  7 Apr 2021 16:49:34 +0200

Add webp image encoding support

Fixes #5924

Diffstat:
Mcommon/hugo/hugo.go | 6+++++-
Mdocs/content/en/content-management/image-processing/index.md | 25++++++++++++++++++++++---
Mgo.mod | 3++-
Mgo.sum | 16++++++++++++++++
Mhugolib/image_test.go | 4++--
Mmedia/mediaType.go | 2++
Mmedia/mediaType_test.go | 2+-
Mresources/image.go | 28++++++----------------------
Aresources/image_extended_test.go | 41+++++++++++++++++++++++++++++++++++++++++
Mresources/images/config.go | 182+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mresources/images/config_test.go | 33+++++++++++++++++++++------------
Mresources/images/image.go | 25+++++++++++++++++++++----
Aresources/images/webp/webp.go | 30++++++++++++++++++++++++++++++
Aresources/images/webp/webp_notavailable.go | 30++++++++++++++++++++++++++++++
Aresources/testdata/sunset.webp | 0
15 files changed, 327 insertions(+), 100 deletions(-)
diff --git a/common/hugo/hugo.go b/common/hugo/hugo.go
@@ -120,7 +120,11 @@ func GetDependencyList() []string {
 	}
 
 	if IsExtended {
-		deps = append(deps, formatDep("github.com/sass/libsass", "3.6.4"))
+		deps = append(
+			deps,
+			formatDep("github.com/sass/libsass", "3.6.4"),
+			formatDep("github.com/webmproject/libwebp", "v1.2.0"),
+		)
 	}
 
 	bi, ok := debug.ReadBuildInfo()
diff --git a/docs/content/en/content-management/image-processing/index.md b/docs/content/en/content-management/image-processing/index.md
@@ -167,14 +167,28 @@ For color codes, see https://www.google.com/search?q=color+picker
 
 **Note** that you also set a default background color to use, see [Image Processing Config](#image-processing-config).
 
-### JPEG Quality
+### JPEG and Webp Quality
 
-Only relevant for JPEG images, values 1 to 100 inclusive, higher is better. Default is 75.
+Only relevant for JPEG and Webp images, values 1 to 100 inclusive, higher is better. Default is 75.
 
 ```go
 {{ $image.Resize "600x q50" }}
 ```
 
+{{< new-in "0.83.0" >}} Webp support was added in Hugo 0.83.0.
+
+### Hint {{< new-in "0.83.0" >}}
+
+Hint about what type of image this is. Currently only used when encoding to Webp.
+
+Default value is `photo`.
+
+Valid values are `picture`, `photo`, `drawing`, `icon`, or `text`.
+
+```go
+{{ $image.Resize "600x webp drawing" }}
+```
+
 ### 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.
@@ -258,9 +272,14 @@ You can configure an `imaging` section in `config.toml` with default image proce
 # See https://github.com/disintegration/imaging
 resampleFilter = "box"
 
-# Default JPEG quality setting. Default is 75.
+# Default JPEG or WEBP quality setting. Default is 75.
 quality = 75
 
+# Default hint about what type of image. Currently only used for Webp encoding.
+# Default is "photo".
+# Valid values are "picture", "photo", "drawing", "icon", or "text".
+hint = "photo"
+
 # Anchor used when cropping pictures.
 # Default is "smart" which does Smart Cropping, using https://github.com/muesli/smartcrop
 # Smart Cropping is content aware and tries to find the best crop for each image.
diff --git a/go.mod b/go.mod
@@ -13,6 +13,7 @@ require (
 	github.com/bep/gitmap v1.1.2
 	github.com/bep/godartsass v0.12.0
 	github.com/bep/golibsass v0.7.0
+	github.com/bep/gowebp v0.1.0 // indirect
 	github.com/bep/tmc v0.5.1
 	github.com/cli/safeexec v1.0.0
 	github.com/disintegration/gift v1.2.1
@@ -59,7 +60,7 @@ require (
 	github.com/yuin/goldmark v1.3.2
 	github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691
 	gocloud.dev v0.20.0
-	golang.org/x/image v0.0.0-20191214001246-9130b4cfad52
+	golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
 	golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 	golang.org/x/text v0.3.5
diff --git a/go.sum b/go.sum
@@ -134,6 +134,20 @@ github.com/bep/godartsass v0.12.0 h1:VvGLA4XpXUjKvp53SI05YFLhRFJ78G+Ybnlaz6Oul7E
 github.com/bep/godartsass v0.12.0/go.mod h1:nXQlHHk4H1ghUk6n/JkYKG5RD43yJfcfp5aHRqT/pc4=
 github.com/bep/golibsass v0.7.0 h1:/ocxgtPZ5rgp7FA+mktzyent+fAg82tJq4iMsTMBAtA=
 github.com/bep/golibsass v0.7.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
+github.com/bep/gowebp v0.0.0-20210408171434-03ecbe0b5d53 h1:bTIhFx2ZEAZD74LwuVdrdZ4070bE9UE5oR5NTBYLtVs=
+github.com/bep/gowebp v0.0.0-20210408171434-03ecbe0b5d53/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI=
+github.com/bep/gowebp v0.0.0-20210409123354-5e38121e4f6b h1:LLrQFlG0VSxmyz3izTUQnPOGf7Mjiy7wlEu2sDLA+qg=
+github.com/bep/gowebp v0.0.0-20210409123354-5e38121e4f6b/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI=
+github.com/bep/gowebp v0.0.0-20210410152255-50a32861b5a2 h1:uEpPD0fLZs5IjgF/96LqWHUNY9Pr/0KqLWIQ4gJnYhY=
+github.com/bep/gowebp v0.0.0-20210410152255-50a32861b5a2/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI=
+github.com/bep/gowebp v0.0.0-20210410161412-b86a3337b39f h1:hvhG2nwoIvHhFnL8GnYtOquHE6dG+mHwthugLqf4spY=
+github.com/bep/gowebp v0.0.0-20210410161412-b86a3337b39f/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI=
+github.com/bep/gowebp v0.0.0-20210411110227-3a211f6b6461 h1:5HLIo8LF4iKFdxPBDo9CO8oTac18mAx7FJsQG6MNbCU=
+github.com/bep/gowebp v0.0.0-20210411110227-3a211f6b6461/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI=
+github.com/bep/gowebp v0.0.0-20210411155607-38d8f20d562b h1:VIW6UmIG4ogbswbDFBjVm6/7j9I5i0GouDJ2USn/NUI=
+github.com/bep/gowebp v0.0.0-20210411155607-38d8f20d562b/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI=
+github.com/bep/gowebp v0.1.0 h1:4/iQpfnxHyXs3x/aTxMMdOpLEQQhFmF6G7EieWPTQyo=
+github.com/bep/gowebp v0.1.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI=
 github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI=
 github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
@@ -566,6 +580,8 @@ golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMx
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/image v0.0.0-20191214001246-9130b4cfad52 h1:2fktqPPvDiVEEVT/vSTeoUPXfmRxRaGy6GU8jypvEn0=
 golang.org/x/image v0.0.0-20191214001246-9130b4cfad52/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
+golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
diff --git a/hugolib/image_test.go b/hugolib/image_test.go
@@ -236,10 +236,10 @@ 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_7645215769587362592.json",
+	b.AssertFileContent("resources/_gen/images/bundle/sunset_3166614710256882113.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_7645215769587362592.json",
+	b.AssertFileContent("resources/_gen/images/sunset_3166614710256882113.json",
 		"DateTimeDigitized|time.Time", "PENTAX")
 
 	// TODO(bep) add this as a default assertion after Build()?
diff --git a/media/mediaType.go b/media/mediaType.go
@@ -180,6 +180,7 @@ var (
 	GIFType  = newMediaType("image", "gif", []string{"gif"})
 	TIFFType = newMediaType("image", "tiff", []string{"tif", "tiff"})
 	BMPType  = newMediaType("image", "bmp", []string{"bmp"})
+	WEBPType = newMediaType("image", "webp", []string{"webp"})
 
 	// Common video types
 	AVIType  = newMediaType("video", "x-msvideo", []string{"avi"})
@@ -214,6 +215,7 @@ var DefaultTypes = Types{
 	TOMLType,
 	PNGType,
 	JPEGType,
+	WEBPType,
 	AVIType,
 	MPEGType,
 	MP4Type,
diff --git a/media/mediaType_test.go b/media/mediaType_test.go
@@ -55,7 +55,7 @@ func TestDefaultTypes(t *testing.T) {
 
 	}
 
-	c.Assert(len(DefaultTypes), qt.Equals, 26)
+	c.Assert(len(DefaultTypes), qt.Equals, 27)
 }
 
 func TestGetByType(t *testing.T) {
diff --git a/resources/image.go b/resources/image.go
@@ -207,7 +207,7 @@ func (i *imageResource) Fill(spec string) (resource.Image, error) {
 }
 
 func (i *imageResource) Filter(filters ...interface{}) (resource.Image, error) {
-	conf := i.Proc.GetDefaultImageConfig("filter")
+	conf := images.GetDefaultImageConfig("filter", i.Proc.Cfg)
 
 	var gfilters []gift.Filter
 
@@ -299,28 +299,11 @@ 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.Cfg)
+	conf, err := images.DecodeImageConfig(action, spec, i.Proc.Cfg, i.Format)
 	if err != nil {
 		return conf, err
 	}
 
-	// default to the source format
-	if conf.TargetFormat == 0 {
-		conf.TargetFormat = i.Format
-	}
-
-	if conf.Quality <= 0 && conf.TargetFormat.RequiresDefaultQuality() {
-		// We need a quality setting for all JPEGs
-		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
 }
 
@@ -360,15 +343,16 @@ 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
+	cfgHash := i.getSpec().imaging.Cfg.CfgHash
 	df := i.getResourcePaths().relTargetDirFile
 	if fi := i.getFileInfo(); fi != nil {
 		df.dir = filepath.Dir(fi.Meta().Path())
 	}
 	p1, _ := helpers.FileAndExt(df.file)
 	h, _ := i.hash()
-	idStr := helpers.HashString(h, i.size(), imageMetaVersionNumber, cfg)
-	return path.Join(df.dir, fmt.Sprintf("%s_%s.json", p1, idStr))
+	idStr := helpers.HashString(h, i.size(), imageMetaVersionNumber, cfgHash)
+	p := path.Join(df.dir, fmt.Sprintf("%s_%s.json", p1, idStr))
+	return p
 }
 
 func (i *imageResource) relTargetPathFromConfig(conf images.ImageConfig) dirFile {
diff --git a/resources/image_extended_test.go b/resources/image_extended_test.go
@@ -0,0 +1,41 @@
+// 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.
+
+// +build extended
+
+package resources
+
+import (
+	"testing"
+
+	"github.com/gohugoio/hugo/media"
+
+	qt "github.com/frankban/quicktest"
+)
+
+func TestImageResizeWebP(t *testing.T) {
+	c := qt.New(t)
+
+	image := fetchImage(c, "sunset.webp")
+
+	c.Assert(image.MediaType(), qt.Equals, media.WEBPType)
+	c.Assert(image.RelPermalink(), qt.Equals, "/a/sunset.webp")
+	c.Assert(image.ResourceType(), qt.Equals, "image")
+	c.Assert(image.Exif(), qt.IsNil)
+
+	resized, err := image.Resize("123x")
+	c.Assert(err, qt.IsNil)
+	c.Assert(image.MediaType(), qt.Equals, media.WEBPType)
+	c.Assert(resized.RelPermalink(), qt.Equals, "/a/sunset_hu36ee0b61ba924719ad36da960c273f96_59826_123x0_resize_q68_h2_linear.webp")
+	c.Assert(resized.Width(), qt.Equals, 123)
+}
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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
+}
diff --git a/resources/testdata/sunset.webp b/resources/testdata/sunset.webp
Binary files differ.