mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
parent
509d39fa6d
commit
33d5f80592
15 changed files with 344 additions and 117 deletions
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
3
go.mod
3
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
|
||||
|
|
16
go.sum
16
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=
|
||||
|
|
|
@ -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()?
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
41
resources/image_extended_test.go
Normal file
41
resources/image_extended_test.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
const (
|
||||
defaultJPEGQuality = 75
|
||||
defaultResampleFilter = "box"
|
||||
defaultBgColor = "ffffff"
|
||||
defaultHint = "photo"
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
if i.BgColor != "" {
|
||||
i.BgColor = strings.TrimPrefix(i.BgColor, "#")
|
||||
} else {
|
||||
i.BgColor = defaultBgColor
|
||||
}
|
||||
var err error
|
||||
ic.BgColor, err = hexStringToColor(i.BgColor)
|
||||
if err != nil {
|
||||
return ic, err
|
||||
}
|
||||
|
||||
if i.Anchor == "" || strings.EqualFold(i.Anchor, smartCropIdentifier) {
|
||||
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.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
|
||||
}
|
||||
|
||||
if strings.TrimSpace(i.Exif.IncludeFields) == "" && strings.TrimSpace(i.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"
|
||||
}
|
||||
|
||||
ic.Cfg = i
|
||||
|
||||
return ic, nil
|
||||
var defaultImaging = Imaging{
|
||||
ResampleFilter: defaultResampleFilter,
|
||||
BgColor: defaultBgColor,
|
||||
Hint: defaultHint,
|
||||
Quality: defaultJPEGQuality,
|
||||
}
|
||||
|
||||
func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, error) {
|
||||
func DecodeConfig(m map[string]interface{}) (ImagingConfig, error) {
|
||||
if m == nil {
|
||||
m = make(map[string]interface{})
|
||||
}
|
||||
|
||||
i := ImagingConfig{
|
||||
Cfg: defaultImaging,
|
||||
CfgHash: helpers.HashString(m),
|
||||
}
|
||||
|
||||
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
|
||||
i.BgColor, err = hexStringToColor(i.Cfg.BgColor)
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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.Cfg.Exif.IncludeFields) == "" && strings.TrimSpace(i.Cfg.Exif.ExcludeFields) == "" {
|
||||
// Don't change this for no good reason. Please don't.
|
||||
i.Cfg.Exif.ExcludeFields = "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance"
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
|
@ -313,9 +362,15 @@ func (i ImageConfig) GetKey(format Format) string {
|
|||
|
||||
type ImagingConfig struct {
|
||||
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
|
||||
|
|
|
@ -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,12 +135,16 @@ func newImageConfig(width, height, quality, rotate int, filter, anchor, bgColor
|
|||
}
|
||||
|
||||
if anchor != "" {
|
||||
if anchor == smartCropIdentifier {
|
||||
c.AnchorStr = anchor
|
||||
} else {
|
||||
anchor = strings.ToLower(anchor)
|
||||
if v, ok := anchorPositions[anchor]; ok {
|
||||
c.Anchor = v
|
||||
c.AnchorStr = anchor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
30
resources/images/webp/webp.go
Normal file
30
resources/images/webp/webp.go
Normal file
|
@ -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)
|
||||
}
|
30
resources/images/webp/webp_notavailable.go
Normal file
30
resources/images/webp/webp_notavailable.go
Normal file
|
@ -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
|
||||
}
|
BIN
resources/testdata/sunset.webp
vendored
Normal file
BIN
resources/testdata/sunset.webp
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
Loading…
Reference in a new issue