diff --git a/lib/constructor.js b/lib/constructor.js index e27983d6..64026a4a 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -136,6 +136,7 @@ const Sharp = function (input, options) { height: -1, canvas: 'crop', crop: 0, + embed: 0, useExifOrientation: false, angle: 0, rotateBeforePreExtract: false, diff --git a/lib/resize.js b/lib/resize.js index b1328a28..6d1775bd 100644 --- a/lib/resize.js +++ b/lib/resize.js @@ -179,11 +179,26 @@ function crop (crop) { * // outputBuffer contains WebP image data of a 200 pixels wide and 300 pixels high * // containing a scaled version, embedded on a transparent canvas, of input.gif * }); - * + * @param {String} [embed='centre'] - A member of `sharp.gravity` to embed to an edge/corner. * @returns {Sharp} + * @throws {Error} Invalid parameters */ -function embed () { +function embed (embed) { this.options.canvas = 'embed'; + + if (!is.defined(embed)) { + // Default + this.options.embed = gravity.center; + } else if (is.integer(embed) && is.inRange(embed, 0, 8)) { + // Gravity (numeric) + this.options.embed = embed; + } else if (is.string(embed) && is.integer(gravity[embed])) { + // Gravity (string) + this.options.embed = gravity[embed]; + } else { + throw is.invalidParameterError('embed', 'valid embed id/name', embed); + } + return this; } diff --git a/package.json b/package.json index bab9a901..82bef6ca 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,8 @@ "Nicolas Coden ", "Matt Parrish ", "Matthew McEachen ", - "Jarda Kotěšovec " + "Jarda Kotěšovec ", + "Kenric D'Souza " ], "scripts": { "clean": "rm -rf node_modules/ build/ vendor/ coverage/ test/fixtures/output.*", @@ -66,6 +67,7 @@ "resize", "thumbnail", "crop", + "embed", "libvips", "vips" ], diff --git a/src/common.cc b/src/common.cc index cd8dfbe6..3060ad2a 100644 --- a/src/common.cc +++ b/src/common.cc @@ -409,7 +409,62 @@ namespace sharp { /* Calculate the (left, top) coordinates of the output image - within the input image, applying the given gravity. + within the input image, applying the given gravity during an embed. + + @Azurebyte: We are basically swapping the inWidth and outWidth, inHeight and outHeight from the CalculateCrop function. + */ + std::tuple CalculateEmbedPosition(int const inWidth, int const inHeight, + int const outWidth, int const outHeight, int const gravity) { + + int left = 0; + int top = 0; + switch (gravity) { + case 1: + // North + left = (outWidth - inWidth) / 2; + break; + case 2: + // East + left = outWidth - inWidth; + top = (outHeight - inHeight) / 2; + break; + case 3: + // South + left = (outWidth - inWidth) / 2; + top = outHeight - inHeight; + break; + case 4: + // West + top = (outHeight - inHeight) / 2; + break; + case 5: + // Northeast + left = outWidth - inWidth; + break; + case 6: + // Southeast + left = outWidth - inWidth; + top = outHeight - inHeight; + break; + case 7: + // Southwest + top = outHeight - inHeight; + break; + case 8: + // Northwest + // Which is the default is 0,0 so we do not assign anything here. + break; + default: + // Centre + left = (outWidth - inWidth) / 2; + top = (outHeight - inHeight) / 2; + } + return std::make_tuple(left, top); + } + + /* + Calculate the (left, top) coordinates of the output image + within the input image, applying the given gravity during a crop. */ std::tuple CalculateCrop(int const inWidth, int const inHeight, int const outWidth, int const outHeight, int const gravity) { diff --git a/src/common.h b/src/common.h index 874a4f2c..0dc618e2 100644 --- a/src/common.h +++ b/src/common.h @@ -206,6 +206,13 @@ namespace sharp { */ std::string VipsWarningPop(); + /* + Calculate the (left, top) coordinates of the output image + within the input image, applying the given gravity during an embed. + */ + std::tuple CalculateEmbedPosition(int const inWidth, int const inHeight, + int const outWidth, int const outHeight, int const gravity); + /* Calculate the (left, top) coordinates of the output image within the input image, applying the given gravity. diff --git a/src/pipeline.cc b/src/pipeline.cc index a16efc12..c80c11ab 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -446,12 +446,25 @@ class PipelineWorker : public Nan::AsyncWorker { image = image.bandjoin( VImage::new_matrix(image.width(), image.height()).new_from_image(255 * multiplier)); } + // Embed - int left = static_cast(round((baton->width - image.width()) / 2)); - int top = static_cast(round((baton->height - image.height()) / 2)); - image = image.embed(left, top, baton->width, baton->height, VImage::option() + + // Calculate where to position the embeded image if gravity specified, else center. + int left; + int top; + + left = static_cast(round((baton->width - image.width()) / 2)); + top = static_cast(round((baton->height - image.height()) / 2)); + + int width = std::max(image.width(), baton->width); + int height = std::max(image.height(), baton->height); + std::tie(left, top) = sharp::CalculateEmbedPosition( + image.width(), image.height(), baton->width, baton->height, baton->embed); + + image = image.embed(left, top, width, height, VImage::option() ->set("extend", VIPS_EXTEND_BACKGROUND) ->set("background", background)); + } else if (baton->canvas != Canvas::IGNORE_ASPECT) { // Crop/max/min if (baton->crop < 9) { @@ -1141,6 +1154,7 @@ NAN_METHOD(pipeline) { // Resize options baton->withoutEnlargement = AttrTo(options, "withoutEnlargement"); baton->crop = AttrTo(options, "crop"); + baton->embed = AttrTo(options, "embed"); baton->kernel = AttrAsStr(options, "kernel"); baton->fastShrinkOnLoad = AttrTo(options, "fastShrinkOnLoad"); // Join Channel Options diff --git a/src/pipeline.h b/src/pipeline.h index 9f320905..40a566c6 100644 --- a/src/pipeline.h +++ b/src/pipeline.h @@ -62,6 +62,7 @@ struct PipelineBaton { int channels; Canvas canvas; int crop; + int embed; bool hasCropOffset; int cropOffsetLeft; int cropOffsetTop; @@ -145,6 +146,7 @@ struct PipelineBaton { channels(0), canvas(Canvas::CROP), crop(0), + embed(0), hasCropOffset(false), cropOffsetLeft(0), cropOffsetTop(0), diff --git a/test/fixtures/embedgravitybird.png b/test/fixtures/embedgravitybird.png new file mode 100644 index 00000000..cd381c9e Binary files /dev/null and b/test/fixtures/embedgravitybird.png differ diff --git a/test/fixtures/expected/embedgravitybird/1-nw.png b/test/fixtures/expected/embedgravitybird/1-nw.png new file mode 100644 index 00000000..7d107748 Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/1-nw.png differ diff --git a/test/fixtures/expected/embedgravitybird/2-n.png b/test/fixtures/expected/embedgravitybird/2-n.png new file mode 100644 index 00000000..7d107748 Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/2-n.png differ diff --git a/test/fixtures/expected/embedgravitybird/3-ne.png b/test/fixtures/expected/embedgravitybird/3-ne.png new file mode 100644 index 00000000..7d107748 Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/3-ne.png differ diff --git a/test/fixtures/expected/embedgravitybird/4-e.png b/test/fixtures/expected/embedgravitybird/4-e.png new file mode 100644 index 00000000..4710a3f5 Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/4-e.png differ diff --git a/test/fixtures/expected/embedgravitybird/5-se.png b/test/fixtures/expected/embedgravitybird/5-se.png new file mode 100644 index 00000000..1c5df9f4 Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/5-se.png differ diff --git a/test/fixtures/expected/embedgravitybird/6-s.png b/test/fixtures/expected/embedgravitybird/6-s.png new file mode 100644 index 00000000..1c5df9f4 Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/6-s.png differ diff --git a/test/fixtures/expected/embedgravitybird/7-sw.png b/test/fixtures/expected/embedgravitybird/7-sw.png new file mode 100644 index 00000000..1c5df9f4 Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/7-sw.png differ diff --git a/test/fixtures/expected/embedgravitybird/8-w.png b/test/fixtures/expected/embedgravitybird/8-w.png new file mode 100644 index 00000000..4710a3f5 Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/8-w.png differ diff --git a/test/fixtures/expected/embedgravitybird/9-c.png b/test/fixtures/expected/embedgravitybird/9-c.png new file mode 100644 index 00000000..4710a3f5 Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/9-c.png differ diff --git a/test/fixtures/expected/embedgravitybird/a1-nw.png b/test/fixtures/expected/embedgravitybird/a1-nw.png new file mode 100644 index 00000000..f79957ec Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/a1-nw.png differ diff --git a/test/fixtures/expected/embedgravitybird/a2-n.png b/test/fixtures/expected/embedgravitybird/a2-n.png new file mode 100644 index 00000000..6669bd62 Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/a2-n.png differ diff --git a/test/fixtures/expected/embedgravitybird/a3-ne.png b/test/fixtures/expected/embedgravitybird/a3-ne.png new file mode 100644 index 00000000..df2fe69c Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/a3-ne.png differ diff --git a/test/fixtures/expected/embedgravitybird/a4-e.png b/test/fixtures/expected/embedgravitybird/a4-e.png new file mode 100644 index 00000000..df2fe69c Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/a4-e.png differ diff --git a/test/fixtures/expected/embedgravitybird/a5-se.png b/test/fixtures/expected/embedgravitybird/a5-se.png new file mode 100644 index 00000000..df2fe69c Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/a5-se.png differ diff --git a/test/fixtures/expected/embedgravitybird/a6-s.png b/test/fixtures/expected/embedgravitybird/a6-s.png new file mode 100644 index 00000000..6669bd62 Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/a6-s.png differ diff --git a/test/fixtures/expected/embedgravitybird/a7-sw.png b/test/fixtures/expected/embedgravitybird/a7-sw.png new file mode 100644 index 00000000..f79957ec Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/a7-sw.png differ diff --git a/test/fixtures/expected/embedgravitybird/a8-w.png b/test/fixtures/expected/embedgravitybird/a8-w.png new file mode 100644 index 00000000..f79957ec Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/a8-w.png differ diff --git a/test/fixtures/expected/embedgravitybird/a9-c.png b/test/fixtures/expected/embedgravitybird/a9-c.png new file mode 100644 index 00000000..6669bd62 Binary files /dev/null and b/test/fixtures/expected/embedgravitybird/a9-c.png differ diff --git a/test/fixtures/index.js b/test/fixtures/index.js index a0cbed15..d8103cfa 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -85,6 +85,7 @@ module.exports = { inputPngBooleanNoAlpha: getPath('bandbool.png'), inputPngTestJoinChannel: getPath('testJoinChannel.png'), inputPngTruncated: getPath('truncated.png'), // gm convert 2569067123_aca715a2ee_o.jpg -resize 320x240 saw.png ; head -c 10000 saw.png > truncated.png + inputPngEmbed: getPath('embedgravitybird.png'), // Released to sharp under a CC BY 4.0 inputWebP: getPath('4.webp'), // http://www.gstatic.com/webp/gallery/4.webp inputWebPWithTransparency: getPath('5_webp_a.webp'), // http://www.gstatic.com/webp/gallery3/5_webp_a.webp diff --git a/test/unit/embed.js b/test/unit/embed.js index d937d90b..8c2ce2b3 100644 --- a/test/unit/embed.js +++ b/test/unit/embed.js @@ -6,6 +6,19 @@ const sharp = require('../../'); const fixtures = require('../fixtures'); describe('Embed', function () { + it('Allows specifying the gravity as a string', function (done) { + sharp(fixtures.inputJpg) + .resize(320, 240) + .embed('center') + .png() + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('embed-3-into-3.png'), data, done); + }); + }); + it('JPEG within PNG, no alpha channel', function (done) { sharp(fixtures.inputJpg) .embed() @@ -132,4 +145,313 @@ describe('Embed', function () { fixtures.assertSimilar(fixtures.expected('embed-enlarge.png'), data, done); }); }); + + it('Embed invalid param values should fail', function () { + assert.throws(function () { + sharp().embed(-1); + }); + assert.throws(function () { + sharp().embed(8.1); + }); + assert.throws(function () { + sharp().embed(9); + }); + assert.throws(function () { + sharp().embed(1000000); + }); + assert.throws(function () { + sharp().embed(false); + }); + assert.throws(function () { + sharp().embed('vallejo'); + }); + }); + + it('Embed gravity horizontal northwest', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 100) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.northwest) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(100, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a1-nw.png'), data, done); + }); + }); + + it('Embed gravity horizontal north', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 100) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.north) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(100, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a2-n.png'), data, done); + }); + }); + + it('Embed gravity horizontal northeast', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 100) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.northeast) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(100, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a3-ne.png'), data, done); + }); + }); + + it('Embed gravity horizontal east', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 100) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.east) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(100, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a4-e.png'), data, done); + }); + }); + + it('Embed gravity horizontal southeast', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 100) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.southeast) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(100, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a5-se.png'), data, done); + }); + }); + + it('Embed gravity horizontal south', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 100) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.south) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(100, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a6-s.png'), data, done); + }); + }); + + it('Embed gravity horizontal southwest', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 100) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.southwest) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(100, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a7-sw.png'), data, done); + }); + }); + + it('Embed gravity horizontal west', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 100) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.west) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(100, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a8-w.png'), data, done); + }); + }); + + it('Embed gravity horizontal center', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 100) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.center) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(100, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/a9-c.png'), data, done); + }); + }); + + it('Embed gravity vertical northwest', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 200) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.northwest) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(200, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/1-nw.png'), data, done); + }); + }); + + it('Embed gravity vertical north', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 200) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.north) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(200, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/2-n.png'), data, done); + }); + }); + + it('Embed gravity vertical northeast', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 200) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.northeast) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(200, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/3-ne.png'), data, done); + }); + }); + + it('Embed gravity vertical east', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 200) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.east) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(200, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/4-e.png'), data, done); + }); + }); + + it('Embed gravity vertical southeast', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 200) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.southeast) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(200, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/5-se.png'), data, done); + }); + }); + + it('Embed gravity vertical south', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 200) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.south) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(200, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/6-s.png'), data, done); + }); + }); + + it('Embed gravity vertical southwest', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 200) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.southwest) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(200, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/7-sw.png'), data, done); + }); + }); + + it('Embed gravity vertical west', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 200) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.west) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(200, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/8-w.png'), data, done); + }); + }); + + it('Embed gravity vertical center', function (done) { + sharp(fixtures.inputPngEmbed) + .resize(200, 200) + .background({r: 0, g: 0, b: 0, alpha: 0}) + .embed(sharp.gravity.center) + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(200, info.width); + assert.strictEqual(200, info.height); + assert.strictEqual(4, info.channels); + fixtures.assertSimilar(fixtures.expected('./embedgravitybird/9-c.png'), data, done); + }); + }); });