diff --git a/index.js b/index.js index 2bde9ce4..40dd8133 100644 --- a/index.js +++ b/index.js @@ -86,6 +86,7 @@ var Sharp = function(input, options) { sharpenJagged: 2, threshold: 0, thresholdGrayscale: true, + trimTolerance: 0, gamma: 0, greyscale: false, normalize: 0, @@ -364,13 +365,13 @@ Sharp.prototype.overlayWith = function(overlay, options) { setTileOption(options.tile, this.options); } if(isDefined(options.cutout)) { - setCutoutOption(options.cutout, this.options); + setCutoutOption(options.cutout, this.options); } if(isDefined(options.left) || isDefined(options.top)) { - setOffsetOption(options.top, options.left, this.options); + setOffsetOption(options.top, options.left, this.options); } if (isDefined(options.gravity)) { - setGravityOption(options.gravity, this.options); + setGravityOption(options.gravity, this.options); } } return this; @@ -384,7 +385,7 @@ function setTileOption(tile, options) { options.overlayTile = tile; } else { throw new Error('Invalid Value for tile ' + tile + ' Only Boolean Values allowed for overlay.tile.'); - } + } } function setCutoutOption(cutout, options) { @@ -553,14 +554,31 @@ Sharp.prototype.threshold = function(threshold, options) { } else { throw new Error('Invalid threshold (0 to 255) ' + threshold); } - + if(typeof options === 'undefined' || options.greyscale === true || options.grayscale === true) { this.options.thresholdGrayscale = true; } else { this.options.thresholdGrayscale = false; } - + + return this; +}; + +/* + Automatically remove "boring" image edges. + tolerance - if present, is a percentaged tolerance level between 0 and 100 to trim away similar color values + Defaulting to 10 when no tolerance is given. + */ +Sharp.prototype.trim = function(tolerance) { + if (typeof tolerance === 'undefined') { + this.options.trimTolerance = 10; + } else if (isInteger(tolerance) && inRange(tolerance, 1, 99)) { + this.options.trimTolerance = tolerance; + } else { + throw new Error('Invalid trim tolerance (1 to 99) ' + tolerance); + } + return this; }; diff --git a/src/common.h b/src/common.h index 6beeb7ee..b8d6afc1 100644 --- a/src/common.h +++ b/src/common.h @@ -115,6 +115,10 @@ namespace sharp { std::tuple CalculateCrop(int const inWidth, int const inHeight, int const outWidth, int const outHeight, int const x, int const y); + /* + Return the image alpha maximum. Useful for combining alpha bands. scRGB + images are 0 - 1 for image data, but the alpha is 0 - 255. + */ int MaximumImageAlpha(VipsInterpretation interpretation); diff --git a/src/operations.cc b/src/operations.cc index e85f5730..58adba1b 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -399,4 +399,56 @@ namespace sharp { return image.bandbool(boolean); } + VImage Trim(VImage image, int const tolerance) { + using sharp::MaximumImageAlpha; + // An equivalent of ImageMagick's -trim in C++ ... automatically remove + // "boring" image edges. + + // We use .project to sum the rows and columns of a 0/255 mask image, the first + // non-zero row or column is the object edge. We make the mask image with an + // amount-different-from-background image plus a threshold. + + // find the value of the pixel at (0, 0) ... we will search for all pixels + // significantly different from this + std::vector background = image(0, 0); + + int max = MaximumImageAlpha(image.interpretation()); + + // we need to smooth the image, subtract the background from every pixel, take + // the absolute value of the difference, then threshold + VImage mask = (image.median(3) - background).abs() > (max * tolerance / 100); + + // sum mask rows and columns, then search for the first non-zero sum in each + // direction + VImage rows; + VImage columns = mask.project(&rows); + + VImage profileLeftV; + VImage profileLeftH = columns.profile(&profileLeftV); + + VImage profileRightV; + VImage profileRightH = columns.fliphor().profile(&profileRightV); + + VImage profileTopV; + VImage profileTopH = rows.profile(&profileTopV); + + VImage profileBottomV; + VImage profileBottomH = rows.flipver().profile(&profileBottomV); + + int left = static_cast(floor(profileLeftV.min())); + int right = columns.width() - static_cast(floor(profileRightV.min())); + int top = static_cast(floor(profileTopH.min())); + int bottom = rows.height() - static_cast(floor(profileBottomH.min())); + + int width = right - left; + int height = bottom - top; + + if(width <= 0 || height <= 0) { + throw VError("Unexpected error while trimming. Try to lower the tolerance"); + } + + // and now crop the original image + return image.extract_area(left, top, width, height); + } + } // namespace sharp diff --git a/src/operations.h b/src/operations.h index 8d1f9122..41a8d0f9 100644 --- a/src/operations.h +++ b/src/operations.h @@ -87,6 +87,11 @@ namespace sharp { */ VImage Bandbool(VImage image, VipsOperationBoolean const boolean); + /* + Trim an image + */ + VImage Trim(VImage image, int const tolerance); + } // namespace sharp #endif // SRC_OPERATIONS_H_ diff --git a/src/pipeline.cc b/src/pipeline.cc index f643190c..a2721449 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -56,6 +56,7 @@ using sharp::EntropyCrop; using sharp::TileCache; using sharp::Threshold; using sharp::Bandbool; +using sharp::Trim; using sharp::ImageType; using sharp::ImageTypeId; @@ -207,6 +208,11 @@ class PipelineWorker : public AsyncWorker { RemoveExifOrientation(image); } + // Trim + if(baton->trimTolerance != 0) { + image = Trim(image, baton->trimTolerance); + } + // Pre extraction if (baton->topOffsetPre != -1) { image = image.extract_area(baton->leftOffsetPre, baton->topOffsetPre, baton->widthPre, baton->heightPre); @@ -1145,6 +1151,10 @@ NAN_METHOD(pipeline) { baton->sharpenJagged = attrAs(options, "sharpenJagged"); baton->threshold = attrAs(options, "threshold"); baton->thresholdGrayscale = attrAs(options, "thresholdGrayscale"); + baton->trimTolerance = attrAs(options, "trimTolerance"); + if(baton->accessMethod == VIPS_ACCESS_SEQUENTIAL && baton->trimTolerance != 0) { + baton->accessMethod = VIPS_ACCESS_RANDOM; + } baton->gamma = attrAs(options, "gamma"); baton->greyscale = attrAs(options, "greyscale"); baton->normalize = attrAs(options, "normalize"); diff --git a/src/pipeline.h b/src/pipeline.h index a1807837..906ee3fb 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -63,6 +63,7 @@ struct PipelineBaton { double sharpenJagged; int threshold; bool thresholdGrayscale; + int trimTolerance; double gamma; bool greyscale; bool normalize; @@ -127,6 +128,7 @@ struct PipelineBaton { sharpenJagged(2.0), threshold(0), thresholdGrayscale(true), + trimTolerance(0), gamma(0.0), greyscale(false), normalize(false), diff --git a/test/fixtures/expected/alpha-layer-1-fill-trim-resize.png b/test/fixtures/expected/alpha-layer-1-fill-trim-resize.png new file mode 100644 index 00000000..ad1cf1d5 Binary files /dev/null and b/test/fixtures/expected/alpha-layer-1-fill-trim-resize.png differ diff --git a/test/fixtures/expected/trim-16bit-rgba.png b/test/fixtures/expected/trim-16bit-rgba.png new file mode 100644 index 00000000..555f79d3 Binary files /dev/null and b/test/fixtures/expected/trim-16bit-rgba.png differ diff --git a/test/unit/trim.js b/test/unit/trim.js new file mode 100644 index 00000000..d73ccec0 --- /dev/null +++ b/test/unit/trim.js @@ -0,0 +1,39 @@ +'use strict'; + +var assert = require('assert'); + +var sharp = require('../../index'); +var fixtures = require('../fixtures'); + +describe('Trim borders', function() { + + it('Threshold default', function(done) { + var expected = fixtures.expected('alpha-layer-1-fill-trim-resize.png'); + sharp(fixtures.inputPngOverlayLayer1) + .resize(450, 322) + .trim() + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual('png', info.format); + assert.strictEqual(450, info.width); + assert.strictEqual(322, info.height); + fixtures.assertSimilar(expected, data, done); + }); + }); + + it('16-bit PNG with alpha channel', function(done) { + sharp(fixtures.inputPngWithTransparency16bit) + .resize(32, 32) + .trim() + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(32, info.width); + assert.strictEqual(32, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('trim-16bit-rgba.png'), data, done); + }); + }); + +});