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
2
go.mod
|
@ -12,7 +12,7 @@ require (
|
|||
github.com/bep/debounce v1.2.0
|
||||
github.com/bep/gitmap v1.1.0
|
||||
github.com/bep/go-tocss v0.6.0
|
||||
github.com/disintegration/imaging v1.6.0
|
||||
github.com/disintegration/gift v1.2.1
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385
|
||||
github.com/fortytw2/leaktest v1.3.0
|
||||
|
|
6
go.sum
|
@ -88,8 +88,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
||||
github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA=
|
||||
github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ=
|
||||
github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
|
||||
github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||
github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg=
|
||||
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
|
@ -338,8 +338,6 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf
|
|||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff h1:+2zgJKVDVAz/BWSsuniCmU1kLCjL88Z8/kv39xCI9NQ=
|
||||
golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
|
|
|
@ -16,18 +16,19 @@ package resources
|
|||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
_ "image/gif"
|
||||
_ "image/png"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/internal"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
|
||||
_errors "github.com/pkg/errors"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/disintegration/gift"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/resources/images"
|
||||
|
||||
|
@ -82,16 +83,26 @@ func (i *imageResource) cloneWithUpdates(u *transformationUpdate) (baseResource,
|
|||
// filter and returns the transformed image. If one of width or height is 0, the image aspect
|
||||
// ratio is preserved.
|
||||
func (i *imageResource) Resize(spec string) (resource.Image, error) {
|
||||
return i.doWithImageConfig("resize", spec, func(src image.Image, conf images.ImageConfig) (image.Image, error) {
|
||||
return i.Proc.Resize(src, conf)
|
||||
conf, err := i.decodeImageConfig("resize", spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
|
||||
return i.Proc.ApplyFiltersFromConfig(src, conf)
|
||||
})
|
||||
}
|
||||
|
||||
// Fit scales down the image using the specified resample filter to fit the specified
|
||||
// maximum width and height.
|
||||
func (i *imageResource) Fit(spec string) (resource.Image, error) {
|
||||
return i.doWithImageConfig("fit", spec, func(src image.Image, conf images.ImageConfig) (image.Image, error) {
|
||||
return i.Proc.Fit(src, conf)
|
||||
conf, err := i.decodeImageConfig("fit", spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
|
||||
return i.Proc.ApplyFiltersFromConfig(src, conf)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -99,8 +110,22 @@ func (i *imageResource) Fit(spec string) (resource.Image, error) {
|
|||
// crops the resized image to the specified dimensions using the given anchor point.
|
||||
// Space delimited config: 200x300 TopLeft
|
||||
func (i *imageResource) Fill(spec string) (resource.Image, error) {
|
||||
return i.doWithImageConfig("fill", spec, func(src image.Image, conf images.ImageConfig) (image.Image, error) {
|
||||
return i.Proc.Fill(src, conf)
|
||||
conf, err := i.decodeImageConfig("fill", spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
|
||||
return i.Proc.ApplyFiltersFromConfig(src, conf)
|
||||
})
|
||||
}
|
||||
|
||||
func (i *imageResource) Filter(filters ...gift.Filter) (resource.Image, error) {
|
||||
conf := i.Proc.GetDefaultImageConfig("filter")
|
||||
conf.Key = internal.HashString(filters)
|
||||
|
||||
return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
|
||||
return i.Proc.Filter(src, filters...)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -118,19 +143,14 @@ const imageProcWorkers = 1
|
|||
|
||||
var imageProcSem = make(chan bool, imageProcWorkers)
|
||||
|
||||
func (i *imageResource) doWithImageConfig(action, spec string, f func(src image.Image, conf images.ImageConfig) (image.Image, error)) (resource.Image, error) {
|
||||
conf, err := i.decodeImageConfig(action, spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src image.Image) (image.Image, error)) (resource.Image, error) {
|
||||
return i.getSpec().imageCache.getOrCreate(i, conf, func() (*imageResource, image.Image, error) {
|
||||
imageProcSem <- true
|
||||
defer func() {
|
||||
<-imageProcSem
|
||||
}()
|
||||
|
||||
errOp := action
|
||||
errOp := conf.Action
|
||||
errPath := i.getSourceFilename()
|
||||
|
||||
src, err := i.decodeSource()
|
||||
|
@ -138,17 +158,12 @@ func (i *imageResource) doWithImageConfig(action, spec string, f func(src image.
|
|||
return nil, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
|
||||
}
|
||||
|
||||
if conf.Rotate != 0 {
|
||||
// Rotate it before any scaling to get the dimensions correct.
|
||||
src = imaging.Rotate(src, float64(conf.Rotate), color.Transparent)
|
||||
}
|
||||
|
||||
converted, err := f(src, conf)
|
||||
converted, err := f(src)
|
||||
if err != nil {
|
||||
return nil, nil, &os.PathError{Op: errOp, Path: errPath, Err: err}
|
||||
}
|
||||
|
||||
if i.Format == imaging.PNG {
|
||||
if i.Format == images.PNG {
|
||||
// Apply the colour palette from the source
|
||||
if paletted, ok := src.(*image.Paletted); ok {
|
||||
tmp := image.NewPaletted(converted.Bounds(), paletted.Palette)
|
||||
|
@ -222,7 +237,7 @@ func (i *imageResource) relTargetPathFromConfig(conf images.ImageConfig) dirFile
|
|||
// Do not change for no good reason.
|
||||
const md5Threshold = 100
|
||||
|
||||
key := conf.Key(i.Format)
|
||||
key := conf.GetKey(i.Format)
|
||||
|
||||
// It is useful to have the key in clear text, but when nesting transforms, it
|
||||
// can easily be too long to read, and maybe even too long
|
||||
|
|
|
@ -16,14 +16,20 @@ package resources
|
|||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/disintegration/gift"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/resources/images"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/gohugoio/hugo/htesting/hqt"
|
||||
|
@ -35,6 +41,9 @@ var eq = qt.CmpEquals(
|
|||
cmp.Comparer(func(p1, p2 *resourceAdapter) bool {
|
||||
return p1.resourceAdapterInner == p2.resourceAdapterInner
|
||||
}),
|
||||
cmp.Comparer(func(p1, p2 os.FileInfo) bool {
|
||||
return p1.Name() == p2.Name() && p1.Size() == p2.Size() && p1.IsDir() == p2.IsDir()
|
||||
}),
|
||||
cmp.Comparer(func(p1, p2 *genericResource) bool { return p1 == p2 }),
|
||||
cmp.Comparer(func(m1, m2 media.Type) bool {
|
||||
return m1.Type() == m2.Type()
|
||||
|
@ -94,7 +103,7 @@ func TestImageTransformBasic(t *testing.T) {
|
|||
fittedAgain, err = fittedAgain.Fit("10x20")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(fittedAgain.RelPermalink(), qt.Equals, "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_3f65ba24dc2b7fba0f56d7f104519157.jpg")
|
||||
assertWidthHeight(fittedAgain, 10, 6)
|
||||
assertWidthHeight(fittedAgain, 10, 7)
|
||||
|
||||
filled, err := image.Fill("200x100 bottomLeft")
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
@ -155,7 +164,10 @@ func TestImagePermalinkPublishOrder(t *testing.T) {
|
|||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
spec := newTestResourceOsFs(c)
|
||||
spec, workDir := newTestResourceOsFs(c)
|
||||
defer func() {
|
||||
os.Remove(workDir)
|
||||
}()
|
||||
|
||||
check1 := func(img resource.Image) {
|
||||
resizedLink := "/a/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_100x50_resize_q75_box.jpg"
|
||||
|
@ -192,7 +204,10 @@ func TestImageTransformConcurrent(t *testing.T) {
|
|||
|
||||
c := qt.New(t)
|
||||
|
||||
spec := newTestResourceOsFs(c)
|
||||
spec, workDir := newTestResourceOsFs(c)
|
||||
defer func() {
|
||||
os.Remove(workDir)
|
||||
}()
|
||||
|
||||
image := fetchImageForSpec(spec, c, "sunset.jpg")
|
||||
|
||||
|
@ -317,6 +332,133 @@ func TestSVGImageContent(t *testing.T) {
|
|||
c.Assert(content.(string), qt.Contains, `<svg height="100" width="100">`)
|
||||
}
|
||||
|
||||
func TestImageOperationsGolden(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
c.Parallel()
|
||||
|
||||
devMode := false
|
||||
|
||||
testImages := []string{"sunset.jpg", "gohugoio8.png", "gohugoio24.png"}
|
||||
|
||||
spec, workDir := newTestResourceOsFs(c)
|
||||
defer func() {
|
||||
if !devMode {
|
||||
os.Remove(workDir)
|
||||
}
|
||||
}()
|
||||
|
||||
if devMode {
|
||||
fmt.Println(workDir)
|
||||
}
|
||||
|
||||
for _, img := range testImages {
|
||||
|
||||
orig := fetchImageForSpec(spec, c, img)
|
||||
for _, resizeSpec := range []string{"200x100", "600x", "200x r90 q50 Box"} {
|
||||
resized, err := orig.Resize(resizeSpec)
|
||||
c.Assert(err, qt.IsNil)
|
||||
rel := resized.RelPermalink()
|
||||
c.Log("resize", rel)
|
||||
c.Assert(rel, qt.Not(qt.Equals), "")
|
||||
}
|
||||
|
||||
for _, fillSpec := range []string{"300x200 Gaussian Smart", "100x100 Center", "300x100 TopLeft NearestNeighbor", "400x200 BottomLeft"} {
|
||||
resized, err := orig.Fill(fillSpec)
|
||||
c.Assert(err, qt.IsNil)
|
||||
rel := resized.RelPermalink()
|
||||
c.Log("fill", rel)
|
||||
c.Assert(rel, qt.Not(qt.Equals), "")
|
||||
}
|
||||
|
||||
for _, fitSpec := range []string{"300x200 Linear"} {
|
||||
resized, err := orig.Fit(fitSpec)
|
||||
c.Assert(err, qt.IsNil)
|
||||
rel := resized.RelPermalink()
|
||||
c.Log("fit", rel)
|
||||
c.Assert(rel, qt.Not(qt.Equals), "")
|
||||
}
|
||||
|
||||
f := &images.Filters{}
|
||||
|
||||
filters := []gift.Filter{
|
||||
f.Grayscale(),
|
||||
f.GaussianBlur(6),
|
||||
f.Saturation(50),
|
||||
f.Sepia(100),
|
||||
f.Brightness(30),
|
||||
f.ColorBalance(10, -10, -10),
|
||||
f.Colorize(240, 50, 100),
|
||||
f.Gamma(1.5),
|
||||
f.UnsharpMask(1, 1, 0),
|
||||
f.Sigmoid(0.5, 7),
|
||||
f.Pixelate(5),
|
||||
f.Invert(),
|
||||
f.Hue(22),
|
||||
f.Contrast(32.5),
|
||||
}
|
||||
|
||||
resized, err := orig.Fill("400x200 center")
|
||||
|
||||
for _, filter := range filters {
|
||||
resized, err := resized.Filter(filter)
|
||||
c.Assert(err, qt.IsNil)
|
||||
rel := resized.RelPermalink()
|
||||
c.Logf("filter: %v %s", filter, rel)
|
||||
c.Assert(rel, qt.Not(qt.Equals), "")
|
||||
}
|
||||
|
||||
resized, err = resized.Filter(filters[0:4]...)
|
||||
c.Assert(err, qt.IsNil)
|
||||
rel := resized.RelPermalink()
|
||||
c.Log("filter all", rel)
|
||||
c.Assert(rel, qt.Not(qt.Equals), "")
|
||||
}
|
||||
|
||||
if devMode {
|
||||
return
|
||||
}
|
||||
|
||||
dir1 := filepath.Join(workDir, "resources/_gen/images/a")
|
||||
dir2 := filepath.FromSlash("testdata/golden")
|
||||
|
||||
// The two dirs above should now be the same.
|
||||
d1, err := os.Open(dir1)
|
||||
c.Assert(err, qt.IsNil)
|
||||
d2, err := os.Open(dir2)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
dirinfos1, err := d1.Readdir(-1)
|
||||
c.Assert(err, qt.IsNil)
|
||||
dirinfos2, err := d2.Readdir(-1)
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(len(dirinfos1), qt.Equals, len(dirinfos2))
|
||||
|
||||
for i, fi1 := range dirinfos1 {
|
||||
if regexp.MustCompile("gauss").MatchString(fi1.Name()) {
|
||||
continue
|
||||
}
|
||||
fi2 := dirinfos2[i]
|
||||
c.Assert(fi1.Name(), qt.Equals, fi2.Name())
|
||||
c.Assert(fi1, eq, fi2)
|
||||
f1, err := os.Open(filepath.Join(dir1, fi1.Name()))
|
||||
c.Assert(err, qt.IsNil)
|
||||
f2, err := os.Open(filepath.Join(dir2, fi2.Name()))
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
hash1, err := helpers.MD5FromReader(f1)
|
||||
c.Assert(err, qt.IsNil)
|
||||
hash2, err := helpers.MD5FromReader(f2)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
f1.Close()
|
||||
f2.Close()
|
||||
|
||||
c.Assert(hash1, qt.Equals, hash2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func BenchmarkResizeParallel(b *testing.B) {
|
||||
c := qt.New(b)
|
||||
img := fetchSunset(c)
|
||||
|
|
|
@ -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
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
214
resources/images/resampling.go
Normal 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)))
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -16,8 +16,6 @@ package internal
|
|||
import (
|
||||
"strconv"
|
||||
|
||||
bp "github.com/gohugoio/hugo/bufferpool"
|
||||
|
||||
"github.com/mitchellh/hashstructure"
|
||||
)
|
||||
|
||||
|
@ -44,18 +42,23 @@ func (k ResourceTransformationKey) Value() string {
|
|||
return k.Name
|
||||
}
|
||||
|
||||
sb := bp.GetBuffer()
|
||||
defer bp.PutBuffer(sb)
|
||||
return k.Name + "_" + HashString(k.elements...)
|
||||
|
||||
sb.WriteString(k.Name)
|
||||
for _, element := range k.elements {
|
||||
hash, err := hashstructure.Hash(element, nil)
|
||||
}
|
||||
|
||||
// HashString returns a hash from the given elements.
|
||||
// It will panic if the hash cannot be calculated.
|
||||
func HashString(elements ...interface{}) string {
|
||||
var o interface{}
|
||||
if len(elements) == 1 {
|
||||
o = elements[0]
|
||||
} else {
|
||||
o = elements
|
||||
}
|
||||
|
||||
hash, err := hashstructure.Hash(o, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
sb.WriteString("_")
|
||||
sb.WriteString(strconv.FormatUint(hash, 10))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
return strconv.FormatUint(hash, 10)
|
||||
}
|
||||
|
|
|
@ -32,5 +32,12 @@ func TestResourceTransformationKey(t *testing.T) {
|
|||
key := NewResourceTransformationKey("testing",
|
||||
testStruct{Name: "test", V1: int64(10), V2: int32(20), V3: 30, V4: uint64(40)})
|
||||
c := qt.New(t)
|
||||
c.Assert("testing_518996646957295636", qt.Equals, key.Value())
|
||||
c.Assert(key.Value(), qt.Equals, "testing_518996646957295636")
|
||||
}
|
||||
|
||||
func TestHashString(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(HashString("a", "b"), qt.Equals, "2712570657419664240")
|
||||
c.Assert(HashString("ab"), qt.Equals, "590647783936702392")
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
package resource
|
||||
|
||||
import (
|
||||
"github.com/disintegration/gift"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
|
||||
|
@ -47,6 +48,7 @@ type ImageOps interface {
|
|||
Fill(spec string) (Image, error)
|
||||
Fit(spec string) (Image, error)
|
||||
Resize(spec string) (Image, error)
|
||||
Filter(filters ...gift.Filter) (Image, error)
|
||||
}
|
||||
|
||||
type ResourceTypesProvider interface {
|
||||
|
|
BIN
resources/testdata/gohugoio24.png
vendored
Normal file
After Width: | Height: | Size: 262 KiB |
BIN
resources/testdata/gohugoio8.png
vendored
Normal file
After Width: | Height: | Size: 72 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 61 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 59 KiB |
After Width: | Height: | Size: 59 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 110 KiB |
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 8.8 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 77 KiB |
After Width: | Height: | Size: 59 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 64 KiB |
After Width: | Height: | Size: 59 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 33 KiB |
BIN
resources/testdata/golden/gohugoio8_hu7f72c00afdf7634587afaa5eff2a25b2_73538_600x0_resize_box_2.png
vendored
Normal file
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 4.1 KiB |
BIN
resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_200x100_resize_q75_box.jpg
vendored
Normal file
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 2 KiB |
After Width: | Height: | Size: 4.6 KiB |
BIN
resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_300x200_fit_q75_linear.jpg
vendored
Normal file
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 6.3 KiB |
BIN
resources/testdata/golden/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_600x0_resize_q75_box.jpg
vendored
Normal file
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 6 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 6.3 KiB |
|
@ -102,11 +102,13 @@ func newTargetPaths(link string) func() page.TargetPaths {
|
|||
}
|
||||
}
|
||||
|
||||
func newTestResourceOsFs(c *qt.C) *Spec {
|
||||
func newTestResourceOsFs(c *qt.C) (*Spec, string) {
|
||||
cfg := createTestCfg()
|
||||
cfg.Set("baseURL", "https://example.com")
|
||||
|
||||
workDir, _ := ioutil.TempDir("", "hugores")
|
||||
workDir, err := ioutil.TempDir("", "hugores")
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(workDir, qt.Not(qt.Equals), "")
|
||||
|
||||
if runtime.GOOS == "darwin" && !strings.HasPrefix(workDir, "/private") {
|
||||
// To get the entry folder in line with the rest. This its a little bit
|
||||
|
@ -127,7 +129,8 @@ func newTestResourceOsFs(c *qt.C) *Spec {
|
|||
|
||||
spec, err := NewSpec(s, filecaches, nil, output.DefaultFormats, media.DefaultTypes)
|
||||
c.Assert(err, qt.IsNil)
|
||||
return spec
|
||||
|
||||
return spec, workDir
|
||||
|
||||
}
|
||||
|
||||
|
@ -139,6 +142,7 @@ func fetchImage(c *qt.C, name string) resource.Image {
|
|||
spec := newTestResourceSpec(specDescriptor{c: c})
|
||||
return fetchImageForSpec(spec, c, name)
|
||||
}
|
||||
|
||||
func fetchImageForSpec(spec *Spec, c *qt.C, name string) resource.Image {
|
||||
r := fetchResourceForSpec(spec, c, name)
|
||||
|
||||
|
@ -153,8 +157,9 @@ func fetchImageForSpec(spec *Spec, c *qt.C, name string) resource.Image {
|
|||
func fetchResourceForSpec(spec *Spec, c *qt.C, name string) resource.ContentResource {
|
||||
src, err := os.Open(filepath.FromSlash("testdata/" + name))
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
out, err := helpers.OpenFileForWriting(spec.Fs.Source, name)
|
||||
workDir := spec.WorkingDir
|
||||
targetFilename := filepath.Join(workDir, name)
|
||||
out, err := helpers.OpenFileForWriting(spec.Fs.Source, targetFilename)
|
||||
c.Assert(err, qt.IsNil)
|
||||
_, err = io.Copy(out, src)
|
||||
out.Close()
|
||||
|
@ -163,8 +168,9 @@ func fetchResourceForSpec(spec *Spec, c *qt.C, name string) resource.ContentReso
|
|||
|
||||
factory := newTargetPaths("/a")
|
||||
|
||||
r, err := spec.New(ResourceSourceDescriptor{Fs: spec.Fs.Source, TargetPaths: factory, LazyPublish: true, SourceFilename: name})
|
||||
r, err := spec.New(ResourceSourceDescriptor{Fs: spec.Fs.Source, TargetPaths: factory, LazyPublish: true, RelTargetFilename: name, SourceFilename: targetFilename})
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(r, qt.Not(qt.IsNil))
|
||||
|
||||
return r.(resource.ContentResource)
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/disintegration/gift"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
bp "github.com/gohugoio/hugo/bufferpool"
|
||||
|
@ -172,6 +173,10 @@ func (r *resourceAdapter) Fit(spec string) (resource.Image, error) {
|
|||
return r.getImageOps().Fit(spec)
|
||||
}
|
||||
|
||||
func (r *resourceAdapter) Filter(filters ...gift.Filter) (resource.Image, error) {
|
||||
return r.getImageOps().Filter(filters...)
|
||||
}
|
||||
|
||||
func (r *resourceAdapter) Height() int {
|
||||
return r.getImageOps().Height()
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2017 The Hugo Authors. All rights reserved.
|
||||
// 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.
|
||||
|
@ -15,10 +15,16 @@
|
|||
package images
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"sync"
|
||||
|
||||
"github.com/disintegration/gift"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/images"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
|
||||
// Importing image codecs for image.DecodeConfig
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
|
@ -34,6 +40,7 @@ import (
|
|||
// New returns a new instance of the images-namespaced template functions.
|
||||
func New(deps *deps.Deps) *Namespace {
|
||||
return &Namespace{
|
||||
Filters: &images.Filters{},
|
||||
cache: map[string]image.Config{},
|
||||
deps: deps,
|
||||
}
|
||||
|
@ -41,6 +48,7 @@ func New(deps *deps.Deps) *Namespace {
|
|||
|
||||
// Namespace provides template functions for the "images" namespace.
|
||||
type Namespace struct {
|
||||
*images.Filters
|
||||
cacheMu sync.RWMutex
|
||||
cache map[string]image.Config
|
||||
|
||||
|
@ -85,3 +93,18 @@ func (ns *Namespace) Config(path interface{}) (image.Config, error) {
|
|||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (ns *Namespace) Filter(args ...interface{}) (resource.Image, error) {
|
||||
if len(args) < 2 {
|
||||
return nil, errors.New("must provide an image and one or more filters")
|
||||
}
|
||||
|
||||
img := args[len(args)-1].(resource.Image)
|
||||
filtersv := args[:len(args)-1]
|
||||
filters := make([]gift.Filter, len(filtersv))
|
||||
for i, f := range filtersv {
|
||||
filters[i] = f.(gift.Filter)
|
||||
}
|
||||
|
||||
return img.Filter(filters...)
|
||||
}
|
||||
|
|