github.com/neohugo/neohugo@v0.123.8/resources/images/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 images
    15  
    16  import (
    17  	"errors"
    18  	"fmt"
    19  	"image"
    20  	"image/color"
    21  	"image/draw"
    22  	"image/gif"
    23  	"image/jpeg"
    24  	"image/png"
    25  	"io"
    26  	"sync"
    27  
    28  	"github.com/bep/gowebp/libwebp/webpoptions"
    29  	"github.com/neohugo/neohugo/config"
    30  	"github.com/neohugo/neohugo/resources/images/webp"
    31  
    32  	"github.com/neohugo/neohugo/media"
    33  	"github.com/neohugo/neohugo/resources/images/exif"
    34  
    35  	"github.com/disintegration/gift"
    36  	"golang.org/x/image/bmp"
    37  	"golang.org/x/image/tiff"
    38  
    39  	"github.com/neohugo/neohugo/common/hugio"
    40  )
    41  
    42  func NewImage(f Format, proc *ImageProcessor, img image.Image, s Spec) *Image {
    43  	if img != nil {
    44  		return &Image{
    45  			Format: f,
    46  			Proc:   proc,
    47  			Spec:   s,
    48  			imageConfig: &imageConfig{
    49  				config:       imageConfigFromImage(img),
    50  				configLoaded: true,
    51  			},
    52  		}
    53  	}
    54  	return &Image{Format: f, Proc: proc, Spec: s, imageConfig: &imageConfig{}}
    55  }
    56  
    57  type Image struct {
    58  	Format Format
    59  	Proc   *ImageProcessor
    60  	Spec   Spec
    61  	*imageConfig
    62  }
    63  
    64  func (i *Image) EncodeTo(conf ImageConfig, img image.Image, w io.Writer) error {
    65  	switch conf.TargetFormat {
    66  	case JPEG:
    67  
    68  		var rgba *image.RGBA
    69  		quality := conf.Quality
    70  
    71  		if nrgba, ok := img.(*image.NRGBA); ok {
    72  			if nrgba.Opaque() {
    73  				rgba = &image.RGBA{
    74  					Pix:    nrgba.Pix,
    75  					Stride: nrgba.Stride,
    76  					Rect:   nrgba.Rect,
    77  				}
    78  			}
    79  		}
    80  		if rgba != nil {
    81  			return jpeg.Encode(w, rgba, &jpeg.Options{Quality: quality})
    82  		}
    83  		return jpeg.Encode(w, img, &jpeg.Options{Quality: quality})
    84  	case PNG:
    85  		encoder := png.Encoder{CompressionLevel: png.DefaultCompression}
    86  		return encoder.Encode(w, img)
    87  
    88  	case GIF:
    89  		if giphy, ok := img.(Giphy); ok {
    90  			g := giphy.GIF()
    91  			return gif.EncodeAll(w, g)
    92  		}
    93  		return gif.Encode(w, img, &gif.Options{
    94  			NumColors: 256,
    95  		})
    96  	case TIFF:
    97  		return tiff.Encode(w, img, &tiff.Options{Compression: tiff.Deflate, Predictor: true})
    98  
    99  	case BMP:
   100  		return bmp.Encode(w, img)
   101  	case WEBP:
   102  		return webp.Encode(
   103  			w,
   104  			img, webpoptions.EncodingOptions{
   105  				Quality:        conf.Quality,
   106  				EncodingPreset: webpoptions.EncodingPreset(conf.Hint),
   107  				UseSharpYuv:    true,
   108  			},
   109  		)
   110  	default:
   111  		return errors.New("format not supported")
   112  	}
   113  }
   114  
   115  // Height returns i's height.
   116  func (i *Image) Height() int {
   117  	//nolint
   118  	i.initConfig()
   119  	return i.config.Height
   120  }
   121  
   122  // Width returns i's width.
   123  func (i *Image) Width() int {
   124  	//nolint
   125  	i.initConfig()
   126  	return i.config.Width
   127  }
   128  
   129  func (i Image) WithImage(img image.Image) *Image {
   130  	i.Spec = nil
   131  	i.imageConfig = &imageConfig{
   132  		config:       imageConfigFromImage(img),
   133  		configLoaded: true,
   134  	}
   135  
   136  	return &i
   137  }
   138  
   139  func (i Image) WithSpec(s Spec) *Image {
   140  	i.Spec = s
   141  	i.imageConfig = &imageConfig{}
   142  	return &i
   143  }
   144  
   145  // InitConfig reads the image config from the given reader.
   146  func (i *Image) InitConfig(r io.Reader) error {
   147  	var err error
   148  	i.configInit.Do(func() {
   149  		i.config, _, err = image.DecodeConfig(r)
   150  	})
   151  	return err
   152  }
   153  
   154  func (i *Image) initConfig() error {
   155  	var err error
   156  	i.configInit.Do(func() {
   157  		if i.configLoaded {
   158  			return
   159  		}
   160  
   161  		var f hugio.ReadSeekCloser
   162  
   163  		f, err = i.Spec.ReadSeekCloser()
   164  		if err != nil {
   165  			return
   166  		}
   167  		defer f.Close()
   168  
   169  		i.config, _, err = image.DecodeConfig(f)
   170  	})
   171  
   172  	if err != nil {
   173  		return fmt.Errorf("failed to load image config: %w", err)
   174  	}
   175  
   176  	return nil
   177  }
   178  
   179  func NewImageProcessor(cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) (*ImageProcessor, error) {
   180  	e := cfg.Config.Imaging.Exif
   181  	exifDecoder, err := exif.NewDecoder(
   182  		exif.WithDateDisabled(e.DisableDate),
   183  		exif.WithLatLongDisabled(e.DisableLatLong),
   184  		exif.ExcludeFields(e.ExcludeFields),
   185  		exif.IncludeFields(e.IncludeFields),
   186  	)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  
   191  	return &ImageProcessor{
   192  		Cfg:         cfg,
   193  		exifDecoder: exifDecoder,
   194  	}, nil
   195  }
   196  
   197  type ImageProcessor struct {
   198  	Cfg         *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]
   199  	exifDecoder *exif.Decoder
   200  }
   201  
   202  func (p *ImageProcessor) DecodeExif(r io.Reader) (*exif.ExifInfo, error) {
   203  	return p.exifDecoder.Decode(r)
   204  }
   205  
   206  func (p *ImageProcessor) FiltersFromConfig(src image.Image, conf ImageConfig) ([]gift.Filter, error) {
   207  	var filters []gift.Filter
   208  
   209  	if conf.Rotate != 0 {
   210  		// Apply any rotation before any resize.
   211  		filters = append(filters, gift.Rotate(float32(conf.Rotate), color.Transparent, gift.NearestNeighborInterpolation))
   212  	}
   213  
   214  	switch conf.Action {
   215  	case "resize":
   216  		filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter))
   217  	case "crop":
   218  		if conf.AnchorStr == smartCropIdentifier {
   219  			bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter)
   220  			if err != nil {
   221  				return nil, err
   222  			}
   223  
   224  			// First crop using the bounds returned by smartCrop.
   225  			filters = append(filters, gift.Crop(bounds))
   226  			// Then center crop the image to get an image the desired size without resizing.
   227  			filters = append(filters, gift.CropToSize(conf.Width, conf.Height, gift.CenterAnchor))
   228  
   229  		} else {
   230  			filters = append(filters, gift.CropToSize(conf.Width, conf.Height, conf.Anchor))
   231  		}
   232  	case "fill":
   233  		if conf.AnchorStr == smartCropIdentifier {
   234  			bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter)
   235  			if err != nil {
   236  				return nil, err
   237  			}
   238  
   239  			// First crop it, then resize it.
   240  			filters = append(filters, gift.Crop(bounds))
   241  			filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter))
   242  
   243  		} else {
   244  			filters = append(filters, gift.ResizeToFill(conf.Width, conf.Height, conf.Filter, conf.Anchor))
   245  		}
   246  	case "fit":
   247  		filters = append(filters, gift.ResizeToFit(conf.Width, conf.Height, conf.Filter))
   248  	default:
   249  
   250  	}
   251  	return filters, nil
   252  }
   253  
   254  func (p *ImageProcessor) ApplyFiltersFromConfig(src image.Image, conf ImageConfig) (image.Image, error) {
   255  	filters, err := p.FiltersFromConfig(src, conf)
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  
   260  	if len(filters) == 0 {
   261  		return p.resolveSrc(src, conf.TargetFormat), nil
   262  	}
   263  
   264  	img, err := p.doFilter(src, conf.TargetFormat, filters...)
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  
   269  	return img, nil
   270  }
   271  
   272  func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.Image, error) {
   273  	return p.doFilter(src, 0, filters...)
   274  }
   275  
   276  func (p *ImageProcessor) resolveSrc(src image.Image, targetFormat Format) image.Image {
   277  	if giph, ok := src.(Giphy); ok {
   278  		g := giph.GIF()
   279  		if len(g.Image) < 2 || (targetFormat == 0 || targetFormat != GIF) {
   280  			src = g.Image[0]
   281  		}
   282  	}
   283  	return src
   284  }
   285  
   286  func (p *ImageProcessor) doFilter(src image.Image, targetFormat Format, filters ...gift.Filter) (image.Image, error) {
   287  	filter := gift.New(filters...)
   288  
   289  	if giph, ok := src.(Giphy); ok {
   290  		g := giph.GIF()
   291  		if len(g.Image) < 2 || (targetFormat == 0 || targetFormat != GIF) {
   292  			src = g.Image[0]
   293  		} else {
   294  			var bounds image.Rectangle
   295  			firstFrame := g.Image[0]
   296  			tmp := image.NewNRGBA(firstFrame.Bounds())
   297  			for i := range g.Image {
   298  				gift.New().DrawAt(tmp, g.Image[i], g.Image[i].Bounds().Min, gift.OverOperator)
   299  				bounds = filter.Bounds(tmp.Bounds())
   300  				dst := image.NewPaletted(bounds, g.Image[i].Palette)
   301  				filter.Draw(dst, tmp)
   302  				g.Image[i] = dst
   303  			}
   304  			g.Config.Width = bounds.Dx()
   305  			g.Config.Height = bounds.Dy()
   306  
   307  			return giph, nil
   308  		}
   309  	}
   310  
   311  	bounds := filter.Bounds(src.Bounds())
   312  
   313  	var dst draw.Image
   314  	switch src.(type) {
   315  	case *image.RGBA:
   316  		dst = image.NewRGBA(bounds)
   317  	case *image.NRGBA:
   318  		dst = image.NewNRGBA(bounds)
   319  	case *image.Gray:
   320  		dst = image.NewGray(bounds)
   321  	default:
   322  		dst = image.NewNRGBA(bounds)
   323  	}
   324  	filter.Draw(dst, src)
   325  
   326  	return dst, nil
   327  }
   328  
   329  func GetDefaultImageConfig(action string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) ImageConfig {
   330  	if defaults == nil {
   331  		defaults = defaultImageConfig
   332  	}
   333  	return ImageConfig{
   334  		Action:  action,
   335  		Hint:    defaults.Config.Hint,
   336  		Quality: defaults.Config.Imaging.Quality,
   337  	}
   338  }
   339  
   340  type Spec interface {
   341  	// Loads the image source.
   342  	ReadSeekCloser() (hugio.ReadSeekCloser, error)
   343  }
   344  
   345  // Format is an image file format.
   346  type Format int
   347  
   348  const (
   349  	JPEG Format = iota + 1
   350  	PNG
   351  	GIF
   352  	TIFF
   353  	BMP
   354  	WEBP
   355  )
   356  
   357  // RequiresDefaultQuality returns if the default quality needs to be applied to
   358  // images of this format.
   359  func (f Format) RequiresDefaultQuality() bool {
   360  	return f == JPEG || f == WEBP
   361  }
   362  
   363  // SupportsTransparency reports whether it supports transparency in any form.
   364  func (f Format) SupportsTransparency() bool {
   365  	return f != JPEG
   366  }
   367  
   368  // DefaultExtension returns the default file extension of this format, starting with a dot.
   369  // For example: .jpg for JPEG
   370  func (f Format) DefaultExtension() string {
   371  	return f.MediaType().FirstSuffix.FullSuffix
   372  }
   373  
   374  // MediaType returns the media type of this image, e.g. image/jpeg for JPEG
   375  func (f Format) MediaType() media.Type {
   376  	switch f {
   377  	case JPEG:
   378  		return media.Builtin.JPEGType
   379  	case PNG:
   380  		return media.Builtin.PNGType
   381  	case GIF:
   382  		return media.Builtin.GIFType
   383  	case TIFF:
   384  		return media.Builtin.TIFFType
   385  	case BMP:
   386  		return media.Builtin.BMPType
   387  	case WEBP:
   388  		return media.Builtin.WEBPType
   389  	default:
   390  		panic(fmt.Sprintf("%d is not a valid image format", f))
   391  	}
   392  }
   393  
   394  type imageConfig struct {
   395  	config       image.Config
   396  	configInit   sync.Once //nolint
   397  	configLoaded bool
   398  }
   399  
   400  func imageConfigFromImage(img image.Image) image.Config {
   401  	if giphy, ok := img.(Giphy); ok {
   402  		return giphy.GIF().Config
   403  	}
   404  	b := img.Bounds()
   405  	return image.Config{Width: b.Max.X, Height: b.Max.Y}
   406  }
   407  
   408  // UnwrapFilter unwraps the given filter if it is a filter wrapper.
   409  func UnwrapFilter(in gift.Filter) gift.Filter {
   410  	if f, ok := in.(filter); ok {
   411  		return f.Filter
   412  	}
   413  	return in
   414  }
   415  
   416  // ToFilters converts the given input to a slice of gift.Filter.
   417  func ToFilters(in any) []gift.Filter {
   418  	switch v := in.(type) {
   419  	case []gift.Filter:
   420  		return v
   421  	case []filter:
   422  		vv := make([]gift.Filter, len(v))
   423  		for i, f := range v {
   424  			vv[i] = f
   425  		}
   426  		return vv
   427  	case gift.Filter:
   428  		return []gift.Filter{v}
   429  	default:
   430  		panic(fmt.Sprintf("%T is not an image filter", in))
   431  	}
   432  }
   433  
   434  // IsOpaque returns false if the image has alpha channel and there is at least 1
   435  // pixel that is not (fully) opaque.
   436  func IsOpaque(img image.Image) bool {
   437  	if oim, ok := img.(interface {
   438  		Opaque() bool
   439  	}); ok {
   440  		return oim.Opaque()
   441  	}
   442  
   443  	return false
   444  }
   445  
   446  // ImageSource identifies and decodes an image.
   447  type ImageSource interface {
   448  	DecodeImage() (image.Image, error)
   449  	Key() string
   450  }
   451  
   452  // Giphy represents a GIF Image that may be animated.
   453  type Giphy interface {
   454  	image.Image    // The first frame.
   455  	GIF() *gif.GIF // All frames.
   456  }