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
5var 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
173exif.MARK_SOI = 0xffd8;  // Start of image data.
174exif.MARK_SOS = 0xffda;  // Start of "stream" (the actual image data).
175exif.MARK_EXIF = 0xffe1;  // Start of exif block.
176
177exif.ALIGN_LITTLE = 0x4949;  // Indicates little endian alignment of exif data.
178exif.ALIGN_BIG = 0x4d4d;  // Indicates big endian alignment of exif data.
179
180exif.TAG_TIFF = 0x002a;  // First tag in the exif data.
181exif.TAG_JPG_THUMB_OFFSET = 0x0201;
182exif.TAG_JPG_THUMB_LENGTH = 0x0202;
183exif.TAG_EXIF_IMAGE_WIDTH = 0xa002;
184exif.TAG_EXIF_IMAGE_HEIGHT = 0xa003;
185
186exif.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
193exif.BufferReader.LITTLE_ENDIAN = 0;  // Intel, 0x1234 is [0x34, 0x12]
194exif.BufferReader.BIG_ENDIAN = 1;  // Motorola, 0x002a is [0x12, 0x34]
195
196exif.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
338var onmessage = exif.onMessage.bind(exif);
339