1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 'use strict'; 6 7 var EXIF_MARK_SOI = 0xffd8; // Start of image data. 8 var EXIF_MARK_SOS = 0xffda; // Start of "stream" (the actual image data). 9 var EXIF_MARK_SOF = 0xffc0; // Start of "frame" 10 var EXIF_MARK_EXIF = 0xffe1; // Start of exif block. 11 12 var EXIF_ALIGN_LITTLE = 0x4949; // Indicates little endian exif data. 13 var EXIF_ALIGN_BIG = 0x4d4d; // Indicates big endian exif data. 14 15 var EXIF_TAG_TIFF = 0x002a; // First directory containing TIFF data. 16 var EXIF_TAG_GPSDATA = 0x8825; // Pointer from TIFF to the GPS directory. 17 var EXIF_TAG_EXIFDATA = 0x8769; // Pointer from TIFF to the EXIF IFD. 18 var EXIF_TAG_SUBIFD = 0x014a; // Pointer from TIFF to "Extra" IFDs. 19 20 var EXIF_TAG_JPG_THUMB_OFFSET = 0x0201; // Pointer from TIFF to thumbnail. 21 var EXIF_TAG_JPG_THUMB_LENGTH = 0x0202; // Length of thumbnail data. 22 23 var EXIF_TAG_ORIENTATION = 0x0112; 24 var EXIF_TAG_X_DIMENSION = 0xA002; 25 var EXIF_TAG_Y_DIMENSION = 0xA003; 26 27 function ExifParser(parent) { 28 ImageParser.call(this, parent, 'jpeg', /\.jpe?g$/i); 29 } 30 31 ExifParser.prototype = {__proto__: ImageParser.prototype}; 32 33 /** 34 * @param {File} file // TODO(JSDOC). 35 * @param {Object} metadata // TODO(JSDOC). 36 * @param {function} callback // TODO(JSDOC). 37 * @param {function} errorCallback // TODO(JSDOC). 38 */ 39 ExifParser.prototype.parse = function(file, metadata, callback, errorCallback) { 40 this.requestSlice(file, callback, errorCallback, metadata, 0); 41 }; 42 43 /** 44 * @param {File} file // TODO(JSDOC). 45 * @param {function} callback // TODO(JSDOC). 46 * @param {function} errorCallback // TODO(JSDOC). 47 * @param {Object} metadata // TODO(JSDOC). 48 * @param {number} filePos // TODO(JSDOC). 49 * @param {number=} opt_length // TODO(JSDOC). 50 */ 51 ExifParser.prototype.requestSlice = function( 52 file, callback, errorCallback, metadata, filePos, opt_length) { 53 // Read at least 1Kb so that we do not issue too many read requests. 54 opt_length = Math.max(1024, opt_length || 0); 55 56 var self = this; 57 var reader = new FileReader(); 58 reader.onerror = errorCallback; 59 reader.onload = function() { self.parseSlice( 60 file, callback, errorCallback, metadata, filePos, reader.result); 61 }; 62 reader.readAsArrayBuffer(file.slice(filePos, filePos + opt_length)); 63 }; 64 65 /** 66 * @param {File} file // TODO(JSDOC). 67 * @param {function} callback // TODO(JSDOC). 68 * @param {function} errorCallback // TODO(JSDOC). 69 * @param {Object} metadata // TODO(JSDOC). 70 * @param {number} filePos // TODO(JSDOC). 71 * @param {ArrayBuffer} buf // TODO(JSDOC). 72 */ 73 ExifParser.prototype.parseSlice = function( 74 file, callback, errorCallback, metadata, filePos, buf) { 75 try { 76 var br = new ByteReader(buf); 77 78 if (!br.canRead(4)) { 79 // We never ask for less than 4 bytes. This can only mean we reached EOF. 80 throw new Error('Unexpected EOF @' + (filePos + buf.byteLength)); 81 } 82 83 if (filePos == 0) { 84 // First slice, check for the SOI mark. 85 var firstMark = this.readMark(br); 86 if (firstMark != EXIF_MARK_SOI) 87 throw new Error('Invalid file header: ' + firstMark.toString(16)); 88 } 89 90 var self = this; 91 var reread = function(opt_offset, opt_bytes) { 92 self.requestSlice(file, callback, errorCallback, metadata, 93 filePos + br.tell() + (opt_offset || 0), opt_bytes); 94 }; 95 96 while (true) { 97 if (!br.canRead(4)) { 98 // Cannot read the mark and the length, request a minimum-size slice. 99 reread(); 100 return; 101 } 102 103 var mark = this.readMark(br); 104 if (mark == EXIF_MARK_SOS) 105 throw new Error('SOS marker found before SOF'); 106 107 var markLength = this.readMarkLength(br); 108 109 var nextSectionStart = br.tell() + markLength; 110 if (!br.canRead(markLength)) { 111 // Get the entire section. 112 if (filePos + br.tell() + markLength > file.size) { 113 throw new Error( 114 'Invalid section length @' + (filePos + br.tell() - 2)); 115 } 116 reread(-4, markLength + 4); 117 return; 118 } 119 120 if (mark == EXIF_MARK_EXIF) { 121 this.parseExifSection(metadata, buf, br); 122 } else if (ExifParser.isSOF_(mark)) { 123 // The most reliable size information is encoded in the SOF section. 124 br.seek(1, ByteReader.SEEK_CUR); // Skip the precision byte. 125 var height = br.readScalar(2); 126 var width = br.readScalar(2); 127 ExifParser.setImageSize(metadata, width, height); 128 callback(metadata); // We are done! 129 return; 130 } 131 132 br.seek(nextSectionStart, ByteReader.SEEK_BEG); 133 } 134 } catch (e) { 135 errorCallback(e.toString()); 136 } 137 }; 138 139 /** 140 * @private 141 * @param {number} mark // TODO(JSDOC). 142 * @return {boolean} // TODO(JSDOC). 143 */ 144 ExifParser.isSOF_ = function(mark) { 145 // There are 13 variants of SOF fragment format distinguished by the last 146 // hex digit of the mark, but the part we want is always the same. 147 if ((mark & ~0xF) != EXIF_MARK_SOF) return false; 148 149 // If the last digit is 4, 8 or 12 it is not really a SOF. 150 var type = mark & 0xF; 151 return (type != 4 && type != 8 && type != 12); 152 }; 153 154 /** 155 * @param {Object} metadata // TODO(JSDOC). 156 * @param {ArrayBuffer} buf // TODO(JSDOC). 157 * @param {ByteReader} br // TODO(JSDOC). 158 */ 159 ExifParser.prototype.parseExifSection = function(metadata, buf, br) { 160 var magic = br.readString(6); 161 if (magic != 'Exif\0\0') { 162 // Some JPEG files may have sections marked with EXIF_MARK_EXIF 163 // but containing something else (e.g. XML text). Ignore such sections. 164 this.vlog('Invalid EXIF magic: ' + magic + br.readString(100)); 165 return; 166 } 167 168 // Offsets inside the EXIF block are based after the magic string. 169 // Create a new ByteReader based on the current position to make offset 170 // calculations simpler. 171 br = new ByteReader(buf, br.tell()); 172 173 var order = br.readScalar(2); 174 if (order == EXIF_ALIGN_LITTLE) { 175 br.setByteOrder(ByteReader.LITTLE_ENDIAN); 176 } else if (order != EXIF_ALIGN_BIG) { 177 this.log('Invalid alignment value: ' + order.toString(16)); 178 return; 179 } 180 181 var tag = br.readScalar(2); 182 if (tag != EXIF_TAG_TIFF) { 183 this.log('Invalid TIFF tag: ' + tag.toString(16)); 184 return; 185 } 186 187 metadata.littleEndian = (order == EXIF_ALIGN_LITTLE); 188 metadata.ifd = { 189 image: {}, 190 thumbnail: {} 191 }; 192 var directoryOffset = br.readScalar(4); 193 194 // Image directory. 195 this.vlog('Read image directory.'); 196 br.seek(directoryOffset); 197 directoryOffset = this.readDirectory(br, metadata.ifd.image); 198 metadata.imageTransform = this.parseOrientation(metadata.ifd.image); 199 200 // Thumbnail Directory chained from the end of the image directory. 201 if (directoryOffset) { 202 this.vlog('Read thumbnail directory.'); 203 br.seek(directoryOffset); 204 this.readDirectory(br, metadata.ifd.thumbnail); 205 // If no thumbnail orientation is encoded, assume same orientation as 206 // the primary image. 207 metadata.thumbnailTransform = 208 this.parseOrientation(metadata.ifd.thumbnail) || 209 metadata.imageTransform; 210 } 211 212 // EXIF Directory may be specified as a tag in the image directory. 213 if (EXIF_TAG_EXIFDATA in metadata.ifd.image) { 214 this.vlog('Read EXIF directory.'); 215 directoryOffset = metadata.ifd.image[EXIF_TAG_EXIFDATA].value; 216 br.seek(directoryOffset); 217 metadata.ifd.exif = {}; 218 this.readDirectory(br, metadata.ifd.exif); 219 } 220 221 // GPS Directory may also be linked from the image directory. 222 if (EXIF_TAG_GPSDATA in metadata.ifd.image) { 223 this.vlog('Read GPS directory.'); 224 directoryOffset = metadata.ifd.image[EXIF_TAG_GPSDATA].value; 225 br.seek(directoryOffset); 226 metadata.ifd.gps = {}; 227 this.readDirectory(br, metadata.ifd.gps); 228 } 229 230 // Thumbnail may be linked from the image directory. 231 if (EXIF_TAG_JPG_THUMB_OFFSET in metadata.ifd.thumbnail && 232 EXIF_TAG_JPG_THUMB_LENGTH in metadata.ifd.thumbnail) { 233 this.vlog('Read thumbnail image.'); 234 br.seek(metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_OFFSET].value); 235 metadata.thumbnailURL = br.readImage( 236 metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_LENGTH].value); 237 } else { 238 this.vlog('Image has EXIF data, but no JPG thumbnail.'); 239 } 240 }; 241 242 /** 243 * @param {Object} metadata // TODO(JSDOC). 244 * @param {number} width // TODO(JSDOC). 245 * @param {number} height // TODO(JSDOC). 246 */ 247 ExifParser.setImageSize = function(metadata, width, height) { 248 if (metadata.imageTransform && metadata.imageTransform.rotate90) { 249 metadata.width = height; 250 metadata.height = width; 251 } else { 252 metadata.width = width; 253 metadata.height = height; 254 } 255 }; 256 257 /** 258 * @param {ByteReader} br // TODO(JSDOC). 259 * @return {number} // TODO(JSDOC). 260 */ 261 ExifParser.prototype.readMark = function(br) { 262 return br.readScalar(2); 263 }; 264 265 /** 266 * @param {ByteReader} br // TODO(JSDOC). 267 * @return {number} // TODO(JSDOC). 268 */ 269 ExifParser.prototype.readMarkLength = function(br) { 270 // Length includes the 2 bytes used to store the length. 271 return br.readScalar(2) - 2; 272 }; 273 274 /** 275 * @param {ByteReader} br // TODO(JSDOC). 276 * @param {Array.<Object>} tags // TODO(JSDOC). 277 * @return {number} // TODO(JSDOC). 278 */ 279 ExifParser.prototype.readDirectory = function(br, tags) { 280 var entryCount = br.readScalar(2); 281 for (var i = 0; i < entryCount; i++) { 282 var tagId = br.readScalar(2); 283 var tag = tags[tagId] = {id: tagId}; 284 tag.format = br.readScalar(2); 285 tag.componentCount = br.readScalar(4); 286 this.readTagValue(br, tag); 287 } 288 289 return br.readScalar(4); 290 }; 291 292 /** 293 * @param {ByteReader} br // TODO(JSDOC). 294 * @param {Object} tag // TODO(JSDOC). 295 */ 296 ExifParser.prototype.readTagValue = function(br, tag) { 297 var self = this; 298 299 function safeRead(size, readFunction, signed) { 300 try { 301 unsafeRead(size, readFunction, signed); 302 } catch (ex) { 303 self.log('error reading tag 0x' + tag.id.toString(16) + '/' + 304 tag.format + ', size ' + tag.componentCount + '*' + size + ' ' + 305 (ex.stack || '<no stack>') + ': ' + ex); 306 tag.value = null; 307 } 308 } 309 310 function unsafeRead(size, readFunction, signed) { 311 if (!readFunction) 312 readFunction = function(size) { return br.readScalar(size, signed) }; 313 314 var totalSize = tag.componentCount * size; 315 if (totalSize < 1) { 316 // This is probably invalid exif data, skip it. 317 tag.componentCount = 1; 318 tag.value = br.readScalar(4); 319 return; 320 } 321 322 if (totalSize > 4) { 323 // If the total size is > 4, the next 4 bytes will be a pointer to the 324 // actual data. 325 br.pushSeek(br.readScalar(4)); 326 } 327 328 if (tag.componentCount == 1) { 329 tag.value = readFunction(size); 330 } else { 331 // Read multiple components into an array. 332 tag.value = []; 333 for (var i = 0; i < tag.componentCount; i++) 334 tag.value[i] = readFunction(size); 335 } 336 337 if (totalSize > 4) { 338 // Go back to the previous position if we had to jump to the data. 339 br.popSeek(); 340 } else if (totalSize < 4) { 341 // Otherwise, if the value wasn't exactly 4 bytes, skip over the 342 // unread data. 343 br.seek(4 - totalSize, ByteReader.SEEK_CUR); 344 } 345 } 346 347 switch (tag.format) { 348 case 1: // Byte 349 case 7: // Undefined 350 safeRead(1); 351 break; 352 353 case 2: // String 354 safeRead(1); 355 if (tag.componentCount == 0) { 356 tag.value = ''; 357 } else if (tag.componentCount == 1) { 358 tag.value = String.fromCharCode(tag.value); 359 } else { 360 tag.value = String.fromCharCode.apply(null, tag.value); 361 } 362 break; 363 364 case 3: // Short 365 safeRead(2); 366 break; 367 368 case 4: // Long 369 safeRead(4); 370 break; 371 372 case 9: // Signed Long 373 safeRead(4, null, true); 374 break; 375 376 case 5: // Rational 377 safeRead(8, function() { 378 return [br.readScalar(4), br.readScalar(4)]; 379 }); 380 break; 381 382 case 10: // Signed Rational 383 safeRead(8, function() { 384 return [br.readScalar(4, true), br.readScalar(4, true)]; 385 }); 386 break; 387 388 default: // ??? 389 this.vlog('Unknown tag format 0x' + Number(tag.id).toString(16) + 390 ': ' + tag.format); 391 safeRead(4); 392 break; 393 } 394 395 this.vlog('Read tag: 0x' + tag.id.toString(16) + '/' + tag.format + ': ' + 396 tag.value); 397 }; 398 399 /** 400 * TODO(JSDOC) 401 * @const 402 * @type {Array.<number>} 403 */ 404 ExifParser.SCALEX = [1, -1, -1, 1, 1, 1, -1, -1]; 405 406 /** 407 * TODO(JSDOC) 408 * @const 409 * @type {Array.<number>} 410 */ 411 ExifParser.SCALEY = [1, 1, -1, -1, -1, 1, 1, -1]; 412 413 /** 414 * TODO(JSDOC) 415 * @const 416 * @type {Array.<number>} 417 */ 418 ExifParser.ROTATE90 = [0, 0, 0, 0, 1, 1, 1, 1]; 419 420 /** 421 * Transform exif-encoded orientation into a set of parameters compatible with 422 * CSS and canvas transforms (scaleX, scaleY, rotation). 423 * 424 * @param {Object} ifd exif property dictionary (image or thumbnail). 425 * @return {Object} // TODO(JSDOC). 426 */ 427 ExifParser.prototype.parseOrientation = function(ifd) { 428 if (ifd[EXIF_TAG_ORIENTATION]) { 429 var index = (ifd[EXIF_TAG_ORIENTATION].value || 1) - 1; 430 return { 431 scaleX: ExifParser.SCALEX[index], 432 scaleY: ExifParser.SCALEY[index], 433 rotate90: ExifParser.ROTATE90[index] 434 }; 435 } 436 return null; 437 }; 438 439 MetadataDispatcher.registerParserClass(ExifParser); 440