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