github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/resources/images/config.go (about)

     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  	"github.com/pkg/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]interface{}) (ImagingConfig, error) {
   135  	if m == nil {
   136  		m = make(map[string]interface{})
   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, errors.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  	if c.Width == 0 && c.Height == 0 {
   257  		return c, errors.New("must provide Width or Height")
   258  	}
   259  
   260  	if c.FilterStr == "" {
   261  		c.FilterStr = defaults.Cfg.ResampleFilter
   262  		c.Filter = defaults.ResampleFilter
   263  	}
   264  
   265  	if c.Hint == 0 {
   266  		c.Hint = webpoptions.EncodingPresetPhoto
   267  	}
   268  
   269  	if c.AnchorStr == "" {
   270  		c.AnchorStr = defaults.Cfg.Anchor
   271  		c.Anchor = defaults.Anchor
   272  	}
   273  
   274  	// default to the source format
   275  	if c.TargetFormat == 0 {
   276  		c.TargetFormat = sourceFormat
   277  	}
   278  
   279  	if c.Quality <= 0 && c.TargetFormat.RequiresDefaultQuality() {
   280  		// We need a quality setting for all JPEGs and WEBPs.
   281  		c.Quality = defaults.Cfg.Quality
   282  	}
   283  
   284  	if c.BgColor == nil && c.TargetFormat != sourceFormat {
   285  		if sourceFormat.SupportsTransparency() && !c.TargetFormat.SupportsTransparency() {
   286  			c.BgColor = defaults.BgColor
   287  			c.BgColorStr = defaults.Cfg.BgColor
   288  		}
   289  	}
   290  
   291  	return c, nil
   292  }
   293  
   294  // ImageConfig holds configuration to create a new image from an existing one, resize etc.
   295  type ImageConfig struct {
   296  	// This defines the output format of the output image. It defaults to the source format.
   297  	TargetFormat Format
   298  
   299  	Action string
   300  
   301  	// If set, this will be used as the key in filenames etc.
   302  	Key string
   303  
   304  	// Quality ranges from 1 to 100 inclusive, higher is better.
   305  	// This is only relevant for JPEG and WEBP images.
   306  	// Default is 75.
   307  	Quality            int
   308  	qualitySetForImage bool // Whether the above is set for this image.
   309  
   310  	// Rotate rotates an image by the given angle counter-clockwise.
   311  	// The rotation will be performed first.
   312  	Rotate int
   313  
   314  	// Used to fill any transparency.
   315  	// When set in site config, it's used when converting to a format that does
   316  	// not support transparency.
   317  	// When set per image operation, it's used even for formats that does support
   318  	// transparency.
   319  	BgColor    color.Color
   320  	BgColorStr string
   321  
   322  	// Hint about what type of picture this is. Used to optimize encoding
   323  	// when target is set to webp.
   324  	Hint webpoptions.EncodingPreset
   325  
   326  	Width  int
   327  	Height int
   328  
   329  	Filter    gift.Resampling
   330  	FilterStr string
   331  
   332  	Anchor    gift.Anchor
   333  	AnchorStr string
   334  }
   335  
   336  func (i ImageConfig) GetKey(format Format) string {
   337  	if i.Key != "" {
   338  		return i.Action + "_" + i.Key
   339  	}
   340  
   341  	k := strconv.Itoa(i.Width) + "x" + strconv.Itoa(i.Height)
   342  	if i.Action != "" {
   343  		k += "_" + i.Action
   344  	}
   345  	// This slightly odd construct is here to preserve the old image keys.
   346  	if i.qualitySetForImage || i.TargetFormat.RequiresDefaultQuality() {
   347  		k += "_q" + strconv.Itoa(i.Quality)
   348  	}
   349  	if i.Rotate != 0 {
   350  		k += "_r" + strconv.Itoa(i.Rotate)
   351  	}
   352  	if i.BgColorStr != "" {
   353  		k += "_bg" + i.BgColorStr
   354  	}
   355  
   356  	if i.TargetFormat == WEBP {
   357  		k += "_h" + strconv.Itoa(int(i.Hint))
   358  	}
   359  
   360  	anchor := i.AnchorStr
   361  	if anchor == smartCropIdentifier {
   362  		anchor = anchor + strconv.Itoa(smartCropVersionNumber)
   363  	}
   364  
   365  	k += "_" + i.FilterStr
   366  
   367  	if strings.EqualFold(i.Action, "fill") || strings.EqualFold(i.Action, "crop") {
   368  		k += "_" + anchor
   369  	}
   370  
   371  	if v, ok := imageFormatsVersions[format]; ok {
   372  		k += "_" + strconv.Itoa(v)
   373  	}
   374  
   375  	if mainImageVersionNumber > 0 {
   376  		k += "_" + strconv.Itoa(mainImageVersionNumber)
   377  	}
   378  
   379  	return k
   380  }
   381  
   382  type ImagingConfig struct {
   383  	BgColor        color.Color
   384  	Hint           webpoptions.EncodingPreset
   385  	ResampleFilter gift.Resampling
   386  	Anchor         gift.Anchor
   387  
   388  	// Config as provided by the user.
   389  	Cfg Imaging
   390  
   391  	// Hash of the config map provided by the user.
   392  	CfgHash string
   393  }
   394  
   395  // Imaging contains default image processing configuration. This will be fetched
   396  // from site (or language) config.
   397  type Imaging struct {
   398  	// Default image quality setting (1-100). Only used for JPEG images.
   399  	Quality int
   400  
   401  	// Resample filter to use in resize operations.
   402  	ResampleFilter string
   403  
   404  	// Hint about what type of image this is.
   405  	// Currently only used when encoding to Webp.
   406  	// Default is "photo".
   407  	// Valid values are "picture", "photo", "drawing", "icon", or "text".
   408  	Hint string
   409  
   410  	// The anchor to use in Fill. Default is "smart", i.e. Smart Crop.
   411  	Anchor string
   412  
   413  	// Default color used in fill operations (e.g. "fff" for white).
   414  	BgColor string
   415  
   416  	Exif ExifConfig
   417  }
   418  
   419  func (cfg *Imaging) init() error {
   420  	if cfg.Quality < 0 || cfg.Quality > 100 {
   421  		return errors.New("image quality must be a number between 1 and 100")
   422  	}
   423  
   424  	cfg.BgColor = strings.ToLower(strings.TrimPrefix(cfg.BgColor, "#"))
   425  	cfg.Anchor = strings.ToLower(cfg.Anchor)
   426  	cfg.ResampleFilter = strings.ToLower(cfg.ResampleFilter)
   427  	cfg.Hint = strings.ToLower(cfg.Hint)
   428  
   429  	return nil
   430  }
   431  
   432  type ExifConfig struct {
   433  
   434  	// Regexp matching the Exif fields you want from the (massive) set of Exif info
   435  	// available. As we cache this info to disk, this is for performance and
   436  	// disk space reasons more than anything.
   437  	// If you want it all, put ".*" in this config setting.
   438  	// Note that if neither this or ExcludeFields is set, Hugo will return a small
   439  	// default set.
   440  	IncludeFields string
   441  
   442  	// Regexp matching the Exif fields you want to exclude. This may be easier to use
   443  	// than IncludeFields above, depending on what you want.
   444  	ExcludeFields string
   445  
   446  	// Hugo extracts the "photo taken" date/time into .Date by default.
   447  	// Set this to true to turn it off.
   448  	DisableDate bool
   449  
   450  	// Hugo extracts the "photo taken where" (GPS latitude and longitude) into
   451  	// .Long and .Lat. Set this to true to turn it off.
   452  	DisableLatLong bool
   453  }