Compare commits

...

6 Commits

Author SHA1 Message Date
Lovell Fuller
f99e42d447 Add support for auto-scaling of width and height 2014-03-03 23:24:09 +00:00
Lovell Fuller
9b4387be97 Improve installation instructions for libvips 2014-02-28 23:11:05 +00:00
Lovell Fuller
9c3631ecb7 Add comparative performance tests using random dimensions 2014-02-28 22:39:02 +00:00
Lovell Fuller
31ca68fb14 Improve documentation of cache method 2014-02-28 22:37:59 +00:00
Lovell Fuller
d5d85a8697 Expose vips internal cache settings and status 2014-02-25 23:31:33 +00:00
Lovell Fuller
ae9a8b0f57 Remove unnecessary temporary copy of input buffer. 2014-02-23 10:52:40 +00:00
8 changed files with 204 additions and 27 deletions

View File

@@ -18,9 +18,23 @@ Under the hood you'll find the blazingly fast [libvips](https://github.com/jcupi
## Prerequisites ## Prerequisites
* Node.js v0.8+ * Node.js v0.8+
* [libvips](https://github.com/jcupitt/libvips) v7.38+ * [libvips](https://github.com/jcupitt/libvips) v7.38.5+
For the sharpest results, please compile libvips from source. ### Install libvips on Mac OS via homebrew
brew tap homebrew/science
brew install vips
### Install libvips on Ubuntu Linux
sudo apt-get install automake build-essential git gobject-introspection gtk-doc-tools libfftw3-dev libglib2.0-dev libjpeg-turbo8-dev libpng12-dev libwebp-dev libtiff5-dev liborc-0.4-dev libxml2-dev swig
git clone https://github.com/jcupitt/libvips.git
cd libvips
./bootstrap.sh
./configure --enable-debug=no
make
sudo make install
sudo ldconfig
## Install ## Install
@@ -38,9 +52,9 @@ Scale and crop to `width` x `height` calling `callback` when complete.
`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`. `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. Use a value of -1 to auto-scale the width to match the height.
`height` is the Number of pixels high the resultant image should be. `height` is the Number of pixels high the resultant image should be. Use a value of -1 to auto-scale the height to match the width.
`options` is optional, and can contain one or more of: `options` is optional, and can contain one or more of:
@@ -51,7 +65,7 @@ Scale and crop to `width` x `height` calling `callback` when complete.
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant image data when a Buffer is requested. `callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant image data when a Buffer is requested.
### Examples #### Examples
```javascript ```javascript
sharp.resize("input.jpg", "output.jpg", 300, 200, function(err) { sharp.resize("input.jpg", "output.jpg", 300, 200, function(err) {
@@ -64,20 +78,20 @@ sharp.resize("input.jpg", "output.jpg", 300, 200, function(err) {
``` ```
```javascript ```javascript
sharp.resize("input.jpg", sharp.buffer.jpeg, 300, 200, {progressive: true}, function(err, buffer) { sharp.resize("input.jpg", sharp.buffer.jpeg, -1, 200, {progressive: true}, function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} }
// buffer contains progressive JPEG image data // buffer contains progressive JPEG image data, 200 pixels high
}); });
``` ```
```javascript ```javascript
sharp.resize("input.webp", sharp.buffer.png, 300, 200, {sharpen: true}, function(err, buffer) { sharp.resize("input.webp", sharp.buffer.png, 300, -1, {sharpen: true}, function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} }
// buffer contains sharpened PNG image data (converted from JPEG) // buffer contains sharpened PNG image data (converted from JPEG), 300 pixels wide
}); });
``` ```
@@ -101,6 +115,20 @@ sharp.resize("input.jpg", sharp.buffer.webp, 200, 300, {canvas: sharp.canvas.emb
}); });
``` ```
### cache([limit])
If `limit` is provided, set the (soft) limit of _libvips_ working/cache memory to this value in MB. The default value is 100.
This method always returns cache statistics, useful for determining how much working memory is required for a particular task.
Warnings such as _Application transferred too many scanlines_ are a good indicator you've set this value too low.
```javascript
var stats = sharp.cache(); // { current: 98, high: 115, limit: 100 }
sharp.cache(200); // { current: 98, high: 115, limit: 200 }
sharp.cache(50); // { current: 49, high: 115, limit: 50 }
```
## Testing ## Testing
npm test npm test

View File

