mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
28143397d6
Note that we will probably need to add some metadata cache for this to scale. Fixes #4600
261 lines
5.5 KiB
Go
261 lines
5.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 (
|
|
"image"
|
|
"image/color"
|
|
"image/gif"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"io"
|
|
"sync"
|
|
|
|
"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 i.Format {
|
|
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)
|
|
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
|
|
}
|
|
|
|
func (i *Image) initConfig() error {
|
|
var err error
|
|
i.configInit.Do(func() {
|
|
if i.configLoaded {
|
|
return
|
|
}
|
|
|
|
var (
|
|
f hugio.ReadSeekCloser
|
|
config image.Config
|
|
)
|
|
|
|
f, err = i.Spec.ReadSeekCloser()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
config, _, err = image.DecodeConfig(f)
|
|
if err != nil {
|
|
return
|
|
}
|
|
i.config = config
|
|
})
|
|
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to load image config")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func NewImageProcessor(cfg Imaging) (*ImageProcessor, error) {
|
|
e := 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 Imaging
|
|
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 "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)
|
|
}
|
|
|
|
return p.Filter(src, filters...)
|
|
}
|
|
|
|
func (p *ImageProcessor) Filter(src image.Image, filters ...gift.Filter) (image.Image, error) {
|
|
g := gift.New(filters...)
|
|
dst := image.NewRGBA(g.Bounds(src.Bounds()))
|
|
g.Draw(dst, src)
|
|
return dst, nil
|
|
}
|
|
|
|
func (p *ImageProcessor) GetDefaultImageConfig(action string) ImageConfig {
|
|
return ImageConfig{
|
|
Action: action,
|
|
Quality: p.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
|
|
)
|
|
|
|
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}
|
|
}
|