diff --git a/Readme.md b/Readme.md index 837d237..7cf4410 100644 --- a/Readme.md +++ b/Readme.md @@ -2,24 +2,49 @@ Image meta-information (EXIF, IPTC, XMP...) extraction using [exiftool](http://www.sno.phy.queensu.ca/~phil/exiftool/) -__NOTE__: This fork from https://github.com/visionmedia/node-exif has a DIFFERENT (improved !) API. - It uses [precise tags](http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/index.html) for field access. +__NOTE__: This fork from https://github.com/Yvem/node-exif has a DIFFERENT +(improved !) API. + + Mayor Changes: + Instead of calling 'exiftool' through 'child_process.exec', it calls: + [child_process.spawn](https://nodejs.org/api/child_process + .html#child_process_child_process_spawn_command_args_options) which avoids buffer limitations. + It also allows to send specific arguments to the 'exiftool' shell command + and to read EXIF info from multiple files at once. ## Installation - $ npm install Yvem/node-exif + $ npm install jmunox/node-exif ## Usage + * Fetch EXIF data from `file` and invoke `fn(err, data)`. + It spawns a child process (see child_process.spawn) and executes [exiftool] + (http://www.sno.phy.queensu.ca/%7Ephil/exiftool/) + + Params: + + @param {String} file or path to folder + + @param {Array} args [optional] List of string arguments to pass to + [exiftool](http://www.sno.phy.queensu.ca/~phil/exiftool/exiftool_pod.html) + + @param {Object} opts [optional] Object that is passed to the + child_process.spawn method as the `options` argument. See options of + [child_process.spawn](https://nodejs.org/api/child_process + .html#child_process_child_process_spawn_command_args_options) + + @param {function} fn callback function to invoke `fn(err, data)` + ```javascript var exif = require('exif2'); -exif(file, function(err, obj){ +exif(file, args, opts function(err, obj){ console.log(obj); // see available tags http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/index.html - console.log(o['FileName']); - console.log(o['Caption-Abstract']); // IPTC Caption [2,120] + console.log(obj['FileName']); + console.log(obj['Caption-Abstract']); // IPTC Caption [2,120] }) ``` @@ -150,23 +175,110 @@ exif(file, function(err, obj){ ## Advanced usage -### Errors -node-exif may throw custom errors : +### Parsing specific TagNames +It is possible to parse specific EXIF metadata from a file by defining the +TagNames in the Arguments: + +```javascript +var exif = require('exif2'); +var file = 'test/fixtures/forest.jpg'; +var exifParams = ['-FileName', '-ImageHeight', '-ImageWidth', '-Orientation', + '-DateTimeOriginal', '-CreateDate', '-ModifyDate', '-FileAccessDate', + '-FileType', '-MIMEType']; + + exif(file, exifParams, function(err, metadata){ -* `Metadata too big !` when metadata are too big to be parsed with current buffer limitations + console.log(metadata.['ImageHeight']); + console.log(metadata.['ImageWidth']); + } -### Special options -For special cases, it is possible to provide exec options. -`exif()` optional second parameter may be an `exec()` option object as described here : -http://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback +``` -Example usage : augment stdout buffer size to handle images with huge metadata : +```json +{ + "SourceFile": "test/fixtures/forest.jpeg", + "FileName": "forest.jpeg", + "ImageWidth": 900, + "ImageHeight": 596 + "Orientation": "Horizontal (normal)", + "DateTimeOriginal": "2012:10:07 11:36:30", + "CreateDate": "2012:10:07 11:36:30", + "ModifyDate": "2012:10:08 19:10:63", + "FileAccessDate": "2014:03:24 15:27:05+01:00", + "FileType": "JPEG", + "MIMEType": "image/jpeg" +} +``` + +### Parsing EXIF from several media files in path +It is also possible to parse EXIF metadata from all the media file by setting + the path to a specific folder. The data is returned in an Array. This is + more efficient, instead of calling `exif` for each file in the folder. ```javascript -// REM : default buffer value is 200*1024 -exif(file, { maxBuffer: 1024*1024 }, function(err, obj) { +var exif = require('exif2'); +var path = 'test/fixtures/'; +var exifParams = ['-FileName', '-ImageWidth', '-ImageHeight', '-Orientation', + '-DateTimeOriginal', '-CreateDate', '-ModifyDate', '-FileAccessDate', + '-FileType', '-MIMEType']; + +exif(file, exifParams, function(err, metadata){ + console.log(metadata[0].['ImageWidth']); // 900 + console.log(metadata[0].['ImageHeight']); // 596 + console.log(metadata[1].['ImageWidth']); // 3776 + console.log(metadata[1].['ImageHeight']); // 3129 + +} +``` +Result: +```json +[{ + "SourceFile": "test/fixtures/forest.jpeg", + "FileName": "forest.jpeg", + "ImageWidth": 900, + "ImageHeight": 596, + "Orientation": "Horizontal (normal)", + "DateTimeOriginal": "2012:10:07 11:36:30", + "CreateDate": "2012:10:07 11:36:30", + "ModifyDate": "2012:10:08 19:10:63", + "FileAccessDate": "2014:03:24 15:27:05+01:00", + "FileType": "JPEG", + "MIMEType": "image/jpeg" +}, +{ + "SourceFile": "test/fixtures/le_livre_de_photographies_vol_III_Phaidon.jpg", + "FileName": "le_livre_de_photographies_vol_III_Phaidon.jpg", + "ImageWidth": 3776, + "ImageHeight": 3129, + "Orientation": "Horizontal (normal)", + "CreateDate": "2014:01:09 12:52:13+01:00", + "ModifyDate": "2014:01:09 15:36:23", + "FileAccessDate": "2016:10:31 16:24:05+01:00", + "FileType": "JPEG", + "MIMEType": "image/jpeg" +}] +``` +### No more known buffer limitation + +Since this version uses `child_process.spawn()` instead of +`child_process.exec()`, the output is handled in a different way, avoiding +buffer limitations. + +See [child_process.spawn](https://nodejs.org/api/child_process + .html#child_process_child_process_spawn_command_args_options) + +### Special execution +For special cases, it is possible to provide options for the [spawn process] +(https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options). +`exif()` optional third parameter may be an `spawn()` option object as +described here: + +```javascript +var opts = { cwd: undefined, env: process.env }; +exif(file, args, opts, function(err, obj) { console.log(obj); }) + ``` ## Test / contribute diff --git a/index.js b/index.js index ef69b55..1adf2d3 100644 --- a/index.js +++ b/index.js @@ -3,37 +3,67 @@ * Module dependencies. */ -var exec = require('child_process').exec; -var command = require('shelly'); +const spawn = require('child_process').spawn; +var shelly = require('shelly'); /** * Fetch EXIF data from `file` and invoke `fn(err, data)`. * * @param {String} file - * @param {Object} execOpts [optional] options to pass to exec() for a finer control + * @param {Array} args [optional] List of string arguments to pass to exiftool * @param {Function} fn * @api public */ -module.exports = function(file, execOpts, fn){ +/** + * Fetch EXIF data from `file` and invoke `fn(err, data)`. It spawns a child + * process (see child_process.spawn) and executes exiftool + * http://www.sno.phy.queensu.ca/%7Ephil/exiftool/ + * + * @param {String} file + * @param {Array} args [optional] List of string arguments to pass to + * exiftool http://www.sno.phy.queensu.ca/~phil/exiftool/exiftool_pod.html + * @param {Object} opts [optional] Object that is passed to the + * child_process.spawn method as the `options` argument + * See options of child_process.spawn: + * https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options + * @param {function} fn callback function to invoke `fn(err, data)` + */ +module.exports = function(file, args, opts, fn){ // rationalize options - if(typeof execOpts === 'function') { - fn = execOpts; - execOpts = {}; + if (typeof args === 'function') { + fn = args; + args = []; + opts = {}; + } else if (typeof opts === 'function') { + fn = opts; + opts = {}; } + args = args || []; + + file = shelly(file); // REM : exiftool options http://www.sno.phy.queensu.ca/~phil/exiftool/exiftool_pod.html // -json : ask JSON output - var cmd = command('exiftool -json ?', file); - exec(cmd, execOpts, function(err, str) - { - if(err) { - if(err.message === 'stdout maxBuffer exceeded.') - err = new Error('Metadata too big !'); // convert to a clearer message - return fn(err); - } + var cmdArgs = ['-json', file].concat(args); + var stdout = ''; + var exif = spawn('exiftool', cmdArgs , opts); - var obj = JSON.parse(str)[0]; // so easy + exif.stdout.on('data', function (data) { + stdout += String(data); + }); + + exif.on('error', function (error) { + return fn(error); + }); - fn(null, obj); + exif.on('close', function (code) { + if (code === 0) { + var obj = JSON.parse(stdout); // so easy + return fn(null, obj.length > 1 ? obj : obj[0]); // array if multiple files + } + else { + // http://www.tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF + return fn('Command closed unexpectedly, Exit Status Code: ' + code); + } }); }; diff --git a/package.json b/package.json index 8da1a58..b279545 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "exif2", - "version": "1.1.0", + "version": "1.3.0", "description": "EXIF extraction with exiftool", "keywords": [], "author": "TJ Holowaychuk ", @@ -20,5 +20,15 @@ "scripts": { "test": "mocha test/index.js --reporter spec", "bench": "matcha bench/index.js" - } + }, + "contributors": [ + { + "name": "Yves-Emmanuel Jutard", + "email": "ye.jutard@gmail.com" + }, + { + "name": "Jesús Muñoz Alcántara", + "email": "jmunoza@live.com" + } + ] } diff --git a/test/index.js b/test/index.js index d15f5bb..47431df 100644 --- a/test/index.js +++ b/test/index.js @@ -5,7 +5,7 @@ var chai = require('chai'); var expect = chai.expect; chai.config.includeStack = true; // defaults to false -describe('exif(file, fn)', function(){ +describe('exif(file[, args, opts,] fn)', function(){ it('respond with EXIF json data', function(done){ exif('test/fixtures/forest.jpeg', function(err, data){ @@ -25,24 +25,62 @@ describe('exif(file, fn)', function(){ }); }); - it('handles too big metadata with a clear error message', function(done){ + it('extracts specific EXIF data defined in arguments', function(done){ + exif('test/fixtures/forest.jpeg', ['-d', '\'%r %a, %B %e, %Y\'', '-DateTimeOriginal', + '-S', '-s'], function(err, data) { + if (err) return done(err); + expect(data, 'DateTimeOriginal').to.have.property('DateTimeOriginal', '\'11:36:30 AM Sun, October 7, 2012\''); + done(); + }); + }); + + it('handles too big metadata without buffer errors', function(done){ // real case taken from : // http://fr.phaidon.com/store/photography/le-livre-de-photographies-une-histoire-vol-3-9780714867755/ - exif('test/fixtures/le_livre_de_photographies_vol_III_Phaidon.jpg', function(err){ - expect( err ).to.be.an.instanceof(Error); - expect( err ).to.have.property('message', 'Metadata too big !'); + exif('test/fixtures/le_livre_de_photographies_vol_III_Phaidon.jpg', function(err, data){ + if(err) return done(err); + expect(data).to.have.property('YCbCrSubSampling', 'YCbCr4:4:4 (1 1)'); + done(); + }); + }); + + it('handles all media files contained in the folder at once', function(done){ + exif('test/fixtures/', function(err, data) { + if (err) return done(err); + expect(data[0], 'FileName').to.have.property('FileName', 'forest.jpeg'); + expect(data[1], 'FileName').to.have.property('FileName', 'le_livre_de_photographies_vol_III_Phaidon.jpg'); done(); }); }); - it('allows handling huge metadatas through special options', function(done){ - // we use the special exec option to ease limitations - exif('test/fixtures/le_livre_de_photographies_vol_III_Phaidon.jpg', { - maxBuffer: 1024*1024 - }, + it('handles all media files contained in the path plus arguments', function(done){ + exif('test/fixtures/', ['-common'], function(err, data) { + if (err) return done(err); + expect(data[0], 'Model').to.have.property('Model', 'NIKON D7000'); + expect(data[1], 'ImageSize').to.have.property('ImageSize', '3129x3776'); + done(); + }); + }); + + it('get specified info from all media files contained in the path', function(done){ + exif('test/fixtures/', ['-ExifImageWidth', '-Megapixels'], function(err, data) { + if (err) return done(err); + expect(data[0], 'ExifImageWidth').to.have.property('ExifImageWidth', 1971); + expect(data[0], 'Megapixels').to.have.property('Megapixels', 0.536); + expect(data[0], 'ExifImageHeight').to.not.have.property('ExifImageHeight'); + expect(data[1], 'ExifImageWidth').to.have.property('ExifImageWidth', 3129); + expect(data[1], 'Megapixels').to.have.property('Megapixels', 11.8); + expect(data[1], 'ExifImageHeight').to.not.have.property('ExifImageHeight'); + done(); + }); + }); + + it('handles all media files in path plus arguments plus spawn options', function(done){ + exif('test/fixtures/', ['-common'], { cwd: undefined, env: process.env }, function(err, data) { if (err) return done(err); - expect(data).to.have.property('YCbCrSubSampling', 'YCbCr4:4:4 (1 1)'); + expect(data[0], 'Model').to.have.property('Model', 'NIKON D7000'); + expect(data[1], 'ImageSize').to.have.property('ImageSize', '3129x3776'); done(); }); });