Skip to content

Commit

Permalink
Use bounding box of alpha+non-alpha for trim op #2166
Browse files Browse the repository at this point in the history
  • Loading branch information
lovell committed Jul 5, 2022
1 parent e3cab7f commit e0d3c6e
Show file tree
Hide file tree
Showing 8 changed files with 44 additions and 14 deletions.
3 changes: 2 additions & 1 deletion docs/api-resize.md
Expand Up @@ -236,7 +236,8 @@ Returns **Sharp**
## trim

Trim "boring" pixels from all edges that contain values similar to the top-left pixel.
Images consisting entirely of a single colour will calculate "boring" using the alpha channel, if any.

Images with an alpha channel will use the combined bounding box of alpha and non-alpha channels.

The `info` response Object, obtained from callback of `.toFile()` or `.toBuffer()`,
will contain `trimOffsetLeft` and `trimOffsetTop` properties.
Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.md
Expand Up @@ -8,6 +8,9 @@ Requires libvips v8.13.0

* Drop support for Node.js 12, now requires Node.js >= 14.15.0.

* Use combined bounding box of alpha and non-alpha channels for `trim` operation.
[#2166](https://github.com/lovell/sharp/issues/2166)

* Re-introduce support for greyscale ICC profiles (temporarily removed in 0.30.2).
[#3114](https://github.com/lovell/sharp/issues/3114)

Expand Down
2 changes: 1 addition & 1 deletion docs/search-index.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion lib/resize.js
Expand Up @@ -430,7 +430,8 @@ function extract (options) {

/**
* Trim "boring" pixels from all edges that contain values similar to the top-left pixel.
* Images consisting entirely of a single colour will calculate "boring" using the alpha channel, if any.
*
* Images with an alpha channel will use the combined bounding box of alpha and non-alpha channels.
*
* The `info` response Object, obtained from callback of `.toFile()` or `.toBuffer()`,
* will contain `trimOffsetLeft` and `trimOffsetTop` properties.
Expand Down
34 changes: 23 additions & 11 deletions src/operations.cc
Expand Up @@ -275,19 +275,31 @@ namespace sharp {
left = image.find_trim(&top, &width, &height, VImage::option()
->set("background", background(0, 0))
->set("threshold", threshold));
if (width == 0 || height == 0) {
if (HasAlpha(image)) {
// Search alpha channel
VImage alpha = image[image.bands() - 1];
VImage backgroundAlpha = alpha.extract_area(0, 0, 1, 1);
left = alpha.find_trim(&top, &width, &height, VImage::option()
->set("background", backgroundAlpha(0, 0))
->set("threshold", threshold));
}
if (width == 0 || height == 0) {
throw VError("Unexpected error while trimming. Try to lower the tolerance");
if (HasAlpha(image)) {
// Search alpha channel (A)
int leftA, topA, widthA, heightA;
VImage alpha = image[image.bands() - 1];
VImage backgroundAlpha = alpha.extract_area(0, 0, 1, 1);
leftA = alpha.find_trim(&topA, &widthA, &heightA, VImage::option()
->set("background", backgroundAlpha(0, 0))
->set("threshold", threshold));
if (widthA > 0 && heightA > 0) {
if (width > 0 && height > 0) {
// Combined bounding box (B)
int const leftB = std::min(left, leftA);
int const topB = std::min(top, topA);
int const widthB = std::max(left + width, leftA + widthA) - leftB;
int const heightB = std::max(top + height, topA + heightA) - topB;
return image.extract_area(leftB, topB, widthB, heightB);
} else {
// Use alpha only
return image.extract_area(leftA, topA, widthA, heightA);
}
}
}
if (width == 0 || height == 0) {
throw VError("Unexpected error while trimming. Try to lower the tolerance");
}
return image.extract_area(left, top, width, height);
}

Expand Down
1 change: 1 addition & 0 deletions test/fixtures/index.js
Expand Up @@ -94,6 +94,7 @@ module.exports = {
inputPngSolidAlpha: getPath('with-alpha.png'), // https://github.com/lovell/sharp/issues/1599
inputPngP3: getPath('p3.png'), // https://github.com/lovell/sharp/issues/2862
inputPngPalette: getPath('swiss.png'), // https://github.com/randy408/libspng/issues/188
inputPngTrimIncludeAlpha: getPath('trim-mc.png'), // https://github.com/lovell/sharp/issues/2166

inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp
inputWebPWithTransparency: getPath('5_webp_a.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp
Expand Down
Binary file added test/fixtures/trim-mc.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions test/unit/trim.js
Expand Up @@ -128,6 +128,18 @@ describe('Trim borders', function () {
)
);

it('Ensure trim uses bounding box of alpha and non-alpha channels', async () => {
const { info } = await sharp(fixtures.inputPngTrimIncludeAlpha)
.trim()
.toBuffer({ resolveWithObject: true });

const { width, height, trimOffsetTop, trimOffsetLeft } = info;
assert.strictEqual(width, 179);
assert.strictEqual(height, 123);
assert.strictEqual(trimOffsetTop, -44);
assert.strictEqual(trimOffsetLeft, -13);
});

describe('Invalid thresholds', function () {
[-1, 'fail', {}].forEach(function (threshold) {
it(JSON.stringify(threshold), function () {
Expand Down

2 comments on commit e0d3c6e

@kleisauke
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, a alternative faster approach would be to do the flattening against magenta. For example, see this patch (the test suite still passes with this):

Details
--- a/src/operations.cc
+++ b/src/operations.cc
@@ -269,34 +269,13 @@ namespace sharp {
     // Top-left pixel provides the background colour
     VImage background = image.extract_area(0, 0, 1, 1);
     if (HasAlpha(background)) {
-      background = background.flatten();
+      background = background.flatten(VImage::option()
+        ->set("background", std::vector<double>{255.0, 0, 255.0}));
     }
     int left, top, width, height;
     left = image.find_trim(&top, &width, &height, VImage::option()
       ->set("background", background(0, 0))
       ->set("threshold", threshold));
-    if (HasAlpha(image)) {
-      // Search alpha channel (A)
-      int leftA, topA, widthA, heightA;
-      VImage alpha = image[image.bands() - 1];
-      VImage backgroundAlpha = alpha.extract_area(0, 0, 1, 1);
-      leftA = alpha.find_trim(&topA, &widthA, &heightA, VImage::option()
-        ->set("background", backgroundAlpha(0, 0))
-        ->set("threshold", threshold));
-      if (widthA > 0 && heightA > 0) {
-        if (width > 0 && height > 0) {
-          // Combined bounding box (B)
-          int const leftB = std::min(left, leftA);
-          int const topB = std::min(top, topA);
-          int const widthB = std::max(left + width, leftA + widthA) - leftB;
-          int const heightB = std::max(top + height, topA + heightA) - topB;
-          return image.extract_area(leftB, topB, widthB, heightB);
-        } else {
-          // Use alpha only
-          return image.extract_area(leftA, topA, widthA, heightA);
-        }
-      }
-    }
     if (width == 0 || height == 0) {
       throw VError("Unexpected error while trimming. Try to lower the tolerance");
     }

By default, libvips flattens transparency against black, making it difficult to find the trim boundaries within the test/fixtures/trim-mc.png image.

$ vips flatten test/fixtures/trim-mc.png x-black.png

x-black

But doing the vips_flatten with a magenta background, ensures it can still find the boundaries.

$ vips flatten test/fixtures/trim-mc.png x-magenta.png --background "255 0 255"

x-magenta

@lovell
Copy link
Owner Author

@lovell lovell commented on e0d3c6e Jul 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the image has pixel values that are already magenta (or close to it) then I think this approach could produce unexpected results, especially with higher threshold values.

Please sign in to comment.