hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

config.go (12724B)

    1 // Copyright 2019 The Hugo Authors. All rights reserved.
    2 //
    3 // Licensed under the Apache License, Version 2.0 (the "License");
    4 // you may not use this file except in compliance with the License.
    5 // You may obtain a copy of the License at
    6 // http://www.apache.org/licenses/LICENSE-2.0
    7 //
    8 // Unless required by applicable law or agreed to in writing, software
    9 // distributed under the License is distributed on an "AS IS" BASIS,
   10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   11 // See the License for the specific language governing permissions and
   12 // limitations under the License.
   13 
   14 package images
   15 
   16 import (
   17 	"fmt"
   18 	"image/color"
   19 	"strconv"
   20 	"strings"
   21 
   22 	"github.com/gohugoio/hugo/helpers"
   23 	"github.com/gohugoio/hugo/media"
   24 
   25 	"errors"
   26 
   27 	"github.com/bep/gowebp/libwebp/webpoptions"
   28 
   29 	"github.com/disintegration/gift"
   30 
   31 	"github.com/mitchellh/mapstructure"
   32 )
   33 
   34 var (
   35 	imageFormats = map[string]Format{
   36 		".jpg":  JPEG,
   37 		".jpeg": JPEG,
   38 		".jpe":  JPEG,
   39 		".jif":  JPEG,
   40 		".jfif": JPEG,
   41 		".png":  PNG,
   42 		".tif":  TIFF,
   43 		".tiff": TIFF,
   44 		".bmp":  BMP,
   45 		".gif":  GIF,
   46 		".webp": WEBP,
   47 	}
   48 
   49 	imageFormatsBySubType = map[string]Format{
   50 		media.JPEGType.SubType: JPEG,
   51 		media.PNGType.SubType:  PNG,
   52 		media.TIFFType.SubType: TIFF,
   53 		media.BMPType.SubType:  BMP,
   54 		media.GIFType.SubType:  GIF,
   55 		media.WEBPType.SubType: WEBP,
   56 	}
   57 
   58 	// Add or increment if changes to an image format's processing requires
   59 	// re-generation.
   60 	imageFormatsVersions = map[Format]int{
   61 		PNG:  3, // Fix transparency issue with 32 bit images.
   62 		WEBP: 2, // Fix transparency issue with 32 bit images.
   63 	}
   64 
   65 	// Increment to mark all processed images as stale. Only use when absolutely needed.
   66 	// See the finer grained smartCropVersionNumber and imageFormatsVersions.
   67 	mainImageVersionNumber = 0
   68 )
   69 
   70 var anchorPositions = map[string]gift.Anchor{
   71 	strings.ToLower("Center"):      gift.CenterAnchor,
   72 	strings.ToLower("TopLeft"):     gift.TopLeftAnchor,
   73 	strings.ToLower("Top"):         gift.TopAnchor,
   74 	strings.ToLower("TopRight"):    gift.TopRightAnchor,
   75 	strings.ToLower("Left"):        gift.LeftAnchor,
   76 	strings.ToLower("Right"):       gift.RightAnchor,
   77 	strings.ToLower("BottomLeft"):  gift.BottomLeftAnchor,
   78 	strings.ToLower("Bottom"):      gift.BottomAnchor,
   79 	strings.ToLower("BottomRight"): gift.BottomRightAnchor,
   80 }
   81 
   82 // These encoding hints are currently only relevant for Webp.
   83 var hints = map[string]webpoptions.EncodingPreset{
   84 	"picture": webpoptions.EncodingPresetPicture,
   85 	"photo":   webpoptions.EncodingPresetPhoto,
   86 	"drawing": webpoptions.EncodingPresetDrawing,
   87 	"icon":    webpoptions.EncodingPresetIcon,
   88 	"text":    webpoptions.EncodingPresetText,
   89 }
   90 
   91 var imageFilters = map[string]gift.Resampling{
   92 
   93 	strings.ToLower("NearestNeighbor"):   gift.NearestNeighborResampling,
   94 	strings.ToLower("Box"):               gift.BoxResampling,
   95 	strings.ToLower("Linear"):            gift.LinearResampling,
   96 	strings.ToLower("Hermite"):           hermiteResampling,
   97 	strings.ToLower("MitchellNetravali"): mitchellNetravaliResampling,
   98 	strings.ToLower("CatmullRom"):        catmullRomResampling,
   99 	strings.ToLower("BSpline"):           bSplineResampling,
  100 	strings.ToLower("Gaussian"):          gaussianResampling,
  101 	strings.ToLower("Lanczos"):           gift.LanczosResampling,
  102 	strings.ToLower("Hann"):              hannResampling,
  103 	strings.ToLower("Hamming"):           hammingResampling,
  104 	strings.ToLower("Blackman"):          blackmanResampling,
  105 	strings.ToLower("Bartlett"):          bartlettResampling,
  106 	strings.ToLower("Welch"):             welchResampling,
  107 	strings.ToLower("Cosine"):            cosineResampling,
  108 }
  109 
  110 func ImageFormatFromExt(ext string) (Format, bool) {
  111 	f, found := imageFormats[ext]
  112 	return f, found
  113 }
  114 
  115 func ImageFormatFromMediaSubType(sub string) (Format, bool) {
  116 	f, found := imageFormatsBySubType[sub]
  117 	return f, found
  118 }
  119 
  120 const (
  121 	defaultJPEGQuality    = 75
  122 	defaultResampleFilter = "box"
  123 	defaultBgColor        = "ffffff"
  124 	defaultHint           = "photo"
  125 )
  126 
  127 var defaultImaging = Imaging{
  128 	ResampleFilter: defaultResampleFilter,
  129 	BgColor:        defaultBgColor,
  130 	Hint:           defaultHint,
  131 	Quality:        defaultJPEGQuality,
  132 }
  133 
  134 func DecodeConfig(m map[string]any) (ImagingConfig, error) {
  135 	if m == nil {
  136 		m = make(map[string]any)
  137 	}
  138 
  139 	i := ImagingConfig{
  140 		Cfg:     defaultImaging,
  141 		CfgHash: helpers.HashString(m),
  142 	}
  143 
  144 	if err := mapstructure.WeakDecode(m, &i.Cfg); err != nil {
  145 		return i, err
  146 	}
  147 
  148 	if err := i.Cfg.init(); err != nil {
  149 		return i, err
  150 	}
  151 
  152 	var err error
  153 	i.BgColor, err = hexStringToColor(i.Cfg.BgColor)
  154 	if err != nil {
  155 		return i, err
  156 	}
  157 
  158 	if i.Cfg.Anchor != "" && i.Cfg.Anchor != smartCropIdentifier {
  159 		anchor, found := anchorPositions[i.Cfg.Anchor]
  160 		if !found {
  161 			return i, fmt.Errorf("invalid anchor value %q in imaging config", i.Anchor)
  162 		}
  163 		i.Anchor = anchor
  164 	} else {
  165 		i.Cfg.Anchor = smartCropIdentifier
  166 	}
  167 
  168 	filter, found := imageFilters[i.Cfg.ResampleFilter]
  169 	if !found {
  170 		return i, fmt.Errorf("%q is not a valid resample filter", filter)
  171 	}
  172 	i.ResampleFilter = filter
  173 
  174 	if strings.TrimSpace(i.Cfg.Exif.IncludeFields) == "" && strings.TrimSpace(i.Cfg.Exif.ExcludeFields) == "" {
  175 		// Don't change this for no good reason. Please don't.
  176 		i.Cfg.Exif.ExcludeFields = "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance"
  177 	}
  178 
  179 	return i, nil
  180 }
  181 
  182 func DecodeImageConfig(action, config string, defaults ImagingConfig, sourceFormat Format) (ImageConfig, error) {
  183 	var (
  184 		c   ImageConfig = GetDefaultImageConfig(action, defaults)
  185 		err error
  186 	)
  187 
  188 	c.Action = action
  189 
  190 	if config == "" {
  191 		return c, errors.New("image config cannot be empty")
  192 	}
  193 
  194 	parts := strings.Fields(config)
  195 	for _, part := range parts {
  196 		part = strings.ToLower(part)
  197 
  198 		if part == smartCropIdentifier {
  199 			c.AnchorStr = smartCropIdentifier
  200 		} else if pos, ok := anchorPositions[part]; ok {
  201 			c.Anchor = pos
  202 			c.AnchorStr = part
  203 		} else if filter, ok := imageFilters[part]; ok {
  204 			c.Filter = filter
  205 			c.FilterStr = part
  206 		} else if hint, ok := hints[part]; ok {
  207 			c.Hint = hint
  208 		} else if part[0] == '#' {
  209 			c.BgColorStr = part[1:]
  210 			c.BgColor, err = hexStringToColor(c.BgColorStr)
  211 			if err != nil {
  212 				return c, err
  213 			}
  214 		} else if part[0] == 'q' {
  215 			c.Quality, err = strconv.Atoi(part[1:])
  216 			if err != nil {
  217 				return c, err
  218 			}
  219 			if c.Quality < 1 || c.Quality > 100 {
  220 				return c, errors.New("quality ranges from 1 to 100 inclusive")
  221 			}
  222 			c.qualitySetForImage = true
  223 		} else if part[0] == 'r' {
  224 			c.Rotate, err = strconv.Atoi(part[1:])
  225 			if err != nil {
  226 				return c, err
  227 			}
  228 		} else if strings.Contains(part, "x") {
  229 			widthHeight := strings.Split(part, "x")
  230 			if len(widthHeight) <= 2 {
  231 				first := widthHeight[0]
  232 				if first != "" {
  233 					c.Width, err = strconv.Atoi(first)
  234 					if err != nil {
  235 						return c, err
  236 					}
  237 				}
  238 
  239 				if len(widthHeight) == 2 {
  240 					second := widthHeight[1]
  241 					if second != "" {
  242 						c.Height, err = strconv.Atoi(second)
  243 						if err != nil {
  244 							return c, err
  245 						}
  246 					}
  247 				}
  248 			} else {
  249 				return c, errors.New("invalid image dimensions")
  250 			}
  251 		} else if f, ok := ImageFormatFromExt("." + part); ok {
  252 			c.TargetFormat = f
  253 		}
  254 	}
  255 
  256 	switch c.Action {
  257 	case "crop", "fill", "fit":
  258 		if c.Width == 0 || c.Height == 0 {
  259 			return c, errors.New("must provide Width and Height")
  260 		}
  261 	case "resize":
  262 		if c.Width == 0 && c.Height == 0 {
  263 			return c, errors.New("must provide Width or Height")
  264 		}
  265 	default:
  266 		return c, fmt.Errorf("BUG: unknown action %q encountered while decoding image configuration", c.Action)
  267 	}
  268 
  269 	if c.FilterStr == "" {
  270 		c.FilterStr = defaults.Cfg.ResampleFilter
  271 		c.Filter = defaults.ResampleFilter
  272 	}
  273 
  274 	if c.Hint == 0 {
  275 		c.Hint = webpoptions.EncodingPresetPhoto
  276 	}
  277 
  278 	if c.AnchorStr == "" {
  279 		c.AnchorStr = defaults.Cfg.Anchor
  280 		c.Anchor = defaults.Anchor
  281 	}
  282 
  283 	// default to the source format
  284 	if c.TargetFormat == 0 {
  285 		c.TargetFormat = sourceFormat
  286 	}
  287 
  288 	if c.Quality <= 0 && c.TargetFormat.RequiresDefaultQuality() {
  289 		// We need a quality setting for all JPEGs and WEBPs.
  290 		c.Quality = defaults.Cfg.Quality
  291 	}
  292 
  293 	if c.BgColor == nil && c.TargetFormat != sourceFormat {
  294 		if sourceFormat.SupportsTransparency() && !c.TargetFormat.SupportsTransparency() {
  295 			c.BgColor = defaults.BgColor
  296 			c.BgColorStr = defaults.Cfg.BgColor
  297 		}
  298 	}
  299 
  300 	return c, nil
  301 }
  302 
  303 // ImageConfig holds configuration to create a new image from an existing one, resize etc.
  304 type ImageConfig struct {
  305 	// This defines the output format of the output image. It defaults to the source format.
  306 	TargetFormat Format
  307 
  308 	Action string
  309 
  310 	// If set, this will be used as the key in filenames etc.
  311 	Key string
  312 
  313 	// Quality ranges from 1 to 100 inclusive, higher is better.
  314 	// This is only relevant for JPEG and WEBP images.
  315 	// Default is 75.
  316 	Quality            int
  317 	qualitySetForImage bool // Whether the above is set for this image.
  318 
  319 	// Rotate rotates an image by the given angle counter-clockwise.
  320 	// The rotation will be performed first.
  321 	Rotate int
  322 
  323 	// Used to fill any transparency.
  324 	// When set in site config, it's used when converting to a format that does
  325 	// not support transparency.
  326 	// When set per image operation, it's used even for formats that does support
  327 	// transparency.
  328 	BgColor    color.Color
  329 	BgColorStr string
  330 
  331 	// Hint about what type of picture this is. Used to optimize encoding
  332 	// when target is set to webp.
  333 	Hint webpoptions.EncodingPreset
  334 
  335 	Width  int
  336 	Height int
  337 
  338 	Filter    gift.Resampling
  339 	FilterStr string
  340 
  341 	Anchor    gift.Anchor
  342 	AnchorStr string
  343 }
  344 
  345 func (i ImageConfig) GetKey(format Format) string {
  346 	if i.Key != "" {
  347 		return i.Action + "_" + i.Key
  348 	}
  349 
  350 	k := strconv.Itoa(i.Width) + "x" + strconv.Itoa(i.Height)
  351 	if i.Action != "" {
  352 		k += "_" + i.Action
  353 	}
  354 	// This slightly odd construct is here to preserve the old image keys.
  355 	if i.qualitySetForImage || i.TargetFormat.RequiresDefaultQuality() {
  356 		k += "_q" + strconv.Itoa(i.Quality)
  357 	}
  358 	if i.Rotate != 0 {
  359 		k += "_r" + strconv.Itoa(i.Rotate)
  360 	}
  361 	if i.BgColorStr != "" {
  362 		k += "_bg" + i.BgColorStr
  363 	}
  364 
  365 	if i.TargetFormat == WEBP {
  366 		k += "_h" + strconv.Itoa(int(i.Hint))
  367 	}
  368 
  369 	anchor := i.AnchorStr
  370 	if anchor == smartCropIdentifier {
  371 		anchor = anchor + strconv.Itoa(smartCropVersionNumber)
  372 	}
  373 
  374 	k += "_" + i.FilterStr
  375 
  376 	if strings.EqualFold(i.Action, "fill") || strings.EqualFold(i.Action, "crop") {
  377 		k += "_" + anchor
  378 	}
  379 
  380 	if v, ok := imageFormatsVersions[format]; ok {
  381 		k += "_" + strconv.Itoa(v)
  382 	}
  383 
  384 	if mainImageVersionNumber > 0 {
  385 		k += "_" + strconv.Itoa(mainImageVersionNumber)
  386 	}
  387 
  388 	return k
  389 }
  390 
  391 type ImagingConfig struct {
  392 	BgColor        color.Color
  393 	Hint           webpoptions.EncodingPreset
  394 	ResampleFilter gift.Resampling
  395 	Anchor         gift.Anchor
  396 
  397 	// Config as provided by the user.
  398 	Cfg Imaging
  399 
  400 	// Hash of the config map provided by the user.
  401 	CfgHash string
  402 }
  403 
  404 // Imaging contains default image processing configuration. This will be fetched
  405 // from site (or language) config.
  406 type Imaging struct {
  407 	// Default image quality setting (1-100). Only used for JPEG images.
  408 	Quality int
  409 
  410 	// Resample filter to use in resize operations.
  411 	ResampleFilter string
  412 
  413 	// Hint about what type of image this is.
  414 	// Currently only used when encoding to Webp.
  415 	// Default is "photo".
  416 	// Valid values are "picture", "photo", "drawing", "icon", or "text".
  417 	Hint string
  418 
  419 	// The anchor to use in Fill. Default is "smart", i.e. Smart Crop.
  420 	Anchor string
  421 
  422 	// Default color used in fill operations (e.g. "fff" for white).
  423 	BgColor string
  424 
  425 	Exif ExifConfig
  426 }
  427 
  428 func (cfg *Imaging) init() error {
  429 	if cfg.Quality < 0 || cfg.Quality > 100 {
  430 		return errors.New("image quality must be a number between 1 and 100")
  431 	}
  432 
  433 	cfg.BgColor = strings.ToLower(strings.TrimPrefix(cfg.BgColor, "#"))
  434 	cfg.Anchor = strings.ToLower(cfg.Anchor)
  435 	cfg.ResampleFilter = strings.ToLower(cfg.ResampleFilter)
  436 	cfg.Hint = strings.ToLower(cfg.Hint)
  437 
  438 	return nil
  439 }
  440 
  441 type ExifConfig struct {
  442 
  443 	// Regexp matching the Exif fields you want from the (massive) set of Exif info
  444 	// available. As we cache this info to disk, this is for performance and
  445 	// disk space reasons more than anything.
  446 	// If you want it all, put ".*" in this config setting.
  447 	// Note that if neither this or ExcludeFields is set, Hugo will return a small
  448 	// default set.
  449 	IncludeFields string
  450 
  451 	// Regexp matching the Exif fields you want to exclude. This may be easier to use
  452 	// than IncludeFields above, depending on what you want.
  453 	ExcludeFields string
  454 
  455 	// Hugo extracts the "photo taken" date/time into .Date by default.
  456 	// Set this to true to turn it off.
  457 	DisableDate bool
  458 
  459 	// Hugo extracts the "photo taken where" (GPS latitude and longitude) into
  460 	// .Long and .Lat. Set this to true to turn it off.
  461 	DisableLatLong bool
  462 }