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 00000000..800f32e9 Binary files /dev/null and b/test/fixtures/expected/extend-equal.jpg differ diff --git a/test/fixtures/expected/extend-unequal.png b/test/fixtures/expected/extend-unequal.png new file mode 100644 index 00000000..1454f016 Binary files /dev/null and b/test/fixtures/expected/extend-unequal.png differ 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}); + }); + }); + +});