mirror of
				https://github.com/gohugoio/hugo.git
				synced 2024-05-11 05:54:58 +00:00 
			
		
		
		
	Introduces the Crop method for image processing which implements gift.CropToSize. Also allows a smartCrop without resizing, and updates the documentation. Fixes #9499
		
			
				
	
	
		
			378 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			378 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2019 The Hugo Authors. All rights reserved.
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| // http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| package images
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"image"
 | |
| 	"image/color"
 | |
| 	"image/draw"
 | |
| 	"image/gif"
 | |
| 	"image/jpeg"
 | |
| 	"image/png"
 | |
| 	"io"
 | |
| 	"sync"
 | |
| 
 | |
| 	"github.com/bep/gowebp/libwebp/webpoptions"
 | |
| 	"github.com/gohugoio/hugo/resources/images/webp"
 | |
| 
 | |
| 	"github.com/gohugoio/hugo/media"
 | |
| 	"github.com/gohugoio/hugo/resources/images/exif"
 | |
| 
 | |
| 	"github.com/disintegration/gift"
 | |
| 	"golang.org/x/image/bmp"
 | |
| 	"golang.org/x/image/tiff"
 | |
| 
 | |
| 	"github.com/gohugoio/hugo/common/hugio"
 | |
| 	"github.com/pkg/errors"
 | |
| )
 | |
| 
 | |
| func NewImage(f Format, proc *ImageProcessor, img image.Image, s Spec) *Image {
 | |
| 	if img != nil {
 | |
| 		return &Image{
 | |
| 			Format: f,
 | |
| 			Proc:   proc,
 | |
| 			Spec:   s,
 | |
| 			imageConfig: &imageConfig{
 | |
| 				config:       imageConfigFromImage(img),
 | |
| 				configLoaded: true,
 | |
| 			},
 | |
| 		}
 | |
| 	}
 | |
| 	return &Image{Format: f, Proc: proc, Spec: s, imageConfig: &imageConfig{}}
 | |
| }
 | |
| 
 | |
| type Image struct {
 | |
| 	Format Format
 | |
| 	Proc   *ImageProcessor
 | |
| 	Spec   Spec
 | |
| 	*imageConfig
 | |
| }
 | |
| 
 | |
