aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--common/hugo/hugo.go6
-rw-r--r--docs/content/en/content-management/image-processing/index.md25
-rw-r--r--go.mod3
-rw-r--r--go.sum16
-rw-r--r--hugolib/image_test.go4
-rw-r--r--media/mediaType.go2
-rw-r--r--media/mediaType_test.go2
-rw-r--r--resources/image.go28
-rw-r--r--resources/image_extended_test.go41
-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
-rw-r--r--resources/testdata/sunset.webpbin0 -> 59826 bytes
15 files changed, 327 insertions, 100 deletions
diff --git a/common/hugo/hugo.go b/common/hugo/hugo.go
index 6e94a8a33..d8d1c7c12 100644
--- 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
index a432b9851..8cd00210f 100644
--- 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
index 781b53c12..187842a8c 100644
--- 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
index a19984430..d2b71844e 100644
--- 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
index 1d1520460..0dacf2a33 100644
--- 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
index a35d80e3e..164ad5fd2 100644
--- 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
index e44ab27ec..6bc42b3d4 100644
--- 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
index 0396c2208..edf05639f 100644
--- 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
new file mode 100644
index 000000000..9fd9304d9
--- /dev/null
+++ 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
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
+}
diff --git a/resources/testdata/sunset.webp b/resources/testdata/sunset.webp
new file mode 100644
index 000000000..4365e7b9f
--- /dev/null
+++ b/resources/testdata/sunset.webp
Binary files differ