diff options
author | Bjørn Erik Pedersen <[email protected]> | 2021-04-07 16:49:34 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2021-04-15 17:22:55 +0200 |
commit | 33d5f805923eb50dfb309d024f6555c59a339846 (patch) | |
tree | cba8fde34849640bc09bd68d85c5d7a6ba042add /resources/images | |
parent | 509d39fa6ddbba106c127b7923a41b0dcaea9381 (diff) | |
download | hugo-33d5f805923eb50dfb309d024f6555c59a339846.tar.gz hugo-33d5f805923eb50dfb309d024f6555c59a339846.zip |
Add webp image encoding support
Fixes #5924
Diffstat (limited to 'resources/images')
-rw-r--r-- | resources/images/config.go | 182 | ||||
-rw-r--r-- | resources/images/config_test.go | 33 | ||||
-rw-r--r-- | resources/images/image.go | 25 | ||||
-rw-r--r-- | resources/images/webp/webp.go | 30 | ||||
-rw-r--r-- | resources/images/webp/webp_notavailable.go | 30 |
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 +} |