Add support to extend for extendWith, allows copy/mirror/repeat (#3556)
@ -197,6 +197,7 @@ const Sharp = function (input, options) {
|
||||
extendLeft: 0,
|
||||
extendRight: 0,
|
||||
extendBackground: [0, 0, 0, 255],
|
||||
extendWith: 'background',
|
||||
withoutEnlargement: false,
|
||||
withoutReduction: false,
|
||||
affineMatrix: [],
|
||||
|
6
lib/index.d.ts
vendored
@ -755,7 +755,7 @@ declare namespace sharp {
|
||||
resize(options: ResizeOptions): Sharp;
|
||||
|
||||
/**
|
||||
* Extends/pads the edges of the image with the provided background colour.
|
||||
* Extends/pads the edges of the image with either the provided background colour or pixels derived from the image.
|
||||
* This operation will always occur after resizing and extraction, if any.
|
||||
* @param extend single pixel count to add to all edges or an Object with per-edge counts
|
||||
* @throws {Error} Invalid parameters
|
||||
@ -1245,6 +1245,8 @@ declare namespace sharp {
|
||||
sigma?: number | undefined;
|
||||
}
|
||||
|
||||
type ExtendWith = 'background' | 'copy' | 'repeat' | 'mirror';
|
||||
|
||||
interface ExtendOptions {
|
||||
/** single pixel count to top edge (optional, default 0) */
|
||||
top?: number | undefined;
|
||||
@ -1256,6 +1258,8 @@ declare namespace sharp {
|
||||
right?: number | undefined;
|
||||
/** background colour, parsed by the color module, defaults to black without transparency. (optional, default {r:0,g:0,b:0,alpha:1}) */
|
||||
background?: Color | undefined;
|
||||
/** how the extension is done, one of: "background", "copy", "repeat", "mirror" (optional, default `'background'`) */
|
||||
extendWith?: ExtendWith | undefined;
|
||||
}
|
||||
|
||||
interface TrimOptions {
|
||||
|
@ -36,6 +36,18 @@ const position = {
|
||||
'left top': 8
|
||||
};
|
||||
|
||||
/**
|
||||
* How to extend the image.
|
||||
* @member
|
||||
* @private
|
||||
*/
|
||||
const extendWith = {
|
||||
background: 'background',
|
||||
copy: 'copy',
|
||||
repeat: 'repeat',
|
||||
mirror: 'mirror'
|
||||
};
|
||||
|
||||
/**
|
||||
* Strategies for automagic cover behaviour.
|
||||
* @member
|
||||
@ -393,6 +405,13 @@ function extend (extend) {
|
||||
}
|
||||
}
|
||||
this._setBackgroundColourOption('extendBackground', extend.background);
|
||||
if (is.defined(extend.extendWith)) {
|
||||
if (is.string(extendWith[extend.extendWith])) {
|
||||
this.options.extendWith = extendWith[extend.extendWith];
|
||||
} else {
|
||||
throw is.invalidParameterError('extendWith', 'valid value', extend.extendWith);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw is.invalidParameterError('extend', 'integer or object', extend);
|
||||
}
|
||||
|
@ -395,11 +395,11 @@ namespace sharp {
|
||||
* Split into frames, embed each frame, reassemble, and update pageHeight.
|
||||
*/
|
||||
VImage EmbedMultiPage(VImage image, int left, int top, int width, int height,
|
||||
std::vector<double> background, int nPages, int *pageHeight) {
|
||||
VipsExtend extendWith, std::vector<double> background, int nPages, int *pageHeight) {
|
||||
if (top == 0 && height == *pageHeight) {
|
||||
// Fast path; no need to adjust the height of the multi-page image
|
||||
return image.embed(left, 0, width, image.height(), VImage::option()
|
||||
->set("extend", VIPS_EXTEND_BACKGROUND)
|
||||
->set("extend", extendWith)
|
||||
->set("background", background));
|
||||
} else if (left == 0 && width == image.width()) {
|
||||
// Fast path; no need to adjust the width of the multi-page image
|
||||
@ -411,7 +411,7 @@ namespace sharp {
|
||||
|
||||
// Do the embed on the wide image
|
||||
image = image.embed(0, top, image.width(), height, VImage::option()
|
||||
->set("extend", VIPS_EXTEND_BACKGROUND)
|
||||
->set("extend", extendWith)
|
||||
->set("background", background));
|
||||
|
||||
// Split the wide image into frames
|
||||
@ -441,7 +441,7 @@ namespace sharp {
|
||||
// Embed each frame in the target size
|
||||
for (int i = 0; i < nPages; i++) {
|
||||
pages[i] = pages[i].embed(left, top, width, height, VImage::option()
|
||||
->set("extend", VIPS_EXTEND_BACKGROUND)
|
||||
->set("extend", extendWith)
|
||||
->set("background", background));
|
||||
}
|
||||
|
||||
|
@ -124,7 +124,7 @@ namespace sharp {
|
||||
* Split into frames, embed each frame, reassemble, and update pageHeight.
|
||||
*/
|
||||
VImage EmbedMultiPage(VImage image, int left, int top, int width, int height,
|
||||
std::vector<double> background, int nPages, int *pageHeight);
|
||||
VipsExtend extendWith, std::vector<double> background, int nPages, int *pageHeight);
|
||||
|
||||
} // namespace sharp
|
||||
|
||||
|
@ -441,7 +441,7 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
|
||||
image = nPages > 1
|
||||
? sharp::EmbedMultiPage(image,
|
||||
left, top, width, height, background, nPages, &targetPageHeight)
|
||||
left, top, width, height, VIPS_EXTEND_BACKGROUND, background, nPages, &targetPageHeight)
|
||||
: image.embed(left, top, width, height, VImage::option()
|
||||
->set("extend", VIPS_EXTEND_BACKGROUND)
|
||||
->set("background", background));
|
||||
@ -532,18 +532,29 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
|
||||
// Extend edges
|
||||
if (baton->extendTop > 0 || baton->extendBottom > 0 || baton->extendLeft > 0 || baton->extendRight > 0) {
|
||||
std::vector<double> background;
|
||||
std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground, shouldPremultiplyAlpha);
|
||||
|
||||
// Embed
|
||||
baton->width = image.width() + baton->extendLeft + baton->extendRight;
|
||||
baton->height = (nPages > 1 ? targetPageHeight : image.height()) + baton->extendTop + baton->extendBottom;
|
||||
|
||||
image = nPages > 1
|
||||
? sharp::EmbedMultiPage(image,
|
||||
baton->extendLeft, baton->extendTop, baton->width, baton->height, background, nPages, &targetPageHeight)
|
||||
: image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
|
||||
VImage::option()->set("extend", VIPS_EXTEND_BACKGROUND)->set("background", background));
|
||||
if (baton->extendWith == VIPS_EXTEND_BACKGROUND) {
|
||||
std::vector<double> background;
|
||||
std::tie(image, background) = sharp::ApplyAlpha(image, baton->extendBackground, shouldPremultiplyAlpha);
|
||||
|
||||
image = nPages > 1
|
||||
? sharp::EmbedMultiPage(image,
|
||||
baton->extendLeft, baton->extendTop, baton->width, baton->height,
|
||||
baton->extendWith, background, nPages, &targetPageHeight)
|
||||
: image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
|
||||
VImage::option()->set("extend", baton->extendWith)->set("background", background));
|
||||
} else {
|
||||
std::vector<double> ignoredBackground(1);
|
||||
image = nPages > 1
|
||||
? sharp::EmbedMultiPage(image,
|
||||
baton->extendLeft, baton->extendTop, baton->width, baton->height,
|
||||
baton->extendWith, ignoredBackground, nPages, &targetPageHeight)
|
||||
: image.embed(baton->extendLeft, baton->extendTop, baton->width, baton->height,
|
||||
VImage::option()->set("extend", baton->extendWith));
|
||||
}
|
||||
}
|
||||
// Median - must happen before blurring, due to the utility of blurring after thresholding
|
||||
if (baton->medianSize > 0) {
|
||||
@ -1500,6 +1511,7 @@ Napi::Value pipeline(const Napi::CallbackInfo& info) {
|
||||
baton->extendLeft = sharp::AttrAsInt32(options, "extendLeft");
|
||||
baton->extendRight = sharp::AttrAsInt32(options, "extendRight");
|
||||
baton->extendBackground = sharp::AttrAsVectorOfDouble(options, "extendBackground");
|
||||
baton->extendWith = sharp::AttrAsEnum<VipsExtend>(options, "extendWith", VIPS_TYPE_EXTEND);
|
||||
baton->extractChannel = sharp::AttrAsInt32(options, "extractChannel");
|
||||
baton->affineMatrix = sharp::AttrAsVectorOfDouble(options, "affineMatrix");
|
||||
baton->affineBackground = sharp::AttrAsVectorOfDouble(options, "affineBackground");
|
||||
|
@ -125,6 +125,7 @@ struct PipelineBaton {
|
||||
int extendLeft;
|
||||
int extendRight;
|
||||
std::vector<double> extendBackground;
|
||||
VipsExtend extendWith;
|
||||
bool withoutEnlargement;
|
||||
bool withoutReduction;
|
||||
std::vector<double> affineMatrix;
|
||||
@ -286,6 +287,7 @@ struct PipelineBaton {
|
||||
extendLeft(0),
|
||||
extendRight(0),
|
||||
extendBackground{ 0.0, 0.0, 0.0, 255.0 },
|
||||
extendWith(VIPS_EXTEND_BACKGROUND),
|
||||
withoutEnlargement(false),
|
||||
withoutReduction(false),
|
||||
affineMatrix{ 1.0, 0.0, 0.0, 1.0 },
|
||||
|
BIN
test/fixtures/expected/extend-2channel-background.png
vendored
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
test/fixtures/expected/extend-2channel-copy.png
vendored
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
test/fixtures/expected/extend-2channel-mirror.png
vendored
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
test/fixtures/expected/extend-2channel-repeat.png
vendored
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
test/fixtures/expected/extend-2channel.png
vendored
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
BIN
test/fixtures/expected/extend-equal-background.webp
vendored
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
test/fixtures/expected/extend-equal-copy.jpg
vendored
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
test/fixtures/expected/extend-equal-copy.webp
vendored
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
test/fixtures/expected/extend-equal-mirror.jpg
vendored
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
test/fixtures/expected/extend-equal-mirror.webp
vendored
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
test/fixtures/expected/extend-equal-repeat.jpg
vendored
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
test/fixtures/expected/extend-equal-repeat.webp
vendored
Normal file
After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
BIN
test/fixtures/expected/extend-unequal-copy.png
vendored
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
test/fixtures/expected/extend-unequal-mirror.png
vendored
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
test/fixtures/expected/extend-unequal-repeat.png
vendored
Normal file
After Width: | Height: | Size: 36 KiB |
@ -32,39 +32,82 @@ describe('Extend', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('extend all sides equally with RGB', function (done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(120)
|
||||
.extend({
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
left: 10,
|
||||
right: 10,
|
||||
background: { r: 255, g: 0, b: 0 }
|
||||
})
|
||||
.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);
|
||||
});
|
||||
});
|
||||
['background', 'copy', 'mirror', 'repeat'].forEach(extendWith => {
|
||||
it(`extends all sides with animated WebP (${extendWith})`, function (done) {
|
||||
sharp(fixtures.inputWebPAnimated, { pages: -1 })
|
||||
.resize(120)
|
||||
.extend({
|
||||
extendWith: extendWith,
|
||||
top: 40,
|
||||
bottom: 40,
|
||||
left: 40,
|
||||
right: 40
|
||||
})
|
||||
.toBuffer(function (err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(200, info.width);
|
||||
assert.strictEqual(200 * 9, info.height);
|
||||
fixtures.assertSimilar(fixtures.expected(`extend-equal-${extendWith}.webp`), data, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('extend sides unequally with RGBA', function (done) {
|
||||
sharp(fixtures.inputPngWithTransparency16bit)
|
||||
.resize(120)
|
||||
.extend({
|
||||
top: 50,
|
||||
left: 10,
|
||||
right: 35,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||
})
|
||||
.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(`extend all sides equally with RGB (${extendWith})`, function (done) {
|
||||
sharp(fixtures.inputJpg)
|
||||
.resize(120)
|
||||
.extend({
|
||||
extendWith: extendWith,
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
left: 10,
|
||||
right: 10,
|
||||
background: { r: 255, g: 0, b: 0 }
|
||||
})
|
||||
.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-${extendWith}.jpg`), data, done);
|
||||
});
|
||||
});
|
||||
|
||||
it(`extend sides unequally with RGBA (${extendWith})`, function (done) {
|
||||
sharp(fixtures.inputPngWithTransparency16bit)
|
||||
.resize(120)
|
||||
.extend({
|
||||
extendWith: extendWith,
|
||||
top: 50,
|
||||
left: 10,
|
||||
right: 35,
|
||||
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
||||
})
|
||||
.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-${extendWith}.png`), data, done);
|
||||
});
|
||||
});
|
||||
|
||||
it(`PNG with 2 channels (${extendWith})`, function (done) {
|
||||
sharp(fixtures.inputPngWithGreyAlpha)
|
||||
.extend({
|
||||
extendWith: extendWith,
|
||||
top: 50,
|
||||
bottom: 50,
|
||||
left: 80,
|
||||
right: 80,
|
||||
background: 'transparent'
|
||||
})
|
||||
.toBuffer(function (err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('png', info.format);
|
||||
assert.strictEqual(560, info.width);
|
||||
assert.strictEqual(400, info.height);
|
||||
assert.strictEqual(4, info.channels);
|
||||
fixtures.assertSimilar(fixtures.expected(`extend-2channel-${extendWith}.png`), data, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('missing parameter fails', function () {
|
||||
@ -101,6 +144,12 @@ describe('Extend', function () {
|
||||
/Expected positive integer for right but received \[object Object\] of type object/
|
||||
);
|
||||
});
|
||||
it('invalid extendWith fails', () => {
|
||||
assert.throws(
|
||||
() => sharp().extend({ extendWith: 'invalid-value' }),
|
||||
/Expected valid value for extendWith but received invalid-value of type string/
|
||||
);
|
||||
});
|
||||
it('can set all edges apart from right', () => {
|
||||
assert.doesNotThrow(() => sharp().extend({ top: 1, left: 2, bottom: 3 }));
|
||||
});
|
||||
@ -121,24 +170,6 @@ describe('Extend', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('PNG with 2 channels', function (done) {
|
||||
sharp(fixtures.inputPngWithGreyAlpha)
|
||||
.extend({
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
background: 'transparent'
|
||||
})
|
||||
.toBuffer(function (err, data, info) {
|
||||
if (err) throw err;
|
||||
assert.strictEqual(true, data.length > 0);
|
||||
assert.strictEqual('png', info.format);
|
||||
assert.strictEqual(420, info.width);
|
||||
assert.strictEqual(320, info.height);
|
||||
assert.strictEqual(4, info.channels);
|
||||
fixtures.assertSimilar(fixtures.expected('extend-2channel.png'), data, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('Premultiply background when compositing', async () => {
|
||||
const background = { r: 191, g: 25, b: 66, alpha: 0.8 };
|
||||
const data = await sharp({
|
||||
|