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