Compare commits

...

10 Commits

Author SHA1 Message Date
Lovell Fuller
2a0d79a78b Minor markdown layout fix 2014-03-09 21:54:31 +00:00
Lovell Fuller
764b57022b Minor markdown layout fix 2014-03-09 21:53:09 +00:00
Lovell Fuller
5f61331d1a Breaking change to API to become more expressive (see #8). Add support for upscaling. 2014-03-09 21:44:03 +00:00
Lovell Fuller
d0e6a4c0f3 Support identity transform when both width and height are not specified 2014-03-04 22:44:53 +00:00
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 465 additions and 145 deletions

164
README.md
View File

@@ -2,10 +2,10 @@
_adj_ _adj_
1. clearly defined; distinct: a sharp photographic image. 1. clearly defined; distinct: a sharp photographic image.
2. quick, brisk, or spirited. 2. quick, brisk, or spirited.
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, PNG, WebP and TIFF images to smaller 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.
@@ -13,48 +13,42 @@ The performance of JPEG resizing is typically 15x-25x faster than ImageMagick an
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_. 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. Anyone who has used the Node.js bindings for [GraphicsMagick](https://github.com/aheckmann/gm) will find the API similarly expressive.
This module is powered by 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
* 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
brew install homebrew/science/vips
### Install libvips on Ubuntu/Debian 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
npm install sharp npm install sharp
## Usage ## Usage examples
var sharp = require("sharp");
### resize(input, output, width, height, [options], callback)
Scale and crop to `width` x `height` calling `callback` when complete.
`input` can either be a filename String or a Buffer.
`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.
`height` is the Number of pixels high the resultant image should be.
`options` is optional, and can contain one or more of:
* `canvas` can be one of `sharp.canvas.crop`, `sharp.canvas.embedWhite` or `sharp.canvas.embedBlack`. Defaults to `sharp.canvas.crop`.
* `sharpen` when set to true will perform a mild sharpen of the resultant image. This typically reduces performance by 30%.
* `progressive` when set will use progressive (interlace) scan for the output. This typically reduces performance by 30%.
* `sequentialRead` is an advanced setting that, when set, switches the libvips access method to `VIPS_ACCESS_SEQUENTIAL`. This will reduce memory usage and can improve performance on some systems.
`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
```javascript ```javascript
sharp.resize("input.jpg", "output.jpg", 300, 200, function(err) { var sharp = require('sharp');
```
```javascript
sharp('input.jpg').resize(300, 200).write('output.jpg', function(err) {
if (err) { if (err) {
throw err; throw err;
} }
@@ -64,43 +58,127 @@ 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('input.jpg').resize(null, 200).progressive().toBuffer(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('input.png').resize(300).sharpen().webp(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} }
// buffer contains sharpened PNG image data (converted from JPEG) // buffer contains sharpened WebP image data (converted from PNG), 300 pixels wide
}); });
``` ```
```javascript ```javascript
sharp.resize(buffer, "output.tiff", 200, 300, {canvas: sharp.canvas.embedWhite}, function(err) { sharp(buffer).resize(200, 300).embedWhite().write('output.tiff', function(err) {
if (err) { if (err) {
throw err; throw err;
} }
// output.tiff 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
// of the image data contained in buffer embedded on a white canvas // version, embedded on a white canvas, of the image data in buffer
}); });
``` ```
```javascript ```javascript
sharp.resize("input.jpg", sharp.buffer.webp, 200, 300, {canvas: sharp.canvas.embedBlack}, function(err, buffer) { sharp('input.jpg').resize(200, 300).embedBlack().webp(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} }
// buffer contains WebP 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, embedded on a black canvas, of input.png
}); });
``` ```
## API
### sharp(input)
Constructor to which further methods are chained.
`input` can either be a filename String or a Buffer.
### resize(width, [height])
Scale to `width` x `height`. By default, the resized image is cropped to the exact size specified.
`width` is the Number of pixels wide the resultant image should be. Use `null` or `undefined` to auto-scale the width to match the height.
`height` is the Number of pixels high the resultant image should be. Use `null` or `undefined` to auto-scale the height to match the width.
### crop()
Crop the resized image to the exact size specified, the default behaviour.
### embedWhite()
Embed the resized image on a white background of the exact size specified.
### embedBlack()
Embed the resized image on a black background of the exact size specified.
### sharpen()
Perform a mild sharpen of the resultant image. This typically reduces performance by 30%.
### progressive()
Use progressive (interlace) scan for the output. This typically reduces performance by 30%.
### sequentialRead()
An advanced setting that switches the libvips access method to `VIPS_ACCESS_SEQUENTIAL`. This will reduce memory usage and can improve performance on some systems.
### write(filename, callback)
`filename` is a String containing the filename to write the image data to. The format is inferred from the extension, with JPEG, PNG, WebP and TIFF supported.
`callback` is called with a single argument `(err)` containing an error message, if any.
### jpeg(callback)
Write JPEG image data to a Buffer.
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant JPEG image data.
### png(callback)
Write PNG image data to a Buffer.
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant PNG image data.
### webp(callback)
Write WebP image data to a Buffer.
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant WebP image data.
### toBuffer(callback)
Write image data to a Buffer, the format of which will match the input image.
`callback` gets two arguments `(err, buffer)` where `err` is an error message, if any, and `buffer` is the resultant image data.
### sharp.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

