From d92ea31858c4ec323b29515ce15b2b81bb84b243 Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Mon, 29 Feb 2016 14:30:29 +0000 Subject: [PATCH] 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); + }); + }); + }); + }); + });