@@ -42,6 +42,10 @@ module.exports.resize = function(input, output, width, height, options, callback
callback("Invalid height " + height); callback("Invalid height " + height);
return; return;
} }
if (outWidth < 1 && outHeight < 1) {
callback("Width and/or height required");
return;
}
var canvas = options.canvas || "c"; var canvas = options.canvas || "c";
if (canvas.length !== 1 || "cwb".indexOf(canvas) === -1) { if (canvas.length !== 1 || "cwb".indexOf(canvas) === -1) {
callback("Invalid canvas " + canvas); callback("Invalid canvas " + canvas);
@@ -50,7 +54,15 @@ module.exports.resize = function(input, output, width, height, options, callback
var sharpen = !!options.sharpen; var sharpen = !!options.sharpen;
var progessive = !!options.progessive; var progessive = !!options.progessive;
var sequentialRead = !!options.sequentialRead; var sequentialRead = !!options.sequentialRead;
sharp.resize(options.inFile, options.inBuffer, output, width, height, canvas, sharpen, progessive, sequentialRead, callback); sharp.resize(options.inFile, options.inBuffer, output, outWidth, outHeight, canvas, sharpen, progessive, sequentialRead, callback);
};
module.exports.cache = function(limit) {
"use strict";
if (Number.isNaN(limit)) {
limit = null;
}
return sharp.cache(limit);
}; };
/* Deprecated v0.0.x methods */ /* Deprecated v0.0.x methods */

View File

@@ -1,10 +1,10 @@
{ {
"name": "sharp", "name": "sharp",
"version": "0.1.6", "version": "0.1.8",
"author": "Lovell Fuller", "author": "Lovell Fuller",
"description": "High performance module to resize JPEG, PNG, WebP and TIFF 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/unit && node tests/perf"
}, },
"main": "index.js", "main": "index.js",
"repository": { "repository": {

View File

@@ -119,7 +119,24 @@ void resize_async(uv_work_t *work) {
double xfactor = static_cast<double>(in->Xsize) / std::max(baton->width, 1); double xfactor = static_cast<double>(in->Xsize) / std::max(baton->width, 1);
double yfactor = static_cast<double>(in->Ysize) / std::max(baton->height, 1); double yfactor = static_cast<double>(in->Ysize) / std::max(baton->height, 1);
double factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor); double factor;
if (baton->width > 0 && baton->height > 0) {
// Fixed width and height
factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor);
} else if (baton->width > 0) {
// Fixed width, auto height
factor = xfactor;
baton->height = floor(in->Ysize * factor);
} else if (baton->height > 0) {
// Fixed height, auto width
factor = yfactor;
baton->width = floor(in->Xsize * factor);
} else {
resize_error(baton, in);
(baton->err).append("Width and/or height required");
return;
}
factor = std::max(factor, 1.0); factor = std::max(factor, 1.0);
int shrink = floor(factor); int shrink = floor(factor);
double residual = shrink / factor; double residual = shrink / factor;
@@ -268,11 +285,6 @@ void resize_async_after(uv_work_t *work, int status) {
resize_baton *baton = static_cast<resize_baton*>(work->data); resize_baton *baton = static_cast<resize_baton*>(work->data);
// Free temporary copy of input buffer
if (baton->buffer_in_len > 0) {
g_free(baton->buffer_in);
}
Handle<Value> argv[2] = { Null(), Null() }; Handle<Value> argv[2] = { Null(), Null() };
if (!baton->err.empty()) { if (!baton->err.empty()) {
// Error // Error
@@ -301,12 +313,8 @@ Handle<Value> resize(const Arguments& args) {
baton->file_in = *String::Utf8Value(args[0]->ToString()); baton->file_in = *String::Utf8Value(args[0]->ToString());
if (args[1]->IsObject()) { if (args[1]->IsObject()) {
Local<Object> buffer = args[1]->ToObject(); Local<Object> buffer = args[1]->ToObject();
// Take temporary copy of input buffer
if (Buffer::Length(buffer) > 0) {
baton->buffer_in_len = Buffer::Length(buffer); baton->buffer_in_len = Buffer::Length(buffer);
baton->buffer_in = g_malloc(baton->buffer_in_len); baton->buffer_in = Buffer::Data(buffer);
memcpy(baton->buffer_in, Buffer::Data(buffer), baton->buffer_in_len);
}
} }
baton->file_out = *String::Utf8Value(args[2]->ToString()); baton->file_out = *String::Utf8Value(args[2]->ToString());
baton->width = args[3]->Int32Value(); baton->width = args[3]->Int32Value();
@@ -332,6 +340,22 @@ Handle<Value> resize(const Arguments& args) {
return scope.Close(Undefined()); return scope.Close(Undefined());
} }
Handle<Value> cache(const Arguments& args) {
HandleScope scope;
// Set cache limit
if (args[0]->IsInt32()) {
vips_cache_set_max_mem(args[0]->Int32Value() * 1048576);
}
// Get cache statistics
Local<Object> cache = Object::New();
cache->Set(String::NewSymbol("current"), Number::New(vips_tracked_get_mem() / 1048576));
cache->Set(String::NewSymbol("high"), Number::New(vips_tracked_get_mem_highwater() / 1048576));
cache->Set(String::NewSymbol("limit"), Number::New(vips_cache_get_max_mem() / 1048576));
return scope.Close(cache);
}
static void at_exit(void* arg) { static void at_exit(void* arg) {
HandleScope scope; HandleScope scope;
vips_shutdown(); vips_shutdown();
@@ -342,6 +366,7 @@ extern "C" void init(Handle<Object> target) {
vips_init(""); vips_init("");
AtExit(at_exit); AtExit(at_exit);
NODE_SET_METHOD(target, "resize", resize); NODE_SET_METHOD(target, "resize", resize);
NODE_SET_METHOD(target, "cache", cache);
} }
NODE_MODULE(sharp, init) NODE_MODULE(sharp, init)

View File

@@ -26,4 +26,6 @@ async.mapSeries([1, 1, 2, 4, 8, 16, 32, 64, 128], function(parallelism, next) {
next(); next();
} }
); );
}, function() {}); }, function() {
console.dir(sharp.cache());
});

