Compare commits

...

14 Commits

Author SHA1 Message Date
Lovell Fuller
7e8af63129 Improve thread/buffer shutdown using new vips_thread_shutdown method - closes #5 2014-01-15 21:38:23 +00:00
Lovell Fuller
f7b8ce1287 Add support for writing image data output to a buffer, boosting ops/sec by ~7%. Partially implements #3. 2014-01-11 21:50:17 +00:00
Lovell Fuller
dde9e94850 From here on in, this module will be using the bleeding edge version of libvips. Why? It just keeps getting faster. 2013-11-23 22:08:51 +00:00
Lovell Fuller
21f12e74ba Add support for PNG format. Close #2. 2013-10-26 16:32:18 +01:00
Lovell Fuller
daeebcc7dc Add epeg module to the perf tests 2013-10-19 13:39:57 +01:00
Lovell Fuller
5546a4f881 Added GraphicsMagick perf stats 2013-10-04 22:01:16 +01:00
Lovell Fuller
4aee725530 README layout clean-up 2013-10-02 23:40:32 +01:00
Lovell Fuller
f3da2284b1 Now requires livbips 7.28 or later 2013-10-02 23:37:29 +01:00
Lovell Fuller
f3cd263cb7 Replaced use of deprecated libvips im_* methods with new, shiny non-deprecated vips_* versions 2013-10-02 21:50:03 +01:00
Lovell Fuller
8443dd5122 Version bump 2013-10-02 19:35:49 +01:00
Lovell Fuller
0b7c8661fb The pkgconfig data file ends up in /usr/local when compiling libvips from source 2013-09-23 21:40:01 +01:00
Lovell Fuller
30f75bcc56 Additional performance test stats 2013-08-29 22:55:42 +01:00
Lovell Fuller
6f5125e889 Corrected order of width and height in usage docs 2013-08-26 16:21:38 +01:00
Lovell Fuller
6e3f9b04de Corrected order of width and height in usage docs 2013-08-26 16:20:43 +01:00
9 changed files with 339 additions and 110 deletions

1
.gitignore vendored
View File

@@ -13,5 +13,6 @@ results
build build
node_modules node_modules
tests/output.jpg tests/output.jpg
tests/output.png
npm-debug.log npm-debug.log

View File

@@ -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

133
README.md
View File

