Home | History | Annotate | Download | only in js
      1 // Copyright (c) 2011 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 var exif = {
      6   verbose: false,
      7 
      8   messageHandlers: {
      9     "init": function() {
     10       this.log('thumbnailer initialized');
     11     },
     12 
     13     "get-exif": function(fileURL) {
     14       this.processOneFile(fileURL, function callback(metadata) {
     15           postMessage({verb: 'give-exif',
     16                        arguments: [fileURL, metadata]});
     17       });
     18     },
     19   },
     20 
     21   processOneFile: function(fileURL, callback) {
     22     var self = this;
     23     var currentStep = -1;
     24 
     25     function nextStep(var_args) {
     26       self.vlog('nextStep: ' + steps[currentStep + 1].name);
     27       steps[++currentStep].apply(self, arguments);
     28     }
     29 
     30     function onError(err) {
     31       self.vlog('Error processing: ' + fileURL + ': step: ' +
     32                 steps[currentStep].name + ": " + err);
     33 
     34       postMessage({verb: 'give-exif-error',
     35                    arguments: [fileURL, steps[currentStep].name, err]});
     36     }
     37 
     38     var steps =
     39     [ // Step one, turn the url into an entry.
     40       function getEntry() {
     41         webkitResolveLocalFileSystemURL(fileURL,
     42                                         function(entry) { nextStep(entry) },
     43                                         onError);
     44       },
     45 
     46       // Step two, turn the entry into a file.
     47       function getFile(entry) {
     48         entry.file(function(file) { nextStep(file) }, onError);
     49       },
     50 
     51       // Step three, read the file header into a byte array.
     52       function readHeader(file) {
     53         var reader = new FileReader(file.webkitSlice(0, 1024));
     54         reader.onerror = onError;
     55         reader.onload = function(event) { nextStep(file, reader.result) };
     56         reader.readAsArrayBuffer(file);
     57       },
     58 
     59       // Step four, find the exif marker and read all exif data.
     60       function findExif(file, buf) {
     61         var br = new exif.BufferReader(buf);
     62         var mark = br.readMark();
     63         if (mark != exif.MARK_SOI)
     64           return onError('Invalid file header: ' + mark.toString(16));
     65 
     66         while (true) {
     67           if (mark == exif.MARK_SOS || br.eof()) {
     68             return onError('Unable to find EXIF marker');
     69           }
     70 
     71           mark = br.readMark();
     72           if (mark == exif.MARK_EXIF) {
     73             var length = br.readMarkLength();
     74 
     75             // Offsets inside the EXIF block are based after this bit of
     76             // magic, so we verify and discard it here, before exif parsing,
     77             // to make offset calculations simpler.
     78             var magic = br.readString(6);
     79             if (magic != 'Exif\0\0')
     80               return onError('Invalid EXIF magic: ' + magic.toString(16));
     81 
     82             var pos = br.tell();
     83             var reader = new FileReader();
     84             reader.onerror = onError;
     85             reader.onload = function(event) { nextStep(file, reader.result) };
     86             reader.readAsArrayBuffer(file.webkitSlice(pos, pos + length - 6));
     87             return;
     88           }
     89 
     90           br.skipMarkData();
     91         }
     92       },
     93 
     94       // Step five, parse the exif data.
     95       function parseExif(file, buf) {
     96         var br = new exif.BufferReader(buf);
     97         var order = br.readScalar(2);
     98         if (order == exif.ALIGN_LITTLE) {
     99           br.setByteOrder(exif.BufferReader.LITTLE_ENDIAN);
    100         } else if (order != exif.ALIGN_BIG) {
    101           return onError('Invalid alignment value: ' + order.toString(16));
    102         }
    103 
    104         var tag = br.readScalar(2);
    105         if (tag != exif.TAG_TIFF)
    106           return onError('Invalid TIFF tag: ' + tag.toString(16));
    107 
    108         var tags = {};
    109         var directoryOffset = br.readScalar(4);
    110 
    111         while (directoryOffset) {
    112           br.seek(directoryOffset);
    113           var entryCount = br.readScalar(2);
    114           for (var i = 0; i < entryCount; i++) {
    115             var tag = tags[br.readScalar(2)] = {};
    116             tag.format = br.readScalar(2);
    117             tag.componentCount = br.readScalar(4);
    118             tag.value = br.readScalar(4);
    119           };
    120 
    121           directoryOffset = br.readScalar(4);
    122         }
    123 
    124         var metadata = { rawTags: tags };
    125 
    126         if (exif.TAG_JPG_THUMB_OFFSET in tags &&
    127             exif.TAG_JPG_THUMB_LENGTH in tags) {
    128           br.seek(tags[exif.TAG_JPG_THUMB_OFFSET].value);
    129           var b64 = br.readBase64(tags[exif.TAG_JPG_THUMB_LENGTH].value);
    130           metadata.thumbnailURL = 'data:image/jpeg;base64,' + b64;
    131         } else {
    132           self.vlog('Image has EXIF data, but no JPG thumbnail.');
    133         }
    134 
    135         if (exif.TAG_EXIF_IMAGE_WIDTH in tags)
    136           metadata.exifImageWidth = tags[exif.TAG_IMAGE_WIDTH];
    137 
    138         if (exif.TAG_EXIF_IMAGE_HEIGHT in tags)
    139           metadata.exifImageHeight = tags[exif.TAG_IMAGE_HEIGHT];
    140 
    141         nextStep(metadata);
    142       },
    143 
    144       // Step six, we're done.
    145       callback
    146     ];
    147 
    148     nextStep();
    149   },
    150 
    151   onMessage: function(event) {
    152     var data = event.data;
    153 
    154     if (this.messageHandlers.hasOwnProperty(data.verb)) {
    155       //this.log('dispatching: ' + data.verb + ': ' + data.arguments);
    156       this.messageHandlers[data.verb].apply(this, data.arguments);
    157     } else {
    158       this.log('Unknown message from client: ' + data.verb, data);
    159     }
    160   },
    161 
    162   log: function(var_args) {
    163     var ary = Array.apply(null, arguments);
    164     postMessage({verb: 'log', arguments: ary});
    165   },
    166 
    167   vlog: function(var_args) {
    168     if (this.verbose)
    169       this.log.apply(this, arguments);
    170   }
    171 };
    172 
    173 exif.MARK_SOI = 0xffd8;  // Start of image data.
    174 exif.MARK_SOS = 0xffda;  // Start of "stream" (the actual image data).
    175 exif.MARK_EXIF = 0xffe1;  // Start of exif block.
    176 
    177 exif.ALIGN_LITTLE = 0x4949;  // Indicates little endian alignment of exif data.
    178 exif.ALIGN_BIG = 0x4d4d;  // Indicates big endian alignment of exif data.
    179 
    180 exif.TAG_TIFF = 0x002a;  // First tag in the exif data.
    181 exif.TAG_JPG_THUMB_OFFSET = 0x0201;
    182 exif.TAG_JPG_THUMB_LENGTH = 0x0202;
    183 exif.TAG_EXIF_IMAGE_WIDTH = 0xa002;
    184 exif.TAG_EXIF_IMAGE_HEIGHT = 0xa003;
    185 
    186 exif.BufferReader = function(buf) {
    187   this.buf_ = buf;
    188   this.ary_ = new Uint8Array(buf);
    189   this.pos_ = 0;
    190   this.setByteOrder(exif.BufferReader.BIG_ENDIAN);
    191 };
    192 
    193 exif.BufferReader.LITTLE_ENDIAN = 0;  // Intel, 0x1234 is [0x34, 0x12]
    194 exif.BufferReader.BIG_ENDIAN = 1;  // Motorola, 0x002a is [0x12, 0x34]
    195 
    196 exif.BufferReader.prototype = {
    197   setByteOrder: function(order) {
    198     this.order_ = order;
    199     if (order == exif.BufferReader.LITTLE_ENDIAN) {
    200       this.readScalar = this.readLittle;
    201     } else {
    202       this.readScalar = this.readBig;
    203     }
    204   },
    205 
    206   eof: function() {
    207     return this.pos_ >= this.ary_.length;
    208   },
    209 
    210   readScalar: null,  // Either readLittle or readBig, according to byte order.
    211 
    212   /**
    213    * Big endian read.  Most significant bytes come first.
    214    */
    215   readBig: function(width) {
    216     var rv = 0;
    217     switch(width) {
    218       case 4:
    219         rv = this.ary_[this.pos_++] << 24;
    220       case 3:
    221         rv |= this.ary_[this.pos_++] << 16;
    222       case 2:
    223         rv |= this.ary_[this.pos_++] << 8;
    224       case 1:
    225         rv |= this.ary_[this.pos_++];
    226     }
    227 
    228     return rv;
    229   },
    230 
    231   /**
    232    * Little endian read.  Least significant bytes come first.
    233    */
    234   readLittle: function(width) {
    235     var rv = 0;
    236     switch(width) {
    237       case 4:
    238         rv = this.ary_[this.pos_ + 3] << 24;
    239       case 3:
    240         rv |= this.ary_[this.pos_ + 2] << 16;
    241       case 2:
    242         rv |= this.ary_[this.pos_+ 1] << 8;
    243       case 1:
    244         rv |= this.ary_[this.pos_];
    245     }
    246 
    247     this.pos_ += width;
    248     return rv;
    249   },
    250 
    251   readString: function(length) {
    252     var chars = [];
    253     for (var i = 0; i < length; i++) {
    254       chars[i] = String.fromCharCode(this.ary_[this.pos_++]);
    255     }
    256 
    257     return chars.join('');
    258   },
    259 
    260   base64Alphabet_: ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
    261                     'abcdefghijklmnopqrstuvwxyz' +
    262                     '0123456789+/').split(''),
    263 
    264   readBase64: function(length) {
    265     var rv = [];
    266     var chars = [];
    267     var padding = 0;
    268 
    269     for (var i = 0; i < length; /* incremented inside */) {
    270       var bits = this.ary_[this.pos_ + i++] << 16;
    271 
    272       if (i < length) {
    273         bits |= this.ary_[this.pos_ + i++] << 8;
    274 
    275         if (i < length) {
    276           bits |= this.ary_[this.pos_ + i++];
    277         } else {
    278           padding = 1;
    279         }
    280       } else {
    281         padding = 2;
    282       }
    283 
    284       chars[3] = this.base64Alphabet_[bits & 63];
    285       chars[2] = this.base64Alphabet_[(bits >> 6) & 63];
    286       chars[1] = this.base64Alphabet_[(bits >> 12) & 63];
    287       chars[0] = this.base64Alphabet_[(bits >> 18) & 63];
    288 
    289       rv.push.apply(rv, chars);
    290     }
    291 
    292     this.pos_ += i;
    293 
    294     if (padding > 0)
    295       chars[chars.length - 1] = '=';
    296     if (padding > 1)
    297       chars[chars.length - 2] = '=';
    298 
    299     return rv.join('');
    300   },
    301 
    302   readMark: function() {
    303     return this.readScalar(2);
    304   },
    305 
    306   readMarkLength: function() {
    307     // Length includes the 2 bytes used to store the length.
    308     return this.readScalar(2) - 2;
    309   },
    310 
    311   readMarkData: function(opt_arrayConstructor) {
    312     var arrayConstructor = opt_arrayConstructor || Uint8Array;
    313 
    314     var length = this.readMarkLength();
    315     var slice = new arrayConstructor(this.buf_, this.pos_, length);
    316     this.pos_ += length;
    317 
    318     return slice;
    319   },
    320 
    321   skipMarkData: function() {
    322     this.skip(this.readMarkLength());
    323   },
    324 
    325   seek: function(pos) {
    326     this.pos_ = pos;
    327   },
    328 
    329   skip: function(count) {
    330     this.pos_ += count;
    331   },
    332 
    333   tell: function() {
    334     return this.pos_;
    335   }
    336 };
    337 
    338 var onmessage = exif.onMessage.bind(exif);
    339