Skip to content

Commit c361000

Browse files
vetlerwillnorris
authored andcommitted
add trim option to remove solid color borders
Fixes #441
1 parent 572ad2d commit c361000

File tree

3 files changed

+135
-2
lines changed

3 files changed

+135
-2
lines changed

data.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const (
3030
optCropWidth = "cw"
3131
optCropHeight = "ch"
3232
optSmartCrop = "sc"
33+
optTrim = "trim"
3334
)
3435

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

8182
// Automatically find good crop points based on image content.
8283
SmartCrop bool
84+
85+
// If true, automatically trim pixels of the same color around the edges
86+
Trim bool
8387
}
8488

8589
func (o Options) String() string {
@@ -123,6 +127,9 @@ func (o Options) String() string {
123127
if o.SmartCrop {
124128
opts = append(opts, optSmartCrop)
125129
}
130+
if o.Trim {
131+
opts = append(opts, optTrim)
132+
}
126133
sort.Strings(opts)
127134
return strings.Join(opts, ",")
128135
}
@@ -132,7 +139,7 @@ func (o Options) String() string {
132139
// the presence of other fields (like Fit). A non-empty Format value is
133140
// assumed to involve a transformation.
134141
func (o Options) transform() bool {
135-
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
142+
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
136143
}
137144

138145
// ParseOptions parses str as a list of comma separated transformation options.
@@ -207,7 +214,7 @@ func (o Options) transform() bool {
207214
//
208215
// # Format
209216
//
210-
// The "jpeg", "png", and "tiff" options can be used to specify the desired
217+
// The "jpeg", "png", and "tiff" options can be used to specify the desired
211218
// image format of the proxied image.
212219
//
213220
// # Signature
@@ -219,6 +226,13 @@ func (o Options) transform() bool {
219226
// See https://github.com/willnorris/imageproxy/blob/master/docs/url-signing.md
220227
// for examples of generating signatures.
221228
//
229+
// # Trim
230+
//
231+
// The "trim" option will automatically trim pixels of the same color around
232+
// the edges of the image. This is useful for removing borders from images
233+
// that have been resized or cropped. The trim option is applied before other
234+
// options such as cropping or resizing.
235+
//
222236
// Examples
223237
//
224238
// 0x0 - no resizing
@@ -251,6 +265,8 @@ func ParseOptions(str string) Options {
251265
options.Format = opt
252266
case opt == optSmartCrop:
253267
options.SmartCrop = true
268+
case opt == optTrim:
269+
options.Trim = true
254270
case strings.HasPrefix(opt, optRotatePrefix):
255271
value := strings.TrimPrefix(opt, optRotatePrefix)
256272
options.Rotate, _ = strconv.Atoi(value)

transform.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,11 @@ func transformImage(m image.Image, opt Options) image.Image {
267267
timer := prometheus.NewTimer(metricTransformationDuration)
268268
defer timer.ObserveDuration()
269269

270+
// trim
271+
if opt.Trim {
272+
m = trimEdges(m)
273+
}
274+
270275
// Parse crop and resize parameters before applying any transforms.
271276
// This is to ensure that any percentage-based values are based off the
272277
// size of the original image.
@@ -311,3 +316,41 @@ func transformImage(m image.Image, opt Options) image.Image {
311316

312317
return m
313318
}
319+
320+
// trimEdges returns a new image with solid color borders of the image removed.
321+
// The pixel at the top left corner is used to match the border color.
322+
func trimEdges(img image.Image) image.Image {
323+
bounds := img.Bounds()
324+
minX, minY, maxX, maxY := bounds.Max.X, bounds.Max.Y, bounds.Min.X, bounds.Min.Y
325+
326+
// Get the color of the first pixel (top-left corner)
327+
baseColor := img.At(bounds.Min.X, bounds.Min.Y)
328+
329+
// Check each pixel and find the bounding box of non-matching pixels
330+
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
331+
for x := bounds.Min.X; x < bounds.Max.X; x++ {
332+
if img.At(x, y) != baseColor { // Non-matching pixel
333+
if x < minX {
334+
minX = x
335+
}
336+
if y < minY {
337+
minY = y
338+
}
339+
if x > maxX {
340+
maxX = x
341+
}
342+
if y > maxY {
343+
maxY = y
344+
}
345+
}
346+
}
347+
}
348+
349+
// If no non-matching pixels are found, return the original image
350+
if minX >= maxX || minY >= maxY {
351+
return img
352+
}
353+
354+
// Crop the image to the bounding box of non-matching pixels
355+
return imaging.Crop(img, image.Rect(minX, minY, maxX+1, maxY+1))
356+
}

transform_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,3 +375,77 @@ func TestTransformImage(t *testing.T) {
375375
}
376376
}
377377
}
378+
379+
func TestTrimEdges(t *testing.T) {
380+
x := color.NRGBA{255, 255, 255, 255}
381+
o := color.NRGBA{0, 0, 0, 255}
382+
383+
tests := []struct {
384+
name string
385+
src image.Image // source image to transform
386+
want image.Image // expected transformed image
387+
}{
388+
{
389+
name: "empty",
390+
src: newImage(0, 0),
391+
want: newImage(0, 0), // same as src
392+
},
393+
{
394+
name: "solid",
395+
src: newImage(8, 8, x),
396+
want: newImage(8, 8, x), // same as src
397+
},
398+
{
399+
name: "square",
400+
src: newImage(4, 4,
401+
x, x, x, x,
402+
x, o, o, x,
403+
x, o, o, x,
404+
x, x, x, x,
405+
),
406+
want: newImage(2, 2,
407+
o, o,
408+
o, o,
409+
),
410+
},
411+
{
412+
name: "diamond",
413+
src: newImage(5, 5,
414+
x, x, x, x, x,
415+
x, x, o, x, x,
416+
x, o, o, o, x,
417+
x, x, o, x, x,
418+
x, x, x, x, x,
419+
),
420+
want: newImage(3, 3,
421+
x, o, x,
422+
o, o, o,
423+
x, o, x,
424+
),
425+
},
426+
{
427+
name: "irregular",
428+
src: newImage(5, 5,
429+
x, o, x, x, x,
430+
x, o, o, x, x,
431+
x, o, o, x, x,
432+
x, x, x, x, x,
433+
x, x, x, x, x,
434+
),
435+
want: newImage(2, 3,
436+
o, x,
437+
o, o,
438+
o, o,
439+
),
440+
},
441+
}
442+
443+
for _, tt := range tests {
444+
t.Run(tt.name, func(t *testing.T) {
445+
got := trimEdges(tt.src)
446+
if !reflect.DeepEqual(got, tt.want) {
447+
t.Errorf("trimEdges() returned image %#v, want %#v", got, tt.want)
448+
}
449+
})
450+
}
451+
}

0 commit comments

Comments
 (0)