Add ability to extend (pad) the edges of an image

This commit is contained in:
Lovell Fuller 2016-03-03 09:18:11 +00:00
parent 86815bc9c4
commit f950294f70
8 changed files with 139 additions and 1 deletions

View File

@ -280,7 +280,7 @@ sharp(input)
#### background(rgba) #### 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. `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`. 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() #### negate()
Produces the "negative" of the image. White => Black, Black => White, Blue => Yellow, etc. Produces the "negative" of the image. White => Black, Black => White, Blue => Yellow, etc.

View File

@ -2,6 +2,10 @@
### v0.14 - "*needle*" ### 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. * Improvements to overlayWith: differing sizes/formats, gravity, buffer input.
[#239](https://github.com/lovell/sharp/issues/239) [#239](https://github.com/lovell/sharp/issues/239)
[@chrisriley](https://github.com/chrisriley) [@chrisriley](https://github.com/chrisriley)

View File

@ -69,6 +69,10 @@ var Sharp = function(input, options) {
rotateBeforePreExtract: false, rotateBeforePreExtract: false,
flip: false, flip: false,
flop: false, flop: false,
extendTop: 0,
extendBottom: 0,
extendLeft: 0,
extendRight: 0,
withoutEnlargement: false, withoutEnlargement: false,
interpolator: 'bicubic', interpolator: 'bicubic',
// operations // operations
@ -650,6 +654,32 @@ Sharp.prototype.tile = function(size, overlap) {
return this; 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) { Sharp.prototype.resize = function(width, height) {
if (!width) { if (!width) {
this.options.width = -1; this.options.width = -1;

View File

@ -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<double> 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 // Threshold - must happen before blurring, due to the utility of blurring after thresholding
if (shouldThreshold) { if (shouldThreshold) {
image = image.colourspace(VIPS_INTERPRETATION_B_W) >= baton->threshold; image = image.colourspace(VIPS_INTERPRETATION_B_W) >= baton->threshold;
@ -991,6 +1012,10 @@ NAN_METHOD(pipeline) {
baton->rotateBeforePreExtract = attrAs<bool>(options, "rotateBeforePreExtract"); baton->rotateBeforePreExtract = attrAs<bool>(options, "rotateBeforePreExtract");
baton->flip = attrAs<bool>(options, "flip"); baton->flip = attrAs<bool>(options, "flip");
baton->flop = attrAs<bool>(options, "flop"); baton->flop = attrAs<bool>(options, "flop");
baton->extendTop = attrAs<int32_t>(options, "extendTop");
baton->extendBottom = attrAs<int32_t>(options, "extendBottom");
baton->extendLeft = attrAs<int32_t>(options, "extendLeft");
baton->extendRight = attrAs<int32_t>(options, "extendRight");
// Output options // Output options
baton->progressive = attrAs<bool>(options, "progressive"); baton->progressive = attrAs<bool>(options, "progressive");
baton->quality = attrAs<int32_t>(options, "quality"); baton->quality = attrAs<int32_t>(options, "quality");

View File

@ -60,6 +60,10 @@ struct PipelineBaton {
bool rotateBeforePreExtract; bool rotateBeforePreExtract;
bool flip; bool flip;
bool flop; bool flop;
int extendTop;
int extendBottom;
int extendLeft;
int extendRight;
bool progressive; bool progressive;
bool withoutEnlargement; bool withoutEnlargement;
VipsAccess accessMethod; VipsAccess accessMethod;
@ -106,6 +110,10 @@ struct PipelineBaton {
angle(0), angle(0),
flip(false), flip(false),
flop(false), flop(false),
extendTop(0),
extendBottom(0),
extendLeft(0),
extendRight(0),
progressive(false), progressive(false),
withoutEnlargement(false), withoutEnlargement(false),
quality(80), quality(80),

BIN
test/fixtures/expected/extend-equal.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

52
test/unit/extend.js Normal file
View File

@ -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});
});
});
});