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 }