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