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 }