From f2f3eb76e1cd0c81bb2f8ae7dfd623f8df810a2a Mon Sep 17 00:00:00 2001 From: Lovell Fuller Date: Fri, 22 Aug 2014 14:17:43 +0100 Subject: [PATCH] Add method for fast access to image metadata #32 --- README.md | 15 +++ index.js | 42 ++++++++ src/sharp.cc | 260 ++++++++++++++++++++++++++++++++++++++++---------- tests/unit.js | 122 +++++++++++++++++++++++ 4 files changed, 387 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 74eb87a6..728a89b9 100755 --- a/README.md +++ b/README.md @@ -169,6 +169,21 @@ JPEG, PNG or WebP format image data can be streamed into the object when `input` JPEG, PNG or WebP format image data can be streamed out from this object. +#### metadata([callback]) + +Fast access to image metadata without decoding any compressed image data. + +`callback`, if present, gets the arguments `(err, metadata)` where `metadata` has the attributes: + +* `format`: Name of decoder to be used to decompress image data e.g. `jpeg`, `png`, `webp` (for file-based input additionally `tiff` and `magick`) +* `width`: Number of pixels wide +* `height`: Number of pixels high +* `space`: Name of colour space interpretation e.g. `srgb`, `rgb`, `scrgb`, `cmyk`, `lab`, `xyz`, `b-w` [...](https://github.com/jcupitt/libvips/blob/master/libvips/iofuncs/enumtypes.c#L502) +* `channels`: Number of bands e.g. `3` for sRGB, `4` for CMYK +* `orientation`: Number value of the EXIF Orientation header, if present + +A Promises/A+ promise is returned when `callback` is not provided. + #### sequentialRead() An advanced setting that switches the libvips access method to `VIPS_ACCESS_SEQUENTIAL`. This will reduce memory usage and can improve performance on some systems. diff --git a/index.js b/index.js index a0830703..74674444 100755 --- a/index.js +++ b/index.js @@ -347,6 +347,48 @@ Sharp.prototype._sharp = function(callback) { } }; +/* + Reads the image header and returns metadata + Supports callback, stream and promise variants +*/ +Sharp.prototype.metadata = function(callback) { + var that = this; + if (typeof callback === 'function') { + if (this.options.streamIn) { + this.on('finish', function() { + sharp.metadata(that.options, callback); + }); + } else { + sharp.metadata(this.options, callback); + } + return this; + } else { + if (this.options.streamIn) { + return new Promise(function(resolve, reject) { + that.on('finish', function() { + sharp.metadata(that.options, function(err, data) { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); + }); + } else { + return new Promise(function(resolve, reject) { + sharp.metadata(that.options, function(err, data) { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); + } + } +}; + /* Get and set cache memory and item limits */ diff --git a/src/sharp.cc b/src/sharp.cc index a1a46a47..0024b0de 100755 --- a/src/sharp.cc +++ b/src/sharp.cc @@ -46,6 +46,7 @@ struct resize_baton { }; typedef enum { + UNKNOWN, JPEG, PNG, WEBP, @@ -153,69 +154,223 @@ sharp_calc_crop(int const inWidth, int const inHeight, int const outWidth, int c return std::make_tuple(left, top); } -class ResizeWorker : public NanAsyncWorker { - public: - ResizeWorker(NanCallback *callback, resize_baton *baton) - : NanAsyncWorker(callback), baton(baton) {} - ~ResizeWorker() {} +/* + Initialise a VipsImage from a buffer. Supports JPEG, PNG and WebP. + Returns the ImageType detected, if any. +*/ +static ImageType +sharp_init_image_from_buffer(VipsImage **image, void *buffer, size_t const length, VipsAccess const access) { + ImageType imageType = UNKNOWN; + if (memcmp(MARKER_JPEG, buffer, 2) == 0) { + if (!vips_jpegload_buffer(buffer, length, image, "access", access, NULL)) { + imageType = JPEG; + } + } else if(memcmp(MARKER_PNG, buffer, 2) == 0) { + if (!vips_pngload_buffer(buffer, length, image, "access", access, NULL)) { + imageType = PNG; + } + } else if(memcmp(MARKER_WEBP, buffer, 2) == 0) { + if (!vips_webpload_buffer(buffer, length, image, "access", access, NULL)) { + imageType = WEBP; + } + } + return imageType; +} - void Execute () { +/* + Initialise a VipsImage from a file. + Returns the ImageType detected, if any. +*/ +static ImageType +sharp_init_image_from_file(VipsImage **image, char const *file, VipsAccess const access) { + ImageType imageType = UNKNOWN; + if (vips_foreign_is_a("jpegload", file)) { + if (!vips_jpegload(file, image, "access", access, NULL)) { + imageType = JPEG; + } + } else if (vips_foreign_is_a("pngload", file)) { + if (!vips_pngload(file, image, "access", access, NULL)) { + imageType = PNG; + } + } else if (vips_foreign_is_a("webpload", file)) { + if (!vips_webpload(file, image, "access", access, NULL)) { + imageType = WEBP; + } + } else if (vips_foreign_is_a("tiffload", file)) { + if (!vips_tiffload(file, image, "access", access, NULL)) { + imageType = TIFF; + } + } else if(vips_foreign_is_a("magickload", file)) { + if (!vips_magickload(file, image, "access", access, NULL)) { + imageType = MAGICK; + } + } + return imageType; +} + +// Metadata + +struct metadata_baton { + // Input + std::string file_in; + void* buffer_in; + size_t buffer_in_len; + // Output + std::string format; + int width; + int height; + std::string space; + int channels; + int orientation; + std::string err; + + metadata_baton(): + buffer_in_len(0), + orientation(0) {} +}; + +class MetadataWorker : public NanAsyncWorker { + + public: + MetadataWorker(NanCallback *callback, metadata_baton *baton) : NanAsyncWorker(callback), baton(baton) {} + ~MetadataWorker() {} + + void Execute() { + // Decrement queued task counter + g_atomic_int_dec_and_test(&counter_queue); + + ImageType imageType = UNKNOWN; + VipsImage *image = vips_image_new(); + if (baton->buffer_in_len > 1) { + // From buffer + imageType = sharp_init_image_from_buffer(&image, baton->buffer_in, baton->buffer_in_len, VIPS_ACCESS_RANDOM); + if (imageType == UNKNOWN) { + (baton->err).append("Input buffer contains unsupported image format"); + } + } else { + // From file + imageType = sharp_init_image_from_file(&image, baton->file_in.c_str(), VIPS_ACCESS_RANDOM); + if (imageType == UNKNOWN) { + (baton->err).append("File is of an unsupported image format"); + } + } + if (imageType != UNKNOWN) { + // Image type + switch (imageType) { + case JPEG: baton->format = "jpeg"; break; + case PNG: baton->format = "png"; break; + case WEBP: baton->format = "webp"; break; + case TIFF: baton->format = "tiff"; break; + case MAGICK: baton->format = "magick"; break; + case UNKNOWN: default: baton->format = ""; + } + // VipsImage attributes + baton->width = image->Xsize; + baton->height = image->Ysize; + baton->space = vips_enum_nick(VIPS_TYPE_INTERPRETATION, image->Type); + baton->channels = image->Bands; + // EXIF Orientation + const char *exif; + if (!vips_image_get_string(image, "exif-ifd0-Orientation", &exif)) { + baton->orientation = atoi(&exif[0]); + } + } + // Clean up + g_object_unref(image); + vips_error_clear(); + vips_thread_shutdown(); + } + + void HandleOKCallback () { + NanScope(); + + Handle argv[2] = { NanNull(), NanNull() }; + if (!baton->err.empty()) { + // Error + argv[0] = NanNew(baton->err.data(), baton->err.size()); + } else { + // Metadata Object + Local info = NanNew(); + info->Set(NanNew("format"), NanNew(baton->format)); + info->Set(NanNew("width"), NanNew(baton->width)); + info->Set(NanNew("height"), NanNew(baton->height)); + info->Set(NanNew("space"), NanNew(baton->space)); + info->Set(NanNew("channels"), NanNew(baton->channels)); + if (baton->orientation > 0) { + info->Set(NanNew("orientation"), NanNew(baton->orientation)); + } + argv[1] = info; + } + delete baton; + + // Return to JavaScript + callback->Call(2, argv); + } + + private: + metadata_baton* baton; +}; + +/* + metadata(options, callback) +*/ +NAN_METHOD(metadata) { + NanScope(); + + // V8 objects are converted to non-V8 types held in the baton struct + metadata_baton *baton = new metadata_baton; + Local options = args[0]->ToObject(); + + // Input filename + baton->file_in = *String::Utf8Value(options->Get(NanNew("fileIn"))->ToString()); + // Input Buffer object + if (options->Get(NanNew("bufferIn"))->IsObject()) { + Local buffer = options->Get(NanNew("bufferIn"))->ToObject(); + baton->buffer_in_len = Buffer::Length(buffer); + baton->buffer_in = Buffer::Data(buffer); + } + + // Join queue for worker thread + NanCallback *callback = new NanCallback(args[1].As()); + NanAsyncQueueWorker(new MetadataWorker(callback, baton)); + + // Increment queued task counter + g_atomic_int_inc(&counter_queue); + + NanReturnUndefined(); +} + +// Resize + +class ResizeWorker : public NanAsyncWorker { + public: + ResizeWorker(NanCallback *callback, resize_baton *baton) : NanAsyncWorker(callback), baton(baton) {} + ~ResizeWorker() {} + + void Execute() { // Decrement queued task counter g_atomic_int_dec_and_test(&counter_queue); // Increment processing task counter g_atomic_int_inc(&counter_process); // Input - ImageType inputImageType = JPEG; + ImageType inputImageType = UNKNOWN; 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(memcmp(MARKER_WEBP, baton->buffer_in, 2) == 0) { - inputImageType = WEBP; - if (vips_webpload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) { - return resize_error(baton, in); - } - } else { - resize_error(baton, in); - (baton->err).append("Unsupported input buffer"); - return; - } - } else if (vips_foreign_is_a("jpegload", baton->file_in.c_str())) { - if (vips_jpegload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) { - return resize_error(baton, in); - } - } else if (vips_foreign_is_a("pngload", baton->file_in.c_str())) { - inputImageType = PNG; - if (vips_pngload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) { - return resize_error(baton, in); - } - } else if (vips_foreign_is_a("webpload", baton->file_in.c_str())) { - inputImageType = WEBP; - if (vips_webpload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) { - return resize_error(baton, in); - } - } else if (vips_foreign_is_a("tiffload", baton->file_in.c_str())) { - inputImageType = TIFF; - if (vips_tiffload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) { - return resize_error(baton, in); - } - } else if(vips_foreign_is_a("magickload", (baton->file_in).c_str())) { - inputImageType = MAGICK; - if (vips_magickload((baton->file_in).c_str(), &in, "access", baton->access_method, NULL)) { - return resize_error(baton, in); + // From buffer + inputImageType = sharp_init_image_from_buffer(&in, baton->buffer_in, baton->buffer_in_len, baton->access_method); + if (inputImageType == UNKNOWN) { + (baton->err).append("Input buffer contains unsupported image format"); } } else { - resize_error(baton, in); - (baton->err).append("Unsupported input file " + baton->file_in); - return; + // From file + inputImageType = sharp_init_image_from_file(&in, baton->file_in.c_str(), baton->access_method); + if (inputImageType == UNKNOWN) { + (baton->err).append("File is of an unsupported image format"); + } + } + if (inputImageType == UNKNOWN) { + return resize_error(baton, in); } // Get input image width and height @@ -613,6 +768,7 @@ extern "C" void init(Handle target) { vips_cache_set_max_mem(100 * 1048576); // 100 MB vips_cache_set_max(500); // 500 operations // Methods available to JavaScript + NODE_SET_METHOD(target, "metadata", metadata); NODE_SET_METHOD(target, "resize", resize); NODE_SET_METHOD(target, "cache", cache); NODE_SET_METHOD(target, "counters", counters); diff --git a/tests/unit.js b/tests/unit.js index 39229db8..f2669508 100755 --- a/tests/unit.js +++ b/tests/unit.js @@ -18,6 +18,10 @@ var outputTiff = path.join(fixturesPath, "output.tiff"); var inputJpgWithExif = path.join(fixturesPath, "Landscape_8.jpg"); // https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_8.jpg +var inputPng = path.join(fixturesPath, "50020484-00001.png"); // http://c.searspartsdirect.com/lis_png/PLDM/50020484-00001.png +var inputWebp = path.join(fixturesPath, "4.webp"); // http://www.gstatic.com/webp/gallery/4.webp +var inputGif = path.join(fixturesPath, "Crash_test.gif"); // http://upload.wikimedia.org/wikipedia/commons/e/e3/Crash_test.gif + // Ensure cache limits can be set sharp.cache(0); // Disable sharp.cache(50, 500); // 50MB, 500 items @@ -398,6 +402,124 @@ async.series([ done(); }); }, + // Metadata - JPEG + function(done) { + sharp(inputJpg).metadata(function(err, metadata) { + if (err) throw err; + assert.strictEqual('jpeg', metadata.format); + assert.strictEqual(2725, metadata.width); + assert.strictEqual(2225, metadata.height); + assert.strictEqual('srgb', metadata.space); + assert.strictEqual(3, metadata.channels); + assert.strictEqual('undefined', typeof metadata.orientation); + done(); + }); + }, + // Metadata - JPEG with EXIF + function(done) { + sharp(inputJpgWithExif).metadata(function(err, metadata) { + if (err) throw err; + assert.strictEqual('jpeg', metadata.format); + assert.strictEqual(450, metadata.width); + assert.strictEqual(600, metadata.height); + assert.strictEqual('srgb', metadata.space); + assert.strictEqual(3, metadata.channels); + assert.strictEqual(8, metadata.orientation); + done(); + }); + }, + // Metadata - TIFF + function(done) { + sharp(inputTiff).metadata(function(err, metadata) { + if (err) throw err; + assert.strictEqual('tiff', metadata.format); + assert.strictEqual(2464, metadata.width); + assert.strictEqual(3248, metadata.height); + assert.strictEqual('b-w', metadata.space); + assert.strictEqual(1, metadata.channels); + done(); + }); + }, + // Metadata - PNG + function(done) { + sharp(inputPng).metadata(function(err, metadata) { + if (err) throw err; + assert.strictEqual('png', metadata.format); + assert.strictEqual(2809, metadata.width); + assert.strictEqual(2074, metadata.height); + assert.strictEqual('b-w', metadata.space); + assert.strictEqual(1, metadata.channels); + done(); + }); + }, + // Metadata - WebP + function(done) { + sharp(inputWebp).metadata(function(err, metadata) { + if (err) throw err; + assert.strictEqual('webp', metadata.format); + assert.strictEqual(1024, metadata.width); + assert.strictEqual(772, metadata.height); + assert.strictEqual('srgb', metadata.space); + assert.strictEqual(3, metadata.channels); + done(); + }); + }, + // Metadata - GIF (via libmagick) + function(done) { + sharp(inputGif).metadata(function(err, metadata) { + if (err) throw err; + assert.strictEqual('magick', metadata.format); + assert.strictEqual(800, metadata.width); + assert.strictEqual(533, metadata.height); + assert.strictEqual('srgb', metadata.space); + assert.strictEqual(3, metadata.channels); + done(); + }); + }, + // Metadata - Promise + function(done) { + sharp(inputJpg).metadata().then(function(metadata) { + assert.strictEqual('jpeg', metadata.format); + assert.strictEqual(2725, metadata.width); + assert.strictEqual(2225, metadata.height); + assert.strictEqual('srgb', metadata.space); + assert.strictEqual(3, metadata.channels); + done(); + }); + }, + // Metadata - Stream + function(done) { + var readable = fs.createReadStream(inputJpg); + var pipeline = sharp().metadata(function(err, metadata) { + if (err) throw err; + assert.strictEqual('jpeg', metadata.format); + assert.strictEqual(2725, metadata.width); + assert.strictEqual(2225, metadata.height); + assert.strictEqual('srgb', metadata.space); + assert.strictEqual(3, metadata.channels); + done(); + }); + readable.pipe(pipeline); + }, + // Get metadata then resize to half width + function(done) { + var image = sharp(inputJpg); + image.metadata(function(err, metadata) { + if (err) throw err; + assert.strictEqual('jpeg', metadata.format); + assert.strictEqual(2725, metadata.width); + assert.strictEqual(2225, metadata.height); + assert.strictEqual('srgb', metadata.space); + assert.strictEqual(3, metadata.channels); + image.resize(metadata.width / 2).toBuffer(function(err, data, info) { + if (err) throw err; + assert.strictEqual(true, data.length > 0); + assert.strictEqual(1362, info.width); + assert.strictEqual(1112, info.height); + done(); + }) + }); + }, // Verify internal counters function(done) { var counters = sharp.counters();