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 }