├── .gitignore ├── Makefile ├── README ├── binding.gyp ├── index.js ├── package.json ├── src ├── myconvert.cc ├── node-vips.cc ├── transform.cc └── transform.h └── test ├── bogus.jpg ├── bogus_value_for_orientation_tag.jpg ├── input.jpg ├── input2.jpg └── simple_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .lock-wscript 2 | node_modules 3 | build 4 | .*.swp 5 | test/output*.jpg 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test/*.js 2 | 3 | all: test 4 | 5 | build: configure compile 6 | 7 | configure: 8 | node-gyp configure 9 | 10 | compile: 11 | node-gyp build 12 | 13 | node_modules/.bin/nodeunit: 14 | # Installing nodeunit under --dev is not good... 15 | npm install nodeunit 16 | npm install --dev 17 | 18 | test: build node_modules/.bin/nodeunit 19 | @./node_modules/.bin/nodeunit \ 20 | $(TESTS) 21 | 22 | clean: 23 | rm -f node-vips.node test/output*.jpg 24 | rm -rf build node_modules 25 | 26 | 27 | .PHONY: clean test build compile all configure 28 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | A node.js module that provides access to the VIPS library and Exiv2 in order 2 | to resize and rotate images. 3 | 4 | Updated from node-waf to node-gyp. 5 | 6 | Tested with VIPS 7.32.1. 7 | 8 | Homebrew users note: vips and exiv2 have moved to homebrew/science, to use run 9 | `brew tap homebrew/science` then run `brew install vips` and `brew install 10 | exiv2` as normal. 11 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [{ 3 | 'target_name': 'vips', 4 | 'sources': [ 5 | 'src/node-vips.cc', 6 | 'src/transform.cc' 7 | ], 8 | 'conditions': [ 9 | ['OS=="mac"', { 10 | 'libraries': [ 11 | '=0.7", 14 | "imagemagick": "*" 15 | }, 16 | "engines": { 17 | "node": ">=0.8" 18 | }, 19 | "author": "erlyco ", 20 | "main": "index", 21 | "scripts": { 22 | "test": "nodeunit test/*.js" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/myconvert.cc: -------------------------------------------------------------------------------- 1 | // Copyright Erly Inc 2011, All Rights Reserved 2 | // Author: Walt Lin 3 | // 4 | // To compile: g++ -o myconvert src/myconvert.cc src/transform.cc `pkg-config --cflags --libs exiv2 vips-7.26` 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "transform.h" 12 | 13 | void Usage() { 14 | printf("usage: myconvert input output command [options]\n" 15 | "command is one of:\n" 16 | " resize (options: width height crop_to_size auto_orient)\n" 17 | " rotate (options: degrees)\n" 18 | " autorotate ; rotates according to exif and strips exif\n"); 19 | } 20 | 21 | int main(int argc, char **argv) { 22 | InitTransform(argv[0]); 23 | 24 | if (argc < 4) { 25 | Usage(); 26 | return 1; 27 | } 28 | 29 | if (strcmp(argv[3], "resize") == 0) { 30 | if (argc != 8) { 31 | Usage(); 32 | return 1; 33 | } 34 | 35 | errno = 0; 36 | int width = strtol(argv[4], NULL, 10); 37 | int height = strtol(argv[5], NULL, 10); 38 | bool crop_to_size = strtol(argv[6], NULL, 10); 39 | bool auto_orient = strtol(argv[7], NULL, 10); 40 | if (errno != 0 || width <= 0 || height <= 0) { 41 | Usage(); 42 | return 1; 43 | } 44 | 45 | std::string err; 46 | if (DoTransform(width, height, crop_to_size, 0, auto_orient, 47 | argv[1], argv[2], NULL, NULL, &err)) { 48 | printf("resize failed: %s\n", err.c_str()); 49 | return 1; 50 | } 51 | } else if (strcmp(argv[3], "rotate") == 0) { 52 | if (argc != 5) { 53 | Usage(); 54 | return 1; 55 | } 56 | 57 | errno = 0; 58 | int degrees = strtol(argv[4], NULL, 10); 59 | if (degrees == 0 && errno != 0) { 60 | Usage(); 61 | return 1; 62 | } 63 | 64 | std::string err; 65 | if (DoTransform(-1, -1, false, degrees, false, 66 | argv[1], argv[2], NULL, NULL, &err)) { 67 | printf("rotate failed: %s\n", err.c_str()); 68 | return 1; 69 | } 70 | } else if (strcmp(argv[3], "autorotate") == 0) { 71 | if (argc != 4) { 72 | Usage(); 73 | return 1; 74 | } 75 | 76 | std::string err; 77 | if (DoTransform(-1, -1, false, 0, true /* auto-orient */, 78 | argv[1], argv[2], NULL, NULL, &err)) { 79 | printf("autorotate failed: %s\n", err.c_str()); 80 | return 1; 81 | } 82 | } else { 83 | Usage(); 84 | return 1; 85 | } 86 | 87 | return 0; 88 | } 89 | -------------------------------------------------------------------------------- /src/node-vips.cc: -------------------------------------------------------------------------------- 1 | // Copyright Erly Inc 2011, All Rights Reserved 2 | // Authors: Bo Wang, Walt Lin 3 | // 4 | // Node native extension to wrap the VIPS library for image manipulation. 5 | // 6 | // Javascript functions exported: 7 | // 8 | // resize(input_path, output_path, new_x, new_y, crop_to_size, 9 | // auto_orient, callback) 10 | // 11 | // rotate(input_path, output_path, degrees, callback) 12 | // 13 | // 'metadata' is an object with 'width' and 'height' properties. 14 | // The functions will reject images they cannot open. 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | 24 | #include "transform.h" 25 | 26 | using namespace v8; 27 | using namespace node; 28 | 29 | namespace { 30 | 31 | // Macros for checking arguments. 32 | 33 | #define REQ_FUN_ARG(I, VAR) \ 34 | if (args.Length() <= (I) || !args[I]->IsFunction()) \ 35 | return ThrowException(Exception::TypeError( \ 36 | String::New("Argument " #I " must be a function"))); \ 37 | Local VAR = Local::Cast(args[I]) 38 | #define REQ_NUM_ARG(I, VAR) \ 39 | if (args.Length() <= (I) || !args[I]->IsNumber()) \ 40 | return ThrowException(Exception::TypeError( \ 41 | String::New("Argument " #I " must be a number"))); \ 42 | int VAR = args[I]->Int32Value() 43 | #define REQ_STR_ARG(I, VAR) \ 44 | if (args.Length() <= (I) || !args[I]->IsString()) \ 45 | return ThrowException(Exception::TypeError( \ 46 | String::New("Argument " #I " must be a string"))); \ 47 | Local VAR = args[I]->ToString() 48 | #define REQ_BOOL_ARG(I, VAR) \ 49 | if (args.Length() <= (I) || !args[I]->IsBoolean()) \ 50 | return ThrowException(Exception::TypeError( \ 51 | String::New("Argument " #I " must be a boolean"))); \ 52 | bool VAR = args[I]->BooleanValue() 53 | 54 | // Data needed for a call to Transform. 55 | // If cols or rows is <= 0, no resizing is done. 56 | // rotate_degrees must be one of 0, 90, 180, or 270. 57 | struct TransformCall { 58 | int cols; // resize to this many columns 59 | int rows; // and this many rows 60 | bool crop_to_size; 61 | int rotate_degrees; // rotate image by this many degrees 62 | bool auto_orient; 63 | int new_width; 64 | int new_height; 65 | std::string src_path; 66 | std::string dst_path; 67 | std::string err_msg; 68 | Persistent cb; 69 | 70 | TransformCall() : 71 | cols(-1), rows(-1), crop_to_size(false), rotate_degrees(0), 72 | auto_orient(false), new_width(0), new_height(0) {} 73 | }; 74 | 75 | void EIO_Transform(uv_work_t *req) { 76 | TransformCall* t = static_cast(req->data); 77 | DoTransform(t->cols, t->rows, t->crop_to_size, t->rotate_degrees, 78 | t->auto_orient, t->src_path, t->dst_path, 79 | &t->new_width, &t->new_height, &t->err_msg); 80 | } 81 | 82 | // Done function that invokes a callback. 83 | void TransformDone(uv_work_t *req, int status) { 84 | HandleScope scope; 85 | TransformCall *c = static_cast(req->data); 86 | 87 | Local argv[2]; 88 | if (!c->err_msg.empty()) { // req->result is NOT set correctly 89 | // Set up an error object. 90 | argv[0] = String::New(c->err_msg.data(), c->err_msg.size()); 91 | argv[1] = Local::New(Null()); 92 | } else { 93 | Local metadata = Object::New(); 94 | metadata->Set(String::New("width"), Integer::New(c->new_width)); 95 | metadata->Set(String::New("height"), Integer::New(c->new_height)); 96 | argv[0] = Local::New(Null()); 97 | argv[1] = metadata; 98 | } 99 | 100 | TryCatch try_catch; 101 | c->cb->Call(Context::GetCurrent()->Global(), 2, argv); 102 | if (try_catch.HasCaught()) { 103 | FatalException(try_catch); 104 | } 105 | 106 | c->cb.Dispose(); 107 | delete c; 108 | delete req; 109 | } 110 | 111 | // ResizeAsync(input_path, output_path, new_x, new_y, auto_orient, callback) 112 | Handle ResizeAsync(const Arguments& args) { 113 | HandleScope scope; 114 | REQ_STR_ARG(0, input_path); 115 | REQ_STR_ARG(1, output_path); 116 | REQ_NUM_ARG(2, new_x_px); 117 | REQ_NUM_ARG(3, new_y_px); 118 | REQ_BOOL_ARG(4, crop_to_size); 119 | REQ_BOOL_ARG(5, auto_orient); 120 | REQ_FUN_ARG(6, cb); 121 | 122 | TransformCall *c = new TransformCall; 123 | c->cols = new_x_px; 124 | c->rows = new_y_px; 125 | c->crop_to_size = crop_to_size; 126 | c->auto_orient = auto_orient; 127 | c->src_path = *String::Utf8Value(input_path); 128 | c->dst_path = *String::Utf8Value(output_path); 129 | c->cb = Persistent::New(cb); 130 | 131 | uv_work_t *req = new uv_work_t; 132 | req->data = c; 133 | uv_queue_work(uv_default_loop(), req, EIO_Transform, (uv_after_work_cb)TransformDone); 134 | return Undefined(); 135 | } 136 | 137 | // RotateAsync(input_path, output_path, degrees, callback) 138 | Handle RotateAsync(const Arguments& args) { 139 | HandleScope scope; 140 | REQ_STR_ARG(0, input_path); 141 | REQ_STR_ARG(1, output_path); 142 | REQ_NUM_ARG(2, degrees); 143 | REQ_FUN_ARG(3, cb); 144 | 145 | TransformCall *c = new TransformCall; 146 | c->rotate_degrees = degrees; 147 | c->src_path = *String::Utf8Value(input_path); 148 | c->dst_path = *String::Utf8Value(output_path); 149 | c->cb = Persistent::New(cb); 150 | 151 | uv_work_t *req = new uv_work_t; 152 | req->data = c; 153 | uv_queue_work(uv_default_loop(), req, EIO_Transform, (uv_after_work_cb)TransformDone); 154 | return Undefined(); 155 | } 156 | 157 | // Data needed for a call to CreatePixel. 158 | struct CreatePixelCall { 159 | unsigned char red; 160 | unsigned char green; 161 | unsigned char blue; 162 | unsigned char alpha; 163 | char *pixelData; 164 | size_t pixelLen; 165 | std::string err_msg; 166 | Persistent cb; 167 | 168 | CreatePixelCall() : 169 | red(0), green(0), blue(0), alpha(255) {} 170 | }; 171 | 172 | void EIO_CreatePixel(uv_work_t *req) { 173 | CreatePixelCall* cp = static_cast(req->data); 174 | PNGPixel(cp->red, cp->green, cp->blue, cp->alpha, &cp->pixelData, 175 | &cp->pixelLen, &cp->err_msg); 176 | 177 | } 178 | 179 | // Done function that invokes a callback. 180 | void CreateDone(uv_work_t *req, int status) { 181 | HandleScope scope; 182 | CreatePixelCall *c = static_cast(req->data); 183 | 184 | Local argv[2]; 185 | if (!c->err_msg.empty()) { // req->result is NOT set correctly 186 | // Set up an error object. 187 | argv[0] = String::New(c->err_msg.data(), c->err_msg.size()); 188 | argv[1] = Local::New(Null()); 189 | } else { 190 | // TODO(nick): use fast buffers 191 | Buffer *slowBuffer = Buffer::New(c->pixelData, c->pixelLen); 192 | argv[0] = Local::New(Null()); 193 | argv[1] = Local::New(slowBuffer->handle_); 194 | free(c->pixelData); 195 | } 196 | 197 | TryCatch try_catch; 198 | c->cb->Call(Context::GetCurrent()->Global(), 2, argv); 199 | if (try_catch.HasCaught()) { 200 | FatalException(try_catch); 201 | } 202 | 203 | c->cb.Dispose(); 204 | delete c; 205 | delete req; 206 | } 207 | 208 | Handle PngPixelAsync(const Arguments& args) { 209 | HandleScope scope; 210 | REQ_NUM_ARG(0, red); 211 | REQ_NUM_ARG(1, green); 212 | REQ_NUM_ARG(2, blue); 213 | REQ_NUM_ARG(3, alpha); 214 | REQ_FUN_ARG(4, cb); 215 | // REQ_NUM_MAX(red, 255); 216 | // REQ_NUM_MAX(green, 255); 217 | // REQ_NUM_MAX(blue, 255); 218 | // REQ_NUM_MAX(alpha, 255); 219 | 220 | CreatePixelCall *c = new CreatePixelCall; 221 | c->red = red; 222 | c->green = green; 223 | c->blue = blue; 224 | c->alpha = alpha; 225 | c->cb = Persistent::New(cb); 226 | 227 | uv_work_t *req = new uv_work_t; 228 | req->data = c; 229 | uv_queue_work(uv_default_loop(), req, EIO_CreatePixel, (uv_after_work_cb)CreateDone); 230 | return Undefined(); 231 | } 232 | 233 | } // anonymous namespace 234 | 235 | extern "C" void init(Handle target) { 236 | InitTransform("node-vips.cc" /* don't have argv[0] */); 237 | HandleScope scope; 238 | NODE_SET_METHOD(target, "resize", ResizeAsync); 239 | NODE_SET_METHOD(target, "rotate", RotateAsync); 240 | NODE_SET_METHOD(target, "createPNGPixel", PngPixelAsync); 241 | }; 242 | 243 | NODE_MODULE(vips, init) 244 | -------------------------------------------------------------------------------- /src/transform.cc: -------------------------------------------------------------------------------- 1 | // Copyright Erly Inc 2011, All Rights Reserved 2 | // Authors: Walt Lin, Bo Wang 3 | // 4 | // Loosely based on vipsthumbnail.c 5 | // 6 | // Note on EXIF data: 7 | // There are a number of tags that control how the image should be displayed, 8 | // among them Exif.Image.Orientation, Exif.Thumbnail.Orientation, and vendor 9 | // specific tags like Exif.Panasonic.Rotation. We only read and write the 10 | // Exif.Image.Orientation tag, which seems to be the most well supported and 11 | // generally the correct thing to do. Since we never use the thumbnails 12 | // embedded in the image we could potentially strip them out if we wanted to 13 | // save space. Notably we do not call Exiv2::orientation which looks at all 14 | // the vendor specific tags, since it may be possible that some library 15 | // rotated the image and stripped Exif.Image.Orientation but left 16 | // Exif.Panasonic.Rotation. Originals taken by panasonics tend to have all 17 | // three of those tags set. 18 | // 19 | // We have also seen some photos on Flickr that have Exif.Thumbnail.Orientation 20 | // and Exif.Panasonic.Rotation but no Exif.Image.Orientation, for example 21 | // http://www.flickr.com/photos/andrewlin12/3717390632/in/set-72157621264148187/ . 22 | // We could potentially try to fix these up by stripping the other tags but 23 | // that's not necessary to get them to display correctly in the browser. 24 | // 25 | // To compile a test program on linux that uses this library: 26 | // g++ -o myconvert src/myconvert.cc src/transform.cc `pkg-config --cflags --libs vips-7.26` `pkg-config --cflags --libs exiv2` 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | 39 | #include 40 | #include 41 | 42 | #include "transform.h" 43 | 44 | #define DEBUG 0 45 | 46 | using std::string; 47 | 48 | static const char kJpegCompressionFactor[] = ":92"; 49 | 50 | static const char kOrientationTag[] = "Exif.Image.Orientation"; 51 | 52 | string SimpleItoa(int x) { 53 | char buf[16]; 54 | snprintf(buf, sizeof(buf), "%d", x); 55 | return string(buf); 56 | } 57 | 58 | // Free VipsImages when this object goes out of scope. 59 | class ImageFreer { 60 | public: 61 | ImageFreer() {} 62 | 63 | ~ImageFreer() { 64 | for (uint16_t i = 0; i < v_.size(); i++) { 65 | if (v_[i] != NULL) { 66 | g_object_unref(v_[i]); 67 | } 68 | } 69 | v_.clear(); 70 | } 71 | 72 | void add(VipsImage* i) { v_.push_back(i); } 73 | 74 | private: 75 | std::vector v_; 76 | }; 77 | 78 | // Set an error message from the vips buffer, and clear it. 79 | static void SetFromVipsError(string* out, const char* msg) { 80 | out->assign(msg); 81 | out->append(": "); 82 | out->append(vips_error_buffer()); 83 | vips_error_clear(); 84 | } 85 | 86 | // Read EXIF data for image in 'path' and return the rotation needed to turn 87 | // it right side up. Return < 0 upon error, and fill in 'err'. 88 | static int GetEXIFRotationNeeded(const string& path, string* err) { 89 | int orientation = 0; 90 | try { 91 | Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(path); 92 | assert(image.get() != 0); 93 | image->readMetadata(); 94 | Exiv2::ExifData &ed = image->exifData(); 95 | Exiv2::ExifData::const_iterator it = 96 | ed.findKey(Exiv2::ExifKey(kOrientationTag)); 97 | if (it != ed.end() && it->count() == 1) { 98 | orientation = it->toLong(); 99 | } else if (it != ed.end()) { 100 | fprintf(stderr, "bogus orientation tag count %ld for %s\n", 101 | it->count(), path.c_str()); 102 | return 0; 103 | } else { 104 | return 0; 105 | } 106 | } catch (Exiv2::Error& e) { 107 | err->assign("exiv2 error"); 108 | return -1; 109 | } 110 | 111 | // We only expect values of 1, 3, 6, 8, see 112 | // http://www.impulseadventure.com/photo/exif-orientation.html 113 | switch (orientation) { 114 | case 1: return 0; 115 | case 3: return 180; 116 | case 6: return 90; 117 | case 8: return 270; 118 | default: 119 | // Don't error out on bogus values, just assume no rotation needed. 120 | if (DEBUG) { 121 | fprintf(stderr, "unexpected orientation value %d for %s\n", 122 | orientation, path.c_str()); 123 | } 124 | return 0; 125 | } 126 | } 127 | 128 | // Write a new value to the EXIF orientation tag for the image in 'path'. 129 | // Return 0 on success. 130 | static int WriteEXIFOrientation(const string& path, uint16_t orientation) { 131 | try { 132 | Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(path); 133 | assert(image.get() != 0); 134 | image->readMetadata(); 135 | image->exifData()[kOrientationTag] = orientation; 136 | image->writeMetadata(); 137 | return 0; 138 | } catch (Exiv2::Error& e) { 139 | //printf("exiv2 error writing orientation: %s\n", e.what()); 140 | return -1; 141 | } 142 | } 143 | 144 | // Calculate shrink factors: return an integer factor to shrink by ( >= 1 ), 145 | // as well as a residual [0,1], so we can shrink in two stages. 146 | static int CalculateShrink(int width, int height, 147 | int new_width, int new_height, 148 | bool crop, double *residual) { 149 | double xf = static_cast(width) / std::max(new_width, 1); 150 | double yf = static_cast(height) / std::max(new_height, 1); 151 | double factor = crop ? std::min(xf, yf) : std::max(xf, yf); 152 | factor = std::max(factor, 1.0); 153 | int shrink = floor(factor); 154 | if (residual != NULL) { 155 | *residual = shrink / factor; 156 | } 157 | return shrink; 158 | } 159 | 160 | // Resize the image, maintaining aspect ratio. If 'crop' is true, the 161 | // image will be scaled down until one dimension reaches the box and 162 | // then the image will be cropped to reach the exact dimensions 163 | // (keeping it centered); otherwise, the image will be scaled until it 164 | // fits inside the requested box. Allocate a new VipsImage and return 165 | // it if successful; it will be local to 'in'. 166 | // 167 | // TODO(walt): add sharpening? 168 | static VipsImage* ResizeAndCrop(VipsImage* in, int new_x, int new_y, 169 | bool crop) { 170 | VipsImage* x = in; 171 | VipsImage* t[4]; 172 | if (im_open_local_array(in, t, 4, "scratch", "p")) { 173 | return NULL; 174 | } 175 | 176 | double residual; 177 | int shrink = CalculateShrink(x->Xsize, x->Ysize, new_x, new_y, 178 | crop, &residual); 179 | 180 | if (DEBUG) { 181 | fprintf(stderr, "resizing image from %dx%d to %dx%d, " 182 | "crop=%d, shrink=%d, residual scale=%f\n", 183 | x->Xsize, x->Ysize, new_x, new_y, crop, shrink, residual); 184 | } 185 | 186 | // First, shrink an integral amount with im_shrink. Then, do the leftover 187 | // part with im_affinei using bilinear interpolation. 188 | VipsInterpolate* interp = vips_interpolate_bilinear_static(); 189 | if (im_shrink(x, t[0], shrink, shrink) || 190 | im_affinei_all(t[0], t[1], interp, residual, 0, 0, residual, 0, 0)) { 191 | return NULL; 192 | } 193 | x = t[1]; 194 | 195 | if (crop) { 196 | int width = std::min(x->Xsize, new_x); 197 | int height = std::min(x->Ysize, new_y); 198 | int left = (x->Xsize - width + 1) / 2; 199 | int top = (x->Ysize - height + 1) / 2; 200 | if (DEBUG) { 201 | fprintf(stderr, "cropping from %dx%d to %dx%d\n", 202 | x->Xsize, x->Ysize, new_x, new_y); 203 | } 204 | if (im_extract_area(x, t[2], left, top, width, height)) { 205 | return NULL; 206 | } 207 | x = t[2]; 208 | } 209 | 210 | return x; 211 | } 212 | 213 | // Rotate the image, returning a new VipsImage that is allocated local 214 | // to 'in'. Return NULL if there is an error. 215 | VipsImage* Rotate(VipsImage* in, int degrees) { 216 | VipsImage* tmp = vips_image_new(); 217 | if (tmp == NULL) { 218 | return NULL; 219 | } 220 | vips_object_local(in, tmp); 221 | 222 | int r = 0; 223 | switch (degrees) { 224 | case 0: return in; 225 | case 90: r = im_rot90(in, tmp); break; 226 | case 180: r = im_rot180(in, tmp); break; 227 | case 270: r = im_rot270(in, tmp); break; 228 | default: return NULL; 229 | } 230 | return r ? NULL : tmp; 231 | } 232 | 233 | int DoTransform(int cols, int rows, bool crop_to_size, 234 | int rotate_degrees, bool auto_orient, 235 | const string& src_path, const string& dst_path, 236 | int* new_width, int* new_height, string* err_msg) { 237 | ImageFreer freer; 238 | 239 | if (src_path == dst_path) { 240 | err_msg->assign("dest path cannot be same as source path"); 241 | return -1; 242 | } 243 | 244 | if (auto_orient && rotate_degrees != 0) { 245 | err_msg->assign("can't rotate and auto-orient"); 246 | return -1; 247 | } 248 | 249 | VipsFormatClass* format = vips_format_for_file(src_path.c_str()); 250 | if (format == NULL) { 251 | SetFromVipsError(err_msg, "could not open file"); 252 | return -1; 253 | } 254 | 255 | string imgformat = VIPS_OBJECT_CLASS(format)->nickname; 256 | if (imgformat != "jpeg" && imgformat != "png" && imgformat != "gif") { 257 | err_msg->assign("unsupported image format "); 258 | err_msg->append(imgformat); 259 | return -1; 260 | } 261 | 262 | // If auto-orienting, find how much we need to rotate. 263 | if (auto_orient) { 264 | int r = GetEXIFRotationNeeded(src_path.c_str(), err_msg); 265 | if (r < 0) { 266 | return -1; 267 | } 268 | rotate_degrees = r; 269 | } 270 | 271 | if (rotate_degrees != 0 && rotate_degrees != 90 && 272 | rotate_degrees != 180 && rotate_degrees != 270) { 273 | err_msg->assign("illegal rotate_degrees"); 274 | return -1; 275 | } 276 | 277 | // Open the input and output images. 278 | VipsImage *in, *out; 279 | { 280 | in = vips_image_new_mode(src_path.c_str(), "rd"); 281 | if (in == NULL) { 282 | SetFromVipsError(err_msg, "could not open input"); 283 | return -1; 284 | } 285 | freer.add(in); 286 | 287 | out = vips_image_new_mode(dst_path.c_str(), "w"); 288 | if (out == NULL) { 289 | SetFromVipsError(err_msg, "could not open output"); 290 | return -1; 291 | } 292 | 293 | vips_object_local(in, out); 294 | } 295 | 296 | // Resize and/or crop. 297 | VipsImage* img = in; 298 | if (cols > 0 && rows > 0) { 299 | img = ResizeAndCrop(img, cols, rows, crop_to_size); 300 | if (img == NULL) { 301 | SetFromVipsError(err_msg, "resize and crop failed"); 302 | return -1; 303 | } 304 | } 305 | 306 | // Rotate. 307 | if (rotate_degrees > 0) { 308 | img = Rotate(img, rotate_degrees); 309 | if (img == NULL) { 310 | SetFromVipsError(err_msg, "rotate failed"); 311 | return -1; 312 | } 313 | } 314 | 315 | // Write the image. 316 | if (im_copy(img, out)) { 317 | err_msg->assign("copy failed"); 318 | return -1; 319 | } 320 | 321 | // Write new EXIF orientation. 322 | if (auto_orient && rotate_degrees > 0) { 323 | if (WriteEXIFOrientation(dst_path, 1 /* orientation */) < 0) { 324 | err_msg->assign("failed to write new EXIF orientation"); 325 | return -1; 326 | } 327 | } 328 | 329 | if (new_width != NULL) *new_width = img->Xsize; 330 | if (new_height != NULL) *new_height = img->Ysize; 331 | 332 | return 0; 333 | } 334 | 335 | void InitTransform(const char* argv0) { 336 | assert(vips_init(argv0) == 0); 337 | 338 | // Need to initialize XmpParser before any threads. 339 | // TODO(walt): when we switch to a newer version of libexiv2, provide a mutex. 340 | Exiv2::XmpParser::initialize(); 341 | } 342 | 343 | int PNGPixel(unsigned char red, unsigned char green, unsigned char blue, 344 | unsigned char alpha, char** pixelData, size_t* pixelLen, string* err_msg) { 345 | void* buf = vips_malloc(NULL, 1000); 346 | if (buf == NULL) { 347 | err_msg->assign("can't allocate 1k bytes for tmp image"); 348 | return -1; 349 | } 350 | 351 | VipsImage* tmp = vips_image_new_from_memory(buf, 1, 1, 4, VIPS_FORMAT_UCHAR); 352 | if (tmp == NULL) { 353 | err_msg->assign("can't create a vips image"); 354 | vips_free(buf); 355 | return -1; 356 | } 357 | PEL RGBA[4]; 358 | RGBA[0] = red; 359 | RGBA[1] = green; 360 | RGBA[2] = blue; 361 | RGBA[3] = alpha; 362 | int result = im_draw_flood(tmp, 0, 0, RGBA, NULL); 363 | if (result != 0) { 364 | err_msg->assign("draw_rect failed."); 365 | vips_free(buf); 366 | return -1; 367 | } 368 | tmp->Xres = 2.835017718860743; 369 | tmp->Yres = 2.835017718860743; 370 | result = im_vips2bufpng(tmp, NULL, 6, 0, pixelData, pixelLen); 371 | if (result != 0) { 372 | err_msg->assign("cannot write as png"); 373 | vips_free(buf); 374 | return -1; 375 | } 376 | vips_free(buf); 377 | 378 | return 0; 379 | } 380 | -------------------------------------------------------------------------------- /src/transform.h: -------------------------------------------------------------------------------- 1 | // Copyright Erly Inc 2011, All Rights Reserved 2 | // Authors: Walt Lin, Bo Wang 3 | 4 | #ifndef NODE_VIPS_TRANSFORM_H__ 5 | #define NODE_VIPS_TRANSFORM_H__ 6 | 7 | #include 8 | 9 | // Transform: resize and/or rotate an image. 10 | // 11 | // If 'cols' or 'rows' is < 0, no resizing is done. Otherwise, the image is 12 | // scaled down such that the aspect ratio is preserved. If 'crop_to_size' is 13 | // true, the resulting image will be the exact size of cols x rows (assuming 14 | // it was larger than that), and the image will be cropped to reach it. 15 | // If 'crop_to_size' is false, it is just scaled down such that the resulting 16 | // image will fit into the cols x rows. 17 | // 18 | // 'rotate_degrees' must be one of 0, 90, 180, or 270. 19 | // 20 | // If 'auto_orient' is true, the orientation is read from EXIF data on the 21 | // image, and it is rotated to be right side up, and an orientation of '1' 22 | // is written back to the EXIF. 23 | // 24 | // Return 0 on success, > 0 if an error and fill in 'err_msg'. 25 | int DoTransform(int cols, int rows, bool crop_to_size, 26 | int rotate_degrees, bool auto_orient, 27 | const std::string& src_path, const std::string& dst_path, 28 | int* new_width, int* new_height, std::string* err_msg); 29 | 30 | // Must be called once before DoTransform. 31 | void InitTransform(const char* argv0); 32 | 33 | // Creates an in memory 1 pixel png with given rgba values (0-255) 34 | // Return 0 on success, > 0 if an error and fill in 'err_msg'. 35 | int PNGPixel(unsigned char red, unsigned char green, unsigned char blue, 36 | unsigned char alpha, char** pixelData, size_t* length, std::string* err_msg); 37 | 38 | #endif // NODE_VIPS_TRANSFORM_H__ 39 | -------------------------------------------------------------------------------- /test/bogus.jpg: -------------------------------------------------------------------------------- 1 | bogus 2 | -------------------------------------------------------------------------------- /test/bogus_value_for_orientation_tag.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dosx/node-vips/c2c49c4942f1a50ba9e262f58698b01975126561/test/bogus_value_for_orientation_tag.jpg -------------------------------------------------------------------------------- /test/input.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dosx/node-vips/c2c49c4942f1a50ba9e262f58698b01975126561/test/input.jpg -------------------------------------------------------------------------------- /test/input2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dosx/node-vips/c2c49c4942f1a50ba9e262f58698b01975126561/test/input2.jpg -------------------------------------------------------------------------------- /test/simple_test.js: -------------------------------------------------------------------------------- 1 | // Copyright Erly Inc 2011, All Rights Reserved 2 | // Authors: Bo Wang, Walt Lin 3 | // 4 | // Tests for node-vips.cc 5 | 6 | var testCase = require('nodeunit').testCase; 7 | var fs = require('fs'); 8 | var imagemagick = require('imagemagick'); 9 | var vips = require('../index'); 10 | 11 | var input1 = 'test/input.jpg'; 12 | var input2 = 'test/input2.jpg'; 13 | 14 | // Return a path for an output file. 15 | var nextOutput = (function() { 16 | var id = 0; 17 | return function() { return 'test/output' + id++ + '.jpg'; } 18 | })(); 19 | 20 | module.exports = testCase({ 21 | test_resize_basic: function(assert) { 22 | vips.resize(input1, nextOutput(), 170, 170, true, false, function(err, m){ 23 | assert.ok(!err, "unexpected error: " + err); 24 | assert.equals(170, m.width); 25 | assert.equals(170, m.height); 26 | assert.done(); 27 | }); 28 | }, 29 | test_resize_basic_nocrop: function(assert) { 30 | vips.resize(input1, nextOutput(), 170, 170, false, false, function(err, m){ 31 | assert.ok(!err, "unexpected error: " + err); 32 | assert.equals(169, m.width); 33 | assert.equals(127, m.height); 34 | assert.done(); 35 | }); 36 | }, 37 | test_resize_with_auto_orient: function(assert) { 38 | vips.resize(input1, nextOutput(), 170, 170, true, true, function(err, m){ 39 | assert.ok(!err, "unexpected error: " + err); 40 | assert.done(); 41 | }); 42 | }, 43 | test_resize_with_auto_orient2: function(assert) { 44 | vips.resize(input2, nextOutput(), 170, 170, true, false, function(err, m){ 45 | assert.ok(!err, "unexpected error: " + err); 46 | assert.done(); 47 | }); 48 | }, 49 | test_resize_notfound: function(assert) { 50 | vips.resize("test/NOTFOUND", nextOutput(), 100, 100, true, true, 51 | function(err, metadata) { 52 | console.log("error: " + err); 53 | assert.ok(err, "expected error but did not get one"); 54 | assert.done(); 55 | }); 56 | }, 57 | test_resize_notfound_no_auto_orient: function(assert) { 58 | vips.resize("test/NOTFOUND", nextOutput(), 100, 100, true, false, 59 | function(err, metadata) { 60 | console.log("error: " + err); 61 | assert.ok(err, "expected error but did not get one"); 62 | assert.done(); 63 | }); 64 | }, 65 | test_resize_bogus_orientation_tag: function(assert) { 66 | vips.resize("test/bogus_value_for_orientation_tag.jpg", nextOutput(), 67 | 100, 100, true, true, function(err, metadata) { 68 | assert.ok(!err, "unexpected error: " + err); 69 | assert.done(); 70 | }); 71 | }, 72 | 73 | // Note: this test will crash if vips is compiled with imagemagick support 74 | // because imagemagick crashes when called from libeio. If vips does not 75 | // have imagemagick support, this test will pass. 76 | test_resize_bad_input: function(assert) { 77 | vips.resize("test/bogus.jpg", nextOutput(), 100, 100, true, false, 78 | function(err, metadata) { 79 | assert.ok(err, "expected error but did not get one"); 80 | assert.done(); 81 | }); 82 | }, 83 | 84 | 85 | test_rotate_basic: function(assert) { 86 | var output = nextOutput(); 87 | vips.rotate(input2, output, 90, function(err, metadata){ 88 | assert.ok(!err, "unexpected error: " + err); 89 | 90 | // Ensure that the dimensions of the new rotated photo are correct. 91 | imagemagick.identify(output, function(err, metadata) { 92 | assert.ok(!err, "unexpected error: " + err); 93 | assert.equals(1920, metadata.height); 94 | assert.equals(1200, metadata.width); 95 | assert.done(); 96 | }); 97 | }); 98 | }, 99 | test_rotate_error_not_found: function(assert) { 100 | vips.rotate("test/NOTFOUND", nextOutput(), 90, function(err, metadata){ 101 | assert.ok(err, "expected error but did not get one"); 102 | assert.done(); 103 | }); 104 | }, 105 | test_rotate_error_bad_degrees: function(assert) { 106 | vips.rotate(input1, nextOutput(), 93, function(err, metadata){ 107 | assert.ok(err, "expected error but did not get one"); 108 | assert.done(); 109 | }); 110 | }, 111 | }); 112 | --------------------------------------------------------------------------------