Home | History | Annotate | Download | only in metadata
      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