'use strict'; const fs = require('fs'); const assert = require('assert'); const fixtures = require('../fixtures'); const sharp = require('../../'); // Helpers const getPaths = function (baseName, extension) { if (typeof extension === 'undefined') { extension = 'png'; } return { actual: fixtures.path('output.' + baseName + '.' + extension), expected: fixtures.expected(baseName + '.' + extension) }; }; // Test describe('Overlays', function () { it('Overlay transparent PNG file on solid background', function (done) { const paths = getPaths('alpha-layer-01'); sharp(fixtures.inputPngOverlayLayer0) .overlayWith(fixtures.inputPngOverlayLayer1) .toFile(paths.actual, function (error) { if (error) return done(error); fixtures.assertMaxColourDistance(paths.actual, paths.expected); done(); }); }); it('Overlay transparent PNG Buffer on solid background', function (done) { const paths = getPaths('alpha-layer-01'); sharp(fixtures.inputPngOverlayLayer0) .overlayWith(fs.readFileSync(fixtures.inputPngOverlayLayer1)) .toFile(paths.actual, function (error) { if (error) return done(error); fixtures.assertMaxColourDistance(paths.actual, paths.expected); done(); }); }); it('Overlay low-alpha transparent PNG on solid background', function (done) { const paths = getPaths('alpha-layer-01-low-alpha'); sharp(fixtures.inputPngOverlayLayer0) .overlayWith(fixtures.inputPngOverlayLayer1LowAlpha) .toFile(paths.actual, function (error) { if (error) return done(error); fixtures.assertMaxColourDistance(paths.actual, paths.expected); done(); }); }); it('Composite three transparent PNGs into one', function (done) { const paths = getPaths('alpha-layer-012'); sharp(fixtures.inputPngOverlayLayer0) .overlayWith(fixtures.inputPngOverlayLayer1) .toBuffer(function (error, data) { if (error) return done(error); sharp(data) .overlayWith(fixtures.inputPngOverlayLayer2) .toFile(paths.actual, function (error) { if (error) return done(error); fixtures.assertMaxColourDistance(paths.actual, paths.expected); done(); }); }); }); it('Composite two transparent PNGs into one', function (done) { const paths = getPaths('alpha-layer-12'); sharp(fixtures.inputPngOverlayLayer1) .overlayWith(fixtures.inputPngOverlayLayer2) .toFile(paths.actual, function (error) { if (error) return done(error); fixtures.assertMaxColourDistance(paths.actual, paths.expected); done(); }); }); it('Composite two low-alpha transparent PNGs into one', function (done) { const paths = getPaths('alpha-layer-12-low-alpha'); sharp(fixtures.inputPngOverlayLayer1LowAlpha) .overlayWith(fixtures.inputPngOverlayLayer2LowAlpha) .toFile(paths.actual, function (error) { if (error) return done(error); fixtures.assertMaxColourDistance(paths.actual, paths.expected, 2); done(); }); }); it('Composite three low-alpha transparent PNGs into one', function (done) { const paths = getPaths('alpha-layer-012-low-alpha'); sharp(fixtures.inputPngOverlayLayer0) .overlayWith(fixtures.inputPngOverlayLayer1LowAlpha) .toBuffer(function (error, data) { if (error) return done(error); sharp(data) .overlayWith(fixtures.inputPngOverlayLayer2LowAlpha) .toFile(paths.actual, function (error) { if (error) return done(error); fixtures.assertMaxColourDistance(paths.actual, paths.expected); done(); }); }); }); it('Composite rgb+alpha PNG onto JPEG', function (done) { const paths = getPaths('overlay-jpeg-with-rgb', 'jpg'); sharp(fixtures.inputJpg) .resize(2048, 1536) .overlayWith(fixtures.inputPngOverlayLayer1) .toFile(paths.actual, function (error, info) { if (error) return done(error); fixtures.assertMaxColourDistance(paths.actual, paths.expected, 102); done(); }); }); it('Composite greyscale+alpha PNG onto JPEG', function (done) { const paths = getPaths('overlay-jpeg-with-greyscale', 'jpg'); sharp(fixtures.inputJpg) .resize(400, 300) .overlayWith(fixtures.inputPngWithGreyAlpha) .toFile(paths.actual, function (error, info) { if (error) return done(error); fixtures.assertMaxColourDistance(paths.actual, paths.expected, 102); done(); }); }); it('Composite WebP onto JPEG', function (done) { const paths = getPaths('overlay-jpeg-with-webp', 'jpg'); sharp(fixtures.inputJpg) .resize(300, 300) .overlayWith(fixtures.inputWebPWithTransparency) .toFile(paths.actual, function (error, info) { if (error) return done(error); fixtures.assertMaxColourDistance(paths.actual, paths.expected, 102); done(); }); }); it('Composite JPEG onto PNG, ensure premultiply', function (done) { sharp(fixtures.inputPngOverlayLayer1) .overlayWith(fixtures.inputJpgWithLandscapeExif1) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual(true, info.premultiplied); done(); }); }); it('Composite opaque JPEG onto JPEG, ensure premultiply', function (done) { sharp(fixtures.inputJpg) .overlayWith(fixtures.inputJpgWithLandscapeExif1) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual(true, info.premultiplied); done(); }); }); it('Fail when overlay is larger', function (done) { sharp(fixtures.inputJpg) .resize(320) .overlayWith(fixtures.inputPngOverlayLayer1) .toBuffer(function (error) { assert.strictEqual(true, error instanceof Error); done(); }); }); it('Fail with empty String parameter', function () { assert.throws(function () { sharp().overlayWith(''); }); }); it('Fail with non-String parameter', function () { assert.throws(function () { sharp().overlayWith(1); }); }); it('Fail with unsupported gravity', function () { assert.throws(function () { sharp() .overlayWith(fixtures.inputPngOverlayLayer1, { gravity: 9 }); }); }); it('Empty options', function () { assert.doesNotThrow(function () { sharp().overlayWith(fixtures.inputPngOverlayLayer1, {}); }); }); describe('Overlay with numeric gravity', function () { Object.keys(sharp.gravity).forEach(function (gravity) { it(gravity, function (done) { const expected = fixtures.expected('overlay-gravity-' + gravity + '.jpg'); sharp(fixtures.inputJpg) .resize(80) .overlayWith(fixtures.inputPngWithTransparency16bit, { gravity: gravity }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(80, info.width); assert.strictEqual(65, info.height); assert.strictEqual(3, info.channels); fixtures.assertSimilar(expected, data, done); }); }); }); }); describe('Overlay with string-based gravity', function () { Object.keys(sharp.gravity).forEach(function (gravity) { it(gravity, function (done) { const expected = fixtures.expected('overlay-gravity-' + gravity + '.jpg'); sharp(fixtures.inputJpg) .resize(80) .overlayWith(fixtures.inputPngWithTransparency16bit, { gravity: sharp.gravity[gravity] }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(80, info.width); assert.strictEqual(65, info.height); assert.strictEqual(3, info.channels); fixtures.assertSimilar(expected, data, done); }); }); }); }); describe('Overlay with tile enabled and gravity', function () { Object.keys(sharp.gravity).forEach(function (gravity) { it(gravity, function (done) { const expected = fixtures.expected('overlay-tile-gravity-' + gravity + '.jpg'); sharp(fixtures.inputJpg) .resize(80) .overlayWith(fixtures.inputPngWithTransparency16bit, { tile: true, gravity: gravity }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(80, info.width); assert.strictEqual(65, info.height); assert.strictEqual(3, info.channels); fixtures.assertSimilar(expected, data, done); }); }); }); }); describe('Overlay with top-left offsets', function () { it('Overlay with 10px top & 10px left offsets', function (done) { const expected = fixtures.expected('overlay-valid-offsets-10-10.jpg'); sharp(fixtures.inputJpg) .resize(400) .overlayWith(fixtures.inputPngWithTransparency16bit, { top: 10, left: 10 }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(3, info.channels); fixtures.assertSimilar(expected, data, done); }); }); it('Overlay with 100px top & 300px left offsets', function (done) { const expected = fixtures.expected('overlay-valid-offsets-100-300.jpg'); sharp(fixtures.inputJpg) .resize(400) .overlayWith(fixtures.inputPngWithTransparency16bit, { top: 100, left: 300 }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(3, info.channels); fixtures.assertSimilar(expected, data, done); }); }); it('Overlay with only top offset', function () { assert.throws(function () { sharp(fixtures.inputJpg) .resize(400) .overlayWith(fixtures.inputPngWithTransparency16bit, { top: 1000 }); }); }); it('Overlay with only left offset', function () { assert.throws(function () { sharp(fixtures.inputJpg) .resize(400) .overlayWith(fixtures.inputPngWithTransparency16bit, { left: 1000 }); }); }); it('Overlay with negative offsets', function () { assert.throws(function () { sharp(fixtures.inputJpg) .resize(400) .overlayWith(fixtures.inputPngWithTransparency16bit, { top: -1000, left: -1000 }); }); }); it('Overlay with 0 offset', function (done) { const expected = fixtures.expected('overlay-offset-0.jpg'); sharp(fixtures.inputJpg) .resize(400) .overlayWith(fixtures.inputPngWithTransparency16bit, { top: 0, left: 0 }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(3, info.channels); fixtures.assertSimilar(expected, data, done); }); }); it('Overlay with offset and gravity', function (done) { const expected = fixtures.expected('overlay-offset-with-gravity.jpg'); sharp(fixtures.inputJpg) .resize(400) .overlayWith(fixtures.inputPngWithTransparency16bit, { left: 10, top: 10, gravity: 4 }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(3, info.channels); fixtures.assertSimilar(expected, data, done); }); }); it('Overlay with offset and gravity and tile', function (done) { const expected = fixtures.expected('overlay-offset-with-gravity-tile.jpg'); sharp(fixtures.inputJpg) .resize(400) .overlayWith(fixtures.inputPngWithTransparency16bit, { left: 10, top: 10, gravity: 4, tile: true }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(3, info.channels); fixtures.assertSimilar(expected, data, done); }); }); it('Overlay with offset and tile', function (done) { const expected = fixtures.expected('overlay-offset-with-tile.jpg'); sharp(fixtures.inputJpg) .resize(400) .overlayWith(fixtures.inputPngWithTransparency16bit, { left: 10, top: 10, tile: true }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(3, info.channels); fixtures.assertSimilar(expected, data, done); }); }); it('Overlay with invalid tile option', function () { assert.throws(function () { sharp().overlayWith('ignore', { tile: 1 }); }); }); it('Overlay with very large offset', function (done) { const expected = fixtures.expected('overlay-very-large-offset.jpg'); sharp(fixtures.inputJpg) .resize(400) .overlayWith(fixtures.inputPngWithTransparency16bit, { left: 10000, top: 10000 }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(3, info.channels); fixtures.assertSimilar(expected, data, done); }); }); it('Overlay 100x100 with 50x50 so bottom edges meet', function (done) { sharp(fixtures.inputJpg) .resize(50, 50) .toBuffer(function (err, overlay) { if (err) throw err; sharp(fixtures.inputJpgWithLandscapeExif1) .resize(100, 100) .overlayWith(overlay, { top: 50, left: 40 }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(100, info.width); assert.strictEqual(100, info.height); fixtures.assertSimilar(fixtures.expected('overlay-bottom-edges-meet.jpg'), data, done); }); }); }); }); it('With tile enabled and image rotated 90 degrees', function (done) { const expected = fixtures.expected('overlay-tile-rotated90.jpg'); sharp(fixtures.inputJpg) .rotate(90) .resize(80) .overlayWith(fixtures.inputPngWithTransparency16bit, { tile: true }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(80, info.width); assert.strictEqual(98, info.height); assert.strictEqual(3, info.channels); fixtures.assertSimilar(expected, data, done); }); }); it('With tile enabled and image rotated 90 degrees and gravity northwest', function (done) { const expected = fixtures.expected('overlay-tile-rotated90-gravity-northwest.jpg'); sharp(fixtures.inputJpg) .rotate(90) .resize(80) .overlayWith(fixtures.inputPngWithTransparency16bit, { tile: true, gravity: 'northwest' }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(80, info.width); assert.strictEqual(98, info.height); assert.strictEqual(3, info.channels); fixtures.assertSimilar(expected, data, done); }); }); describe('Overlay with cutout enabled and gravity', function () { Object.keys(sharp.gravity).forEach(function (gravity) { it(gravity, function (done) { const expected = fixtures.expected('overlay-cutout-gravity-' + gravity + '.jpg'); sharp(fixtures.inputJpg) .resize(80) .overlayWith(fixtures.inputPngWithTransparency16bit, { cutout: true, gravity: gravity }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(80, info.width); assert.strictEqual(65, info.height); assert.strictEqual(3, info.channels); fixtures.assertSimilar(expected, data, done); }); }); }); }); it('With cutout enabled and image rotated 90 degrees', function (done) { const expected = fixtures.expected('overlay-cutout-rotated90.jpg'); sharp(fixtures.inputJpg) .rotate(90) .resize(80) .overlayWith(fixtures.inputPngWithTransparency16bit, { cutout: true }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(80, info.width); assert.strictEqual(98, info.height); assert.strictEqual(3, info.channels); fixtures.assertSimilar(expected, data, done); }); }); it('With cutout enabled and image rotated 90 degrees and gravity northwest', function (done) { const expected = fixtures.expected('overlay-cutout-rotated90-gravity-northwest.jpg'); sharp(fixtures.inputJpg) .rotate(90) .resize(80) .overlayWith(fixtures.inputPngWithTransparency16bit, { cutout: true, gravity: 'northwest' }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(80, info.width); assert.strictEqual(98, info.height); assert.strictEqual(3, info.channels); fixtures.assertSimilar(expected, data, done); }); }); it('Composite RGBA raw buffer onto JPEG', function (done) { sharp(fixtures.inputPngOverlayLayer1) .raw() .toBuffer(function (err, data, info) { if (err) throw err; sharp(fixtures.inputJpg) .resize(2048, 1536) .overlayWith(data, { raw: info }) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual(true, info.premultiplied); fixtures.assertSimilar(fixtures.expected('overlay-jpeg-with-rgb.jpg'), data, done); }); }); }); it('Returns an error when called with an invalid file', function (done) { sharp(fixtures.inputJpg) .overlayWith('notfound.png') .toBuffer(function (err) { assert(err instanceof Error); done(); }); }); it('Composite JPEG onto JPEG', function (done) { sharp(fixtures.inputJpg) .resize(480, 320) .overlayWith(fixtures.inputJpgBooleanTest) .toBuffer(function (err, data, info) { if (err) throw err; assert.strictEqual('jpeg', info.format); assert.strictEqual(480, info.width); assert.strictEqual(320, info.height); assert.strictEqual(3, info.channels); assert.strictEqual(true, info.premultiplied); fixtures.assertSimilar(fixtures.expected('overlay-jpeg-with-jpeg.jpg'), data, done); }); }); });