Improve performance and accuracy of multi-image composite #2286
@ -4,6 +4,11 @@
|
||||
|
||||
Requires libvips v8.12.2
|
||||
|
||||
### v0.30.2 - TBD
|
||||
|
||||
* Improve performance and accuracy when compositing multiple images.
|
||||
[#2286](https://github.com/lovell/sharp/issues/2286)
|
||||
|
||||
### v0.30.1 - 9th February 2022
|
||||
|
||||
* Allow use of `toBuffer` and `toFile` on the same instance.
|
||||
|
@ -162,7 +162,6 @@ function composite (images) {
|
||||
throw is.invalidParameterError('premultiplied', 'boolean', image.premultiplied);
|
||||
}
|
||||
}
|
||||
|
||||
return composite;
|
||||
});
|
||||
return this;
|
||||
|
@ -581,6 +581,8 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
|
||||
// Composite
|
||||
if (shouldComposite) {
|
||||
std::vector<VImage> images = { image };
|
||||
std::vector<int> modes, xs, ys;
|
||||
for (Composite *composite : baton->composite) {
|
||||
VImage compositeImage;
|
||||
sharp::ImageType compositeImageType = sharp::ImageType::UNKNOWN;
|
||||
@ -626,12 +628,12 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
// gravity was used for extract_area, set it back to its default value of 0
|
||||
composite->gravity = 0;
|
||||
}
|
||||
// Ensure image to composite is sRGB with premultiplied alpha
|
||||
// Ensure image to composite is sRGB with unpremultiplied alpha
|
||||
compositeImage = compositeImage.colourspace(VIPS_INTERPRETATION_sRGB);
|
||||
if (!sharp::HasAlpha(compositeImage)) {
|
||||
compositeImage = sharp::EnsureAlpha(compositeImage, 1);
|
||||
}
|
||||
if (!composite->premultiplied) compositeImage = compositeImage.premultiply();
|
||||
if (composite->premultiplied) compositeImage = compositeImage.unpremultiply();
|
||||
// Calculate position
|
||||
int left;
|
||||
int top;
|
||||
@ -649,12 +651,12 @@ class PipelineWorker : public Napi::AsyncWorker {
|
||||
std::tie(left, top) = sharp::CalculateCrop(image.width(), image.height(),
|
||||
compositeImage.width(), compositeImage.height(), composite->gravity);
|
||||
}
|
||||
// Composite
|
||||
image = image.composite2(compositeImage, composite->mode, VImage::option()
|
||||
->set("premultiplied", TRUE)
|
||||
->set("x", left)
|
||||
->set("y", top));
|
||||
images.push_back(compositeImage);
|
||||
modes.push_back(composite->mode);
|
||||
xs.push_back(left);
|
||||
ys.push_back(top);
|
||||
}
|
||||
image = image.composite(images, modes, VImage::option()->set("x", xs)->set("y", ys));
|
||||
}
|
||||
|
||||
// Reverse premultiplication after all transformations:
|
||||
|
BIN
test/fixtures/expected/composite-multiple.png
vendored
Before Width: | Height: | Size: 222 B After Width: | Height: | Size: 320 B |
BIN
test/fixtures/expected/composite.blend.dest-over.png
vendored
Before Width: | Height: | Size: 197 B After Width: | Height: | Size: 291 B |
BIN
test/fixtures/expected/composite.blend.over.png
vendored
Before Width: | Height: | Size: 197 B After Width: | Height: | Size: 292 B |
BIN
test/fixtures/expected/composite.blend.saturate.png
vendored
Before Width: | Height: | Size: 194 B After Width: | Height: | Size: 288 B |
BIN
test/fixtures/expected/composite.blend.xor.png
vendored
Before Width: | Height: | Size: 192 B After Width: | Height: | Size: 286 B |
@ -45,22 +45,20 @@ const blends = [
|
||||
|
||||
// Test
|
||||
describe('composite', () => {
|
||||
it('blend', () => Promise.all(
|
||||
blends.map(blend => {
|
||||
blends.forEach(blend => {
|
||||
it(`blend ${blend}`, async () => {
|
||||
const filename = `composite.blend.${blend}.png`;
|
||||
const actual = fixtures.path(`output.${filename}`);
|
||||
const expected = fixtures.expected(filename);
|
||||
return sharp(redRect)
|
||||
await sharp(redRect)
|
||||
.composite([{
|
||||
input: blueRect,
|
||||
blend
|
||||
}])
|
||||
.toFile(actual)
|
||||
.then(() => {
|
||||
fixtures.assertMaxColourDistance(actual, expected);
|
||||
});
|
||||
})
|
||||
));
|
||||
.toFile(actual);
|
||||
fixtures.assertMaxColourDistance(actual, expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('premultiplied true', () => {
|
||||
const filename = 'composite.premultiplied.png';
|
||||
@ -121,11 +119,11 @@ describe('composite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('multiple', () => {
|
||||
it('multiple', async () => {
|
||||
const filename = 'composite-multiple.png';
|
||||
const actual = fixtures.path(`output.${filename}`);
|
||||
const expected = fixtures.expected(filename);
|
||||
return sharp(redRect)
|
||||
await sharp(redRect)
|
||||
.composite([{
|
||||
input: blueRect,
|
||||
gravity: 'northeast'
|
||||
@ -133,10 +131,8 @@ describe('composite', () => {
|
||||
input: greenRect,
|
||||
gravity: 'southwest'
|
||||
}])
|
||||
.toFile(actual)
|
||||
.then(() => {
|
||||
fixtures.assertMaxColourDistance(actual, expected);
|
||||
});
|
||||
.toFile(actual);
|
||||
fixtures.assertMaxColourDistance(actual, expected);
|
||||
});
|
||||
|
||||
it('zero offset', done => {
|
||||
|
@ -140,7 +140,7 @@ describe('Extend', function () {
|
||||
});
|
||||
|
||||
it('Premultiply background when compositing', async () => {
|
||||
const background = '#bf1942cc';
|
||||
const background = { r: 191, g: 25, b: 66, alpha: 0.8 };
|
||||
const data = await sharp({
|
||||
create: {
|
||||
width: 1, height: 1, channels: 4, background: '#fff0'
|
||||
@ -158,10 +158,6 @@ describe('Extend', function () {
|
||||
})
|
||||
.raw()
|
||||
.toBuffer();
|
||||
const [r1, g1, b1, a1, r2, g2, b2, a2] = data;
|
||||
assert.strictEqual(true, Math.abs(r2 - r1) < 2);
|
||||
assert.strictEqual(true, Math.abs(g2 - g1) < 2);
|
||||
assert.strictEqual(true, Math.abs(b2 - b1) < 2);
|
||||
assert.strictEqual(true, Math.abs(a2 - a1) < 2);
|
||||
assert.deepStrictEqual(Array.from(data), [191, 25, 65, 204, 238, 31, 82, 204]);
|
||||
});
|
||||
});
|
||||
|