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