From 21fbe546b8437f379972a4041ee2747c11f89c7c Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Tue, 2 Oct 2018 11:24:32 +0100 Subject: [PATCH] Switch from custom trim op to vips_find_trim #914 --- docs/changelog.md | 3 +++ lib/constructor.js | 2 +- lib/resize.js | 16 ++++++------- src/operations.cc | 60 ++++++++++------------------------------------ src/operations.h | 2 +- src/pipeline.cc | 10 ++++---- src/pipeline.h | 4 ++-- test/unit/trim.js | 2 +- 8 files changed, 34 insertions(+), 65 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index f24d687d..c2906e0c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -19,6 +19,9 @@ Requires libvips v8.7.0. Per-operation `background` options added to `resize`, `extend` and `flatten` operations. [#1392](https://github.com/lovell/sharp/issues/1392) +* Switch from custom trim operation to `vips_find_trim`. + [#914](https://github.com/lovell/sharp/issues/914) + * Drop Node 4 support. [#1212](https://github.com/lovell/sharp/issues/1212) diff --git a/lib/constructor.js b/lib/constructor.js index 179fb4e6..24a2eb69 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -134,7 +134,7 @@ const Sharp = function (input, options) { sharpenJagged: 2, threshold: 0, thresholdGrayscale: true, - trimTolerance: 0, + trimThreshold: 0, gamma: 0, greyscale: false, normalise: 0, diff --git a/lib/resize.js b/lib/resize.js index 13a77476..1cca849d 100644 --- a/lib/resize.js +++ b/lib/resize.js @@ -359,18 +359,18 @@ function extract (options) { } /** - * Trim "boring" pixels from all edges that contain values within a percentage similarity of the top-left pixel. - * @param {Number} [tolerance=10] value between 1 and 99 representing the percentage similarity. + * Trim "boring" pixels from all edges that contain values similar to the top-left pixel. + * @param {Number} [threshold=10] the allowed difference from the top-left pixel, a number greater than zero. * @returns {Sharp} * @throws {Error} Invalid parameters */ -function trim (tolerance) { - if (!is.defined(tolerance)) { - this.options.trimTolerance = 10; - } else if (is.integer(tolerance) && is.inRange(tolerance, 1, 99)) { - this.options.trimTolerance = tolerance; +function trim (threshold) { + if (!is.defined(threshold)) { + this.options.trimThreshold = 10; + } else if (is.number(threshold) && threshold > 0) { + this.options.trimThreshold = threshold; } else { - throw new Error('Invalid trim tolerance (1 to 99) ' + tolerance); + throw is.invalidParameterError('threshold', 'number greater than zero', threshold); } return this; } diff --git a/src/operations.cc b/src/operations.cc index 0a07be8a..97a35903 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -324,55 +324,21 @@ namespace sharp { return image.boolean(imageR, 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); - - double const 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) { + /* + Trim an image + */ + VImage Trim(VImage image, int const threshold) { + // Top-left pixel provides the background colour + VImage background = image.extract_area(0, 0, 1, 1); + if (HasAlpha(background)) { + background = background.flatten(); + } + int top, width, height; + int const left = image.find_trim(&top, &width, &height, + VImage::option()->set("background", background(0, 0))); + 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); } diff --git a/src/operations.h b/src/operations.h index c49ddc0b..5053af44 100644 --- a/src/operations.h +++ b/src/operations.h @@ -100,7 +100,7 @@ namespace sharp { /* Trim an image */ - VImage Trim(VImage image, int const tolerance); + VImage Trim(VImage image, int const threshold); /* * Linear adjustment (a * in + b) diff --git a/src/pipeline.cc b/src/pipeline.cc index d192e64e..6193c9ce 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -100,8 +100,8 @@ class PipelineWorker : public Nan::AsyncWorker { } // Trim - if (baton->trimTolerance != 0) { - image = sharp::Trim(image, baton->trimTolerance); + if (baton->trimThreshold > 0.0) { + image = sharp::Trim(image, baton->trimThreshold); } // Pre extraction @@ -233,7 +233,7 @@ class PipelineWorker : public Nan::AsyncWorker { if ( xshrink == yshrink && xshrink >= 2 * shrink_on_load_factor && (inputImageType == ImageType::JPEG || inputImageType == ImageType::WEBP) && - baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimTolerance == 0 + baton->gamma == 0 && baton->topOffsetPre == -1 && baton->trimThreshold == 0.0 ) { if (xshrink >= 8 * shrink_on_load_factor) { xfactor = xfactor / 8; @@ -1183,7 +1183,7 @@ NAN_METHOD(pipeline) { baton->sharpenJagged = AttrTo(options, "sharpenJagged"); baton->threshold = AttrTo(options, "threshold"); baton->thresholdGrayscale = AttrTo(options, "thresholdGrayscale"); - baton->trimTolerance = AttrTo(options, "trimTolerance"); + baton->trimThreshold = AttrTo(options, "trimThreshold"); baton->gamma = AttrTo(options, "gamma"); baton->linearA = AttrTo(options, "linearA"); baton->linearB = AttrTo(options, "linearB"); @@ -1293,7 +1293,7 @@ NAN_METHOD(pipeline) { } // Force random access for certain operations if (baton->accessMethod == VIPS_ACCESS_SEQUENTIAL && ( - baton->trimTolerance != 0 || baton->normalise || + baton->trimThreshold > 0.0 || baton->normalise || baton->position == 16 || baton->position == 17)) { baton->accessMethod = VIPS_ACCESS_RANDOM; } diff --git a/src/pipeline.h b/src/pipeline.h index 0e04c3ab..8d1fa80a 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -81,7 +81,7 @@ struct PipelineBaton { double sharpenJagged; int threshold; bool thresholdGrayscale; - int trimTolerance; + double trimThreshold; double linearA; double linearB; double gamma; @@ -176,7 +176,7 @@ struct PipelineBaton { sharpenJagged(2.0), threshold(0), thresholdGrayscale(true), - trimTolerance(0), + trimThreshold(0.0), linearA(1.0), linearB(0.0), gamma(0.0), diff --git a/test/unit/trim.js b/test/unit/trim.js index 3be3fb7c..1f0587a0 100644 --- a/test/unit/trim.js +++ b/test/unit/trim.js @@ -50,7 +50,7 @@ describe('Trim borders', function () { }); describe('Invalid thresholds', function () { - [-1, 100, 'fail', {}].forEach(function (threshold) { + [-1, 'fail', {}].forEach(function (threshold) { it(JSON.stringify(threshold), function () { assert.throws(function () { sharp().trim(threshold);