169
index.js
View File

@@ -1,65 +1,128 @@
var sharp = require("./build/Release/sharp"); /*jslint node: true */
'use strict';
module.exports.buffer = { var sharp = require('./build/Release/sharp');
jpeg: "__jpeg",
png: "__png",
webp: "__webp"
};
module.exports.canvas = { var Sharp = function(input) {
crop: "c", if (!(this instanceof Sharp)) {
embedWhite: "w", return new Sharp(input);
embedBlack: "b"
};
module.exports.resize = function(input, output, width, height, options, callback) {
"use strict";
if (typeof options === 'function') {
callback = options;
options = {};
} else {
options = options || {};
} }
this.options = {
width: -1,
height: -1,
canvas: 'c',
sharpen: false,
progressive: false,
sequentialRead: false,
output: '__jpeg'
};
if (typeof input === 'string') { if (typeof input === 'string') {
options.inFile = input; this.options.inFile = input;
} else if (typeof input ==='object' && input instanceof Buffer) { } else if (typeof input ==='object' && input instanceof Buffer) {
options.inBuffer = input; this.options.inBuffer = input;
} else { } else {
callback("Unsupported input " + typeof input); throw 'Unsupported input ' + typeof input;
return;
} }
if (!output || output.length === 0) { return this;
callback("Invalid output"); };
return; module.exports = Sharp;
}
var outWidth = Number(width); Sharp.prototype.crop = function() {
if (Number.isNaN(outWidth)) { this.options.canvas = 'c';
callback("Invalid width " + width); return this;
return;
}
var outHeight = Number(height);
if (Number.isNaN(outHeight)) {
callback("Invalid height " + height);
return;
}
var canvas = options.canvas || "c";
if (canvas.length !== 1 || "cwb".indexOf(canvas) === -1) {
callback("Invalid canvas " + canvas);
return;
}
var sharpen = !!options.sharpen;
var progessive = !!options.progessive;
var sequentialRead = !!options.sequentialRead;
sharp.resize(options.inFile, options.inBuffer, output, width, height, canvas, sharpen, progessive, sequentialRead, callback);
}; };
/* Deprecated v0.0.x methods */ Sharp.prototype.embedWhite = function() {
module.exports.crop = function(input, output, width, height, sharpen, callback) { this.options.canvas = 'w';
sharp.resize(input, output, width, height, {canvas: "c", sharpen: true}, callback); return this;
}; };
module.exports.embedWhite = function(input, output, width, height, callback) {
sharp.resize(input, output, width, height, {canvas: "w", sharpen: true}, callback); Sharp.prototype.embedBlack = function() {
this.options.canvas = 'b';
return this;
}; };
module.exports.embedBlack = function(input, output, width, height, callback) {
sharp.resize(input, output, width, height, {canvas: "b", sharpen: true}, callback); Sharp.prototype.sharpen = function(sharpen) {
this.options.sharpen = (typeof sharpen === 'boolean') ? sharpen : true;
return this;
};
Sharp.prototype.progressive = function(progressive) {
this.options.progressive = (typeof progressive === 'boolean') ? progressive : true;
return this;
};
Sharp.prototype.sequentialRead = function(sequentialRead) {
this.options.sequentialRead = (typeof sequentialRead === 'boolean') ? sequentialRead : true;
return this;
};
Sharp.prototype.resize = function(width, height) {
if (!width) {
this.options.width = -1;
} else {
if (!Number.isNaN(width)) {
this.options.width = width;
} else {
throw 'Invalid width ' + width;
}
}
if (!height) {
this.options.height = -1;
} else {
if (!Number.isNaN(height)) {
this.options.height = height;
} else {
throw 'Invalid height ' + height;
}
}
return this;
};
Sharp.prototype.write = function(output, callback) {
if (!output || output.length === 0) {
throw 'Invalid output';
} else {
this._sharp(output, callback);
}
return this;
};
Sharp.prototype.toBuffer = function(callback) {
return this._sharp('__input', callback);
};
Sharp.prototype.jpeg = function(callback) {
return this._sharp('__jpeg', callback);
};
Sharp.prototype.png = function(callback) {
return this._sharp('__png', callback);
};
Sharp.prototype.webp = function(callback) {
return this._sharp('__webp', callback);
};
Sharp.prototype._sharp = function(output, callback) {
sharp.resize(
this.options.inFile,
this.options.inBuffer,
output,
this.options.width,
this.options.height,
this.options.canvas,
this.options.sharpen,
this.options.progressive,
this.options.sequentialRead,
callback
);
return this;
};
module.exports.cache = function(limit) {
if (Number.isNaN(limit)) {
limit = null;
}
return sharp.cache(limit);
}; };

