Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 128 additions & 16 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major

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]
})
```

Expand Down Expand Up @@ -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
Expand Down
64 changes: 47 additions & 17 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
};
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "exif2",
"version": "1.1.0",
"version": "1.3.0",
"description": "EXIF extraction with exiftool",
"keywords": [],
"author": "TJ Holowaychuk <tj@vision-media.ca>",
Expand All @@ -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"
}
]
}
60 changes: 49 additions & 11 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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){
Expand All @@ -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();
});
});
Expand Down