Add a set of image filters

With this you can do variants of this:

```
{{ $img := resources.Get "images/misc/3-jenny.jpg" }}
{{ $img := $img.Resize "300x" }}
{{ $g1 := $img.Filter images.Grayscale }}
{{ $g2 := $img | images.Filter (images.Saturate 30) (images.GaussianBlur 3) }}
```

Fixes #6255
This commit is contained in:
Bjørn Erik Pedersen
2019-08-26 19:12:41 +02:00
parent f9978ed164
commit 823f53c861
89 changed files with 791 additions and 139 deletions

View File

@@ -19,7 +19,8 @@ import (
"strconv"
"strings"
"github.com/disintegration/imaging"
"github.com/disintegration/gift"
"github.com/mitchellh/mapstructure"
)
@@ -29,61 +30,59 @@ const (
)
var (
imageFormats = map[string]imaging.Format{
".jpg": imaging.JPEG,
".jpeg": imaging.JPEG,
".png": imaging.PNG,
".tif": imaging.TIFF,
".tiff": imaging.TIFF,
".bmp": imaging.BMP,
".gif": imaging.GIF,
imageFormats = map[string]Format{
".jpg": JPEG,
".jpeg": JPEG,
".png": PNG,
".tif": TIFF,
".tiff": TIFF,
".bmp": BMP,
".gif": GIF,
}
// Add or increment if changes to an image format's processing requires
// re-generation.
imageFormatsVersions = map[imaging.Format]int{
imaging.PNG: 2, // Floyd Steinberg dithering
imageFormatsVersions = map[Format]int{
PNG: 2, // Floyd Steinberg dithering
}
// Increment to mark all processed images as stale. Only use when absolutely needed.
// See the finer grained smartCropVersionNumber and imageFormatsVersions.
mainImageVersionNumber = 0
// Increment to mark all traced SVGs as stale.
traceVersionNumber = 0
)
var anchorPositions = map[string]imaging.Anchor{
strings.ToLower("Center"): imaging.Center,
strings.ToLower("TopLeft"): imaging.TopLeft,
strings.ToLower("Top"): imaging.Top,
strings.ToLower("TopRight"): imaging.TopRight,
strings.ToLower("Left"): imaging.Left,
strings.ToLower("Right"): imaging.Right,
strings.ToLower("BottomLeft"): imaging.BottomLeft,
strings.ToLower("Bottom"): imaging.Bottom,
strings.ToLower("BottomRight"): imaging.BottomRight,
var anchorPositions = map[string]gift.Anchor{
strings.ToLower("Center"): gift.CenterAnchor,
strings.ToLower("TopLeft"): gift.TopLeftAnchor,
strings.ToLower("Top"): gift.TopAnchor,
strings.ToLower("TopRight"): gift.TopRightAnchor,
strings.ToLower("Left"): gift.LeftAnchor,
strings.ToLower("Right"): gift.RightAnchor,
strings.ToLower("BottomLeft"): gift.BottomLeftAnchor,
strings.ToLower("Bottom"): gift.BottomAnchor,
strings.ToLower("BottomRight"): gift.BottomRightAnchor,
}
var imageFilters = map[string]imaging.ResampleFilter{
strings.ToLower("NearestNeighbor"): imaging.NearestNeighbor,
strings.ToLower("Box"): imaging.Box,
strings.ToLower("Linear"): imaging.Linear,
strings.ToLower("Hermite"): imaging.Hermite,
strings.ToLower("MitchellNetravali"): imaging.MitchellNetravali,
strings.ToLower("CatmullRom"): imaging.CatmullRom,
strings.ToLower("BSpline"): imaging.BSpline,
strings.ToLower("Gaussian"): imaging.Gaussian,
strings.ToLower("Lanczos"): imaging.Lanczos,
strings.ToLower("Hann"): imaging.Hann,
strings.ToLower("Hamming"): imaging.Hamming,
strings.ToLower("Blackman"): imaging.Blackman,
strings.ToLower("Bartlett"): imaging.Bartlett,
strings.ToLower("Welch"): imaging.Welch,
strings.ToLower("Cosine"): imaging.Cosine,
var imageFilters = map[string]gift.Resampling{
strings.ToLower("NearestNeighbor"): gift.NearestNeighborResampling,
strings.ToLower("Box"): gift.BoxResampling,
strings.ToLower("Linear"): gift.LinearResampling,
strings.ToLower("Hermite"): hermiteResampling,
strings.ToLower("MitchellNetravali"): mitchellNetravaliResampling,
strings.ToLower("CatmullRom"): catmullRomResampling,
strings.ToLower("BSpline"): bSplineResampling,
strings.ToLower("Gaussian"): gaussianResampling,
strings.ToLower("Lanczos"): gift.LanczosResampling,
strings.ToLower("Hann"): hannResampling,
strings.ToLower("Hamming"): hammingResampling,
strings.ToLower("Blackman"): blackmanResampling,
strings.ToLower("Bartlett"): bartlettResampling,
strings.ToLower("Welch"): welchResampling,
strings.ToLower("Cosine"): cosineResampling,
}
func ImageFormatFromExt(ext string) (imaging.Format, bool) {
func ImageFormatFromExt(ext string) (Format, bool) {
f, found := imageFormats[ext]
return f, found
}
@@ -100,8 +99,8 @@ func DecodeConfig(m map[string]interface{}) (Imaging, error) {
return i, errors.New("JPEG quality must be a number between 1 and 100")
}
if i.Anchor == "" || strings.EqualFold(i.Anchor, SmartCropIdentifier) {
i.Anchor = SmartCropIdentifier
if i.Anchor == "" || strings.EqualFold(i.Anchor, smartCropIdentifier) {
i.Anchor = smartCropIdentifier
} else {
i.Anchor = strings.ToLower(i.Anchor)
if _, found := anchorPositions[i.Anchor]; !found {
@@ -139,8 +138,8 @@ func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, er
for _, part := range parts {
part = strings.ToLower(part)
if part == SmartCropIdentifier {
c.AnchorStr = SmartCropIdentifier
if part == smartCropIdentifier {
c.AnchorStr = smartCropIdentifier
} else if pos, ok := anchorPositions[part]; ok {
c.Anchor = pos
c.AnchorStr = part
@@ -198,7 +197,7 @@ func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, er
if c.AnchorStr == "" {
c.AnchorStr = defaults.Anchor
if !strings.EqualFold(c.AnchorStr, SmartCropIdentifier) {
if !strings.EqualFold(c.AnchorStr, smartCropIdentifier) {
c.Anchor = anchorPositions[c.AnchorStr]
}
}
@@ -210,6 +209,9 @@ func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, er
type ImageConfig struct {
Action string
// If set, this will be used as the key in filenames etc.
Key string
// Quality ranges from 1 to 100 inclusive, higher is better.
// This is only relevant for JPEG images.
// Default is 75.
@@ -222,14 +224,18 @@ type ImageConfig struct {
Width int
Height int
Filter imaging.ResampleFilter
Filter gift.Resampling
FilterStr string
Anchor imaging.Anchor
Anchor gift.Anchor
AnchorStr string
}
func (i ImageConfig) Key(format imaging.Format) string {
func (i ImageConfig) GetKey(format Format) string {
if i.Key != "" {
return i.Action + "_" + i.Key
}
k := strconv.Itoa(i.Width) + "x" + strconv.Itoa(i.Height)
if i.Action != "" {
k += "_" + i.Action
@@ -241,7 +247,7 @@ func (i ImageConfig) Key(format imaging.Format) string {
k += "_r" + strconv.Itoa(i.Rotate)
}
anchor := i.AnchorStr
if anchor == SmartCropIdentifier {
if anchor == smartCropIdentifier {
anchor = anchor + strconv.Itoa(smartCropVersionNumber)
}
@@ -268,9 +274,9 @@ type Imaging struct {
// Default image quality setting (1-100). Only used for JPEG images.
Quality int
// Resample filter used. See https://github.com/disintegration/imaging
// Resample filter to use in resize operations..
ResampleFilter string
// The anchor used in Fill. Default is "smart", i.e. Smart Crop.
// The anchor to use in Fill. Default is "smart", i.e. Smart Crop.
Anchor string
}

168
resources/images/filters.go Normal file
View File

@@ -0,0 +1,168 @@
// 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 provides template functions for manipulating images.
package images
import (
"github.com/disintegration/gift"
"github.com/spf13/cast"
)
// Increment for re-generation of images using these filters.
const filterAPIVersion = 0
type Filters struct {
}
// Brightness creates a filter that changes the brightness of an image.
// The percentage parameter must be in range (-100, 100).
func (*Filters) Brightness(percentage interface{}) gift.Filter {
return filter{
Options: newFilterOpts(percentage),
Filter: gift.Brightness(cast.ToFloat32(percentage)),
}
}
// ColorBalance creates a filter that changes the color balance of an image.
// The percentage parameters for each color channel (red, green, blue) must be in range (-100, 500).
func (*Filters) ColorBalance(percentageRed, percentageGreen, percentageBlue interface{}) gift.Filter {
return filter{
Options: newFilterOpts(percentageRed, percentageGreen, percentageBlue),
Filter: gift.ColorBalance(cast.ToFloat32(percentageRed), cast.ToFloat32(percentageGreen), cast.ToFloat32(percentageBlue)),
}
}
// Colorize creates a filter that produces a colorized version of an image.
// The hue parameter is the angle on the color wheel, typically in range (0, 360).
// The saturation parameter must be in range (0, 100).
// The percentage parameter specifies the strength of the effect, it must be in range (0, 100).
func (*Filters) Colorize(hue, saturation, percentage interface{}) gift.Filter {
return filter{
Options: newFilterOpts(hue, saturation, percentage),
Filter: gift.Colorize(cast.ToFloat32(hue), cast.ToFloat32(saturation), cast.ToFloat32(percentage)),
}
}
// Contrast creates a filter that changes the contrast of an image.
// The percentage parameter must be in range (-100, 100).
func (*Filters) Contrast(percentage interface{}) gift.Filter {
return filter{
Options: newFilterOpts(percentage),
Filter: gift.Contrast(cast.ToFloat32(percentage)),
}
}
// Gamma creates a filter that performs a gamma correction on an image.
// The gamma parameter must be positive. Gamma = 1 gives the original image.
// Gamma less than 1 darkens the image and gamma greater than 1 lightens it.
func (*Filters) Gamma(gamma interface{}) gift.Filter {
return filter{
Options: newFilterOpts(gamma),
Filter: gift.Gamma(cast.ToFloat32(gamma)),
}
}
// GaussianBlur creates a filter that applies a gaussian blur to an image.
func (*Filters) GaussianBlur(sigma interface{}) gift.Filter {
return filter{
Options: newFilterOpts(sigma),
Filter: gift.GaussianBlur(cast.ToFloat32(sigma)),
}
}
// Grayscale creates a filter that produces a grayscale version of an image.
func (*Filters) Grayscale() gift.Filter {
return filter{
Filter: gift.Grayscale(),
}
}
// Hue creates a filter that rotates the hue of an image.
// The hue angle shift is typically in range -180 to 180.
func (*Filters) Hue(shift interface{}) gift.Filter {
return filter{
Options: newFilterOpts(shift),
Filter: gift.Hue(cast.ToFloat32(shift)),
}
}
// Invert creates a filter that negates the colors of an image.
func (*Filters) Invert() gift.Filter {
return filter{
Filter: gift.Invert(),
}
}
// Pixelate creates a filter that applies a pixelation effect to an image.
func (*Filters) Pixelate(size interface{}) gift.Filter {
return filter{
Options: newFilterOpts(size),
Filter: gift.Pixelate(cast.ToInt(size)),
}
}
// Saturation creates a filter that changes the saturation of an image.
func (*Filters) Saturation(percentage interface{}) gift.Filter {
return filter{
Options: newFilterOpts(percentage),
Filter: gift.Saturation(cast.ToFloat32(percentage)),
}
}
// Sepia creates a filter that produces a sepia-toned version of an image.
func (*Filters) Sepia(percentage interface{}) gift.Filter {
return filter{
Options: newFilterOpts(percentage),
Filter: gift.Sepia(cast.ToFloat32(percentage)),
}
}
// Sigmoid creates a filter that changes the contrast of an image using a sigmoidal function and returns the adjusted image.
// It's a non-linear contrast change useful for photo adjustments as it preserves highlight and shadow detail.
func (*Filters) Sigmoid(midpoint, factor interface{}) gift.Filter {
return filter{
Options: newFilterOpts(midpoint, factor),
Filter: gift.Sigmoid(cast.ToFloat32(midpoint), cast.ToFloat32(factor)),
}
}
// UnsharpMask creates a filter that sharpens an image.
// The sigma parameter is used in a gaussian function and affects the radius of effect.
// Sigma must be positive. Sharpen radius roughly equals 3 * sigma.
// The amount parameter controls how much darker and how much lighter the edge borders become. Typically between 0.5 and 1.5.
// The threshold parameter controls the minimum brightness change that will be sharpened. Typically between 0 and 0.05.
func (*Filters) UnsharpMask(sigma, amount, threshold interface{}) gift.Filter {
return filter{
Options: newFilterOpts(sigma, amount, threshold),
Filter: gift.UnsharpMask(cast.ToFloat32(sigma), cast.ToFloat32(amount), cast.ToFloat32(threshold)),
}
}
type filter struct {
Options filterOpts
gift.Filter
}
// For cache-busting.
type filterOpts struct {
Version int
Vals interface{}
}
func newFilterOpts(vals ...interface{}) filterOpts {
return filterOpts{
Version: filterAPIVersion,
Vals: vals,
}
}

View File

@@ -15,16 +15,22 @@ package images
import (
"image"
"image/color"
"image/gif"
"image/jpeg"
"image/png"
"io"
"sync"
"github.com/disintegration/imaging"
"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 imaging.Format, proc *ImageProcessor, img image.Image, s Spec) *Image {
func NewImage(f Format, proc *ImageProcessor, img image.Image, s Spec) *Image {
if img != nil {
return &Image{
Format: f,
@@ -40,7 +46,7 @@ func NewImage(f imaging.Format, proc *ImageProcessor, img image.Image, s Spec) *
}
type Image struct {
Format imaging.Format
Format Format
Proc *ImageProcessor
@@ -51,7 +57,7 @@ type Image struct {
func (i *Image) EncodeTo(conf ImageConfig, img image.Image, w io.Writer) error {
switch i.Format {
case imaging.JPEG:
case JPEG:
var rgba *image.RGBA
quality := conf.Quality
@@ -69,9 +75,23 @@ func (i *Image) EncodeTo(conf ImageConfig, img image.Image, w io.Writer) error {
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 imaging.Encode(w, img, i.Format)
return errors.New("format not supported")
}
}
// Height returns i's height.
@@ -138,19 +158,52 @@ type ImageProcessor struct {
Cfg Imaging
}
func (p *ImageProcessor) Fill(src image.Image, conf ImageConfig) (image.Image, error) {
if conf.AnchorStr == SmartCropIdentifier {
return smartCrop(src, conf.Width, conf.Height, conf.Anchor, conf.Filter)
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))
}
return imaging.Fill(src, conf.Width, conf.Height, conf.Anchor, conf.Filter), nil
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) Fit(src image.Image, conf ImageConfig) (image.Image, error) {
return imaging.Fit(src, conf.Width, conf.Height, conf.Filter), nil
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) Resize(src image.Image, conf ImageConfig) (image.Image, error) {
return imaging.Resize(src, conf.Width, conf.Height, conf.Filter), nil
func (p *ImageProcessor) GetDefaultImageConfig(action string) ImageConfig {
return ImageConfig{
Action: action,
Quality: p.Cfg.Quality,
}
}
type Spec interface {
@@ -158,6 +211,17 @@ type Spec interface {
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

View File

@@ -0,0 +1,214 @@
// 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 "math"
// We moved from imaging to the gift package for image processing at some point.
// That package had more, but also less resampling filters. So we add the missing
// ones here. They are fairly exotic, but someone may use them, so keep them here
// for now.
//
// The filters below are ported from https://github.com/disintegration/imaging/blob/9aab30e6aa535fe3337b489b76759ef97dfaf362/resize.go#L369
// MIT License.
var (
// Hermite cubic spline filter (BC-spline; B=0; C=0).
hermiteResampling = resamp{
name: "Hermite",
support: 1.0,
kernel: func(x float32) float32 {
x = absf32(x)
if x < 1.0 {
return bcspline(x, 0.0, 0.0)
}
return 0
},
}
// Mitchell-Netravali cubic filter (BC-spline; B=1/3; C=1/3).
mitchellNetravaliResampling = resamp{
name: "MitchellNetravali",
support: 2.0,
kernel: func(x float32) float32 {
x = absf32(x)
if x < 2.0 {
return bcspline(x, 1.0/3.0, 1.0/3.0)
}
return 0
},
}
// Catmull-Rom - sharp cubic filter (BC-spline; B=0; C=0.5).
catmullRomResampling = resamp{
name: "CatmullRomResampling",
support: 2.0,
kernel: func(x float32) float32 {
x = absf32(x)
if x < 2.0 {
return bcspline(x, 0.0, 0.5)
}
return 0
},
}
// BSpline is a smooth cubic filter (BC-spline; B=1; C=0).
bSplineResampling = resamp{
name: "BSplineResampling",
support: 2.0,
kernel: func(x float32) float32 {
x = absf32(x)
if x < 2.0 {
return bcspline(x, 1.0, 0.0)
}
return 0
},
}
// Gaussian blurring filter.
gaussianResampling = resamp{
name: "GaussianResampling",
support: 2.0,
kernel: func(x float32) float32 {
x = absf32(x)
if x < 2.0 {
return float32(math.Exp(float64(-2 * x * x)))
}
return 0
},
}
// Hann-windowed sinc filter (3 lobes).
hannResampling = resamp{
name: "HannResampling",
support: 3.0,
kernel: func(x float32) float32 {
x = absf32(x)
if x < 3.0 {
return sinc(x) * float32(0.5+0.5*math.Cos(math.Pi*float64(x)/3.0))
}
return 0
},
}
hammingResampling = resamp{
name: "HammingResampling",
support: 3.0,
kernel: func(x float32) float32 {
x = absf32(x)
if x < 3.0 {
return sinc(x) * float32(0.54+0.46*math.Cos(math.Pi*float64(x)/3.0))
}
return 0
},
}
// Blackman-windowed sinc filter (3 lobes).
blackmanResampling = resamp{
name: "BlackmanResampling",
support: 3.0,
kernel: func(x float32) float32 {
x = absf32(x)
if x < 3.0 {
return sinc(x) * float32(0.42-0.5*math.Cos(math.Pi*float64(x)/3.0+math.Pi)+0.08*math.Cos(2.0*math.Pi*float64(x)/3.0))
}
return 0
},
}
bartlettResampling = resamp{
name: "BartlettResampling",
support: 3.0,
kernel: func(x float32) float32 {
x = absf32(x)
if x < 3.0 {
return sinc(x) * (3.0 - x) / 3.0
}
return 0
},
}
// Welch-windowed sinc filter (parabolic window, 3 lobes).
welchResampling = resamp{
name: "WelchResampling",
support: 3.0,
kernel: func(x float32) float32 {
x = absf32(x)
if x < 3.0 {
return sinc(x) * (1.0 - (x * x / 9.0))
}
return 0
},
}
// Cosine-windowed sinc filter (3 lobes).
cosineResampling = resamp{
name: "CosineResampling",
support: 3.0,
kernel: func(x float32) float32 {
x = absf32(x)
if x < 3.0 {
return sinc(x) * float32(math.Cos((math.Pi/2.0)*(float64(x)/3.0)))
}
return 0
},
}
)
// The following code is borrowed from https://raw.githubusercontent.com/disintegration/gift/master/resize.go
// MIT licensed.
type resamp struct {
name string
support float32
kernel func(float32) float32
}
func (r resamp) String() string {
return r.name
}
func (r resamp) Support() float32 {
return r.support
}
func (r resamp) Kernel(x float32) float32 {
return r.kernel(x)
}
func bcspline(x, b, c float32) float32 {
if x < 0 {
x = -x
}
if x < 1 {
return ((12-9*b-6*c)*x*x*x + (-18+12*b+6*c)*x*x + (6 - 2*b)) / 6
}
if x < 2 {
return ((-b-6*c)*x*x*x + (6*b+30*c)*x*x + (-12*b-48*c)*x + (8*b + 24*c)) / 6
}
return 0
}
func absf32(x float32) float32 {
if x < 0 {
return -x
}
return x
}
func sinc(x float32) float32 {
if x == 0 {
return 1
}
return float32(math.Sin(math.Pi*float64(x)) / (math.Pi * float64(x)))
}

View File

@@ -16,36 +16,38 @@ package images
import (
"image"
"github.com/disintegration/imaging"
"github.com/disintegration/gift"
"github.com/muesli/smartcrop"
)
const (
// Do not change.
// TODO(bep) image unexport
SmartCropIdentifier = "smart"
smartCropIdentifier = "smart"
// This is just a increment, starting on 1. If Smart Crop improves its cropping, we
// need a way to trigger a re-generation of the crops in the wild, so increment this.
smartCropVersionNumber = 1
)
func newSmartCropAnalyzer(filter imaging.ResampleFilter) smartcrop.Analyzer {
return smartcrop.NewAnalyzer(imagingResizer{filter: filter})
func (p *ImageProcessor) newSmartCropAnalyzer(filter gift.Resampling) smartcrop.Analyzer {
return smartcrop.NewAnalyzer(imagingResizer{p: p, filter: filter})
}
// Needed by smartcrop
type imagingResizer struct {
filter imaging.ResampleFilter
p *ImageProcessor
filter gift.Resampling
}
func (r imagingResizer) Resize(img image.Image, width, height uint) image.Image {
return imaging.Resize(img, int(width), int(height), r.filter)
result, _ := r.p.Filter(img, gift.Resize(int(width), int(height), r.filter))
return result
}
func smartCrop(img image.Image, width, height int, anchor imaging.Anchor, filter imaging.ResampleFilter) (*image.NRGBA, error) {
func (p *ImageProcessor) smartCrop(img image.Image, width, height int, filter gift.Resampling) (image.Rectangle, error) {
if width <= 0 || height <= 0 {
return &image.NRGBA{}, nil
return image.Rectangle{}, nil
}
srcBounds := img.Bounds()
@@ -53,23 +55,20 @@ func smartCrop(img image.Image, width, height int, anchor imaging.Anchor, filter
srcH := srcBounds.Dy()
if srcW <= 0 || srcH <= 0 {
return &image.NRGBA{}, nil
return image.Rectangle{}, nil
}
if srcW == width && srcH == height {
return imaging.Clone(img), nil
return srcBounds, nil
}
smart := newSmartCropAnalyzer(filter)
smart := p.newSmartCropAnalyzer(filter)
rect, err := smart.FindBestCrop(img, width, height)
if err != nil {
return nil, err
return image.Rectangle{}, err
}
b := img.Bounds().Intersect(rect)
return img.Bounds().Intersect(rect), nil
cropped := imaging.Crop(img, b)
return imaging.Resize(cropped, width, height, filter), nil
}