| OLD | NEW |
| (Empty) |
| 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); | |
| OLD | NEW |