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,
extendRight: 0,
extendBackground: [0, 0, 0, 255],
extendWith: 'background',
withoutEnlargement: false,
withoutReduction: false,
affineMatrix: [],

6
lib/index.d.ts vendored
View File

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

View File

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

View File

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

View File

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

View File

@ -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;
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, background, nPages, &targetPageHeight)
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", VIPS_EXTEND_BACKGROUND)->set("background", background));
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");

View File

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

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,10 +32,30 @@ describe('Extend', function () {
});
});
it('extend all sides equally with RGB', function (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 all sides equally with RGB (${extendWith})`, function (done) {
sharp(fixtures.inputJpg)
.resize(120)
.extend({
extendWith: extendWith,
top: 10,
bottom: 10,
left: 10,
@ -46,14 +66,15 @@ describe('Extend', function () {
if (err) throw err;
assert.strictEqual(140, info.width);
assert.strictEqual(118, info.height);
fixtures.assertSimilar(fixtures.expected('extend-equal.jpg'), data, done);
fixtures.assertSimilar(fixtures.expected(`extend-equal-${extendWith}.jpg`), data, done);
});
});
it('extend sides unequally with RGBA', function (done) {
it(`extend sides unequally with RGBA (${extendWith})`, function (done) {
sharp(fixtures.inputPngWithTransparency16bit)
.resize(120)
.extend({
extendWith: extendWith,
top: 50,
left: 10,
right: 35,
@ -63,7 +84,29 @@ describe('Extend', function () {
if (err) throw err;
assert.strictEqual(165, info.width);
assert.strictEqual(170, info.height);
fixtures.assertSimilar(fixtures.expected('extend-unequal.png'), data, done);
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);
});
});
});
@ -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({