Add support for WebP and TIFF image formats. Closes #7.

This commit is contained in:
Lovell Fuller 2014-02-22 21:48:00 +00:00
parent 16551bc058
commit 0899252a72
8 changed files with 210 additions and 19 deletions

3
.gitignore vendored
View File

@ -12,7 +12,6 @@ logs
results results
build build
node_modules node_modules
tests/output.jpg tests/output.*
tests/output.png
npm-debug.log npm-debug.log

View File

@ -7,10 +7,12 @@ _adj_
3. shrewd or astute: a sharp bargainer. 3. shrewd or astute: a sharp bargainer.
4. (Informal.) very stylish: a sharp dresser; a sharp jacket. 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. 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. 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 ## 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. 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. `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 ```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) { if (err) {
throw err; throw err;
} }
@ -80,21 +82,21 @@ sharp.resize("input.jpg", sharp.buffer.png, 300, 200, {sharpen: true}, function(
``` ```
```javascript ```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) { if (err) {
throw 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 // of the image data contained in buffer embedded on a white canvas
}); });
``` ```
```javascript ```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) { if (err) {
throw 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 // 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: Test environment:
* AMD Athlon 4 core 3.3GHz 512KB L2 CPU 1333 DDR3 * AMD Athlon 4 core 3.3GHz 512KB L2 CPU 1333 DDR3
* libvips 7.37 * libvips 7.38
* libjpeg-turbo8 1.3.0 * libjpeg-turbo8 1.3.0
* libpng 1.6.6 * libpng 1.6.6
* zlib1g 1.2.7 * 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. `-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-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) * 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 ## Licence
Copyright 2013, 2014 Lovell Fuller Copyright 2013, 2014 Lovell Fuller

View File

@ -2,7 +2,8 @@ var sharp = require("./build/Release/sharp");
module.exports.buffer = { module.exports.buffer = {
jpeg: "__jpeg", jpeg: "__jpeg",
png: "__png" png: "__png",
webp: "__webp"
}; };
module.exports.canvas = { module.exports.canvas = {

View File

@ -1,8 +1,8 @@
{ {
"name": "sharp", "name": "sharp",
"version": "0.1.5", "version": "0.1.6",
"author": "Lovell Fuller", "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": { "scripts": {
"test": "node tests/perf.js" "test": "node tests/perf.js"
}, },
@ -14,12 +14,15 @@
"keywords": [ "keywords": [
"jpeg", "jpeg",
"png", "png",
"webp",
"tiff",
"resize", "resize",
"thumbnail", "thumbnail",
"sharpen", "sharpen",
"crop", "crop",
"embed", "embed",
"libvips", "libvips",
"vips",
"fast", "fast",
"buffer" "buffer"
], ],

View File

@ -30,22 +30,33 @@ struct resize_baton {
typedef enum { typedef enum {
JPEG, JPEG,
PNG PNG,
WEBP,
TIFF
} ImageType; } ImageType;
unsigned char MARKER_JPEG[] = {0xff, 0xd8}; unsigned char MARKER_JPEG[] = {0xff, 0xd8};
unsigned char MARKER_PNG[] = {0x89, 0x50}; unsigned char MARKER_PNG[] = {0x89, 0x50};
unsigned char MARKER_WEBP[] = {0x52, 0x49};
bool ends_with(std::string const &str, std::string const &end) { 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); return str.length() >= end.length() && 0 == str.compare(str.length() - end.length(), end.length(), end);
} }
bool is_jpeg(std::string const &str) { 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) { 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) { 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)) { if (vips_pngload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in); 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)) { } else if (is_jpeg(baton->file_in)) {
if (vips_jpegload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) { 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)) { if (vips_pngload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in); 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 { } else {
resize_error(baton, in); resize_error(baton, in);
(baton->err).append("Unsupported input " + baton->file_in); (baton->err).append("Unsupported input file " + baton->file_in);
return; 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)) { 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); 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)) { } else if (is_jpeg(baton->file_out)) {
// Write JPEG to file // Write JPEG to file
if (vips_jpegsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) { 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)) { if (vips_pngsave(sharpened, baton->file_out.c_str(), "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) {
return resize_error(baton, sharpened); 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 { } else {
(baton->err).append("Unsupported output " + baton->file_out); (baton->err).append("Unsupported output " + baton->file_out);
} }

BIN
tests/4.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

BIN
tests/G31D.TIF Normal file

Binary file not shown.

View File

@ -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 inputPng = __dirname + "/50020484-00001.png"; // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
var outputPng = __dirname + "/output.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 width = 720;
var height = 480; var height = 480;
@ -290,7 +296,125 @@ async.series({
}).on("complete", function() { }).on("complete", function() {
callback(null, this.filter("fastest").pluck("name")); callback(null, this.filter("fastest").pluck("name"));
}).run(); }).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) { }, function(err, results) {
assert(!err, err); assert(!err, err);
Object.keys(results).forEach(function(format) { Object.keys(results).forEach(function(format) {