Add support to extend for extendWith, allows copy/mirror/repeat (#3556)

This commit is contained in:
Tomasz Janowski 2023-02-18 01:01:24 +11:00 committed by GitHub
parent ebf4ccd124
commit 6f0e6f2e65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 134 additions and 65 deletions

View File

@ -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
View File

@ -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 {

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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 },

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -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({