mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-30 22:39:58 +02:00
Image resource refactor
This commit pulls most of the image related logic into its own package, to make it easier to reason about and extend. This is also a rewrite of the transformation logic used in Hugo Pipes, mostly to allow constructs like the one below: {{ ($myimg | fingerprint ).Width }} Fixes #5903 Fixes #6234 Fixes #6266
This commit is contained in:
276
resources/images/config.go
Normal file
276
resources/images/config.go
Normal file
@@ -0,0 +1,276 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultJPEGQuality = 75
|
||||
defaultResampleFilter = "box"
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 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,
|
||||
}
|
||||
|
||||
func ImageFormatFromExt(ext string) (imaging.Format, bool) {
|
||||
f, found := imageFormats[ext]
|
||||
return f, found
|
||||
}
|
||||
|
||||
func DecodeConfig(m map[string]interface{}) (Imaging, error) {
|
||||
var i Imaging
|
||||
if err := mapstructure.WeakDecode(m, &i); err != nil {
|
||||
return i, err
|
||||
}
|
||||
|
||||
if i.Quality == 0 {
|
||||
i.Quality = defaultJPEGQuality
|
||||
} else if i.Quality < 0 || i.Quality > 100 {
|
||||
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
|
||||
} else {
|
||||
i.Anchor = strings.ToLower(i.Anchor)
|
||||
if _, found := anchorPositions[i.Anchor]; !found {
|
||||
return i, errors.New("invalid anchor value in imaging config")
|
||||
}
|
||||
}
|
||||
|
||||
if i.ResampleFilter == "" {
|
||||
i.ResampleFilter = defaultResampleFilter
|
||||
} else {
|
||||
filter := strings.ToLower(i.ResampleFilter)
|
||||
_, found := imageFilters[filter]
|
||||
if !found {
|
||||
return i, fmt.Errorf("%q is not a valid resample filter", filter)
|
||||
}
|
||||
i.ResampleFilter = filter
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func DecodeImageConfig(action, config string, defaults Imaging) (ImageConfig, error) {
|
||||
var (
|
||||
c ImageConfig
|
||||
err error
|
||||
)
|
||||
|
||||
c.Action = action
|
||||
|
||||
if config == "" {
|
||||
return c, errors.New("image config cannot be empty")
|
||||
}
|
||||
|
||||
parts := strings.Fields(config)
|
||||
for _, part := range parts {
|
||||
part = strings.ToLower(part)
|
||||
|
||||
if part == SmartCropIdentifier {
|
||||
c.AnchorStr = SmartCropIdentifier
|
||||
} else if pos, ok := anchorPositions[part]; ok {
|
||||
c.Anchor = pos
|
||||
c.AnchorStr = part
|
||||
} else if filter, ok := imageFilters[part]; ok {
|
||||
c.Filter = filter
|
||||
c.FilterStr = part
|
||||
} else if part[0] == 'q' {
|
||||
c.Quality, err = strconv.Atoi(part[1:])
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
if c.Quality < 1 || c.Quality > 100 {
|
||||
return c, errors.New("quality ranges from 1 to 100 inclusive")
|
||||
}
|
||||
} else if part[0] == 'r' {
|
||||
c.Rotate, err = strconv.Atoi(part[1:])
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
} else if strings.Contains(part, "x") {
|
||||
widthHeight := strings.Split(part, "x")
|
||||
if len(widthHeight) <= 2 {
|
||||
first := widthHeight[0]
|
||||
if first != "" {
|
||||
c.Width, err = strconv.Atoi(first)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(widthHeight) == 2 {
|
||||
second := widthHeight[1]
|
||||
if second != "" {
|
||||
c.Height, err = strconv.Atoi(second)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return c, errors.New("invalid image dimensions")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if c.Width == 0 && c.Height == 0 {
|
||||
return c, errors.New("must provide Width or Height")
|
||||
}
|
||||
|
||||
if c.FilterStr == "" {
|
||||
c.FilterStr = defaults.ResampleFilter
|
||||
c.Filter = imageFilters[c.FilterStr]
|
||||
}
|
||||
|
||||
if c.AnchorStr == "" {
|
||||
c.AnchorStr = defaults.Anchor
|
||||
if !strings.EqualFold(c.AnchorStr, SmartCropIdentifier) {
|
||||
c.Anchor = anchorPositions[c.AnchorStr]
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ImageConfig holds configuration to create a new image from an existing one, resize etc.
|
||||
type ImageConfig struct {
|
||||
Action string
|
||||
|
||||
// Quality ranges from 1 to 100 inclusive, higher is better.
|
||||
// This is only relevant for JPEG images.
|
||||
// Default is 75.
|
||||
Quality int
|
||||
|
||||
// Rotate rotates an image by the given angle counter-clockwise.
|
||||
// The rotation will be performed first.
|
||||
Rotate int
|
||||
|
||||
Width int
|
||||
Height int
|
||||
|
||||
Filter imaging.ResampleFilter
|
||||
FilterStr string
|
||||
|
||||
Anchor imaging.Anchor
|
||||
AnchorStr string
|
||||
}
|
||||
|
||||
func (i ImageConfig) Key(format imaging.Format) string {
|
||||
k := strconv.Itoa(i.Width) + "x" + strconv.Itoa(i.Height)
|
||||
if i.Action != "" {
|
||||
k += "_" + i.Action
|
||||
}
|
||||
if i.Quality > 0 {
|
||||
k += "_q" + strconv.Itoa(i.Quality)
|
||||
}
|
||||
if i.Rotate != 0 {
|
||||
k += "_r" + strconv.Itoa(i.Rotate)
|
||||
}
|
||||
anchor := i.AnchorStr
|
||||
if anchor == SmartCropIdentifier {
|
||||
anchor = anchor + strconv.Itoa(smartCropVersionNumber)
|
||||
}
|
||||
|
||||
k += "_" + i.FilterStr
|
||||
|
||||
if strings.EqualFold(i.Action, "fill") {
|
||||
k += "_" + anchor
|
||||
}
|
||||
|
||||
if v, ok := imageFormatsVersions[format]; ok {
|
||||
k += "_" + strconv.Itoa(v)
|
||||
}
|
||||
|
||||
if mainImageVersionNumber > 0 {
|
||||
k += "_" + strconv.Itoa(mainImageVersionNumber)
|
||||
}
|
||||
|
||||
return k
|
||||
}
|
||||
|
||||
// Imaging contains default image processing configuration. This will be fetched
|
||||
// from site (or language) config.
|
||||
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
|
||||
ResampleFilter string
|
||||
|
||||
// The anchor used in Fill. Default is "smart", i.e. Smart Crop.
|
||||
Anchor string
|
||||
}
|
125
resources/images/config_test.go
Normal file
125
resources/images/config_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// 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"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
)
|
||||
|
||||
func TestDecodeConfig(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
m := map[string]interface{}{
|
||||
"quality": 42,
|
||||
"resampleFilter": "NearestNeighbor",
|
||||
"anchor": "topLeft",
|
||||
}
|
||||
|
||||
imaging, err := DecodeConfig(m)
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(imaging.Quality, qt.Equals, 42)
|
||||
c.Assert(imaging.ResampleFilter, qt.Equals, "nearestneighbor")
|
||||
c.Assert(imaging.Anchor, qt.Equals, "topleft")
|
||||
|
||||
m = map[string]interface{}{}
|
||||
|
||||
imaging, err = DecodeConfig(m)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(imaging.Quality, qt.Equals, defaultJPEGQuality)
|
||||
c.Assert(imaging.ResampleFilter, qt.Equals, "box")
|
||||
c.Assert(imaging.Anchor, qt.Equals, "smart")
|
||||
|
||||
_, err = DecodeConfig(map[string]interface{}{
|
||||
"quality": 123,
|
||||
})
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
|
||||
_, err = DecodeConfig(map[string]interface{}{
|
||||
"resampleFilter": "asdf",
|
||||
})
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
|
||||
_, err = DecodeConfig(map[string]interface{}{
|
||||
"anchor": "asdf",
|
||||
})
|
||||
c.Assert(err, qt.Not(qt.IsNil))
|
||||
|
||||
imaging, err = DecodeConfig(map[string]interface{}{
|
||||
"anchor": "Smart",
|
||||
})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(imaging.Anchor, qt.Equals, "smart")
|
||||
}
|
||||
|
||||
func TestDecodeImageConfig(t *testing.T) {
|
||||
for i, this := range []struct {
|
||||
in string
|
||||
expect interface{}
|
||||
}{
|
||||
{"300x400", newImageConfig(300, 400, 0, 0, "", "")},
|
||||
{"100x200 bottomRight", newImageConfig(100, 200, 0, 0, "", "BottomRight")},
|
||||
{"10x20 topleft Lanczos", newImageConfig(10, 20, 0, 0, "Lanczos", "topleft")},
|
||||
{"linear left 10x r180", newImageConfig(10, 0, 0, 180, "linear", "left")},
|
||||
{"x20 riGht Cosine q95", newImageConfig(0, 20, 95, 0, "cosine", "right")},
|
||||
|
||||
{"", false},
|
||||
{"foo", false},
|
||||
} {
|
||||
|
||||
result, err := DecodeImageConfig("resize", this.in, Imaging{})
|
||||
if b, ok := this.expect.(bool); ok && !b {
|
||||
if err == nil {
|
||||
t.Errorf("[%d] parseImageConfig didn't return an expected error", i)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("[%d] err: %s", i, err)
|
||||
}
|
||||
if fmt.Sprint(result) != fmt.Sprint(this.expect) {
|
||||
t.Fatalf("[%d] got\n%v\n but expected\n%v", i, result, this.expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newImageConfig(width, height, quality, rotate int, filter, anchor string) ImageConfig {
|
||||
var c ImageConfig
|
||||
c.Action = "resize"
|
||||
c.Width = width
|
||||
c.Height = height
|
||||
c.Quality = quality
|
||||
c.Rotate = rotate
|
||||
|
||||
if filter != "" {
|
||||
filter = strings.ToLower(filter)
|
||||
if v, ok := imageFilters[filter]; ok {
|
||||
c.Filter = v
|
||||
c.FilterStr = filter
|
||||
}
|
||||
}
|
||||
|
||||
if anchor != "" {
|
||||
anchor = strings.ToLower(anchor)
|
||||
if v, ok := anchorPositions[anchor]; ok {
|
||||
c.Anchor = v
|
||||
c.AnchorStr = anchor
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
170
resources/images/image.go
Normal file
170
resources/images/image.go
Normal file
@@ -0,0 +1,170 @@
|
||||
// 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/jpeg"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func NewImage(f imaging.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 imaging.Format
|
||||
|
||||
Proc *ImageProcessor
|
||||
|
||||
Spec Spec
|
||||
|
||||
*imageConfig
|
||||
}
|
||||
|
||||
func (i *Image) EncodeTo(conf ImageConfig, img image.Image, w io.Writer) error {
|
||||
switch i.Format {
|
||||
case imaging.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})
|
||||
default:
|
||||
return imaging.Encode(w, img, i.Format)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return imaging.Fill(src, conf.Width, conf.Height, conf.Anchor, conf.Filter), nil
|
||||
}
|
||||
|
||||
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) Resize(src image.Image, conf ImageConfig) (image.Image, error) {
|
||||
return imaging.Resize(src, conf.Width, conf.Height, conf.Filter), nil
|
||||
}
|
||||
|
||||
type Spec interface {
|
||||
// Loads the image source.
|
||||
ReadSeekCloser() (hugio.ReadSeekCloser, error)
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
75
resources/images/smartcrop.go
Normal file
75
resources/images/smartcrop.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// 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"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/muesli/smartcrop"
|
||||
)
|
||||
|
||||
const (
|
||||
// Do not change.
|
||||
// TODO(bep) image unexport
|
||||
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})
|
||||
}
|
||||
|
||||
// Needed by smartcrop
|
||||
type imagingResizer struct {
|
||||
filter imaging.ResampleFilter
|
||||
}
|
||||
|
||||
func (r imagingResizer) Resize(img image.Image, width, height uint) image.Image {
|
||||
return imaging.Resize(img, int(width), int(height), r.filter)
|
||||
}
|
||||
|
||||
func smartCrop(img image.Image, width, height int, anchor imaging.Anchor, filter imaging.ResampleFilter) (*image.NRGBA, error) {
|
||||
if width <= 0 || height <= 0 {
|
||||
return &image.NRGBA{}, nil
|
||||
}
|
||||
|
||||
srcBounds := img.Bounds()
|
||||
srcW := srcBounds.Dx()
|
||||
srcH := srcBounds.Dy()
|
||||
|
||||
if srcW <= 0 || srcH <= 0 {
|
||||
return &image.NRGBA{}, nil
|
||||
}
|
||||
|
||||
if srcW == width && srcH == height {
|
||||
return imaging.Clone(img), nil
|
||||
}
|
||||
|
||||
smart := newSmartCropAnalyzer(filter)
|
||||
|
||||
rect, err := smart.FindBestCrop(img, width, height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b := img.Bounds().Intersect(rect)
|
||||
|
||||
cropped := imaging.Crop(img, b)
|
||||
|
||||
return imaging.Resize(cropped, width, height, filter), nil
|
||||
}
|
Reference in New Issue
Block a user