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