mirror of
https://github.com/willnorris/imageproxy.git
synced 2026-06-22 19:48:07 +02:00
parent
572ad2db78
commit
c361000ff4
3 changed files with 135 additions and 2 deletions
20
data.go
20
data.go
|
|
@ -30,6 +30,7 @@ const (
|
||||||
optCropWidth = "cw"
|
optCropWidth = "cw"
|
||||||
optCropHeight = "ch"
|
optCropHeight = "ch"
|
||||||
optSmartCrop = "sc"
|
optSmartCrop = "sc"
|
||||||
|
optTrim = "trim"
|
||||||
)
|
)
|
||||||
|
|
||||||
// URLError reports a malformed URL error.
|
// URLError reports a malformed URL error.
|
||||||
|
|
@ -80,6 +81,9 @@ type Options struct {
|
||||||
|
|
||||||
// Automatically find good crop points based on image content.
|
// Automatically find good crop points based on image content.
|
||||||
SmartCrop bool
|
SmartCrop bool
|
||||||
|
|
||||||
|
// If true, automatically trim pixels of the same color around the edges
|
||||||
|
Trim bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o Options) String() string {
|
func (o Options) String() string {
|
||||||
|
|
@ -123,6 +127,9 @@ func (o Options) String() string {
|
||||||
if o.SmartCrop {
|
if o.SmartCrop {
|
||||||
opts = append(opts, optSmartCrop)
|
opts = append(opts, optSmartCrop)
|
||||||
}
|
}
|
||||||
|
if o.Trim {
|
||||||
|
opts = append(opts, optTrim)
|
||||||
|
}
|
||||||
sort.Strings(opts)
|
sort.Strings(opts)
|
||||||
return strings.Join(opts, ",")
|
return strings.Join(opts, ",")
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +139,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.CropX != 0 || o.CropY != 0 || 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 || o.Trim
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseOptions parses str as a list of comma separated transformation options.
|
// ParseOptions parses str as a list of comma separated transformation options.
|
||||||
|
|
@ -207,7 +214,7 @@ func (o Options) transform() bool {
|
||||||
//
|
//
|
||||||
// # Format
|
// # Format
|
||||||
//
|
//
|
||||||
// The "jpeg", "png", and "tiff" options can be used to specify the desired
|
// The "jpeg", "png", and "tiff" options can be used to specify the desired
|
||||||
// image format of the proxied image.
|
// image format of the proxied image.
|
||||||
//
|
//
|
||||||
// # Signature
|
// # Signature
|
||||||
|
|
@ -219,6 +226,13 @@ func (o Options) transform() bool {
|
||||||
// See https://github.com/willnorris/imageproxy/blob/master/docs/url-signing.md
|
// See https://github.com/willnorris/imageproxy/blob/master/docs/url-signing.md
|
||||||
// for examples of generating signatures.
|
// for examples of generating signatures.
|
||||||
//
|
//
|
||||||
|
// # Trim
|
||||||
|
//
|
||||||
|
// The "trim" option will automatically trim pixels of the same color around
|
||||||
|
// the edges of the image. This is useful for removing borders from images
|
||||||
|
// that have been resized or cropped. The trim option is applied before other
|
||||||
|
// options such as cropping or resizing.
|
||||||
|
//
|
||||||
// Examples
|
// Examples
|
||||||
//
|
//
|
||||||
// 0x0 - no resizing
|
// 0x0 - no resizing
|
||||||
|
|
@ -251,6 +265,8 @@ func ParseOptions(str string) Options {
|
||||||
options.Format = opt
|
options.Format = opt
|
||||||
case opt == optSmartCrop:
|
case opt == optSmartCrop:
|
||||||
options.SmartCrop = true
|
options.SmartCrop = true
|
||||||
|
case opt == optTrim:
|
||||||
|
options.Trim = 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)
|
||||||
|
|
|
||||||
43
transform.go
43
transform.go
|
|
@ -267,6 +267,11 @@ func transformImage(m image.Image, opt Options) image.Image {
|
||||||
timer := prometheus.NewTimer(metricTransformationDuration)
|
timer := prometheus.NewTimer(metricTransformationDuration)
|
||||||
defer timer.ObserveDuration()
|
defer timer.ObserveDuration()
|
||||||
|
|
||||||
|
// trim
|
||||||
|
if opt.Trim {
|
||||||
|
m = trimEdges(m)
|
||||||
|
}
|
||||||
|
|
||||||
// Parse crop and resize parameters before applying any transforms.
|
// Parse crop and resize parameters before applying any transforms.
|
||||||
// This is to ensure that any percentage-based values are based off the
|
// This is to ensure that any percentage-based values are based off the
|
||||||
// size of the original image.
|
// size of the original image.
|
||||||
|
|
@ -311,3 +316,41 @@ func transformImage(m image.Image, opt Options) image.Image {
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trimEdges returns a new image with solid color borders of the image removed.
|
||||||
|
// The pixel at the top left corner is used to match the border color.
|
||||||
|
func trimEdges(img image.Image) image.Image {
|
||||||
|
bounds := img.Bounds()
|
||||||
|
minX, minY, maxX, maxY := bounds.Max.X, bounds.Max.Y, bounds.Min.X, bounds.Min.Y
|
||||||
|
|
||||||
|
// Get the color of the first pixel (top-left corner)
|
||||||
|
baseColor := img.At(bounds.Min.X, bounds.Min.Y)
|
||||||
|
|
||||||
|
// Check each pixel and find the bounding box of non-matching pixels
|
||||||
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||||
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||||
|
if img.At(x, y) != baseColor { // Non-matching pixel
|
||||||
|
if x < minX {
|
||||||
|
minX = x
|
||||||
|
}
|
||||||
|
if y < minY {
|
||||||
|
minY = y
|
||||||
|
}
|
||||||
|
if x > maxX {
|
||||||
|
maxX = x
|
||||||
|
}
|
||||||
|
if y > maxY {
|
||||||
|
maxY = y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no non-matching pixels are found, return the original image
|
||||||
|
if minX >= maxX || minY >= maxY {
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crop the image to the bounding box of non-matching pixels
|
||||||
|
return imaging.Crop(img, image.Rect(minX, minY, maxX+1, maxY+1))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -375,3 +375,77 @@ func TestTransformImage(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTrimEdges(t *testing.T) {
|
||||||
|
x := color.NRGBA{255, 255, 255, 255}
|
||||||
|
o := color.NRGBA{0, 0, 0, 255}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
src image.Image // source image to transform
|
||||||
|
want image.Image // expected transformed image
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
src: newImage(0, 0),
|
||||||
|
want: newImage(0, 0), // same as src
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "solid",
|
||||||
|
src: newImage(8, 8, x),
|
||||||
|
want: newImage(8, 8, x), // same as src
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "square",
|
||||||
|
src: newImage(4, 4,
|
||||||
|
x, x, x, x,
|
||||||
|
x, o, o, x,
|
||||||
|
x, o, o, x,
|
||||||
|
x, x, x, x,
|
||||||
|
),
|
||||||
|
want: newImage(2, 2,
|
||||||
|
o, o,
|
||||||
|
o, o,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "diamond",
|
||||||
|
src: newImage(5, 5,
|
||||||
|
x, x, x, x, x,
|
||||||
|
x, x, o, x, x,
|
||||||
|
x, o, o, o, x,
|
||||||
|
x, x, o, x, x,
|
||||||
|
x, x, x, x, x,
|
||||||
|
),
|
||||||
|
want: newImage(3, 3,
|
||||||
|
x, o, x,
|
||||||
|
o, o, o,
|
||||||
|
x, o, x,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "irregular",
|
||||||
|
src: newImage(5, 5,
|
||||||
|
x, o, x, x, x,
|
||||||
|
x, o, o, x, x,
|
||||||
|
x, o, o, x, x,
|
||||||
|
x, x, x, x, x,
|
||||||
|
x, x, x, x, x,
|
||||||
|
),
|
||||||
|
want: newImage(2, 3,
|
||||||
|
o, x,
|
||||||
|
o, o,
|
||||||
|
o, o,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := trimEdges(tt.src)
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("trimEdges() returned image %#v, want %#v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue