hugo

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

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

image.go (9500B)

    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 	"fmt"
   18 	"image"
   19 	"image/color"
   20 	"image/draw"
   21 	"image/gif"
   22 	"image/jpeg"
   23 	"image/png"
   24 	"io"
   25 	"sync"
   26 
   27 	"github.com/bep/gowebp/libwebp/webpoptions"
   28 	"github.com/gohugoio/hugo/resources/images/webp"
   29 
   30 	"github.com/gohugoio/hugo/media"
   31 	"github.com/gohugoio/hugo/resources/images/exif"
   32 
   33 	"github.com/disintegration/gift"
   34 	"golang.org/x/image/bmp"
   35 	"golang.org/x/image/tiff"
   36 
   37 	"errors"
   38 
   39 	"github.com/gohugoio/hugo/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 	i.initConfig()
  118 	return i.config.Height
  119 }
  120 
  121 // Width returns i's width.
  122 func (i *Image) Width() int {
  123 	i.initConfig()
  124 	return i.config.Width
  125 }
  126 
  127 func (i Image) WithImage(img image.Image) *Image {
  128 	i.Spec = nil
  129 	i.imageConfig = &imageConfig{
  130 		config:       imageConfigFromImage(img),
  131 		configLoaded: true,
  132 	}
  133 
  134 	return &i
  135 }
  136 
  137 func (i Image) WithSpec(s Spec) *Image {
  138 	i.Spec = s
  139 	i.imageConfig = &imageConfig{}
  140 	return &i
  141 }
  142 
  143 // InitConfig reads the image config from the given reader.
  144 func (i *Image) InitConfig(r io.Reader) error {
  145 	var err error
  146 	i.configInit.Do(func() {
  147 		i.config, _, err = image.DecodeConfig(r)
  148 	})
  149 	return err
  150 }
  151 
  152 func (i *Image) initConfig() error {
  153 	var err error
  154 	i.configInit.Do(func() {
  155 		if i.configLoaded {
  156 			return
  157 		}
  158 
  159 		var f hugio.ReadSeekCloser
  160 
  161 		f, err = i.Spec.ReadSeekCloser()
  162 		if err != nil {
  163 			return
  164 		}
  165 		defer f.Close()
  166 
  167 		i.config, _, err = image.DecodeConfig(f)
  168 	})
  169 
  170 	if err != nil {
  171 		return fmt.Errorf("failed to load image config: %w", err)
  172 	}
  173 
  174 	return nil
  175 }
  176 
  177 func NewImageProcessor(cfg ImagingConfig) (*ImageProcessor, error) {
  178 	e := cfg.Cfg.Exif
  179 	exifDecoder, err := exif.NewDecoder(
  180 		exif.WithDateDisabled(e.DisableDate),
  181 		exif.WithLatLongDisabled(e.DisableLatLong),
  182 		exif.ExcludeFields(e.ExcludeFields),
  183 		exif.IncludeFields(e.IncludeFields),
  184 	)
  185 	if err != nil {
  186 		return nil, err
  187 	}
  188 
  189 	return &ImageProcessor{
  190 		Cfg:         cfg,
  191 		exifDecoder: exifDecoder,
  192 	}, nil
  193 }
  194 
  195 type ImageProcessor struct {
  196 	Cfg         ImagingConfig
  197 	exifDecoder *exif.Decoder
  198 }
  199 
  200 func (p *ImageProcessor) DecodeExif(r io.Reader) (*exif.ExifInfo, error) {
  201 	return p.exifDecoder.Decode(r)
  202 }
  203 
  204 func (p *ImageProcessor) ApplyFiltersFromConfig(src image.Image, conf ImageConfig) (image.Image, error) {
  205 	var filters []gift.Filter
  206 
  207 	if conf.Rotate != 0 {
  208 		// Apply any rotation before any resize.
  209 		filters = append(filters, gift.Rotate(float32(conf.Rotate), color.Transparent, gift.NearestNeighborInterpolation))
  210 	}
  211 
  212 	switch conf.Action {
  213 	case "resize":
  214 		filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter))
  215 	case "crop":
  216 		if conf.AnchorStr == smartCropIdentifier {
  217 			bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter)
  218 			if err != nil {
  219 				return nil, err
  220 			}
  221 
  222 			// First crop using the bounds returned by smartCrop.
  223 			filters = append(filters, gift.Crop(bounds))
  224 			// Then center crop the image to get an image the desired size without resizing.
  225 			filters = append(filters, gift.CropToSize(conf.Width, conf.Height, gift.CenterAnchor))
  226 
  227 		} else {
  228 			filters = append(filters, gift.CropToSize(conf.Width, conf.Height, conf.Anchor))
  229 		}
  230 	case "fill":
  231 		if conf.AnchorStr == smartCropIdentifier {
  232 			bounds, err := p.smartCrop(src, conf.Width, conf.Height, conf.Filter)
  233 			if err != nil {
  234 				return nil, err
  235 			}
  236 
  237 			// First crop it, then resize it.
  238 			filters = append(filters, gift.Crop(bounds))
  239 			filters = append(filters, gift.Resize(conf.Width, conf.Height, conf.Filter))
  240 
  241 		} else {
  242 			filters = append(filters, gift.ResizeToFill(conf.Width, conf.Height, conf.Filter, conf.Anchor))
  243 		}
  244 	case "fit":
  245 		filters = append(filters, gift.ResizeToFit(conf.Width, conf.Height, conf.Filter))
  246 	default:
  247 		return nil, fmt.Errorf("unsupported action: %q", conf.Action)
  248 	}
  249 
  250 	img, err := p.Filter(src, filters...)
  251 	if err != nil {
  252 		return nil, err
  253 	}
  254 
  255 	return img, nil
  256 }
  257 
  258 func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.Image, error) {
  259 
  260 	filter := gift.New(filters...)
  261 
  262 	if giph, ok := src.(Giphy); ok && len(giph.GIF().Image) > 1 {
  263 		g := giph.GIF()
  264 		var bounds image.Rectangle
  265 		firstFrame := g.Image[0]
  266 		tmp := image.NewNRGBA(firstFrame.Bounds())
  267 		for i := range g.Image {
  268 			gift.New().DrawAt(tmp, g.Image[i], g.Image[i].Bounds().Min, gift.OverOperator)
  269 			bounds = filter.Bounds(tmp.Bounds())
  270 			dst := image.NewPaletted(bounds, g.Image[i].Palette)
  271 			filter.Draw(dst, tmp)
  272 			g.Image[i] = dst
  273 		}
  274 		g.Config.Width = bounds.Dx()
  275 		g.Config.Height = bounds.Dy()
  276 
  277 		return giph, nil
  278 	}
  279 
  280 	bounds := filter.Bounds(src.Bounds())
  281 
  282 	var dst draw.Image
  283 	switch src.(type) {
  284 	case *image.RGBA:
  285 		dst = image.NewRGBA(bounds)
  286 	case *image.NRGBA:
  287 		dst = image.NewNRGBA(bounds)
  288 	case *image.Gray:
  289 		dst = image.NewGray(bounds)
  290 	default:
  291 		dst = image.NewNRGBA(bounds)
  292 	}
  293 	filter.Draw(dst, src)
  294 
  295 	return dst, nil
  296 }
  297 
  298 func GetDefaultImageConfig(action string, defaults ImagingConfig) ImageConfig {
  299 	return ImageConfig{
  300 		Action:  action,
  301 		Hint:    defaults.Hint,
  302 		Quality: defaults.Cfg.Quality,
  303 	}
  304 }
  305 
  306 type Spec interface {
  307 	// Loads the image source.
  308 	ReadSeekCloser() (hugio.ReadSeekCloser, error)
  309 }
  310 
  311 // Format is an image file format.
  312 type Format int
  313 
  314 const (
  315 	JPEG Format = iota + 1
  316 	PNG
  317 	GIF
  318 	TIFF
  319 	BMP
  320 	WEBP
  321 )
  322 
  323 // RequiresDefaultQuality returns if the default quality needs to be applied to
  324 // images of this format.
  325 func (f Format) RequiresDefaultQuality() bool {
  326 	return f == JPEG || f == WEBP
  327 }
  328 
  329 // SupportsTransparency reports whether it supports transparency in any form.
  330 func (f Format) SupportsTransparency() bool {
  331 	return f != JPEG
  332 }
  333 
  334 // DefaultExtension returns the default file extension of this format, starting with a dot.
  335 // For example: .jpg for JPEG
  336 func (f Format) DefaultExtension() string {
  337 	return f.MediaType().FirstSuffix.FullSuffix
  338 }
  339 
  340 // MediaType returns the media type of this image, e.g. image/jpeg for JPEG
  341 func (f Format) MediaType() media.Type {
  342 	switch f {
  343 	case JPEG:
  344 		return media.JPEGType
  345 	case PNG:
  346 		return media.PNGType
  347 	case GIF:
  348 		return media.GIFType
  349 	case TIFF:
  350 		return media.TIFFType
  351 	case BMP:
  352 		return media.BMPType
  353 	case WEBP:
  354 		return media.WEBPType
  355 	default:
  356 		panic(fmt.Sprintf("%d is not a valid image format", f))
  357 	}
  358 }
  359 
  360 type imageConfig struct {
  361 	config       image.Config
  362 	configInit   sync.Once
  363 	configLoaded bool
  364 }
  365 
  366 func imageConfigFromImage(img image.Image) image.Config {
  367 	b := img.Bounds()
  368 	return image.Config{Width: b.Max.X, Height: b.Max.Y}
  369 }
  370 
  371 func ToFilters(in any) []gift.Filter {
  372 	switch v := in.(type) {
  373 	case []gift.Filter:
  374 		return v
  375 	case []filter:
  376 		vv := make([]gift.Filter, len(v))
  377 		for i, f := range v {
  378 			vv[i] = f
  379 		}
  380 		return vv
  381 	case gift.Filter:
  382 		return []gift.Filter{v}
  383 	default:
  384 		panic(fmt.Sprintf("%T is not an image filter", in))
  385 	}
  386 }
  387 
  388 // IsOpaque returns false if the image has alpha channel and there is at least 1
  389 // pixel that is not (fully) opaque.
  390 func IsOpaque(img image.Image) bool {
  391 	if oim, ok := img.(interface {
  392 		Opaque() bool
  393 	}); ok {
  394 		return oim.Opaque()
  395 	}
  396 
  397 	return false
  398 }
  399 
  400 // ImageSource identifies and decodes an image.
  401 type ImageSource interface {
  402 	DecodeImage() (image.Image, error)
  403 	Key() string
  404 }
  405 
  406 // Giphy represents a GIF Image that may be animated.
  407 type Giphy interface {
  408 	image.Image    // The first frame.
  409 	GIF() *gif.GIF // All frames.
  410 }