View File

@@ -1,10 +1,10 @@
{ {
"name": "sharp", "name": "sharp",
"version": "0.1.6", "version": "0.2.0",
"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

@@ -15,8 +15,8 @@ struct resize_baton {
std::string file_out; std::string file_out;
void* buffer_out; void* buffer_out;
size_t buffer_out_len; size_t buffer_out_len;
int width; int width;
int height; int height;
bool crop; bool crop;
VipsExtend extend; VipsExtend extend;
bool sharpen; bool sharpen;
@@ -70,6 +70,7 @@ void resize_error(resize_baton *baton, VipsImage *unref) {
void resize_async(uv_work_t *work) { void resize_async(uv_work_t *work) {
resize_baton* baton = static_cast<resize_baton*>(work->data); resize_baton* baton = static_cast<resize_baton*>(work->data);
// Input
ImageType inputImageType = JPEG; ImageType inputImageType = JPEG;
VipsImage *in = vips_image_new(); VipsImage *in = vips_image_new();
if (baton->buffer_in_len > 1) { if (baton->buffer_in_len > 1) {
@@ -117,12 +118,32 @@ void resize_async(uv_work_t *work) {
return; return;
} }
double xfactor = static_cast<double>(in->Xsize) / std::max(baton->width, 1); // Scaling calculations
double yfactor = static_cast<double>(in->Ysize) / std::max(baton->height, 1); double factor;
double factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor); if (baton->width > 0 && baton->height > 0) {
factor = std::max(factor, 1.0); // Fixed width and height
double xfactor = (double)(in->Xsize) / (double)(baton->width);
double yfactor = (double)(in->Ysize) / (double)(baton->height);
factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor);
} else if (baton->width > 0) {
// Fixed width, auto height
factor = (double)(in->Xsize) / (double)(baton->width);
baton->height = floor((double)(in->Ysize) / factor);
} else if (baton->height > 0) {
// Fixed height, auto width
factor = (double)(in->Ysize) / (double)(baton->height);
baton->width = floor((double)(in->Xsize) / factor);
} else {
// Identity transform
factor = 1;
baton->width = in->Xsize;
baton->height = in->Ysize;
}
int shrink = floor(factor); int shrink = floor(factor);
double residual = shrink / factor; if (shrink < 1) {
shrink = 1;
}
double residual = shrink / (double)factor;
// Try to use libjpeg shrink-on-load // Try to use libjpeg shrink-on-load
int shrink_on_load = 1; int shrink_on_load = 1;
@@ -172,7 +193,7 @@ void resize_async(uv_work_t *work) {
// Use vips_affine with the remaining float part using bilinear interpolation // Use vips_affine with the remaining float part using bilinear interpolation
VipsImage *affined = vips_image_new(); VipsImage *affined = vips_image_new();
if (residual > 0) { if (residual != 0) {
if (vips_affine(shrunk, &affined, residual, 0, 0, residual, "interpolate", vips_interpolate_bilinear_static(), NULL)) { if (vips_affine(shrunk, &affined, residual, 0, 0, residual, "interpolate", vips_interpolate_bilinear_static(), NULL)) {
return resize_error(baton, shrunk); return resize_error(baton, shrunk);
} }
@@ -181,6 +202,7 @@ void resize_async(uv_work_t *work) {
} }
g_object_unref(shrunk); g_object_unref(shrunk);
// Crop/embed
VipsImage *canvased = vips_image_new(); VipsImage *canvased = vips_image_new();
if (affined->Xsize != baton->width || affined->Ysize != baton->height) { if (affined->Xsize != baton->width || affined->Ysize != baton->height) {
if (baton->crop) { if (baton->crop) {
@@ -221,17 +243,18 @@ void resize_async(uv_work_t *work) {
} }
g_object_unref(canvased); g_object_unref(canvased);
if (baton->file_out == "__jpeg") { // Output
if (baton->file_out == "__jpeg" || (baton->file_out == "__input" && inputImageType == JPEG)) {
// Write JPEG to buffer // Write JPEG to buffer
if (vips_jpegsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) { if (vips_jpegsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
return resize_error(baton, sharpened); return resize_error(baton, sharpened);
} }
} else if (baton->file_out == "__png") { } else if (baton->file_out == "__png" || (baton->file_out == "__input" && inputImageType == PNG)) {
// Write PNG to buffer // Write PNG to buffer
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") { } else if (baton->file_out == "__webp" || (baton->file_out == "__input" && inputImageType == WEBP)) {
// Write WEBP to buffer // Write WEBP to buffer
if (vips_webpsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, NULL)) { if (vips_webpsave_buffer(sharpened, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, NULL)) {
return resize_error(baton, sharpened); return resize_error(baton, sharpened);
@@ -268,11 +291,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 +319,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 baton->buffer_in_len = Buffer::Length(buffer);
if (Buffer::Length(buffer) > 0) { baton->buffer_in = Buffer::Data(buffer);
baton->buffer_in_len = Buffer::Length(buffer);
baton->buffer_in = g_malloc(baton->buffer_in_len);
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 +346,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 +372,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

@@ -11,7 +11,7 @@ async.mapSeries([1, 1, 2, 4, 8, 16, 32, 64, 128], function(parallelism, next) {
var start = new Date().getTime(); var start = new Date().getTime();
async.times(parallelism, async.times(parallelism,
function(id, callback) { function(id, callback) {
sharp.resize(inputJpg, sharp.buffer.jpeg, width, height, function(err, buffer) { sharp(inputJpg).resize(width, height).toBuffer(function(err, buffer) {
buffer = null; buffer = null;
callback(err, new Date().getTime() - start); callback(err, new Date().getTime() - start);
}); });
@@ -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

@@ -83,7 +83,7 @@ async.series({
}).add("sharp-buffer-file", { }).add("sharp-buffer-file", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputJpgBuffer, outputJpg, width, height, function(err) { sharp(inputJpgBuffer).resize(width, height).write(outputJpg, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -94,7 +94,7 @@ async.series({
}).add("sharp-buffer-buffer", { }).add("sharp-buffer-buffer", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputJpgBuffer, sharp.buffer.jpeg, width, height, function(err, buffer) { sharp(inputJpgBuffer).resize(width, height).toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -106,7 +106,7 @@ async.series({
}).add("sharp-file-file", { }).add("sharp-file-file", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputJpg, outputJpg, width, height, function(err) { sharp(inputJpg).resize(width, height).write(outputJpg, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -117,7 +117,7 @@ async.series({
}).add("sharp-file-buffer", { }).add("sharp-file-buffer", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputJpg, sharp.buffer.jpeg, width, height, function(err, buffer) { sharp(inputJpg).resize(width, height).toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -129,7 +129,7 @@ async.series({
}).add("sharp-file-buffer-sharpen", { }).add("sharp-file-buffer-sharpen", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputJpg, sharp.buffer.jpeg, width, height, {sharpen: true}, function(err, buffer) { sharp(inputJpg).resize(width, height).sharpen().toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -141,7 +141,7 @@ async.series({
}).add("sharp-file-buffer-progressive", { }).add("sharp-file-buffer-progressive", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputJpg, sharp.buffer.jpeg, width, height, {progressive: true}, function(err, buffer) { sharp(inputJpg).resize(width, height).progressive().toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -153,7 +153,7 @@ async.series({
}).add("sharp-file-buffer-sequentialRead", { }).add("sharp-file-buffer-sequentialRead", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputJpg, sharp.buffer.jpeg, width, height, {sequentialRead: true}, function(err, buffer) { sharp(inputJpg).resize(width, height).sequentialRead().toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -212,7 +212,7 @@ async.series({
}).add("sharp-buffer-file", { }).add("sharp-buffer-file", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputPngBuffer, outputPng, width, height, function(err) { sharp(inputPngBuffer).resize(width, height).write(outputPng, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -223,7 +223,7 @@ async.series({
}).add("sharp-buffer-buffer", { }).add("sharp-buffer-buffer", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputPngBuffer, sharp.buffer.png, width, height, function(err, buffer) { sharp(inputPngBuffer).resize(width, height).toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -235,7 +235,7 @@ async.series({
}).add("sharp-file-file", { }).add("sharp-file-file", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputPng, outputPng, width, height, function(err) { sharp(inputPng).resize(width, height).write(outputPng, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -246,7 +246,7 @@ async.series({
}).add("sharp-file-buffer", { }).add("sharp-file-buffer", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputPng, sharp.buffer.png, width, height, function(err, buffer) { sharp(inputPng).resize(width, height).toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -258,7 +258,7 @@ async.series({
}).add("sharp-file-buffer-sharpen", { }).add("sharp-file-buffer-sharpen", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputPng, sharp.buffer.png, width, height, {sharpen: true}, function(err, buffer) { sharp(inputPng).resize(width, height).sharpen().toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -270,7 +270,7 @@ async.series({
}).add("sharp-file-buffer-progressive", { }).add("sharp-file-buffer-progressive", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputPng, sharp.buffer.png, width, height, {progressive: true}, function(err, buffer) { sharp(inputPng).resize(width, height).progressive().toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -282,7 +282,7 @@ async.series({
}).add("sharp-file-buffer-sequentialRead", { }).add("sharp-file-buffer-sequentialRead", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputPng, sharp.buffer.png, width, height, {sequentialRead: true}, function(err, buffer) { sharp(inputPng).sequentialRead().resize(width, height).toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -302,7 +302,7 @@ async.series({
(new Benchmark.Suite("webp")).add("sharp-buffer-file", { (new Benchmark.Suite("webp")).add("sharp-buffer-file", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputWebpBuffer, outputWebp, width, height, function(err) { sharp(inputWebpBuffer).resize(width, height).write(outputWebp, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -313,7 +313,7 @@ async.series({
}).add("sharp-buffer-buffer", { }).add("sharp-buffer-buffer", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputWebpBuffer, sharp.buffer.webp, width, height, function(err, buffer) { sharp(inputWebpBuffer).resize(width, height).toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -325,7 +325,7 @@ async.series({
}).add("sharp-file-file", { }).add("sharp-file-file", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputWebp, outputWebp, width, height, function(err) { sharp(inputWebp).resize(width, height).write(outputWebp, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -336,7 +336,7 @@ async.series({
}).add("sharp-file-buffer", { }).add("sharp-file-buffer", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputWebp, sharp.buffer.webp, width, height, function(err, buffer) { sharp(inputWebp).resize(width, height).toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -348,7 +348,7 @@ async.series({
}).add("sharp-file-buffer-sharpen", { }).add("sharp-file-buffer-sharpen", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputWebp, sharp.buffer.webp, width, height, {sharpen: true}, function(err, buffer) { sharp(inputWebp).resize(width, height).sharpen().toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -360,7 +360,7 @@ async.series({
}).add("sharp-file-buffer-sequentialRead", { }).add("sharp-file-buffer-sequentialRead", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputWebp, sharp.buffer.webp, width, height, {sequentialRead: true}, function(err, buffer) { sharp(inputWebp).sequentialRead().resize(width, height).toBuffer(function(err, buffer) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -379,7 +379,7 @@ async.series({
(new Benchmark.Suite("tiff")).add("sharp-file-file", { (new Benchmark.Suite("tiff")).add("sharp-file-file", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputTiff, outputTiff, width, height, function(err) { sharp(inputTiff).resize(width, height).write(outputTiff, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -390,7 +390,7 @@ async.series({
}).add("sharp-file-file-sharpen", { }).add("sharp-file-file-sharpen", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputTiff, outputTiff, width, height, {sharpen: true}, function(err) { sharp(inputTiff).resize(width, height).sharpen().write(outputTiff, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -401,7 +401,7 @@ async.series({
}).add("sharp-file-file-sequentialRead", { }).add("sharp-file-file-sequentialRead", {
defer: true, defer: true,
fn: function(deferred) { fn: function(deferred) {
sharp.resize(inputTiff, outputTiff, width, height, {sequentialRead: true}, function(err) { sharp(inputTiff).sequentialRead().resize(width, height).write(outputTiff, function(err) {
if (err) { if (err) {
throw err; throw err;
} else { } else {
@@ -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(inputJpg).resize(randomDimension(), randomDimension()).toBuffer(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();

70
tests/unit.js Executable file
View File

@@ -0,0 +1,70 @@
var sharp = require("../index");
var imagemagick = require("imagemagick");
var assert = require("assert");
var async = require("async");
var inputJpg = __dirname + "/2569067123_aca715a2ee_o.jpg"; // http://www.flickr.com/photos/grizdave/2569067123/
var outputJpg = __dirname + "/output.jpg";
async.series([
// Resize with exact crop
function(done) {
sharp(inputJpg).resize(320, 240).write(outputJpg, 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);
done();
});
});
},
// Resize to fixed width
function(done) {
sharp(inputJpg).resize(320).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(320, features.width);
assert.strictEqual(261, features.height);
done();
});
});
},
// Resize to fixed height
function(done) {
sharp(inputJpg).resize(null, 320).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(391, features.width);
assert.strictEqual(320, features.height);
done();
});
});
},
// Identity transform
function(done) {
sharp(inputJpg).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(2725, features.width);
assert.strictEqual(2225, features.height);
done();
});
});
},
// Upscale
function(done) {
sharp(inputJpg).resize(3000).write(outputJpg, function(err) {
if (err) throw err;
imagemagick.identify(outputJpg, function(err, features) {
if (err) throw err;
assert.strictEqual(3000, features.width);
assert.strictEqual(2449, features.height);
done();
});
});
}
]);