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 }