diff --git a/lib/constructor.js b/lib/constructor.js index 12617b63..c49394aa 100644 --- a/lib/constructor.js +++ b/lib/constructor.js @@ -93,6 +93,9 @@ const debuglog = util.debuglog('sharp'); * a String containing the path to an JPEG, PNG, WebP, GIF, SVG or TIFF image file. * JPEG, PNG, WebP, GIF, SVG, TIFF or raw pixel image data can be streamed into the object when not present. * @param {Object} [options] - if present, is an Object with optional attributes. + * @param {Boolean} [options.failOnError=false] - by default apply a "best effort" + * to decode images, even if the data is corrupt or invalid. Set this flag to true + * if you'd rather halt processing and raise an error when loading invalid images. * @param {Number} [options.density=72] - integral number representing the DPI for vector images. * @param {Object} [options.raw] - describes raw pixel input image data. See `raw()` for pixel ordering. * @param {Number} [options.raw.width] diff --git a/lib/input.js b/lib/input.js index 3a0f534c..eabd147c 100644 --- a/lib/input.js +++ b/lib/input.js @@ -9,7 +9,7 @@ const sharp = require('../build/Release/sharp.node'); * @private */ function _createInputDescriptor (input, inputOptions, containerOptions) { - const inputDescriptor = {}; + const inputDescriptor = { failOnError: false }; if (is.string(input)) { // filesystem inputDescriptor.file = input; @@ -26,6 +26,14 @@ function _createInputDescriptor (input, inputOptions, containerOptions) { throw new Error('Unsupported input ' + typeof input); } if (is.object(inputOptions)) { + // Fail on error + if (is.defined(inputOptions.failOnError)) { + if (is.bool(inputOptions.failOnError)) { + inputDescriptor.failOnError = inputOptions.failOnError; + } else { + throw new Error('Invalid failOnError (boolean) ' + inputOptions.failOnError); + } + } // Density if (is.defined(inputOptions.density)) { if (is.integer(inputOptions.density) && is.inRange(inputOptions.density, 1, 2400)) { diff --git a/src/common.cc b/src/common.cc index 024a163d..cd8dfbe6 100644 --- a/src/common.cc +++ b/src/common.cc @@ -52,6 +52,7 @@ namespace sharp { descriptor->buffer = node::Buffer::Data(buffer); buffersToPersist.push_back(buffer); } + descriptor->failOnError = AttrTo(input, "failOnError"); // Density for vector-based input if (HasAttr(input, "density")) { descriptor->density = AttrTo(input, "density"); @@ -219,7 +220,9 @@ namespace sharp { imageType = DetermineImageType(descriptor->buffer, descriptor->bufferLength); if (imageType != ImageType::UNKNOWN) { try { - vips::VOption *option = VImage::option()->set("access", accessMethod); + vips::VOption *option = VImage::option() + ->set("access", accessMethod) + ->set("fail", descriptor->failOnError); if (imageType == ImageType::SVG || imageType == ImageType::PDF) { option->set("dpi", static_cast(descriptor->density)); } @@ -256,7 +259,9 @@ namespace sharp { imageType = DetermineImageType(descriptor->file.data()); if (imageType != ImageType::UNKNOWN) { try { - vips::VOption *option = VImage::option()->set("access", accessMethod); + vips::VOption *option = VImage::option() + ->set("access", accessMethod) + ->set("fail", descriptor->failOnError); if (imageType == ImageType::SVG || imageType == ImageType::PDF) { option->set("dpi", static_cast(descriptor->density)); } diff --git a/src/common.h b/src/common.h index 96a577d4..874a4f2c 100644 --- a/src/common.h +++ b/src/common.h @@ -47,6 +47,7 @@ namespace sharp { std::string name; std::string file; char *buffer; + bool failOnError; size_t bufferLength; int density; int rawChannels; @@ -59,6 +60,7 @@ namespace sharp { InputDescriptor(): buffer(nullptr), + failOnError(FALSE), bufferLength(0), density(72), rawChannels(0), diff --git a/src/pipeline.cc b/src/pipeline.cc index 517b10df..cc7ecbba 100644 --- a/src/pipeline.cc +++ b/src/pipeline.cc @@ -249,7 +249,9 @@ class PipelineWorker : public Nan::AsyncWorker { } if (shrink_on_load > 1) { // Reload input using shrink-on-load - vips::VOption *option = VImage::option()->set("shrink", shrink_on_load); + vips::VOption *option = VImage::option() + ->set("shrink", shrink_on_load) + ->set("fail", baton->input->failOnError); if (baton->input->buffer != nullptr) { VipsBlob *blob = vips_blob_new(nullptr, baton->input->buffer, baton->input->bufferLength); if (inputImageType == ImageType::JPEG) { diff --git a/test/fixtures/expected/truncated.jpg b/test/fixtures/expected/truncated.jpg new file mode 100644 index 00000000..0bd7b808 Binary files /dev/null and b/test/fixtures/expected/truncated.jpg differ diff --git a/test/fixtures/index.js b/test/fixtures/index.js index 2876fd87..ee9aaa02 100644 --- a/test/fixtures/index.js +++ b/test/fixtures/index.js @@ -66,6 +66,7 @@ module.exports = { inputJpgLarge: getPath('giant-image.jpg'), inputJpg320x240: getPath('320x240.jpg'), // http://www.andrewault.net/2010/01/26/create-a-test-pattern-video-with-perl/ inputJpgOverlayLayer2: getPath('alpha-layer-2-ink.jpg'), + inputJpgTruncated: getPath('truncated.jpg'), // head -c 10000 2569067123_aca715a2ee_o.jpg > truncated.jpg inputPng: getPath('50020484-00001.png'), // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png inputPngWithTransparency: getPath('blackbug.png'), // public domain @@ -81,6 +82,7 @@ module.exports = { inputPngAlphaPremultiplicationLarge: getPath('alpha-premultiply-2048x1536-paper.png'), 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 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/fixtures/truncated.jpg b/test/fixtures/truncated.jpg new file mode 100644 index 00000000..2a3fe7a7 Binary files /dev/null and b/test/fixtures/truncated.jpg differ diff --git a/test/fixtures/truncated.png b/test/fixtures/truncated.png new file mode 100644 index 00000000..3ab39b69 Binary files /dev/null and b/test/fixtures/truncated.png differ diff --git a/test/unit/failOnError.js b/test/unit/failOnError.js new file mode 100644 index 00000000..b6a2957f --- /dev/null +++ b/test/unit/failOnError.js @@ -0,0 +1,77 @@ +'use strict'; + +const assert = require('assert'); + +const sharp = require('../../'); +const fixtures = require('../fixtures'); + +describe('failOnError', function () { + it('handles truncated JPEG by default', function (done) { + sharp(fixtures.inputJpgTruncated) + .resize(320, 240) + // .toFile(fixtures.expected('truncated.jpg'), done); + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual('jpeg', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + fixtures.assertSimilar(fixtures.expected('truncated.jpg'), data, done); + }); + }); + + it('handles truncated PNG by default', function (done) { + sharp(fixtures.inputPngTruncated) + .resize(320, 240) + // .toFile(fixtures.expected('truncated.png'), done); + .toBuffer(function (err, data, info) { + if (err) throw err; + assert.strictEqual('png', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + done(); + }); + }); + + it('rejects invalid values', function () { + assert.doesNotThrow(function () { + sharp(fixtures.inputJpg, { failOnError: true }); + }); + + assert.throws(function () { + sharp(fixtures.inputJpg, { failOnError: 'zoinks' }); + }); + + assert.throws(function () { + sharp(fixtures.inputJpg, { failOnError: 1 }); + }); + }); + + it('returns errors to callback for truncated JPEG when failOnError is set', function (done) { + sharp(fixtures.inputJpgTruncated, { failOnError: true }).toBuffer(function (err, data, info) { + assert.ok(err.message.includes('VipsJpeg: Premature end of JPEG file'), err); + assert.equal(data, null); + assert.equal(info, null); + done(); + }); + }); + + it('returns errors to callback for truncated PNG when failOnError is set', function (done) { + sharp(fixtures.inputPngTruncated, { failOnError: true }).toBuffer(function (err, data, info) { + assert.ok(err.message.includes('vipspng: libpng read error'), err); + assert.equal(data, null); + assert.equal(info, null); + done(); + }); + }); + + it('rejects promises for truncated JPEG when failOnError is set', function (done) { + sharp(fixtures.inputJpgTruncated, { failOnError: true }) + .toBuffer() + .then(() => { + throw new Error('Expected rejection'); + }) + .catch(err => { + done(err.message.includes('VipsJpeg: Premature end of JPEG file') ? undefined : err); + }); + }); +});