Add top/left offset support to overlayWith operation (#473)

This commit is contained in:
Rahul Nanwani 2016-07-05 14:42:02 +05:30 committed by Lovell Fuller
parent a5d85b8a54
commit 278273b5c3
15 changed files with 338 additions and 41 deletions

View File

@ -93,6 +93,8 @@ var Sharp = function(input, options) {
overlayFileIn: '',
overlayBufferIn: null,
overlayGravity: 0,
overlayXOffset : -1,
overlayYOffset : -1,
overlayTile: false,
overlayCutout: false,
// output options
@ -357,35 +359,61 @@ Sharp.prototype.overlayWith = function(overlay, options) {
throw new Error('Unsupported overlay ' + typeof overlay);
}
if (isObject(options)) {
if (typeof options.tile === 'undefined') {
this.options.overlayTile = false;
if(isDefined(options.tile)) {
setTileOption(options.tile, this.options);
}
else if (isBoolean(options.tile)) {
this.options.overlayTile = options.tile;
} else {
throw new Error('Invalid Value for tile ' + options.tile + ' Only Boolean Values allowed for overlay.tile.');
if(isDefined(options.cutout)) {
setCutoutOption(options.cutout, this.options);
}
if (typeof options.cutout === 'undefined') {
this.options.overlayCutout = false;
if(isDefined(options.left) || isDefined(options.top)) {
setOffsetOption(options.top, options.left, this.options);
}
else if (isBoolean(options.cutout)) {
this.options.overlayCutout = options.cutout;
} else {
throw new Error('Invalid Value for cutout ' + options.cutout + ' Only Boolean Values allowed for overlay.cutout.');
}
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);
if (isDefined(options.gravity)) {
setGravityOption(options.gravity, this.options);
}
}
return this;
};
/*
Supporting functions for overlayWith
*/
function setTileOption(tile, options) {
if(isBoolean(tile)) {
options.overlayTile = tile;
} else {
throw new Error('Invalid Value for tile ' + tile + ' Only Boolean Values allowed for overlay.tile.');
}
}
function setCutoutOption(cutout, options) {
if(isBoolean(cutout)) {
options.overlayCutout = cutout;
} else {
throw new Error('Invalid Value for cutout ' + cutout + ' Only Boolean Values allowed for overlay.cutout.');
}
}
function setOffsetOption(top, left, options) {
if(isInteger(left) && left >= 0 && isInteger(top) && top >= 0) {
options.overlayXOffset = left;
options.overlayYOffset = top;
} else {
throw new Error('Unsupported top and/or left offset values');
}
}
function setGravityOption(gravity, options) {
if(isInteger(gravity) && inRange(gravity, 0, 8)) {
options.overlayGravity = gravity;
} else if (isString(gravity) && isInteger(module.exports.gravity[gravity])) {
options.overlayGravity = module.exports.gravity[gravity];
} else {
throw new Error('Unsupported overlay gravity ' + gravity);
}
}
/*
Rotate output image by 0, 90, 180 or 270 degrees
Auto-rotation based on the EXIF Orientation tag is represented by an angle of -1

View File

@ -277,6 +277,40 @@ namespace sharp {
return std::make_tuple(left, top);
}
/*
Calculate the (left, top) coordinates of the output image
within the input image, applying the given x and y offsets.
*/
std::tuple<int, int> CalculateCrop(int const inWidth, int const inHeight,
int const outWidth, int const outHeight, int const x, int const y) {
// default values
int left = 0;
int top = 0;
// assign only if valid
if(x >= 0 && x < (inWidth - outWidth)) {
left = x;
} else if(x >= (inWidth - outWidth)) {
left = inWidth - outWidth;
}
if(y >= 0 && y < (inHeight - outHeight)) {
top = y;
} else if(x >= (inHeight - outHeight)) {
top = inHeight - outHeight;
}
// the resulting left and top could have been outside the image after calculation from bottom/right edges
if(left < 0) {
left = 0;
}
if(top < 0) {
top = 0;
}
return std::make_tuple(left, top);
}
/*
Return the image alpha maximum. Useful for combining alpha bands. scRGB
images are 0 - 1 for image data, but the alpha is 0 - 255.

View File

@ -108,8 +108,16 @@ namespace sharp {
std::tuple<int, int> CalculateCrop(int const inWidth, int const inHeight,
int const outWidth, int const outHeight, int const gravity);
/*
Calculate the (left, top) coordinates of the output image
within the input image, applying the given x and y offsets of the output image.
*/
std::tuple<int, int> CalculateCrop(int const inWidth, int const inHeight,
int const outWidth, int const outHeight, int const x, int const y);
int MaximumImageAlpha(VipsInterpretation interpretation);
} // namespace sharp
#endif // SRC_COMMON_H_

View File

@ -16,6 +16,49 @@ namespace sharp {
Assumes alpha channels are already premultiplied and will be unpremultiplied after.
*/
VImage Composite(VImage src, VImage dst, const int gravity) {
if(IsInputValidForComposition(src, dst)) {
// 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<double> 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)
);
}
return CompositeImage(src, dst);
}
// If the input was not valid for composition the return the input image itself
return dst;
}
VImage Composite(VImage src, VImage dst, const int x, const int y) {
if(IsInputValidForComposition(src, dst)) {
// 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(), x, y);
// Embed onto transparent background
std::vector<double> 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)
);
}
return CompositeImage(src, dst);
}
// If the input was not valid for composition the return the input image itself
return dst;
}
bool IsInputValidForComposition(VImage src, VImage dst) {
using sharp::CalculateCrop;
using sharp::HasAlpha;
@ -29,20 +72,10 @@ namespace sharp {
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<double> 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)
);
}
return true;
}
VImage CompositeImage(VImage src, VImage dst) {
// 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);

View File

@ -16,8 +16,24 @@ namespace sharp {
VImage Composite(VImage src, VImage dst, const int gravity);
/*
Cutout src over dst with given gravity.
Alpha composite src over dst with given x and y offsets.
Assumes alpha channels are already premultiplied and will be unpremultiplied after.
*/
VImage Composite(VImage src, VImage dst, const int x, const int y);
/*
Check if the src and dst Images for composition operation are valid
*/
bool IsInputValidForComposition(VImage src, VImage dst);
/*
Given a valid src and dst, returns the composite of the two images
*/
VImage CompositeImage(VImage src, VImage dst);
/*
Cutout src over dst with given gravity.
*/
VImage Cutout(VImage src, VImage dst, const int gravity);
/*

View File

@ -703,10 +703,19 @@ class PipelineWorker : public AsyncWorker {
int left;
int top;
overlayImage = overlayImage.replicate(across, down);
// the overlayGravity will now be used to CalculateCrop for extract_area
std::tie(left, top) = CalculateCrop(
overlayImage.width(), overlayImage.height(), image.width(), image.height(), baton->overlayGravity
);
if(baton->overlayXOffset >= 0 && baton->overlayYOffset >= 0) {
// the overlayX/YOffsets will now be used to CalculateCrop for extract_area
std::tie(left, top) = CalculateCrop(
overlayImage.width(), overlayImage.height(), image.width(), image.height(),
baton->overlayXOffset, baton->overlayYOffset
);
} else {
// the overlayGravity will now be used to CalculateCrop for extract_area
std::tie(left, top) = CalculateCrop(
overlayImage.width(), overlayImage.height(), image.width(), image.height(), baton->overlayGravity
);
}
overlayImage = overlayImage.extract_area(
left, top, image.width(), image.height()
);
@ -720,8 +729,13 @@ class PipelineWorker : public AsyncWorker {
} else {
// Ensure overlay is premultiplied sRGB
overlayImage = overlayImage.colourspace(VIPS_INTERPRETATION_sRGB).premultiply();
// Composite images with given gravity
image = Composite(overlayImage, image, baton->overlayGravity);
if(baton->overlayXOffset >= 0 && baton->overlayYOffset >= 0) {
// Composite images with given offsets
image = Composite(overlayImage, image, baton->overlayXOffset, baton->overlayYOffset);
} else {
// Composite images with given gravity
image = Composite(overlayImage, image, baton->overlayGravity);
}
}
}
@ -1105,6 +1119,8 @@ NAN_METHOD(pipeline) {
baton->overlayBufferIn = node::Buffer::Data(overlayBufferIn);
}
baton->overlayGravity = attrAs<int32_t>(options, "overlayGravity");
baton->overlayXOffset = attrAs<int32_t>(options, "overlayXOffset");
baton->overlayYOffset = attrAs<int32_t>(options, "overlayYOffset");
baton->overlayTile = attrAs<bool>(options, "overlayTile");
baton->overlayCutout = attrAs<bool>(options, "overlayCutout");
// Resize options

View File

@ -35,6 +35,8 @@ struct PipelineBaton {
char *overlayBufferIn;
size_t overlayBufferInLength;
int overlayGravity;
int overlayXOffset;
int overlayYOffset;
bool overlayTile;
bool overlayCutout;
int topOffsetPre;
@ -107,6 +109,8 @@ struct PipelineBaton {
bufferOutLength(0),
overlayBufferInLength(0),
overlayGravity(0),
overlayXOffset(-1),
overlayYOffset(-1),
overlayTile(false),
overlayCutout(false),
topOffsetPre(-1),

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -264,6 +264,164 @@ describe('Overlays', function() {
});
});
describe("Overlay with top-left offsets", function() {
it('Overlay with 10px top & 10px left offsets', function(done) {
var expected = fixtures.expected('overlay-valid-offsets-10-10.jpg');
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
top: 10,
left: 10
})
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
it('Overlay with 100px top & 300px left offsets', function(done) {
var expected = fixtures.expected('overlay-valid-offsets-100-300.jpg');
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
top: 100,
left: 300
})
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
it('Overlay with only top offset', function() {
assert.throws(function() {
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
top: 1000
});
});
});
it('Overlay with only left offset', function() {
assert.throws(function() {
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
left: 1000
});
});
});
it('Overlay with negative offsets', function() {
assert.throws(function() {
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
top: -1000,
left: -1000
});
});
});
it('Overlay with 0 offset', function(done) {
var expected = fixtures.expected('overlay-offset-0.jpg');
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
top: 0,
left: 0
})
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
it('Overlay with offset and gravity', function(done) {
var expected = fixtures.expected('overlay-offset-with-gravity.jpg');
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
left: 10,
top: 10,
gravity : 4
})
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
it('Overlay with offset and gravity and tile', function(done) {
var expected = fixtures.expected('overlay-offset-with-gravity-tile.jpg');
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
left: 10,
top: 10,
gravity : 4,
tile: true
})
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
it('Overlay with offset and tile', function(done) {
var expected = fixtures.expected('overlay-offset-with-tile.jpg');
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
left: 10,
top: 10,
tile: true
})
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
it('Overlay with very large offset', function(done) {
var expected = fixtures.expected('overlay-very-large-offset.jpg');
sharp(fixtures.inputJpg)
.resize(400)
.overlayWith(fixtures.inputPngWithTransparency16bit, {
left: 1000000,
top: 100000
})
.toBuffer(function(err, data, info) {
if (err) throw err;
assert.strictEqual('jpeg', info.format);
assert.strictEqual(3, info.channels);
fixtures.assertSimilar(expected, data, done);
});
});
});
it('With tile enabled and image rotated 90 degrees', function(done) {
var expected = fixtures.expected('overlay-tile-rotated90.jpg');
sharp(fixtures.inputJpg)