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  }