@@ -1,4 +1,4 @@
# sharp # sharp
_adj_ _adj_
@@ -7,30 +7,23 @@ _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 performance 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. 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.
Speed is typically 4x faster than the imagemagick equivalent. Performance is 4x-8x faster than ImageMagick and 2x-4x faster than GraphicsMagick, based mainly on the number of CPU cores available.
## Prerequisites ## Prerequisites
Requires node-gyp and libvips-dev to build. * Node.js v0.8+
* node-gyp
* [libvips](https://github.com/jcupitt/libvips) v7.37+
sudo npm install -g node-gyp For the sharpest results, please compile libvips from source.
sudo apt-get install libvips-dev
Requires vips-7.xx.pc (installed with libvips-dev) to be symlinked as /usr/lib/pkgconfig/vips.pc If you prefer to run a stable, package-managed environment such as Ubuntu 12.04 LTS, [v0.0.3](https://github.com/lovell/sharp/tree/v0.0.3) will work with the libvips-dev package.
Ubuntu 12.04 LTS:
sudo ln -s /usr/lib/pkgconfig/vips-7.26.pc /usr/lib/pkgconfig/vips.pc
Ubuntu 13.04 (64-bit):
sudo ln -s /usr/lib/x86_64-linux-gnu/pkgconfig/vips-7.28.pc /usr/lib/pkgconfig/vips.pc
## Install ## Install
@@ -40,11 +33,9 @@ Ubuntu 13.04 (64-bit):
var sharp = require("sharp"); var sharp = require("sharp");
### crop(inputPath, outputPath, width, height, callback) ### crop(input, output, width, height, callback)
Scale and crop JPEG `inputPath` to `width` x `height` and write JPEG to `outputPath` calling `callback` when complete. Scale and crop to `width` x `height` calling `callback` when complete.
Example:
```javascript ```javascript
sharp.crop("input.jpg", "output.jpg", 300, 200, function(err) { sharp.crop("input.jpg", "output.jpg", 300, 200, function(err) {
@@ -56,41 +47,113 @@ sharp.crop("input.jpg", "output.jpg", 300, 200, function(err) {
}); });
``` ```
### embedWhite(inputPath, outputPath, width, height, callback) ```javascript
sharp.crop("input.jpg", sharp.buffer.jpeg, 300, 200, function(err, buffer) {
if (err) {
throw err;
}
// buffer contains JPEG image data
});
```
Scale and embed JPEG `inputPath` to `width` x `height` using a white canvas and write JPEG to `outputPath` calling `callback` when complete. ```javascript
sharp.crop("input.jpg", sharp.buffer.png, 300, 200, function(err, buffer) {
if (err) {
throw err;
}
// buffer contains PNG image data (converted from JPEG)
});
```
### embedWhite(input, output, width, height, callback)
Scale and embed to `width` x `height` using a white canvas calling `callback` when complete.
```javascript ```javascript
sharp.embedWhite("input.jpg", "output.jpg", 200, 300, function(err) { sharp.embedWhite("input.jpg", "output.jpg", 200, 300, function(err) {
if (err) { if (err) {
throw err; throw err;
} }
// output.jpg is a 300 pixels wide and 200 pixels high image // 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.
```javascript ```javascript
sharp.embedBlack("input.jpg", "output.jpg", 200, 300, function(err) { sharp.embedWhite("input.jpg", sharp.buffer.jpeg, 200, 300, function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} }
// output.jpg is a 300 pixels wide and 200 pixels high image // buffer contains JPEG image data
// containing a scaled version of input.jpg embedded on a black canvas
}); });
``` ```
## Testing [![Build Status](https://travis-ci.org/lovell/sharp.png?branch=master)](https://travis-ci.org/lovell/sharp) ### embedBlack(input, output, width, height, callback)
Scale and embed to `width` x `height` using a black canvas calling `callback` when complete.
```javascript
sharp.embedBlack("input.png", "output.png", 200, 300, function(err) {
if (err) {
throw err;
}
// output.png is a 200 pixels wide and 300 pixels high image
// containing a scaled version of input.png embedded on a black canvas
});
```
### Parameters common to all methods
#### input
String containing the filename to read from.
#### output
One of:
* String containing the filename to write to.
* `sharp.buffer.jpeg` to pass a Buffer containing JPEG image data to `callback`.
* `sharp.buffer.png` to pass a Buffer containing PNG image data to `callback`.
## Testing
npm test npm test
## Performance ## Performance
Using an AMD Athlon quad core CPU with 512KB L2 cache clocked at 3.3GHz with 8GB RAM: Test environment:
* imagemagick x 5.55 ops/sec <20>0.68% (31 runs sampled) * AMD Athlon 4 core 3.3GHz 512KB L2 CPU 1333 DDR3
* sharp x 24.49 ops/sec <20>6.85% (64 runs sampled) * libvips 7.37
* libjpeg-turbo8 1.3.0
* libpng 1.6.6
* zlib1g 1.2.7
#### JPEG
* 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-file x 31.60 ops/sec ±8.80% (80 runs sampled)
* sharp-buffer x 34.04 ops/sec ±0.36% (82 runs sampled)
#### PNG
* imagemagick x 4.65 ops/sec ±0.37% (27 runs sampled)
* gm x 21.65 ops/sec ±0.18% (56 runs sampled)
* sharp-file x 39.47 ops/sec ±6.78% (68 runs sampled)
* sharp-buffer x 42.87 ops/sec ±0.19% (71 runs sampled)
## Licence
Copyright 2013, 2014 Lovell Fuller
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)
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.

View File

@@ -2,7 +2,10 @@
'targets': [{ 'targets': [{
'target_name': 'sharp', 'target_name': 'sharp',
'sources': ['src/sharp.cc'], 'sources': ['src/sharp.cc'],
'libraries': ['<!@(PKG_CONFIG_PATH="/usr/lib/pkgconfig" pkg-config --libs glib-2.0 vips)'], 'libraries': [
'<!@(PKG_CONFIG_PATH="/usr/local/lib/pkgconfig" pkg-config --libs vips)',
'<!@(PKG_CONFIG_PATH="/usr/lib/pkgconfig" pkg-config --libs vips)'
],
'include_dirs': [ 'include_dirs': [
'/usr/include/glib-2.0', '/usr/include/glib-2.0',
'/usr/lib/glib-2.0/include', '/usr/lib/glib-2.0/include',

View File

@@ -1,13 +1,18 @@
var sharp = require("./build/Release/sharp"); var sharp = require("./build/Release/sharp");
module.exports.buffer = {
jpeg: "__jpeg",
png: "__png"
};
module.exports.crop = function(input, output, width, height, callback) { 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) { 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) { 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);
} };

View File

@@ -1,32 +1,37 @@
{ {
"name": "sharp", "name": "sharp",
"version": "0.0.2", "version": "0.0.8",
"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", "main": "index.js",
"description": "High performance Node.js module to resize JPEG images using the libvips image processing library",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/lovell/sharp" "url": "git://github.com/lovell/sharp"
}, },
"devDependencies": {
"imagemagick": "*",
"benchmark": "*"
},
"scripts": {
"test": "node tests/perf.js"
},
"engines": {
"node": "*"
},
"keywords": [ "keywords": [
"jpeg", "jpeg",
"png",
"resize", "resize",
"thumbnail", "thumbnail",
"sharpen", "sharpen",
"crop", "crop",
"embed", "embed",
"libvips", "libvips",
"fast" "fast",
"buffer"
], ],
"author": "Lovell Fuller", "devDependencies": {
"license": "Apache 2.0" "imagemagick": "*",
"gm": "*",
"epeg": "*",
"async": "*",
"benchmark": "*"
},
"license": "Apache 2.0",
"engines": {
"node": ">=0.8"
}
} }

View File

@@ -3,8 +3,10 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <vips/vips.h> #include <vips/vips.h>
#include <node_buffer.h>
using namespace v8; using namespace v8;
using namespace node;
// Free VipsImage children when object goes out of scope // Free VipsImage children when object goes out of scope
// Thanks due to https://github.com/dosx/node-vips // Thanks due to https://github.com/dosx/node-vips
@@ -27,19 +29,34 @@ class ImageFreer {
struct ResizeBaton { struct ResizeBaton {
std::string src; std::string src;
std::string dst; std::string dst;
void* buffer_out;
size_t buffer_out_len;
int cols; int cols;
int rows; int rows;
bool crop; bool crop;
int embed; int embed;
std::string err; std::string err;
Persistent<Function> callback; Persistent<Function> callback;
ResizeBaton() : buffer_out_len(0) {}
}; };
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) { void ResizeAsync(uv_work_t *work) {
ResizeBaton* baton = static_cast<ResizeBaton*>(work->data); ResizeBaton* baton = static_cast<ResizeBaton*>(work->data);
VipsImage *in = vips_image_new_mode((baton->src).c_str(), "p"); VipsImage *in = vips_image_new_mode((baton->src).c_str(), "p");
im_jpeg2vips((baton->src).c_str(), in); 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) { if (in == NULL) {
(baton->err).append(vips_error_buffer()); (baton->err).append(vips_error_buffer());
vips_error_clear(); vips_error_clear();
@@ -114,10 +131,36 @@ void ResizeAsync(uv_work_t *work) {
} }
img = t[3]; img = t[3];
if (im_vips2jpeg(img, baton->dst.c_str())) { if (baton->dst == "__jpeg") {
// Write JPEG to buffer
if (vips_jpegsave_buffer(img, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, "optimize_coding", TRUE, NULL)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
} else if (baton->dst == "__png") {
// Write PNG to buffer
if (vips_pngsave_buffer(img, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", 6, "interlace", FALSE, NULL)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
} else if (EndsWith(baton->dst, ".jpg") || EndsWith(baton->dst, ".jpeg")) {
// Write JPEG to file
if (vips_foreign_save(img, baton->dst.c_str(), "strip", TRUE, "Q", 80, "optimize_coding", TRUE, NULL)) {
(baton->err).append(vips_error_buffer()); (baton->err).append(vips_error_buffer());
vips_error_clear(); vips_error_clear();
} }
} else if (EndsWith(baton->dst, ".png")) {
// Write PNG to file
if (vips_foreign_save(img, baton->dst.c_str(), "strip", TRUE, "compression", 6, "interlace", FALSE, NULL)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
}
} else {
(baton->err).append("Unsupported output file type");
}
vips_thread_shutdown();
} }
void ResizeAsyncAfter(uv_work_t *work, int status) { void ResizeAsyncAfter(uv_work_t *work, int status) {
@@ -125,14 +168,19 @@ void ResizeAsyncAfter(uv_work_t *work, int status) {
ResizeBaton *baton = static_cast<ResizeBaton*>(work->data); ResizeBaton *baton = static_cast<ResizeBaton*>(work->data);
Local<Value> argv[1]; Local<Value> null = Local<Value>::New(Null());
Local<Value> argv[2] = {null, null};
if (!baton->err.empty()) { if (!baton->err.empty()) {
// Error
argv[0] = String::New(baton->err.data(), baton->err.size()); argv[0] = String::New(baton->err.data(), baton->err.size());
} else { } else if (baton->buffer_out_len > 0) {
argv[0] = Local<Value>::New(Null()); // Buffer
Buffer *buffer = Buffer::New((const char*)(baton->buffer_out), baton->buffer_out_len);
argv[1] = Local<Object>::New(buffer->handle_);
vips_free(baton->buffer_out);
} }
baton->callback->Call(Context::GetCurrent()->Global(), 1, argv); baton->callback->Call(Context::GetCurrent()->Global(), 2, argv);
baton->callback.Dispose(); baton->callback.Dispose();
delete baton; delete baton;
delete work; delete work;
@@ -164,10 +212,16 @@ Handle<Value> Resize(const Arguments& args) {
return Undefined(); return Undefined();
} }
static void at_exit(void* arg) {
HandleScope scope;
vips_shutdown();
}
extern "C" void init(Handle<Object> target) { extern "C" void init(Handle<Object> target) {
HandleScope scope; HandleScope scope;
vips_init(""); vips_init("");
AtExit(at_exit);
NODE_SET_METHOD(target, "resize", Resize); NODE_SET_METHOD(target, "resize", Resize);
}; };
NODE_MODULE(sharp, init) NODE_MODULE(sharp, init);

BIN
tests/50020484-00001.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -1,21 +1,31 @@
var sharp = require("../index"); var sharp = require("../index");
var imagemagick = require("imagemagick"); var imagemagick = require("imagemagick");
var gm = require("gm");
var epeg = require("epeg");
var async = require("async");
var assert = require("assert"); var assert = require("assert");
var Benchmark = require("benchmark"); var Benchmark = require("benchmark");
var input = __dirname + "/2569067123_aca715a2ee_o.jpg"; // http://www.flickr.com/photos/grizdave/2569067123/ var inputJpg = __dirname + "/2569067123_aca715a2ee_o.jpg"; // http://www.flickr.com/photos/grizdave/2569067123/
var output = __dirname + "/output.jpg"; var outputJpg = __dirname + "/output.jpg";
var outputJpgLength = 47035;
var inputPng = __dirname + "/50020484-00001.png"; // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png
var outputPng = __dirname + "/output.png";
var outputPngLength = 60380;
var width = 640; var width = 640;
var height = 480; var height = 480;
var suite = new Benchmark.Suite; async.series({
suite.add("imagemagick", { jpeg: function(callback) {
"defer": true, (new Benchmark.Suite("jpeg")).add("imagemagick", {
"fn": function(deferred) { defer: true,
fn: function(deferred) {
imagemagick.resize({ imagemagick.resize({
srcPath: input, srcPath: inputJpg,
dstPath: output, dstPath: outputJpg,
quality: 0.75, quality: 0.8,
width: width, width: width,
height: height height: height
}, function(err) { }, function(err) {
@@ -26,10 +36,10 @@ suite.add("imagemagick", {
} }
}); });
} }
}).add("sharp", { }).add("gm", {
"defer": true, defer: true,
"fn": function(deferred) { fn: function(deferred) {
sharp.crop(input, output, width, height, function(err) { gm(inputJpg).crop(width, height).quality(80).write(outputJpg, function (err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -37,8 +47,104 @@ suite.add("imagemagick", {
} }
}); });
} }
}).add("epeg", {
defer: true,
fn: function(deferred) {
var image = new epeg.Image({path: inputJpg});
image.downsize(width, height, 80).saveTo(outputJpg);
deferred.resolve();
}
}).add("sharp-file", {
defer: true,
fn: function(deferred) {
sharp.crop(inputJpg, outputJpg, width, height, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-buffer", {
defer: true,
fn: function(deferred) {
sharp.crop(inputJpg, sharp.buffer.jpeg, width, height, function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
assert.strictEqual(outputJpgLength, buffer.length);
deferred.resolve();
}
});
}
}).on("cycle", function(event) { }).on("cycle", function(event) {
console.log(String(event.target)); console.log("jpeg " + String(event.target));
}).on("complete", function() { }).on("complete", function() {
assert(this.filter("fastest").pluck("name") == "sharp"); callback(null, this.filter("fastest").pluck("name"));
}).run(); }).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-file", {
defer: true,
fn: function(deferred) {
sharp.crop(inputPng, outputPng, width, height, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-buffer", {
defer: true,
fn: function(deferred) {
sharp.crop(inputPng, sharp.buffer.png, width, height, function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
assert.strictEqual(outputPngLength, buffer.length);
deferred.resolve();
}
});
}
}).on("cycle", function(event) {
console.log(" png " + 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) {
assert.strictEqual("sharp", results[format].toString().substr(0, 5), "sharp was slower than " + results[format] + " for " + format);
});
});