From d92ea31858c4ec323b29515ce15b2b81bb84b243 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Mon, 29 Feb 2016 14:30:29 +0000 Subject: [PATCH 1/8] overlayWith improvements: diff sizes/formats, gravity, buffer input --- docs/api.md | 19 +- docs/changelog.md | 6 + index.js | 46 ++- package.json | 2 +- src/common.cc | 41 +++ src/common.h | 9 + src/operations.cc | 49 ++-- src/operations.h | 6 +- src/pipeline.cc | 264 +++++------------- src/pipeline.h | 121 ++++++++ .../expected/overlay-gravity-center.jpg | Bin 0 -> 2063 bytes .../expected/overlay-gravity-centre.jpg | Bin 0 -> 2063 bytes .../expected/overlay-gravity-east.jpg | Bin 0 -> 2171 bytes .../expected/overlay-gravity-north.jpg | Bin 0 -> 2155 bytes .../expected/overlay-gravity-northeast.jpg | Bin 0 -> 2237 bytes .../expected/overlay-gravity-northwest.jpg | Bin 0 -> 2112 bytes .../expected/overlay-gravity-south.jpg | Bin 0 -> 2075 bytes .../expected/overlay-gravity-southeast.jpg | Bin 0 -> 2161 bytes .../expected/overlay-gravity-southwest.jpg | Bin 0 -> 2146 bytes .../expected/overlay-gravity-west.jpg | Bin 0 -> 2127 bytes test/unit/overlay.js | 86 +++++- 21 files changed, 407 insertions(+), 242 deletions(-) create mode 100644 test/fixtures/expected/overlay-gravity-center.jpg create mode 100644 test/fixtures/expected/overlay-gravity-centre.jpg create mode 100644 test/fixtures/expected/overlay-gravity-east.jpg create mode 100644 test/fixtures/expected/overlay-gravity-north.jpg create mode 100644 test/fixtures/expected/overlay-gravity-northeast.jpg create mode 100644 test/fixtures/expected/overlay-gravity-northwest.jpg create mode 100644 test/fixtures/expected/overlay-gravity-south.jpg create mode 100644 test/fixtures/expected/overlay-gravity-southeast.jpg create mode 100644 test/fixtures/expected/overlay-gravity-southwest.jpg create mode 100644 test/fixtures/expected/overlay-gravity-west.jpg diff --git a/docs/api.md b/docs/api.md index e2c9630d..963561fe 100644 --- a/docs/api.md +++ b/docs/api.md @@ -365,13 +365,18 @@ The output image will still be web-friendly sRGB and contain three (identical) c Enhance output image contrast by stretching its luminance to cover the full dynamic range. This typically reduces performance by 30%. -#### overlayWith(path) +#### overlayWith(image, [options]) -_Experimental_ +Overlay (composite) a image containing an alpha channel over the processed (resized, extracted etc.) image. -Alpha composite image at `path` over the processed (resized, extracted) image. The dimensions of the two images must match. +`image` is one of the following, and must be the same size or smaller than the processed image: -* `path` is a String containing the path to an image file with an alpha channel. +* Buffer containing PNG, WebP, GIF or SVG image data, or +* String containing the path to an image file, with most major transparency formats supported. + +`options`, if present, is an Object with the following optional attributes: + +* `gravity` is a String or an attribute of the `sharp.gravity` Object e.g. `sharp.gravity.north` at which to place the overlay, defaulting to `center`/`centre`. ```javascript sharp('input.png') @@ -379,7 +384,7 @@ sharp('input.png') .resize(300) .flatten() .background('#ff6600') - .overlayWith('overlay.png') + .overlayWith('overlay.png', { gravity: sharp.gravity.southeast } ) .sharpen() .withMetadata() .quality(90) @@ -387,8 +392,8 @@ sharp('input.png') .toBuffer() .then(function(outputBuffer) { // outputBuffer contains upside down, 300px wide, alpha channel flattened - // onto orange background, composited with overlay.png, sharpened, - // with metadata, 90% quality WebP image data. Phew! + // onto orange background, composited with overlay.png with SE gravity, + // sharpened, with metadata, 90% quality WebP image data. Phew! }); ``` diff --git a/docs/changelog.md b/docs/changelog.md index 46f93c63..fa521173 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # Changelog +### v0.14 - "*needle*" + +* Improvements to overlayWith: differing sizes/formats, gravity, buffer input. + [#239](https://github.com/lovell/sharp/issues/239) + [@chrisriley](https://github.com/chrisriley) + ### v0.13 - "*mind*" #### v0.13.1 - 27th February 2016 diff --git a/index.js b/index.js index 583b84a3..d959e1f3 100644 --- a/index.js +++ b/index.js @@ -84,7 +84,9 @@ var Sharp = function(input, options) { greyscale: false, normalize: 0, // overlay - overlayPath: '', + overlayFileIn: '', + overlayBufferIn: null, + overlayGravity: 0, // output options formatOut: 'input', fileOut: '', @@ -106,13 +108,13 @@ var Sharp = function(input, options) { module.exports.queue.emit('change', queueLength); } }; - if (typeof input === 'string') { + if (isString(input)) { // input=file this.options.fileIn = input; - } else if (typeof input === 'object' && input instanceof Buffer) { + } else if (isBuffer(input)) { // input=buffer this.options.bufferIn = input; - } else if (typeof input === 'undefined' || input === null) { + } else if (!isDefined(input)) { // input=stream this.options.streamIn = true; } else { @@ -148,6 +150,12 @@ var isDefined = function(val) { var isObject = function(val) { return typeof val === 'object'; }; +var isBuffer = function(val) { + return typeof val === 'object' && val instanceof Buffer; +}; +var isString = function(val) { + return typeof val === 'string' && val.length > 0; +}; var isInteger = function(val) { return typeof val === 'number' && !Number.isNaN(val) && val % 1 === 0; }; @@ -232,11 +240,11 @@ module.exports.gravity = { Sharp.prototype.crop = function(gravity) { this.options.canvas = 'crop'; - if (typeof gravity === 'undefined') { + if (!isDefined(gravity)) { this.options.gravity = module.exports.gravity.center; - } else if (typeof gravity === 'number' && !Number.isNaN(gravity) && gravity >= 0 && gravity <= 8) { + } else if (isInteger(gravity) && inRange(gravity, 0, 8)) { this.options.gravity = gravity; - } else if (typeof gravity === 'string' && typeof module.exports.gravity[gravity] === 'number') { + } else if (isString(gravity) && isInteger(module.exports.gravity[gravity])) { this.options.gravity = module.exports.gravity[gravity]; } else { throw new Error('Unsupported crop gravity ' + gravity); @@ -316,14 +324,26 @@ Sharp.prototype.negate = function(negate) { return this; }; -Sharp.prototype.overlayWith = function(overlayPath) { - if (typeof overlayPath !== 'string') { - throw new Error('The overlay path must be a string'); +/* + Overlay with another image, using an optional gravity +*/ +Sharp.prototype.overlayWith = function(overlay, options) { + if (isString(overlay)) { + this.options.overlayFileIn = overlay; + } else if (isBuffer(overlay)) { + this.options.overlayBufferIn = overlay; + } else { + throw new Error('Unsupported overlay ' + typeof overlay); } - if (overlayPath === '') { - throw new Error('The overlay path cannot be empty'); + if (isObject(options)) { + if (isInteger(options.gravity) && inRange(options.gravity, 0, 8)) { + this.options.overlayGravity = options.gravity; + } else if (isString(options.gravity) && isInteger(module.exports.gravity[options.gravity])) { + this.options.overlayGravity = module.exports.gravity[options.gravity]; + } else if (isDefined(options.gravity)) { + throw new Error('Unsupported overlay gravity ' + options.gravity); + } } - this.options.overlayPath = overlayPath; return this; }; diff --git a/package.json b/package.json index c9319cc6..de2aa324 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharp", - "version": "0.13.1", + "version": "0.14.0", "author": "Lovell Fuller ", "contributors": [ "Pierre Inglebert ", diff --git a/src/common.cc b/src/common.cc index 8fce1af4..92aaaefb 100644 --- a/src/common.cc +++ b/src/common.cc @@ -185,4 +185,45 @@ namespace sharp { } } + /* + Calculate the (left, top) coordinates of the output image + within the input image, applying the given gravity. + */ + std::tuple CalculateCrop(int const inWidth, int const inHeight, + int const outWidth, int const outHeight, int const gravity) { + + int left = 0; + int top = 0; + switch (gravity) { + case 1: // North + left = (inWidth - outWidth + 1) / 2; + break; + case 2: // East + left = inWidth - outWidth; + top = (inHeight - outHeight + 1) / 2; + break; + case 3: // South + left = (inWidth - outWidth + 1) / 2; + top = inHeight - outHeight; + break; + case 4: // West + top = (inHeight - outHeight + 1) / 2; + break; + case 5: // Northeast + left = inWidth - outWidth; + break; + case 6: // Southeast + left = inWidth - outWidth; + top = inHeight - outHeight; + case 7: // Southwest + top = inHeight - outHeight; + case 8: // Northwest + break; + default: // Centre + left = (inWidth - outWidth + 1) / 2; + top = (inHeight - outHeight + 1) / 2; + } + return std::make_tuple(left, top); + } + } // namespace sharp diff --git a/src/common.h b/src/common.h index 083e58c5..a1fcfdb4 100644 --- a/src/common.h +++ b/src/common.h @@ -2,6 +2,8 @@ #define SRC_COMMON_H_ #include +#include + #include using vips::VImage; @@ -80,6 +82,13 @@ namespace sharp { */ void FreeCallback(char* data, void* hint); + /* + Calculate the (left, top) coordinates of the output image + within the input image, applying the given gravity. + */ + std::tuple CalculateCrop(int const inWidth, int const inHeight, + int const outWidth, int const outHeight, int const gravity); + } // namespace sharp #endif // SRC_COMMON_H_ diff --git a/src/operations.cc b/src/operations.cc index 16fdec9e..7cbedbab 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -4,34 +4,49 @@ #include "operations.h" using vips::VImage; +using vips::VError; namespace sharp { /* - Alpha composite src over dst - Assumes alpha channels are already premultiplied and will be unpremultiplied after + Alpha composite src over dst with given gravity. + Assumes alpha channels are already premultiplied and will be unpremultiplied after. */ - VImage Composite(VImage src, VImage dst) { + VImage Composite(VImage src, VImage dst, const int gravity) { + using sharp::CalculateCrop; using sharp::HasAlpha; - // Split src into non-alpha and alpha + if (!HasAlpha(src)) { + throw VError("Overlay image must have an alpha channel"); + } + if (!HasAlpha(dst)) { + throw VError("Image to be overlaid must have an alpha channel"); + } + if (src.width() > dst.width() || src.height() > dst.height()) { + throw VError("Overlay image must have same dimensions or smaller"); + } + + // Enlarge overlay src, if required + if (src.width() < dst.width() || src.height() < dst.height()) { + // Calculate the (left, top) coordinates of the output image within the input image, applying the given gravity. + int left; + int top; + std::tie(left, top) = CalculateCrop(dst.width(), dst.height(), src.width(), src.height(), gravity); + // Embed onto transparent background + std::vector background { 0.0, 0.0, 0.0, 0.0 }; + src = src.embed(left, top, dst.width(), dst.height(), VImage::option() + ->set("extend", VIPS_EXTEND_BACKGROUND) + ->set("background", background) + ); + } + + // Split src into non-alpha and alpha channels VImage srcWithoutAlpha = src.extract_band(0, VImage::option()->set("n", src.bands() - 1)); VImage srcAlpha = src[src.bands() - 1] * (1.0 / 255.0); // Split dst into non-alpha and alpha channels - VImage dstWithoutAlpha; - VImage dstAlpha; - if (HasAlpha(dst)) { - // Non-alpha: extract all-but-last channel - dstWithoutAlpha = dst.extract_band(0, VImage::option()->set("n", dst.bands() - 1)); - // Alpha: Extract last channel - dstAlpha = dst[dst.bands() - 1] * (1.0 / 255.0); - } else { - // Non-alpha: Copy reference - dstWithoutAlpha = dst; - // Alpha: Use blank, opaque (0xFF) image - dstAlpha = VImage::black(dst.width(), dst.height()).invert(); - } + VImage dstWithoutAlpha = dst.extract_band(0, VImage::option()->set("n", dst.bands() - 1)); + VImage dstAlpha = dst[dst.bands() - 1] * (1.0 / 255.0); // // Compute normalized output alpha channel: diff --git a/src/operations.h b/src/operations.h index ba69b25d..7c9d8899 100644 --- a/src/operations.h +++ b/src/operations.h @@ -8,10 +8,10 @@ using vips::VImage; namespace sharp { /* - Composite images `src` and `dst` with premultiplied alpha channel and output - image with premultiplied alpha. + Alpha composite src over dst with given gravity. + Assumes alpha channels are already premultiplied and will be unpremultiplied after. */ - VImage Composite(VImage src, VImage dst); + VImage Composite(VImage src, VImage dst, const int gravity); /* * Stretch luminance to cover full dynamic range. diff --git a/src/pipeline.cc b/src/pipeline.cc index afb19299..561eff7d 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -1,10 +1,12 @@ -#include #include -#include #include +#include +#include + +#include + #include #include -#include #include "nan.h" @@ -62,133 +64,22 @@ using sharp::IsWebp; using sharp::IsTiff; using sharp::IsDz; using sharp::FreeCallback; +using sharp::CalculateCrop; using sharp::counterProcess; using sharp::counterQueue; -enum class Canvas { - CROP, - EMBED, - MAX, - MIN, - IGNORE_ASPECT -}; - -struct PipelineBaton { - std::string fileIn; - char *bufferIn; - size_t bufferInLength; - std::string iccProfilePath; - int limitInputPixels; - std::string density; - int rawWidth; - int rawHeight; - int rawChannels; - std::string formatOut; - std::string fileOut; - void *bufferOut; - size_t bufferOutLength; - int topOffsetPre; - int leftOffsetPre; - int widthPre; - int heightPre; - int topOffsetPost; - int leftOffsetPost; - int widthPost; - int heightPost; - int width; - int height; - int channels; - Canvas canvas; - int gravity; - std::string interpolator; - double background[4]; - bool flatten; - bool negate; - double blurSigma; - int sharpenRadius; - double sharpenFlat; - double sharpenJagged; - int threshold; - std::string overlayPath; - double gamma; - bool greyscale; - bool normalize; - int angle; - bool rotateBeforePreExtract; - bool flip; - bool flop; - bool progressive; - bool withoutEnlargement; - VipsAccess accessMethod; - int quality; - int compressionLevel; - bool withoutAdaptiveFiltering; - bool withoutChromaSubsampling; - bool trellisQuantisation; - bool overshootDeringing; - bool optimiseScans; - std::string err; - bool withMetadata; - int withMetadataOrientation; - int tileSize; - int tileOverlap; - - PipelineBaton(): - bufferInLength(0), - limitInputPixels(0), - density(""), - rawWidth(0), - rawHeight(0), - rawChannels(0), - formatOut(""), - fileOut(""), - bufferOutLength(0), - topOffsetPre(-1), - topOffsetPost(-1), - channels(0), - canvas(Canvas::CROP), - gravity(0), - flatten(false), - negate(false), - blurSigma(0.0), - sharpenRadius(0), - sharpenFlat(1.0), - sharpenJagged(2.0), - threshold(0), - gamma(0.0), - greyscale(false), - normalize(false), - angle(0), - flip(false), - flop(false), - progressive(false), - withoutEnlargement(false), - quality(80), - compressionLevel(6), - withoutAdaptiveFiltering(false), - withoutChromaSubsampling(false), - trellisQuantisation(false), - overshootDeringing(false), - optimiseScans(false), - withMetadata(false), - withMetadataOrientation(-1), - tileSize(256), - tileOverlap(0) { - background[0] = 0.0; - background[1] = 0.0; - background[2] = 0.0; - background[3] = 255.0; - } -}; - class PipelineWorker : public AsyncWorker { public: - PipelineWorker(Callback *callback, PipelineBaton *baton, Callback *queueListener, const Local &bufferIn) : + PipelineWorker(Callback *callback, PipelineBaton *baton, Callback *queueListener, + const Local &bufferIn, const Local &overlayBufferIn) : AsyncWorker(callback), baton(baton), queueListener(queueListener) { if (baton->bufferInLength > 0) { SaveToPersistent("bufferIn", bufferIn); } + if (baton->overlayBufferInLength > 0) { + SaveToPersistent("overlayBufferIn", overlayBufferIn); + } } ~PipelineWorker() {} @@ -508,11 +399,19 @@ class PipelineWorker : public AsyncWorker { } } + // Ensure image has an alpha channel when there is an overlay + bool hasOverlay = baton->overlayBufferInLength > 0 || !baton->overlayFileIn.empty(); + if (hasOverlay && !HasAlpha(image)) { + double multiplier = (image.interpretation() == VIPS_INTERPRETATION_RGB16) ? 256.0 : 1.0; + image = image.bandjoin( + VImage::new_matrix(image.width(), image.height()).new_from_image(255 * multiplier) + ); + } + bool shouldAffineTransform = xresidual != 0.0 || yresidual != 0.0; bool shouldBlur = baton->blurSigma != 0.0; bool shouldSharpen = baton->sharpenRadius != 0; bool shouldThreshold = baton->threshold != 0; - bool hasOverlay = !baton->overlayPath.empty(); bool shouldPremultiplyAlpha = HasAlpha(image) && (shouldAffineTransform || shouldBlur || shouldSharpen || hasOverlay); @@ -634,38 +533,41 @@ class PipelineWorker : public AsyncWorker { // Composite with overlay, if present if (hasOverlay) { VImage overlayImage; - ImageType overlayImageType = DetermineImageType(baton->overlayPath.data()); - if (overlayImageType != ImageType::UNKNOWN) { - overlayImage = VImage::new_from_file( - baton->overlayPath.data(), - VImage::option()->set("access", baton->accessMethod) - ); + ImageType overlayImageType = ImageType::UNKNOWN; + if (baton->overlayBufferInLength > 0) { + // Overlay with image from buffer + overlayImageType = DetermineImageType(baton->overlayBufferIn, baton->overlayBufferInLength); + if (overlayImageType != ImageType::UNKNOWN) { + try { + overlayImage = VImage::new_from_buffer(baton->overlayBufferIn, baton->overlayBufferInLength, + nullptr, VImage::option()->set("access", baton->accessMethod)); + } catch (...) { + (baton->err).append("Overlay buffer has corrupt header"); + overlayImageType = ImageType::UNKNOWN; + } + } else { + (baton->err).append("Overlay buffer contains unsupported image format"); + } } else { - (baton->err).append("Overlay image is of an unsupported image format"); + // Overlay with image from file + overlayImageType = DetermineImageType(baton->overlayFileIn.data()); + if (overlayImageType != ImageType::UNKNOWN) { + try { + overlayImage = VImage::new_from_file(baton->overlayFileIn.data(), + VImage::option()->set("access", baton->accessMethod)); + } catch (...) { + (baton->err).append("Overlay file has corrupt header"); + overlayImageType = ImageType::UNKNOWN; + } + } + } + if (overlayImageType == ImageType::UNKNOWN) { return Error(); } - if (image.format() != VIPS_FORMAT_UCHAR && image.format() != VIPS_FORMAT_FLOAT) { - (baton->err).append("Expected image band format to be uchar or float: "); - (baton->err).append(vips_enum_nick(VIPS_TYPE_BAND_FORMAT, image.format())); - return Error(); - } - if (overlayImage.format() != VIPS_FORMAT_UCHAR && overlayImage.format() != VIPS_FORMAT_FLOAT) { - (baton->err).append("Expected overlay image band format to be uchar or float: "); - (baton->err).append(vips_enum_nick(VIPS_TYPE_BAND_FORMAT, overlayImage.format())); - return Error(); - } - if (!HasAlpha(overlayImage)) { - (baton->err).append("Overlay image must have an alpha channel"); - return Error(); - } - if (overlayImage.width() != image.width() && overlayImage.height() != image.height()) { - (baton->err).append("Overlay image must have same dimensions as resized image"); - return Error(); - } - // Ensure overlay is sRGB and premutiplied + // Ensure overlay is premultiplied sRGB overlayImage = overlayImage.colourspace(VIPS_INTERPRETATION_sRGB).premultiply(); - - image = Composite(overlayImage, image); + // Composite images with given gravity + image = Composite(overlayImage, image, baton->overlayGravity); } // Reverse premultiplication after all transformations: @@ -708,6 +610,9 @@ class PipelineWorker : public AsyncWorker { SetExifOrientation(image, baton->withMetadataOrientation); } + // Number of channels used in output image + baton->channels = image.bands(); + // Output if (baton->fileOut == "") { // Buffer output @@ -728,6 +633,7 @@ class PipelineWorker : public AsyncWorker { area->free_fn = nullptr; vips_area_unref(area); baton->formatOut = "jpeg"; + baton->channels = std::min(baton->channels, 3); } else if (baton->formatOut == "png" || (baton->formatOut == "input" && inputImageType == ImageType::PNG)) { // Write PNG to buffer VipsArea *area = VIPS_AREA(image.pngsave_buffer(VImage::option() @@ -800,6 +706,7 @@ class PipelineWorker : public AsyncWorker { ->set("interlace", baton->progressive) ); baton->formatOut = "jpeg"; + baton->channels = std::min(baton->channels, 3); } else if (baton->formatOut == "png" || isPng || (matchInput && inputImageType == ImageType::PNG)) { // Write PNG to file image.pngsave(const_cast(baton->fileOut.data()), VImage::option() @@ -824,6 +731,7 @@ class PipelineWorker : public AsyncWorker { ->set("compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG) ); baton->formatOut = "tiff"; + baton->channels = std::min(baton->channels, 3); } else if (baton->formatOut == "dz" || IsDz(baton->fileOut)) { // Write DZ to file image.dzsave(const_cast(baton->fileOut.data()), VImage::option() @@ -838,8 +746,6 @@ class PipelineWorker : public AsyncWorker { return Error(); } } - // Number of channels used in output image - baton->channels = image.bands(); } catch (VError const &err) { (baton->err).append(err.what()); } @@ -890,10 +796,13 @@ class PipelineWorker : public AsyncWorker { } } - // Dispose of Persistent wrapper around input Buffer so it can be garbage collected + // Dispose of Persistent wrapper around input Buffers so they can be garbage collected if (baton->bufferInLength > 0) { GetFromPersistent("bufferIn"); } + if (baton->overlayBufferInLength > 0) { + GetFromPersistent("overlayBufferIn"); + } delete baton; // Decrement processing task counter @@ -944,46 +853,6 @@ class PipelineWorker : public AsyncWorker { return std::make_tuple(rotate, flip, flop); } - /* - Calculate the (left, top) coordinates of the output image - within the input image, applying the given gravity. - */ - std::tuple - CalculateCrop(int const inWidth, int const inHeight, int const outWidth, int const outHeight, int const gravity) { - int left = 0; - int top = 0; - switch (gravity) { - case 1: // North - left = (inWidth - outWidth + 1) / 2; - break; - case 2: // East - left = inWidth - outWidth; - top = (inHeight - outHeight + 1) / 2; - break; - case 3: // South - left = (inWidth - outWidth + 1) / 2; - top = inHeight - outHeight; - break; - case 4: // West - top = (inHeight - outHeight + 1) / 2; - break; - case 5: // Northeast - left = inWidth - outWidth; - break; - case 6: // Southeast - left = inWidth - outWidth; - top = inHeight - outHeight; - case 7: // Southwest - top = inHeight - outHeight; - case 8: // Northwest - break; - default: // Centre - left = (inWidth - outWidth + 1) / 2; - top = (inHeight - outHeight + 1) / 2; - } - return std::make_tuple(left, top); - } - /* Calculate integral shrink given factor and interpolator window size */ @@ -1088,7 +957,14 @@ NAN_METHOD(pipeline) { baton->background[i] = To(Get(background, i).ToLocalChecked()).FromJust(); } // Overlay options - baton->overlayPath = attrAsStr(options, "overlayPath"); + baton->overlayFileIn = attrAsStr(options, "overlayFileIn"); + Local overlayBufferIn; + if (node::Buffer::HasInstance(Get(options, New("overlayBufferIn").ToLocalChecked()).ToLocalChecked())) { + overlayBufferIn = Get(options, New("overlayBufferIn").ToLocalChecked()).ToLocalChecked().As(); + baton->overlayBufferInLength = node::Buffer::Length(overlayBufferIn); + baton->overlayBufferIn = node::Buffer::Data(overlayBufferIn); + } + baton->overlayGravity = attrAs(options, "overlayGravity"); // Resize options baton->withoutEnlargement = attrAs(options, "withoutEnlargement"); baton->gravity = attrAs(options, "gravity"); @@ -1131,7 +1007,7 @@ NAN_METHOD(pipeline) { // Join queue for worker thread Callback *callback = new Callback(info[1].As()); - AsyncQueueWorker(new PipelineWorker(callback, baton, queueListener, bufferIn)); + AsyncQueueWorker(new PipelineWorker(callback, baton, queueListener, bufferIn, overlayBufferIn)); // Increment queued task counter g_atomic_int_inc(&counterQueue); diff --git a/src/pipeline.h b/src/pipeline.h index 411ae2c6..19c33fab 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -5,4 +5,125 @@ NAN_METHOD(pipeline); +enum class Canvas { + CROP, + EMBED, + MAX, + MIN, + IGNORE_ASPECT +}; + +struct PipelineBaton { + std::string fileIn; + char *bufferIn; + size_t bufferInLength; + std::string iccProfilePath; + int limitInputPixels; + std::string density; + int rawWidth; + int rawHeight; + int rawChannels; + std::string formatOut; + std::string fileOut; + void *bufferOut; + size_t bufferOutLength; + std::string overlayFileIn; + char *overlayBufferIn; + size_t overlayBufferInLength; + int overlayGravity; + int topOffsetPre; + int leftOffsetPre; + int widthPre; + int heightPre; + int topOffsetPost; + int leftOffsetPost; + int widthPost; + int heightPost; + int width; + int height; + int channels; + Canvas canvas; + int gravity; + std::string interpolator; + double background[4]; + bool flatten; + bool negate; + double blurSigma; + int sharpenRadius; + double sharpenFlat; + double sharpenJagged; + int threshold; + double gamma; + bool greyscale; + bool normalize; + int angle; + bool rotateBeforePreExtract; + bool flip; + bool flop; + bool progressive; + bool withoutEnlargement; + VipsAccess accessMethod; + int quality; + int compressionLevel; + bool withoutAdaptiveFiltering; + bool withoutChromaSubsampling; + bool trellisQuantisation; + bool overshootDeringing; + bool optimiseScans; + std::string err; + bool withMetadata; + int withMetadataOrientation; + int tileSize; + int tileOverlap; + + PipelineBaton(): + bufferInLength(0), + limitInputPixels(0), + density(""), + rawWidth(0), + rawHeight(0), + rawChannels(0), + formatOut(""), + fileOut(""), + bufferOutLength(0), + overlayBufferInLength(0), + overlayGravity(0), + topOffsetPre(-1), + topOffsetPost(-1), + channels(0), + canvas(Canvas::CROP), + gravity(0), + flatten(false), + negate(false), + blurSigma(0.0), + sharpenRadius(0), + sharpenFlat(1.0), + sharpenJagged(2.0), + threshold(0), + gamma(0.0), + greyscale(false), + normalize(false), + angle(0), + flip(false), + flop(false), + progressive(false), + withoutEnlargement(false), + quality(80), + compressionLevel(6), + withoutAdaptiveFiltering(false), + withoutChromaSubsampling(false), + trellisQuantisation(false), + overshootDeringing(false), + optimiseScans(false), + withMetadata(false), + withMetadataOrientation(-1), + tileSize(256), + tileOverlap(0) { + background[0] = 0.0; + background[1] = 0.0; + background[2] = 0.0; + background[3] = 255.0; + } +}; + #endif // SRC_PIPELINE_H_ diff --git a/test/fixtures/expected/overlay-gravity-center.jpg b/test/fixtures/expected/overlay-gravity-center.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8b65ebcf754b07756d9f4f2a8aa7570292d6fab9 GIT binary patch literal 2063 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/expected/overlay-gravity-north.jpg b/test/fixtures/expected/overlay-gravity-north.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b61fc8375239abd59a30f44548e6317359b16307 GIT binary patch literal 2155 zcmb7_`#;l*AICql8{?GO(rlCaHAHc8J6LjA zOcWuBK!}MU(1#97NFGIFFh|kS(pXs)IV?^IFD)&vC9kBarlF}JBd4vat*)!0uA%-D z0)oTg2vGzIfk3HarLpS&xAr>#q$ofDoWLMOKm-W_BSHJG06YMQfI$ZW{t*-gK;U2z zkm!MHBngOsz+e$D6bgrce{O(8zz`@BkWzr@p$Ig^lc${Na5VD~_lUHT{u!T0(&dyF zoeLiiqB0=hC-)!f!8{0x06%YPAOR2<0)l|R(Er(>gAx#=f}RwV;Ov7!E7DHV519Lt zfCTu!LV}TiIpAG4t(_y_gl#jc=f+S?a~-mpF)_P1yRkku(~@sNoTz)q*SuiCfa{{;kuVoZ9fH0o(b6 z>d>l*F;$y{_Ya%3|Y<{9W*NnZ2 zQ%MZGXBAr6f^lnFdDw*$X~yL^%oB`yLSuK6%N{><9k)C})uuKEMCEC8u&t)8$6Nv~ zV3s4-9ObBT8!l%a1#566(#0X-Yb;iI_HNUM^WIuqQkP0lSmm!7@!O;5`x)$OjDnj< zaJEC%DF9baUgC-HsDWnYjQ!6 zn6cl=)OEoa=}`O=m%e1WWSeWJ{Bg8*&p=1g&ZXLq=pMU1n0Myd zI^Rf+3axXm$@UChc&fbE=m$fI>-y7rjyH(t&1F2uiq@L{WB zajbR5ijcFBxhC9k@x5!Yf@qysDlaBW>4o|gwqI|&%yE}TMDe(P?Yw_B7BOg*hS`|P z6q8@MF|ODZ;nQOcp|ebbJyrTo+bm-j{7M$K#Nhpm=*Scw3CSj}$ZlF=9Q^&CaV80R z1958B8vdQ#lFPTe_C|ByL}OZGuU@*@nj@_g=2dr@Nh@lka|im1F~- zR6g9XqPaaXDM(bQ%k8?!#2T;6az8ro1?)1 zKhl-E6U2A2qOD>v!B0>mD{M0}KhJ>(#^BO0Lc2t=uUWQgL0fa%8v+qWc1qlsdH7Cd zQ&Abo%rH0GRk_TY){(x_uN|g^9PU%_RgHM}>Shp7WPKXN&lC!M*=%;TnW>(b^eK4= zo!7h(ogJ=0PRJ|?P<&Tw6Jg3N=g-a(gJnDDGjhDqLe@)V9Ybhz)86nFemSXO<%fnw z89fSfoMH3YRkM{cr5o^y%QX#?&lvUax3s9wlta>Zq<3%&DjxPED?iPa-?Ue-3!2*; zb*))j6m4pw+HY_GLlNC1s>I#LRcvm%apKFtbk^Vn$4OMbB%{1gMhvpREh4NL3^G6T zznQ=(tz{faEmcT)p`cW6>;C)?qp4}r`4x#KFP;QE+t*#1YEjP%1r1U(Qm2vmp}fzLg(gqOUu}~hM~VU zrK;UdM8a~k8^nIGUM?>alKDulZW$bHcGn&R1+*gxf~14CM5)3yW~0aKd;c*M3;iY;%H|Sajo>S>pC3>N|f} z3=v*5>yUb8aj3p3x6A8%RK`rUAZfO|oV6y!-$s+BKHg>z+nVVoEyft=t10PGD!v^& zG6sjdw-0<22u5!DpNu}r%XooRAnpUqA29|76+tIpL%fPlmMa|Lv_LQnOA-Haaev}( DLCV4) literal 0 HcmV?d00001 diff --git a/test/fixtures/expected/overlay-gravity-northeast.jpg b/test/fixtures/expected/overlay-gravity-northeast.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9523792beb8cb2a128bce8d94badd519ee7e3419 GIT binary patch literal 2237 zcmb8pc{J1u8wc>;?2L7?CSx0#v0h6mB>NIshM_E3LliZG%7n69HEt%OD3dVPk{MBC zzh$XRv=GKRN-j0oLzV{3bPuj_bTh^>*Dk^aGJP(5fB1_ zKnfwHkVq+fI9@F27pl@2nr;41?&R=Fa!hw0sjpQ4nPqQu+VO4 zA_;&&5C|9ogG1qveUl+Q|03i5or-|BSp{}Enw)+voKk`k8ZSJgRN3Ewy}O+|ExNpBf9 z)t3tZ(T+QwoUV}If(venR;YJXzc+WnIs37>Sav2zt-@2pYMqL;Oet2x!udH(SQ zkfQ1QoX%rW)lDza$BozP=+-{PV5S^#pBE}$9@Mq|fHE}~Uc%6m766(18O(B9AC1f5 zD&>ALAFE|3(-CJdh_*!iwdhaucAt>4Os5ZA_h_Y!CDun7Vs-P_;$UuP@LjTpd=H4` z%8M>uIan8&V-^GHbv;-=ZPXOoxYe^gr zb6Rrew=r33ayH&SzMeftWgoe6Ve735fBpP~&t3PA-yXxlAD$!Fa@DPcUeB6E1Rnb# zMsK-a!YFNV!e)ICx~2P+I=ZGPzO%O4JBD>OZmTS;>}Zy){Bf|zn6G?(en)2SXZ8YYs!DpJ2))SIr3rgduteF zO1V&Y=fLX$qIq?<+gTRDw=>U)!eK6BXzPzNcn#LoIZ%Ib2PNX|g#`2w7~rc5XjP8_*a zl+0fqFn93`WzqsP3NwpCqAtz9l<(RW07(Z2xGf$Fwz+t}m?sC`<_cYj9Y0OW3a=67 zMxCknt?EV6q3z`p1HFx%WbzG(RPR6a-#C8WvQ6QmH3C}vnDer)eqlsw`<}g`=~|l^ z8E)u^yMiGU$`W>i9~^Ugn+Y zY=e|08a3`b%Qs?7oqIRnX@0hcJ%^)wr7_&ZKGBf^JssNH+e$fEy&>{Debw2|85$QW zbh5j^gs3tGU};&emZwdAj?6|yot3yHYHfG33#1aWX!byz`-U)~jvKwvd=51(p%WQ8 z^o+qe1V-FTUUI#dyjPXju3gsn=~gxmv-8kx=9r6)$h{7K(otWgSO3sx+Chyh%2XsY zd5rFllaI9N*CExA(u(z{kQ2vTY)eDANj*ux5`Ts@yiSDr1yq+`@&Xh=@)oTbc0+@l zkqPwCNgms6WAOHR`l>C4`Ox0l>hQUU>?>lioJ&iFkuI8>*RR`nNOlBe(?&~mCTbuj z=JQzhjU_$?Q}$cVkAAFMNo0$s_3soL;WSfL)CDb5X6J1i9YH41k

