Skip to content
Browse files

Add support for reading image previews

Did some benchmarking and found that when running parallel tasks reading
previews it was better to do it in the worker thread and copy it over. Should
probably look at doing the same thing for the tag reading.
  • Loading branch information...
1 parent 1450247 commit e6ce7bf84cf6257ed3974dc443923de68b030d28 @drewish drewish committed
Showing with 214 additions and 4 deletions.
  1. +14 −2 examples/simple.js
  2. +154 −2 exiv2node.cc
  3. +46 −0 test/test.js
View
16 examples/simple.js
@@ -4,7 +4,7 @@ var fs = require('fs')
var dir = __dirname + '/../test/images';
-// Test basic image with exif tags
+// Test basic image with exif tags:
ex.getImageTags(dir + '/books.jpg', function(err, tags) {
console.log(tags);
});
@@ -12,7 +12,7 @@ ex.getImageTags(dir + '/books.jpg', function(err, tags) {
// Make a copy of our file so we don't polute the original.
fs.writeFileSync(dir + '/copy.jpg', fs.readFileSync(dir + '/books.jpg'));
-// Set some tags on the image
+// Set some tags on the image:
var tags = {
"Exif.Photo.UserComment" : "Some books..",
"Exif.Canon.OwnerName" : "Damo's camera"
@@ -24,3 +24,15 @@ ex.setImageTags(dir + '/copy.jpg', tags, function(err){
assert.equal("Damo's camera", tags["Exif.Canon.OwnerName"]);
});
});
+
+// Load the preview images:
+ex.getImagePreviews(dir + '/books.jpg', function(err, previews) {
+ // Display information about the previews.
+ console.log("Preview images:");
+ previews.forEach(function (p, index) {
+ console.log("%d: %s %dx%dpx %d bytes", index, p.mimeType, p.height, p.width, p.data.length);
+ });
+ // Or you can save them--though you'll probably want to check the MIME type
+ // before picking an extension.
+ fs.writeFile('preview.jpg', previews[0].data);
+});
View
156 exiv2node.cc
@@ -1,10 +1,12 @@
#include <v8.h>
#include <node.h>
+#include <node_buffer.h>
#include <unistd.h>
#include <string>
#include <map>
#include <exiv2/image.hpp>
#include <exiv2/exif.hpp>
+#include <exiv2/preview.hpp>
using namespace node;
using namespace v8;
@@ -45,10 +47,70 @@ struct SetTagsBaton : Baton {
}
};
+// Holds a preview image when we copy it from the worker to V8.
+struct Preview {
+ std::string mimeType;
+ uint32_t height;
+ uint32_t width;
+ char* data;
+ size_t size;
+
+ Preview(std::string type_, uint32_t height_, uint32_t width_, const char *data_, size_t size_)
+ : mimeType(type_), height(height_), width(width_), size(size_)
+ {
+ data = new char[size_];
+ memcpy(data, data_, size_);
+ }
+ virtual ~Preview() {
+ delete[] data;
+ }
+};
+
+struct GetPreviewBaton : Baton {
+ Preview **previews;
+ size_t size;
+
+ GetPreviewBaton(Local<String> fn_, Handle<Function> cb_)
+ : Baton(fn_, cb_), previews(0), size(0)
+ {}
+ virtual ~GetPreviewBaton() {
+ for (size_t i = 0; i < size; ++i) {
+ delete previews[i];
+ }
+ delete previews;
+ }
+};
+
+/**
+ * Create a Buffer object from raw data.
+ *
+ * Based on code from: http://sambro.is-super-awesome.com/tag/slowbuffer/
+ */
+static Local<Object> makeBuffer(char* data, size_t size) {
+ HandleScope scope;
+
+ // It ends up being kind of a pain to convert a slow buffer into a fast
+ // one since the fast part is implemented in JavaScript.
+ Local<Buffer> slowBuffer = Buffer::New(data, size);
+ // First get the Buffer from global scope...
+ Local<Object> global = Context::GetCurrent()->Global();
+ Local<Value> bv = global->Get(String::NewSymbol("Buffer"));
+ assert(bv->IsFunction());
+ Local<Function> b = Local<Function>::Cast(bv);
+ // ...call Buffer() with the slow buffer, its size and the starting offset...
+ Handle<Value> argv[3] = { slowBuffer->handle_, Integer::New(size), Integer::New(0) };
+ Local<Object> fastBuffer = b->NewInstance(3, argv);
+
+ // ...and get a fast buffer that we can return.
+ return scope.Close(fastBuffer);
+}
+
static void GetImageTagsWorker(uv_work_t* req);
static void AfterGetImageTags(uv_work_t* req);
static void SetImageTagsWorker(uv_work_t *req);
static void AfterSetImageTags(uv_work_t *req);
+static void GetImagePreviewsWorker(uv_work_t* req);
+static void AfterGetImagePreviews(uv_work_t* req);
static Handle<Value> GetImageTags(const Arguments& args) {
HandleScope scope;
@@ -194,9 +256,8 @@ static void AfterSetImageTags(uv_work_t *req) {
HandleScope scope;
SetTagsBaton *thread_data = static_cast<SetTagsBaton*> (req->data);
+ // Create an argument array for any errors.
Local<Value> argv[1];
-
- /* Create a V8 array with all the image tags and their corresponding values */
if (thread_data->exifException.empty()) {
argv[0] = Local<Value>::New(Null());
} else {
@@ -213,8 +274,99 @@ static void AfterSetImageTags(uv_work_t *req) {
delete thread_data;
}
+
+// Fetch preview images
+static Handle<Value> GetImagePreviews(const Arguments& args) {
+ HandleScope scope;
+
+ // Usage arguments
+ if (args.Length() <= (1) || !args[1]->IsFunction())
+ return ThrowException(Exception::TypeError(String::New("Usage: <filename> <callback function>")));
+
+ Local<String> fileName = Local<String>::Cast(args[0]);
+ Local<Function> cb = Local<Function>::Cast(args[1]);
+
+ // Set up our thread data struct, pass off to the libuv thread pool.
+ GetPreviewBaton *thread_data = new GetPreviewBaton(fileName, cb);
+
+ int status = uv_queue_work(uv_default_loop(), &thread_data->request, GetImagePreviewsWorker, AfterGetImagePreviews);
+ assert(status == 0);
+
+ return Undefined();
+}
+
+// Extract the previews and copy them into the baton.
+static void GetImagePreviewsWorker(uv_work_t *req) {
+ GetPreviewBaton *thread_data = static_cast<GetPreviewBaton*> (req->data);
+
+ try {
+ thread_data->image = Exiv2::ImageFactory::open(thread_data->fileName);
+ assert(thread_data->image.get() != 0);
+ thread_data->image->readMetadata();
+
+ Exiv2::PreviewManager manager(*thread_data->image);
+ Exiv2::PreviewPropertiesList list = manager.getPreviewProperties();
+
+ // Make sure we're not reusing a baton with memory allocated.
+ assert(thread_data->previews == 0);
+ assert(thread_data->size == 0);
+
+ thread_data->size = list.size();
+ thread_data->previews = new Preview*[thread_data->size];
+
+ int i = 0;
+ for (Exiv2::PreviewPropertiesList::iterator pos = list.begin(); pos != list.end(); pos++) {
+ Exiv2::PreviewImage image = manager.getPreviewImage(*pos);
+
+ thread_data->previews[i++] = new Preview(pos->mimeType_, pos->height_,
+ pos->width_, (char*) image.pData(), pos->size_);
+ }
+ } catch (Exiv2::AnyError& e) {
+ thread_data->exifException.append(e.what());
+ }
+}
+
+// Convert the previews from the baton into V8 objects and fire the callback.
+static void AfterGetImagePreviews(uv_work_t *req) {
+ HandleScope scope;
+ GetPreviewBaton *thread_data = static_cast<GetPreviewBaton*> (req->data);
+
+ Local<Value> argv[2];
+ if (!thread_data->exifException.empty()){
+ argv[0] = Local<String>::New(String::New(thread_data->exifException.c_str()));
+ argv[1] = Local<Value>::New(Null());
+ } else {
+ // Convert the data into V8 values.
+ Local<Array> previews = Array::New(thread_data->size);
+ for (size_t i = 0; i < thread_data->size; ++i) {
+ Preview *p = thread_data->previews[i];
+
+ Local<Object> preview = Object::New();
+ preview->Set(String::New("mimeType"), String::New(p->mimeType.c_str()));
+ preview->Set(String::New("height"), Number::New(p->height));
+ preview->Set(String::New("width"), Number::New(p->width));
+ preview->Set(String::New("data"), makeBuffer(p->data, p->size));
+
+ previews->Set(i, preview);
+ }
+
+ argv[0] = Local<Value>::New(Null());
+ argv[1] = previews;
+ }
+
+ // Pass the argv array object to our callback function.
+ TryCatch try_catch;
+ thread_data->cb->Call(Context::GetCurrent()->Global(), 2, argv);
+ if (try_catch.HasCaught()) {
+ FatalException(try_catch);
+ }
+
+ delete thread_data;
+}
+
void InitAll(Handle<Object> target) {
target->Set(String::NewSymbol("getImageTags"), FunctionTemplate::New(GetImageTags)->GetFunction());
target->Set(String::NewSymbol("setImageTags"), FunctionTemplate::New(SetImageTags)->GetFunction());
+ target->Set(String::NewSymbol("getImagePreviews"), FunctionTemplate::New(GetImagePreviews)->GetFunction());
}
NODE_MODULE(exiv2, InitAll)
View
46 test/test.js
@@ -89,4 +89,50 @@ describe('exiv2', function(){
});
});
});
+
+ describe('.getImagePreviews()', function(){
+ it("should callback with image's previews", function(done) {
+ exiv.getImagePreviews(dir + '/books.jpg', function(err, previews) {
+ should.not.exist(err);
+ previews.should.be.an.instanceof(Array);
+ previews.should.have.lengthOf(1);
+ previews[0].should.have.property('mimeType', 'image/jpeg');
+ previews[0].should.have.property('height', 120);
+ previews[0].should.have.property('width', 160);
+ previews[0].should.have.property('data').with.instanceof(Buffer);
+ previews[0].data.should.have.property('length', 6071);
+ done();
+ });
+ });
+
+ it('should callback with an empty array for files no previews', function(done) {
+ exiv.getImagePreviews(dir + '/damien.jpg', function(err, previews) {
+ should.not.exist(err);
+ previews.should.be.an.instanceof(Array);
+ previews.should.have.lengthOf(0);
+ done();
+ })
+ });
+
+
+ it('should throw if no file path is provided', function() {
+ (function(){
+ exiv.getImagePreviews()
+ }).should.throw();
+ });
+
+ it('should throw if no callback is provided', function() {
+ (function(){
+ exiv.getImagePreviews(dir + '/books.jpg')
+ }).should.throw();
+ });
+
+ it('should report an error on an invalid path', function(done) {
+ exiv.getImagePreviews('idontexist.jpg', function(err, previews) {
+ should.exist(err);
+ should.not.exist(previews);
+ done();
+ });
+ });
+ });
})

0 comments on commit e6ce7bf

Please sign in to comment.
Something went wrong with that request. Please try again.