diff --git a/lib/constructor.js b/lib/constructor.js index 8b51ce34..10444b4b 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -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: [], diff --git a/lib/index.d.ts b/lib/index.d.ts index a41b31b1..bc0e881e 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -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 { diff --git a/lib/resize.js b/lib/resize.js index 8d67ede4..3c8544ec 100644 --- a/lib/resize.js +++ b/lib/resize.js @@ -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); } diff --git a/src/operations.cc b/src/operations.cc index 012712e1..94169961 100644 --- a/src/operations.cc +++ b/src/operations.cc @@ -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 background, int nPages, int *pageHeight) { + VipsExtend extendWith, std::vector 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)); } diff --git a/src/operations.h b/src/operations.h index 0fe1cac9..eabb1212 100644 --- a/src/operations.h +++ b/src/operations.h @@ -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 background, int nPages, int *pageHeight); + VipsExtend extendWith, std::vector background, int nPages, int *pageHeight); } // namespace sharp diff --git a/src/pipeline.cc b/src/pipeline.cc index e4d4c727..f13e70c7 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -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 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 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 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(options, "extendWith", VIPS_TYPE_EXTEND); baton->extractChannel = sharp::AttrAsInt32(options, "extractChannel"); baton->affineMatrix = sharp::AttrAsVectorOfDouble(options, "affineMatrix"); baton->affineBackground = sharp::AttrAsVectorOfDouble(options, "affineBackground"); diff --git a/src/pipeline.h b/src/pipeline.h index 4a9f4650..1612eded 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -125,6 +125,7 @@ struct PipelineBaton { int extendLeft; int extendRight; std::vector extendBackground; + VipsExtend extendWith; bool withoutEnlargement; bool withoutReduction; std::vector 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 }, diff --git a/test/fixtures/expected/extend-2channel-background.png b/test/fixtures/expected/extend-2channel-background.png new file mode 100644 index 00000000..e29a5eba Binary files /dev/null and b/test/fixtures/expected/extend-2channel-background.png differ diff --git a/test/fixtures/expected/extend-2channel-copy.png b/test/fixtures/expected/extend-2channel-copy.png new file mode 100644 index 00000000..e29a5eba Binary files /dev/null and b/test/fixtures/expected/extend-2channel-copy.png differ diff --git a/test/fixtures/expected/extend-2channel-mirror.png b/test/fixtures/expected/extend-2channel-mirror.png new file mode 100644 index 00000000..9a0ea8f9 Binary files /dev/null and b/test/fixtures/expected/extend-2channel-mirror.png differ diff --git a/test/fixtures/expected/extend-2channel-repeat.png b/test/fixtures/expected/extend-2channel-repeat.png new file mode 100644 index 00000000..0c4b2b5f Binary files /dev/null and b/test/fixtures/expected/extend-2channel-repeat.png differ diff --git a/test/fixtures/expected/extend-2channel.png b/test/fixtures/expected/extend-2channel.png deleted file mode 100644 index bd57b5a9..00000000 Binary files a/test/fixtures/expected/extend-2channel.png and /dev/null differ diff --git a/test/fixtures/expected/extend-equal.jpg b/test/fixtures/expected/extend-equal-background.jpg similarity index 100% rename from test/fixtures/expected/extend-equal.jpg rename to test/fixtures/expected/extend-equal-background.jpg diff --git a/test/fixtures/expected/extend-equal-background.webp b/test/fixtures/expected/extend-equal-background.webp new file mode 100644 index 00000000..c7898aef Binary files /dev/null and b/test/fixtures/expected/extend-equal-background.webp differ diff --git a/test/fixtures/expected/extend-equal-copy.jpg b/test/fixtures/expected/extend-equal-copy.jpg new file mode 100644 index 00000000..160395eb Binary files /dev/null and b/test/fixtures/expected/extend-equal-copy.jpg differ diff --git a/test/fixtures/expected/extend-equal-copy.webp b/test/fixtures/expected/extend-equal-copy.webp new file mode 100644 index 00000000..7123beee Binary files /dev/null and b/test/fixtures/expected/extend-equal-copy.webp differ diff --git a/test/fixtures/expected/extend-equal-mirror.jpg b/test/fixtures/expected/extend-equal-mirror.jpg new file mode 100644 index 00000000..9ff2a3cb Binary files /dev/null and b/test/fixtures/expected/extend-equal-mirror.jpg differ diff --git a/test/fixtures/expected/extend-equal-mirror.webp b/test/fixtures/expected/extend-equal-mirror.webp new file mode 100644 index 00000000..9b3b3518 Binary files /dev/null and b/test/fixtures/expected/extend-equal-mirror.webp differ diff --git a/test/fixtures/expected/extend-equal-repeat.jpg b/test/fixtures/expected/extend-equal-repeat.jpg new file mode 100644 index 00000000..860787da Binary files /dev/null and b/test/fixtures/expected/extend-equal-repeat.jpg differ diff --git a/test/fixtures/expected/extend-equal-repeat.webp b/test/fixtures/expected/extend-equal-repeat.webp new file mode 100644 index 00000000..17320d84 Binary files /dev/null and b/test/fixtures/expected/extend-equal-repeat.webp differ diff --git a/test/fixtures/expected/extend-unequal.png b/test/fixtures/expected/extend-unequal-background.png similarity index 100% rename from test/fixtures/expected/extend-unequal.png rename to test/fixtures/expected/extend-unequal-background.png diff --git a/test/fixtures/expected/extend-unequal-copy.png b/test/fixtures/expected/extend-unequal-copy.png new file mode 100644 index 00000000..ab324ef4 Binary files /dev/null and b/test/fixtures/expected/extend-unequal-copy.png differ diff --git a/test/fixtures/expected/extend-unequal-mirror.png b/test/fixtures/expected/extend-unequal-mirror.png new file mode 100644 index 00000000..99e35bf9 Binary files /dev/null and b/test/fixtures/expected/extend-unequal-mirror.png differ diff --git a/test/fixtures/expected/extend-unequal-repeat.png b/test/fixtures/expected/extend-unequal-repeat.png new file mode 100644 index 00000000..6f0afd4b Binary files /dev/null and b/test/fixtures/expected/extend-unequal-repeat.png differ diff --git a/test/unit/extend.js b/test/unit/extend.js index 1e22df77..ef77cddc 100644 --- a/test/unit/extend.js +++ b/test/unit/extend.js @@ -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({