diff --git a/.gitignore b/.gitignore index f356293e..2bc8b9f7 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,6 @@ lib-cov pids logs results +build npm-debug.log diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..ef8c916a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: node_js +node_js: + - "0.11" + - "0.10" + - "0.8" +before_install: + - sudo apt-get update -qq + - sudo apt-get install -qq libvips-dev + - sudo ln -s /usr/lib/pkgconfig/vips-7.26.pc /usr/lib/pkgconfig/vips.pc \ No newline at end of file diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 3105fc79..221a5a0b --- a/README.md +++ b/README.md @@ -1,4 +1,46 @@ -sharp -===== +# sharp (_adj_) -High performance Node.js module to resize JPEG images using the libvips image processing library +1. clearly defined; distinct: a sharp photographic image. +2. quick, brisk, or spirited. +3. shrewd or astute: a sharp bargainer. +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. + +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 the University of Southampton. + +## Prerequisites + +Requires node-gyp and libvips-dev to build. + + sudo npm install -g node-gyp + 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 + + npm install sharp + +## Usage + +```javascript +var sharp = require("sharp"); +var cropLandscape = sharp.resize("input.jpg", "output.jpg", 300, 200, "c"); +var embedPortraitWhiteBorder = sharp.resize("input.jpg", "output.jpg", 200, 300, "w"); +var embedPortraitBlackBorder = sharp.resize("input.jpg", "output.jpg", 200, 300, "b"); +``` + +## Testing [![Build Status](https://travis-ci.org/lovell/sharp.png?branch=master)](https://travis-ci.org/lovell/sharp) + + npm test diff --git a/binding.gyp b/binding.gyp new file mode 100755 index 00000000..a7109665 --- /dev/null +++ b/binding.gyp @@ -0,0 +1,14 @@ +{ + 'targets': [{ + 'target_name': 'sharp', + 'sources': ['src/sharp.cc'], + 'libraries': [' +#include +#include +#include +#include + +using namespace v8; + +// Free VipsImage children when object goes out of scope +// Thanks due to https://github.com/dosx/node-vips +class ImageFreer { + public: + ImageFreer() {} + ~ImageFreer() { + for (uint16_t i = 0; i < v_.size(); i++) { + if (v_[i] != NULL) { + g_object_unref(v_[i]); + } + } + v_.clear(); + } + void add(VipsImage* i) { v_.push_back(i); } + private: + std::vector v_; +}; + +struct ResizeBaton { + std::string src; + std::string dst; + int cols; + int rows; + bool crop; + int embed; + std::string err; + Persistent callback; +}; + +void ResizeAsync(uv_work_t *work) { + ResizeBaton* baton = static_cast(work->data); + + VipsImage *in = vips_image_new_mode((baton->src).c_str(), "p"); + im_jpeg2vips((baton->src).c_str(), in); + if (in == NULL) { + (baton->err).append(vips_error_buffer()); + vips_error_clear(); + return; + } + ImageFreer freer; + freer.add(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(); + return; + } + + double xfactor = static_cast(img->Xsize) / std::max(baton->cols, 1); + double yfactor = static_cast(img->Ysize) / std::max(baton->rows, 1); + double factor = baton->crop ? std::min(xfactor, yfactor) : std::max(xfactor, yfactor); + factor = std::max(factor, 1.0); + int shrink = floor(factor); + double residual = shrink / factor; + + // Use im_shrink with the integral reduction + if (im_shrink(img, t[0], shrink, shrink)) { + (baton->err).append(vips_error_buffer()); + vips_error_clear(); + return; + } + + // Use im_affinei with the remaining float part using bilinear interpolation + if (im_affinei_all(t[0], t[1], vips_interpolate_bilinear_static(), residual, 0, 0, residual, 0, 0)) { + (baton->err).append(vips_error_buffer()); + vips_error_clear(); + 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(); + 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(); + 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(); + return; + } + img = t[3]; + + if (im_vips2jpeg(img, baton->dst.c_str())) { + (baton->err).append(vips_error_buffer()); + vips_error_clear(); + } +} + +void ResizeAsyncAfter(uv_work_t *work, int status) { + HandleScope scope; + + ResizeBaton *baton = static_cast(work->data); + + Local argv[1]; + if (!baton->err.empty()) { + argv[0] = String::New(baton->err.data(), baton->err.size()); + } else { + argv[0] = Local::New(Null()); + } + + baton->callback->Call(Context::GetCurrent()->Global(), 1, argv); + baton->callback.Dispose(); + delete baton; + delete work; +} + +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(); + if (canvas->Equals(String::NewSymbol("c"))) { + baton->crop = true; + } else if (canvas->Equals(String::NewSymbol("w"))) { + baton->crop = false; + baton->embed = 4; + } else if (canvas->Equals(String::NewSymbol("b"))) { + baton->crop = false; + baton->embed = 0; + } + baton->callback = Persistent::New(Local::Cast(args[5])); + + 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(); +} + +extern "C" void init(Handle target) { + HandleScope scope; + vips_init(""); + NODE_SET_METHOD(target, "resize", Resize); +}; + +NODE_MODULE(vips, init)