diff --git a/README.md b/README.md index d76e5bd72..3b43d8cea 100755 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ This module is powered by the blazingly fast [libvips](https://github.com/jcupit ### Prerequisites * Node.js v0.10+ -* [libvips](https://github.com/jcupitt/libvips) v7.38.5+ +* [libvips](https://github.com/jcupitt/libvips) v7.38.5+ (7.40.9+ recommended) To install the latest version of libvips on the following Operating Systems: @@ -214,12 +214,12 @@ sharp(inputBuffer) Constructor to which further methods are chained. `input`, if present, can be one of: -* Buffer containing JPEG, PNG or WebP image data, or +* Buffer containing JPEG, PNG, WebP or TIFF (libvips 7.40.0+) image data, or * String containing the filename of an image, with most major formats supported. The object returned implements the [stream.Duplex](http://nodejs.org/api/stream.html#stream_class_stream_duplex) class. -JPEG, PNG or WebP format image data can be streamed into the object when `input` is not provided. +JPEG, PNG, WebP or TIFF (libvips 7.40.0+) format image data can be streamed into the object when `input` is not provided. JPEG, PNG or WebP format image data can be streamed out from this object. @@ -388,6 +388,12 @@ An advanced setting for the _zlib_ compression level of the lossless PNG output `compressionLevel` is a Number between 0 and 9. +#### withoutAdaptiveFiltering() + +_Requires libvips 7.41.0+_ + +An advanced and experimental PNG output setting to disable adaptive row filtering. + ### Output methods #### toFile(filename, [callback]) diff --git a/index.js b/index.js index 7a817d198..474c7270d 100755 --- a/index.js +++ b/index.js @@ -3,10 +3,12 @@ var util = require('util'); var stream = require('stream'); +var semver = require('semver'); var color = require('color'); var BluebirdPromise = require('bluebird'); var sharp = require('./build/Release/sharp'); +var libvipsVersion = sharp.libvipsVersion(); var Sharp = function(input) { if (!(this instanceof Sharp)) { @@ -46,6 +48,7 @@ var Sharp = function(input) { progressive: false, quality: 80, compressionLevel: 6, + withoutAdaptiveFiltering: false, streamOut: false, withMetadata: false }; @@ -55,14 +58,22 @@ var Sharp = function(input) { } else if (typeof input === 'object' && input instanceof Buffer) { // input=buffer if ( - (input.length > 1) && - (input[0] === 0xff && input[1] === 0xd8) || // JPEG - (input[0] === 0x89 && input[1] === 0x50) || // PNG - (input[0] === 0x52 && input[1] === 0x49) // WebP + (input.length > 3) && + // JPEG + (input[0] === 0xFF && input[1] === 0xD8) || + // PNG + (input[0] === 0x89 && input[1] === 0x50) || + // WebP + (input[0] === 0x52 && input[1] === 0x49) || + // TIFF - requires libvips 7.40.0+ + (semver.gte(libvipsVersion, '7.40.0') && ( + (input[0] === 0x4D && input[1] === 0x4D && input[2] === 0x00 && (input[3] === 0x2A || input[3] === 0x2B)) || + (input[0] === 0x49 && input[1] === 0x49 && (input[2] === 0x2A || input[2] === 0x2B) && input[3] === 0x00) + )) ) { this.options.bufferIn = input; } else { - throw new Error('Buffer contains an unsupported image format. JPEG, PNG and WebP are currently supported.'); + throw new Error('Buffer contains an unsupported image format. JPEG, PNG, WebP and TIFF are currently supported.'); } } else { // input=stream @@ -266,6 +277,9 @@ Sharp.prototype.quality = function(quality) { return this; }; +/* + zlib compression level for PNG output +*/ Sharp.prototype.compressionLevel = function(compressionLevel) { if (!Number.isNaN(compressionLevel) && compressionLevel >= 0 && compressionLevel <= 9) { this.options.compressionLevel = compressionLevel; @@ -275,6 +289,18 @@ Sharp.prototype.compressionLevel = function(compressionLevel) { return this; }; +/* + Disable the use of adaptive row filtering for PNG output - requires libvips 7.41.0+ +*/ +Sharp.prototype.withoutAdaptiveFiltering = function(withoutAdaptiveFiltering) { + if (semver.gte(libvipsVersion, '7.41.0')) { + this.options.withoutAdaptiveFiltering = (typeof withoutAdaptiveFiltering === 'boolean') ? withoutAdaptiveFiltering : true; + } else { + console.error('withoutAdaptiveFiltering requires libvips 7.41.0+'); + } + return this; +}; + Sharp.prototype.withMetadata = function(withMetadata) { this.options.withMetadata = (typeof withMetadata === 'boolean') ? withMetadata : true; return this; @@ -503,3 +529,10 @@ module.exports.concurrency = function(concurrency) { module.exports.counters = function() { return sharp.counters(); }; + +/* + Get the version of the libvips library +*/ +module.exports.libvipsVersion = function() { + return libvipsVersion; +}; diff --git a/package.json b/package.json index c859a3352..a4dc4d872 100755 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "dependencies": { "bluebird": "^2.3.9", "color": "^0.7.1", - "nan": "^1.3.0" + "nan": "^1.3.0", + "semver": "^4.1.0" }, "devDependencies": { "mocha": "^2.0.1", diff --git a/src/common.cc b/src/common.cc index cfe3fb5db..6bd304c72 100755 --- a/src/common.cc +++ b/src/common.cc @@ -27,12 +27,22 @@ bool is_tiff(std::string const &str) { return ends_with(str, ".tif") || ends_with(str, ".tiff") || ends_with(str, ".TIF") || ends_with(str, ".TIFF"); } +// Buffer content checkers +static bool buffer_is_tiff(char *buffer, size_t len) { + return ( + len >= 4 && ( + (buffer[0] == 'M' && buffer[1] == 'M' && buffer[2] == '\0' && (buffer[3] == '*' || buffer[3] == '+')) || + (buffer[0] == 'I' && buffer[1] == 'I' && (buffer[2] == '*' || buffer[2] == '+') && buffer[3] == '\0') + ) + ); +} + unsigned char const MARKER_JPEG[] = {0xff, 0xd8}; unsigned char const MARKER_PNG[] = {0x89, 0x50}; unsigned char const MARKER_WEBP[] = {0x52, 0x49}; /* - Initialise a VipsImage from a buffer. Supports JPEG, PNG and WebP. + Initialise a VipsImage from a buffer. Supports JPEG, PNG, WebP and TIFF. Returns the ImageType detected, if any. */ ImageType @@ -42,14 +52,20 @@ sharp_init_image_from_buffer(VipsImage **image, void *buffer, size_t const lengt if (!vips_jpegload_buffer(buffer, length, image, "access", access, NULL)) { imageType = JPEG; } - } else if(memcmp(MARKER_PNG, buffer, 2) == 0) { + } else if (memcmp(MARKER_PNG, buffer, 2) == 0) { if (!vips_pngload_buffer(buffer, length, image, "access", access, NULL)) { imageType = PNG; } - } else if(memcmp(MARKER_WEBP, buffer, 2) == 0) { + } else if (memcmp(MARKER_WEBP, buffer, 2) == 0) { if (!vips_webpload_buffer(buffer, length, image, "access", access, NULL)) { imageType = WEBP; } +#if (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 40) + } else if (buffer_is_tiff(static_cast(buffer), length)) { + if (!vips_tiffload_buffer(buffer, length, image, "access", access, NULL)) { + imageType = TIFF; + } +#endif } return imageType; } diff --git a/src/resize.cc b/src/resize.cc index eb111d4d1..8afdecc35 100755 --- a/src/resize.cc +++ b/src/resize.cc @@ -60,6 +60,7 @@ struct ResizeBaton { VipsAccess accessMethod; int quality; int compressionLevel; + bool withoutAdaptiveFiltering; std::string err; bool withMetadata; @@ -504,7 +505,8 @@ class ResizeWorker : public NanAsyncWorker { image = colourspaced; } - // Generate image tile cache when interlace output is required +#if !(VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 40 && VIPS_MINOR_VERSION >= 5) + // Generate image tile cache when interlace output is required - no longer required as of libvips 7.40.5+ if (baton->progressive) { VipsImage *cached = vips_image_new(); vips_object_local(hook, cached); @@ -514,6 +516,7 @@ class ResizeWorker : public NanAsyncWorker { g_object_unref(image); image = cached; } +#endif // Output if (baton->output == "__jpeg" || (baton->output == "__input" && inputImageType == JPEG)) { @@ -524,11 +527,21 @@ class ResizeWorker : public NanAsyncWorker { } baton->outputFormat = "jpeg"; } else if (baton->output == "__png" || (baton->output == "__input" && inputImageType == PNG)) { +#if (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 41) + // Select PNG row filter + int filter = baton->withoutAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_NONE : VIPS_FOREIGN_PNG_FILTER_ALL; + // Write PNG to buffer + if (vips_pngsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata, + "compression", baton->compressionLevel, "interlace", baton->progressive, "filter", filter, NULL)) { + return Error(baton, hook); + } +#else // Write PNG to buffer if (vips_pngsave_buffer(image, &baton->bufferOut, &baton->bufferOutLength, "strip", !baton->withMetadata, "compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) { return Error(baton, hook); } +#endif baton->outputFormat = "png"; } else if (baton->output == "__webp" || (baton->output == "__input" && inputImageType == WEBP)) { // Write WEBP to buffer @@ -551,11 +564,21 @@ class ResizeWorker : public NanAsyncWorker { } baton->outputFormat = "jpeg"; } else if (output_png || (match_input && inputImageType == PNG)) { +#if (VIPS_MAJOR_VERSION >= 7 && VIPS_MINOR_VERSION >= 41) + // Select PNG row filter + int filter = baton->withoutAdaptiveFiltering ? VIPS_FOREIGN_PNG_FILTER_NONE : VIPS_FOREIGN_PNG_FILTER_ALL; + // Write PNG to file + if (vips_pngsave(image, baton->output.c_str(), "strip", !baton->withMetadata, + "compression", baton->compressionLevel, "interlace", baton->progressive, "filter", filter, NULL)) { + return Error(baton, hook); + } +#else // Write PNG to file if (vips_pngsave(image, baton->output.c_str(), "strip", !baton->withMetadata, "compression", baton->compressionLevel, "interlace", baton->progressive, NULL)) { return Error(baton, hook); } +#endif baton->outputFormat = "png"; } else if (output_webp || (match_input && inputImageType == WEBP)) { // Write WEBP to file @@ -780,6 +803,7 @@ NAN_METHOD(resize) { baton->progressive = options->Get(NanNew("progressive"))->BooleanValue(); baton->quality = options->Get(NanNew("quality"))->Int32Value(); baton->compressionLevel = options->Get(NanNew("compressionLevel"))->Int32Value(); + baton->withoutAdaptiveFiltering = options->Get(NanNew("withoutAdaptiveFiltering"))->BooleanValue(); baton->withMetadata = options->Get(NanNew("withMetadata"))->BooleanValue(); // Output filename or __format for Buffer baton->output = *String::Utf8Value(options->Get(NanNew("output"))->ToString()); diff --git a/src/sharp.cc b/src/sharp.cc index 93e3dcd7a..cc6791058 100755 --- a/src/sharp.cc +++ b/src/sharp.cc @@ -33,6 +33,7 @@ extern "C" void init(Handle target) { NODE_SET_METHOD(target, "cache", cache); NODE_SET_METHOD(target, "concurrency", concurrency); NODE_SET_METHOD(target, "counters", counters); + NODE_SET_METHOD(target, "libvipsVersion", libvipsVersion); } NODE_MODULE(sharp, init) diff --git a/src/utilities.cc b/src/utilities.cc index ea31f36e6..0ee2fd824 100755 --- a/src/utilities.cc +++ b/src/utilities.cc @@ -62,3 +62,13 @@ NAN_METHOD(counters) { counters->Set(NanNew("process"), NanNew(counter_process)); NanReturnValue(counters); } + +/* + Get libvips version +*/ +NAN_METHOD(libvipsVersion) { + NanScope(); + char version[9]; + snprintf(version, 9, "%d.%d.%d", vips_version(0), vips_version(1), vips_version(2)); + NanReturnValue(NanNew(version)); +} diff --git a/src/utilities.h b/src/utilities.h index 6e0df7ca4..9f6f80d3a 100755 --- a/src/utilities.h +++ b/src/utilities.h @@ -6,5 +6,6 @@ NAN_METHOD(cache); NAN_METHOD(concurrency); NAN_METHOD(counters); +NAN_METHOD(libvipsVersion); #endif diff --git a/test/bench/package.json b/test/bench/package.json index 56d7b9738..cc3ea39c8 100755 --- a/test/bench/package.json +++ b/test/bench/package.json @@ -10,7 +10,7 @@ "devDependencies": { "imagemagick": "^0.1.3", "imagemagick-native": "^1.4.0", - "gm": "^1.16.0", + "gm": "^1.17.0", "async": "^0.9.0", "benchmark": "^1.0.0" }, diff --git a/test/bench/perf.js b/test/bench/perf.js index eb885e6eb..3016dc49a 100755 --- a/test/bench/perf.js +++ b/test/bench/perf.js @@ -422,6 +422,18 @@ async.series({ } }); } + }).add('sharp-withoutAdaptiveFiltering', { + defer: true, + fn: function(deferred) { + sharp(inputPngBuffer).resize(width, height).withoutAdaptiveFiltering().toBuffer(function(err, buffer) { + if (err) { + throw err; + } else { + assert.notStrictEqual(null, buffer); + deferred.resolve(); + } + }); + } }).on('cycle', function(event) { console.log(' png ' + String(event.target)); }).on('complete', function() { diff --git a/test/unit/io.js b/test/unit/io.js index 328ad0176..e50a3adad 100755 --- a/test/unit/io.js +++ b/test/unit/io.js @@ -3,6 +3,8 @@ var fs = require('fs'); var assert = require('assert'); +var semver = require('semver'); + var sharp = require('../../index'); var fixtures = require('../fixtures'); @@ -343,9 +345,9 @@ describe('Input/output', function() { }); - describe('PNG compression level', function() { + describe('PNG output', function() { - it('valid', function(done) { + it('compression level is valid', function(done) { var isValid = false; try { sharp().compressionLevel(0); @@ -355,7 +357,7 @@ describe('Input/output', function() { done(); }); - it('invalid', function(done) { + it('compression level is invalid', function(done) { var isValid = false; try { sharp().compressionLevel(-1); @@ -365,6 +367,52 @@ describe('Input/output', function() { done(); }); + if (semver.gte(sharp.libvipsVersion(), '7.41.0')) { + it('withoutAdaptiveFiltering generates smaller file [libvips 7.41.0+]', function(done) { + // First generate with adaptive filtering + sharp(fixtures.inputPng) + .resize(320, 240) + .withoutAdaptiveFiltering(false) + .toBuffer(function(err, dataAdaptive, info) { + if (err) throw err; + assert.strictEqual(true, dataAdaptive.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + // Then generate without + sharp(fixtures.inputPng) + .resize(320, 240) + .withoutAdaptiveFiltering() + .toBuffer(function(err, dataWithoutAdaptive, info) { + if (err) throw err; + assert.strictEqual(true, dataWithoutAdaptive.length > 0); + assert.strictEqual('png', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + assert.strictEqual(true, dataWithoutAdaptive.length < dataAdaptive.length); + done(); + }); + }); + }); + } + }); + if (semver.gte(sharp.libvipsVersion(), '7.40.0')) { + it('Load TIFF from Buffer [libvips 7.40.0+]', function(done) { + var inputTiffBuffer = fs.readFileSync(fixtures.inputTiff); + sharp(inputTiffBuffer) + .resize(320, 240) + .jpeg() + .toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual('jpeg', info.format); + assert.strictEqual(320, info.width); + assert.strictEqual(240, info.height); + done(); + }); + }); + } + });