mirror of
https://github.com/willnorris/imageproxy.git
synced 2024-12-30 22:34:18 -05:00
allow crop options to be floats and negative
values between 0 and 1 have the same behavior as the size option - it is treated as a percentage of the original image size. Negative values for cx and cy are calculated from the bottom and right edges of the image.
This commit is contained in:
parent
430baac0b0
commit
4533f0c68a
4 changed files with 71 additions and 50 deletions
|
@ -23,10 +23,10 @@ imageproxy URLs are of the form `http://localhost/{options}/{remote_url}`.
|
||||||
|
|
||||||
### Options ###
|
### Options ###
|
||||||
|
|
||||||
Options are available for resizing, rotation, flipping, and digital signatures
|
Options are available for cropping, resizing, rotation, flipping, and digital
|
||||||
among a few others. Options for are specified as a comma delimited list of
|
signatures among a few others. Options for are specified as a comma delimited
|
||||||
parameters, which can be supplied in any order. Duplicate parameters overwrite
|
list of parameters, which can be supplied in any order. Duplicate parameters
|
||||||
previous values.
|
overwrite previous values.
|
||||||
|
|
||||||
See the full list of available options at
|
See the full list of available options at
|
||||||
<https://godoc.org/willnorris.com/go/imageproxy#ParseOptions>.
|
<https://godoc.org/willnorris.com/go/imageproxy#ParseOptions>.
|
||||||
|
@ -60,6 +60,7 @@ x0.15 | 15% original height, proportional width | <a href="https://willnorris
|
||||||
100,fv,fh | 100px square, flipped horizontal and vertical | <a href="https://willnorris.com/api/imageproxy/100,fv,fh/https://willnorris.com/2013/12/small-things.jpg"><img src="https://willnorris.com/api/imageproxy/100,fv,fh/https://willnorris.com/2013/12/small-things.jpg" alt="100,fv,fh"></a>
|
100,fv,fh | 100px square, flipped horizontal and vertical | <a href="https://willnorris.com/api/imageproxy/100,fv,fh/https://willnorris.com/2013/12/small-things.jpg"><img src="https://willnorris.com/api/imageproxy/100,fv,fh/https://willnorris.com/2013/12/small-things.jpg" alt="100,fv,fh"></a>
|
||||||
200x,q60 | 200px wide, proportional height, 60% quality | <a href="https://willnorris.com/api/imageproxy/200x,q60/https://willnorris.com/2013/12/small-things.jpg"><img src="https://willnorris.com/api/imageproxy/200x,q60/https://willnorris.com/2013/12/small-things.jpg" alt="200x,q60"></a>
|
200x,q60 | 200px wide, proportional height, 60% quality | <a href="https://willnorris.com/api/imageproxy/200x,q60/https://willnorris.com/2013/12/small-things.jpg"><img src="https://willnorris.com/api/imageproxy/200x,q60/https://willnorris.com/2013/12/small-things.jpg" alt="200x,q60"></a>
|
||||||
200x,png | 200px wide, converted to PNG format | <a href="https://willnorris.com/api/imageproxy/200x,png/https://willnorris.com/2013/12/small-things.jpg"><img src="https://willnorris.com/api/imageproxy/200x,png/https://willnorris.com/2013/12/small-things.jpg" alt="200x,png"></a>
|
200x,png | 200px wide, converted to PNG format | <a href="https://willnorris.com/api/imageproxy/200x,png/https://willnorris.com/2013/12/small-things.jpg"><img src="https://willnorris.com/api/imageproxy/200x,png/https://willnorris.com/2013/12/small-things.jpg" alt="200x,png"></a>
|
||||||
|
cx175,cw400,ch300,100x | crop to 400x300px starting at (175,0), scale to 100px wide | <a href="https://willnorris.com/api/imageproxy/cx175,cw400,ch300,100x/https://willnorris.com/2013/12/small-things.jpg"><img src="https://willnorris.com/api/imageproxy/cx175,cw400,ch300,100x/https://willnorris.com/2013/12/small-things.jpg" alt="cx175,cw400,ch300,100x"></a>
|
||||||
|
|
||||||
Transformation also works on animated gifs. Here is [this source
|
Transformation also works on animated gifs. Here is [this source
|
||||||
image][material-animation] resized to 200px square and rotated 270 degrees:
|
image][material-animation] resized to 200px square and rotated 270 degrees:
|
||||||
|
|
52
data.go
52
data.go
|
@ -81,10 +81,10 @@ type Options struct {
|
||||||
Format string
|
Format string
|
||||||
|
|
||||||
// Crop rectangle params
|
// Crop rectangle params
|
||||||
CropX int
|
CropX float64
|
||||||
CropY int
|
CropY float64
|
||||||
CropWidth int
|
CropWidth float64
|
||||||
CropHeight int
|
CropHeight float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o Options) String() string {
|
func (o Options) String() string {
|
||||||
|
@ -114,16 +114,16 @@ func (o Options) String() string {
|
||||||
opts = append(opts, o.Format)
|
opts = append(opts, o.Format)
|
||||||
}
|
}
|
||||||
if o.CropX != 0 {
|
if o.CropX != 0 {
|
||||||
opts = append(opts, fmt.Sprintf("%s%d", string(optCropX), o.CropX))
|
opts = append(opts, fmt.Sprintf("%s%v", string(optCropX), o.CropX))
|
||||||
}
|
}
|
||||||
if o.CropY != 0 {
|
if o.CropY != 0 {
|
||||||
opts = append(opts, fmt.Sprintf("%s%d", string(optCropY), o.CropY))
|
opts = append(opts, fmt.Sprintf("%s%v", string(optCropY), o.CropY))
|
||||||
}
|
}
|
||||||
if o.CropWidth != 0 {
|
if o.CropWidth != 0 {
|
||||||
opts = append(opts, fmt.Sprintf("%s%d", string(optCropWidth), o.CropWidth))
|
opts = append(opts, fmt.Sprintf("%s%v", string(optCropWidth), o.CropWidth))
|
||||||
}
|
}
|
||||||
if o.CropHeight != 0 {
|
if o.CropHeight != 0 {
|
||||||
opts = append(opts, fmt.Sprintf("%s%d", string(optCropHeight), o.CropHeight))
|
opts = append(opts, fmt.Sprintf("%s%v", string(optCropHeight), o.CropHeight))
|
||||||
}
|
}
|
||||||
return strings.Join(opts, ",")
|
return strings.Join(opts, ",")
|
||||||
}
|
}
|
||||||
|
@ -133,7 +133,7 @@ func (o Options) String() string {
|
||||||
// the presence of other fields (like Fit). A non-empty Format value is
|
// the presence of other fields (like Fit). A non-empty Format value is
|
||||||
// assumed to involve a transformation.
|
// assumed to involve a transformation.
|
||||||
func (o Options) transform() bool {
|
func (o Options) transform() bool {
|
||||||
return o.Width != 0 || o.Height != 0 || o.Rotate != 0 || o.FlipHorizontal || o.FlipVertical || o.Quality != 0 || o.Format != "" || (o.CropWidth != 0 && o.CropHeight != 0)
|
return o.Width != 0 || o.Height != 0 || o.Rotate != 0 || o.FlipHorizontal || o.FlipVertical || o.Quality != 0 || o.Format != "" || o.CropX != 0 || o.CropY != 0 || o.CropWidth != 0 || o.CropHeight != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseOptions parses str as a list of comma separated transformation options.
|
// ParseOptions parses str as a list of comma separated transformation options.
|
||||||
|
@ -144,17 +144,19 @@ func (o Options) transform() bool {
|
||||||
//
|
//
|
||||||
// There are four options controlling rectangle crop:
|
// There are four options controlling rectangle crop:
|
||||||
//
|
//
|
||||||
// cx{x} - X coordinate of top left rectangle corner
|
// cx{x} - X coordinate of top left rectangle corner (default: 0)
|
||||||
// cy{y} - Y coordinate of top left rectangle corner
|
// cy{y} - Y coordinate of top left rectangle corner (default: 0)
|
||||||
// cw{width} - rectangle width
|
// cw{width} - rectangle width (default: image width)
|
||||||
// ch{height} - rectangle height
|
// ch{height} - rectangle height (default: image height)
|
||||||
//
|
//
|
||||||
// ch and cw are required to enable crop and they must be positive integers. If
|
// For all options, integer values are interpreted as exact pixel values and
|
||||||
// the rectangle is larger than the image, crop will not be applied. If the
|
// floats between 0 and 1 are interpreted as percentages of the original image
|
||||||
// rectangle does not fit the image in any of the dimensions, it will be moved
|
// size. Negative values for cx and cy are measured from the right and bottom
|
||||||
// to produce an image of given size. Crop is applied before any other
|
// edges of the image, respectively.
|
||||||
// transformations. If the rectangle is smaller than the requested resize and
|
//
|
||||||
// scaleUp is disabled, the image will be of the same size as the rectangle.
|
// If the crop width or height exceed the width or height of the image, the
|
||||||
|
// crop width or height will be adjusted, preserving the specified cx and cy
|
||||||
|
// values. Rectangular crop is applied before any other transformations.
|
||||||
//
|
//
|
||||||
// Size and Cropping
|
// Size and Cropping
|
||||||
//
|
//
|
||||||
|
@ -224,8 +226,8 @@ func (o Options) transform() bool {
|
||||||
// 100,fv,fh - 100 pixels square, flipped horizontal and vertical
|
// 100,fv,fh - 100 pixels square, flipped horizontal and vertical
|
||||||
// 200x,q60 - 200 pixels wide, proportional height, 60% quality
|
// 200x,q60 - 200 pixels wide, proportional height, 60% quality
|
||||||
// 200x,png - 200 pixels wide, converted to PNG format
|
// 200x,png - 200 pixels wide, converted to PNG format
|
||||||
// cw100,ch200 - crop fragment that starts at (0,0), is 100px wide and 200px tall
|
// cw100,ch100 - crop image to 100px square, starting at (0,0)
|
||||||
// cw100,ch200,cx10,cy20 - crop fragment that start at (10,20) is 100px wide and 200px tall
|
// cx10,cy20,cw100,ch200 - crop image starting at (10,20) is 100px wide and 200px tall
|
||||||
func ParseOptions(str string) Options {
|
func ParseOptions(str string) Options {
|
||||||
var options Options
|
var options Options
|
||||||
|
|
||||||
|
@ -253,16 +255,16 @@ func ParseOptions(str string) Options {
|
||||||
options.Signature = strings.TrimPrefix(opt, optSignaturePrefix)
|
options.Signature = strings.TrimPrefix(opt, optSignaturePrefix)
|
||||||
case strings.HasPrefix(opt, optCropX):
|
case strings.HasPrefix(opt, optCropX):
|
||||||
value := strings.TrimPrefix(opt, optCropX)
|
value := strings.TrimPrefix(opt, optCropX)
|
||||||
options.CropX, _ = strconv.Atoi(value)
|
options.CropX, _ = strconv.ParseFloat(value, 64)
|
||||||
case strings.HasPrefix(opt, optCropY):
|
case strings.HasPrefix(opt, optCropY):
|
||||||
value := strings.TrimPrefix(opt, optCropY)
|
value := strings.TrimPrefix(opt, optCropY)
|
||||||
options.CropY, _ = strconv.Atoi(value)
|
options.CropY, _ = strconv.ParseFloat(value, 64)
|
||||||
case strings.HasPrefix(opt, optCropWidth):
|
case strings.HasPrefix(opt, optCropWidth):
|
||||||
value := strings.TrimPrefix(opt, optCropWidth)
|
value := strings.TrimPrefix(opt, optCropWidth)
|
||||||
options.CropWidth, _ = strconv.Atoi(value)
|
options.CropWidth, _ = strconv.ParseFloat(value, 64)
|
||||||
case strings.HasPrefix(opt, optCropHeight):
|
case strings.HasPrefix(opt, optCropHeight):
|
||||||
value := strings.TrimPrefix(opt, optCropHeight)
|
value := strings.TrimPrefix(opt, optCropHeight)
|
||||||
options.CropHeight, _ = strconv.Atoi(value)
|
options.CropHeight, _ = strconv.ParseFloat(value, 64)
|
||||||
case strings.Contains(opt, optSizeDelimiter):
|
case strings.Contains(opt, optSizeDelimiter):
|
||||||
size := strings.SplitN(opt, optSizeDelimiter, 2)
|
size := strings.SplitN(opt, optSizeDelimiter, 2)
|
||||||
if w := size[0]; w != "" {
|
if w := size[0]; w != "" {
|
||||||
|
|
48
transform.go
48
transform.go
|
@ -130,31 +130,47 @@ 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) (x0, y0, x1, y1 int, crop bool) {
|
func cropParams(m image.Image, opt Options) (x0, y0, x1, y1 int, crop bool) {
|
||||||
// crop params not set
|
if opt.CropX == 0 && opt.CropY == 0 && opt.CropWidth == 0 && opt.CropHeight == 0 {
|
||||||
if opt.CropHeight <= 0 || opt.CropWidth <= 0 {
|
|
||||||
return 0, 0, 0, 0, false
|
return 0, 0, 0, 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// width and height of image
|
||||||
imgW := m.Bounds().Max.X - m.Bounds().Min.X
|
imgW := m.Bounds().Max.X - m.Bounds().Min.X
|
||||||
imgH := m.Bounds().Max.Y - m.Bounds().Min.Y
|
imgH := m.Bounds().Max.Y - m.Bounds().Min.Y
|
||||||
|
|
||||||
x0 = opt.CropX
|
// top left coordinate of crop
|
||||||
y0 = opt.CropY
|
x0 = evaluateFloat(math.Abs(opt.CropX), imgW)
|
||||||
|
if opt.CropX < 0 {
|
||||||
// crop rectangle out of image bounds horizontally
|
x0 = imgW - x0
|
||||||
// -> moved to point (image_width - rectangle_width) or 0, whichever is larger
|
|
||||||
if opt.CropX > imgW || opt.CropX+opt.CropWidth > imgW {
|
|
||||||
x0 = int(math.Max(0, float64(imgW-opt.CropWidth)))
|
|
||||||
}
|
}
|
||||||
// crop rectangle out of image bounds vertically
|
y0 = evaluateFloat(math.Abs(opt.CropY), imgH)
|
||||||
// -> moved to point (image_height - rectangle_height) or 0, whichever is larger
|
if opt.CropY < 0 {
|
||||||
if opt.CropY > imgH || opt.CropY+opt.CropHeight > imgH {
|
y0 = imgH - y0
|
||||||
y0 = int(math.Max(0, float64(imgH-opt.CropHeight)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// make rectangle fit the image
|
// width and height of crop
|
||||||
x1 = int(math.Min(float64(imgW), float64(opt.CropX+opt.CropWidth)))
|
w := evaluateFloat(opt.CropWidth, imgW)
|
||||||
y1 = int(math.Min(float64(imgH), float64(opt.CropY+opt.CropHeight)))
|
if w == 0 {
|
||||||
|
w = imgW
|
||||||
|
}
|
||||||
|
h := evaluateFloat(opt.CropHeight, imgH)
|
||||||
|
if h == 0 {
|
||||||
|
h = imgH
|
||||||
|
}
|
||||||
|
|
||||||
|
if x0 == 0 && y0 == 0 && w == imgW && h == imgH {
|
||||||
|
return 0, 0, 0, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// bottom right coordinate of crop
|
||||||
|
x1 = x0 + w
|
||||||
|
if x1 > imgW {
|
||||||
|
x1 = imgW
|
||||||
|
}
|
||||||
|
y1 = y0 + h
|
||||||
|
if y1 > imgH {
|
||||||
|
y1 = imgH
|
||||||
|
}
|
||||||
|
|
||||||
return x0, y0, x1, y1, true
|
return x0, y0, x1, y1, true
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,13 +81,15 @@ func TestCropParams(t *testing.T) {
|
||||||
x0, y0, x1, y1 int
|
x0, y0, x1, y1 int
|
||||||
crop bool
|
crop bool
|
||||||
}{
|
}{
|
||||||
{Options{CropHeight: 0, CropWidth: 10}, 0, 0, 0, 0, false},
|
{Options{CropWidth: 10, CropHeight: 0}, 0, 0, 10, 128, true},
|
||||||
{Options{CropHeight: 10, CropWidth: 0}, 0, 0, 0, 0, false},
|
{Options{CropWidth: 0, CropHeight: 10}, 0, 0, 64, 10, true},
|
||||||
{Options{CropHeight: -1, CropWidth: -1}, 0, 0, 0, 0, false},
|
{Options{CropWidth: -1, CropHeight: -1}, 0, 0, 0, 0, false},
|
||||||
{Options{CropWidth: 50, CropHeight: 100}, 0, 0, 50, 100, true},
|
{Options{CropWidth: 50, CropHeight: 100}, 0, 0, 50, 100, true},
|
||||||
{Options{CropWidth: 100, CropHeight: 100}, 0, 0, 64, 100, true},
|
{Options{CropWidth: 100, CropHeight: 100}, 0, 0, 64, 100, true},
|
||||||
{Options{CropX: 50, CropY: 100, CropWidth: 50, CropHeight: 100}, 14, 28, 64, 128, true},
|
{Options{CropX: 50, CropY: 100}, 50, 100, 64, 128, true},
|
||||||
{Options{CropX: 50, CropY: 100, CropWidth: 100, CropHeight: 150}, 0, 0, 64, 128, true},
|
{Options{CropX: 50, CropY: 100, CropWidth: 100, CropHeight: 150}, 50, 100, 64, 128, true},
|
||||||
|
{Options{CropX: -50, CropY: -50}, 14, 78, 64, 128, true},
|
||||||
|
{Options{CropY: 0.5, CropWidth: 0.5}, 0, 64, 32, 128, true},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
x0, y0, x1, y1, crop := cropParams(src, tt.opt)
|
x0, y0, x1, y1, crop := cropParams(src, tt.opt)
|
||||||
|
|
Loading…
Reference in a new issue