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
7var EXIF_MARK_SOI = 0xffd8;  // Start of image data.
8var EXIF_MARK_SOS = 0xffda;  // Start of "stream" (the actual image data).
9var EXIF_MARK_SOF = 0xffc0;  // Start of "frame"
10var EXIF_MARK_EXIF = 0xffe1;  // Start of exif block.
11
12var EXIF_ALIGN_LITTLE = 0x4949;  // Indicates little endian exif data.
13var EXIF_ALIGN_BIG = 0x4d4d;  // Indicates big endian exif data.
14
15var EXIF_TAG_TIFF = 0x002a;  // First directory containing TIFF data.
16var EXIF_TAG_GPSDATA = 0x8825;  // Pointer from TIFF to the GPS directory.
17var EXIF_TAG_EXIFDATA = 0x8769;  // Pointer from TIFF to the EXIF IFD.
18var EXIF_TAG_SUBIFD = 0x014a;  // Pointer from TIFF to "Extra" IFDs.
19
20var EXIF_TAG_JPG_THUMB_OFFSET = 0x0201;  // Pointer from TIFF to thumbnail.
21var EXIF_TAG_JPG_THUMB_LENGTH = 0x0202;  // Length of thumbnail data.
22
23var EXIF_TAG_ORIENTATION = 0x0112;
24var EXIF_TAG_X_DIMENSION = 0xA002;
25var EXIF_TAG_Y_DIMENSION = 0xA003;
26
27function ExifParser(parent) {
28  ImageParser.call(this, parent, 'jpeg', /\.jpe?g$/i);
29}
30
31ExifParser.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 */
39ExifParser.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 */
51ExifParser.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 */
73ExifParser.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 */
144ExifParser.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 */
159ExifParser.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 */
247ExifParser.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 */
261ExifParser.prototype.readMark = function(br) {
262  return br.readScalar(2);
263};
264
265/**
266 * @param {ByteReader} br  // TODO(JSDOC).
267 * @return {number}  // TODO(JSDOC).
268 */
269ExifParser.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 */
279ExifParser.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 */
296ExifParser.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 */
404ExifParser.SCALEX = [1, -1, -1, 1, 1, 1, -1, -1];
405
406/**
407 * TODO(JSDOC)
408 * @const
409 * @type {Array.<number>}
410 */
411ExifParser.SCALEY = [1, 1, -1, -1, -1, 1, 1, -1];
412
413/**
414 * TODO(JSDOC)
415 * @const
416 * @type {Array.<number>}
417 */
418ExifParser.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 */
427ExifParser.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
439MetadataDispatcher.registerParserClass(ExifParser);
440