krtXWq(;iifYC9oq>#{Cba(B zB<961pU|VX(zsp$Q{Jv3=uwPH27gBD!n5G~ghM#j8-uyXx)rD94O)UyiEUVg2%POg z6;GIat{;ac{~`Sz1zAGp4v+G9X}k%q&QIR%5rpi7h3VN^tc;XtbE4Oe;ln$l<_%}3GYP{O62>yj3`S%*=9FzL*_ja|W2fw6ETKf%j#G`XWNejvEu+m| z5=SW{G%8zkQ?^{>ls&iZ>z+U0zJGl_&-?l8zuTVxOaV9)28Ti6a2OncfD0f+kw`&7 zBwAQRNK_InB_)Z*V5DW0vC@Z?WH1pR0sRE{2N)cHA|POpz|WK{3Vfugj;6ii@>KH^8I5$L4V{c%7D0{ZzW1O*rY zzL|RCqdCX_wlO6=N+S`;%EpAsRSUrDfhRHMJ+>4|hjZ0l{w$y?qZx7CidY zBA6}fn|Th@#;VBgZ-G1tT>Dbgda`Z59A;_iTYdx2F1cp%*klkf=MvhYM*&HoQO?Qm z%+}V)mFkit{5kOi6`_IyfMK)e#D^7EYo{h3RVtGB-9(D+*}8I@xmFLe3RbhXLyjjk z#(TMWCB?M5>wFqKSwoehlms71WuX@%-Ko|NO2I|pxadj51xejB=a%hJowMs7z1UAj zAC`vHHPcGN4SJX3L(JYJDBMX*X-(9<@uJ|*fiyo=rKBsv>Mj27Np?JM1WixhNX8|g zuDoC)AbYoxi*q?$w~-q-@eN*ZE6e|~#&fC~?cO_3l4m8&jU!BiR}q2IkTA=_fk*T|955hRZ_YjKxK-FJ`0!&Cc5m*!X+4|6YBpY_^u! z3;p3-wpZ+`?q_S>986>twijx&H+#5Uu*mJGd-pg_Uhi5lbYD3#J+?r$MU%G z%Uv5z_tAXnpiSK3XG_pvN=`AwMLaC#K+jT+!ZV*sU5|4{d3b0i#Q30`^8-%dx{Po~$oi{sl|1K8I$1+{?cnlGR_>05A1=`h zn_cgZe7_GUlBf6y1}T^)ue{dx0q5cCQLj(hka(8i%x_8Ekt}WcvZhRN#a(~l*6L$H z*Fr|xFAH$E%~ckYE9Ua)`jElAS4nSXTnh11wW>=2OdVWaR(ZYu&5p&Qd+++3__ z?nc9k&FM|oh!ZiBm#pCV%S`3l$>|D6Xj1WJ?V5Vc(aGeyJ=E1LXv)+Hok&Pm-XRxy z8|g-vgZ%Eioc**=RIybB%A=bhKvodk2Xy*VYL7*o8LLbYpavdSh3i%_Uk?z*xHmj9F z2C@lgEp}$_lHI-KaoVz=SW)wotJ8}ZAk?MgZR`K<{&HS4DmZ$FL zgs{+RQy(iFE5X-zH$&O8vAO0h1GQtG@9{^=WrZ;Tx7;dxog>-cU?8Typ{}KQz{c#D z4ePL_X;6Vn4bV%Y)hFiqIqF96HBV)*&MVnf`8-uUYHtORI(%8`bH;8}b)uqO+u0>@ zA=F}28c{WIf1<)r$t?M()^w+vvhTx54NS!(CiS?KEyWdCh=z@0^9T;R;f>=T(%$gYL*Uc*jqI! zy9~!Zzcl$^7U4Bm#JdRxX z&}3G9K-PKWIzM(ryRkZV*Yqz_GX)cMearb8B=>K5=N;u+c10rP_ebQ6-MBX=9Pq7O zrE7^p=*nca&^|ywNbr~53hOmrI!Pw?8f^1r$wE5qE#lk^*xaE|NngS)1!ILREu&xM zf9D?T4~_VcPw_{*X8$&s$>2dQGHhlfT8n>tA%H%sWv-y}uVoX3@vYO9CWH<_umf8t*yn^fw`o+q5~Pv*_(8aC$A5*jH%Ib@)?FUT?@|ih&+2F=Sy}L z<#9Au-&1qo(e+%sKtEB?HdfX?p%S0{nQ-+!*!5jys4ICc?VR^e^rE_jlbML+Jhx_a z?qkX{k1iQ|GRwEHIM ztE{it;F_>goqJ6*qn00iTtdnj+`L09Hja JI&*P<>_0wzj9~x( literal 0 HcmV?d00001 diff --git a/test/fixtures/expected/overlay-gravity-south.jpg b/test/fixtures/expected/overlay-gravity-south.jpg new file mode 100644 index 0000000000000000000000000000000000000000..96cd451d87ab705f4aad85b3a8f45afcbef21a9f GIT binary patch literal 2075 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 aG1chyLSCzQ7rAGY%k6`Mg|-X?pZ^2sQp6wt literal 0 HcmV?d00001 diff --git a/test/unit/overlay.js b/test/unit/overlay.js index b0a94896..36a6f2a2 100644 --- a/test/unit/overlay.js +++ b/test/unit/overlay.js @@ -1,5 +1,6 @@ 'use strict'; +var fs = require('fs'); var assert = require('assert'); var fixtures = require('../fixtures'); var sharp = require('../../index'); @@ -17,7 +18,7 @@ var getPaths = function(baseName, extension) { // Test describe('Overlays', function() { - it('Overlay transparent PNG on solid background', function(done) { + it('Overlay transparent PNG file on solid background', function(done) { var paths = getPaths('alpha-layer-01'); sharp(fixtures.inputPngOverlayLayer0) @@ -29,6 +30,18 @@ describe('Overlays', function() { }); }); + it('Overlay transparent PNG Buffer on solid background', function(done) { + var paths = getPaths('alpha-layer-01'); + + sharp(fixtures.inputPngOverlayLayer0) + .overlayWith(fs.readFileSync(fixtures.inputPngOverlayLayer1)) + .toFile(paths.actual, function (error) { + if (error) return done(error); + fixtures.assertMaxColourDistance(paths.actual, paths.expected); + done(); + }); + }); + it('Overlay low-alpha transparent PNG on solid background', function(done) { var paths = getPaths('alpha-layer-01-low-alpha'); @@ -141,18 +154,19 @@ describe('Overlays', function() { }); } - it('Fail when compositing images with different dimensions', function(done) { - sharp(fixtures.inputJpg) - .overlayWith(fixtures.inputPngWithGreyAlpha) + it('Fail when overlay does not contain alpha channel', function(done) { + sharp(fixtures.inputPngOverlayLayer1) + .overlayWith(fixtures.inputJpg) .toBuffer(function(error) { assert.strictEqual(true, error instanceof Error); done(); }); }); - it('Fail when compositing non-PNG image', function(done) { - sharp(fixtures.inputPngOverlayLayer1) - .overlayWith(fixtures.inputJpg) + it('Fail when overlay is larger', function(done) { + sharp(fixtures.inputJpg) + .resize(320) + .overlayWith(fixtures.inputPngOverlayLayer1) .toBuffer(function(error) { assert.strictEqual(true, error instanceof Error); done(); @@ -170,4 +184,62 @@ describe('Overlays', function() { sharp().overlayWith(1); }); }); + + it('Fail with unsupported gravity', function() { + assert.throws(function() { + sharp() + .overlayWith(fixtures.inputPngOverlayLayer1, { + gravity: 9 + }); + }); + }); + + it('Empty options', function() { + assert.doesNotThrow(function() { + sharp().overlayWith(fixtures.inputPngOverlayLayer1, {}); + }); + }); + + describe('Overlay with numeric gravity', function() { + Object.keys(sharp.gravity).forEach(function(gravity) { + it(gravity, function(done) { + var expected = fixtures.expected('overlay-gravity-' + gravity + '.jpg'); + sharp(fixtures.inputJpg) + .resize(80) + .overlayWith(fixtures.inputPngWithTransparency16bit, { + gravity: gravity + }) + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual('jpeg', info.format); + assert.strictEqual(80, info.width); + assert.strictEqual(65, info.height); + assert.strictEqual(3, info.channels); + fixtures.assertSimilar(expected, data, done); + }); + }); + }); + }); + + describe('Overlay with string-based gravity', function() { + Object.keys(sharp.gravity).forEach(function(gravity) { + it(gravity, function(done) { + var expected = fixtures.expected('overlay-gravity-' + gravity + '.jpg'); + sharp(fixtures.inputJpg) + .resize(80) + .overlayWith(fixtures.inputPngWithTransparency16bit, { + gravity: sharp.gravity[gravity] + }) + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual('jpeg', info.format); + assert.strictEqual(80, info.width); + assert.strictEqual(65, info.height); + assert.strictEqual(3, info.channels); + fixtures.assertSimilar(expected, data, done); + }); + }); + }); + }); + }); From bb37dc1ea689af4b76ccf9c6d00031584f8b2477 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Tue, 1 Mar 2016 19:27:47 +0000 Subject: [PATCH 2/8] Expose density metadata; set density of images from vector input --- docs/api.md | 1 + docs/changelog.md | 4 ++++ index.js | 4 ++-- src/common.cc | 24 ++++++++++++++++++++++++ src/common.h | 15 +++++++++++++++ src/metadata.cc | 10 ++++++++++ src/operations.cc | 1 + src/operations.h | 1 + src/pipeline.cc | 13 ++++++++++--- src/pipeline.h | 4 ++-- test/unit/io.js | 18 ++++++++++++++++-- test/unit/metadata.js | 12 ++++++++++++ 12 files changed, 98 insertions(+), 9 deletions(-) diff --git a/docs/api.md b/docs/api.md index 963561fe..a69ae6c4 100644 --- a/docs/api.md +++ b/docs/api.md @@ -48,6 +48,7 @@ Fast access to image metadata without decoding any compressed image data. * `height`: Number of pixels high * `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `scrgb`, `cmyk`, `lab`, `xyz`, `b-w` [...](https://github.com/jcupitt/libvips/blob/master/libvips/iofuncs/enumtypes.c#L522) * `channels`: Number of bands e.g. `3` for sRGB, `4` for CMYK +* `density`: Number of pixels per inch (DPI), if present * `hasProfile`: Boolean indicating the presence of an embedded ICC profile * `hasAlpha`: Boolean indicating the presence of an alpha transparency channel * `orientation`: Number value of the EXIF Orientation header, if present diff --git a/docs/changelog.md b/docs/changelog.md index fa521173..8393616c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,10 @@ [#239](https://github.com/lovell/sharp/issues/239) [@chrisriley](https://github.com/chrisriley) +* Expose density metadata; set density of images from vector input. + [#338](https://github.com/lovell/sharp/issues/338) + [@lookfirst](https://github.com/lookfirst) + ### v0.13 - "*mind*" #### v0.13.1 - 27th February 2016 diff --git a/index.js b/index.js index d959e1f3..4a906b4e 100644 --- a/index.js +++ b/index.js @@ -46,7 +46,7 @@ var Sharp = function(input, options) { streamIn: false, sequentialRead: false, limitInputPixels: maximum.pixels, - density: '72', + density: 72, rawWidth: 0, rawHeight: 0, rawChannels: 0, @@ -172,7 +172,7 @@ Sharp.prototype._inputOptions = function(options) { // Density if (isDefined(options.density)) { if (isInteger(options.density) && inRange(options.density, 1, 2400)) { - this.options.density = options.density.toString(); + this.options.density = options.density; } else { throw new Error('Invalid density (1 to 2400) ' + options.density); } diff --git a/src/common.cc b/src/common.cc index 92aaaefb..46872aa8 100644 --- a/src/common.cc +++ b/src/common.cc @@ -176,6 +176,30 @@ namespace sharp { SetExifOrientation(image, 0); } + /* + Does this image have a non-default density? + */ + bool HasDensity(VImage image) { + return image.xres() > 1.0; + } + + /* + Get pixels/mm resolution as pixels/inch density. + */ + int GetDensity(VImage image) { + return static_cast(round(image.xres() * 25.4)); + } + + /* + Set pixels/mm resolution based on a pixels/inch density. + */ + void SetDensity(VImage image, const int density) { + const double pixelsPerMm = static_cast(density) / 25.4; + image.set("Xres", pixelsPerMm); + image.set("Yres", pixelsPerMm); + image.set(VIPS_META_RESOLUTION_UNIT, "in"); + } + /* Called when a Buffer undergoes GC, required to support mixed runtime libraries in Windows */ diff --git a/src/common.h b/src/common.h index a1fcfdb4..be7a2e78 100644 --- a/src/common.h +++ b/src/common.h @@ -77,6 +77,21 @@ namespace sharp { */ void RemoveExifOrientation(VImage image); + /* + Does this image have a non-default density? + */ + bool HasDensity(VImage image); + + /* + Get pixels/mm resolution as pixels/inch density. + */ + int GetDensity(VImage image); + + /* + Set pixels/mm resolution based on a pixels/inch density. + */ + void SetDensity(VImage image, const int density); + /* Called when a Buffer undergoes GC, required to support mixed runtime libraries in Windows */ diff --git a/src/metadata.cc b/src/metadata.cc index 196b6f54..f451fedc 100644 --- a/src/metadata.cc +++ b/src/metadata.cc @@ -38,6 +38,8 @@ using sharp::DetermineImageType; using sharp::HasProfile; using sharp::HasAlpha; using sharp::ExifOrientation; +using sharp::HasDensity; +using sharp::GetDensity; using sharp::FreeCallback; using sharp::counterQueue; @@ -52,6 +54,7 @@ struct MetadataBaton { int height; std::string space; int channels; + int density; bool hasProfile; bool hasAlpha; int orientation; @@ -63,6 +66,7 @@ struct MetadataBaton { MetadataBaton(): bufferInLength(0), + density(0), orientation(0), exifLength(0), iccLength(0) {} @@ -120,6 +124,9 @@ class MetadataWorker : public AsyncWorker { baton->height = image.height(); baton->space = vips_enum_nick(VIPS_TYPE_INTERPRETATION, image.interpretation()); baton->channels = image.bands(); + if (HasDensity(image)) { + baton->density = GetDensity(image); + } baton->hasProfile = HasProfile(image); // Derived attributes baton->hasAlpha = HasAlpha(image); @@ -161,6 +168,9 @@ class MetadataWorker : public AsyncWorker { Set(info, New("height").ToLocalChecked(), New(baton->height)); Set(info, New("space").ToLocalChecked(), New(baton->space).ToLocalChecked()); Set(info, New("channels").ToLocalChecked(), New(baton->channels)); + if (baton->density > 0) { + Set(info, New("density").ToLocalChecked(), New(baton->density)); + } Set(info, New("hasProfile").ToLocalChecked(), New(baton->hasProfile)); Set(info, New("hasAlpha").ToLocalChecked(), New(baton->hasAlpha)); if (baton->orientation > 0) { diff --git a/src/operations.cc b/src/operations.cc index 7cbedbab..be3c97a9 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -169,4 +169,5 @@ namespace sharp { ); } } + } // namespace sharp diff --git a/src/operations.h b/src/operations.h index 7c9d8899..059d8188 100644 --- a/src/operations.h +++ b/src/operations.h @@ -32,6 +32,7 @@ namespace sharp { * Sharpen flat and jagged areas. Use radius of -1 for fast sharpen. */ VImage Sharpen(VImage image, int const radius, double const flat, double const jagged); + } // namespace sharp #endif // SRC_OPERATIONS_H_ diff --git a/src/pipeline.cc b/src/pipeline.cc index 561eff7d..df5770e6 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -58,6 +58,7 @@ using sharp::HasAlpha; using sharp::ExifOrientation; using sharp::SetExifOrientation; using sharp::RemoveExifOrientation; +using sharp::SetDensity; using sharp::IsJpeg; using sharp::IsPng; using sharp::IsWebp; @@ -118,9 +119,12 @@ class PipelineWorker : public AsyncWorker { try { VOption *option = VImage::option()->set("access", baton->accessMethod); if (inputImageType == ImageType::MAGICK) { - option->set("density", baton->density.data()); + option->set("density", std::to_string(baton->density).data()); } image = VImage::new_from_buffer(baton->bufferIn, baton->bufferInLength, nullptr, option); + if (inputImageType == ImageType::MAGICK) { + SetDensity(image, baton->density); + } } catch (...) { (baton->err).append("Input buffer has corrupt header"); inputImageType = ImageType::UNKNOWN; @@ -136,9 +140,12 @@ class PipelineWorker : public AsyncWorker { try { VOption *option = VImage::option()->set("access", baton->accessMethod); if (inputImageType == ImageType::MAGICK) { - option->set("density", baton->density.data()); + option->set("density", std::to_string(baton->density).data()); } image = VImage::new_from_file(baton->fileIn.data(), option); + if (inputImageType == ImageType::MAGICK) { + SetDensity(image, baton->density); + } } catch (...) { (baton->err).append("Input file has corrupt header"); inputImageType = ImageType::UNKNOWN; @@ -921,7 +928,7 @@ NAN_METHOD(pipeline) { // Limit input images to a given number of pixels, where pixels = width * height baton->limitInputPixels = attrAs(options, "limitInputPixels"); // Density/DPI at which to load vector images via libmagick - baton->density = attrAsStr(options, "density"); + baton->density = attrAs(options, "density"); // Raw pixel input baton->rawWidth = attrAs(options, "rawWidth"); baton->rawHeight = attrAs(options, "rawHeight"); diff --git a/src/pipeline.h b/src/pipeline.h index 19c33fab..a5778a29 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -19,7 +19,7 @@ struct PipelineBaton { size_t bufferInLength; std::string iccProfilePath; int limitInputPixels; - std::string density; + int density; int rawWidth; int rawHeight; int rawChannels; @@ -79,7 +79,7 @@ struct PipelineBaton { PipelineBaton(): bufferInLength(0), limitInputPixels(0), - density(""), + density(72), rawWidth(0), rawHeight(0), rawChannels(0), diff --git a/test/unit/io.js b/test/unit/io.js index 4ae2de36..a776dbf5 100644 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -662,7 +662,14 @@ describe('Input/output', function() { assert.strictEqual('png', info.format); assert.strictEqual(40, info.width); assert.strictEqual(40, info.height); - fixtures.assertSimilar(fixtures.expected('svg72.png'), data, done); + fixtures.assertSimilar(fixtures.expected('svg72.png'), data, function(err) { + if (err) throw err; + sharp(data).metadata(function(err, info) { + if (err) throw err; + assert.strictEqual(72, info.density); + done(); + }); + }); } }); }); @@ -679,7 +686,14 @@ describe('Input/output', function() { assert.strictEqual('png', info.format); assert.strictEqual(40, info.width); assert.strictEqual(40, info.height); - fixtures.assertSimilar(fixtures.expected('svg1200.png'), data, done); + fixtures.assertSimilar(fixtures.expected('svg1200.png'), data, function(err) { + if (err) throw err; + sharp(data).metadata(function(err, info) { + if (err) throw err; + assert.strictEqual(1200, info.density); + done(); + }); + }); } }); }); diff --git a/test/unit/metadata.js b/test/unit/metadata.js index 26e48fca..05f9503a 100644 --- a/test/unit/metadata.js +++ b/test/unit/metadata.js @@ -18,6 +18,7 @@ describe('Image metadata', function() { assert.strictEqual(2225, metadata.height); assert.strictEqual('srgb', metadata.space); assert.strictEqual(3, metadata.channels); + assert.strictEqual('undefined', typeof metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -35,6 +36,7 @@ describe('Image metadata', function() { assert.strictEqual(600, metadata.height); assert.strictEqual('srgb', metadata.space); assert.strictEqual(3, metadata.channels); + assert.strictEqual(72, metadata.density); assert.strictEqual(true, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual(8, metadata.orientation); @@ -64,6 +66,7 @@ describe('Image metadata', function() { assert.strictEqual(3248, metadata.height); assert.strictEqual('b-w', metadata.space); assert.strictEqual(1, metadata.channels); + assert.strictEqual(300, metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -82,6 +85,7 @@ describe('Image metadata', function() { assert.strictEqual(2074, metadata.height); assert.strictEqual('b-w', metadata.space); assert.strictEqual(1, metadata.channels); + assert.strictEqual(300, metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -99,6 +103,7 @@ describe('Image metadata', function() { assert.strictEqual(1536, metadata.height); assert.strictEqual('srgb', metadata.space); assert.strictEqual(4, metadata.channels); + assert.strictEqual(72, metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(true, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -117,6 +122,7 @@ describe('Image metadata', function() { assert.strictEqual(772, metadata.height); assert.strictEqual('srgb', metadata.space); assert.strictEqual(3, metadata.channels); + assert.strictEqual('undefined', typeof metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -135,6 +141,7 @@ describe('Image metadata', function() { assert.strictEqual(800, metadata.width); assert.strictEqual(533, metadata.height); assert.strictEqual(3, metadata.channels); + assert.strictEqual('undefined', typeof metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -153,6 +160,7 @@ describe('Image metadata', function() { assert.strictEqual(2220, metadata.width); assert.strictEqual(2967, metadata.height); assert.strictEqual(4, metadata.channels); + assert.strictEqual('undefined', typeof metadata.density); assert.strictEqual('rgb', metadata.space); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(true, metadata.hasAlpha); @@ -171,6 +179,7 @@ describe('Image metadata', function() { assert.strictEqual(2225, metadata.height); assert.strictEqual('srgb', metadata.space); assert.strictEqual(3, metadata.channels); + assert.strictEqual('undefined', typeof metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -198,6 +207,7 @@ describe('Image metadata', function() { assert.strictEqual(2225, metadata.height); assert.strictEqual('srgb', metadata.space); assert.strictEqual(3, metadata.channels); + assert.strictEqual('undefined', typeof metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -219,6 +229,7 @@ describe('Image metadata', function() { assert.strictEqual(2225, metadata.height); assert.strictEqual('srgb', metadata.space); assert.strictEqual(3, metadata.channels); + assert.strictEqual('undefined', typeof metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); @@ -238,6 +249,7 @@ describe('Image metadata', function() { assert.strictEqual(2225, metadata.height); assert.strictEqual('srgb', metadata.space); assert.strictEqual(3, metadata.channels); + assert.strictEqual('undefined', typeof metadata.density); assert.strictEqual(false, metadata.hasProfile); assert.strictEqual(false, metadata.hasAlpha); assert.strictEqual('undefined', typeof metadata.orientation); From 86815bc9c49c49ab0a80f5f47de0a6a5f980ec39 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Tue, 1 Mar 2016 20:08:05 +0000 Subject: [PATCH 3/8] Emit post-processing 'info' event for Stream-based output --- docs/api.md | 14 ++++++++++++++ docs/changelog.md | 4 ++++ index.js | 6 ++++-- test/unit/io.js | 20 ++++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/docs/api.md b/docs/api.md index a69ae6c4..47ccf865 100644 --- a/docs/api.md +++ b/docs/api.md @@ -27,6 +27,7 @@ The object returned by the constructor implements the [stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class. JPEG, PNG or WebP format image data can be streamed out from this object. +When using Stream based output, derived attributes are available from the `info` event. ```javascript sharp('input.jpg') @@ -37,6 +38,19 @@ sharp('input.jpg') }); ``` +```javascript +// Read image data from readableStream, +// resize to 300 pixels wide, +// emit an 'info' event with calculated dimensions +// and finally write image data to writableStream +var transformer = sharp() + .resize(300) + .on('info', function(info) { + console.log('Image height is ' + info.height); + }); +readableStream.pipe(transformer).pipe(writableStream); +``` + #### metadata([callback]) Fast access to image metadata without decoding any compressed image data. diff --git a/docs/changelog.md b/docs/changelog.md index 8393616c..b8678b41 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,6 +10,10 @@ [#338](https://github.com/lovell/sharp/issues/338) [@lookfirst](https://github.com/lookfirst) +* Emit post-processing 'info' event for Stream output. + [#367](https://github.com/lovell/sharp/issues/367) + [@salzhrani](https://github.com/salzhrani) + ### v0.13 - "*mind*" #### v0.13.1 - 27th February 2016 diff --git a/index.js b/index.js index 4a906b4e..39a3aa60 100644 --- a/index.js +++ b/index.js @@ -803,10 +803,11 @@ Sharp.prototype._pipeline = function(callback) { if (this.options.streamIn) { // output=stream, input=stream this.on('finish', function() { - sharp.pipeline(that.options, function(err, data) { + sharp.pipeline(that.options, function(err, data, info) { if (err) { that.emit('error', err); } else { + that.emit('info', info); that.push(data); } that.push(null); @@ -814,10 +815,11 @@ Sharp.prototype._pipeline = function(callback) { }); } else { // output=stream, input=file/buffer - sharp.pipeline(this.options, function(err, data) { + sharp.pipeline(this.options, function(err, data, info) { if (err) { that.emit('error', err); } else { + that.emit('info', info); that.push(data); } that.push(null); diff --git a/test/unit/io.js b/test/unit/io.js index a776dbf5..923b4d9e 100644 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -109,6 +109,26 @@ describe('Input/output', function() { readable.pipe(pipeline).pipe(writable); }); + it('Stream should emit info event', function(done) { + var readable = fs.createReadStream(fixtures.inputJpg); + var writable = fs.createWriteStream(fixtures.outputJpg); + var pipeline = sharp().resize(320, 240); + var infoEventEmitted = false; + pipeline.on('info', function(info) { + assert.strictEqual('jpeg', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + assert.strictEqual(3, info.channels); + infoEventEmitted = true; + }); + writable.on('finish', function() { + assert.strictEqual(true, infoEventEmitted); + fs.unlinkSync(fixtures.outputJpg); + done(); + }); + readable.pipe(pipeline).pipe(writable); + }); + it('Handle Stream to Stream error ', function(done) { var pipeline = sharp().resize(320, 240); var anErrorWasEmitted = false; From f950294f705638c5a9e72839fd30f1a3e9227567 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Thu, 3 Mar 2016 09:18:11 +0000 Subject: [PATCH 4/8] Add ability to extend (pad) the edges of an image --- docs/api.md | 21 ++++++++- docs/changelog.md | 4 ++ index.js | 30 +++++++++++++ src/pipeline.cc | 25 +++++++++++ src/pipeline.h | 8 ++++ test/fixtures/expected/extend-equal.jpg | Bin 0 -> 4195 bytes test/fixtures/expected/extend-unequal.png | Bin 0 -> 18100 bytes test/unit/extend.js | 52 ++++++++++++++++++++++ 8 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/expected/extend-equal.jpg create mode 100644 test/fixtures/expected/extend-unequal.png create mode 100644 test/unit/extend.js diff --git a/docs/api.md b/docs/api.md index 47ccf865..b8641e6f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -280,7 +280,7 @@ sharp(input) #### background(rgba) -Set the background for the `embed` and `flatten` operations. +Set the background for the `embed`, `flatten` and `extend` operations. `rgba` is parsed by the [color](https://www.npmjs.org/package/color) module to extract values for red, green, blue and alpha. @@ -292,6 +292,25 @@ The default background is `{r: 0, g: 0, b: 0, a: 1}`, black without transparency Merge alpha transparency channel, if any, with `background`. +#### extend(extension) + +Extends/pads the edges of the image with `background`, where `extension` is one of: + +* a Number representing the pixel count to add to each edge, or +* an Object containing `top`, `left`, `bottom` and `right` attributes, each a Number of pixels to add to that edge. + +This operation will always occur after resizing and extraction, if any. + +```javascript +// Resize to 140 pixels wide, then add 10 transparent pixels +// to the top, left and right edges and 20 to the bottom edge +sharp(input) + .resize(140) + .background({r: 0, g: 0, b: 0, a: 0}) + .extend({top: 10, bottom: 20, left: 10, right: 10}) + ... +``` + #### negate() Produces the "negative" of the image. White => Black, Black => White, Blue => Yellow, etc. diff --git a/docs/changelog.md b/docs/changelog.md index b8678b41..60b8cac9 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,10 @@ ### v0.14 - "*needle*" +* Add ability to extend (pad) the edges of an image. + [#128](https://github.com/lovell/sharp/issues/128) + [@blowsie](https://github.com/blowsie) + * Improvements to overlayWith: differing sizes/formats, gravity, buffer input. [#239](https://github.com/lovell/sharp/issues/239) [@chrisriley](https://github.com/chrisriley) diff --git a/index.js b/index.js index 39a3aa60..265ad690 100644 --- a/index.js +++ b/index.js @@ -69,6 +69,10 @@ var Sharp = function(input, options) { rotateBeforePreExtract: false, flip: false, flop: false, + extendTop: 0, + extendBottom: 0, + extendLeft: 0, + extendRight: 0, withoutEnlargement: false, interpolator: 'bicubic', // operations @@ -650,6 +654,32 @@ Sharp.prototype.tile = function(size, overlap) { return this; }; +/* + Extend edges +*/ +Sharp.prototype.extend = function(extend) { + if (isInteger(extend) && extend > 0) { + this.options.extendTop = extend; + this.options.extendBottom = extend; + this.options.extendLeft = extend; + this.options.extendRight = extend; + } else if ( + isObject(extend) && + isInteger(extend.top) && extend.top >= 0 && + isInteger(extend.bottom) && extend.bottom >= 0 && + isInteger(extend.left) && extend.left >= 0 && + isInteger(extend.right) && extend.right >= 0 + ) { + this.options.extendTop = extend.top; + this.options.extendBottom = extend.bottom; + this.options.extendLeft = extend.left; + this.options.extendRight = extend.right; + } else { + throw new Error('Invalid edge extension ' + extend); + } + return this; +}; + Sharp.prototype.resize = function(width, height) { if (!width) { this.options.width = -1; diff --git a/src/pipeline.cc b/src/pipeline.cc index df5770e6..f153559f 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -522,6 +522,27 @@ class PipelineWorker : public AsyncWorker { ); } + // Extend edges + if (baton->extendTop > 0 || baton->extendBottom > 0 || baton->extendLeft > 0 || baton->extendRight > 0) { + // Scale up 8-bit values to match 16-bit input image + const double multiplier = (image.interpretation() == VIPS_INTERPRETATION_RGB16) ? 256.0 : 1.0; + // Create background colour + std::vector background { + baton->background[0] * multiplier, + baton->background[1] * multiplier, + baton->background[2] * multiplier + }; + // Add alpha channel to background colour + if (HasAlpha(image)) { + background.push_back(baton->background[3] * multiplier); + } + // Embed + baton->width = image.width() + baton->extendLeft + baton->extendRight; + baton->height = image.height() + baton->extendTop + baton->extendBottom; + image = image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height, + VImage::option()->set("extend", VIPS_EXTEND_BACKGROUND)->set("background", background)); + } + // Threshold - must happen before blurring, due to the utility of blurring after thresholding if (shouldThreshold) { image = image.colourspace(VIPS_INTERPRETATION_B_W) >= baton->threshold; @@ -991,6 +1012,10 @@ NAN_METHOD(pipeline) { baton->rotateBeforePreExtract = attrAs(options, "rotateBeforePreExtract"); baton->flip = attrAs(options, "flip"); baton->flop = attrAs(options, "flop"); + baton->extendTop = attrAs(options, "extendTop"); + baton->extendBottom = attrAs(options, "extendBottom"); + baton->extendLeft = attrAs(options, "extendLeft"); + baton->extendRight = attrAs(options, "extendRight"); // Output options baton->progressive = attrAs(options, "progressive"); baton->quality = attrAs(options, "quality"); diff --git a/src/pipeline.h b/src/pipeline.h index a5778a29..ae024614 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -60,6 +60,10 @@ struct PipelineBaton { bool rotateBeforePreExtract; bool flip; bool flop; + int extendTop; + int extendBottom; + int extendLeft; + int extendRight; bool progressive; bool withoutEnlargement; VipsAccess accessMethod; @@ -106,6 +110,10 @@ struct PipelineBaton { angle(0), flip(false), flop(false), + extendTop(0), + extendBottom(0), + extendLeft(0), + extendRight(0), progressive(false), withoutEnlargement(false), quality(80), diff --git a/test/fixtures/expected/extend-equal.jpg b/test/fixtures/expected/extend-equal.jpg new file mode 100644 index 0000000000000000000000000000000000000000..800f32e968c91cc2cd9d639cadc0bffa6b39715d GIT binary patch literal 4195 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/expected/extend-unequal.png b/test/fixtures/expected/extend-unequal.png new file mode 100644 index 0000000000000000000000000000000000000000..1454f01644032dd339e5ad9b72cc5ddf80affeb1 GIT binary patch literal 18100 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 QRsaA107*qoM6N<$f_ha5p#T5? literal 0 HcmV?d00001 diff --git a/test/unit/extend.js b/test/unit/extend.js new file mode 100644 index 00000000..2232512d --- /dev/null +++ b/test/unit/extend.js @@ -0,0 +1,52 @@ +'use strict'; + +var assert = require('assert'); + +var sharp = require('../../index'); +var fixtures = require('../fixtures'); + +describe('Extend', function () { + + it('extend all sides equally with RGB', function(done) { + sharp(fixtures.inputJpg) + .resize(120) + .background({r: 255, g: 0, b: 0}) + .extend(10) + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(140, info.width); + assert.strictEqual(118, info.height); + fixtures.assertSimilar(fixtures.expected('extend-equal.jpg'), data, done); + }); + }); + + it('extend sides unequally with RGBA', function(done) { + sharp(fixtures.inputPngWithTransparency16bit) + .resize(120) + .background({r: 0, g: 0, b: 0, a: 0}) + .extend({top: 50, bottom: 0, left: 10, right: 35}) + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(165, info.width); + assert.strictEqual(170, info.height); + fixtures.assertSimilar(fixtures.expected('extend-unequal.png'), data, done); + }); + }); + + it('missing parameter fails', function() { + assert.throws(function() { + sharp().extend(); + }); + }); + it('negative fails', function() { + assert.throws(function() { + sharp().extend(-1); + }); + }); + it('partial object fails', function() { + assert.throws(function() { + sharp().extend({top: 1}); + }); + }); + +}); From 38ddb3b866029d72174d39bd5baef110155e01a9 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Thu, 3 Mar 2016 20:29:23 +0000 Subject: [PATCH 5/8] Add support for Zoomify and Google tile layouts Breaks existing tile API --- docs/api.md | 19 ++-- docs/changelog.md | 4 + index.js | 49 ++++++---- src/pipeline.cc | 11 +++ src/pipeline.h | 6 +- test/unit/tile.js | 231 ++++++++++++++++++++++------------------------ 6 files changed, 176 insertions(+), 144 deletions(-) diff --git a/docs/api.md b/docs/api.md index b8641e6f..cd186926 100644 --- a/docs/api.md +++ b/docs/api.md @@ -510,18 +510,25 @@ This has no effect if the input image does not have an EXIF `Orientation` tag. The default behaviour, when `withMetadata` is not used, is to strip all metadata and convert to the device-independent sRGB colour space. -#### tile([size], [overlap]) +#### tile(options) -The size and overlap, in pixels, of square Deep Zoom image pyramid tiles. +The size, overlap and directory layout to use when generating square Deep Zoom image pyramid tiles. + +`options` is an Object with one or more of the following attributes: * `size` is an integral Number between 1 and 8192. The default value is 256 pixels. * `overlap` is an integral Number between 0 and 8192. The default value is 0 pixels. +* `layout` is a String, with value `dz`, `zoomify` or `google`. The default value is `dz`. ```javascript -sharp('input.tiff').tile(256).toFile('output.dzi', function(err, info) { - // The output.dzi file is the XML format Deep Zoom definition - // The output_files directory contains 256x256 pixel tiles grouped by zoom level -}); +sharp('input.tiff') + .tile({ + size: 512 + }) + .toFile('output.dzi', function(err, info) { + // output.dzi is the Deep Zoom XML definition + // output_files contains 512x512 tiles grouped by zoom level + }); ``` #### withoutChromaSubsampling() diff --git a/docs/changelog.md b/docs/changelog.md index 60b8cac9..4bac9062 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,10 @@ [#128](https://github.com/lovell/sharp/issues/128) [@blowsie](https://github.com/blowsie) +* Add support for Zoomify and Google tile layouts. Breaks existing tile API. + [#223](https://github.com/lovell/sharp/issues/223) + [@bdunnette](https://github.com/bdunnette) + * Improvements to overlayWith: differing sizes/formats, gravity, buffer input. [#239](https://github.com/lovell/sharp/issues/239) [@chrisriley](https://github.com/chrisriley) diff --git a/index.js b/index.js index 265ad690..65ad0c09 100644 --- a/index.js +++ b/index.js @@ -166,6 +166,9 @@ var isInteger = function(val) { var inRange = function(val, min, max) { return val >= min && val <= max; }; +var contains = function(val, list) { + return list.indexOf(val) !== -1; +}; /* Set input-related options @@ -629,26 +632,36 @@ Sharp.prototype.withMetadata = function(withMetadata) { }; /* - Tile size and overlap for Deep Zoom output + Tile-based deep zoom output options: size, overlap, layout */ -Sharp.prototype.tile = function(size, overlap) { - // Size of square tiles, in pixels - if (typeof size !== 'undefined' && size !== null) { - if (!Number.isNaN(size) && size % 1 === 0 && size >= 1 && size <= 8192) { - this.options.tileSize = size; - } else { - throw new Error('Invalid tile size (1 to 8192) ' + size); - } - } - // Overlap of tiles, in pixels - if (typeof overlap !== 'undefined' && overlap !== null) { - if (!Number.isNaN(overlap) && overlap % 1 === 0 && overlap >= 0 && overlap <= 8192) { - if (overlap > this.options.tileSize) { - throw new Error('Tile overlap ' + overlap + ' cannot be larger than tile size ' + this.options.tileSize); +Sharp.prototype.tile = function(tile) { + if (isObject(tile)) { + // Size of square tiles, in pixels + if (isDefined(tile.size)) { + if (isInteger(tile.size) && inRange(tile.size, 1, 8192)) { + this.options.tileSize = tile.size; + } else { + throw new Error('Invalid tile size (1 to 8192) ' + tile.size); + } + } + // Overlap of tiles, in pixels + if (isDefined(tile.overlap)) { + if (isInteger(tile.overlap) && inRange(tile.overlap, 0, 8192)) { + if (tile.overlap > this.options.tileSize) { + throw new Error('Tile overlap ' + tile.overlap + ' cannot be larger than tile size ' + this.options.tileSize); + } + this.options.tileOverlap = tile.overlap; + } else { + throw new Error('Invalid tile overlap (0 to 8192) ' + tile.overlap); + } + } + // Layout + if (isDefined(tile.layout)) { + if (isString(tile.layout) && contains(tile.layout, ['dz', 'google', 'zoomify'])) { + this.options.tileLayout = tile.layout; + } else { + throw new Error('Invalid tile layout ' + tile.layout); } - this.options.tileOverlap = overlap; - } else { - throw new Error('Invalid tile overlap (0 to 8192) ' + overlap); } } return this; diff --git a/src/pipeline.cc b/src/pipeline.cc index f153559f..a4f3e91d 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -766,6 +766,7 @@ class PipelineWorker : public AsyncWorker { ->set("strip", !baton->withMetadata) ->set("tile_size", baton->tileSize) ->set("overlap", baton->tileOverlap) + ->set("layout", baton->tileLayout) ); baton->formatOut = "dz"; } else { @@ -1030,8 +1031,18 @@ NAN_METHOD(pipeline) { // Output baton->formatOut = attrAsStr(options, "formatOut"); baton->fileOut = attrAsStr(options, "fileOut"); + // Tile output baton->tileSize = attrAs(options, "tileSize"); baton->tileOverlap = attrAs(options, "tileOverlap"); + std::string tileLayout = attrAsStr(options, "tileLayout"); + if (tileLayout == "google") { + baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_GOOGLE; + } else if (tileLayout == "zoomify") { + baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_ZOOMIFY; + } else { + baton->tileLayout = VIPS_FOREIGN_DZ_LAYOUT_DZ; + } + // Function to notify of queue length changes Callback *queueListener = new Callback( Get(options, New("queueListener").ToLocalChecked()).ToLocalChecked().As() diff --git a/src/pipeline.h b/src/pipeline.h index ae024614..4f473bfe 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -1,6 +1,8 @@ #ifndef SRC_PIPELINE_H_ #define SRC_PIPELINE_H_ +#include + #include "nan.h" NAN_METHOD(pipeline); @@ -79,6 +81,7 @@ struct PipelineBaton { int withMetadataOrientation; int tileSize; int tileOverlap; + VipsForeignDzLayout tileLayout; PipelineBaton(): bufferInLength(0), @@ -126,7 +129,8 @@ struct PipelineBaton { withMetadata(false), withMetadataOrientation(-1), tileSize(256), - tileOverlap(0) { + tileOverlap(0), + tileLayout(VIPS_FOREIGN_DZ_LAYOUT_DZ) { background[0] = 0.0; background[1] = 0.0; background[2] = 0.0; diff --git a/test/unit/tile.js b/test/unit/tile.js index 6b0c797e..f0dd044c 100644 --- a/test/unit/tile.js +++ b/test/unit/tile.js @@ -47,156 +47,149 @@ var assertDeepZoomTiles = function(directory, expectedSize, expectedLevels, done describe('Tile', function() { - describe('Invalid tile values', function() { - it('size - NaN', function(done) { - var isValid = true; - try { - sharp().tile('zoinks'); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + it('Valid size values pass', function() { + [1, 8192].forEach(function(size) { + assert.doesNotThrow(function() { + sharp().tile({ + size: size + }); + }); }); + }); - it('size - float', function(done) { - var isValid = true; - try { - sharp().tile(1.1); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + it('Invalid size values fail', function() { + ['zoinks', 1.1, -1, 0, 8193].forEach(function(size) { + assert.throws(function() { + sharp().tile({ + size: size + }); + }); }); + }); - it('size - negative', function(done) { - var isValid = true; - try { - sharp().tile(-1); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + it('Valid overlap values pass', function() { + [0, 8192].forEach(function(overlap) { + assert.doesNotThrow(function() { + sharp().tile({ + size: 8192, + overlap: overlap + }); + }); }); + }); - it('size - zero', function(done) { - var isValid = true; - try { - sharp().tile(0); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + it('Invalid overlap values fail', function() { + ['zoinks', 1.1, -1, 8193].forEach(function(overlap) { + assert.throws(function() { + sharp().tile({ + overlap: overlap + }); + }); }); + }); - it('size - too large', function(done) { - var isValid = true; - try { - sharp().tile(8193); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + it('Valid layout values pass', function() { + ['dz', 'google', 'zoomify'].forEach(function(layout) { + assert.doesNotThrow(function() { + sharp().tile({ + layout: layout + }); + }); }); + }); - it('overlap - NaN', function(done) { - var isValid = true; - try { - sharp().tile(null, 'zoinks'); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + it('Invalid layout values fail', function() { + ['zoinks', 1].forEach(function(layout) { + assert.throws(function() { + sharp().tile({ + layout: layout + }); + }); }); + }); - it('overlap - float', function(done) { - var isValid = true; - try { - sharp().tile(null, 1.1); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + it('Prevent larger overlap than default size', function() { + assert.throws(function() { + sharp().tile({overlap: 257}); }); + }); - it('overlap - negative', function(done) { - var isValid = true; - try { - sharp().tile(null, -1); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); + it('Prevent larger overlap than provided size', function() { + assert.throws(function() { + sharp().tile({size: 512, overlap: 513}); }); - - it('overlap - too large', function(done) { - var isValid = true; - try { - sharp().tile(null, 8193); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); - }); - - it('overlap - larger than default size', function(done) { - var isValid = true; - try { - sharp().tile(null, 257); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); - }); - - it('overlap - larger than provided size', function(done) { - var isValid = true; - try { - sharp().tile(512, 513); - } catch (err) { - isValid = false; - } - assert.strictEqual(false, isValid); - done(); - }); - }); if (sharp.format.dz.output.file) { - describe('Deep Zoom output', function() { - it('Tile size - 256px default', function(done) { - var directory = fixtures.path('output.256_files'); - rimraf(directory, function() { - sharp(fixtures.inputJpg).toFile(fixtures.path('output.256.dzi'), function(err, info) { + it('Deep Zoom layout', function(done) { + var directory = fixtures.path('output.dz_files'); + rimraf(directory, function() { + sharp(fixtures.inputJpg) + .toFile(fixtures.path('output.dz.dzi'), function(err, info) { if (err) throw err; assert.strictEqual('dz', info.format); assertDeepZoomTiles(directory, 256, 13, done); }); - }); }); + }); - it('Tile size/overlap - 512/16px', function(done) { - var directory = fixtures.path('output.512_files'); - rimraf(directory, function() { - sharp(fixtures.inputJpg).tile(512, 16).toFile(fixtures.path('output.512.dzi'), function(err, info) { + it('Deep Zoom layout with custom size+overlap', function(done) { + var directory = fixtures.path('output.dz.512_files'); + rimraf(directory, function() { + sharp(fixtures.inputJpg) + .tile({ + size: 512, + overlap: 16 + }) + .toFile(fixtures.path('output.dz.512.dzi'), function(err, info) { if (err) throw err; assert.strictEqual('dz', info.format); assertDeepZoomTiles(directory, 512 + 2 * 16, 13, done); }); - }); }); - }); + + it('Zoomify layout', function(done) { + var directory = fixtures.path('output.zoomify'); + rimraf(directory, function() { + sharp(fixtures.inputJpg) + .tile({ + layout: 'zoomify' + }) + .toFile(fixtures.path('output.zoomify.dzi'), function(err, info) { + if (err) throw err; + assert.strictEqual('dz', info.format); + fs.stat(path.join(directory, 'ImageProperties.xml'), function(err, stat) { + if (err) throw err; + assert.strictEqual(true, stat.isFile()); + assert.strictEqual(true, stat.size > 0); + done(); + }); + }); + }); + }); + + it('Google layout', function(done) { + var directory = fixtures.path('output.google'); + rimraf(directory, function() { + sharp(fixtures.inputJpg) + .tile({ + layout: 'google' + }) + .toFile(fixtures.path('output.google.dzi'), function(err, info) { + if (err) throw err; + assert.strictEqual('dz', info.format); + fs.stat(path.join(directory, '0', '0', '0.jpg'), function(err, stat) { + if (err) throw err; + assert.strictEqual(true, stat.isFile()); + assert.strictEqual(true, stat.size > 0); + done(); + }); + }); + }); + }); + } }); From 2034efcf558bbb0a6936b1804c0422103d43ad69 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Sat, 5 Mar 2016 12:29:16 +0000 Subject: [PATCH 6/8] Add experimental, entropy-based auto-crop Remove deprecated extract API --- docs/api.md | 25 +++++-- docs/changelog.md | 4 ++ index.js | 65 +++++++++--------- src/operations.cc | 78 +++++++++++++++++++++ src/operations.h | 10 +++ src/pipeline.cc | 15 +++-- src/pipeline.h | 4 +- test/fixtures/expected/crop-entropy.jpg | Bin 0 -> 8521 bytes test/fixtures/expected/crop-entropy.png | Bin 0 -> 6148 bytes test/unit/crop.js | 86 +++++++++++++++--------- test/unit/extract.js | 23 ------- 11 files changed, 214 insertions(+), 96 deletions(-) create mode 100644 test/fixtures/expected/crop-entropy.jpg create mode 100644 test/fixtures/expected/crop-entropy.png diff --git a/docs/api.md b/docs/api.md index cd186926..fd0cab2f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -125,23 +125,34 @@ Scale output to `width` x `height`. By default, the resized image is cropped to `height` is the integral Number of pixels high the resultant image should be, between 1 and 16383. Use `null` or `undefined` to auto-scale the height to match the width. -#### crop([gravity]) +#### crop([option]) Crop the resized image to the exact size specified, the default behaviour. -`gravity`, if present, is a String or an attribute of the `sharp.gravity` Object e.g. `sharp.gravity.north`. +`option`, if present, is an attribute of: -Possible values are `north`, `northeast`, `east`, `southeast`, `south`, `southwest`, `west`, `northwest`, `center` and `centre`. -The default gravity is `center`/`centre`. +* `sharp.gravity` e.g. `sharp.gravity.north`, to crop to an edge or corner, or +* `sharp.strategy` e.g. `sharp.strategy.entropy`, to crop dynamically. + +Possible attributes of `sharp.gravity` are +`north`, `northeast`, `east`, `southeast`, `south`, +`southwest`, `west`, `northwest`, `center` and `centre`. + +`sharp.strategy` currently contains only the experimental `entropy`, +which will retain the part of the image with the highest +[Shannon entropy](https://en.wikipedia.org/wiki/Entropy_%28information_theory%29) value. + +The default option is a `center`/`centre` gravity. ```javascript var transformer = sharp() - .resize(300, 200) - .crop(sharp.gravity.north) + .resize(200, 200) + .crop(sharp.strategy.entropy) .on('error', function(err) { console.log(err); }); -// Read image data from readableStream, resize and write image data to writableStream +// Read image data from readableStream +// Write 200px square auto-cropped image data to writableStream readableStream.pipe(transformer).pipe(writableStream); ``` diff --git a/docs/changelog.md b/docs/changelog.md index 4bac9062..866114d4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -14,6 +14,10 @@ [#239](https://github.com/lovell/sharp/issues/239) [@chrisriley](https://github.com/chrisriley) +* Add entropy-based strategy to determine crop region. + [#295](https://github.com/lovell/sharp/issues/295) + [@rightaway](https://github.com/rightaway) + * Expose density metadata; set density of images from vector input. [#338](https://github.com/lovell/sharp/issues/338) [@lookfirst](https://github.com/lookfirst) diff --git a/index.js b/index.js index 65ad0c09..dbb70a48 100644 --- a/index.js +++ b/index.js @@ -64,7 +64,7 @@ var Sharp = function(input, options) { width: -1, height: -1, canvas: 'crop', - gravity: 0, + crop: 0, angle: 0, rotateBeforePreExtract: false, flip: false, @@ -231,48 +231,53 @@ Sharp.prototype._write = function(chunk, encoding, callback) { } }; -// Crop this part of the resized image (Center/Centre, North, East, South, West) +// Weighting to apply to image crop module.exports.gravity = { - 'center': 0, - 'centre': 0, - 'north': 1, - 'east': 2, - 'south': 3, - 'west': 4, - 'northeast': 5, - 'southeast': 6, - 'southwest': 7, - 'northwest': 8 + center: 0, + centre: 0, + north: 1, + east: 2, + south: 3, + west: 4, + northeast: 5, + southeast: 6, + southwest: 7, + northwest: 8 }; -Sharp.prototype.crop = function(gravity) { +// Strategies for automagic behaviour +module.exports.strategy = { + entropy: 16 +}; + +/* + What part of the image should be retained when cropping? +*/ +Sharp.prototype.crop = function(crop) { this.options.canvas = 'crop'; - if (!isDefined(gravity)) { - this.options.gravity = module.exports.gravity.center; - } else if (isInteger(gravity) && inRange(gravity, 0, 8)) { - this.options.gravity = gravity; - } else if (isString(gravity) && isInteger(module.exports.gravity[gravity])) { - this.options.gravity = module.exports.gravity[gravity]; + if (!isDefined(crop)) { + // Default + this.options.crop = module.exports.gravity.center; + } else if (isInteger(crop) && inRange(crop, 0, 8)) { + // Gravity (numeric) + this.options.crop = crop; + } else if (isString(crop) && isInteger(module.exports.gravity[crop])) { + // Gravity (string) + this.options.crop = module.exports.gravity[crop]; + } else if (isInteger(crop) && crop === module.exports.strategy.entropy) { + // Strategy + this.options.crop = crop; } else { - throw new Error('Unsupported crop gravity ' + gravity); + throw new Error('Unsupported crop ' + crop); } return this; }; Sharp.prototype.extract = function(options) { - if (!options || typeof options !== 'object') { - // Legacy extract(top,left,width,height) syntax - options = { - left: arguments[1], - top: arguments[0], - width: arguments[2], - height: arguments[3] - }; - } var suffix = this.options.width === -1 && this.options.height === -1 ? 'Pre' : 'Post'; ['left', 'top', 'width', 'height'].forEach(function (name) { var value = options[name]; - if (typeof value === 'number' && !Number.isNaN(value) && value % 1 === 0 && value >= 0) { + if (isInteger(value) && value >= 0) { this.options[name + (name === 'left' || name === 'top' ? 'Offset' : '') + suffix] = value; } else { throw new Error('Non-integer value for ' + name + ' of ' + value); diff --git a/src/operations.cc b/src/operations.cc index be3c97a9..ad9f8406 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -170,4 +170,82 @@ namespace sharp { } } + /* + Calculate crop area based on image entropy + */ + std::tuple EntropyCrop(VImage image, int const outWidth, int const outHeight) { + int left = 0; + int top = 0; + int const inWidth = image.width(); + int const inHeight = image.height(); + if (inWidth > outWidth) { + // Reduce width by repeated removing slices from edge with lowest entropy + int width = inWidth; + double leftEntropy = 0.0; + double rightEntropy = 0.0; + // Max width of each slice + int const maxSliceWidth = static_cast(ceil((inWidth - outWidth) / 8.0)); + while (width > outWidth) { + // Width of current slice + int const slice = std::min(width - outWidth, maxSliceWidth); + if (leftEntropy == 0.0) { + // Update entropy of left slice + leftEntropy = Entropy(image.extract_area(left, 0, slice, inHeight)); + } + if (rightEntropy == 0.0) { + // Update entropy of right slice + rightEntropy = Entropy(image.extract_area(width - slice - 1, 0, slice, inHeight)); + } + // Keep slice with highest entropy + if (leftEntropy >= rightEntropy) { + // Discard right slice + rightEntropy = 0.0; + } else { + // Discard left slice + leftEntropy = 0.0; + left = left + slice; + } + width = width - slice; + } + } + if (inHeight > outHeight) { + // Reduce height by repeated removing slices from edge with lowest entropy + int height = inHeight; + double topEntropy = 0.0; + double bottomEntropy = 0.0; + // Max height of each slice + int const maxSliceHeight = static_cast(ceil((inHeight - outHeight) / 8.0)); + while (height > outHeight) { + // Height of current slice + int const slice = std::min(height - outHeight, maxSliceHeight); + if (topEntropy == 0.0) { + // Update entropy of top slice + topEntropy = Entropy(image.extract_area(0, top, inWidth, slice)); + } + if (bottomEntropy == 0.0) { + // Update entropy of bottom slice + bottomEntropy = Entropy(image.extract_area(0, height - slice - 1, inWidth, slice)); + } + // Keep slice with highest entropy + if (topEntropy >= bottomEntropy) { + // Discard bottom slice + bottomEntropy = 0.0; + } else { + // Discard top slice + topEntropy = 0.0; + top = top + slice; + } + height = height - slice; + } + } + return std::make_tuple(left, top); + } + + /* + Calculate the Shannon entropy for an image + */ + double Entropy(VImage image) { + return image.hist_find().hist_entropy(); + } + } // namespace sharp diff --git a/src/operations.h b/src/operations.h index 059d8188..5ece5743 100644 --- a/src/operations.h +++ b/src/operations.h @@ -33,6 +33,16 @@ namespace sharp { */ VImage Sharpen(VImage image, int const radius, double const flat, double const jagged); + /* + Calculate crop area based on image entropy + */ + std::tuple EntropyCrop(VImage image, int const outWidth, int const outHeight); + + /* + Calculate the Shannon entropy for an image + */ + double Entropy(VImage image); + } // namespace sharp #endif // SRC_OPERATIONS_H_ diff --git a/src/pipeline.cc b/src/pipeline.cc index a4f3e91d..41a992c0 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -49,6 +49,7 @@ using sharp::Normalize; using sharp::Gamma; using sharp::Blur; using sharp::Sharpen; +using sharp::EntropyCrop; using sharp::ImageType; using sharp::ImageTypeId; @@ -506,9 +507,15 @@ class PipelineWorker : public AsyncWorker { // Crop/max/min int left; int top; - std::tie(left, top) = CalculateCrop( - image.width(), image.height(), baton->width, baton->height, baton->gravity - ); + if (baton->crop < 9) { + // Gravity-based crop + std::tie(left, top) = CalculateCrop( + image.width(), image.height(), baton->width, baton->height, baton->crop + ); + } else { + // Entropy-based crop + std::tie(left, top) = EntropyCrop(image, baton->width, baton->height); + } int width = std::min(image.width(), baton->width); int height = std::min(image.height(), baton->height); image = image.extract_area(left, top, width, height); @@ -996,7 +1003,7 @@ NAN_METHOD(pipeline) { baton->overlayGravity = attrAs(options, "overlayGravity"); // Resize options baton->withoutEnlargement = attrAs(options, "withoutEnlargement"); - baton->gravity = attrAs(options, "gravity"); + baton->crop = attrAs(options, "crop"); baton->interpolator = attrAsStr(options, "interpolator"); // Operators baton->flatten = attrAs(options, "flatten"); diff --git a/src/pipeline.h b/src/pipeline.h index 4f473bfe..eba987e3 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -45,7 +45,7 @@ struct PipelineBaton { int height; int channels; Canvas canvas; - int gravity; + int crop; std::string interpolator; double background[4]; bool flatten; @@ -99,7 +99,7 @@ struct PipelineBaton { topOffsetPost(-1), channels(0), canvas(Canvas::CROP), - gravity(0), + crop(0), flatten(false), negate(false), blurSigma(0.0), diff --git a/test/fixtures/expected/crop-entropy.jpg b/test/fixtures/expected/crop-entropy.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c1c08061cbacca3db8640b27eea743df89580811 GIT binary patch literal 8521 zcmb7|WmFS@*T#o53P?(K4ICmR-8qmL=?D=}8eueuN=gZHv@~p_1C(tHkQ&kw(%oH3 z2ol2U|NPH+&-?X#zCQQ-p8M&Z=iHmAn^}M+fQ*EcjFg0ojFgO=oQ#6%E)^9eB^4tL z?VY>#7+F~EF)}l=aSC#?vGa23Rhw9`Eri^@&&wL{^J`^$WLq50vU>0zM zQ3y(D7XOltTpsxcU?T$jga2Rre*t9wJQ2Eo(f<(XPdePkrmG=%Mg?=tNo3U;w^J5S*~aO zWeX(rPx!&6TpQCuN7NC}Fn#74PzAAXqtbBg&nUgVZ5MQQfGl z8vtE<(ia>F=A3uf2ST}jT+!FSNl%_|kumh<9nr%T<9HvVbv?+MHAkrTqGI$xT^XaKrLj>4tPBGr@L#{l1u@k=&Zgabc6!BBU|X zDg%6S3oR{~cY#xTvv%Yt51@6SiDNsCKO9 zrO|9wy$=`snI^SZaYh*t6g|;um`jn# zmg#X%G5zX2y9Y18^sD!!XDqX+bhvznDNXg1=_%!h?9&7*0t6EGTc=Tk=!VDKf@OB3 zAd>QZNp{X2l6?80nb_uNz@cEK5Ogx&*%+^%ki4MpA5%{;pzfs+!^Ln{bnc1A8dJ!1 zAEqiK=u61{OxA+nR>km%K|RZU=Kgn3ZWve=tj2X_Z_=%bs6cLiC zSi$TducU&1Cv+GM2h60wEb>YHvlKa67`EHyF$N#_``z4*=L5hq3_lsmQVdBs@>MVf zA+g`P=0nCoReqo3o}|1*DW}2H(OOG)uBrlaFjzsjeYoeWDTj?Te zF8gLvu|JZ;>p0BrD=dpy+jAJ`a}p85)s=p4n?oro-xsgUD@pw=;bMQWHBIr%3r~*{ z^$m&E1)w{m1CFzfCbZX)s%XN+?YE3rQI# zg}(znZ%R*od+Hd1O=GfoYFS!`#cOsL<38O0K04u5zv;k~feO@IPpW(JN_lO(Mxtny z5F=?t2blMBh_KYQifXrS^WG2H+&4E2BUp_jA`WW%TTe$Pi`sjKd|aK>zQ))*HP^8X zxi5d&9jT#1@kejBixylTBZ5at^rPwpDvT7&BMX4aw{$`g3MNT;p|`c>pTc0ji@kj) z!|PcSig%QT0%!_JIck)NjGN{r6`veX%Rdr(2DHgDmm~QL?5|4u&eESXl)ZLtZ;p|r zLL4bLaemG(+W@LkN&5+kOW53=3zDTIG*g9>Pejk76LlLmwuM&3`2@<-bUgZBCONxQ zi|tEsf+>LfR3^q9yVkpw6E0mnGjM|1Tv6}O6le}As=Ae431+gJNU6`Mz7Jmb&_6wF zk~Hb{c>Ay7dWMZBC1TDLz%bap7&2r(Rbgd`QY?RFW1pOPTmRSqgl9~DCP$pFuzFO< z&a|?OvNrIWarXd~gbnGia~fp#F3^&sWV(MDO=WL1%=R^p7S(J*B+;AC=<BwbG{>fX(-a#V2p8X6}*HVv>Z(1OsS$wt1Wbmd=CKNZ2 zyzwMFGa>txFcA&S>(xm{vVrwpON+LqmJAdjZB(2$xbN5Jo(rN`^y8VSvmYDLQnQ9z zpPlgi#`lHY@dCGu@W5#b$r~%e_oLY=tXH~y(pBFnl$>^Jd zz%NG3nQ||`IbT&Q7D1=k{0T?jgH&J50JxwRe<5Mqn_3H;-8RtD@0UQsSY?!)!8_O9 z)!QePXd8!gq8os_nS9EJ%p4^b&Kp3pajo5oZqmC+)|W^T=~5d-b1cUa@p0k4$-U2; zyk3jqZg2X?76@0ZG9}TfJ83~cmYvX(Em6+H``V2isT8>ALPn5bE;(J&GtC+fhvAcu zi@sG#ig@>Uk0dn%aL8ONLat5ir|H#&3!A9TrIsf~%C3=y;>7Xbs1@?eJcGKx(m^|gs z)Jf3*xFhKHH+JMbKypXI3#j_+dQ@aOcP6Lnqmpul@e0rw6>BvLQ)}bwd~%jeru~VT@d>I`U~L9snz9E-sT|jK16Xine{4_M0^@|Q zipx^jbT}j>UCHf_q>k3bVkgeIjb?1zHi<%jYtG(c{JE#yr^u3#@KTRKS$a5%iKKqa zc?0X=QH)#^EQ+b=@yv?=3tVHLK0jU)ya70~=Y#!{{1O_GG0pET!s$0<4*H*a7dl98 z@;>DgJ?M6<@62#6o)|SFG#|Cb)a6$ggIPy{9tdf$dI?HKQ1L#YQgwHl{Yl-IW}DG7 zw?!$chJ=YA$YoS2o2ilrwQ-OL;s3smwwud?;O4DWhGC`Q{yYZ!8rC8SYg2S zL46JsFGBrEtcZW?b(%6t9e(*4UYw54EzG11NuF_tnU`nXI(kfX)fvJ`^=YkWc>de)DPgT6QQP;8~@NtEyeXk&upKyyV zV}5E`4jyXpuqMqMeVau&Uy&;|h~ zy z^bJvj`VjSQCi!kl%g;A}$4be~lJ2RUw+Xz{({KA`k5SJzA=7^t@`*0SqVGLzGJ-(T z9x?hhvFZu9d>A_dhSac!KYC~RN8T^(;;ypjhhan@Rgcfw((hg9@}R9Q0$mXR*mRG5 zBYLQ-#5TF^ymn_&IX5qq`qQJ>gP))nkZ00cbzXV$3scSr2=Brw*0Up8f#EAZOl?SM zB;7RE&226QhY}9*(Yqd7`UO6Pe@nbSeiX6MWqQs1E%zEz+lJifR~1CmJIX9{$zse`0b~ zdH%3kI<6~l|LtVY)=P`oevX%Kgx6pqmzHy*d_?IOacT4SvyDKzE#_TO!I?wOo~pBX zfkG|`A4wH#z$Y#`jE$FEYUae<1sWADA1o-qT}8T@s?Z3jkT7zk-#W~k=Uz(YsnvfG9GrjPlEQ6{6W%| z*|b<6o47e#zL|(BL4s!axN<-6o#QMB?ks0{?Cz_TI_4W?XyD)F`6buwFz!ggQtc0; z0(TjjIJk!CDUKx^Mb0lbVFNKz-~8B>x?zX?CPyR79;#Vg{i+j_^r_6-VmRU&9(+ED zEC0OuwLVDUvR^)Y1O^YMr&YK2px7g{7K_=;xRtjIYZ$Xfw5|DbNsv~~d_UHW zMwEJkfSuj#CIGpRi+~akhzi#m_*tr>*}J0-!AKy)u5rW^{?e;x5s7{JStF|#-0_7G z(z50?bXu_>rWMdHpbw)D>b4&{fAyB>>Z$0RroMnsqXEvnXQBaZ%IZ9{YwjLik|~z4 z4`RLVdHUwT-vsBD2wbo|f)2xB>X##q0|BWo>%L_5sX%pz+7;76W(=e~?(@J3bwpv^-aq{s~T)fI@B&%ld&R89AVug zuljUn3g`DD4~rJHn|W!`5pcL98o%VUZH1dPB&SE&K{E5i{42cfJO4D5>%B;5j{99a z&q^&hs3Dn_-k2L1`bPA))r@qy3UMhff9YNyJ99f=Bv?aNMqE6nQ5K_A-kVHcsF82Y za%p-mfw{{owBiQfU^&615=zhXlRwDB_jtf)7-k32c&Z1Ftk#TDMZ2x#hVW<$ZH-;$ zHw}!Q$x8_;|fUzsV9 zBM#nRFLBxwZ!-NM)^iJ_40Jf}(2F`e!@c@+PgZ7qp=-W!?F8psbDa=$KypByf|ih; zNQ-+Wc>d#sGb>*wbQmPQydH@|ALf{FfSmEVqe>=5Wn*Wt7T1;= zD~u%=UMdoo?J3imxYsP-8xPXUsvjs~<;{gNbl%J+Q}Zw2dXjd_12S@;j1WwDq&jD% zALNo`wrjhjVju(PoLRgi7X)N&M?)}J)SqolIK+fz?UoUw20F%`fdRg3L>*Y0PzpHh zP}UR#ss#mMKFQDV2CuY0x2j|AB@LUBopW|gQ_CLnpm;U~xb!R0td-ieLP~M^ue=FV z!KS@GwNxYu8-#B4C-@iD9g4~I7A?|d$B6%7L0~Z3tRDQly1Vu*)XSC-l@r;Y@#X{k zK~FOv=^B23(^cubU-mWz>Mt2_`aMoZ*r_-1NanSGhPQXD zqsR9`z4{i!OkZ1%=~=9@dzE*#5**SQUgBJK_hh|{8b<_37b$a<5mJgO!p`z!Nidfse*Dt>M5V_jGDUYTrT<}U3yo8O_E|{Bc5P!H*4!N)z=(_vzjBeA8 z?W>!=Oi*0xF#PTW!H*DYL`}TiOXoN@4f6n-h_W-sI3UN9zcw`Z!{1FXl1W6DJ0L^3 zQEH!Uv`1D(xm=%%=t1K->yi>q4m-PmlE3QB{T*>tT64pxt>2?JNlvFiM_Ps)a0bJs zT!wi^Mc93+%|FSv-_&dy=4n%Gdr@+2aKl#V2wByIfOJ5u&f03VGBv5&fYKZY#HaK; z+uDGeEsHl&rAnEFu?ur6wVq9UJ^iH;Xn*fQfa zZQG*%TK@OqBl0+-W5-DLnYJJ{z<45OLeQE4MHt_kz0LX|@^-^H!=kd7pmSP9-Nb9% zBxW$x9JJ7l?DHhTI!J*Cto-$ddf za5gkO@E-(AJ$3{w>-jKQQGQ|`!Mw9JL$hCu!dPqUNCZ;$qV=9|TIo45(>^)7X%5p| z2$T}Efwpa3F1rtnqJ^<4m5HG(vrE%LI{vg-(s1$oKgpRpJ`j&^1krU8aO6t`JN*E^w6 zr1VPP7;{74hEmOz9!SiN{XovWwRNTq*Zq(>{P#U`a7|e>et*F_^YgF$z@46JPr zKVXAdKTW~D@5N4=8K;#m>%IY(+p^~OHM|g8=zFJxRji)}4=oE%;B@N^e7XW2E*J*Q zfpCZF6p(igp$z(m9gB3WcI%7ekDj&Urk%paAt19i-);KRGIHf-zNQ3`Z+1%fd!gx0 z_N=G~&UjRXz9_$YLS0H<^XCDqJgDDpXKcLVyIKg<_FH38;fbW0Qx@DGx^=aXc1|b1 z$M?f}nG*Xt9;6(eO`!uVn!FKZrZH(ENuTccD28vltV-3$?#ZRV!Qe3jL!_?J8?Zuz zfBH{c{cyTS71vK;&We{SWA#D{d=>kJ#d0jh-zSXe-t^tRm#Ap{^a3wEH8xbyO2&T7 z(cc+63=3ITdn0PD;4@?FMacO$L0Pc8O)bDwj&gDxR0FIu&vDIDs@jlXq&uf)%m#3o zTG7HHraTTXc}5GLdi^o3ai2PZd1Q3Rml8pz0Z*Dry(_2ff!nTUta&drT}@77)xbGF zBNh@p>#STFr9fXWdx}<=rL=sCPi8^lQYr<6r!;~O*s5cc2_C)2s}h&7?1?nY@jUbj z^kP3}Gaq`s;KnIrd5ScSs{KmmLR#d1{e0co5KlL6p7RP(X7h@Mo>FC}nPR{UI?Ir; z<}YwhZ`q|-+Z}42eVm<>iC9~0YP8$OIh%A&uK-0^Q;yEEKe)MHvAG2f0XaJd1Lssj?f;1<3qTVJ;G6xmqdgILPf zG7_Eaq+XY}Q;8clP;pGW4bju^2_Gl^cm2t$-Aq>;^c!^zT%oXG)~9C4zekzRVazk@ zb>6rsI{sgFMu#p5~@Bk0@wb$aj1bl<9qej=XO5M6nlZWmj5jyd zu`cNEp?G1vFa^(X7ej9NR+eiO5xd=&#!Zd`#O8@ijh?&ByU%@*u^$8CTs)pE>#rrb zjH10Jp!v%Vqa)(iJ^g#N5X)Kj-gdbmX@plZ(zP35@-+J6by92F#j_q7O+O>NQ_~?* z&yW0fC4PZ^E6xD2I`smBx^o3|7Y*%NN?e6w9>d4{T=O&S ztktH}Syj;ijVEvab?`mjkqZz?`*b!^BiDzXeyNA}a(uoXJOgS6-<=B>hr@K`PCDuE zQ(hT42E2MKMvqpx(Rv+CY8nu|O_c()mY`)N90JSxH#y_&mykM^#M^xI{3 zNs~VFqDUR7zaBO4Um|_Ok_rO3E|bkXtt%=7{+@_e&znn)to>hg164EJR?ajATsJ$r zZ5+5@k00^K;iU89Uiup6ug07FCKami4=Co?IF>=Gm^Y(D1@0D@=z=}4&N$sK?Fo*R z1|Qla>>)iz%KCdS8qulK8NQ3>xTtXPfrfv5|NSnDm+pt&A#P~>*zof?%>E4D%$z{C zud=+7^Xp>V6Oclv_P5&N><20&@6z5$*jTnvsdL88VEuGE=9pvjHJE;t4XOM(cexy! zTUoPENBXkxii-Hf2rO-4jq7X$pyGx@xR#8yFR=8xj4BRMQ$MuNEFa?*T{I1CLT8_^ z;}G~I1QCC#>FmTHmmLP0q2lTI@q>{6*Djgjv|BGNhf}wTy02w8FJII@%xInYVMoC_ zlZl{e(M-5)4-cS;Zs2=Dwf$M(-=@tdE(3M_KnS^d!w6*=zWN5xzTMI~oE~WBbLF2Y zNTWswN3(K^LUzw5*%6s9@rJ$08-un~itozYTT@X1^l+>;o|oc^7m}qq!-M~Yz7bZ?d*i;Qyk0w?5^Pyi43fTCz|8o-I>7c zPtwyG@pEJ=p}!O?(o07L<34-U4J#LARUVTq73PO-)W$n%4=U;JN%m8Bn1=j5^G_;E zfk;7MuSXxyP)R~87i*U4e^@o;WLn=D^um3AyP~;mL<6BD=3loQ`OWfP=I~)dGLGif zYz8Lq-<_Hi;v?Juh!iOmA@yS%@_*u2Wvo8kt3)k$|0w*PvR4VsHqb7lFnXA@(!`}R z`vn%M516#kiEYpi)GUK&g?BQEczd-Y!UH#|v=cQX3Tgioga6Xs&AkDvS`7ZNdu3Py zB2FC)TB91IiE5sG{_go~sxq7c6w=9hzUF7N(GsKoJ}j^%iWJG4VGp)dH|PaRLX7>Z zIxP^VH=;C>t13JBVj!--;UU60SBCprw`*pa z;v{G6#;YEUX@Zgp7Va94jlJ|97c(gFLKBKGVnxOyq) zC&^88&jR{=Q%7%y{rhvLon&6$#yP*|=y*w@3fdT}UQfN+SrAIV;E=6CW`mM66s7<+ z{ttXU`DwV=v38CFtFNgLIP>S9f>h^@iMcI=BKmS02a$3tYYHzvkswfj7UIsg3^my> z3|6FNf$Ba*GhjIl6g3!9r>MG?y<=CD5e9}_>XvF2MqS9;hL{;=EeOLc9dW zN&Sl$HVG{ofB%SX^{cI-{#O=AoQ+X?8*vd+RG>c(iNyPO@`%QSLSoR9XRw_miP^q0 z4Y-8yy0vNve=_cj%6x#f4m{7#dqX@Zh4+BU z<*aJdFlJmRV%GZdbdRG_c;<)c{l#E9U;J`&Vf>?~Hju3ZdqVnjqi4mh8OKY**QJKx zmY$brWwC!16%~!VI|5Xcf8h;E5z&dhN3gK44FCO8js2{frDsY?iUPYq{gT~)2Mjsj z|M@e%riI0topu_m6a@eYHE@`gW-p6QanPg>6DrEQ9X?8Y(V1APYA_tGY0ZQ zEm3Y=$=fqcVSGp!GX5cwIW)7GTEy!R=G{4L5C~&{QmhjiE5CiCAEMp+CKewlcCDR{ zUeC0$woYt1N^-Gp8a#-|WX@D8xIUNP>(4q8%a^E7Sz1~uLl0|96Y~?zudkoD zn5uK`ud1q2f?96*OUvc#H-tQ9>hJ68dtv=N_`$aaIk~yC)Ag>{VoP-Hkf*1oBG~## zl4X*a5z0690ptonPIZkNU=g@b=chDhCgn7jCZ7v8U1w+Rtb&3r++qvp;$%k|wVL(+BKjl$GtXEvb@{lh^C(1<$i0MWag$8sewlG3`maA8lyfiRrTW z{N>BiB#0t?U|F61HfCXAVdRvY!%&z4>YsR7ysGBuDd2i_?i$~;=%FhiA<_5%l3_R=qpBkRe)biR9UCQ z-)3UUYp_A+xvcainjf$9qb)FnnhU>v{UQOW=3!=Kjfsto&6a-}y_1=lc?>?db24fF zV5QF*K|z@Jx>$Rjf}GqObMy+7nnHm_+H|EQI(>^q+FgYN^Ji+RSJZh$!FoV~jck-N zQ6V_sF0rU)vWP{MZdH2v?2PZ#IqA++-9ciqe>1pG0=v>2M=V;t;h}nWkAcXz{8WTJ zLC^bOMNzY-oLKZ!d}VVpCGEfeIK>16>hhJ-MQ_~P+^&sV{BH@^V)2QH?3!J%@wg0a zg`*vKgB@cw$918hq5R0tSxxWO;m6@m_;kJfv!ue$mxr>XiKHWGm&dvDkH72J?b}Dm zxFmKiZczz1EwLkIN~Z-Rs+ZT+zV^T2Ogi$qk_LrxL`*B}a0}9|b;$7TOX3YPdcP+b zBu=kgs#RnzAtj|m5+cDzO-&uRg%xmKA-w2M72;PbXA(^p3(CwtudS^$chba{RCSGr zh$wjlLw?3pM7z$+%orM!yxUu7XHQ+-XkJ@eOJs~%Lj@Dlnc+f*Tazc1VPRpKt?)w^ z`alP5Hmv}er{@F^JpzG<>N1Hh)vM_j;^X7X$j@ii$QoKg^EAn_C-`k+vEIyVY;0(K z14l>4wDIwAb?+0x!N*)&T$Ln}82P7+%0kGp) zuKiwsn>z*<+Qr^bsup2b0)7tW)z>G9cpq;H^dxX3xMj$pdV71%M%w&GAzL<1&Rs-I zSEJ+E{_;?Ix3 z(+1CDo2*;`!uo9ipukyuAp*Bs=lS2qZ+!?n+8p+$VA zW@g8>c6P_PmR_$xibWtuQpC4MzxjRL0@yW9t)O!L*QfhVYr1;vI8f2|rgykwxQvX9 z#|U2Dz5RoO2L@e6y1If(0R4C&5CgJ>o zf-@i)+hw;D6cziDxy}2z0)9HIpC4hJ0aRxS3JW)e&-7l=!w50!E<2TqVId&}cl;-g zu%mYqSt}gofBk!Qc9tGa#k+wBZVj>byIf88TZxa4Cn|F-rYR^cb}9b;ecK_Nmm!eb zOHI^H4$>IL@sy;(H;Vt-F1vpw7f>^bRDQ0yHk)P)(21F;@RuAlH#C$JH zNhBXa{)L5wHOeXcZzJ(`|8;ggcs#00LeS2^%gbvQPjLJ8?J|s26*~60db6eM#Y(b0 z;mOV{$^<*7l*0c|Pfg7!-*#fDD+Wso#BBF>)IX>?)kQ3}Ve{%_Hr<*l(FIU7TTx-* zdA3g)?p-1embh+115($e-w)j+B_)F&uXKK)j}4Gj#073Kp7v#?m@78O~S zbeUMq%KIT-OVI_wCM&*F8{#pbJ@-O0X?b~gBsM2&N?SAH1 zQuQ^(9|G1PK+2#2vG);;*N4J>Ik>qU&pYtX7Sm zk@8yZj#HqcOZU6n@8L-Dxj3er_1!o|wUI=;8J=Lk4$bD|21eDh|`e03n5 zZ}J~^XJ_@5r6me(E-pd+hcE4Cg!x{-9yOo}Uui|>Df@}qP1V+a?@+>j>ymf(d_6C{ zn*Ei$y!?}q!9n`sqM{!AesMeuG;H`oGbt!8J2p*pQ%T7_vgUSdA#SBD=v6hPm~<7K zwz2MpA@g(O6Owdk<#UEh*vN_w0jntM*C@)!$lyUg92YxAN{#27>hg}feDc(bfRp&b zz`&rjWfUUPLd$7AQVfSqzn7P*GRMi`vMpn0Q8$A^?%N(u!aEuZ)bm6wyuW1T!-1B_ zv5`qPUmZ_sDTj3R^k5g4mRtgRmg7OH5RovaQ4=nVn^r`h*{C|nQJZ`6U-~Ne{@sl$ z0aaEucro7^eEHxB;=Q_x%G}z$}Q+dPYV@l%%9bgT3=qNx3-? z6ih^;2HdKPqKV0pN0sh$?VF!CW;!~F*yF9qus%$Y3bU24hzKbJ>FDTOoA;;4HUq*t zJwHE>YHx2ZL0v3%MlwBRV)8Q$bAHasn$_LY(>GJ814`+zH0-Om}JXIZK9Z;%g%xOhE3s^P`ZZl8jfu*L^)b)>(DQ$Us<7Lc%DbTwDKe zyJ2Uh5^e@mbI1ZQ)#%A>ak@M2UHsg8XGY{4TZ(c6f_HH64v@ooOmwu?`qBA$^%(^r z-W>>la;*9l{Wsn12p4gdI6)p>-g&dT%D8v|b-;Prn8rih35)b=;ROCRV~~8W0e0;_c~~YL^_Jmex!FZ724i zOu8P?CFT0*H+i{ciqqGU(9xmB^hNb3cq|5QZr{1nWIDeq)^6@6r(ZuxeTA&MG$157!RHuAMyE2-$rKT==5T5gLw zI5@nwv-_S6454(D?$(V})r6v4&JS0^^+wMF6JgpNugN}lQ?BusWMP^^(eZ$@Ru&fa zWp#AMABTTaD>(dPRYg~=7#0z6oXBH2yw;C%d~|eVQm)NsNBhh3z%)24)VeMxt4&R*JIc@dU~m9fkv<( zD>@Spr(U9hndM|9aNsB$|Mr=ChkpL4HO9?-d84$XkWg6#fAFrNR=|@wPR{vs&>!5fOWAm&uN& zmlykpRTYBr8b{c$iqow3D=8U~d0j(8*j*y(TI;3XQP<$0yF5n?J|b5{ALcxhk8i_) zvh(sH9}#?-eL%^z6hd4#I5_wm5WA1D%x|*_AbZqfULo&}HZWFzdRti9oaP-1bq;9D72q#4(nZ(my_eV1tp65)R#XC?^*s^yGlE1 zfD8V#w6qvZhY*ibFi1d=OuR9mM-4pg1Zy*fW^MyEt7uh5@bT5eq@|@981jf&41OyI z6zyhWl6{{c@Hq^>dDw!-AT5&5>QDZ2&|HLM7=yxdfXCOa3K1sKr-EP)W2`By(ZhiI>s^Hc-IaQ7IhMndj#OrGA7`P%;bZl(p+{F&6 zhJRpCx9D{#S)m4teCzKJwE~TYWMs#=Ir}yUZ@qL;pA}r_{ngRvUz1aOOjj8Q>&9S? zXh5+q=j7z@K;o{hyglvh%P%riPE!HS^T50B11Qs0c^z#~C@U*hh^RsSb5wI zjWz@UmoB7OI?7S?mvb<_l!jJTh2ljR-NyKFJ)k$%_KkZ%;{}q79pT9vYiqR?w*QFZ zzqJ|vAz9{o<@KRtrEh!~?9Fzi$P`J%%MzDsSrIM*wE#`)+&#No=-=LajQkoE6;**A z9!{gM>`mg8&mF~11An9&e8?DO;VpgCW%46B!O*K~bJ`qYejEt2 z)%%`6!>|T*(b13GA$ri~U#DV*N{z%s0)FPBEqG9gEt4~U3e?e{lQ4k%@7yQza2ss( zfIlY*-Te3wrvfd6&#*b&;C`|OCv0k;ofVt;`7^@?cpb9Wp)llExg4jH)4fI8YWYa5 zqAQ1vFO+0?Q3|PonyZ_e?=!(9A^7t0lBl+!fo^BAW`~auZ!st!pw%Ez;n>HeVN z6u3Ikh|GL?clQ$xg8TP*TZ4&-U7Vb}p0TsrRj3sR_pTne6MXvR2?(dObrG+uiYX7h z&ehY?<9Kpz{vdsT7{?zD<-0mNTVG|70IX10*3e{eZCN=Joy zdfsq;KR-QX8*lQi0YeHgT~*b$$NgIsR>Rq{y#UoBt|9_@lMU`1aj~&a*Vty$M7(Y4 z-%5eEBx*98o#*sBKU`-c3T}<*S-u41BAc3;dIrtSZEf`_Dl41oZTt&540c(7Ha90H z!Pumv4SKRL4yCW0|CXZHsmWjHO4P&x{vE-EfH5t3@W#~i*@4l`d4iyjP_nct2|46J z=1VU+_ftDvAD_7YOE5Q){)loY`f-kSI}6(NE4Ry_Tq~ z`$!r!mN=m~$5+PA^YimNDkQFcfC8f+=;h0of_3_a@Hn|huvT?5Ut?oqCgi_%bk4LQ z!>(6dtgNlAO(?0_>M1-E=x#aOGECnIah60e2C4wc5rf1*Ya$Ez(+IiYdkD{vrYfDN zy~a}V+36EO|AE2{dnFtt6Z`88=XP*VP_yIV+R%vR6F}Ia0K5mMryf)61g^hZTNmGX zOgaCktE{Yi2z;}jZL$>pTPbO2REJqb=}U?)*pQGAv}KYKE~KWd&0of}Al}6G;>Fg$ zu7_AtFm|NmKt@*9an-5r1k4JHH>_xYYl+8&?&0H?)v_$LlN%9!V=lb@L~f|PzZfaz zRcg?HFoef!eY33B${K2&-gAP|8YapEILNKHkBer>eWs13NGSEv1@|S5B<3Sc$%p~ieJqT=_*RRQqxc3-dmXew}06ew5-}RY!*ysZ$#UvAe1wpn) zA>E_|?RTW5JN-ZsHWQ~3sg6W}oCq*8D5w^zgr%%fO7izn2PpuF0N(GJ->l}PUW}G^KQGiDc>3IB*1>s$P zGos*wikKL5;w@Y}?WR_F&=xU5|7B-q->Z&fLj%%W<-PI-g| z%o#H>HjbQKMJ~(nW&y+)cAd(jO|l&`gLp{D1pw- z&XX#4HhfnWe*RO}M1^YSotdVAt5AFs?@ymTl?Vk4qzG_RLsW|J7>v<;=zr5=dqM$2 z++Sb|zvmscm6H*QWRD+nlR_JyJ9P5y`~*WbA>lEp3ws`XUlq)3Wn_LcdUpnZr*I}F pX) Date: Sat, 5 Mar 2016 13:52:12 +0000 Subject: [PATCH 7/8] Improve entropy-based crop docs based on feedback. Fix includes to keep MSVC compiler happy. Additional memory leak suppressions for latest V8. --- docs/api.md | 11 +++++++---- docs/changelog.md | 2 +- src/operations.cc | 2 ++ src/operations.h | 1 + test/leak/sharp.supp | 37 ++++++++++++++++++++++++++++++++++++- 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/docs/api.md b/docs/api.md index fd0cab2f..e422e012 100644 --- a/docs/api.md +++ b/docs/api.md @@ -138,11 +138,14 @@ Possible attributes of `sharp.gravity` are `north`, `northeast`, `east`, `southeast`, `south`, `southwest`, `west`, `northwest`, `center` and `centre`. -`sharp.strategy` currently contains only the experimental `entropy`, -which will retain the part of the image with the highest -[Shannon entropy](https://en.wikipedia.org/wiki/Entropy_%28information_theory%29) value. +Possible attributes of the experimental `sharp.strategy` are: -The default option is a `center`/`centre` gravity. +* `entropy`: resize so one dimension is at its target size +then repeatedly remove pixels from the edge with the lowest +[Shannon entropy](https://en.wikipedia.org/wiki/Entropy_%28information_theory%29) +until it too reaches the target size. + +The default crop option is a `center`/`centre` gravity. ```javascript var transformer = sharp() diff --git a/docs/changelog.md b/docs/changelog.md index 866114d4..09ea8ec4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -14,7 +14,7 @@ [#239](https://github.com/lovell/sharp/issues/239) [@chrisriley](https://github.com/chrisriley) -* Add entropy-based strategy to determine crop region. +* Add entropy-based crop strategy to remove least interesting edges. [#295](https://github.com/lovell/sharp/issues/295) [@rightaway](https://github.com/rightaway) diff --git a/src/operations.cc b/src/operations.cc index ad9f8406..555f183d 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -1,3 +1,5 @@ +#include +#include #include #include "common.h" diff --git a/src/operations.h b/src/operations.h index 5ece5743..48b14834 100644 --- a/src/operations.h +++ b/src/operations.h @@ -1,6 +1,7 @@ #ifndef SRC_OPERATIONS_H_ #define SRC_OPERATIONS_H_ +#include #include using vips::VImage; diff --git a/test/leak/sharp.supp b/test/leak/sharp.supp index bf224789..680ca215 100644 --- a/test/leak/sharp.supp +++ b/test/leak/sharp.supp @@ -248,6 +248,34 @@ ... fun:_ZN2v88internal12_GLOBAL__N_117CreateICUCollatorEPNS0_7IsolateERKN6icu_556LocaleENS0_6HandleINS0_8JSObjectEEE } +{ + leak_v8_CallInterfaceDescriptorData + Memcheck:Leak + match-leak-kinds: possible + ... + fun:_ZN2v88internal27CallInterfaceDescriptorData26InitializePlatformSpecificEiPNS0_8RegisterEPNS0_27PlatformInterfaceDescriptorE +} +{ + leak_v8_InitializePlatformSpecific14 + Memcheck:Leak + match-leak-kinds: possible + ... + fun:_ZN2v88internal14LoadDescriptor26InitializePlatformSpecificEPNS0_27CallInterfaceDescriptorDataE +} +{ + leak_v8_InitializePlatformSpecific15 + Memcheck:Leak + match-leak-kinds: possible + ... + fun:_ZN2v88internal15StoreDescriptor26InitializePlatformSpecificEPNS0_27CallInterfaceDescriptorDataE +} +{ + leak_v8_Malloced + Memcheck:Leak + match-leak-kinds: possible + ... + fun:_ZN2v88internal8Malloced3NewEm +} # vips__init warnings { @@ -279,7 +307,7 @@ fun:vips__magick_read_header } { - cond_magick_is_palette_image + cond_magick_is_palette_image_get_bands Memcheck:Cond fun:IsPaletteImage ... @@ -292,6 +320,13 @@ ... fun:get_bands } +{ + cond_magick_is_palette_image_parse_header + Memcheck:Cond + fun:IsPaletteImage + ... + fun:parse_header +} # glib g_file_read_link # https://github.com/GNOME/glib/commit/49a5d0f6f2aed99cd78f25655f137f4448e47d92 From 5b4f4b06728a5261d29c0f460bc95fddf1550d5e Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Tue, 22 Mar 2016 09:25:29 +0000 Subject: [PATCH 8/8] Upgrade to libvips v8.2.3 ahead of sharp v0.14.0 --- binding.gyp | 2 +- package.json | 4 ++-- packaging/arm/build.sh | 6 +++--- packaging/build.sh | 10 ++++++---- packaging/lin/Dockerfile | 6 +++--- packaging/win/Dockerfile | 8 +++++--- 6 files changed, 20 insertions(+), 16 deletions(-) diff --git a/binding.gyp b/binding.gyp index e561c761..8f87dd13 100644 --- a/binding.gyp +++ b/binding.gyp @@ -221,7 +221,7 @@ '<(module_root_dir)/lib/libintl-8.dll', '<(module_root_dir)/lib/libjpeg-62.dll', '<(module_root_dir)/lib/liblcms2-2.dll', - '<(module_root_dir)/lib/libopenjpeg-1.dll', + '<(module_root_dir)/lib/libopenjp2.dll', '<(module_root_dir)/lib/libopenslide-0.dll', '<(module_root_dir)/lib/libpango-1.0-0.dll', '<(module_root_dir)/lib/libpangocairo-1.0-0.dll', diff --git a/package.json b/package.json index de2aa324..3dd1b01c 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "vips" ], "dependencies": { - "bluebird": "^3.3.3", + "bluebird": "^3.3.4", "color": "^0.11.1", "nan": "^2.2.0", "semver": "^5.1.0", @@ -68,7 +68,7 @@ }, "license": "Apache-2.0", "config": { - "libvips": "8.2.2" + "libvips": "8.2.3" }, "engines": { "node": ">=0.10" diff --git a/packaging/arm/build.sh b/packaging/arm/build.sh index e677dab9..3b1e3b1a 100755 --- a/packaging/arm/build.sh +++ b/packaging/arm/build.sh @@ -19,7 +19,7 @@ export CXXFLAGS="-O3" # Dependency version numbers VERSION_ZLIB=1.2.8 VERSION_FFI=3.2.1 -VERSION_GLIB=2.47.5 +VERSION_GLIB=2.47.6 VERSION_XML2=2.9.3 VERSION_GSF=1.14.34 VERSION_EXIF=0.6.21 @@ -29,8 +29,8 @@ VERSION_JPEG=1.4.2 VERSION_PNG16=1.6.21 VERSION_WEBP=0.5.0 VERSION_TIFF=4.0.6 -VERSION_ORC=0.4.24 -VERSION_VIPS=8.2.2 +VERSION_ORC=0.4.25 +VERSION_VIPS=8.2.3 mkdir ${DEPS}/zlib curl -Ls http://zlib.net/zlib-${VERSION_ZLIB}.tar.xz | tar xJC ${DEPS}/zlib --strip-components=1 diff --git a/packaging/build.sh b/packaging/build.sh index 24f8f849..2d8d619a 100755 --- a/packaging/build.sh +++ b/packaging/build.sh @@ -1,5 +1,7 @@ #!/bin/sh +VERSION_VIPS=8.2.3 + # Is docker available? if ! type docker >/dev/null; then @@ -13,15 +15,15 @@ fi docker build -t vips-dev-win win WIN_CONTAINER_ID=$(docker run -d vips-dev-win) -docker cp $WIN_CONTAINER_ID:/libvips-8.2.2-win.tar.gz . -docker rm $WIN_CONTAINER_ID +docker cp "${WIN_CONTAINER_ID}:/libvips-${VERSION_VIPS}-win.tar.gz" . +docker rm "${WIN_CONTAINER_ID}" # Linux docker build -t vips-dev-lin lin LIN_CONTAINER_ID=$(docker run -d vips-dev-lin) -docker cp $LIN_CONTAINER_ID:/libvips-8.2.2-lin.tar.gz . -docker rm $LIN_CONTAINER_ID +docker cp "${LIN_CONTAINER_ID}:/libvips-${VERSION_VIPS}-lin.tar.gz" . +docker rm "${LIN_CONTAINER_ID}" # Checksums diff --git a/packaging/lin/Dockerfile b/packaging/lin/Dockerfile index 7c19d927..c057e4a2 100644 --- a/packaging/lin/Dockerfile +++ b/packaging/lin/Dockerfile @@ -20,7 +20,7 @@ ENV PKG_CONFIG_PATH=${PKG_CONFIG_PATH}:${TARGET}/lib/pkgconfig \ # Dependency version numbers ENV VERSION_ZLIB=1.2.8 \ VERSION_FFI=3.2.1 \ - VERSION_GLIB=2.47.5 \ + VERSION_GLIB=2.47.6 \ VERSION_XML2=2.9.3 \ VERSION_GSF=1.14.34 \ VERSION_EXIF=0.6.21 \ @@ -30,8 +30,8 @@ ENV VERSION_ZLIB=1.2.8 \ VERSION_PNG16=1.6.21 \ VERSION_WEBP=0.5.0 \ VERSION_TIFF=4.0.6 \ - VERSION_ORC=0.4.24 \ - VERSION_VIPS=8.2.2 + VERSION_ORC=0.4.25 \ + VERSION_VIPS=8.2.3 RUN mkdir ${DEPS}/zlib RUN curl -Ls http://zlib.net/zlib-${VERSION_ZLIB}.tar.xz | tar xJC ${DEPS}/zlib --strip-components=1 diff --git a/packaging/win/Dockerfile b/packaging/win/Dockerfile index 74ddc3d6..07c49c9c 100644 --- a/packaging/win/Dockerfile +++ b/packaging/win/Dockerfile @@ -3,11 +3,13 @@ MAINTAINER Lovell Fuller RUN apt-get update && apt-get install -y curl zip +ENV VERSION_VIPS=8.2.3 + # Fetch and unzip RUN mkdir /vips WORKDIR /vips -RUN curl -O http://www.vips.ecs.soton.ac.uk/supported/8.2/win32/vips-dev-w64-8.2.2.zip -RUN unzip vips-dev-w64-8.2.2.zip +RUN curl -O http://www.vips.ecs.soton.ac.uk/supported/8.2/win32/vips-dev-w64-8.2.zip +RUN unzip vips-dev-w64-8.2.zip # Clean and zip WORKDIR /vips/vips-dev-8.2 @@ -15,4 +17,4 @@ RUN rm bin/libvipsCC-42.dll bin/libvips-cpp-42.dll bin/libgsf-win32-1-114.dll bi RUN cp bin/*.dll lib/ RUN cp -r lib64/* lib/ -RUN GZIP=-9 tar czf /libvips-8.2.2-win.tar.gz include lib/glib-2.0 lib/libvips.lib lib/libglib-2.0.lib lib/libgobject-2.0.lib lib/*.dll +RUN GZIP=-9 tar czf /libvips-${VERSION_VIPS}-win.tar.gz include lib/glib-2.0 lib/libvips.lib lib/libglib-2.0.lib lib/libgobject-2.0.lib lib/*.dll