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