From 3ff7fa8b6462d4732ecc6cfd06bc454c36b4f6a0 Mon Sep 17 00:00:00 2001 From: Vetle Leinonen-Roeim Date: Fri, 28 Mar 2025 10:11:43 +0100 Subject: [PATCH 01/10] add trim option to image processing and implement trimEdges function --- data.go | 11 ++++++++++- transform.go | 42 ++++++++++++++++++++++++++++++++++++++++++ transform_test.go | 19 +++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/data.go b/data.go index 0545f8b72..7f4ffe701 100644 --- a/data.go +++ b/data.go @@ -30,6 +30,7 @@ const ( optCropWidth = "cw" optCropHeight = "ch" optSmartCrop = "sc" + optTrim = "trim" ) // URLError reports a malformed URL error. @@ -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 { @@ -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, ",") } @@ -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. @@ -251,6 +258,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) diff --git a/transform.go b/transform.go index 103e20f65..b9d90c96f 100644 --- a/transform.go +++ b/transform.go @@ -309,5 +309,47 @@ func transformImage(m image.Image, opt Options) image.Image { m = imaging.FlipH(m) } + // trim + if opt.Trim { + m = trimEdges(m) + } + 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 + + // 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 + croppedImg := imaging.Crop(img, image.Rect(minX, minY, maxX+1, maxY+1)) + return croppedImg +} diff --git a/transform_test.go b/transform_test.go index b22afabc2..b130ea4ad 100644 --- a/transform_test.go +++ b/transform_test.go @@ -375,3 +375,22 @@ func TestTransformImage(t *testing.T) { } } } + +func TestTrimBordersOfSameColor(t *testing.T) { + src := newImage(4, 4, + color.NRGBA{255, 255, 255, 255}, color.NRGBA{255, 255, 255, 255}, color.NRGBA{255, 255, 255, 255}, color.NRGBA{255, 255, 255, 255}, + color.NRGBA{255, 255, 255, 255}, color.NRGBA{255, 0, 0, 255}, color.NRGBA{255, 0, 0, 255}, color.NRGBA{255, 255, 255, 255}, + color.NRGBA{255, 255, 255, 255}, color.NRGBA{255, 0, 0, 255}, color.NRGBA{255, 0, 0, 255}, color.NRGBA{255, 255, 255, 255}, + color.NRGBA{255, 255, 255, 255}, color.NRGBA{255, 255, 255, 255}, color.NRGBA{255, 255, 255, 255}, color.NRGBA{255, 255, 255, 255}, + ) + + want := newImage(2, 2, + color.NRGBA{255, 0, 0, 255}, color.NRGBA{255, 0, 0, 255}, + color.NRGBA{255, 0, 0, 255}, color.NRGBA{255, 0, 0, 255}, + ) + + got := trimEdges(src) + if !reflect.DeepEqual(got, want) { + t.Errorf("trimEdges() = %v, want %v", got, want) + } +} From a54d7831fac0e58b41e08898be904a19b7e41aa5 Mon Sep 17 00:00:00 2001 From: Vetle Leinonen-Roeim Date: Fri, 28 Mar 2025 11:26:27 +0100 Subject: [PATCH 02/10] add documentation for the trim option in image processing --- data.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/data.go b/data.go index 7f4ffe701..54f58a61f 100644 --- a/data.go +++ b/data.go @@ -226,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 From b1b53c0773c2a600d10e3df224ea7995868875da Mon Sep 17 00:00:00 2001 From: Vetle Leinonen-Roeim Date: Sun, 30 Mar 2025 17:07:16 +0200 Subject: [PATCH 03/10] add test for trimEdges function with single color image --- transform_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/transform_test.go b/transform_test.go index b130ea4ad..2c0625e5c 100644 --- a/transform_test.go +++ b/transform_test.go @@ -394,3 +394,19 @@ func TestTrimBordersOfSameColor(t *testing.T) { t.Errorf("trimEdges() = %v, want %v", got, want) } } + +func TestTrimEdgesSingleColorImage(t *testing.T) { + // Create a 4x4 image filled with a single color (white) + src := newImage(4, 4, 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) + + // Check if the result matches the expected image + if !reflect.DeepEqual(got, want) { + t.Errorf("trimEdges() = %v, want %v", got.Bounds(), want.Bounds()) + } +} From 3a6c85148a7e594fe44370f382d2c009936e2df7 Mon Sep 17 00:00:00 2001 From: Vetle Leinonen-Roeim Date: Sun, 30 Mar 2025 17:08:46 +0200 Subject: [PATCH 04/10] update test for TrimEdges function to use an 8x8 single color image --- transform_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/transform_test.go b/transform_test.go index 2c0625e5c..6dcb01e2a 100644 --- a/transform_test.go +++ b/transform_test.go @@ -396,8 +396,8 @@ func TestTrimBordersOfSameColor(t *testing.T) { } func TestTrimEdgesSingleColorImage(t *testing.T) { - // Create a 4x4 image filled with a single color (white) - src := newImage(4, 4, color.NRGBA{255, 255, 255, 255}) + // 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 From 5c2594a13927d8d94204639e6b5e2d2a9a35b640 Mon Sep 17 00:00:00 2001 From: Vetle Leinonen-Roeim Date: Sun, 30 Mar 2025 17:23:25 +0200 Subject: [PATCH 05/10] add test for trimEdges with circle image --- transform_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/transform_test.go b/transform_test.go index 6dcb01e2a..6dd5e3e28 100644 --- a/transform_test.go +++ b/transform_test.go @@ -410,3 +410,60 @@ func TestTrimEdgesSingleColorImage(t *testing.T) { t.Errorf("trimEdges() = %v, want %v", got.Bounds(), want.Bounds()) } } + +func TestTrimEdgesCircle(t *testing.T) { + // Define colors for better readability + white := color.NRGBA{255, 255, 255, 255} + red := 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, + white, white, white, white, white, white, white, white, white, + white, white, white, red, red, red, white, white, white, + white, white, red, red, red, red, red, white, white, + white, red, red, red, red, red, red, red, white, + white, red, red, red, red, red, red, red, white, + white, red, red, red, red, red, red, red, white, + white, white, red, red, red, red, red, white, white, + white, white, white, red, red, red, white, white, white, + white, white, white, white, white, white, white, white, white, + ) + + // Expected result: a trimmed 7x7 image containing only the circle + want := newImage(7, 7, + white, white, red, red, red, white, white, + white, red, red, red, red, red, white, + red, red, red, red, red, red, red, + red, red, red, red, red, red, red, + red, red, red, red, red, red, red, + white, red, red, red, red, red, white, + white, white, red, red, red, white, white, + ) + + // 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 +} From 209bcc54a3a7535866731133f632803b6028e6ff Mon Sep 17 00:00:00 2001 From: Vetle Leinonen-Roeim Date: Sun, 30 Mar 2025 18:19:29 +0200 Subject: [PATCH 06/10] add test for trimEdges function with uneven vertical rectangle image --- transform_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/transform_test.go b/transform_test.go index 6dd5e3e28..36420a5d7 100644 --- a/transform_test.go +++ b/transform_test.go @@ -449,6 +449,44 @@ func TestTrimEdgesCircle(t *testing.T) { } } +func TestTrimEdgesUnevenVerticalRectangle(t *testing.T) { + // Define colors for better readability + white := color.NRGBA{255, 255, 255, 255} + red := color.NRGBA{255, 0, 0, 255} + + // Create a 9x5 image with a white background and a red diagonal shape + src := newImage(5, 9, + white, white, white, white, white, + white, white, white, red, white, + white, white, red, white, white, + white, red, white, white, white, + white, red, white, white, white, + white, red, white, white, white, + white, white, red, white, white, + white, white, white, red, white, + white, white, white, white, white, + ) + + // Expected result: a trimmed 5x5 image containing only the diagonal shape + want := newImage(3, 7, + white, white, red, + white, red, white, + red, white, white, + red, white, white, + red, white, white, + white, red, white, + white, white, red, + ) + + // 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() From df9676c2b6fdfe7d7747cb3201b8fe3cfe161e12 Mon Sep 17 00:00:00 2001 From: Vetle Leinonen-Roeim Date: Sun, 30 Mar 2025 18:24:07 +0200 Subject: [PATCH 07/10] refactor tests for trimEdges function to improve readability and consistency --- transform_test.go | 97 ++++++++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/transform_test.go b/transform_test.go index 36420a5d7..0c26de9d6 100644 --- a/transform_test.go +++ b/transform_test.go @@ -377,21 +377,24 @@ 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, - color.NRGBA{255, 255, 255, 255}, color.NRGBA{255, 255, 255, 255}, color.NRGBA{255, 255, 255, 255}, color.NRGBA{255, 255, 255, 255}, - color.NRGBA{255, 255, 255, 255}, color.NRGBA{255, 0, 0, 255}, color.NRGBA{255, 0, 0, 255}, color.NRGBA{255, 255, 255, 255}, - color.NRGBA{255, 255, 255, 255}, color.NRGBA{255, 0, 0, 255}, color.NRGBA{255, 0, 0, 255}, color.NRGBA{255, 255, 255, 255}, - color.NRGBA{255, 255, 255, 255}, color.NRGBA{255, 255, 255, 255}, color.NRGBA{255, 255, 255, 255}, color.NRGBA{255, 255, 255, 255}, + w, w, w, w, + w, r, r, w, + w, r, r, w, + w, w, w, w, ) want := newImage(2, 2, - color.NRGBA{255, 0, 0, 255}, color.NRGBA{255, 0, 0, 255}, - color.NRGBA{255, 0, 0, 255}, color.NRGBA{255, 0, 0, 255}, + r, r, + r, r, ) got := trimEdges(src) - if !reflect.DeepEqual(got, want) { - t.Errorf("trimEdges() = %v, want %v", got, want) + // Compare pixel data + if !compareImages(got, want) { + t.Errorf("trimEdges() pixel data does not match expected result") } } @@ -405,39 +408,39 @@ func TestTrimEdgesSingleColorImage(t *testing.T) { // Apply the trimEdges function got := trimEdges(src) - // Check if the result matches the expected image - if !reflect.DeepEqual(got, want) { - t.Errorf("trimEdges() = %v, want %v", got.Bounds(), want.Bounds()) + // 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 - white := color.NRGBA{255, 255, 255, 255} - red := color.NRGBA{255, 0, 0, 255} + 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, - white, white, white, white, white, white, white, white, white, - white, white, white, red, red, red, white, white, white, - white, white, red, red, red, red, red, white, white, - white, red, red, red, red, red, red, red, white, - white, red, red, red, red, red, red, red, white, - white, red, red, red, red, red, red, red, white, - white, white, red, red, red, red, red, white, white, - white, white, white, red, red, red, white, white, white, - white, white, white, white, white, white, white, white, white, + 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, - white, white, red, red, red, white, white, - white, red, red, red, red, red, white, - red, red, red, red, red, red, red, - red, red, red, red, red, red, red, - red, red, red, red, red, red, red, - white, red, red, red, red, red, white, - white, white, red, red, red, white, white, + 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 @@ -451,31 +454,31 @@ func TestTrimEdgesCircle(t *testing.T) { func TestTrimEdgesUnevenVerticalRectangle(t *testing.T) { // Define colors for better readability - white := color.NRGBA{255, 255, 255, 255} - red := color.NRGBA{255, 0, 0, 255} + 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, - white, white, white, white, white, - white, white, white, red, white, - white, white, red, white, white, - white, red, white, white, white, - white, red, white, white, white, - white, red, white, white, white, - white, white, red, white, white, - white, white, white, red, white, - white, white, white, white, white, + 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, - white, white, red, - white, red, white, - red, white, white, - red, white, white, - red, white, white, - white, red, white, - white, white, red, + 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 From 5a66c793b95bd3521993afe8d198eea2c0a0ae67 Mon Sep 17 00:00:00 2001 From: Vetle Leinonen-Roeim Date: Sun, 30 Mar 2025 18:26:09 +0200 Subject: [PATCH 08/10] add test for trimEdges function with uneven image dimensions --- transform_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/transform_test.go b/transform_test.go index 0c26de9d6..014e84281 100644 --- a/transform_test.go +++ b/transform_test.go @@ -508,3 +508,34 @@ func compareImages(img1, img2 image.Image) bool { } 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") + } +} From 493fe180d3dd009f20b58c1a72d471a2271d7df1 Mon Sep 17 00:00:00 2001 From: Vetle Leinonen-Roeim Date: Sun, 30 Mar 2025 18:36:14 +0200 Subject: [PATCH 09/10] add test for trimEdges function with empty image --- transform_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/transform_test.go b/transform_test.go index 014e84281..253c69b41 100644 --- a/transform_test.go +++ b/transform_test.go @@ -539,3 +539,19 @@ func TestTrimEdgesUneven(t *testing.T) { 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()) + } +} From 26f485ca3c2cadfbea9fc74cd43612a65873e543 Mon Sep 17 00:00:00 2001 From: Vetle Leinonen-Roeim Date: Tue, 8 Apr 2025 23:01:48 +0200 Subject: [PATCH 10/10] trim edges before applying other transformations --- transform.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/transform.go b/transform.go index b9d90c96f..df4da7fff 100644 --- a/transform.go +++ b/transform.go @@ -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. @@ -309,11 +314,6 @@ func transformImage(m image.Image, opt Options) image.Image { m = imaging.FlipH(m) } - // trim - if opt.Trim { - m = trimEdges(m) - } - return m }