Upstream version 7.35.144.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / file_manager / foreground / js / metadata / exif_parser.js
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);