├── LICENSE ├── README.md ├── bower.json ├── demo.html ├── demo.js ├── demo.py ├── jpegmeta.js └── package.json /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2013 Ben Leslie 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 | = JPEG Meta Data in JavaScript = 2 | 3 | To try it out run `python demo.py` and go to http://localhost:8000 4 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsjpegmeta", 3 | "description": "Get exif info from Jpeg files", 4 | "version": "1.0.1", 5 | "main": "jpegmeta.js", 6 | "ignore": [ 7 | "**/.*", 8 | "demo.html", 9 | "demo.js", 10 | "demo.py", 11 | "package.json" 12 | ], 13 | "dependencies": { 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | JsJpegMeta Demo 7 | 8 | 9 | 10 | 11 | 12 |

JsJpegMeta Demo

13 |
14 |

Choose JPEG file:

15 |
16 | Image preview 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | /* Imports */ 3 | var $j = this.JpegMeta.JpegFile; 4 | 5 | /* Implementation */ 6 | function $(x) { 7 | return document.getElementById(x); 8 | } 9 | 10 | function dragEnterHandler(e) { 11 | e.preventDefault(); 12 | } 13 | 14 | function dragOverHandler(e) { 15 | e.preventDefault(); 16 | } 17 | 18 | function dropHandler(e) { 19 | e.preventDefault(); 20 | loadFiles(e.dataTransfer.files); 21 | } 22 | 23 | function strComp(a, b) { 24 | return (a > b) ? 1 : (a == b) ? 0 : -1; 25 | } 26 | 27 | function loadFiles(files) { 28 | var dataurl_reader = new FileReader(); 29 | 30 | function display(data, filename) { 31 | var jpeg = new $j(data, filename); 32 | var groups = new Array; 33 | var props; 34 | var group; 35 | var prop; 36 | $("status").innerHTML += "JPEG File " + jpeg + "
"; 37 | 38 | if (jpeg.gps && jpeg.gps.longitude) { 39 | $("status").innerHTML += "Locate on map (opens a new window)
"; 40 | } 41 | 42 | for (group in jpeg.metaGroups) { 43 | if (jpeg.metaGroups.hasOwnProperty(group)) { 44 | groups.push(jpeg.metaGroups[group]); 45 | } 46 | } 47 | 48 | groups.sort(function (a, b) { 49 | if (a.description == "General") { 50 | return -1; 51 | } else if (b.description == "General") { 52 | return 1; 53 | } else { 54 | return strComp(a.description, b.description); 55 | } 56 | }); 57 | 58 | for (var i = 0; i < groups.length; i++) { 59 | group = groups[i]; 60 | props = new Array(); 61 | $("status").innerHTML += "" + group.description + "
"; 62 | for (prop in group.metaProps) { 63 | if (group.metaProps.hasOwnProperty(prop)) { 64 | props.push(group.metaProps[prop]); 65 | } 66 | } 67 | props.sort(function (a, b) { return strComp(a.description, b.description); }); 68 | for (var j = 0; j < props.length; j++) { 69 | prop = props[j]; 70 | $("status").innerHTML += "" + prop.description + ": " + prop.value + "
"; 71 | } 72 | } 73 | } 74 | 75 | dataurl_reader.onloadend = function() { 76 | $("img").src = this.result; 77 | display(atob(this.result.replace(/^.*?,/,'')), files[0]); 78 | } 79 | 80 | $("status").innerHTML = ""; 81 | $("img").src = ""; 82 | dataurl_reader.readAsDataURL(files[0]); 83 | $("form").reset(); 84 | } 85 | 86 | window.onload = function() { 87 | var drop_el = $("dropbox"); 88 | var file_el = $("fileWidget"); 89 | drop_el.addEventListener("dragenter", dragEnterHandler, false); 90 | drop_el.addEventListener("dragover", dragOverHandler, true); 91 | drop_el.addEventListener("drop", dropHandler, true); 92 | file_el.addEventListener("change", function() { loadFiles(this.files); }, true); 93 | } 94 | /* No exports */ 95 | })(); 96 | -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | import SimpleHTTPServer 2 | import SocketServer 3 | 4 | # minimal web server. serves files relative to the 5 | # current directory. 6 | def main(): 7 | import sys 8 | try: 9 | port = int(sys.argv[1]) 10 | except: 11 | port = 8000 12 | 13 | Handler = SimpleHTTPServer.SimpleHTTPRequestHandler 14 | httpd = SocketServer.TCPServer(("", port), Handler) 15 | 16 | print "serving at port", port 17 | httpd.serve_forever() 18 | 19 | if __name__ == "__main__": 20 | main() 21 | -------------------------------------------------------------------------------- /jpegmeta.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2009 Ben Leslie 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | /* 24 | This JavaScript library is used to parse meta-data from files 25 | with mime-type image/jpeg. 26 | 27 | Include it with something like: 28 | 29 | 30 | 31 | This adds a single 'module' object called 'JpegMeta' to the global 32 | namespace. 33 | 34 | Public Functions 35 | ---------------- 36 | JpegMeta.parseNum - parse unsigned integers from binary data 37 | JpegMeta.parseSnum - parse signed integers from binary data 38 | 39 | Public Classes 40 | -------------- 41 | JpegMeta.Rational - A rational number class 42 | JpegMeta.JfifSegment 43 | JpegMeta.ExifSegment 44 | JpegMeta.JpegFile - Primary class for Javascript parsing 45 | */ 46 | 47 | if (this.JpegMeta) { 48 | throw Error("Library included multiple times"); 49 | } 50 | 51 | var JpegMeta = {}; 52 | 53 | JpegMeta.stringIsClean = function stringIsClean(str) { 54 | for (var i = 0; i < str.length; i++) { 55 | if (str.charCodeAt(i) < 0x20) { 56 | return false; 57 | } 58 | } 59 | return true; 60 | } 61 | 62 | /* 63 | parse an unsigned number of size bytes at offset in some binary string data. 64 | If endian 65 | is "<" parse the data as little endian, if endian 66 | is ">" parse as big-endian. 67 | */ 68 | JpegMeta.parseNum = function parseNum(endian, data, offset, size) { 69 | var i; 70 | var ret; 71 | var big_endian = (endian === ">"); 72 | if (offset === undefined) offset = 0; 73 | if (size === undefined) size = data.length - offset; 74 | for (big_endian ? i = offset : i = offset + size - 1; 75 | big_endian ? i < offset + size : i >= offset; 76 | big_endian ? i++ : i--) { 77 | ret <<= 8; 78 | ret += data.charCodeAt(i); 79 | } 80 | return ret; 81 | }; 82 | 83 | /* 84 | parse an signed number of size bytes at offset in some binary string data. 85 | If endian 86 | is "<" parse the data as little endian, if endian 87 | is ">" parse as big-endian. 88 | */ 89 | JpegMeta.parseSnum = function parseSnum(endian, data, offset, size) { 90 | var i; 91 | var ret; 92 | var neg; 93 | var big_endian = (endian === ">"); 94 | if (offset === undefined) offset = 0; 95 | if (size === undefined) size = data.length - offset; 96 | for (big_endian ? i = offset : i = offset + size - 1; 97 | big_endian ? i < offset + size : i >= offset; 98 | big_endian ? i++ : i--) { 99 | if (neg === undefined) { 100 | /* Negative if top bit is set */ 101 | neg = (data.charCodeAt(i) & 0x80) === 0x80; 102 | } 103 | ret <<= 8; 104 | /* If it is negative we invert the bits */ 105 | ret += neg ? ~data.charCodeAt(i) & 0xff: data.charCodeAt(i); 106 | } 107 | if (neg) { 108 | /* If it is negative we do two's complement */ 109 | ret += 1; 110 | ret *= -1; 111 | } 112 | return ret; 113 | }; 114 | 115 | /* Rational number class */ 116 | JpegMeta.Rational = function Rational(num, den) 117 | { 118 | this.num = num; 119 | this.den = den || 1; 120 | return this; 121 | }; 122 | 123 | /* Rational number methods */ 124 | JpegMeta.Rational.prototype.toString = function toString() { 125 | if (this.num === 0) { 126 | return "" + this.num; 127 | } 128 | if (this.den === 1) { 129 | return "" + this.num; 130 | } 131 | if (this.num === 1) { 132 | return this.num + " / " + this.den; 133 | } 134 | return this.num / this.den; // + "/" + this.den; 135 | }; 136 | 137 | JpegMeta.Rational.prototype.asFloat = function asFloat() { 138 | return this.num / this.den; 139 | }; 140 | 141 | 142 | /* MetaGroup class */ 143 | JpegMeta.MetaGroup = function MetaGroup(fieldName, description) { 144 | this.fieldName = fieldName; 145 | this.description = description; 146 | this.metaProps = {}; 147 | return this; 148 | }; 149 | 150 | JpegMeta.MetaGroup.prototype._addProperty = function _addProperty(fieldName, description, value) { 151 | var property = new JpegMeta.MetaProp(fieldName, description, value); 152 | if (typeof this[property.fieldName] == 'undefined') { 153 | this[property.fieldName] = property; 154 | this.metaProps[property.fieldName] = property; 155 | } else { 156 | var currentProperty = this[property.fieldName]; 157 | var currentMetaProperty = this.metaProps[property.fieldName]; 158 | if (! Array.isArray(currentProperty)) { 159 | this[property.fieldName] = new Array(); 160 | this[property.fieldName].push(currentProperty); 161 | this[property.fieldName].push(property); 162 | this.metaProps[property.fieldName] = new Array(); 163 | this.metaProps[property.fieldName].push(currentProperty); 164 | this.metaProps[property.fieldName].push(property); 165 | } else { 166 | currentProperty.push(property); 167 | currentMetaProperty.push(property); 168 | } 169 | } 170 | }; 171 | 172 | JpegMeta.MetaGroup.prototype.toString = function toString() { 173 | return "[MetaGroup " + this.description + "]"; 174 | }; 175 | 176 | 177 | /* MetaProp class */ 178 | JpegMeta.MetaProp = function MetaProp(fieldName, description, value) { 179 | this.fieldName = fieldName; 180 | this.description = description; 181 | this.value = value; 182 | return this; 183 | }; 184 | 185 | JpegMeta.MetaProp.prototype.toString = function toString() { 186 | return "" + this.value; 187 | }; 188 | 189 | 190 | 191 | /* JpegFile class */ 192 | this.JpegMeta.JpegFile = function JpegFile(binary_data, filename) { 193 | /* Change this to EOI if we want to parse. */ 194 | var break_segment = this._SOS; 195 | 196 | this.metaGroups = {}; 197 | this._binary_data = binary_data; 198 | this.filename = filename; 199 | 200 | /* Go through and parse. */ 201 | var pos = 0; 202 | var pos_start_of_segment = 0; 203 | var delim; 204 | var mark; 205 | var _mark; 206 | var segsize; 207 | var headersize; 208 | var mark_code; 209 | var mark_fn; 210 | 211 | /* Check to see if this looks like a JPEG file */ 212 | if (this._binary_data.slice(0, 2) !== this._SOI_MARKER) { 213 | throw new Error("Doesn't look like a JPEG file. First two bytes are " + 214 | this._binary_data.charCodeAt(0) + "," + 215 | this._binary_data.charCodeAt(1) + "."); 216 | } 217 | 218 | pos += 2; 219 | 220 | while (pos < this._binary_data.length) { 221 | delim = this._binary_data.charCodeAt(pos++); 222 | mark = this._binary_data.charCodeAt(pos++); 223 | 224 | pos_start_of_segment = pos; 225 | 226 | if (delim != this._DELIM) { 227 | break; 228 | } 229 | 230 | if (mark === break_segment) { 231 | break; 232 | } 233 | 234 | headersize = JpegMeta.parseNum(">", this._binary_data, pos, 2); 235 | 236 | /* Find the end */ 237 | pos += headersize; 238 | while (pos < this._binary_data.length) { 239 | delim = this._binary_data.charCodeAt(pos++); 240 | if (delim == this._DELIM) { 241 | _mark = this._binary_data.charCodeAt(pos++); 242 | if (_mark != 0x0) { 243 | pos -= 2; 244 | break; 245 | } 246 | } 247 | } 248 | 249 | segsize = pos - pos_start_of_segment; 250 | 251 | if (this._markers[mark]) { 252 | mark_code = this._markers[mark][0]; 253 | mark_fn = this._markers[mark][1]; 254 | } else { 255 | mark_code = "UNKN"; 256 | mark_fn = undefined; 257 | } 258 | 259 | if (mark_fn) { 260 | this[mark_fn](mark, pos_start_of_segment + 2, segsize - 2); 261 | } 262 | 263 | } 264 | 265 | if (this.general === undefined) { 266 | throw Error("Invalid JPEG file."); 267 | } 268 | 269 | return this; 270 | }; 271 | 272 | this.JpegMeta.JpegFile.prototype.toString = function () { 273 | return "[JpegFile " + this.filename + " " + 274 | this.general.type + " " + 275 | this.general.pixelWidth + "x" + 276 | this.general.pixelHeight + 277 | " Depth: " + this.general.depth + "]"; 278 | }; 279 | 280 | /* Some useful constants */ 281 | this.JpegMeta.JpegFile.prototype._SOI_MARKER = '\xff\xd8'; 282 | this.JpegMeta.JpegFile.prototype._DELIM = 0xff; 283 | this.JpegMeta.JpegFile.prototype._EOI = 0xd9; 284 | this.JpegMeta.JpegFile.prototype._SOS = 0xda; 285 | 286 | this.JpegMeta.JpegFile.prototype._sofHandler = function _sofHandler (mark, pos) { 287 | if (this.general !== undefined) { 288 | throw Error("Unexpected multiple-frame image"); 289 | } 290 | 291 | this._addMetaGroup("general", "General"); 292 | this.general._addProperty("depth", "Depth", JpegMeta.parseNum(">", this._binary_data, pos, 1)); 293 | this.general._addProperty("pixelHeight", "Pixel Height", JpegMeta.parseNum(">", this._binary_data, pos + 1, 2)); 294 | this.general._addProperty("pixelWidth", "Pixel Width",JpegMeta.parseNum(">", this._binary_data, pos + 3, 2)); 295 | this.general._addProperty("type", "Type", this._markers[mark][2]); 296 | }; 297 | 298 | this.JpegMeta.JpegFile.prototype._commentHandler = function _commentHandler (mark, pos, size) { 299 | 300 | var _pos, result; 301 | pos++; 302 | size--; 303 | _pos = pos; 304 | result = ""; 305 | 306 | while(_pos < pos+size) { 307 | result += String.fromCharCode(this._binary_data.charCodeAt(_pos)); 308 | _pos++; 309 | } 310 | 311 | this._addMetaGroup("comment", "Comment"); 312 | this.comment._addProperty("comment", "Comment", result); 313 | }; 314 | 315 | 316 | /* JFIF idents */ 317 | this.JpegMeta.JpegFile.prototype._JFIF_IDENT = "JFIF\x00"; 318 | this.JpegMeta.JpegFile.prototype._JFXX_IDENT = "JFXX\x00"; 319 | 320 | /* EXIF idents */ 321 | this.JpegMeta.JpegFile.prototype._EXIF_IDENT = "Exif\x00"; 322 | 323 | /* TIFF types */ 324 | this.JpegMeta.JpegFile.prototype._types = { 325 | /* The format is identifier : ["type name", type_size_in_bytes ] */ 326 | 1 : ["BYTE", 1], 327 | 2 : ["ASCII", 1], 328 | 3 : ["SHORT", 2], 329 | 4 : ["LONG", 4], 330 | 5 : ["RATIONAL", 8], 331 | 6 : ["SBYTE", 1], 332 | 7 : ["UNDEFINED", 1], 333 | 8 : ["SSHORT", 2], 334 | 9 : ["SLONG", 4], 335 | 10 : ["SRATIONAL", 8], 336 | 11 : ["FLOAT", 4], 337 | 12 : ["DOUBLE", 8] 338 | }; 339 | 340 | this.JpegMeta.JpegFile.prototype._tifftags = { 341 | /* A. Tags relating to image data structure */ 342 | 256 : ["Image width", "ImageWidth"], 343 | 257 : ["Image height", "ImageLength"], 344 | 258 : ["Number of bits per component", "BitsPerSample"], 345 | 259 : ["Compression scheme", "Compression", 346 | {1 : "uncompressed", 6 : "JPEG compression" }], 347 | 262 : ["Pixel composition", "PhotmetricInerpretation", 348 | {2 : "RGB", 6 : "YCbCr"}], 349 | 274 : ["Orientation of image", "Orientation", 350 | /* FIXME: Check the mirror-image / reverse encoding and rotation */ 351 | {1 : "Normal", 2 : "Reverse?", 352 | 3 : "Upside-down", 4 : "Upside-down Reverse", 353 | 5 : "90 degree CW", 6 : "90 degree CW reverse", 354 | 7 : "90 degree CCW", 8 : "90 degree CCW reverse"}], 355 | 277 : ["Number of components", "SamplesPerPixel"], 356 | 284 : ["Image data arrangement", "PlanarConfiguration", 357 | {1 : "chunky format", 2 : "planar format"}], 358 | 530 : ["Subsampling ratio of Y to C", "YCbCrSubSampling"], 359 | 531 : ["Y and C positioning", "YCbCrPositioning", 360 | {1 : "centered", 2 : "co-sited"}], 361 | 282 : ["X Resolution", "XResolution"], 362 | 283 : ["Y Resolution", "YResolution"], 363 | 296 : ["Resolution Unit", "ResolutionUnit", 364 | {2 : "inches", 3 : "centimeters"}], 365 | /* B. Tags realting to recording offset */ 366 | 273 : ["Image data location", "StripOffsets"], 367 | 278 : ["Number of rows per strip", "RowsPerStrip"], 368 | 279 : ["Bytes per compressed strip", "StripByteCounts"], 369 | 513 : ["Offset to JPEG SOI", "JPEGInterchangeFormat"], 370 | 514 : ["Bytes of JPEG Data", "JPEGInterchangeFormatLength"], 371 | /* C. Tags relating to image data characteristics */ 372 | 301 : ["Transfer function", "TransferFunction"], 373 | 318 : ["White point chromaticity", "WhitePoint"], 374 | 319 : ["Chromaticities of primaries", "PrimaryChromaticities"], 375 | 529 : ["Color space transformation matrix coefficients", "YCbCrCoefficients"], 376 | 532 : ["Pair of black and white reference values", "ReferenceBlackWhite"], 377 | /* D. Other tags */ 378 | 306 : ["Date and time", "DateTime"], 379 | 270 : ["Image title", "ImageDescription"], 380 | 271 : ["Make", "Make"], 381 | 272 : ["Model", "Model"], 382 | 305 : ["Software", "Software"], 383 | 315 : ["Person who created the image", "Artist"], 384 | 316 : ["Host Computer", "HostComputer"], 385 | 33432 : ["Copyright holder", "Copyright"], 386 | 387 | 34665 : ["Exif tag", "ExifIfdPointer"], 388 | 34853 : ["GPS tag", "GPSInfoIfdPointer"] 389 | }; 390 | 391 | this.JpegMeta.JpegFile.prototype._exiftags = { 392 | /* Tag Support Levels (2) - 0th IFX Exif Private Tags */ 393 | /* A. Tags Relating to Version */ 394 | 36864 : ["Exif Version", "ExifVersion"], 395 | 40960 : ["FlashPix Version", "FlashpixVersion"], 396 | 397 | /* B. Tag Relating to Image Data Characteristics */ 398 | 40961 : ["Color Space", "ColorSpace"], 399 | 400 | /* C. Tags Relating to Image Configuration */ 401 | 37121 : ["Meaning of each component", "ComponentsConfiguration"], 402 | 37122 : ["Compressed Bits Per Pixel", "CompressedBitsPerPixel"], 403 | 40962 : ["Pixel X Dimension", "PixelXDimension"], 404 | 40963 : ["Pixel Y Dimension", "PixelYDimension"], 405 | 406 | /* D. Tags Relating to User Information */ 407 | 37500 : ["Manufacturer notes", "MakerNote"], 408 | 37510 : ["User comments", "UserComment"], 409 | 410 | /* E. Tag Relating to Related File Information */ 411 | 40964 : ["Related audio file", "RelatedSoundFile"], 412 | 413 | /* F. Tags Relating to Date and Time */ 414 | 36867 : ["Date Time Original", "DateTimeOriginal"], 415 | 36868 : ["Date Time Digitized", "DateTimeDigitized"], 416 | 37520 : ["DateTime subseconds", "SubSecTime"], 417 | 37521 : ["DateTimeOriginal subseconds", "SubSecTimeOriginal"], 418 | 37522 : ["DateTimeDigitized subseconds", "SubSecTimeDigitized"], 419 | 420 | /* G. Tags Relating to Picture-Taking Conditions */ 421 | 33434 : ["Exposure time", "ExposureTime"], 422 | 33437 : ["FNumber", "FNumber"], 423 | 34850 : ["Exposure program", "ExposureProgram"], 424 | 34852 : ["Spectral sensitivity", "SpectralSensitivity"], 425 | 34855 : ["ISO Speed Ratings", "ISOSpeedRatings"], 426 | 34856 : ["Optoelectric coefficient", "OECF"], 427 | 37377 : ["Shutter Speed", "ShutterSpeedValue"], 428 | 37378 : ["Aperture Value", "ApertureValue"], 429 | 37379 : ["Brightness", "BrightnessValue"], 430 | 37380 : ["Exposure Bias Value", "ExposureBiasValue"], 431 | 37381 : ["Max Aperture Value", "MaxApertureValue"], 432 | 37382 : ["Subject Distance", "SubjectDistance"], 433 | 37383 : ["Metering Mode", "MeteringMode"], 434 | 37384 : ["Light Source", "LightSource"], 435 | 37385 : ["Flash", "Flash"], 436 | 37386 : ["Focal Length", "FocalLength"], 437 | 37396 : ["Subject Area", "SubjectArea"], 438 | 41483 : ["Flash Energy", "FlashEnergy"], 439 | 41484 : ["Spatial Frequency Response", "SpatialFrequencyResponse"], 440 | 41486 : ["Focal Plane X Resolution", "FocalPlaneXResolution"], 441 | 41487 : ["Focal Plane Y Resolution", "FocalPlaneYResolution"], 442 | 41488 : ["Focal Plane Resolution Unit", "FocalPlaneResolutionUnit"], 443 | 41492 : ["Subject Location", "SubjectLocation"], 444 | 41493 : ["Exposure Index", "ExposureIndex"], 445 | 41495 : ["Sensing Method", "SensingMethod"], 446 | 41728 : ["File Source", "FileSource"], 447 | 41729 : ["Scene Type", "SceneType"], 448 | 41730 : ["CFA Pattern", "CFAPattern"], 449 | 41985 : ["Custom Rendered", "CustomRendered"], 450 | 41986 : ["Exposure Mode", "Exposure Mode"], 451 | 41987 : ["White Balance", "WhiteBalance"], 452 | 41988 : ["Digital Zoom Ratio", "DigitalZoomRatio"], 453 | 41989 : ["Focal length in 35 mm film", "FocalLengthIn35mmFilm"], 454 | 41990 : ["Scene Capture Type", "SceneCaptureType"], 455 | 41991 : ["Gain Control", "GainControl"], 456 | 41992 : ["Contrast", "Contrast"], 457 | 41993 : ["Saturation", "Saturation"], 458 | 41994 : ["Sharpness", "Sharpness"], 459 | 41995 : ["Device settings description", "DeviceSettingDescription"], 460 | 41996 : ["Subject distance range", "SubjectDistanceRange"], 461 | 462 | /* H. Other Tags */ 463 | 42016 : ["Unique image ID", "ImageUniqueID"], 464 | 465 | 40965 : ["Interoperability tag", "InteroperabilityIFDPointer"] 466 | }; 467 | 468 | this.JpegMeta.JpegFile.prototype._gpstags = { 469 | /* A. Tags Relating to GPS */ 470 | 0 : ["GPS tag version", "GPSVersionID"], 471 | 1 : ["North or South Latitude", "GPSLatitudeRef"], 472 | 2 : ["Latitude", "GPSLatitude"], 473 | 3 : ["East or West Longitude", "GPSLongitudeRef"], 474 | 4 : ["Longitude", "GPSLongitude"], 475 | 5 : ["Altitude reference", "GPSAltitudeRef"], 476 | 6 : ["Altitude", "GPSAltitude"], 477 | 7 : ["GPS time (atomic clock)", "GPSTimeStamp"], 478 | 8 : ["GPS satellites usedd for measurement", "GPSSatellites"], 479 | 9 : ["GPS receiver status", "GPSStatus"], 480 | 10 : ["GPS mesaurement mode", "GPSMeasureMode"], 481 | 11 : ["Measurement precision", "GPSDOP"], 482 | 12 : ["Speed unit", "GPSSpeedRef"], 483 | 13 : ["Speed of GPS receiver", "GPSSpeed"], 484 | 14 : ["Reference for direction of movement", "GPSTrackRef"], 485 | 15 : ["Direction of movement", "GPSTrack"], 486 | 16 : ["Reference for direction of image", "GPSImgDirectionRef"], 487 | 17 : ["Direction of image", "GPSImgDirection"], 488 | 18 : ["Geodetic survey data used", "GPSMapDatum"], 489 | 19 : ["Reference for latitude of destination", "GPSDestLatitudeRef"], 490 | 20 : ["Latitude of destination", "GPSDestLatitude"], 491 | 21 : ["Reference for longitude of destination", "GPSDestLongitudeRef"], 492 | 22 : ["Longitude of destination", "GPSDestLongitude"], 493 | 23 : ["Reference for bearing of destination", "GPSDestBearingRef"], 494 | 24 : ["Bearing of destination", "GPSDestBearing"], 495 | 25 : ["Reference for distance to destination", "GPSDestDistanceRef"], 496 | 26 : ["Distance to destination", "GPSDestDistance"], 497 | 27 : ["Name of GPS processing method", "GPSProcessingMethod"], 498 | 28 : ["Name of GPS area", "GPSAreaInformation"], 499 | 29 : ["GPS Date", "GPSDateStamp"], 500 | 30 : ["GPS differential correction", "GPSDifferential"] 501 | }; 502 | 503 | this.JpegMeta.JpegFile.prototype._iptctags = { 504 | 0 : ['Record Version', 'recordVersion'], 505 | 3 : ['Object Type Reference', 'objectType'], 506 | 4 : ['Object Attribute Reference', 'objectAttribute'], 507 | 5 : ['Object Name', 'objectName'], 508 | 7 : ['Edit Status', 'editStatus'], 509 | 8 : ['Editorial Update', 'editorialUpdate'], 510 | 10 : ['Urgency', 'urgency'], 511 | 12 : ['Subject Reference', 'subjectRef'], 512 | 15 : ['Category', 'category'], 513 | 20 : ['Supplemental Category', 'supplCategory'], 514 | 22 : ['Fixture Identifier', 'fixtureID'], 515 | 25 : ['Keywords', 'keywords'], 516 | 26 : ['Content Location Code', 'contentLocCode'], 517 | 27 : ['Content Location Name', 'contentLocName'], 518 | 30 : ['Release Date', 'releaseDate'], 519 | 35 : ['Release Time', 'releaseTime'], 520 | 37 : ['Expiration Date', 'expirationDate'], 521 | 38 : ['Expiration Time', 'expirationTime'], 522 | 40 : ['Special Instructions', 'specialInstructions'], 523 | 42 : ['Action Advised', 'actionAdvised'], 524 | 45 : ['Reference Service', 'refService'], 525 | 47 : ['Reference Date', 'refDate'], 526 | 50 : ['Reference Number', 'refNumber'], 527 | 55 : ['Date Created', 'dateCreated'], 528 | 60 : ['Time Created', 'timeCreated'], 529 | 62 : ['Digital Creation Date', 'digitalCreationDate'], 530 | 63 : ['Digital Creation Time', 'digitalCreationTime'], 531 | 65 : ['Originating Program', 'originatingProgram'], 532 | 70 : ['Program Version', 'programVersion'], 533 | 75 : ['Object Cycle', 'objectCycle'], 534 | 80 : ['By-line', 'byline'], 535 | 85 : ['By-line Title', 'bylineTitle'], 536 | 90 : ['City', 'city'], 537 | 92 : ['Sub-location', 'sublocation'], 538 | 95 : ['Province/State', 'state'], 539 | 100 : ['Country Code', 'countryCode'], 540 | 101 : ['Country Name', 'countryName'], 541 | 103 : ['Original Transmission Reference', 'origTransRef'], 542 | 105 : ['Headline', 'headline'], 543 | 110 : ['Credit', 'credit'], 544 | 115 : ['Source', 'source'], 545 | 116 : ['Copyright Notice', 'copyrightNotice'], 546 | 118 : ['Contact', 'contact'], 547 | 120 : ['Caption/Abstract', 'caption'], 548 | 122 : ['Writer/Editor', 'writerEditor'], 549 | 125 : ['Rasterized Caption', 'rasterizedCaption'], 550 | 130 : ['Image Type', 'imageType'], 551 | 131 : ['Image Orientation', 'imageOrientation'], 552 | 135 : ['Language Identifier', 'languageID'], 553 | 150 : ['Audio Type', 'audioType'], 554 | 151 : ['Audio Sampling Rate', 'audioSamplingRate'], 555 | 152 : ['Audio Sampling Resolution', 'audioSamplingRes'], 556 | 153 : ['Audio Duration', 'audioDuration'], 557 | 154 : ['Audio Outcue', 'audioOutcue'], 558 | 200 : ['Preview File Format', 'previewFileFormat'], 559 | 201 : ['Preview File Format Version', 'previewFileFormatVer'], 560 | 202 : ['Preview Data', 'previewData'] 561 | }; 562 | 563 | this.JpegMeta.JpegFile.prototype._markers = { 564 | /* Start Of Frame markers, non-differential, Huffman coding */ 565 | 0xc0: ["SOF0", "_sofHandler", "Baseline DCT"], 566 | 0xc1: ["SOF1", "_sofHandler", "Extended sequential DCT"], 567 | 0xc2: ["SOF2", "_sofHandler", "Progressive DCT"], 568 | 0xc3: ["SOF3", "_sofHandler", "Lossless (sequential)"], 569 | 570 | /* Start Of Frame markers, differential, Huffman coding */ 571 | 0xc5: ["SOF5", "_sofHandler", "Differential sequential DCT"], 572 | 0xc6: ["SOF6", "_sofHandler", "Differential progressive DCT"], 573 | 0xc7: ["SOF7", "_sofHandler", "Differential lossless (sequential)"], 574 | 575 | /* Start Of Frame markers, non-differential, arithmetic coding */ 576 | 0xc8: ["JPG", null, "Reserved for JPEG extensions"], 577 | 0xc9: ["SOF9", "_sofHandler", "Extended sequential DCT"], 578 | 0xca: ["SOF10", "_sofHandler", "Progressive DCT"], 579 | 0xcb: ["SOF11", "_sofHandler", "Lossless (sequential)"], 580 | 581 | /* Start Of Frame markers, differential, arithmetic coding */ 582 | 0xcd: ["SOF13", "_sofHandler", "Differential sequential DCT"], 583 | 0xce: ["SOF14", "_sofHandler", "Differential progressive DCT"], 584 | 0xcf: ["SOF15", "_sofHandler", "Differential lossless (sequential)"], 585 | 586 | /* Huffman table specification */ 587 | 0xc4: ["DHT", null, "Define Huffman table(s)"], 588 | 0xcc: ["DAC", null, "Define arithmetic coding conditioning(s)"], 589 | 590 | /* Restart interval termination" */ 591 | 0xd0: ["RST0", null, "Restart with modulo 8 count “0”"], 592 | 0xd1: ["RST1", null, "Restart with modulo 8 count “1”"], 593 | 0xd2: ["RST2", null, "Restart with modulo 8 count “2”"], 594 | 0xd3: ["RST3", null, "Restart with modulo 8 count “3”"], 595 | 0xd4: ["RST4", null, "Restart with modulo 8 count “4”"], 596 | 0xd5: ["RST5", null, "Restart with modulo 8 count “5”"], 597 | 0xd6: ["RST6", null, "Restart with modulo 8 count “6”"], 598 | 0xd7: ["RST7", null, "Restart with modulo 8 count “7”"], 599 | 600 | /* Other markers */ 601 | 0xd8: ["SOI", null, "Start of image"], 602 | 0xd9: ["EOI", null, "End of image"], 603 | 0xda: ["SOS", null, "Start of scan"], 604 | 0xdb: ["DQT", null, "Define quantization table(s)"], 605 | 0xdc: ["DNL", null, "Define number of lines"], 606 | 0xdd: ["DRI", null, "Define restart interval"], 607 | 0xde: ["DHP", null, "Define hierarchical progression"], 608 | 0xdf: ["EXP", null, "Expand reference component(s)"], 609 | 0xe0: ["APP0", "_app0Handler", "Reserved for application segments"], 610 | 0xe1: ["APP1", "_app1Handler"], 611 | 0xe2: ["APP2", null], 612 | 0xe3: ["APP3", null], 613 | 0xe4: ["APP4", null], 614 | 0xe5: ["APP5", null], 615 | 0xe6: ["APP6", null], 616 | 0xe7: ["APP7", null], 617 | 0xe8: ["APP8", null], 618 | 0xe9: ["APP9", null], 619 | 0xea: ["APP10", null], 620 | 0xeb: ["APP11", null], 621 | 0xec: ["APP12", null], 622 | 0xed: ["IPTC", "_iptcHandler", "IPTC Photo Metadata"], 623 | 0xee: ["APP14", null], 624 | 0xef: ["APP15", null], 625 | 0xf0: ["JPG0", null], /* Reserved for JPEG extensions */ 626 | 0xf1: ["JPG1", null], 627 | 0xf2: ["JPG2", null], 628 | 0xf3: ["JPG3", null], 629 | 0xf4: ["JPG4", null], 630 | 0xf5: ["JPG5", null], 631 | 0xf6: ["JPG6", null], 632 | 0xf7: ["JPG7", null], 633 | 0xf8: ["JPG8", null], 634 | 0xf9: ["JPG9", null], 635 | 0xfa: ["JPG10", null], 636 | 0xfb: ["JPG11", null], 637 | 0xfc: ["JPG12", null], 638 | 0xfd: ["JPG13", null], 639 | 0xfe: ["COM", "_commentHandler", "Comment"], /* Comment */ 640 | 641 | /* Reserved markers */ 642 | 0x01: ["JPG13", null] /* For temporary private use in arithmetic coding */ 643 | /* 02 -> bf are reserverd */ 644 | }; 645 | 646 | /* Private methods */ 647 | this.JpegMeta.JpegFile.prototype._addMetaGroup = function _addMetaGroup(name, description) { 648 | var group = new JpegMeta.MetaGroup(name, description); 649 | this[group.fieldName] = group; 650 | this.metaGroups[group.fieldName] = group; 651 | return group; 652 | }; 653 | 654 | this.JpegMeta.JpegFile.prototype._parseIfd = function _parseIfd(endian, _binary_data, base, ifd_offset, tags, name, description) { 655 | var num_fields = JpegMeta.parseNum(endian, _binary_data, base + ifd_offset, 2); 656 | /* Per tag variables */ 657 | var tag_base; 658 | var tag_field; 659 | var type, type_field, type_size; 660 | var num_values; 661 | var value_offset; 662 | var value; 663 | var _val; 664 | var num; 665 | var den; 666 | 667 | var group; 668 | 669 | group = this._addMetaGroup(name, description); 670 | 671 | for (var i = 0; i < num_fields; i++) { 672 | /* parse the field */ 673 | tag_base = base + ifd_offset + 2 + (i * 12); 674 | tag_field = JpegMeta.parseNum(endian, _binary_data, tag_base, 2); 675 | type_field = JpegMeta.parseNum(endian, _binary_data, tag_base + 2, 2); 676 | num_values = JpegMeta.parseNum(endian, _binary_data, tag_base + 4, 4); 677 | value_offset = JpegMeta.parseNum(endian, _binary_data, tag_base + 8, 4); 678 | if (this._types[type_field] === undefined) { 679 | continue; 680 | } 681 | type = this._types[type_field][0]; 682 | type_size = this._types[type_field][1]; 683 | 684 | if (type_size * num_values <= 4) { 685 | /* Data is in-line */ 686 | value_offset = tag_base + 8; 687 | } else { 688 | value_offset = base + value_offset; 689 | } 690 | 691 | /* Read the value */ 692 | if (type == "UNDEFINED") { 693 | /* FIXME: This should be done better */ 694 | /*value = _binary_data.slice(value_offset, value_offset + num_values); */ 695 | value = undefined; 696 | } else if (type == "ASCII") { 697 | value = _binary_data.slice(value_offset, value_offset + num_values); 698 | value = value.split('\x00')[0]; 699 | if (!JpegMeta.stringIsClean(value)) { 700 | value = ""; 701 | } 702 | /* strip trail nul */ 703 | } else { 704 | value = new Array(); 705 | for (var j = 0; j < num_values; j++, value_offset += type_size) { 706 | if (type == "BYTE" || type == "SHORT" || type == "LONG") { 707 | value.push(JpegMeta.parseNum(endian, _binary_data, value_offset, type_size)); 708 | } 709 | if (type == "SBYTE" || type == "SSHORT" || type == "SLONG") { 710 | value.push(JpegMeta.parseSnum(endian, _binary_data, value_offset, type_size)); 711 | } 712 | if (type == "RATIONAL") { 713 | num = JpegMeta.parseNum(endian, _binary_data, value_offset, 4); 714 | den = JpegMeta.parseNum(endian, _binary_data, value_offset + 4, 4); 715 | value.push(new JpegMeta.Rational(num, den)); 716 | } 717 | if (type == "SRATIONAL") { 718 | num = JpegMeta.parseSnum(endian, _binary_data, value_offset, 4); 719 | den = JpegMeta.parseSnum(endian, _binary_data, value_offset + 4, 4); 720 | value.push(new JpegMeta.Rational(num, den)); 721 | } 722 | value.push(); 723 | } 724 | if (num_values === 1) { 725 | value = value[0]; 726 | } 727 | } 728 | if (tags.hasOwnProperty(tag_field)) { 729 | group._addProperty(tags[tag_field][1], tags[tag_field][0], value); 730 | } else { 731 | console.log("WARNING(jpegmeta.js): Unknown tag: ", tag_field); 732 | } 733 | } 734 | }; 735 | 736 | this.JpegMeta.JpegFile.prototype._jfifHandler = function _jfifHandler(mark, pos) { 737 | if (this.jfif !== undefined) { 738 | throw Error("Multiple JFIF segments found"); 739 | } 740 | this._addMetaGroup("jfif", "JFIF"); 741 | this.jfif._addProperty("version_major", "Version Major", this._binary_data.charCodeAt(pos + 5)); 742 | this.jfif._addProperty("version_minor", "Version Minor", this._binary_data.charCodeAt(pos + 6)); 743 | this.jfif._addProperty("version", "JFIF Version", this.jfif.version_major.value + "." + this.jfif.version_minor.value); 744 | this.jfif._addProperty("units", "Density Unit", this._binary_data.charCodeAt(pos + 7)); 745 | this.jfif._addProperty("Xdensity", "X density", JpegMeta.parseNum(">", this._binary_data, pos + 8, 2)); 746 | this.jfif._addProperty("Ydensity", "Y Density", JpegMeta.parseNum(">", this._binary_data, pos + 10, 2)); 747 | this.jfif._addProperty("Xthumbnail", "X Thumbnail", JpegMeta.parseNum(">", this._binary_data, pos + 12, 1)); 748 | this.jfif._addProperty("Ythumbnail", "Y Thumbnail", JpegMeta.parseNum(">", this._binary_data, pos + 13, 1)); 749 | }; 750 | 751 | 752 | /* Handle app0 segments */ 753 | this.JpegMeta.JpegFile.prototype._app0Handler = function app0Handler(mark, pos) { 754 | var ident = this._binary_data.slice(pos, pos + 5); 755 | if (ident == this._JFIF_IDENT) { 756 | this._jfifHandler(mark, pos); 757 | } else if (ident == this._JFXX_IDENT) { 758 | /* Don't handle JFXX Ident yet */ 759 | } else { 760 | /* Don't know about other idents */ 761 | } 762 | }; 763 | 764 | 765 | /* Handle app1 segments */ 766 | this.JpegMeta.JpegFile.prototype._app1Handler = function _app1Handler(mark, pos) { 767 | var ident = this._binary_data.slice(pos, pos + 5); 768 | if (ident == this._EXIF_IDENT) { 769 | this._exifHandler(mark, pos + 6); 770 | } else { 771 | /* Don't know about other idents */ 772 | } 773 | }; 774 | 775 | /* Handle exif segments */ 776 | JpegMeta.JpegFile.prototype._exifHandler = function _exifHandler(mark, pos) { 777 | if (this.exif !== undefined) { 778 | throw new Error("Multiple JFIF segments found"); 779 | } 780 | 781 | /* Parse this TIFF header */ 782 | var endian; 783 | var magic_field; 784 | var ifd_offset; 785 | var primary_ifd, exif_ifd, gps_ifd; 786 | var endian_field = this._binary_data.slice(pos, pos + 2); 787 | 788 | /* Trivia: This 'I' is for Intel, the 'M' is for Motorola */ 789 | if (endian_field === "II") { 790 | endian = "<"; 791 | } else if (endian_field === "MM") { 792 | endian = ">"; 793 | } else { 794 | throw new Error("Malformed TIFF meta-data. Unknown endianess: " + endian_field); 795 | } 796 | 797 | magic_field = JpegMeta.parseNum(endian, this._binary_data, pos + 2, 2); 798 | 799 | if (magic_field !== 42) { 800 | throw new Error("Malformed TIFF meta-data. Bad magic: " + magic_field); 801 | } 802 | 803 | ifd_offset = JpegMeta.parseNum(endian, this._binary_data, pos + 4, 4); 804 | 805 | /* Parse 0th IFD */ 806 | this._parseIfd(endian, this._binary_data, pos, ifd_offset, this._tifftags, "tiff", "TIFF"); 807 | 808 | if (this.tiff.ExifIfdPointer) { 809 | this._parseIfd(endian, this._binary_data, pos, this.tiff.ExifIfdPointer.value, this._exiftags, "exif", "Exif"); 810 | } 811 | 812 | if (this.tiff.GPSInfoIfdPointer) { 813 | this._parseIfd(endian, this._binary_data, pos, this.tiff.GPSInfoIfdPointer.value, this._gpstags, "gps", "GPS"); 814 | if (this.gps.GPSLatitude) { 815 | var latitude; 816 | latitude = this.gps.GPSLatitude.value[0].asFloat() + 817 | (1 / 60) * this.gps.GPSLatitude.value[1].asFloat() + 818 | (1 / 3600) * this.gps.GPSLatitude.value[2].asFloat(); 819 | if (this.gps.GPSLatitudeRef.value === "S") { 820 | latitude = -latitude; 821 | } 822 | this.gps._addProperty("latitude", "Dec. Latitude", latitude); 823 | } 824 | if (this.gps.GPSLongitude) { 825 | var longitude; 826 | longitude = this.gps.GPSLongitude.value[0].asFloat() + 827 | (1 / 60) * this.gps.GPSLongitude.value[1].asFloat() + 828 | (1 / 3600) * this.gps.GPSLongitude.value[2].asFloat(); 829 | if (this.gps.GPSLongitudeRef.value === "W") { 830 | longitude = -longitude; 831 | } 832 | this.gps._addProperty("longitude", "Dec. Longitude", longitude); 833 | } 834 | } 835 | }; 836 | 837 | this.JpegMeta.JpegFile.prototype._iptcHandler = function _iptcHandler(mark, pos, segsize) { 838 | this._addMetaGroup("iptc", "IPTC"); 839 | 840 | var endian = '<'; 841 | var offset, fieldStart, title, value, tag; 842 | var length = JpegMeta.parseNum(endian, this._binary_data, pos + 4, 1); 843 | var FILE_SEPARATOR_CHAR = 28, 844 | START_OF_TEXT_CHAR = 2; 845 | 846 | for (var i = 0; i < segsize; i++) { 847 | fieldStart = pos + i; 848 | if (JpegMeta.parseNum(endian, this._binary_data, fieldStart, 1) == START_OF_TEXT_CHAR) { 849 | tag = JpegMeta.parseNum(endian, this._binary_data, fieldStart + 1, 1); 850 | tag_desc = this._iptctags[tag]; 851 | 852 | if (!tag_desc) continue; 853 | length = 0; 854 | offset = 2; 855 | 856 | while ( 857 | offset < segsize && 858 | JpegMeta.parseNum(endian, this._binary_data, fieldStart + offset, 1) != FILE_SEPARATOR_CHAR && 859 | JpegMeta.parseNum(endian, this._binary_data, fieldStart + offset + 1, 1) != START_OF_TEXT_CHAR) { 860 | offset++; 861 | length++; 862 | } 863 | 864 | if (!length) continue; 865 | 866 | value = this._binary_data.slice(pos + i + 2, pos + i + 2 + length); 867 | value = value.replace('\000', '').trim(); 868 | 869 | this.iptc._addProperty(tag_desc[1], tag_desc[0], value); 870 | i += length - 1; 871 | } 872 | } 873 | }; 874 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsjpegmeta", 3 | "version": "1.0.1", 4 | "description": "Get exif info from Jpeg files", 5 | "main": "jpegmeta.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Zacknero/jsjpegmeta.git" 12 | }, 13 | "keywords": [ 14 | "Exif", 15 | "Image" 16 | ], 17 | "author": "Ben Leslie","Zacknero" 18 | "license": "BSD", 19 | "dependencies": { 20 | 21 | } 22 | } 23 | --------------------------------------------------------------------------------