diff --git a/.gitignore b/.gitignore index e5c5a80c..688e56d5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ logs results build node_modules -tests/output.jpg -tests/output.png +tests/output.* npm-debug.log diff --git a/README.md b/README.md index 88f5e293..25877c3e 100755 --- a/README.md +++ b/README.md @@ -7,10 +7,12 @@ _adj_ 3. shrewd or astute: a sharp bargainer. 4. (Informal.) very stylish: a sharp dresser; a sharp jacket. -The typical use case for this high speed Node.js module is to convert large JPEG and PNG images to smaller JPEG and PNG images of varying dimensions. +The typical use case for this high speed Node.js module is to convert large JPEG, PNG, WebP and TIFF images to smaller images of varying dimensions. The performance of JPEG resizing is typically 15x-25x faster than ImageMagick and GraphicsMagick, based mainly on the number of CPU cores available. +This module supports reading and writing images to and from both the filesystem and Buffer objects (TIFF is limited to filesystem only). Everything remains non-blocking thanks to _libuv_. + Under the hood you'll find the blazingly fast [libvips](https://github.com/jcupitt/libvips) image processing library, originally created in 1989 at Birkbeck College and currently maintained by John Cupitt. ## Prerequisites @@ -32,9 +34,9 @@ For the sharpest results, please compile libvips from source. Scale and crop to `width` x `height` calling `callback` when complete. -`input` can either be a filename String or a Buffer. When using a filename libvips will `mmap` the file for improved performance. +`input` can either be a filename String or a Buffer. -`output` can either be a filename String or one of `sharp.buffer.jpeg` or `sharp.buffer.png` to pass a Buffer containing image data to `callback`. +`output` can either be a filename String or one of `sharp.buffer.jpeg`, `sharp.buffer.png` or `sharp.buffer.webp` to pass a Buffer containing JPEG, PNG or WebP image data to `callback`. `width` is the Number of pixels wide the resultant image should be. @@ -71,7 +73,7 @@ sharp.resize("input.jpg", sharp.buffer.jpeg, 300, 200, {progressive: true}, func ``` ```javascript -sharp.resize("input.jpg", sharp.buffer.png, 300, 200, {sharpen: true}, function(err, buffer) { +sharp.resize("input.webp", sharp.buffer.png, 300, 200, {sharpen: true}, function(err, buffer) { if (err) { throw err; } @@ -80,21 +82,21 @@ sharp.resize("input.jpg", sharp.buffer.png, 300, 200, {sharpen: true}, function( ``` ```javascript -sharp.resize(buffer, "output.jpg", 200, 300, {canvas: sharp.canvas.embedWhite}, function(err) { +sharp.resize(buffer, "output.tiff", 200, 300, {canvas: sharp.canvas.embedWhite}, function(err) { if (err) { throw err; } - // output.jpg is a 200 pixels wide and 300 pixels high image containing a scaled version + // output.tiff is a 200 pixels wide and 300 pixels high image containing a scaled version // of the image data contained in buffer embedded on a white canvas }); ``` ```javascript -sharp.resize("input.jpg", sharp.buffer.jpeg, 200, 300, {canvas: sharp.canvas.embedBlack}, function(err, buffer) { +sharp.resize("input.jpg", sharp.buffer.webp, 200, 300, {canvas: sharp.canvas.embedBlack}, function(err, buffer) { if (err) { throw err; } - // buffer contains JPEG image data of a 200 pixels wide and 300 pixels high image + // buffer contains WebP image data of a 200 pixels wide and 300 pixels high image // containing a scaled version of input.png embedded on a black canvas }); ``` @@ -108,10 +110,12 @@ sharp.resize("input.jpg", sharp.buffer.jpeg, 200, 300, {canvas: sharp.canvas.emb Test environment: * AMD Athlon 4 core 3.3GHz 512KB L2 CPU 1333 DDR3 -* libvips 7.37 +* libvips 7.38 * libjpeg-turbo8 1.3.0 * libpng 1.6.6 * zlib1g 1.2.7 +* libwebp 0.3.0 +* libtiff 4.0.2 `-file-buffer` indicates read from file and write to buffer, `-buffer-file` indicates read from buffer and write to file etc. @@ -149,6 +153,21 @@ Test environment: * sharp-file-buffer-progressive x 46.35 ops/sec ±0.20% (76 runs sampled) * sharp-file-buffer-sequentialRead x 29.02 ops/sec ±0.62% (72 runs sampled) +### WebP + +* sharp-buffer-file x 3.30 ops/sec ±117.14% (19 runs sampled) +* sharp-buffer-buffer x 7.66 ops/sec ±5.83% (43 runs sampled) +* sharp-file-file x 9.88 ops/sec ±0.98% (52 runs sampled) +* sharp-file-buffer x 9.95 ops/sec ±0.25% (52 runs sampled) +* sharp-file-buffer-sharpen x 9.05 ops/sec ±0.36% (48 runs sampled) +* sharp-file-buffer-sequentialRead x 9.87 ops/sec ±0.98% (52 runs sampled) + +### TIFF + +* sharp-file-file x 68.24 ops/sec ±5.93% (85 runs sampled) +* sharp-file-file-sharpen x 50.76 ops/sec ±0.52% (82 runs sampled) +* sharp-file-file-sequentialRead x 36.37 ops/sec ±0.90% (87 runs sampled) + ## Licence Copyright 2013, 2014 Lovell Fuller diff --git a/index.js b/index.js index c1c3aba6..b68da114 100755 --- a/index.js +++ b/index.js @@ -2,7 +2,8 @@ var sharp = require("./build/Release/sharp"); module.exports.buffer = { jpeg: "__jpeg", - png: "__png" + png: "__png", + webp: "__webp" }; module.exports.canvas = { diff --git a/package.json b/package.json index 6dde76e0..552665b6 100755 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "sharp", - "version": "0.1.5", + "version": "0.1.6", "author": "Lovell Fuller", - "description": "High performance module to resize JPEG and PNG images using the libvips image processing library", + "description": "High performance module to resize JPEG, PNG, WebP and TIFF images using the libvips image processing library", "scripts": { "test": "node tests/perf.js" }, @@ -14,12 +14,15 @@ "keywords": [ "jpeg", "png", + "webp", + "tiff", "resize", "thumbnail", "sharpen", "crop", "embed", "libvips", + "vips", "fast", "buffer" ], diff --git a/src/sharp.cc b/src/sharp.cc index 2d0dcad3..a838f0d1 100755 --- a/src/sharp.cc +++ b/src/sharp.cc @@ -30,22 +30,33 @@ struct resize_baton { typedef enum { JPEG, - PNG + PNG, + WEBP, + TIFF } ImageType; unsigned char MARKER_JPEG[] = {0xff, 0xd8}; unsigned char MARKER_PNG[] = {0x89, 0x50}; +unsigned char MARKER_WEBP[] = {0x52, 0x49}; bool ends_with(std::string const &str, std::string const &end) { return str.length() >= end.length() && 0 == str.compare(str.length() - end.length(), end.length(), end); } bool is_jpeg(std::string const &str) { - return ends_with(str, ".jpg") || ends_with(str, ".jpeg"); + return ends_with(str, ".jpg") || ends_with(str, ".jpeg") || ends_with(str, ".JPG") || ends_with(str, ".JPEG"); } bool is_png(std::string const &str) { - return ends_with(str, ".png"); + return ends_with(str, ".png") || ends_with(str, ".PNG"); +} + +bool is_webp(std::string const &str) { + return ends_with(str, ".webp") || ends_with(str, ".WEBP"); +} + +bool is_tiff(std::string const &str) { + return ends_with(str, ".tif") || ends_with(str, ".tiff") || ends_with(str, ".TIF") || ends_with(str, ".TIFF"); } void resize_error(resize_baton *baton, VipsImage *unref) { @@ -71,6 +82,15 @@ void resize_async(uv_work_t *work) { if (vips_pngload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) { return resize_error(baton, in); } + } else if(memcmp(MARKER_WEBP, baton->buffer_in, 2) == 0) { + inputImageType = WEBP; + if (vips_webpload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) { + return resize_error(baton, in); + } + } else { + resize_error(baton, in); + (baton->err).append("Unsupported input buffer"); + return; } } else if (is_jpeg(baton->file_in)) { if (vips_jpegload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) { @@ -81,9 +101,19 @@ void resize_async(uv_work_t *work) { if (vips_pngload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) { return resize_error(baton, in); } + } else if (is_webp(baton->file_in)) { + inputImageType = WEBP; + if (vips_webpload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) { + return resize_error(baton, in); + } + } else if (is_tiff(baton->file_in)) { + inputImageType = TIFF; + if (vips_tiffload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) { + return resize_error(baton, in); + } } else { resize_error(baton, in); - (baton->err).append("Unsupported input " + baton->file_in); + (baton->err).append("Unsupported input file " + baton->file_in); return; } @@ -201,6 +231,11 @@ void resize_async(uv_work_t *work) { if (vips_pngsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) { return resize_error(baton, sharpened); } + } else if (baton->file_out == "__webp") { + // Write WEBP to buffer + if (vips_webpsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, NULL)) { + return resize_error(baton, sharpened); + } } else if (is_jpeg(baton->file_out)) { // Write JPEG to file if (vips_jpegsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) { @@ -211,6 +246,16 @@ void resize_async(uv_work_t *work) { if (vips_pngsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) { return resize_error(baton, sharpened); } + } else if (is_webp(baton->file_out)) { + // Write WEBP to file + if (vips_webpsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", 80, NULL)) { + return resize_error(baton, sharpened); + } + } else if (is_tiff(baton->file_out)) { + // Write TIFF to file + if (vips_tiffsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", VIPS_FOREIGN_TIFF_COMPRESSION_JPEG, "Q", 80, NULL)) { + return resize_error(baton, sharpened); + } } else { (baton->err).append("Unsupported output " + baton->file_out); } diff --git a/tests/4.webp b/tests/4.webp new file mode 100644 index 00000000..a608fc85 Binary files /dev/null and b/tests/4.webp differ diff --git a/tests/G31D.TIF b/tests/G31D.TIF new file mode 100644 index 00000000..697ed21b Binary files /dev/null and b/tests/G31D.TIF differ diff --git a/tests/perf.js b/tests/perf.js index 4c5b8198..414ddbd1 100755 --- a/tests/perf.js +++ b/tests/perf.js @@ -13,6 +13,12 @@ var outputJpg = __dirname + "/output.jpg"; var inputPng = __dirname + "/50020484-00001.png"; // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png var outputPng = __dirname + "/output.png"; +var inputWebp = __dirname + "/4.webp"; // http://www.gstatic.com/webp/gallery/4.webp +var outputWebp = __dirname + "/output.webp"; + +var inputTiff = __dirname + "/G31D.TIF"; // http://www.fileformat.info/format/tiff/sample/e6c9a6e5253348f4aef6d17b534360ab/index.htm +var outputTiff = __dirname + "/output.tiff"; + var width = 720; var height = 480; @@ -290,7 +296,125 @@ async.series({ }).on("complete", function() { callback(null, this.filter("fastest").pluck("name")); }).run(); - } + }, + webp: function(callback) { + var inputWebpBuffer = fs.readFileSync(inputWebp); + (new Benchmark.Suite("webp")).add("sharp-buffer-file", { + defer: true, + fn: function(deferred) { + sharp.resize(inputWebpBuffer, outputWebp, width, height, function(err) { + if (err) { + throw err; + } else { + deferred.resolve(); + } + }); + } + }).add("sharp-buffer-buffer", { + defer: true, + fn: function(deferred) { + sharp.resize(inputWebpBuffer, sharp.buffer.webp, width, height, function(err, buffer) { + if (err) { + throw err; + } else { + assert.notStrictEqual(null, buffer); + deferred.resolve(); + } + }); + } + }).add("sharp-file-file", { + defer: true, + fn: function(deferred) { + sharp.resize(inputWebp, outputWebp, width, height, function(err) { + if (err) { + throw err; + } else { + deferred.resolve(); + } + }); + } + }).add("sharp-file-buffer", { + defer: true, + fn: function(deferred) { + sharp.resize(inputWebp, sharp.buffer.webp, width, height, function(err, buffer) { + if (err) { + throw err; + } else { + assert.notStrictEqual(null, buffer); + deferred.resolve(); + } + }); + } + }).add("sharp-file-buffer-sharpen", { + defer: true, + fn: function(deferred) { + sharp.resize(inputWebp, sharp.buffer.webp, width, height, {sharpen: true}, function(err, buffer) { + if (err) { + throw err; + } else { + assert.notStrictEqual(null, buffer); + deferred.resolve(); + } + }); + } + }).add("sharp-file-buffer-sequentialRead", { + defer: true, + fn: function(deferred) { + sharp.resize(inputWebp, sharp.buffer.webp, width, height, {sequentialRead: true}, function(err, buffer) { + if (err) { + throw err; + } else { + assert.notStrictEqual(null, buffer); + deferred.resolve(); + } + }); + } + }).on("cycle", function(event) { + console.log("webp " + String(event.target)); + }).on("complete", function() { + callback(null, this.filter("fastest").pluck("name")); + }).run(); + }, + tiff: function(callback) { + (new Benchmark.Suite("tiff")).add("sharp-file-file", { + defer: true, + fn: function(deferred) { + sharp.resize(inputTiff, outputTiff, width, height, function(err) { + if (err) { + throw err; + } else { + deferred.resolve(); + } + }); + } + }).add("sharp-file-file-sharpen", { + defer: true, + fn: function(deferred) { + sharp.resize(inputTiff, outputTiff, width, height, {sharpen: true}, function(err) { + if (err) { + throw err; + } else { + deferred.resolve(); + } + }); + } + }).add("sharp-file-file-sequentialRead", { + defer: true, + fn: function(deferred) { + sharp.resize(inputTiff, outputTiff, width, height, {sequentialRead: true}, function(err) { + if (err) { + throw err; + } else { + deferred.resolve(); + } + }); + } + }).on("cycle", function(event) { + console.log("tiff " + String(event.target)); + }).on("complete", function() { + callback(null, this.filter("fastest").pluck("name")); + }).run(); + } }, function(err, results) { assert(!err, err); Object.keys(results).forEach(function(format) {