mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
a043d3ec63
The hint setting controls WebP encoding (preset) and AVIF encoding (chroma subsampling), but only lived on imaging.webp. Add imaging.avif.hint so it shows up under the AVIF section in the docs, with the same root-level backwards compatibility as imaging.webp.hint. Per-image hint now resolves from the target format. Fixes #14992 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
745 lines
22 KiB
Go
745 lines
22 KiB
Go
// 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.
|
|
|
|
package images
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"image/color"
|
|
"maps"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gohugoio/hugo/common/hashing"
|
|
"github.com/gohugoio/hugo/common/hmaps"
|
|
"github.com/gohugoio/hugo/config"
|
|
"github.com/gohugoio/hugo/media"
|
|
"github.com/mitchellh/mapstructure"
|
|
|
|
"github.com/gohugoio/gift"
|
|
)
|
|
|
|
const (
|
|
ActionResize = "resize"
|
|
ActionCrop = "crop"
|
|
ActionFit = "fit"
|
|
ActionFill = "fill"
|
|
)
|
|
|
|
var Actions = map[string]bool{
|
|
ActionResize: true,
|
|
ActionCrop: true,
|
|
ActionFit: true,
|
|
ActionFill: true,
|
|
}
|
|
|
|
var (
|
|
imageFormats = map[string]Format{
|
|
".jpg": JPEG,
|
|
".jpeg": JPEG,
|
|
".jpe": JPEG,
|
|
".jif": JPEG,
|
|
".jfif": JPEG,
|
|
".png": PNG,
|
|
".tif": TIFF,
|
|
".tiff": TIFF,
|
|
".bmp": BMP,
|
|
".gif": GIF,
|
|
".webp": WEBP,
|
|
".avif": AVIF,
|
|
".heif": HEIF,
|
|
".heic": HEIC,
|
|
}
|
|
|
|
// These are the image types we can process.
|
|
processableImageSubTypes = map[string]Format{
|
|
media.Builtin.JPEGType.SubType: JPEG,
|
|
media.Builtin.PNGType.SubType: PNG,
|
|
media.Builtin.TIFFType.SubType: TIFF,
|
|
media.Builtin.BMPType.SubType: BMP,
|
|
media.Builtin.GIFType.SubType: GIF,
|
|
media.Builtin.WEBPType.SubType: WEBP,
|
|
media.Builtin.AVIFType.SubType: AVIF,
|
|
}
|
|
|
|
// We cannot process these formats, but we can provide metadata support for them (including width/height).
|
|
metaOnlyImageSubTypes = map[string]Format{
|
|
media.Builtin.AVIFType.SubType: AVIF,
|
|
media.Builtin.HEIFType.SubType: HEIF,
|
|
media.Builtin.HEICType.SubType: HEIC,
|
|
}
|
|
|
|
// Increment to mark all processed images as stale. Only use when absolutely needed.
|
|
// See the finer grained smartCropVersionNumber.
|
|
mainImageVersionNumber = 1
|
|
)
|
|
|
|
var anchorPositions = map[string]gift.Anchor{
|
|
strings.ToLower("Center"): gift.CenterAnchor,
|
|
strings.ToLower("TopLeft"): gift.TopLeftAnchor,
|
|
strings.ToLower("Top"): gift.TopAnchor,
|
|
strings.ToLower("TopRight"): gift.TopRightAnchor,
|
|
strings.ToLower("Left"): gift.LeftAnchor,
|
|
strings.ToLower("Right"): gift.RightAnchor,
|
|
strings.ToLower("BottomLeft"): gift.BottomLeftAnchor,
|
|
strings.ToLower("Bottom"): gift.BottomAnchor,
|
|
strings.ToLower("BottomRight"): gift.BottomRightAnchor,
|
|
smartCropIdentifier: SmartCropAnchor,
|
|
}
|
|
|
|
var compressionMethods = map[string]bool{
|
|
"lossy": true,
|
|
"lossless": true,
|
|
}
|
|
|
|
// These encoding hints are used by Webp (preset) and Avif (chroma subsampling).
|
|
var hints = map[string]bool{
|
|
"picture": true,
|
|
"photo": true,
|
|
"drawing": true,
|
|
"icon": true,
|
|
"text": true,
|
|
}
|
|
|
|
var imageFilters = map[string]gift.Resampling{
|
|
strings.ToLower("NearestNeighbor"): gift.NearestNeighborResampling,
|
|
strings.ToLower("Box"): gift.BoxResampling,
|
|
strings.ToLower("Linear"): gift.LinearResampling,
|
|
strings.ToLower("Hermite"): hermiteResampling,
|
|
strings.ToLower("MitchellNetravali"): mitchellNetravaliResampling,
|
|
strings.ToLower("CatmullRom"): catmullRomResampling,
|
|
strings.ToLower("BSpline"): bSplineResampling,
|
|
strings.ToLower("Gaussian"): gaussianResampling,
|
|
strings.ToLower("Lanczos"): gift.LanczosResampling,
|
|
strings.ToLower("Hann"): hannResampling,
|
|
strings.ToLower("Hamming"): hammingResampling,
|
|
strings.ToLower("Blackman"): blackmanResampling,
|
|
strings.ToLower("Bartlett"): bartlettResampling,
|
|
strings.ToLower("Welch"): welchResampling,
|
|
strings.ToLower("Cosine"): cosineResampling,
|
|
}
|
|
|
|
func ImageFormatFromExt(ext string) (Format, bool) {
|
|
f, found := imageFormats[ext]
|
|
return f, found
|
|
}
|
|
|
|
type ImageResourceType int
|
|
|
|
const (
|
|
// ImageResourceTypeNone means that the resource is not an image, and thus does not support any image operations.
|
|
ImageResourceTypeNone ImageResourceType = iota
|
|
// This is an image, but with no support for any image operations.
|
|
ImageResourceTypeBasic
|
|
// ImageResourceTypeMetaOnly means that only metadata operations (e.g. getting width/height and other metadata) are supported for this format.
|
|
ImageResourceTypeMetaOnly
|
|
// ImageResourceTypeProcessable means that all image operations (resizing, cropping, etc.) are supported for this format.
|
|
ImageResourceTypeProcessable
|
|
)
|
|
|
|
// ImageFormatFromMediaSubType returns the image format for the given media subtype, and how much image processing operations are supported for this format.
|
|
func ImageFormatFromMediaSubType(sub string) (Format, ImageResourceType) {
|
|
f, found := processableImageSubTypes[sub]
|
|
if found {
|
|
return f, ImageResourceTypeProcessable
|
|
}
|
|
if f, found = metaOnlyImageSubTypes[sub]; found {
|
|
return f, ImageResourceTypeMetaOnly
|
|
}
|
|
return f, ImageResourceTypeBasic
|
|
}
|
|
|
|
const (
|
|
defaultJPEGQuality = 75
|
|
defaultAVIFQuality = 60
|
|
defaultResampleFilter = "box"
|
|
defaultBgColor = "#ffffff"
|
|
defaultHint = "photo"
|
|
defaultCompression = "lossy"
|
|
defaultWebpUseSharpYuv = false
|
|
defaultWebpMethod = 2
|
|
defaultAvifEncoderSpeed = 10
|
|
)
|
|
|
|
var (
|
|
defaultImaging = map[string]any{
|
|
"resampleFilter": defaultResampleFilter,
|
|
"bgColor": defaultBgColor,
|
|
"hint": defaultHint,
|
|
"compression": defaultCompression,
|
|
"webp": map[string]any{
|
|
"useSharpYuv": defaultWebpUseSharpYuv,
|
|
"method": defaultWebpMethod,
|
|
},
|
|
"avif": map[string]any{
|
|
"encoderSpeed": defaultAvifEncoderSpeed,
|
|
},
|
|
}
|
|
|
|
defaultImageConfig *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]
|
|
)
|
|
|
|
func init() {
|
|
var err error
|
|
defaultImageConfig, err = DecodeConfig(defaultImaging)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, ImagingConfigInternal], error) {
|
|
if in == nil {
|
|
in = make(map[string]any)
|
|
}
|
|
|
|
buildConfig := func(in any) (ImagingConfigInternal, any, error) {
|
|
m, err := hmaps.ToStringMapE(in)
|
|
if err != nil {
|
|
return ImagingConfigInternal{}, nil, err
|
|
}
|
|
_, qualityExplicit := m["quality"]
|
|
// Merge in the defaults.
|
|
hmaps.MergeShallow(m, defaultImaging)
|
|
|
|
// Deep merge webp defaults.
|
|
if webp, ok := m["webp"].(map[string]any); ok {
|
|
hmaps.MergeShallow(webp, defaultImaging["webp"].(map[string]any))
|
|
}
|
|
|
|
// Deep merge avif defaults.
|
|
if avif, ok := m["avif"].(map[string]any); ok {
|
|
hmaps.MergeShallow(avif, defaultImaging["avif"].(map[string]any))
|
|
}
|
|
|
|
var i ImagingConfigInternal
|
|
if err := mapstructure.Decode(m, &i.Imaging); err != nil {
|
|
return i, nil, err
|
|
}
|
|
i.Imaging.qualityExplicit = qualityExplicit
|
|
|
|
if err := i.Imaging.init(); err != nil {
|
|
return i, nil, err
|
|
}
|
|
|
|
i.BgColor, err = hexStringToColorGo(i.Imaging.BgColor)
|
|
if err != nil {
|
|
return i, nil, err
|
|
}
|
|
|
|
if i.Imaging.Anchor != "" {
|
|
anchor, found := anchorPositions[i.Imaging.Anchor]
|
|
if !found {
|
|
return i, nil, fmt.Errorf("invalid anchor value %q in imaging config", i.Anchor)
|
|
}
|
|
i.Anchor = anchor
|
|
}
|
|
|
|
filter, found := imageFilters[i.Imaging.ResampleFilter]
|
|
if !found {
|
|
return i, nil, fmt.Errorf("%q is not a valid resample filter", filter)
|
|
}
|
|
|
|
i.ResampleFilter = filter
|
|
|
|
return i, i.Imaging, nil
|
|
}
|
|
|
|
ns, err := config.DecodeNamespace[ImagingConfig](in, buildConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ns, nil
|
|
}
|
|
|
|
func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal], sourceFormat Format) (ImageConfig, error) {
|
|
var (
|
|
c ImageConfig = GetDefaultImageConfig(defaults)
|
|
err error
|
|
qualitySet bool
|
|
)
|
|
|
|
// Make to lower case, trim space and remove any empty strings.
|
|
n := 0
|
|
for _, s := range options {
|
|
s = strings.TrimSpace(s)
|
|
if s != "" {
|
|
options[n] = strings.ToLower(s)
|
|
n++
|
|
}
|
|
}
|
|
options = options[:n]
|
|
|
|
for _, part := range options {
|
|
if _, ok := Actions[part]; ok {
|
|
c.Action = part
|
|
} else if pos, ok := anchorPositions[part]; ok {
|
|
c.Anchor = pos
|
|
} else if filter, ok := imageFilters[part]; ok {
|
|
c.Filter = filter
|
|
} else if _, ok := hints[part]; ok {
|
|
c.Hint = part
|
|
} else if _, ok := compressionMethods[part]; ok {
|
|
c.Compression = part
|
|
} else if f, ok := ImageFormatFromExt("." + part); ok {
|
|
c.TargetFormat = f
|
|
} else if part[0] == '#' {
|
|
c.BgColor, err = hexStringToColorGo(part[1:])
|
|
if err != nil {
|
|
return c, err
|
|
}
|
|
} else if part[0] == 'q' {
|
|
c.Quality, err = strconv.Atoi(part[1:])
|
|
if err != nil {
|
|
return c, err
|
|
}
|
|
if c.Quality < 1 || c.Quality > 100 {
|
|
return c, errors.New("quality ranges from 1 to 100 inclusive")
|
|
}
|
|
qualitySet = true
|
|
} else if part[0] == 'r' {
|
|
c.Rotate, err = strconv.Atoi(part[1:])
|
|
if err != nil {
|
|
return c, err
|
|
}
|
|
} else if strings.Contains(part, "x") {
|
|
widthHeight := strings.Split(part, "x")
|
|
if len(widthHeight) <= 2 {
|
|
first := widthHeight[0]
|
|
if first != "" {
|
|
c.Width, err = strconv.Atoi(first)
|
|
if err != nil {
|
|
return c, err
|
|
}
|
|
}
|
|
|
|
if len(widthHeight) == 2 {
|
|
second := widthHeight[1]
|
|
if second != "" {
|
|
c.Height, err = strconv.Atoi(second)
|
|
if err != nil {
|
|
return c, err
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return c, errors.New("invalid image dimensions")
|
|
}
|
|
}
|
|
}
|
|
|
|
switch c.Action {
|
|
case ActionCrop, ActionFill, ActionFit:
|
|
if c.Width == 0 || c.Height == 0 {
|
|
return c, errors.New("must provide Width and Height")
|
|
}
|
|
case ActionResize:
|
|
if c.Width == 0 && c.Height == 0 {
|
|
return c, errors.New("must provide Width or Height")
|
|
}
|
|
default:
|
|
if c.Width != 0 || c.Height != 0 {
|
|
return c, errors.New("width or height are not supported for this action")
|
|
}
|
|
}
|
|
|
|
if c.Action != "" && c.Filter == nil {
|
|
c.Filter = defaults.Config.ResampleFilter
|
|
}
|
|
|
|
if c.Action != "" && c.Anchor == -1 {
|
|
c.Anchor = defaults.Config.Anchor
|
|
}
|
|
|
|
// default to the source format
|
|
if c.TargetFormat == 0 {
|
|
c.TargetFormat = sourceFormat
|
|
}
|
|
|
|
if c.Hint == "" {
|
|
c.Hint = defaults.Config.Imaging.hintFor(c.TargetFormat)
|
|
}
|
|
|
|
if !qualitySet && c.TargetFormat.RequiresDefaultQuality() {
|
|
// Apply the per-format (or global) default quality unless the user
|
|
// explicitly set a quality for this image operation.
|
|
c.Quality = defaults.Config.Imaging.qualityFor(c.TargetFormat)
|
|
}
|
|
|
|
if c.Compression == "" {
|
|
c.Compression = defaults.Config.Imaging.Compression
|
|
}
|
|
|
|
if c.BgColor == nil && c.TargetFormat != sourceFormat {
|
|
if sourceFormat.SupportsTransparency() && !c.TargetFormat.SupportsTransparency() {
|
|
c.BgColor = defaults.Config.BgColor
|
|
}
|
|
}
|
|
|
|
if mainImageVersionNumber > 0 {
|
|
options = append(options, strconv.Itoa(mainImageVersionNumber))
|
|
}
|
|
|
|
usesSmartCrop := c.Anchor == SmartCropAnchor && (c.Action == ActionCrop || c.Action == ActionFill)
|
|
if smartCropVersionNumber > 0 && usesSmartCrop {
|
|
options = append(options, strconv.Itoa(smartCropVersionNumber))
|
|
}
|
|
|
|
c.Key = hashing.HashStringHex(options)
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// 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.
|
|
TargetFormat Format
|
|
|
|
Action string
|
|
|
|
// If set, this will be used as the key in filenames etc.
|
|
Key string
|
|
|
|
// Quality ranges from 1 to 100 inclusive, higher is better.
|
|
// This is only relevant for JPEG and WEBP images.
|
|
// For WEBP it's only relevant for lossy encoding.
|
|
// Default is 75.
|
|
Quality int
|
|
|
|
// Rotate rotates an image by the given angle counter-clockwise.
|
|
// The rotation will be performed first.
|
|
Rotate int
|
|
|
|
// Used to fill any transparency.
|
|
// When set in project config, it's used when converting to a format that does
|
|
// not support transparency.
|
|
// When set per image operation, it's used even for formats that does support
|
|
// transparency.
|
|
BgColor color.Color
|
|
|
|
// Hint about what type of picture this is. Used to optimize encoding
|
|
// when target is webp (preset) or avif (chroma subsampling).
|
|
Hint string
|
|
|
|
Compression string
|
|
|
|
// WebP-specific options.
|
|
UseSharpYuv bool
|
|
Method int
|
|
|
|
// AVIF-specific options.
|
|
EncoderSpeed int
|
|
|
|
Width int
|
|
Height int
|
|
|
|
Filter gift.Resampling
|
|
|
|
Anchor gift.Anchor
|
|
}
|
|
|
|
func (cfg ImageConfig) Reanchor(a gift.Anchor) ImageConfig {
|
|
cfg.Anchor = a
|
|
cfg.Key = hashing.HashStringHex(cfg.Key, "reanchor", a)
|
|
return cfg
|
|
}
|
|
|
|
type ImagingConfigInternal struct {
|
|
BgColor color.Color
|
|
ResampleFilter gift.Resampling
|
|
Anchor gift.Anchor
|
|
|
|
Imaging ImagingConfig
|
|
}
|
|
|
|
func (i *ImagingConfigInternal) Compile(externalCfg *ImagingConfig) error {
|
|
var err error
|
|
i.BgColor, err = hexStringToColorGo(externalCfg.BgColor)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if externalCfg.Anchor != "" {
|
|
anchor, found := anchorPositions[externalCfg.Anchor]
|
|
if !found {
|
|
return fmt.Errorf("invalid anchor value %q in imaging config", i.Anchor)
|
|
}
|
|
i.Anchor = anchor
|
|
}
|
|
|
|
filter, found := imageFilters[externalCfg.ResampleFilter]
|
|
if !found {
|
|
return fmt.Errorf("%q is not a valid resample filter", filter)
|
|
}
|
|
i.ResampleFilter = filter
|
|
|
|
return nil
|
|
}
|
|
|
|
// ImagingConfig contains default image processing configuration. This will be fetched
|
|
// from site (or language) config.
|
|
type ImagingConfig struct {
|
|
// Default image quality setting (1-100). Used as the fallback for JPEG,
|
|
// WebP and AVIF when no per-format quality is set. When left unset, JPEG
|
|
// and WebP default to 75 and AVIF to 60 (its scale differs perceptually).
|
|
// Deprecated in v0.164.0: set the quality per format instead, see
|
|
// imaging.jpeg.quality, imaging.webp.quality and imaging.avif.quality.
|
|
Quality int
|
|
|
|
// Whether Quality was explicitly set in the config. When false, AVIF uses
|
|
// its own lower default instead of the global Quality.
|
|
qualityExplicit bool
|
|
|
|
// Compression method to use.
|
|
// One of "lossy" or "lossless".
|
|
// Note that lossless is currently only supported for WebP and AVIF.
|
|
Compression string
|
|
|
|
// Resample filter to use in resize operations.
|
|
ResampleFilter string
|
|
|
|
// Hint about what type of image this is.
|
|
// Used when encoding to Webp (preset) and Avif (chroma subsampling).
|
|
// Default is "photo".
|
|
// Valid values are "picture", "photo", "drawing", "icon", or "text".
|
|
// Moved to WebpConfig in v0.155.0, but kept here for backwards compatibility.
|
|
Hint string `json:"-"`
|
|
|
|
// The anchor to use in Fill. Default is "smart", i.e. Smart Crop.
|
|
Anchor string
|
|
|
|
// Default color used in fill operations (e.g. "fff" for white).
|
|
BgColor string
|
|
|
|
Exif ExifConfig
|
|
Meta MetaConfig
|
|
Jpeg JpegConfig
|
|
Webp WebpConfig
|
|
Avif AvifConfig
|
|
}
|
|
|
|
// qualityFor returns the configured quality for the given target format,
|
|
// falling back to the global Quality when no per-format quality is set.
|
|
func (cfg *ImagingConfig) qualityFor(f Format) int {
|
|
var q int
|
|
switch f {
|
|
case JPEG:
|
|
q = cfg.Jpeg.Quality
|
|
case WEBP:
|
|
q = cfg.Webp.Quality
|
|
case AVIF:
|
|
q = cfg.Avif.Quality
|
|
}
|
|
if q > 0 {
|
|
return q
|
|
}
|
|
if f == AVIF && !cfg.qualityExplicit {
|
|
return defaultAVIFQuality
|
|
}
|
|
return cfg.Quality
|
|
}
|
|
|
|
// hintFor returns the configured hint for the given target format.
|
|
func (cfg *ImagingConfig) hintFor(f Format) string {
|
|
if f == AVIF {
|
|
return cfg.Avif.Hint
|
|
}
|
|
return cfg.Webp.Hint
|
|
}
|
|
|
|
var validMetaSources = map[string]bool{
|
|
"exif": true,
|
|
"iptc": true,
|
|
"xmp": true,
|
|
}
|
|
|
|
func (cfg *ImagingConfig) init() error {
|
|
if cfg.Quality == 0 && !cfg.qualityExplicit {
|
|
cfg.Quality = defaultJPEGQuality
|
|
}
|
|
if cfg.Quality < 1 || 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)
|
|
cfg.Compression = strings.ToLower(cfg.Compression)
|
|
|
|
if cfg.Anchor == "" {
|
|
cfg.Anchor = smartCropIdentifier
|
|
}
|
|
|
|
if strings.TrimSpace(cfg.Exif.IncludeFields) == "" && strings.TrimSpace(cfg.Exif.ExcludeFields) == "" {
|
|
// Don't change this for no good reason. Please don't.
|
|
cfg.Exif.ExcludeFields = "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance"
|
|
}
|
|
|
|
if len(cfg.Meta.Fields) == 0 {
|
|
// Default: include all fields except technical metadata.
|
|
// Don't change this for no good reason. Please don't.
|
|
cfg.Meta.Fields = []string{
|
|
"! *{GPS,Exif,Exposure[MPB],Contrast,Resolution,Sharp,JPEG,Metering,Sensing,Saturation,ColorSpace,Flash,WhiteBalance}*",
|
|
}
|
|
}
|
|
|
|
if len(cfg.Meta.Sources) == 0 {
|
|
// Default to EXIF and IPTC (XMP is slower to decode).
|
|
cfg.Meta.Sources = []string{"exif", "iptc"}
|
|
} else {
|
|
// Normalize to lowercase.
|
|
for i, s := range cfg.Meta.Sources {
|
|
cfg.Meta.Sources[i] = strings.ToLower(s)
|
|
if !validMetaSources[cfg.Meta.Sources[i]] {
|
|
keys := slices.Collect(maps.Keys(validMetaSources))
|
|
slices.Sort(keys)
|
|
return fmt.Errorf("invalid metadata source %q in imaging.meta.sources config; must be one of %s", s, keys)
|
|
}
|
|
}
|
|
}
|
|
|
|
// WebP config with backwards compatibility for root-level Hint.
|
|
cfg.Webp.Hint = strings.ToLower(cfg.Webp.Hint)
|
|
if cfg.Webp.Hint == "" {
|
|
// Fall back to root-level hint for backwards compatibility.
|
|
if cfg.Hint != "" {
|
|
cfg.Webp.Hint = cfg.Hint
|
|
} else {
|
|
cfg.Webp.Hint = defaultHint
|
|
}
|
|
}
|
|
if cfg.Webp.Hint != "" && !hints[cfg.Webp.Hint] {
|
|
return fmt.Errorf("invalid webp hint %q; must be one of picture, photo, drawing, icon, or text", cfg.Webp.Hint)
|
|
}
|
|
if cfg.Webp.Method == 0 {
|
|
cfg.Webp.Method = defaultWebpMethod
|
|
}
|
|
if cfg.Webp.Method < 0 || cfg.Webp.Method > 6 {
|
|
return fmt.Errorf("webp method must be between 0 and 6, got %d", cfg.Webp.Method)
|
|
}
|
|
|
|
for name, q := range map[string]int{"jpeg": cfg.Jpeg.Quality, "webp": cfg.Webp.Quality, "avif": cfg.Avif.Quality} {
|
|
if q != 0 && (q < 1 || q > 100) {
|
|
return fmt.Errorf("%s quality must be a number between 1 and 100", name)
|
|
}
|
|
}
|
|
|
|
// AVIF config with backwards compatibility for root-level Hint.
|
|
cfg.Avif.Hint = strings.ToLower(cfg.Avif.Hint)
|
|
if cfg.Avif.Hint == "" {
|
|
// Fall back to root-level hint for backwards compatibility.
|
|
if cfg.Hint != "" {
|
|
cfg.Avif.Hint = cfg.Hint
|
|
} else {
|
|
cfg.Avif.Hint = defaultHint
|
|
}
|
|
}
|
|
if !hints[cfg.Avif.Hint] {
|
|
return fmt.Errorf("invalid avif hint %q; must be one of picture, photo, drawing, icon, or text", cfg.Avif.Hint)
|
|
}
|
|
|
|
if cfg.Avif.EncoderSpeed == 0 {
|
|
cfg.Avif.EncoderSpeed = defaultAvifEncoderSpeed
|
|
}
|
|
if cfg.Avif.EncoderSpeed < 1 || cfg.Avif.EncoderSpeed > 10 {
|
|
return fmt.Errorf("avif encoderSpeed must be between 1 and 10, got %d", cfg.Avif.EncoderSpeed)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type ExifConfig struct {
|
|
// Regexp matching the Exif fields you want from the (massive) set of Exif info
|
|
// available. As we cache this info to disk, this is for performance and
|
|
// disk space reasons more than anything.
|
|
// If you want it all, put ".*" in this config setting.
|
|
// Note that if neither this or ExcludeFields is set, Hugo will return a small
|
|
// default set.
|
|
IncludeFields string
|
|
|
|
// Regexp matching the Exif fields you want to exclude. This may be easier to use
|
|
// than IncludeFields above, depending on what you want.
|
|
ExcludeFields string
|
|
|
|
// Hugo extracts the "photo taken" date/time into .Date by default.
|
|
// Set this to true to turn it off.
|
|
DisableDate bool
|
|
|
|
// Hugo extracts the "photo taken where" (GPS latitude and longitude) into
|
|
// .Long and .Lat. Set this to true to turn it off.
|
|
DisableLatLong bool
|
|
}
|
|
|
|
type MetaConfig struct {
|
|
// Glob patterns for which metadata fields to include.
|
|
// Use "! " prefix to exclude patterns (e.g., "! *GPS*" excludes GPS fields).
|
|
// Patterns are OR'd together for inclusion, AND'd for exclusion.
|
|
// If empty, a default set excluding technical metadata is used.
|
|
// Use ["**"] to include all fields.
|
|
Fields []string
|
|
|
|
// Which metadata sources to include.
|
|
// Valid values are "exif", "iptc", "xmp".
|
|
// Default is ["exif", "iptc"] (XMP is excluded for performance reasons).
|
|
Sources []string
|
|
}
|
|
|
|
// JpegConfig holds JPEG-specific encoding configuration.
|
|
type JpegConfig struct {
|
|
// Quality setting (1-100). Falls back to the global imaging.quality if unset.
|
|
Quality int
|
|
}
|
|
|
|
// AvifConfig holds AVIF-specific encoding configuration.
|
|
type AvifConfig struct {
|
|
// Quality setting (1-100). Falls back to the global imaging.quality if unset.
|
|
Quality int
|
|
|
|
// Hint about what type of image this is. Used for chroma subsampling.
|
|
// Valid values are "picture", "photo", "drawing", "icon", or "text".
|
|
// Default is "photo".
|
|
Hint string
|
|
|
|
// Encoder quality/speed trade-off, 1 (slowest, best quality / smallest
|
|
// files) to 10 (fastest). Default is 10 — fast enough for incremental
|
|
// builds with quality indistinguishable from slower settings at typical
|
|
// web thumbnail sizes. Lower values reduce file size at the cost of
|
|
// build time.
|
|
// We recommend sticking with the default of 10 unless you have a specific reason to change it,
|
|
// and to stay above 5 to avoid very long build times and timeouts.
|
|
// 0 is treated as unset and falls back to the default.
|
|
EncoderSpeed int
|
|
}
|
|
|
|
// WebpConfig holds WebP-specific encoding configuration.
|
|
type WebpConfig struct {
|
|
// Quality setting (1-100). Falls back to the global imaging.quality if unset.
|
|
// Only relevant for lossy encoding.
|
|
Quality int
|
|
|
|
// Hint about what type of image this is.
|
|
// Valid values are "picture", "photo", "drawing", "icon", or "text".
|
|
// Default is "photo".
|
|
Hint string
|
|
|
|
// Use sharp (and slow) RGB->YUV conversion.
|
|
// Default is true.
|
|
UseSharpYuv bool
|
|
|
|
// Quality/speed trade-off (0=fast, 6=slower-better).
|
|
// Default is 2.
|
|
Method int
|
|
}
|