View File

@@ -420,4 +420,5 @@ async.series({
Object.keys(results).forEach(function(format) { Object.keys(results).forEach(function(format) {
assert.strictEqual("sharp", results[format].toString().substr(0, 5), "sharp was slower than " + results[format] + " for " + format); assert.strictEqual("sharp", results[format].toString().substr(0, 5), "sharp was slower than " + results[format] + " for " + format);
}); });
console.dir(sharp.cache());
}); });

75
tests/random.js Executable file
View File

@@ -0,0 +1,75 @@
var sharp = require("../index");
var fs = require("fs");
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 inputJpg = __dirname + "/2569067123_aca715a2ee_o.jpg"; // http://www.flickr.com/photos/grizdave/2569067123/
var outputJpg = __dirname + "/output.jpg";
var min = 320;
var max = 960;
var randomDimension = function() {
return Math.random() * (max - min) + min;
};
new Benchmark.Suite("random").add("imagemagick", {
defer: true,
fn: function(deferred) {
imagemagick.resize({
srcPath: inputJpg,
dstPath: outputJpg,
quality: 0.8,
width: randomDimension(),
height: randomDimension()
}, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("gm", {
defer: true,
fn: function(deferred) {
gm(inputJpg).resize(randomDimension(), randomDimension()).quality(80).toBuffer(function (err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("epeg", {
defer: true,
fn: function(deferred) {
var image = new epeg.Image({path: inputJpg});
var buffer = image.downsize(randomDimension(), randomDimension(), 80).process();
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
}).add("sharp", {
defer: true,
fn: function(deferred) {
sharp.resize(inputJpg, sharp.buffer.jpeg, randomDimension(), randomDimension(), function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).on("cycle", function(event) {
console.log(String(event.target));
}).on("complete", function() {
var winner = this.filter("fastest").pluck("name");
assert.strictEqual("sharp", String(winner), "sharp was slower than " + winner);
console.dir(sharp.cache());
}).run();

34
tests/unit.js Executable file
View File

@@ -0,0 +1,34 @@
var sharp = require("../index");
var imagemagick = require("imagemagick");
var assert = require("assert");
var inputJpg = __dirname + "/2569067123_aca715a2ee_o.jpg"; // http://www.flickr.com/photos/grizdave/2569067123/
var outputJpg = __dirname + "/output.jpg";
sharp.resize(inputJpg, outputJpg, 320, 240, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(240, features.height);
sharp.resize(inputJpg, outputJpg, 320, -1, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(262, features.height);
});
sharp.resize(inputJpg, outputJpg, -1, 320, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(392, features.width);
assert.strictEqual(320, features.height);
});
});
});
});
});