| func (i *Image) EncodeTo(conf ImageConfig, img image.Image, w io.Writer) error {
 | |
| 	switch conf.TargetFormat {
 | |
| 	case JPEG:
 | |
| 
 | |
| 		var rgba *image.RGBA
 | |
| 		quality := conf.Quality
 | |
| 
 | |
| 		if nrgba, ok := img.(*image.NRGBA); ok {
 | |
| 			if nrgba.Opaque() {
 | |
| 				rgba = &image.RGBA{
 | |
| 					Pix:    nrgba.Pix,
 | |
| 					Stride: nrgba.Stride,
 | |
| 					Rect:   nrgba.Rect,
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		if rgba != nil {
 | |
| 			return jpeg.Encode(w, rgba, &jpeg.Options{Quality: quality})
 | |
| 		}
 | |
| 		return jpeg.Encode(w, img, &jpeg.Options{Quality: quality})
 | |
| 	case PNG:
 | |
| 		encoder := png.Encoder{CompressionLevel: png.DefaultCompression}
 | |
| 		return encoder.Encode(w, img)
 | |
| 
 | |
| 	case GIF:
 | |
| 		return gif.Encode(w, img, &gif.Options{
 | |
| 			NumColors: 256,
 | |
| 		})
 | |
| 	case TIFF:
 | |
| 		return tiff.Encode(w, img, &tiff.Options{Compression: tiff.Deflate, Predictor: true})
 | |
| 
 | |
| 	case BMP:
 | |
| 		return bmp.Encode(w, img)
 | |
| 	case WEBP:
 | |
| 		return webp.Encode(
 | |
| 			w,
 | |
| 			img, webpoptions.EncodingOptions{
 | |
| 				Quality:        conf.Quality,
 | |
| 				EncodingPreset: webpoptions.EncodingPreset(conf.Hint),
 | |
| 				UseSharpYuv:    true,
 | |
| 			},
 | |
| 		)
 | |
| 	default:
 | |
| 		return errors.New("format not supported")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Height returns i's height.
 | |
| func (i *Image) Height() int {
 | |
| 	i.initConfig()
 | |
| 	return i.config.Height
 | |
| }
 | |
| 
 | |
| // Width returns i's width.
 | |
| func (i *Image) Width() int {
 | |
| 	i.initConfig()
 | |
| 	return i.config.Width
 | |
| }
 | |
| 
 | |
| func (i Image) WithImage(img image.Image) *Image {
 | |
| 	i.Spec = nil
 | |
| 	i.imageConfig = &imageConfig{
 | |
| 		config:       imageConfigFromImage(img),
 | |
| 		configLoaded: true,
 | |
| 	}
 | |
| 
 | |
| 	return &i
 | |
| }
 | |
| 
 | |
| func (i Image) WithSpec(s Spec) *Image {
 | |
| 	i.Spec = s
 | |
| 	i.imageConfig = &imageConfig{}
 | |
| 	return &i
 | |
| }
 | |
| 
 | |
| // InitConfig reads the image config from the given reader.
 | |
| func (i *Image) InitConfig(r io.Reader) error {
 | |
| 	var err error
 | |
| 	i.configInit.Do(func() {
 | |
| 		i.config, _, err = image.DecodeConfig(r)
 | |
| 	})
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func (i *Image) initConfig() error {
 | |
| 	var err error
 | |
| 	i.configInit.Do(func() {
 | |
| 		if i.configLoaded {
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		var f hugio.ReadSeekCloser
 | |
| 
 | |
| 		f, err = i.Spec.ReadSeekCloser()
 | |
| 		if err != nil {
 | |
| 			return
 | |
| 		}
 | |
| 		defer f.Close()
 | |
| 
 | |
| 		i.config, _, err = image.DecodeConfig(f)
 | |
| 	})
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return errors.Wrap(err, "failed to load image config")
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func NewImageProcessor(cfg ImagingConfig) (*ImageProcessor, error) {
 | |
| 	e := cfg.Cfg.Exif
 | |
| 	exifDecoder, err := exif.NewDecoder(
 | |
| 		exif.WithDateDisabled(e.DisableDate),
 | |
| 		exif.WithLatLongDisabled(e.DisableLatLong),
 | |
| 		exif.ExcludeFields(e.ExcludeFields),
 | |
| 		exif.IncludeFields(e.IncludeFields),
 | |
| 	)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return &ImageProcessor{
 | |
| 		Cfg:         cfg,
 | |
| 		exifDecoder: exifDecoder,
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| type ImageProcessor struct {
 | |
| 	Cfg         ImagingConfig
 | |
| 	exifDecoder *exif.Decoder
 | |
| }
 | |
| 
 | |
| func (p *ImageProcessor) DecodeExif(r io.Reader) (*exif.Exif, error) {
 | |
| 	return p.exifDecoder.Decode(r)
 | |
| }
 | |
| 
 | |
| func (p *ImageProcessor) ApplyFiltersFromConfig(src image.Image, conf ImageConfig) (image.Image, error) {
 | |
| 	var filters []gift.Filter
 | |
| 
 | |
| 	if conf.Rotate != 0 {
 | |
| 		// Apply any rotation before any resize.
 | |
| 		filters = append(filters, gift.Rotate(float32(conf.Rotate), color.Transparent, gift.NearestNeighborInterpolation))
 | |
| 	}
 | |
| 
 | |
| 	switch conf.Action {
 | |
| 	case "resize":
 | |
| 		filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter))
 | |
| 	case "crop":
 | |
| 		if conf.AnchorStr == smartCropIdentifier {
 | |
| 			bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter)
 | |
| 			if err != nil {
 | |
| 				return nil, err
 | |
| 			}
 | |
| 
 | |
| 			// First crop using the bounds returned by smartCrop.
 | |
| 			filters = append(filters, gift.Crop(bounds))
 | |
| 			// Then center crop the image to get an image the desired size without resizing.
 | |
| 			filters = append(filters, gift.CropToSize(conf.Width, conf.Height, gift.CenterAnchor))
 | |
| 
 | |
| 		} else {
 | |
| 			filters = append(filters, gift.CropToSize(conf.Width, conf.Height, conf.Anchor))
 | |
| 		}
 | |
| 	case "fill":
 | |
| 		if conf.AnchorStr == smartCropIdentifier {
 | |
| 			bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter)
 | |
| 			if err != nil {
 | |
| 				return nil, err
 | |
| 			}
 | |
| 
 | |
| 			// First crop it, then resize it.
 | |
| 			filters = append(filters, gift.Crop(bounds))
 | |
| 			filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter))
 | |
| 
 | |
| 		} else {
 | |
| 			filters = append(filters, gift.ResizeToFill(conf.Width, conf.Height, conf.Filter, conf.Anchor))
 | |
| 		}
 | |
| 	case "fit":
 | |
| 		filters = append(filters, gift.ResizeToFit(conf.Width, conf.Height, conf.Filter))
 | |
| 	default:
 | |
| 		return nil, errors.Errorf("unsupported action: %q", conf.Action)
 | |
| 	}
 | |
| 
 | |
| 	img, err := p.Filter(src, filters...)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return img, nil
 | |
| }
 | |
| 
 | |
| func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.Image, error) {
 | |
| 	g := gift.New(filters...)
 | |
| 	bounds := g.Bounds(src.Bounds())
 | |
| 	var dst draw.Image
 | |
| 	switch src.(type) {
 | |
| 	case *image.RGBA:
 | |
| 		dst = image.NewRGBA(bounds)
 | |
| 	case *image.NRGBA:
 | |
| 		dst = image.NewNRGBA(bounds)
 | |
| 	case *image.Gray:
 | |
| 		dst = image.NewGray(bounds)
 | |
| 	default:
 | |
| 		dst = image.NewNRGBA(bounds)
 | |
| 	}
 | |
| 	g.Draw(dst, src)
 | |
| 	return dst, nil
 | |
| }
 | |
| 
 | |
| func GetDefaultImageConfig(action string, defaults ImagingConfig) ImageConfig {
 | |
| 	return ImageConfig{
 | |
| 		Action:  action,
 | |
| 		Hint:    defaults.Hint,
 | |
| 		Quality: defaults.Cfg.Quality,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type Spec interface {
 | |
| 	// Loads the image source.
 | |
| 	ReadSeekCloser() (hugio.ReadSeekCloser, error)
 | |
| }
 | |
| 
 | |
| // Format is an image file format.
 | |
| type Format int
 | |
| 
 | |
| const (
 | |
| 	JPEG Format = iota + 1
 | |
| 	PNG
 | |
| 	GIF
 | |
| 	TIFF
 | |
| 	BMP
 | |
| 	WEBP
 | |
| )
 | |
| 
 | |
| // RequiresDefaultQuality returns if the default quality needs to be applied to
 | |
| // images of this format.
 | |
| func (f Format) RequiresDefaultQuality() bool {
 | |
| 	return f == JPEG || f == WEBP
 | |
| }
 | |
| 
 | |
| // SupportsTransparency reports whether it supports transparency in any form.
 | |
| func (f Format) SupportsTransparency() bool {
 | |
| 	return f != JPEG
 | |
| }
 | |
| 
 | |
| // DefaultExtension returns the default file extension of this format, starting with a dot.
 | |
| // For example: .jpg for JPEG
 | |
| func (f Format) DefaultExtension() string {
 | |
| 	return f.MediaType().FirstSuffix.FullSuffix
 | |
| }
 | |
| 
 | |
| // MediaType returns the media type of this image, e.g. image/jpeg for JPEG
 | |
| func (f Format) MediaType() media.Type {
 | |
| 	switch f {
 | |
| 	case JPEG:
 | |
| 		return media.JPEGType
 | |
| 	case PNG:
 | |
| 		return media.PNGType
 | |
| 	case GIF:
 | |
| 		return media.GIFType
 | |
| 	case TIFF:
 | |
| 		return media.TIFFType
 | |
| 	case BMP:
 | |
| 		return media.BMPType
 | |
| 	case WEBP:
 | |
| 		return media.WEBPType
 | |
| 	default:
 | |
| 		panic(fmt.Sprintf("%d is not a valid image format", f))
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type imageConfig struct {
 | |
| 	config       image.Config
 | |
| 	configInit   sync.Once
 | |
| 	configLoaded bool
 | |
| }
 | |
| 
 | |
| func imageConfigFromImage(img image.Image) image.Config {
 | |
| 	b := img.Bounds()
 | |
| 	return image.Config{Width: b.Max.X, Height: b.Max.Y}
 | |
| }
 | |
| 
 | |
| func ToFilters(in interface{}) []gift.Filter {
 | |
| 	switch v := in.(type) {
 | |
| 	case []gift.Filter:
 | |
| 		return v
 | |
| 	case []filter:
 | |
| 		vv := make([]gift.Filter, len(v))
 | |
| 		for i, f := range v {
 | |
| 			vv[i] = f
 | |
| 		}
 | |
| 		return vv
 | |
| 	case gift.Filter:
 | |
| 		return []gift.Filter{v}
 | |
| 	default:
 | |
| 		panic(fmt.Sprintf("%T is not an image filter", in))
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // IsOpaque returns false if the image has alpha channel and there is at least 1
 | |
| // pixel that is not (fully) opaque.
 | |
| func IsOpaque(img image.Image) bool {
 | |
| 	if oim, ok := img.(interface {
 | |
| 		Opaque() bool
 | |
| 	}); ok {
 | |
| 		return oim.Opaque()
 | |
| 	}
 | |
| 
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| // ImageSource identifies and decodes an image.
 | |
| type ImageSource interface {
 | |
| 	DecodeImage() (image.Image, error)
 | |
| 	Key() string
 | |
| }
 |