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 }