├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib └── exiftool.js ├── package.json └── tests ├── resources ├── chvrches.jpg ├── hobbit.mov ├── lauren.png └── working.pdf └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.js] 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_store 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /tests 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_install: 5 | - sudo apt-get update -qq 6 | - sudo apt-get install -qq libimage-exiftool-perl 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Nathan Peck (https://github.com/nathanpeck) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # exiftool [![Build Status](https://travis-ci.org/nathanpeck/exiftool.svg?branch=master)](https://travis-ci.org/nathanpeck/exiftool) 2 | 3 | A node.js wrapper around [exiftool](http://owl.phy.queensu.ca/~phil/exiftool/), a commandline utility that can extract metadata from many different filetypes, including JPEG, PNG, PDF, WMV, MOV. For a full list see the [exiftool list of supported filetypes](http://www.sno.phy.queensu.ca/~phil/exiftool/#supported). 4 | 5 | ## Installation 6 | 7 | To make use of exiftool you will need to download and install the appropriate exiftool package for your system. 8 | 9 | __Mac OS X:__ 10 | 11 | ``` 12 | sudo brew update 13 | sudo brew install exiftool 14 | ``` 15 | 16 | __Ubuntu:__ 17 | 18 | ``` 19 | sudo apt-get update 20 | sudo apt-get install libimage-exiftool-perl 21 | ``` 22 | 23 | For other systems or for information on how to compile exiftool from source refer to the [official documentation for exiftool](http://www.sno.phy.queensu.ca/~phil/exiftool/install.html). 24 | 25 | ## Usage 26 | 27 | ```js 28 | var exif = require('exiftool'); 29 | var fs = require('fs'); 30 | 31 | fs.readFile('./tests/resources/chvrches.jpg', function (err, data) { 32 | if (err) 33 | throw err; 34 | else { 35 | exif.metadata(data, function (err, metadata) { 36 | if (err) 37 | throw err; 38 | else 39 | console.log(metadata); 40 | }); 41 | } 42 | }); 43 | ``` 44 | 45 | ```js 46 | var exif = require('exiftool'); 47 | 48 | exif.metadata('./tests/resources/chvrches.jpg', function (err, metadata) { 49 | if (err) 50 | throw err; 51 | else 52 | console.log(metadata); 53 | }); 54 | ``` 55 | 56 | The properties and contents of the metadata dictionary returned by exiftool will vary widely depending on the filetype but you can expect dictionaries that look similar to this: 57 | 58 | __From a JPG:__ 59 | 60 | ```js 61 | { exiftoolVersionNumber: 9.58, 62 | fileType: 'JPEG', 63 | mimeType: 'image/jpeg', 64 | jfifVersion: 1.01, 65 | resolutionUnit: 'None', 66 | xResolution: 1, 67 | yResolution: 1, 68 | imageWidth: 620, 69 | imageHeight: 413, 70 | encodingProcess: 'Baseline DCT, Huffman coding', 71 | bitsPerSample: 8, 72 | colorComponents: 3, 73 | yCbCrSubSampling: 'YCbCr4:2:0 (2 2)', 74 | imageSize: '620x413' } 75 | ``` 76 | 77 | __From a MOV:__ 78 | 79 | ```js 80 | { exiftoolVersionNumber: 9.58, 81 | fileType: 'MOV', 82 | mimeType: 'video/quicktime', 83 | majorBrand: 'Apple QuickTime (.MOV/QT)', 84 | minorVersion: '2005.3.0', 85 | compatibleBrands: 'qt', 86 | movieHeaderVersion: 0, 87 | createDate: '2012:09:18 17:18:25', 88 | modifyDate: '2012:09:18 17:18:25', 89 | timeScale: 2997, 90 | duration: '0:02:31', 91 | preferredRate: 1, 92 | preferredVolume: '100.00%', 93 | previewTime: '0 s', 94 | previewDuration: '0 s', 95 | posterTime: '0 s', 96 | selectionTime: '0 s', 97 | selectionDuration: '0 s', 98 | currentTime: '0 s', 99 | nextTrackID: 4, 100 | trackHeaderVersion: 0, 101 | trackCreateDate: '2012:09:18 16:24:43', 102 | trackModifyDate: '2012:09:18 17:18:25', 103 | trackID: 1, 104 | trackDuration: '0:02:31', 105 | trackLayer: 0, 106 | trackVolume: '0.00%', 107 | imageWidth: 320, 108 | imageHeight: 136, 109 | cleanApertureDimensions: '320x136', 110 | productionApertureDimensions: '320x136', 111 | encodedPixelsDimensions: '320x136', 112 | graphicsMode: 'ditherCopy', 113 | opColor: '32768 32768 32768', 114 | compressorID: 'avc1', 115 | vendorID: 'Apple', 116 | sourceImageWidth: 320, 117 | sourceImageHeight: 136, 118 | xResolution: 72, 119 | yResolution: 72, 120 | compressorName: 'H.264', 121 | bitDepth: 24, 122 | videoFrameRate: 23.976, 123 | audioFormat: 'mp4a', 124 | audioChannels: 2, 125 | audioBitsPerSample: 16, 126 | audioSampleRate: 44100, 127 | purchaseFileFormat: 'mp4a', 128 | matrixStructure: '1 0 0 0 1 0 0 0 1', 129 | mediaHeaderVersion: 0, 130 | mediaCreateDate: '2012:09:18 17:18:25', 131 | mediaModifyDate: '2012:09:18 17:18:25', 132 | mediaTimeScale: 2997, 133 | mediaDuration: '0:02:31', 134 | genMediaVersion: 0, 135 | genFlags: '0 0 0', 136 | genGraphicsMode: 'ditherCopy', 137 | genOpColor: '32768 32768 32768', 138 | genBalance: 0, 139 | textFont: 'Unknown (1024)', 140 | textFace: 'Plain', 141 | textSize: 12, 142 | textColor: '0 0 0', 143 | backgroundColor: '65535 65535 65535', 144 | fontName: 'Lucida Grande', 145 | handlerClass: 'Data Handler', 146 | handlerVendorID: 'Apple', 147 | handlerDescription: 'Apple Alias Data Handler', 148 | otherFormat: 'tmcd', 149 | handlerType: 'Metadata Tags', 150 | audioGain: 1, 151 | trebel: 0, 152 | bass: 0, 153 | balance: 0, 154 | pitchShift: 0, 155 | mute: 'Off', 156 | brightness: 0, 157 | color: 1, 158 | tint: 0, 159 | contrast: 1, 160 | playerVersion: '7.6 (7.6)', 161 | version: '7.6.0 (1290) 0x7608000 (Mac OS X, 10.5.6, 9G71)', 162 | 'comment(err)': 'Encoded and delivered by apple.com/trailers/', 163 | 'copyright(err)': '� 2012 Warner Bros. Pictures. All Rights Reserved', 164 | 'userDataDes(err)': 'In theaters 2012', 165 | windowLocation: '45 21', 166 | playSelection: 0, 167 | playAllFrames: 0, 168 | movieDataSize: 8636129, 169 | movieDataOffset: 98160, 170 | comment: 'Encoded and delivered by apple.com/trailers/', 171 | copyright: '� 2012 Warner Bros. Pictures. All Rights Reserved', 172 | userDataDes: 'In theaters 2012', 173 | avgBitrate: '457 kbps', 174 | imageSize: '320x136', 175 | rotation: 0 } 176 | ``` 177 | 178 | ## Filtering metadata 179 | 180 | You can also provide an optional list of extra parameters to pass into exiftool, if you want it to return only specific metadata keys: 181 | 182 | ```js 183 | var exif = require('exiftool'); 184 | var fs = require('fs'); 185 | 186 | fs.readFile('./tests/resources/chvrches.jpg', function (err, data) { 187 | if (err) 188 | throw err; 189 | else { 190 | exif.metadata(data, ['-imageWidth', '-imageHeight'], function (err, metadata) { 191 | if (err) 192 | throw err; 193 | else 194 | console.log(metadata); 195 | }); 196 | } 197 | }); 198 | ``` 199 | -------------------------------------------------------------------------------- /lib/exiftool.js: -------------------------------------------------------------------------------- 1 | var ChildProcess = require('child_process'); 2 | 3 | // Accepts the raw binary content of a file or a path and returns the meta data of the file. 4 | exports.metadata = function (source, tags, doneGettingMetadata) { 5 | // tags is an optional parameter, hence it may be a callback instead. 6 | if (typeof tags == 'function') { 7 | doneGettingMetadata = tags; 8 | tags = []; 9 | } 10 | 11 | var usingBinaryData = false; 12 | if (typeof source === 'string') { 13 | tags.push(source); 14 | } else { 15 | // The dash specifies to read data from stdin. 16 | tags.push("-"); 17 | usingBinaryData = true; 18 | } 19 | 20 | var exif = ChildProcess.spawn('exiftool', tags); 21 | 22 | //Check for error because of the child process not being found / launched. 23 | exif.on('error', function (err) { 24 | doneGettingMetadata('Fatal Error: Unable to load exiftool. ' + err); 25 | }); 26 | 27 | // Read the binary data back 28 | var response = ''; 29 | var errorMessage = ''; 30 | exif.stdout.on("data", function (data) { 31 | response += data; 32 | }); 33 | 34 | // Read an error response back and deal with it. 35 | exif.stderr.on("data", function (data) { 36 | errorMessage += data.toString(); 37 | }); 38 | 39 | // Handle the response to the callback to hand the metadata back. 40 | exif.on("close", function () { 41 | if (errorMessage) 42 | { 43 | doneGettingMetadata(errorMessage); 44 | } 45 | else 46 | { 47 | // Split the response into lines. 48 | response = response.split("\n"); 49 | 50 | //For each line of the response extract the meta data into a nice associative array 51 | var metaData = []; 52 | response.forEach(function (responseLine) { 53 | var pieces = responseLine.split(": "); 54 | //Is this a line with a meta data pair on it? 55 | if (pieces.length == 2) 56 | { 57 | //Turn the plain text data key into a camel case key. 58 | var key = pieces[0].trim().split(' ').map( 59 | function (tokenInKey, tokenNumber) { 60 | if (tokenNumber === 0) 61 | return tokenInKey.toLowerCase(); 62 | else 63 | return tokenInKey[0].toUpperCase() + tokenInKey.slice(1); 64 | } 65 | ).join(''); 66 | //Trim the value associated with the key to make it nice. 67 | var value = pieces[1].trim(); 68 | if (!isNaN(value)) 69 | { 70 | value = parseFloat(value, 10); 71 | } 72 | metaData[key] = value; 73 | } 74 | }); 75 | doneGettingMetadata(null, metaData); 76 | } 77 | }); 78 | 79 | if (usingBinaryData) 80 | { 81 | //Give the source binary data to the process which will extract the meta data. 82 | exif.stdin.write(source); 83 | exif.stdin.end(); 84 | } 85 | 86 | return exif; 87 | }; 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exiftool", 3 | "version": "0.0.4", 4 | "description": "Metadata extraction from numerous filetypes including JPEG, PNG, PDF, MOV, WMV, MP3, MP4, and others.", 5 | "main": "./lib/exiftool", 6 | "author": { 7 | "name": "Nathan Peck", 8 | "email": "nathan@storydesk.com" 9 | }, 10 | "devDependencies": { 11 | "mocha": "1.17.1", 12 | "chai": "1.8.1" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/nathanpeck/exiftool.git" 17 | }, 18 | "keywords": [ 19 | "exif", 20 | "file", 21 | "metadata", 22 | "binary", 23 | "image", 24 | "video", 25 | "PNG", 26 | "PDF" 27 | ], 28 | "licenses": [ 29 | { 30 | "type": "MIT", 31 | "url": "https://raw.githubusercontent.com/nathanpeck/exiftool/master/LICENSE" 32 | } 33 | ], 34 | "readmeFilename": "README.md", 35 | "scripts": { 36 | "test": "./node_modules/.bin/mocha -R spec -s 100 ./tests/test.js" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/resources/chvrches.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathanpeck/exiftool/c78da67cf924402c8a1c162b078bde7d4eb4b917/tests/resources/chvrches.jpg -------------------------------------------------------------------------------- /tests/resources/hobbit.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathanpeck/exiftool/c78da67cf924402c8a1c162b078bde7d4eb4b917/tests/resources/hobbit.mov -------------------------------------------------------------------------------- /tests/resources/lauren.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathanpeck/exiftool/c78da67cf924402c8a1c162b078bde7d4eb4b917/tests/resources/lauren.png -------------------------------------------------------------------------------- /tests/resources/working.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathanpeck/exiftool/c78da67cf924402c8a1c162b078bde7d4eb4b917/tests/resources/working.pdf -------------------------------------------------------------------------------- /tests/test.js: -------------------------------------------------------------------------------- 1 | var exif = require(process.cwd() + '/lib/exiftool.js'), 2 | expect = require('chai').expect, 3 | fs = require('fs'); 4 | 5 | describe('Giving JPG image binary data to exiftool', function () { 6 | var response; 7 | 8 | before(function (done) { 9 | fs.readFile(process.cwd() + '/tests/resources/chvrches.jpg', function (err, data) { 10 | if (err) 11 | throw err; 12 | else 13 | exif.metadata(data, function (err, metadata) { 14 | if (err) 15 | throw err; 16 | else 17 | { 18 | console.log(metadata); 19 | response = metadata; 20 | done(); 21 | } 22 | }); 23 | }); 24 | }); 25 | 26 | it('metadata return should be image metadata', function () { 27 | expect(response).to.have.property('fileType'); 28 | expect(response.fileType).to.equal('JPEG'); 29 | }); 30 | }); 31 | 32 | describe('Giving PNG image binary data to exiftool', function () { 33 | var response; 34 | 35 | before(function (done) { 36 | fs.readFile(process.cwd() + '/tests/resources/lauren.png', function (err, data) { 37 | if (err) 38 | throw err; 39 | else 40 | exif.metadata(data, function (err, metadata) { 41 | if (err) 42 | throw err; 43 | else 44 | { 45 | console.log(metadata); 46 | response = metadata; 47 | done(); 48 | } 49 | }); 50 | }); 51 | }); 52 | 53 | it('metadata return should be image metadata', function () { 54 | expect(response).to.have.property('fileType'); 55 | expect(response.fileType).to.equal('PNG'); 56 | }); 57 | }); 58 | 59 | describe('Giving PDF binary data to exiftool', function () { 60 | var response; 61 | 62 | before(function (done) { 63 | fs.readFile(process.cwd() + '/tests/resources/working.pdf', function (err, data) { 64 | if (err) 65 | throw err; 66 | else 67 | exif.metadata(data, function (err, metadata) { 68 | if (err) 69 | throw err; 70 | else 71 | { 72 | console.log(metadata); 73 | response = metadata; 74 | done(); 75 | } 76 | }); 77 | }); 78 | }); 79 | 80 | it('metadata return should be pdf metadata', function () { 81 | expect(response).to.have.property('fileType'); 82 | expect(response.fileType).to.equal('PDF'); 83 | }); 84 | }); 85 | 86 | describe('Giving MOV video binary data to exiftool', function () { 87 | var response; 88 | 89 | before(function (done) { 90 | fs.readFile(process.cwd() + '/tests/resources/hobbit.mov', function (err, data) { 91 | if (err) 92 | throw err; 93 | else 94 | exif.metadata(data, function (err, metadata) { 95 | if (err) 96 | throw err; 97 | else 98 | { 99 | console.log(metadata); 100 | response = metadata; 101 | done(); 102 | } 103 | }); 104 | }); 105 | }); 106 | 107 | it('metadata return should be pdf metadata', function () { 108 | expect(response).to.have.property('fileType'); 109 | expect(response.fileType).to.equal('MOV'); 110 | }); 111 | }); 112 | 113 | describe('Doing exif.metadata() on a JPG image with tags input -imageWidth and -imageHeight', function () { 114 | var response; 115 | 116 | before(function (done) { 117 | fs.readFile(process.cwd() + '/tests/resources/chvrches.jpg', function (err, data) { 118 | if (err) 119 | throw err; 120 | else 121 | exif.metadata(data, ['-imageWidth', '-imageHeight'],function (err, metadata) { 122 | if (err) 123 | throw err; 124 | else 125 | { 126 | console.log(metadata); 127 | response = metadata; 128 | done(); 129 | } 130 | }); 131 | }); 132 | }); 133 | 134 | it('matadata return should be the values of tags', function () { 135 | expect(response).to.have.property('imageWidth'); 136 | expect(response).to.have.property('imageHeight'); 137 | }); 138 | }); 139 | --------------------------------------------------------------------------------