Add method for fast access to image metadata #32

This commit is contained in:
Lovell Fuller 2014-08-22 14:17:43 +01:00
parent 5fe945fca8
commit f2f3eb76e1
4 changed files with 387 additions and 52 deletions

View File

@ -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. 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() #### 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. 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.

View File

@ -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 Get and set cache memory and item limits
*/ */

View File

@ -46,6 +46,7 @@ struct resize_baton {
}; };
typedef enum { typedef enum {
UNKNOWN,
JPEG, JPEG,
PNG, PNG,
WEBP, WEBP,
@ -153,10 +154,197 @@ sharp_calc_crop(int const inWidth, int const inHeight, int const outWidth, int c
return std::make_tuple(left, top); return std::make_tuple(left, top);
} }
/*
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;
}
/*
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<Value> argv[2] = { NanNull(), NanNull() };
if (!baton->err.empty()) {
// Error
argv[0] = NanNew<String>(baton->err.data(), baton->err.size());
} else {
// Metadata Object
Local<Object> info = NanNew<Object>();
info->Set(NanNew<String>("format"), NanNew<String>(baton->format));
info->Set(NanNew<String>("width"), NanNew<Number>(baton->width));
info->Set(NanNew<String>("height"), NanNew<Number>(baton->height));
info->Set(NanNew<String>("space"), NanNew<String>(baton->space));
info->Set(NanNew<String>("channels"), NanNew<Number>(baton->channels));
if (baton->orientation > 0) {
info->Set(NanNew<String>("orientation"), NanNew<Number>(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<Object> options = args[0]->ToObject();
// Input filename
baton->file_in = *String::Utf8Value(options->Get(NanNew<String>("fileIn"))->ToString());
// Input Buffer object
if (options->Get(NanNew<String>("bufferIn"))->IsObject()) {
Local<Object> buffer = options->Get(NanNew<String>("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<v8::Function>());
NanAsyncQueueWorker(new MetadataWorker(callback, baton));
// Increment queued task counter
g_atomic_int_inc(&counter_queue);
NanReturnUndefined();
}
// Resize
class ResizeWorker : public NanAsyncWorker { class ResizeWorker : public NanAsyncWorker {
public: public:
ResizeWorker(NanCallback *callback, resize_baton *baton) ResizeWorker(NanCallback *callback, resize_baton *baton) : NanAsyncWorker(callback), baton(baton) {}
: NanAsyncWorker(callback), baton(baton) {}
~ResizeWorker() {} ~ResizeWorker() {}
void Execute() { void Execute() {
@ -166,57 +354,24 @@ class ResizeWorker : public NanAsyncWorker {
g_atomic_int_inc(&counter_process); g_atomic_int_inc(&counter_process);
// Input // Input
ImageType inputImageType = JPEG; ImageType inputImageType = UNKNOWN;
VipsImage *in = vips_image_new(); VipsImage *in = vips_image_new();
if (baton->buffer_in_len > 1) { if (baton->buffer_in_len > 1) {
if (memcmp(MARKER_JPEG, baton->buffer_in, 2) == 0) { // From buffer
if (vips_jpegload_buffer(baton->buffer_in, baton->buffer_in_len, &in, "access", baton->access_method, NULL)) { inputImageType = sharp_init_image_from_buffer(&in, baton->buffer_in, baton->buffer_in_len, baton->access_method);
return resize_error(baton, in); if (inputImageType == UNKNOWN) {
} (baton->err).append("Input buffer contains unsupported image format");
} 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 { } else {
resize_error(baton, in); // From file
(baton->err).append("Unsupported input buffer"); inputImageType = sharp_init_image_from_file(&in, baton->file_in.c_str(), baton->access_method);
return; if (inputImageType == UNKNOWN) {
(baton->err).append("File is of an unsupported image format");
} }
} 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)) { if (inputImageType == UNKNOWN) {
return resize_error(baton, in); 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);
}
} else {
resize_error(baton, in);
(baton->err).append("Unsupported input file " + baton->file_in);
return;
}
// Get input image width and height // Get input image width and height
int inputWidth = in->Xsize; int inputWidth = in->Xsize;
@ -613,6 +768,7 @@ extern "C" void init(Handle<Object> target) {
vips_cache_set_max_mem(100 * 1048576); // 100 MB vips_cache_set_max_mem(100 * 1048576); // 100 MB
vips_cache_set_max(500); // 500 operations vips_cache_set_max(500); // 500 operations
// Methods available to JavaScript // Methods available to JavaScript
NODE_SET_METHOD(target, "metadata", metadata);
NODE_SET_METHOD(target, "resize", resize); NODE_SET_METHOD(target, "resize", resize);
NODE_SET_METHOD(target, "cache", cache); NODE_SET_METHOD(target, "cache", cache);
NODE_SET_METHOD(target, "counters", counters); NODE_SET_METHOD(target, "counters", counters);

View File

@ -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 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 // Ensure cache limits can be set
sharp.cache(0); // Disable sharp.cache(0); // Disable
sharp.cache(50, 500); // 50MB, 500 items sharp.cache(50, 500); // 50MB, 500 items
@ -398,6 +402,124 @@ async.series([
done(); 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 // Verify internal counters
function(done) { function(done) {
var counters = sharp.counters(); var counters = sharp.counters();