diff --git a/.gitignore b/.gitignore index bfc4e960..e5c5a80c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,6 @@ results build node_modules tests/output.jpg +tests/output.png npm-debug.log diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 26dc54cc..00000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: node_js -node_js: - - "0.11" - - "0.10" -before_install: - - sudo apt-get update -qq - - sudo apt-get install -qq libvips-dev imagemagick - - sudo ln -s /usr/lib/pkgconfig/vips-7.26.pc /usr/lib/pkgconfig/vips.pc \ No newline at end of file diff --git a/README.md b/README.md index aac41cd7..fb63e462 100755 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ _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 a large JPEG image to smaller JPEG images of varying dimensions. +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. -It is somewhat opinionated in that it only deals with JPEG images, always obeys the requested dimensions by either cropping or embedding and insists on a mild sharpen of the resulting image. +It is somewhat opinionated in that it only deals with JPEG and PNG images, always obeys the requested dimensions by either cropping or embedding and insists on a mild sharpen of the resulting image. 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 the University of Southampton. @@ -19,7 +19,7 @@ Performance is 4x-8x faster than ImageMagick and 2x-4x faster than GraphicsMagic * Node.js v0.8+ * node-gyp -* libvips-dev 7.28+ +* libvips-dev 7.28+ (7.36+ for optimal JPEG Huffman coding) ``` sudo npm install -g node-gyp @@ -40,7 +40,7 @@ When installed as a package, please symlink `vips-7.28.pc` (or later, installed ### crop(inputPath, outputPath, width, height, callback) -Scale and crop JPEG `inputPath` to `width` x `height` and write JPEG to `outputPath` calling `callback` when complete. +Scale and crop `inputPath` to `width` x `height` and write to `outputPath` calling `callback` when complete. Example: @@ -56,29 +56,29 @@ sharp.crop("input.jpg", "output.jpg", 300, 200, function(err) { ### embedWhite(inputPath, outputPath, width, height, callback) -Scale and embed JPEG `inputPath` to `width` x `height` using a white canvas and write JPEG to `outputPath` calling `callback` when complete. +Scale and embed `inputPath` to `width` x `height` using a white canvas and write to `outputPath` calling `callback` when complete. ```javascript -sharp.embedWhite("input.jpg", "output.jpg", 200, 300, function(err) { +sharp.embedWhite("input.jpg", "output.png", 200, 300, function(err) { if (err) { throw err; } // output.jpg is a 200 pixels wide and 300 pixels high image - // containing a scaled version of input.jpg embedded on a white canvas + // containing a scaled version of input.png embedded on a white canvas }); ``` ### embedBlack(inputPath, outputPath, width, height, callback) -Scale and embed JPEG `inputPath` to `width` x `height` using a black canvas and write JPEG to `outputPath` calling `callback` when complete. +Scale and embed `inputPath` to `width` x `height` using a black canvas and write to `outputPath` calling `callback` when complete. ```javascript -sharp.embedBlack("input.jpg", "output.jpg", 200, 300, function(err) { +sharp.embedBlack("input.png", "output.png", 200, 300, function(err) { if (err) { throw err; } - // output.jpg is a 200 pixels wide and 300 pixels high image - // containing a scaled version of input.jpg embedded on a black canvas + // output.png is a 200 pixels wide and 300 pixels high image + // containing a scaled version of input.png embedded on a black canvas }); ``` @@ -89,29 +89,37 @@ sharp.embedBlack("input.jpg", "output.jpg", 200, 300, function(err) { ## Performance -### AMD Athlon 4x core 3.3GHz 512KB L2 +Test environment: -* imagemagick x 5.55 ops/sec ±0.45% (31 runs sampled) -* gm x 10.31 ops/sec ±3.57% (53 runs sampled) -* epeg x 27.79 ops/sec ±0.12% (69 runs sampled) -* sharp x 31.52 ops/sec ±8.74% (80 runs sampled) +* AMD Athlon 4 core 3.3GHz 512KB L2 CPU 1333 DDR3 +* libvips 7.36 +* libjpeg-turbo8 1.2.1 +* libpng 1.6.6 +* zlib1g 1.2.7 -### AWS t1.micro +#### JPEG -* imagemagick x 1.36 ops/sec ±0.96% (11 runs sampled) -* sharp x 12.42 ops/sec ±5.84% (64 runs sampled) +* imagemagick x 5.53 ops/sec ±0.55% (31 runs sampled) +* gm x 10.86 ops/sec ±0.43% (56 runs sampled) +* epeg x 28.07 ops/sec ±0.07% (70 runs sampled) +* sharp x 31.60 ops/sec ±8.80% (80 runs sampled) -### AWS m1.medium +#### PNG -* imagemagick x 1.38 ops/sec ±0.45% (11 runs sampled) -* sharp x 12.66 ops/sec ±5.54% (65 runs sampled) +* imagemagick x 4.65 ops/sec ±0.37% (27 runs sampled) +* gm x 21.65 ops/sec ±0.18% (56 runs sampled) +* sharp x 39.47 ops/sec ±6.78% (68 runs sampled) -### AWS c1.medium +## Licence -* imagemagick x 2.10 ops/sec ±0.67% (15 runs sampled) -* sharp x 18.97 ops/sec ±10.54% (52 runs sampled) +Copyright 2013 Lovell Fuller -### AWS m3.xlarge +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0.html) -* imagemagick x 4.46 ops/sec ±0.33% (26 runs sampled) -* sharp x 28.89 ops/sec ±7.75% (74 runs sampled) +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/index.js b/index.js index 148d9772..1d95cf89 100755 --- a/index.js +++ b/index.js @@ -1,13 +1,13 @@ var sharp = require("./build/Release/sharp"); module.exports.crop = function(input, output, width, height, callback) { - sharp.resize(input, output, width, height, "c", callback) -} + sharp.resize(input, output, width, height, "c", callback); +}; module.exports.embedWhite = function(input, output, width, height, callback) { - sharp.resize(input, output, width, height, "w", callback) -} + sharp.resize(input, output, width, height, "w", callback); +}; module.exports.embedBlack = function(input, output, width, height, callback) { - sharp.resize(input, output, width, height, "b", callback) -} + sharp.resize(input, output, width, height, "b", callback); +}; diff --git a/package.json b/package.json index 5525f92f..a145ffb9 100755 --- a/package.json +++ b/package.json @@ -1,26 +1,19 @@ { "name": "sharp", - "version": "0.0.4", + "version": "0.0.5", + "author": "Lovell Fuller", + "description": "High performance module to resize JPEG and PNG images using the libvips image processing library", + "scripts": { + "test": "node tests/perf.js" + }, "main": "index.js", - "description": "High performance Node.js module to resize JPEG images using the libvips image processing library", "repository": { "type": "git", "url": "git://github.com/lovell/sharp" }, - "devDependencies": { - "imagemagick": "*", - "gm": "*", - "epeg": "*", - "benchmark": "*" - }, - "scripts": { - "test": "node tests/perf.js" - }, - "engines": { - "node": ">=0.8" - }, "keywords": [ "jpeg", + "png", "resize", "thumbnail", "sharpen", @@ -29,6 +22,15 @@ "libvips", "fast" ], - "author": "Lovell Fuller", - "license": "Apache 2.0" + "devDependencies": { + "imagemagick": "*", + "gm": "*", + "epeg": "*", + "async": "*", + "benchmark": "*" + }, + "license": "Apache 2.0", + "engines": { + "node": ">=0.8" + } } \ No newline at end of file diff --git a/src/sharp.cc b/src/sharp.cc index 062ca944..07f81aab 100755 --- a/src/sharp.cc +++ b/src/sharp.cc @@ -35,11 +35,22 @@ struct ResizeBaton { Persistent callback; }; +bool EndsWith(std::string const &str, std::string const &end) { + return str.length() >= end.length() && 0 == str.compare(str.length() - end.length(), end.length(), end); +} + void ResizeAsync(uv_work_t *work) { ResizeBaton* baton = static_cast(work->data); VipsImage *in = vips_image_new_mode((baton->src).c_str(), "p"); - vips_jpegload((baton->src).c_str(), &in, NULL); + if (EndsWith(baton->src, ".jpg") || EndsWith(baton->src, ".jpeg")) { + vips_jpegload((baton->src).c_str(), &in, NULL); + } else if (EndsWith(baton->src, ".png")) { + vips_pngload((baton->src).c_str(), &in, NULL); + } else { + (baton->err).append("Unsupported input file type"); + return; + } if (in == NULL) { (baton->err).append(vips_error_buffer()); vips_error_clear(); @@ -114,9 +125,18 @@ void ResizeAsync(uv_work_t *work) { } img = t[3]; - if (vips_jpegsave(img, baton->dst.c_str(), "Q", 80, "profile", "none", "optimize_coding", TRUE, NULL)) { - (baton->err).append(vips_error_buffer()); - vips_error_clear(); + if (EndsWith(baton->dst, ".jpg") || EndsWith(baton->dst, ".jpeg")) { + if (vips_jpegsave(img, baton->dst.c_str(), "Q", 80, "profile", "none", "optimize_coding", TRUE, NULL)) { + (baton->err).append(vips_error_buffer()); + vips_error_clear(); + } + } else if (EndsWith(baton->dst, ".png")) { + if (vips_pngsave(img, baton->dst.c_str(), "compression", 6, "interlace", FALSE, NULL)) { + (baton->err).append(vips_error_buffer()); + vips_error_clear(); + } + } else { + (baton->err).append("Unsupported output file type"); } } diff --git a/tests/50020484-00001.png b/tests/50020484-00001.png new file mode 100644 index 00000000..64db47f7 Binary files /dev/null and b/tests/50020484-00001.png differ diff --git a/tests/perf.js b/tests/perf.js index 9cbb7742..f796476a 100755 --- a/tests/perf.js +++ b/tests/perf.js @@ -2,63 +2,120 @@ var sharp = require("../index"); var imagemagick = require("imagemagick"); var gm = require("gm"); var epeg = require("epeg"); +var async = require("async"); var assert = require("assert"); var Benchmark = require("benchmark"); -var input = __dirname + "/2569067123_aca715a2ee_o.jpg"; // http://www.flickr.com/photos/grizdave/2569067123/ -var output = __dirname + "/output.jpg"; +var inputJpg = __dirname + "/2569067123_aca715a2ee_o.jpg"; // http://www.flickr.com/photos/grizdave/2569067123/ +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 width = 640; var height = 480; -var suite = new Benchmark.Suite; -suite.add("imagemagick", { - "defer": true, - "fn": function(deferred) { - imagemagick.resize({ - srcPath: input, - dstPath: output, - quality: 0.75, - width: width, - height: height - }, function(err) { - if (err) { - throw err; - } else { +async.series({ + jpeg: function(callback) { + (new Benchmark.Suite("jpeg")).add("imagemagick", { + "defer": true, + "fn": function(deferred) { + imagemagick.resize({ + srcPath: inputJpg, + dstPath: outputJpg, + quality: 0.8, + width: width, + height: height + }, function(err) { + if (err) { + throw err; + } else { + deferred.resolve(); + } + }); + } + }).add("gm", { + "defer": true, + "fn": function(deferred) { + gm(inputJpg).crop(width, height).quality(80).write(outputJpg, function (err) { + if (err) { + throw err; + } else { + deferred.resolve(); + } + }); + } + }).add("epeg", { + "defer": true, + "fn": function(deferred) { + var image = new epeg.Image({path: inputJpg}); + image.downsize(width, height, 80).saveTo(outputJpg); deferred.resolve(); } - }); - } -}).add("gm", { - "defer": true, - "fn": function(deferred) { - gm(input).crop(width, height).write(output, function (err) { - if (err) { - throw err; - } else { - deferred.resolve(); + }).add("sharp", { + "defer": true, + "fn": function(deferred) { + sharp.crop(inputJpg, outputJpg, width, height, function(err) { + if (err) { + throw err; + } else { + deferred.resolve(); + } + }); } - }); - } -}).add("epeg", { - "defer": true, - "fn": function(deferred) { - var image = new epeg.Image({path: input}); - image.downsize(width, height).saveTo(output); - deferred.resolve(); - } -}).add("sharp", { - "defer": true, - "fn": function(deferred) { - sharp.crop(input, output, width, height, function(err) { - if (err) { - throw err; - } else { - deferred.resolve(); + }).on("cycle", function(event) { + console.log("jpeg " + String(event.target)); + }).on("complete", function() { + callback(null, this.filter("fastest").pluck("name")); + }).run(); + }, + png: function(callback) { + (new Benchmark.Suite("png")).add("imagemagick", { + "defer": true, + "fn": function(deferred) { + imagemagick.resize({ + srcPath: inputPng, + dstPath: outputPng, + width: width, + height: height + }, function(err) { + if (err) { + throw err; + } else { + deferred.resolve(); + } + }); } - }); + }).add("gm", { + "defer": true, + "fn": function(deferred) { + gm(inputPng).crop(width, height).write(outputPng, function (err) { + if (err) { + throw err; + } else { + deferred.resolve(); + } + }); + } + }).add("sharp", { + "defer": true, + "fn": function(deferred) { + sharp.crop(inputPng, outputPng, width, height, function(err) { + if (err) { + throw err; + } else { + deferred.resolve(); + } + }); + } + }).on("cycle", function(event) { + console.log(" png " + String(event.target)); + }).on("complete", function() { + callback(null, this.filter("fastest").pluck("name")); + }).run(); } -}).on("cycle", function(event) { - console.log(String(event.target)); -}).on("complete", function() { - assert(this.filter("fastest").pluck("name") == "sharp"); -}).run(); +}, function(err, results) { + results.forEach(function(format, fastest) { + assert(fastest === "sharp", "sharp was slower than " + fastest + " for " + format); + }); +});