Compare commits

...

17 Commits

Author SHA1 Message Date
Lovell Fuller
d509458ba1 Major rewrite and therefore API change. Despite the name, image sharpening is now optional, plus there are other new options. Can now handle buffers in and out. Doubled performance in certain cases. Closes #3. Closes #4. 2014-01-19 21:38:37 +00:00
Lovell Fuller
be8f35d830 Use shrink-on-load for JPEG images, partially implementing #4. Switch to new vip_ methods from legacy im_ methods. Large performance gains all round. 2014-01-16 22:51:44 +00:00
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
Lovell Fuller
b836704451 Improved performance benchmarks and usage docs 2013-08-26 16:10:48 +01:00
9 changed files with 726 additions and 209 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

165
README.md
View File

@@ -7,28 +7,18 @@ _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. 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 the University of Southampton. Speed is typically 25-30% faster than the imagemagick equivalent. Performance is up to 18x faster than ImageMagick and up to 8x 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+
* [libvips](https://github.com/jcupitt/libvips) v7.38+
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
Ubuntu 12.04 LTS:
sudo ln -s /usr/lib/pkgconfig/vips-7.26.pc /usr/lib/pkgconfig/vips.pc
Ubuntu 13.04:
sudo ln -s /usr/lib/x86_64-linux-gnu/pkgconfig/vips-7.28.pc /usr/lib/pkgconfig/vips.pc
## Install ## Install
@@ -36,28 +26,139 @@ Ubuntu 13.04:
## Usage ## Usage
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. When using a filename libvips will `mmap` the file for improved performance.
`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`.
`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
var sharp = require("sharp"); sharp.resize("input.jpg", "output.jpg", 300, 200, function(err) {
sharp.crop("input.jpg", "output.jpg", 300, 200, function(err) {
if (err) { if (err) {
throw err; throw err;
} }
// output.jpg is cropped input.jpg // output.jpg is a 300 pixels wide and 200 pixels high image
}); // containing a scaled and cropped version of input.jpg
sharp.embedWhite("input.jpg", "output.jpg", 200, 300, function(err) {
if (err) {
throw err;
}
// output.jpg contains input.jpg embedded with a white border
});
sharp.embedBlack("input.jpg", "output.jpg", 200, 300, function(err) {
if (err) {
throw err;
}
// output.jpg contains input.jpg embedded with a black border
}); });
``` ```
## Testing [![Build Status](https://travis-ci.org/lovell/sharp.png?branch=master)](https://travis-ci.org/lovell/sharp) ```javascript
sharp.resize("input.jpg", sharp.buffer.jpeg, 300, 200, {progressive: true}, function(err, buffer) {
if (err) {
throw err;
}
// buffer contains progressive JPEG image data
});
```
```javascript
sharp.resize("input.jpg", sharp.buffer.png, 300, 200, {sharpen: true}, function(err, buffer) {
if (err) {
throw err;
}
// buffer contains sharpened PNG image data (converted from JPEG)
});
```
```javascript
sharp.resize(buffer, "output.jpg", 200, 300, {canvas: sharp.canvas.embedWhite}, function(err) {
if (err) {
throw err;
}
// output.jpg 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
});
```
```javascript
sharp.resize("input.jpg", sharp.buffer.jpeg, 200, 300, {canvas: sharp.canvas.embedBlack}, function(err, buffer) {
if (err) {
throw err;
}
// buffer contains JPEG image data of a 200 pixels wide and 300 pixels high image
// containing a scaled version of input.png embedded on a black canvas
});
```
## Testing
npm test npm test
## Performance
Test environment:
* AMD Athlon 4 core 3.3GHz 512KB L2 CPU 1333 DDR3
* libvips 7.37
* libjpeg-turbo8 1.3.0
* libpng 1.6.6
* zlib1g 1.2.7
`-file-buffer` indicates read from file and write to buffer, `-buffer-file` indicates read from buffer and write to file etc.
`-sharpen`, `-progressive` etc. demonstrate the negative effect of options on performance.
### JPEG
* imagemagick x 5.50 ops/sec ±0.48% (31 runs sampled)
* gm-file-file x 11.19 ops/sec ±0.51% (57 runs sampled)
* gm-file-buffer x 11.11 ops/sec ±0.42% (57 runs sampled)
* epeg-file-file x 28.59 ops/sec ±0.09% (71 runs sampled)
* epeg-file-buffer x 28.67 ops/sec ±0.14% (71 runs sampled)
* sharp-buffer-file x 24.72 ops/sec ±0.42% (62 runs sampled)
* sharp-buffer-buffer x 24.24 ops/sec ±0.36% (61 runs sampled)
* sharp-file-file x 97.15 ops/sec ±0.44% (80 runs sampled)
* sharp-file-buffer x __98.51 ops/sec__ ±0.42% (80 runs sampled)
* sharp-file-buffer-sharpen x 56.99 ops/sec ±5.43% (57 runs sampled)
* sharp-file-buffer-progressive x 64.89 ops/sec ±0.42% (79 runs sampled)
* sharp-file-buffer-sequentialRead x 64.13 ops/sec ±0.40% (78 runs sampled)
### PNG
* imagemagick x 4.31 ops/sec ±0.27% (26 runs sampled)
* gm-file-file x 17.89 ops/sec ±0.21% (86 runs sampled)
* gm-file-buffer x 14.74 ops/sec ±0.15% (73 runs sampled)
* sharp-buffer-file x 4.97 ops/sec ±120.47% (26 runs sampled)
* sharp-buffer-buffer x 13.00 ops/sec ±0.53% (65 runs sampled)
* sharp-file-file x 53.00 ops/sec ±7.15% (88 runs sampled)
* sharp-file-buffer x __55.43 ops/sec__ ±0.65% (89 runs sampled)
* sharp-file-buffer-sharpen x 45.37 ops/sec ±0.38% (74 runs sampled)
* sharp-file-buffer-progressive x 55.49 ops/sec ±0.45% (89 runs sampled)
* sharp-file-buffer-sequentialRead x 32.27 ops/sec ±0.29% (79 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,13 +2,16 @@
'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',
'/usr/lib/x86_64-linux-gnu/glib-2.0/include' '/usr/lib/x86_64-linux-gnu/glib-2.0/include'
], ],
'cflags': ['-fexceptions'], 'cflags': ['-fexceptions', '-O3'],
'cflags_cc': ['-fexceptions'] 'cflags_cc': ['-fexceptions', '-O3']
}] }]
} }

View File

@@ -1,13 +1,64 @@
var sharp = require("./build/Release/sharp"); var sharp = require("./build/Release/sharp");
module.exports.crop = function(input, output, width, height, callback) { module.exports.buffer = {
sharp.resize(input, output, width, height, "c", callback) jpeg: "__jpeg",
} png: "__png"
};
module.exports.canvas = {
crop: "c",
embedWhite: "w",
embedBlack: "b"
};
module.exports.resize = function(input, output, width, height, options, callback) {
"use strict";
if (typeof options === 'function') {
callback = options;
options = {};
} else {
options = options || {};
}
if (typeof input === 'string') {
options.inFile = input;
} else if (typeof input ==='object' && input instanceof Buffer) {
options.inBuffer = input;
} else {
callback("Unsupported input " + typeof input);
return;
}
if (!output || output.length === 0) {
callback("Invalid output");
return;
}
var outWidth = Number(width);
if (Number.isNaN(outWidth)) {
callback("Invalid width " + width);
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 */
module.exports.crop = function(input, output, width, height, sharpen, callback) {
sharp.resize(input, output, width, height, {canvas: "c", sharpen: true}, 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, {canvas: "w", sharpen: true}, 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, {canvas: "b", sharpen: true}, callback);
} };

View File

@@ -1,31 +1,37 @@
{ {
"name": "sharp", "name": "sharp",
"version": "0.0.1", "version": "0.1.0",
"main": "./build/Release/sharp", "author": "Lovell Fuller",
"description": "High performance Node.js module to resize JPEG images using the libvips image processing library", "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",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/lovell/sharp" "url": "git://github.com/lovell/sharp"
}, },
"devDependencies": {
"imagemagick": "*"
},
"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

@@ -1,154 +1,251 @@
#include <node.h> #include <node.h>
#include <node_buffer.h>
#include <math.h> #include <math.h>
#include <string> #include <string>
#include <vector> #include <string.h>
#include <vips/vips.h> #include <vips/vips.h>
using namespace v8; using namespace v8;
using namespace node;
// Free VipsImage children when object goes out of scope struct resize_baton {
// Thanks due to https://github.com/dosx/node-vips std::string file_in;
class ImageFreer { void* buffer_in;
public: size_t buffer_in_len;
ImageFreer() {} std::string file_out;
~ImageFreer() { void* buffer_out;
for (uint16_t i = 0; i < v_.size(); i++) { size_t buffer_out_len;
if (v_[i] != NULL) { int width;
g_object_unref(v_[i]); int height;
}
}
v_.clear();
}
void add(VipsImage* i) { v_.push_back(i); }
private:
std::vector<VipsImage*> v_;
};
struct ResizeBaton {
std::string src;
std::string dst;
int cols;
int rows;
bool crop; bool crop;
int embed; int embed;
bool sharpen;
bool progessive;
VipsAccess access_method;
std::string err; std::string err;
Persistent<Function> callback; Persistent<Function> callback;
resize_baton(): buffer_in_len(0), buffer_out_len(0) {}
}; };
void ResizeAsync(uv_work_t *work) { typedef enum {
ResizeBaton* baton = static_cast<ResizeBaton*>(work->data); JPEG,
PNG
} ImageType;
VipsImage *in = vips_image_new_mode((baton->src).c_str(), "p"); unsigned char MARKER_JPEG[] = {0xff, 0xd8};
im_jpeg2vips((baton->src).c_str(), in); unsigned char MARKER_PNG[] = {0x89, 0x50};
if (in == NULL) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
ImageFreer freer;
freer.add(in);
VipsImage* img = in; bool ends_with(std::string const &str, std::string const &end) {
VipsImage* t[4]; return str.length() >= end.length() && 0 == str.compare(str.length() - end.length(), end.length(), end);
}
if (im_open_local_array(img, t, 4, "temp", "p")) { bool is_jpeg(std::string const &str) {
(baton->err).append(vips_error_buffer()); return ends_with(str, ".jpg") || ends_with(str, ".jpeg");
vips_error_clear(); }
bool is_png(std::string const &str) {
return ends_with(str, ".png");
}
void resize_error(resize_baton *baton, VipsImage *unref) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
g_object_unref(unref);
vips_thread_shutdown();
return;
}
void resize_async(uv_work_t *work) {
resize_baton* baton = static_cast<resize_baton*>(work->data);
ImageType inputImageType = JPEG;
VipsImage *in = vips_image_new();
if (baton->buffer_in_len > 1) {
if (memcmp(MARKER_JPEG, baton->buffer_in, 2) == 0) {
if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else if(memcmp(MARKER_PNG, baton->buffer_in, 2) == 0) {
inputImageType = PNG;
if (vips_pngload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
}
} else if (is_jpeg(baton->file_in)) {
if (vips_jpegload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else if (is_png(baton->file_in)) {
inputImageType = PNG;
if (vips_pngload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) {
return resize_error(baton, in);
}
} else {
resize_error(baton, in);
(baton->err).append("Unsupported input " + baton->file_in);
return; return;
} }
double xfactor = static_cast<double>(img->Xsize) / std::max(baton->cols, 1); double xfactor = static_cast<double>(in->Xsize) / std::max(baton->width, 1);
double yfactor = static_cast<double>(img->Ysize) / std::max(baton->rows, 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 = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor);
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;
// Use im_shrink with the integral reduction // Try to use libjpeg shrink-on-load
if (im_shrink(img, t[0], shrink, shrink)) { int shrink_on_load = 1;
(baton->err).append(vips_error_buffer()); if (inputImageType == JPEG) {
vips_error_clear(); if (shrink >= 8) {
return; residual = residual * shrink / 8;
} shrink_on_load = 8;
shrink = 1;
// Use im_affinei with the remaining float part using bilinear interpolation } else if (shrink >= 4) {
if (im_affinei_all(t[0], t[1], vips_interpolate_bilinear_static(), residual, 0, 0, residual, 0, 0)) { residual = residual * shrink / 4;
(baton->err).append(vips_error_buffer()); shrink_on_load = 4;
vips_error_clear(); shrink = 1;
return; } else if (shrink >= 2) {
} residual = residual * shrink / 2;
img = t[1]; shrink_on_load = 2;
shrink = 1;
if (baton->crop) { }
int width = std::min(img->Xsize, baton->cols); if (shrink_on_load > 1) {
int height = std::min(img->Ysize, baton->rows); g_object_unref(in);
int left = (img->Xsize - width + 1) / 2; in = vips_image_new();
int top = (img->Ysize - height + 1) / 2; if (baton->buffer_in_len > 1) {
if (im_extract_area(img, t[2], left, top, width, height)) { if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "shrink", shrink_on_load, NULL)) {
(baton->err).append(vips_error_buffer()); return resize_error(baton, in);
vips_error_clear(); }
return; } else {
if (vips_jpegload((baton->file_in).c_str(), &in, "shrink", shrink_on_load, NULL)) {
return resize_error(baton, in);
}
}
}
}
VipsImage *shrunk = vips_image_new();
if (shrink > 1) {
// Use vips_shrink with the integral reduction
if (vips_shrink(in, &shrunk, shrink, shrink, NULL)) {
return resize_error(baton, in);
} }
img = t[2];
} else { } else {
int left = (baton->cols - img->Xsize) / 2; vips_copy(in, &shrunk, NULL);
int top = (baton->rows - img->Ysize) / 2;
if (im_embed(img, t[2], baton->embed, left, top, baton->cols, baton->rows)) {
(baton->err).append(vips_error_buffer());
vips_error_clear();
return;
}
img = t[2];
} }
g_object_unref(in);
// Use vips_affine with the remaining float part using bilinear interpolation
VipsImage *affined = vips_image_new();
if (vips_affine(shrunk, &affined, residual, 0, 0, residual, "interpolate", vips_interpolate_bilinear_static(), NULL)) {
return resize_error(baton, shrunk);
}
g_object_unref(shrunk);
VipsImage *canvased = vips_image_new();
if (baton->crop) {
int width = std::min(affined->Xsize, baton->width);
int height = std::min(affined->Ysize, baton->height);
int left = (affined->Xsize - width + 1) / 2;
int top = (affined->Ysize - height + 1) / 2;
if (vips_extract_area(affined, &canvased, left, top, width, height, NULL)) {
return resize_error(baton, affined);
}
} else {
int left = (baton->width - affined->Xsize) / 2;
int top = (baton->height - affined->Ysize) / 2;
if (vips_embed(affined, &canvased, baton->embed, left, top, baton->width, baton->height, NULL)) {
return resize_error(baton, affined);
}
}
g_object_unref(affined);
// Mild sharpen // Mild sharpen
INTMASK* sharpen = im_create_imaskv("sharpen", 3, 3, VipsImage *sharpened = vips_image_new();
-1, -1, -1, if (baton->sharpen) {
-1, 32, -1, INTMASK* sharpen = im_create_imaskv("sharpen", 3, 3,
-1, -1, -1); -1, -1, -1,
sharpen->scale = 24; -1, 32, -1,
if (im_conv(img, t[3], sharpen)) { -1, -1, -1);
(baton->err).append(vips_error_buffer()); sharpen->scale = 24;
vips_error_clear(); if (im_conv(canvased, sharpened, sharpen)) {
return; return resize_error(baton, canvased);
}
} else {
vips_copy(canvased, &sharpened, NULL);
} }
img = t[3]; g_object_unref(canvased);
if (im_vips2jpeg(img, baton->dst.c_str())) { if (baton->file_out == "__jpeg") {
(baton->err).append(vips_error_buffer()); // Write JPEG to buffer
vips_error_clear(); if (vips_jpegsave_buffer(canvased, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
return resize_error(baton, canvased);
}
} else if (baton->file_out == "__png") {
// Write PNG to buffer
if (vips_pngsave_buffer(canvased, &baton->buffer_out, &baton->buffer_out_len, "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) {
return resize_error(baton, canvased);
}
} else if (is_jpeg(baton->file_out)) {
// Write JPEG to file
if (vips_jpegsave(canvased, baton->file_out.c_str(), "strip", TRUE, "Q", 80, "optimize_coding", TRUE, "interlace", baton->progessive, NULL)) {
return resize_error(baton, canvased);
}
} else if (is_png(baton->file_out)) {
// Write PNG to file
if (vips_pngsave(canvased, baton->file_out.c_str(), "strip", TRUE, "compression", 6, "interlace", baton->progessive, NULL)) {
return resize_error(baton, canvased);
}
} else {
(baton->err).append("Unsupported output " + baton->file_out);
} }
g_object_unref(canvased);
vips_thread_shutdown();
} }
void ResizeAsyncAfter(uv_work_t *work, int status) { void resize_async_after(uv_work_t *work, int status) {
HandleScope scope; HandleScope scope;
ResizeBaton *baton = static_cast<ResizeBaton*>(work->data); resize_baton *baton = static_cast<resize_baton*>(work->data);
Local<Value> argv[1]; Handle<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 *slowBuffer = Buffer::New(baton->buffer_out_len);
memcpy(Buffer::Data(slowBuffer), baton->buffer_out, baton->buffer_out_len);
Local<Object> globalObj = Context::GetCurrent()->Global();
Local<Function> bufferConstructor = Local<Function>::Cast(globalObj->Get(String::New("Buffer")));
Handle<Value> constructorArgs[3] = { slowBuffer->handle_, v8::Integer::New(baton->buffer_out_len), v8::Integer::New(0) };
argv[1] = bufferConstructor->NewInstance(3, constructorArgs);
g_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;
} }
Handle<Value> Resize(const Arguments& args) { Handle<Value> resize(const Arguments& args) {
HandleScope scope; HandleScope scope;
ResizeBaton *baton = new ResizeBaton; resize_baton *baton = new resize_baton;
baton->src = *String::Utf8Value(args[0]->ToString()); baton->file_in = *String::Utf8Value(args[0]->ToString());
baton->dst = *String::Utf8Value(args[1]->ToString()); if (args[1]->IsObject()) {
baton->cols = args[2]->Int32Value(); Local<Object> buffer = args[1]->ToObject();
baton->rows = args[3]->Int32Value(); baton->buffer_in = Buffer::Data(buffer);
Local<String> canvas = args[4]->ToString(); baton->buffer_in_len = Buffer::Length(buffer);
}
baton->file_out = *String::Utf8Value(args[2]->ToString());
baton->width = args[3]->Int32Value();
baton->height = args[4]->Int32Value();
Local<String> canvas = args[5]->ToString();
if (canvas->Equals(String::NewSymbol("c"))) { if (canvas->Equals(String::NewSymbol("c"))) {
baton->crop = true; baton->crop = true;
} else if (canvas->Equals(String::NewSymbol("w"))) { } else if (canvas->Equals(String::NewSymbol("w"))) {
baton->crop = false; baton->crop = false;
baton->embed = 4; baton->embed = 4;
@@ -156,18 +253,27 @@ Handle<Value> Resize(const Arguments& args) {
baton->crop = false; baton->crop = false;
baton->embed = 0; baton->embed = 0;
} }
baton->callback = Persistent<Function>::New(Local<Function>::Cast(args[5])); baton->sharpen = args[6]->BooleanValue();
baton->progessive = args[7]->BooleanValue();
baton->access_method = args[8]->BooleanValue() ? VIPS_ACCESS_SEQUENTIAL : VIPS_ACCESS_RANDOM;
baton->callback = Persistent<Function>::New(Local<Function>::Cast(args[9]));
uv_work_t *work = new uv_work_t; uv_work_t *work = new uv_work_t;
work->data = baton; work->data = baton;
uv_queue_work(uv_default_loop(), work, ResizeAsync, (uv_after_work_cb)ResizeAsyncAfter); uv_queue_work(uv_default_loop(), work, resize_async, (uv_after_work_cb)resize_async_after);
return Undefined(); return scope.Close(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("");
NODE_SET_METHOD(target, "resize", Resize); AtExit(at_exit);
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,42 +1,299 @@
var sharp = require("../index"); var sharp = require("../index");
var fs = require("fs");
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");
// http://www.flickr.com/photos/grizdave/2569067123/ var inputJpg = __dirname + "/2569067123_aca715a2ee_o.jpg"; // http://www.flickr.com/photos/grizdave/2569067123/
var input = __dirname + "/2569067123_aca715a2ee_o.jpg"; var outputJpg = __dirname + "/output.jpg";
var output = __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 width = 640;
var height = 480; var height = 480;
// imagemagick async.series({
var time = process.hrtime(); jpeg: function(callback) {
imagemagick.resize({ var inputJpgBuffer = fs.readFileSync(inputJpg);
srcPath: input, (new Benchmark.Suite("jpeg")).add("imagemagick", {
dstPath: output, defer: true,
quality: 0.75, fn: function(deferred) {
width: width, imagemagick.resize({
height: height srcPath: inputJpg,
}, function(err) { dstPath: outputJpg,
if (err) { quality: 0.8,
throw err; width: width,
height: height
}, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("gm-file-file", {
defer: true,
fn: function(deferred) {
gm(inputJpg).crop(width, height).quality(80).write(outputJpg, function (err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("gm-file-buffer", {
defer: true,
fn: function(deferred) {
gm(inputJpg).crop(width, height).quality(80).toBuffer(function (err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("epeg-file-file", {
defer: true,
fn: function(deferred) {
var image = new epeg.Image({path: inputJpg});
image.downsize(width, height, 80).saveTo(outputJpg);
deferred.resolve();
}
}).add("epeg-file-buffer", {
defer: true,
fn: function(deferred) {
var image = new epeg.Image({path: inputJpg});
var buffer = image.downsize(width, height, 80).process();
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
}).add("sharp-buffer-file", {
defer: true,
fn: function(deferred) {
sharp.resize(inputJpgBuffer, outputJpg, width, height, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-buffer-buffer", {
defer: true,
fn: function(deferred) {
sharp.resize(inputJpgBuffer, sharp.buffer.jpeg, 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(inputJpg, outputJpg, width, height, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-file-buffer", {
defer: true,
fn: function(deferred) {
sharp.resize(inputJpg, sharp.buffer.jpeg, 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(inputJpg, sharp.buffer.jpeg, width, height, {sharpen: true}, function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-progressive", {
defer: true,
fn: function(deferred) {
sharp.resize(inputJpg, sharp.buffer.jpeg, width, height, {progressive: 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(inputJpg, sharp.buffer.jpeg, width, height, {sequentialRead: true}, function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
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) {
var inputPngBuffer = fs.readFileSync(inputPng);
(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-file-file", {
defer: true,
fn: function(deferred) {
gm(inputPng).crop(width, height).write(outputPng, function (err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("gm-file-buffer", {
defer: true,
fn: function(deferred) {
gm(inputPng).crop(width, height).quality(80).toBuffer(function (err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-buffer-file", {
defer: true,
fn: function(deferred) {
sharp.resize(inputPngBuffer, outputPng, width, height, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-buffer-buffer", {
defer: true,
fn: function(deferred) {
sharp.resize(inputPngBuffer, sharp.buffer.png, 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(inputPng, outputPng, width, height, function(err) {
if (err) {
throw err;
} else {
deferred.resolve();
}
});
}
}).add("sharp-file-buffer", {
defer: true,
fn: function(deferred) {
sharp.resize(inputPng, sharp.buffer.png, 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(inputPng, sharp.buffer.png, width, height, {sharpen: true}, function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).add("sharp-file-buffer-progressive", {
defer: true,
fn: function(deferred) {
sharp.resize(inputPng, sharp.buffer.png, width, height, {progressive: 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(inputPng, sharp.buffer.png, width, height, {sequentialRead: true}, function(err, buffer) {
if (err) {
throw err;
} else {
assert.notStrictEqual(null, buffer);
deferred.resolve();
}
});
}
}).on("cycle", function(event) {
console.log(" png " + String(event.target));
}).on("complete", function() {
callback(null, this.filter("fastest").pluck("name"));
}).run();
} }
var diff = process.hrtime(time); }, function(err, results) {
imagemagickTime = diff[0] * 1e9 + diff[1]; assert(!err, err);
console.log("imagemagick took %d nanoseconds", imagemagickTime); Object.keys(results).forEach(function(format) {
assert.strictEqual("sharp", results[format].toString().substr(0, 5), "sharp was slower than " + results[format] + " for " + format);
// sharp
time = process.hrtime();
sharp.crop(input, output, width, height, function(err) {
if (err) {
throw err;
}
diff = process.hrtime(time);
var sharpTime = diff[0] * 1e9 + diff[1];
console.log("sharp took %d nanoseconds", sharpTime);
// diff
assert(sharpTime < imagemagickTime, "sharp was blunt");
console.log("sharp was %d%% faster", (imagemagickTime - sharpTime) / imagemagickTime * 100);
}); });
}); });