diff --git a/README.md b/README.md index c9e3d937..ac7bef8d 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# sharp +# sharp _adj_ @@ -9,22 +9,17 @@ _adj_ 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 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 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. - -Performance is 12x-15x faster than ImageMagick and 4x-6x faster than GraphicsMagick, based mainly on the number of CPU cores available. +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 * Node.js v0.8+ -* node-gyp -* [libvips](https://github.com/jcupitt/libvips) v7.37+ +* [libvips](https://github.com/jcupitt/libvips) v7.38+ For the sharpest results, please compile libvips from source. -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. - ## Install npm install sharp @@ -33,12 +28,31 @@ If you prefer to run a stable, package-managed environment such as Ubuntu 12.04 var sharp = require("sharp"); -### crop(input, output, width, height, callback) +### 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 -sharp.crop("input.jpg", "output.jpg", 300, 200, function(err) { +sharp.resize("input.jpg", "output.jpg", 300, 200, function(err) { if (err) { throw err; } @@ -48,73 +62,43 @@ sharp.crop("input.jpg", "output.jpg", 300, 200, function(err) { ``` ```javascript -sharp.crop("input.jpg", sharp.buffer.jpeg, 300, 200, function(err, buffer) { +sharp.resize("input.jpg", sharp.buffer.jpeg, 300, 200, {progressive: true}, function(err, buffer) { if (err) { throw err; } - // buffer contains JPEG image data + // buffer contains progressive JPEG image data }); ``` ```javascript -sharp.crop("input.jpg", sharp.buffer.png, 300, 200, function(err, buffer) { +sharp.resize("input.jpg", sharp.buffer.png, 300, 200, {sharpen: true}, 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 -sharp.embedWhite("input.jpg", "output.jpg", 200, 300, function(err) { - if (err) { - throw err; - } - // output.jpg is a 200 pixels wide and 300 pixels high image - // containing a scaled version of input.png embedded on a white canvas + // buffer contains sharpened PNG image data (converted from JPEG) }); ``` ```javascript -sharp.embedWhite("input.jpg", sharp.buffer.jpeg, 200, 300, function(err, buffer) { +sharp.resize(buffer, "output.jpg", 200, 300, {canvas: sharp.canvas.embedWhite}, function(err) { if (err) { throw err; } - // buffer contains JPEG image data + // 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 }); ``` -### 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) { +sharp.resize("input.jpg", sharp.buffer.jpeg, 200, 300, {canvas: sharp.canvas.embedBlack}, function(err, buffer) { if (err) { throw err; } - // output.png is a 200 pixels wide and 300 pixels high image + // 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 }); ``` -### 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 @@ -129,20 +113,41 @@ Test environment: * libpng 1.6.6 * zlib1g 1.2.7 -#### JPEG +`-file-buffer` indicates read from file and write to buffer, `-buffer-file` indicates read from buffer and write to file etc. -* 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 72.01 ops/sec ±7.19% (74 runs sampled) -* sharp-buffer x 75.73 ops/sec ±0.44% (75 runs sampled) +`-sharpen`, `-progressive` etc. demonstrate the negative effect of options on performance. -#### PNG +### JPEG -* 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 43.80 ops/sec ±6.81% (75 runs sampled) -* sharp-buffer x 45.67 ops/sec ±0.41% (75 runs sampled) +* 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 diff --git a/binding.gyp b/binding.gyp index cf9dbde4..ae9abac6 100755 --- a/binding.gyp +++ b/binding.gyp @@ -11,7 +11,7 @@ '/usr/lib/glib-2.0/include', '/usr/lib/x86_64-linux-gnu/glib-2.0/include' ], - 'cflags': ['-fexceptions'], - 'cflags_cc': ['-fexceptions'] + 'cflags': ['-fexceptions', '-O3'], + 'cflags_cc': ['-fexceptions', '-O3'] }] } diff --git a/index.js b/index.js index 97bbcb28..c1c3aba6 100755 --- a/index.js +++ b/index.js @@ -5,14 +5,60 @@ module.exports.buffer = { png: "__png" }; -module.exports.crop = function(input, output, width, height, callback) { - sharp.resize(input, output, width, height, "c", callback); +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) { - 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) { - sharp.resize(input, output, width, height, "b", callback); + sharp.resize(input, output, width, height, {canvas: "b", sharpen: true}, callback); }; diff --git a/package.json b/package.json index c4926b7e..b82f6e84 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharp", - "version": "0.0.9", + "version": "0.1.0", "author": "Lovell Fuller", "description": "High performance module to resize JPEG and PNG images using the libvips image processing library", "scripts": { diff --git a/src/sharp.cc b/src/sharp.cc index b031ac40..9259a482 100755 --- a/src/sharp.cc +++ b/src/sharp.cc @@ -2,58 +2,93 @@ #include #include #include +#include #include using namespace v8; using namespace node; -struct ResizeBaton { - std::string src; - std::string dst; +struct resize_baton { + std::string file_in; + void* buffer_in; + size_t buffer_in_len; + std::string file_out; void* buffer_out; size_t buffer_out_len; - int cols; - int rows; + int width; + int height; bool crop; int embed; + bool sharpen; + bool progessive; + VipsAccess access_method; std::string err; Persistent callback; - ResizeBaton() : buffer_out_len(0) {} + resize_baton(): buffer_in_len(0), buffer_out_len(0) {} }; -bool EndsWith(std::string const &str, std::string const &end) { +typedef enum { + JPEG, + PNG +} ImageType; + +unsigned char MARKER_JPEG[] = {0xff, 0xd8}; +unsigned char MARKER_PNG[] = {0x89, 0x50}; + +bool ends_with(std::string const &str, std::string const &end) { return str.length() >= end.length() && 0 == str.compare(str.length() - end.length(), end.length(), end); } -bool IsJpeg(std::string const &str) { - return EndsWith(str, ".jpg") || EndsWith(str, ".jpeg"); +bool is_jpeg(std::string const &str) { + return ends_with(str, ".jpg") || ends_with(str, ".jpeg"); } -bool IsPng(std::string const &str) { - return EndsWith(str, ".png"); +bool is_png(std::string const &str) { + return ends_with(str, ".png"); } -void ResizeAsync(uv_work_t *work) { - ResizeBaton* baton = static_cast(work->data); +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(work->data); + + ImageType inputImageType = JPEG; VipsImage *in = vips_image_new(); - if (IsJpeg(baton->src)) { - vips_jpegload((baton->src).c_str(), &in, NULL); - } else if (IsPng(baton->src)) { - vips_pngload((baton->src).c_str(), &in, NULL); + 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 { - (baton->err).append("Unsupported input file type"); - return; - } - if (in == NULL) { - (baton->err).append(vips_error_buffer()); - vips_error_clear(); + resize_error(baton, in); + (baton->err).append("Unsupported input " + baton->file_in); return; } - double xfactor = static_cast(in->Xsize) / std::max(baton->cols, 1); - double yfactor = static_cast(in->Ysize) / std::max(baton->rows, 1); + double xfactor = static_cast(in->Xsize) / std::max(baton->width, 1); + double yfactor = static_cast(in->Ysize) / std::max(baton->height, 1); double factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor); factor = std::max(factor, 1.0); int shrink = floor(factor); @@ -61,7 +96,7 @@ void ResizeAsync(uv_work_t *work) { // Try to use libjpeg shrink-on-load int shrink_on_load = 1; - if (IsJpeg(baton->src)) { + if (inputImageType == JPEG) { if (shrink >= 8) { residual = residual * shrink / 8; shrink_on_load = 8; @@ -76,129 +111,117 @@ void ResizeAsync(uv_work_t *work) { shrink = 1; } if (shrink_on_load > 1) { - if (vips_jpegload((baton->src).c_str(), &in, "shrink", shrink_on_load, NULL)) { - (baton->err).append(vips_error_buffer()); - vips_error_clear(); - g_object_unref(in); - return; + g_object_unref(in); + in = vips_image_new(); + if (baton->buffer_in_len > 1) { + if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "shrink", shrink_on_load, NULL)) { + return resize_error(baton, in); + } + } else { + if (vips_jpegload((baton->file_in).c_str(), &in, "shrink", shrink_on_load, NULL)) { + return resize_error(baton, in); + } } } } - VipsImage* img = in; - VipsImage* t[4]; - if (im_open_local_array(img, t, 4, "temp", "p")) { - (baton->err).append(vips_error_buffer()); - vips_error_clear(); - g_object_unref(in); - return; - } - + VipsImage *shrunk = vips_image_new(); if (shrink > 1) { // Use vips_shrink with the integral reduction - if (vips_shrink(img, &t[0], shrink, shrink, NULL)) { - (baton->err).append(vips_error_buffer()); - vips_error_clear(); - g_object_unref(in); - return; + if (vips_shrink(in, &shrunk, shrink, shrink, NULL)) { + return resize_error(baton, in); } } else { - t[0] = img; - } - - // Use vips_affine with the remaining float part using bilinear interpolation - if (vips_affine(t[0], &t[1], residual, 0, 0, residual, "interpolate", vips_interpolate_bilinear_static(), NULL)) { - (baton->err).append(vips_error_buffer()); - vips_error_clear(); - g_object_unref(in); - return; - } - img = t[1]; - - if (baton->crop) { - int width = std::min(img->Xsize, baton->cols); - int height = std::min(img->Ysize, baton->rows); - int left = (img->Xsize - width + 1) / 2; - int top = (img->Ysize - height + 1) / 2; - if (im_extract_area(img, t[2], left, top, width, height)) { - (baton->err).append(vips_error_buffer()); - vips_error_clear(); - g_object_unref(in); - return; - } - img = t[2]; - } else { - int left = (baton->cols - img->Xsize) / 2; - 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(); - g_object_unref(in); - return; - } - img = t[2]; - } - - // Mild sharpen - INTMASK* sharpen = im_create_imaskv("sharpen", 3, 3, - -1, -1, -1, - -1, 32, -1, - -1, -1, -1); - sharpen->scale = 24; - if (im_conv(img, t[3], sharpen)) { - (baton->err).append(vips_error_buffer()); - vips_error_clear(); - g_object_unref(in); - return; - } - img = t[3]; - - 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(); - } - } 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(); - } - } 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()); - 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_copy(in, &shrunk, NULL); } 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 + VipsImage *sharpened = vips_image_new(); + if (baton->sharpen) { + INTMASK* sharpen = im_create_imaskv("sharpen", 3, 3, + -1, -1, -1, + -1, 32, -1, + -1, -1, -1); + sharpen->scale = 24; + if (im_conv(canvased, sharpened, sharpen)) { + return resize_error(baton, canvased); + } + } else { + vips_copy(canvased, &sharpened, NULL); + } + g_object_unref(canvased); + + if (baton->file_out == "__jpeg") { + // Write JPEG to buffer + 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; - ResizeBaton *baton = static_cast(work->data); + resize_baton *baton = static_cast(work->data); - Local null = Local::New(Null()); - Local argv[2] = {null, null}; + Handle argv[2] = { Null(), Null() }; if (!baton->err.empty()) { // Error argv[0] = String::New(baton->err.data(), baton->err.size()); } else if (baton->buffer_out_len > 0) { // Buffer - Buffer *buffer = Buffer::New((const char*)(baton->buffer_out), baton->buffer_out_len); - argv[1] = Local::New(buffer->handle_); - vips_free(baton->buffer_out); + Buffer *slowBuffer = Buffer::New(baton->buffer_out_len); + memcpy(Buffer::Data(slowBuffer), baton->buffer_out, baton->buffer_out_len); + Local globalObj = Context::GetCurrent()->Global(); + Local bufferConstructor = Local::Cast(globalObj->Get(String::New("Buffer"))); + Handle 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(), 2, argv); @@ -207,15 +230,20 @@ void ResizeAsyncAfter(uv_work_t *work, int status) { delete work; } -Handle Resize(const Arguments& args) { +Handle resize(const Arguments& args) { HandleScope scope; - ResizeBaton *baton = new ResizeBaton; - baton->src = *String::Utf8Value(args[0]->ToString()); - baton->dst = *String::Utf8Value(args[1]->ToString()); - baton->cols = args[2]->Int32Value(); - baton->rows = args[3]->Int32Value(); - Local canvas = args[4]->ToString(); + resize_baton *baton = new resize_baton; + baton->file_in = *String::Utf8Value(args[0]->ToString()); + if (args[1]->IsObject()) { + Local buffer = args[1]->ToObject(); + baton->buffer_in = Buffer::Data(buffer); + 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 canvas = args[5]->ToString(); if (canvas->Equals(String::NewSymbol("c"))) { baton->crop = true; } else if (canvas->Equals(String::NewSymbol("w"))) { @@ -225,12 +253,15 @@ Handle Resize(const Arguments& args) { baton->crop = false; baton->embed = 0; } - baton->callback = Persistent::New(Local::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::New(Local::Cast(args[9])); uv_work_t *work = new uv_work_t; work->data = baton; - uv_queue_work(uv_default_loop(), work, ResizeAsync, (uv_after_work_cb)ResizeAsyncAfter); - return Undefined(); + uv_queue_work(uv_default_loop(), work, resize_async, (uv_after_work_cb)resize_async_after); + return scope.Close(Undefined()); } static void at_exit(void* arg) { @@ -242,7 +273,7 @@ extern "C" void init(Handle target) { HandleScope scope; vips_init(""); AtExit(at_exit); - NODE_SET_METHOD(target, "resize", Resize); + NODE_SET_METHOD(target, "resize", resize); }; NODE_MODULE(sharp, init); diff --git a/tests/perf.js b/tests/perf.js index 01851372..c54115d6 100755 --- a/tests/perf.js +++ b/tests/perf.js @@ -1,4 +1,5 @@ var sharp = require("../index"); +var fs = require("fs"); var imagemagick = require("imagemagick"); var gm = require("gm"); var epeg = require("epeg"); @@ -17,6 +18,7 @@ var height = 480; async.series({ jpeg: function(callback) { + var inputJpgBuffer = fs.readFileSync(inputJpg); (new Benchmark.Suite("jpeg")).add("imagemagick", { defer: true, fn: function(deferred) { @@ -34,7 +36,7 @@ async.series({ } }); } - }).add("gm", { + }).add("gm-file-file", { defer: true, fn: function(deferred) { gm(inputJpg).crop(width, height).quality(80).write(outputJpg, function (err) { @@ -45,17 +47,37 @@ async.series({ } }); } - }).add("epeg", { + }).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("sharp-file", { + }).add("epeg-file-buffer", { defer: true, fn: function(deferred) { - sharp.crop(inputJpg, outputJpg, width, height, function(err) { + 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 { @@ -63,10 +85,69 @@ async.series({ } }); } - }).add("sharp-buffer", { + }).add("sharp-buffer-buffer", { defer: true, fn: function(deferred) { - sharp.crop(inputJpg, sharp.buffer.jpeg, width, height, function(err, buffer) { + 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 { @@ -82,6 +163,7 @@ async.series({ }).run(); }, png: function(callback) { + var inputPngBuffer = fs.readFileSync(inputPng); (new Benchmark.Suite("png")).add("imagemagick", { defer: true, fn: function(deferred) { @@ -98,7 +180,7 @@ async.series({ } }); } - }).add("gm", { + }).add("gm-file-file", { defer: true, fn: function(deferred) { gm(inputPng).crop(width, height).write(outputPng, function (err) { @@ -109,10 +191,22 @@ async.series({ } }); } - }).add("sharp-file", { + }).add("gm-file-buffer", { defer: true, fn: function(deferred) { - sharp.crop(inputPng, outputPng, width, height, function(err) { + 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 { @@ -120,10 +214,69 @@ async.series({ } }); } - }).add("sharp-buffer", { + }).add("sharp-buffer-buffer", { defer: true, fn: function(deferred) { - sharp.crop(inputPng, sharp.buffer.png, width, height, function(err, buffer) { + 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 {