mirror of
https://github.com/willnorris/imageproxy.git
synced 2024-12-30 22:34:18 -05:00
parent
20c0a50a31
commit
afbd254cdc
4 changed files with 48 additions and 14 deletions
|
@ -71,6 +71,10 @@ image][material-animation] resized to 200px square and rotated 270 degrees:
|
||||||
|
|
||||||
<a href="https://willnorris.com/api/imageproxy/200,r270/https://willnorris.com/2015/05/material-animations.gif"><img src="https://willnorris.com/api/imageproxy/200,r270/https://willnorris.com/2015/05/material-animations.gif" alt="200,r270"></a>
|
<a href="https://willnorris.com/api/imageproxy/200,r270/https://willnorris.com/2015/05/material-animations.gif"><img src="https://willnorris.com/api/imageproxy/200,r270/https://willnorris.com/2015/05/material-animations.gif" alt="200,r270"></a>
|
||||||
|
|
||||||
|
The smart crop feature can best be seen by comparing the following images, with and without smart crop.
|
||||||
|
|
||||||
|
<a href="https://willnorris.com/api/imageproxy/150x300/https://judahnorris.com/images/judah-sheets.jpg"><img src="https://willnorris.com/api/imageproxy/150x300/https://judahnorris.com/images/judah-sheets.jpg" alt="200x400"></a>
|
||||||
|
<a href="https://willnorris.com/api/imageproxy/150x300,sc/https://judahnorris.com/images/judah-sheets.jpg"><img src="https://willnorris.com/api/imageproxy/150x300,sc/https://judahnorris.com/images/judah-sheets.jpg" alt="200x400,sc"></a>
|
||||||
|
|
||||||
## Getting Started ##
|
## Getting Started ##
|
||||||
|
|
||||||
|
|
15
data.go
15
data.go
|
@ -39,6 +39,7 @@ const (
|
||||||
optCropY = "cy"
|
optCropY = "cy"
|
||||||
optCropWidth = "cw"
|
optCropWidth = "cw"
|
||||||
optCropHeight = "ch"
|
optCropHeight = "ch"
|
||||||
|
optSmartCrop = "sc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// URLError reports a malformed URL error.
|
// URLError reports a malformed URL error.
|
||||||
|
@ -86,6 +87,9 @@ type Options struct {
|
||||||
CropY float64
|
CropY float64
|
||||||
CropWidth float64
|
CropWidth float64
|
||||||
CropHeight float64
|
CropHeight float64
|
||||||
|
|
||||||
|
// Automatically find good crop points based on image content.
|
||||||
|
SmartCrop bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o Options) String() string {
|
func (o Options) String() string {
|
||||||
|
@ -126,6 +130,9 @@ func (o Options) String() string {
|
||||||
if o.CropHeight != 0 {
|
if o.CropHeight != 0 {
|
||||||
opts = append(opts, fmt.Sprintf("%s%v", string(optCropHeight), o.CropHeight))
|
opts = append(opts, fmt.Sprintf("%s%v", string(optCropHeight), o.CropHeight))
|
||||||
}
|
}
|
||||||
|
if o.SmartCrop {
|
||||||
|
opts = append(opts, optSmartCrop)
|
||||||
|
}
|
||||||
return strings.Join(opts, ",")
|
return strings.Join(opts, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,6 +166,12 @@ func (o Options) transform() bool {
|
||||||
// crop width or height will be adjusted, preserving the specified cx and cy
|
// crop width or height will be adjusted, preserving the specified cx and cy
|
||||||
// values. Rectangular crop is applied before any other transformations.
|
// values. Rectangular crop is applied before any other transformations.
|
||||||
//
|
//
|
||||||
|
// Smart Crop
|
||||||
|
//
|
||||||
|
// The "sc" option will perform a content-aware smart crop to fit the
|
||||||
|
// requested image width and height dimensions (see Size and Cropping below).
|
||||||
|
// The smart crop option will override any requested rectangular crop.
|
||||||
|
//
|
||||||
// Size and Cropping
|
// Size and Cropping
|
||||||
//
|
//
|
||||||
// The size option takes the general form "{width}x{height}", where width and
|
// The size option takes the general form "{width}x{height}", where width and
|
||||||
|
@ -246,6 +259,8 @@ func ParseOptions(str string) Options {
|
||||||
options.ScaleUp = true
|
options.ScaleUp = true
|
||||||
case opt == optFormatJPEG, opt == optFormatPNG, opt == optFormatTIFF:
|
case opt == optFormatJPEG, opt == optFormatPNG, opt == optFormatTIFF:
|
||||||
options.Format = opt
|
options.Format = opt
|
||||||
|
case opt == optSmartCrop:
|
||||||
|
options.SmartCrop = true
|
||||||
case strings.HasPrefix(opt, optRotatePrefix):
|
case strings.HasPrefix(opt, optRotatePrefix):
|
||||||
value := strings.TrimPrefix(opt, optRotatePrefix)
|
value := strings.TrimPrefix(opt, optRotatePrefix)
|
||||||
options.Rotate, _ = strconv.Atoi(value)
|
options.Rotate, _ = strconv.Atoi(value)
|
||||||
|
|
26
data_test.go
26
data_test.go
|
@ -31,19 +31,19 @@ func TestOptions_String(t *testing.T) {
|
||||||
"0x0",
|
"0x0",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Options{1, 2, true, 90, true, true, 80, "", false, "", 0, 0, 0, 0},
|
Options{1, 2, true, 90, true, true, 80, "", false, "", 0, 0, 0, 0, false},
|
||||||
"1x2,fit,r90,fv,fh,q80",
|
"1x2,fit,r90,fv,fh,q80",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Options{0.15, 1.3, false, 45, false, false, 95, "c0ffee", false, "png", 0, 0, 0, 0},
|
Options{0.15, 1.3, false, 45, false, false, 95, "c0ffee", false, "png", 0, 0, 0, 0, false},
|
||||||
"0.15x1.3,r45,q95,sc0ffee,png",
|
"0.15x1.3,r45,q95,sc0ffee,png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Options{0.15, 1.3, false, 45, false, false, 95, "c0ffee", false, "", 100, 200, 0, 0},
|
Options{0.15, 1.3, false, 45, false, false, 95, "c0ffee", false, "", 100, 200, 0, 0, false},
|
||||||
"0.15x1.3,r45,q95,sc0ffee,cx100,cy200",
|
"0.15x1.3,r45,q95,sc0ffee,cx100,cy200",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Options{0.15, 1.3, false, 45, false, false, 95, "c0ffee", false, "png", 100, 200, 300, 400},
|
Options{0.15, 1.3, false, 45, false, false, 95, "c0ffee", false, "png", 100, 200, 300, 400, false},
|
||||||
"0.15x1.3,r45,q95,sc0ffee,png,cx100,cy200,cw300,ch400",
|
"0.15x1.3,r45,q95,sc0ffee,png,cx100,cy200,cw300,ch400",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -94,19 +94,19 @@ func TestParseOptions(t *testing.T) {
|
||||||
{"FOO,1,BAR,r90,BAZ", Options{Width: 1, Height: 1, Rotate: 90}},
|
{"FOO,1,BAR,r90,BAZ", Options{Width: 1, Height: 1, Rotate: 90}},
|
||||||
|
|
||||||
// flags, in different orders
|
// flags, in different orders
|
||||||
{"q70,1x2,fit,r90,fv,fh,sc0ffee,png", Options{1, 2, true, 90, true, true, 70, "c0ffee", false, "png", 0, 0, 0, 0}},
|
{"q70,1x2,fit,r90,fv,fh,sc0ffee,png", Options{1, 2, true, 90, true, true, 70, "c0ffee", false, "png", 0, 0, 0, 0, false}},
|
||||||
{"r90,fh,sc0ffee,png,q90,1x2,fv,fit", Options{1, 2, true, 90, true, true, 90, "c0ffee", false, "png", 0, 0, 0, 0}},
|
{"r90,fh,sc0ffee,png,q90,1x2,fv,fit", Options{1, 2, true, 90, true, true, 90, "c0ffee", false, "png", 0, 0, 0, 0, false}},
|
||||||
|
|
||||||
// all flags, in different orders with crop
|
// all flags, in different orders with crop
|
||||||
{"q70,cx100,cw300,1x2,fit,cy200,r90,fv,ch400,fh,sc0ffee,png", Options{1, 2, true, 90, true, true, 70, "c0ffee", false, "png", 100, 200, 300, 400}},
|
{"q70,cx100,cw300,1x2,fit,cy200,r90,fv,ch400,fh,sc0ffee,png", Options{1, 2, true, 90, true, true, 70, "c0ffee", false, "png", 100, 200, 300, 400, false}},
|
||||||
{"ch400,r90,cw300,fh,sc0ffee,png,cx100,q90,cy200,1x2,fv,fit", Options{1, 2, true, 90, true, true, 90, "c0ffee", false, "png", 100, 200, 300, 400}},
|
{"ch400,r90,cw300,fh,sc0ffee,png,cx100,q90,cy200,1x2,fv,fit", Options{1, 2, true, 90, true, true, 90, "c0ffee", false, "png", 100, 200, 300, 400, false}},
|
||||||
|
|
||||||
// all flags, in different orders with crop & different resizes
|
// all flags, in different orders with crop & different resizes
|
||||||
{"q70,cx100,cw300,x2,fit,cy200,r90,fv,ch400,fh,sc0ffee,png", Options{0, 2, true, 90, true, true, 70, "c0ffee", false, "png", 100, 200, 300, 400}},
|
{"q70,cx100,cw300,x2,fit,cy200,r90,fv,ch400,fh,sc0ffee,png", Options{0, 2, true, 90, true, true, 70, "c0ffee", false, "png", 100, 200, 300, 400, false}},
|
||||||
{"ch400,r90,cw300,fh,sc0ffee,png,cx100,q90,cy200,1x,fv,fit", Options{1, 0, true, 90, true, true, 90, "c0ffee", false, "png", 100, 200, 300, 400}},
|
{"ch400,r90,cw300,fh,sc0ffee,png,cx100,q90,cy200,1x,fv,fit", Options{1, 0, true, 90, true, true, 90, "c0ffee", false, "png", 100, 200, 300, 400, false}},
|
||||||
{"ch400,r90,cw300,fh,sc0ffee,png,cx100,q90,cy200,cw,fv,fit", Options{0, 0, true, 90, true, true, 90, "c0ffee", false, "png", 100, 200, 0, 400}},
|
{"ch400,r90,cw300,fh,sc0ffee,png,cx100,q90,cy200,cw,fv,fit", Options{0, 0, true, 90, true, true, 90, "c0ffee", false, "png", 100, 200, 0, 400, false}},
|
||||||
{"ch400,r90,cw300,fh,sc0ffee,png,cx100,q90,cy200,cw,fv,fit,123x321", Options{123, 321, true, 90, true, true, 90, "c0ffee", false, "png", 100, 200, 0, 400}},
|
{"ch400,r90,cw300,fh,sc0ffee,png,cx100,q90,cy200,cw,fv,fit,123x321", Options{123, 321, true, 90, true, true, 90, "c0ffee", false, "png", 100, 200, 0, 400, false}},
|
||||||
{"123x321,ch400,r90,cw300,fh,sc0ffee,png,cx100,q90,cy200,cw,fv,fit", Options{123, 321, true, 90, true, true, 90, "c0ffee", false, "png", 100, 200, 0, 400}},
|
{"123x321,ch400,r90,cw300,fh,sc0ffee,png,cx100,q90,cy200,cw,fv,fit", Options{123, 321, true, 90, true, true, 90, "c0ffee", false, "png", 100, 200, 0, 400, false}},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|
17
transform.go
17
transform.go
|
@ -22,9 +22,11 @@ import (
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
|
"github.com/muesli/smartcrop"
|
||||||
"github.com/rwcarlsen/goexif/exif"
|
"github.com/rwcarlsen/goexif/exif"
|
||||||
"golang.org/x/image/tiff" // register tiff format
|
"golang.org/x/image/tiff" // register tiff format
|
||||||
_ "golang.org/x/image/webp" // register webp format
|
_ "golang.org/x/image/webp" // register webp format
|
||||||
|
@ -156,7 +158,7 @@ func resizeParams(m image.Image, opt Options) (w, h int, resize bool) {
|
||||||
|
|
||||||
// cropParams calculates crop rectangle parameters to keep it in image bounds
|
// cropParams calculates crop rectangle parameters to keep it in image bounds
|
||||||
func cropParams(m image.Image, opt Options) image.Rectangle {
|
func cropParams(m image.Image, opt Options) image.Rectangle {
|
||||||
if opt.CropX == 0 && opt.CropY == 0 && opt.CropWidth == 0 && opt.CropHeight == 0 {
|
if !opt.SmartCrop && opt.CropX == 0 && opt.CropY == 0 && opt.CropWidth == 0 && opt.CropHeight == 0 {
|
||||||
return m.Bounds()
|
return m.Bounds()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,6 +166,19 @@ func cropParams(m image.Image, opt Options) image.Rectangle {
|
||||||
imgW := m.Bounds().Dx()
|
imgW := m.Bounds().Dx()
|
||||||
imgH := m.Bounds().Dy()
|
imgH := m.Bounds().Dy()
|
||||||
|
|
||||||
|
if opt.SmartCrop {
|
||||||
|
w := evaluateFloat(opt.Width, imgW)
|
||||||
|
h := evaluateFloat(opt.Height, imgH)
|
||||||
|
log.Printf("smartcrop input: %dx%d", w, h)
|
||||||
|
r, err := smartcrop.SmartCrop(m, w, h)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error with smartcrop: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("smartcrop rectangle: %v", r)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// top left coordinate of crop
|
// top left coordinate of crop
|
||||||
x0 := evaluateFloat(math.Abs(opt.CropX), imgW)
|
x0 := evaluateFloat(math.Abs(opt.CropX), imgW)
|
||||||
if opt.CropX < 0 {
|
if opt.CropX < 0 {
|
||||||
|
|
Loading…
Reference in a new issue