hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

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 }