Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion data.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
optCropWidth = "cw"
optCropHeight = "ch"
optSmartCrop = "sc"
optTrim = "trim"
)

// URLError reports a malformed URL error.
Expand Down Expand Up @@ -80,6 +81,9 @@ type Options struct {

// Automatically find good crop points based on image content.
SmartCrop bool

// If true, automatically trim pixels of the same color around the edges
Trim bool
}

func (o Options) String() string {
Expand Down Expand Up @@ -123,6 +127,9 @@ func (o Options) String() string {
if o.SmartCrop {
opts = append(opts, optSmartCrop)
}
if o.Trim {
opts = append(opts, optTrim)
}
sort.Strings(opts)
return strings.Join(opts, ",")
}
Expand All @@ -132,7 +139,7 @@ func (o Options) String() string {
// the presence of other fields (like Fit). A non-empty Format value is
// assumed to involve a transformation.
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.
Expand Down Expand Up @@ -219,6 +226,13 @@ func (o Options) transform() bool {
// See https://github.com/willnorris/imageproxy/blob/master/docs/url-signing.md
// 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 after any
// cropping or resizing has been performed.
//
// Examples
//
// 0x0 - no resizing
Expand Down Expand Up @@ -251,6 +265,8 @@ func ParseOptions(str string) Options {
options.Format = opt
case opt == optSmartCrop:
options.SmartCrop = true
case opt == optTrim:
options.Trim = true
case strings.HasPrefix(opt, optRotatePrefix):
value := strings.TrimPrefix(opt, optRotatePrefix)
options.Rotate, _ = strconv.Atoi(value)
Expand Down
42 changes: 42 additions & 0 deletions transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,11 @@ func transformImage(m image.Image, opt Options) image.Image {
timer := prometheus.NewTimer(metricTransformationDuration)
defer timer.ObserveDuration()

// trim
if opt.Trim {
m = trimEdges(m)
}

// Parse crop and resize parameters before applying any transforms.
// This is to ensure that any percentage-based values are based off the
// size of the original image.
Expand Down Expand Up @@ -311,3 +316,40 @@ func transformImage(m image.Image, opt Options) image.Image {

return m
}

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
Comment thread
vetler marked this conversation as resolved.

// 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
Comment thread
vetler marked this conversation as resolved.
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
croppedImg := imaging.Crop(img, image.Rect(minX, minY, maxX+1, maxY+1))
return croppedImg
}
180 changes: 180 additions & 0 deletions transform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,183 @@ func TestTransformImage(t *testing.T) {
}
}
}

func TestTrimBordersOfSameColor(t *testing.T) {
w := color.NRGBA{255, 255, 255, 255}
r := color.NRGBA{255, 0, 0, 255}
src := newImage(4, 4,
Comment thread
vetler marked this conversation as resolved.
w, w, w, w,
w, r, r, w,
w, r, r, w,
w, w, w, w,
)

want := newImage(2, 2,
r, r,
r, r,
)

got := trimEdges(src)
// Compare pixel data
if !compareImages(got, want) {
t.Errorf("trimEdges() pixel data does not match expected result")
}
}

func TestTrimEdgesSingleColorImage(t *testing.T) {
// Create an 8x8 image filled with a single color (white)
src := newImage(8, 8, color.NRGBA{255, 255, 255, 255})

// The expected result should be the same as the source image
want := src

// Apply the trimEdges function
got := trimEdges(src)

// Compare pixel data
if !compareImages(got, want) {
t.Errorf("trimEdges() pixel data does not match expected result")
}
}

func TestTrimEdgesCircle(t *testing.T) {
// Define colors for better readability
w := color.NRGBA{255, 255, 255, 255}
r := color.NRGBA{255, 0, 0, 255}

// Create a 9x9 image with a white background and a larger red circle in the center
src := newImage(9, 9,
w, w, w, w, w, w, w, w, w,
w, w, w, r, r, r, w, w, w,
w, w, r, r, r, r, r, w, w,
w, r, r, r, r, r, r, r, w,
w, r, r, r, r, r, r, r, w,
w, r, r, r, r, r, r, r, w,
w, w, r, r, r, r, r, w, w,
w, w, w, r, r, r, w, w, w,
w, w, w, w, w, w, w, w, w,
)

// Expected result: a trimmed 7x7 image containing only the circle
want := newImage(7, 7,
w, w, r, r, r, w, w,
w, r, r, r, r, r, w,
r, r, r, r, r, r, r,
r, r, r, r, r, r, r,
r, r, r, r, r, r, r,
w, r, r, r, r, r, w,
w, w, r, r, r, w, w,
)

// Apply the trimEdges function
got := trimEdges(src)

// Compare pixel data
if !compareImages(got, want) {
t.Errorf("trimEdges() pixel data does not match expected result")
}
}

func TestTrimEdgesUnevenVerticalRectangle(t *testing.T) {
// Define colors for better readability
w := color.NRGBA{255, 255, 255, 255}
r := color.NRGBA{255, 0, 0, 255}

// Create a 9x5 image with a white background and a red diagonal shape
src := newImage(5, 9,
w, w, w, w, w,
w, w, w, r, w,
w, w, r, w, w,
w, r, w, w, w,
w, r, w, w, w,
w, r, w, w, w,
w, w, r, w, w,
w, w, w, r, w,
w, w, w, w, w,
)

// Expected result: a trimmed 5x5 image containing only the diagonal shape
want := newImage(3, 7,
w, w, r,
w, r, w,
r, w, w,
r, w, w,
r, w, w,
w, r, w,
w, w, r,
)

// Apply the trimEdges function
got := trimEdges(src)

// Compare pixel data
if !compareImages(got, want) {
t.Errorf("trimEdges() pixel data does not match expected result")
}
}

func compareImages(img1, img2 image.Image) bool {
bounds1 := img1.Bounds()
bounds2 := img2.Bounds()
if !bounds1.Eq(bounds2) {
return false
}

for y := bounds1.Min.Y; y < bounds1.Max.Y; y++ {
for x := bounds1.Min.X; x < bounds1.Max.X; x++ {
r1, g1, b1, a1 := img1.At(x, y).RGBA()
r2, g2, b2, a2 := img2.At(x, y).RGBA()
if r1 != r2 || g1 != g2 || b1 != b2 || a1 != a2 {
return false
}
}
}
return true
}

func TestTrimEdgesUneven(t *testing.T) {
// Define colors for better readability
w := color.NRGBA{255, 255, 255, 255} // white
r := color.NRGBA{255, 0, 0, 255} // red

// Create a 6x4 image with a white background and a red inner rectangle
src := newImage(4, 6,
w, w, w, w,
w, w, r, w,
w, r, r, w,
w, r, r, w,
w, w, w, w,
w, w, w, w,
)

// Expected result: a trimmed 2x3 image containing only the red rectangle
want := newImage(2, 3,
w, r,
r, r,
r, r,
)

// Apply the trimEdges function
got := trimEdges(src)

// Compare pixel data
if !compareImages(got, want) {
t.Errorf("trimEdges() pixel data does not match expected result")
}
}

func TestTrimEdgesEmptyImage(t *testing.T) {
// Create an empty image (0x0 dimensions)
src := newImage(0, 0)

// The expected result should also be an empty image
want := src

// Apply the trimEdges function
got := trimEdges(src)

// Compare pixel data
if !compareImages(got, want) {
t.Errorf("trimEdges() for empty image returned %v, want %v", got.Bounds(), want.Bounds())
}
}