image.go (12178B)
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 resources
15
16 import (
17 "encoding/json"
18 "fmt"
19 "image"
20 "image/color"
21 "image/draw"
22 "image/gif"
23 _ "image/gif"
24 _ "image/png"
25 "io"
26 "io/ioutil"
27 "os"
28 "path"
29 "path/filepath"
30 "strings"
31 "sync"
32
33 "github.com/gohugoio/hugo/common/paths"
34
35 "github.com/disintegration/gift"
36
37 "github.com/gohugoio/hugo/cache/filecache"
38 "github.com/gohugoio/hugo/resources/images/exif"
39
40 "github.com/gohugoio/hugo/resources/resource"
41
42 "github.com/gohugoio/hugo/helpers"
43 "github.com/gohugoio/hugo/resources/images"
44
45 // Blind import for image.Decode
46 _ "golang.org/x/image/webp"
47 )
48
49 var (
50 _ images.ImageResource = (*imageResource)(nil)
51 _ resource.Source = (*imageResource)(nil)
52 _ resource.Cloner = (*imageResource)(nil)
53 )
54
55 // imageResource represents an image resource.
56 type imageResource struct {
57 *images.Image
58
59 // When a image is processed in a chain, this holds the reference to the
60 // original (first).
61 root *imageResource
62
63 metaInit sync.Once
64 metaInitErr error
65 meta *imageMeta
66
67 baseResource
68 }
69
70 type imageMeta struct {
71 Exif *exif.ExifInfo
72 }
73
74 func (i *imageResource) Exif() *exif.ExifInfo {
75 return i.root.getExif()
76 }
77
78 func (i *imageResource) getExif() *exif.ExifInfo {
79 i.metaInit.Do(func() {
80 supportsExif := i.Format == images.JPEG || i.Format == images.TIFF
81 if !supportsExif {
82 return
83 }
84
85 key := i.getImageMetaCacheTargetPath()
86
87 read := func(info filecache.ItemInfo, r io.ReadSeeker) error {
88 meta := &imageMeta{}
89 data, err := ioutil.ReadAll(r)
90 if err != nil {
91 return err
92 }
93
94 if err = json.Unmarshal(data, &meta); err != nil {
95 return err
96 }
97
98 i.meta = meta
99
100 return nil
101 }
102
103 create := func(info filecache.ItemInfo, w io.WriteCloser) (err error) {
104 f, err := i.root.ReadSeekCloser()
105 if err != nil {
106 i.metaInitErr = err
107 return
108 }
109 defer f.Close()
110
111 x, err := i.getSpec().imaging.DecodeExif(f)
112 if err != nil {
113 i.getSpec().Logger.Warnf("Unable to decode Exif metadata from image: %s", i.Key())
114 return nil
115 }
116
117 i.meta = &imageMeta{Exif: x}
118
119 // Also write it to cache
120 enc := json.NewEncoder(w)
121 return enc.Encode(i.meta)
122 }
123
124 _, i.metaInitErr = i.getSpec().imageCache.fileCache.ReadOrCreate(key, read, create)
125 })
126
127 if i.metaInitErr != nil {
128 panic(fmt.Sprintf("metadata init failed: %s", i.metaInitErr))
129 }
130
131 if i.meta == nil {
132 return nil
133 }
134
135 return i.meta.Exif
136 }
137
138 // Clone is for internal use.
139 func (i *imageResource) Clone() resource.Resource {
140 gr := i.baseResource.Clone().(baseResource)
141 return &imageResource{
142 root: i.root,
143 Image: i.WithSpec(gr),
144 baseResource: gr,
145 }
146 }
147
148 func (i *imageResource) cloneTo(targetPath string) resource.Resource {
149 gr := i.baseResource.cloneTo(targetPath).(baseResource)
150 return &imageResource{
151 root: i.root,
152 Image: i.WithSpec(gr),
153 baseResource: gr,
154 }
155 }
156
157 func (i *imageResource) cloneWithUpdates(u *transformationUpdate) (baseResource, error) {
158 base, err := i.baseResource.cloneWithUpdates(u)
159 if err != nil {
160 return nil, err
161 }
162
163 var img *images.Image
164
165 if u.isContentChanged() {
166 img = i.WithSpec(base)
167 } else {
168 img = i.Image
169 }
170
171 return &imageResource{
172 root: i.root,
173 Image: img,
174 baseResource: base,
175 }, nil
176 }
177
178 // Resize resizes the image to the specified width and height using the specified resampling
179 // filter and returns the transformed image. If one of width or height is 0, the image aspect
180 // ratio is preserved.
181 func (i *imageResource) Resize(spec string) (images.ImageResource, error) {
182 conf, err := i.decodeImageConfig("resize", spec)
183 if err != nil {
184 return nil, err
185 }
186
187 return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
188 return i.Proc.ApplyFiltersFromConfig(src, conf)
189 })
190 }
191
192 // Crop the image to the specified dimensions without resizing using the given anchor point.
193 // Space delimited config, e.g. `200x300 TopLeft`.
194 func (i *imageResource) Crop(spec string) (images.ImageResource, error) {
195 conf, err := i.decodeImageConfig("crop", spec)
196 if err != nil {
197 return nil, err
198 }
199
200 return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
201 return i.Proc.ApplyFiltersFromConfig(src, conf)
202 })
203 }
204
205 // Fit scales down the image using the specified resample filter to fit the specified
206 // maximum width and height.
207 func (i *imageResource) Fit(spec string) (images.ImageResource, error) {
208 conf, err := i.decodeImageConfig("fit", spec)
209 if err != nil {
210 return nil, err
211 }
212
213 return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
214 return i.Proc.ApplyFiltersFromConfig(src, conf)
215 })
216 }
217
218 // Fill scales the image to the smallest possible size that will cover the specified dimensions,
219 // crops the resized image to the specified dimensions using the given anchor point.
220 // Space delimited config, e.g. `200x300 TopLeft`.
221 func (i *imageResource) Fill(spec string) (images.ImageResource, error) {
222 conf, err := i.decodeImageConfig("fill", spec)
223 if err != nil {
224 return nil, err
225 }
226
227 img, err := i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
228 return i.Proc.ApplyFiltersFromConfig(src, conf)
229 })
230
231 if err != nil {
232 return nil, err
233 }
234
235 if conf.Anchor == 0 && img.Width() == 0 || img.Height() == 0 {
236 // See https://github.com/gohugoio/hugo/issues/7955
237 // Smartcrop fails silently in some rare cases.
238 // Fall back to a center fill.
239 conf.Anchor = gift.CenterAnchor
240 conf.AnchorStr = "center"
241 return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
242 return i.Proc.ApplyFiltersFromConfig(src, conf)
243 })
244 }
245
246 return img, err
247 }
248
249 func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) {
250 conf := images.GetDefaultImageConfig("filter", i.Proc.Cfg)
251
252 var gfilters []gift.Filter
253
254 for _, f := range filters {
255 gfilters = append(gfilters, images.ToFilters(f)...)
256 }
257
258 conf.Key = helpers.HashString(gfilters)
259 conf.TargetFormat = i.Format
260
261 return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
262 return i.Proc.Filter(src, gfilters...)
263 })
264 }
265
266 // Serialize image processing. The imaging library spins up its own set of Go routines,
267 // so there is not much to gain from adding more load to the mix. That
268 // can even have negative effect in low resource scenarios.
269 // Note that this only effects the non-cached scenario. Once the processed
270 // image is written to disk, everything is fast, fast fast.
271 const imageProcWorkers = 1
272
273 var imageProcSem = make(chan bool, imageProcWorkers)
274
275 func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src image.Image) (image.Image, error)) (images.ImageResource, error) {
276 img, err := i.getSpec().imageCache.getOrCreate(i, conf, func() (*imageResource, image.Image, error) {
277 imageProcSem <- true
278 defer func() {
279 <-imageProcSem
280 }()
281
282 errOp := conf.Action
283 errPath := i.getSourceFilename()
284
285 src, err := i.DecodeImage()
286 if err != nil {
287 return nil, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
288 }
289
290 converted, err := f(src)
291 if err != nil {
292 return nil, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
293 }
294
295 hasAlpha := !images.IsOpaque(converted)
296 shouldFill := conf.BgColor != nil && hasAlpha
297 shouldFill = shouldFill || (!conf.TargetFormat.SupportsTransparency() && hasAlpha)
298 var bgColor color.Color
299
300 if shouldFill {
301 bgColor = conf.BgColor
302 if bgColor == nil {
303 bgColor = i.Proc.Cfg.BgColor
304 }
305 tmp := image.NewRGBA(converted.Bounds())
306 draw.Draw(tmp, tmp.Bounds(), image.NewUniform(bgColor), image.Point{}, draw.Src)
307 draw.Draw(tmp, tmp.Bounds(), converted, converted.Bounds().Min, draw.Over)
308 converted = tmp
309 }
310
311 if conf.TargetFormat == images.PNG {
312 // Apply the colour palette from the source
313 if paletted, ok := src.(*image.Paletted); ok {
314 palette := paletted.Palette
315 if bgColor != nil && len(palette) < 256 {
316 palette = images.AddColorToPalette(bgColor, palette)
317 } else if bgColor != nil {
318 images.ReplaceColorInPalette(bgColor, palette)
319 }
320 tmp := image.NewPaletted(converted.Bounds(), palette)
321 draw.FloydSteinberg.Draw(tmp, tmp.Bounds(), converted, converted.Bounds().Min)
322 converted = tmp
323 }
324 }
325
326 ci := i.clone(converted)
327 ci.setBasePath(conf)
328 ci.Format = conf.TargetFormat
329 ci.setMediaType(conf.TargetFormat.MediaType())
330
331 return ci, converted, nil
332 })
333 if err != nil {
334 if i.root != nil && i.root.getFileInfo() != nil {
335 return nil, fmt.Errorf("image %q: %w", i.root.getFileInfo().Meta().Filename, err)
336 }
337 }
338 return img, nil
339 }
340
341 func (i *imageResource) decodeImageConfig(action, spec string) (images.ImageConfig, error) {
342 conf, err := images.DecodeImageConfig(action, spec, i.Proc.Cfg, i.Format)
343 if err != nil {
344 return conf, err
345 }
346
347 return conf, nil
348 }
349
350 type giphy struct {
351 image.Image
352 gif *gif.GIF
353 }
354
355 func (g *giphy) GIF() *gif.GIF {
356 return g.gif
357 }
358
359 // DecodeImage decodes the image source into an Image.
360 // This an internal method and may change.
361 func (i *imageResource) DecodeImage() (image.Image, error) {
362 f, err := i.ReadSeekCloser()
363 if err != nil {
364 return nil, fmt.Errorf("failed to open image for decode: %w", err)
365 }
366 defer f.Close()
367
368 if i.Format == images.GIF {
369 g, err := gif.DecodeAll(f)
370 if err != nil {
371 return nil, fmt.Errorf("failed to decode gif: %w", err)
372 }
373 return &giphy{gif: g, Image: g.Image[0]}, nil
374 }
375 img, _, err := image.Decode(f)
376 return img, err
377 }
378
379 func (i *imageResource) clone(img image.Image) *imageResource {
380 spec := i.baseResource.Clone().(baseResource)
381
382 var image *images.Image
383 if img != nil {
384 image = i.WithImage(img)
385 } else {
386 image = i.WithSpec(spec)
387 }
388
389 return &imageResource{
390 Image: image,
391 root: i.root,
392 baseResource: spec,
393 }
394 }
395
396 func (i *imageResource) setBasePath(conf images.ImageConfig) {
397 i.getResourcePaths().relTargetDirFile = i.relTargetPathFromConfig(conf)
398 }
399
400 func (i *imageResource) getImageMetaCacheTargetPath() string {
401 const imageMetaVersionNumber = 1 // Increment to invalidate the meta cache
402
403 cfgHash := i.getSpec().imaging.Cfg.CfgHash
404 df := i.getResourcePaths().relTargetDirFile
405 if fi := i.getFileInfo(); fi != nil {
406 df.dir = filepath.Dir(fi.Meta().Path)
407 }
408 p1, _ := paths.FileAndExt(df.file)
409 h, _ := i.hash()
410 idStr := helpers.HashString(h, i.size(), imageMetaVersionNumber, cfgHash)
411 p := path.Join(df.dir, fmt.Sprintf("%s_%s.json", p1, idStr))
412 return p
413 }
414
415 func (i *imageResource) relTargetPathFromConfig(conf images.ImageConfig) dirFile {
416 p1, p2 := paths.FileAndExt(i.getResourcePaths().relTargetDirFile.file)
417 if conf.TargetFormat != i.Format {
418 p2 = conf.TargetFormat.DefaultExtension()
419 }
420
421 h, _ := i.hash()
422 idStr := fmt.Sprintf("_hu%s_%d", h, i.size())
423
424 // Do not change for no good reason.
425 const md5Threshold = 100
426
427 key := conf.GetKey(i.Format)
428
429 // It is useful to have the key in clear text, but when nesting transforms, it
430 // can easily be too long to read, and maybe even too long
431 // for the different OSes to handle.
432 if len(p1)+len(idStr)+len(p2) > md5Threshold {
433 key = helpers.MD5String(p1 + key + p2)
434 huIdx := strings.Index(p1, "_hu")
435 if huIdx != -1 {
436 p1 = p1[:huIdx]
437 } else {
438 // This started out as a very long file name. Making it even longer
439 // could melt ice in the Arctic.
440 p1 = ""
441 }
442 } else if strings.Contains(p1, idStr) {
443 // On scaling an already scaled image, we get the file info from the original.
444 // Repeating the same info in the filename makes it stuttery for no good reason.
445 idStr = ""
446 }
447
448 return dirFile{
449 dir: i.getResourcePaths().relTargetDirFile.dir,
450 file: fmt.Sprintf("%s%s_%s%s", p1, idStr, key, p2),
451 }
452 }