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 }