Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / 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 var EXIF_MARK_SOI = 0xffd8;  // Start of image data.
6 var EXIF_MARK_SOS = 0xffda;  // Start of "stream" (the actual image data).
7 var EXIF_MARK_SOF = 0xffc0;  // Start of "frame"
8 var EXIF_MARK_EXIF = 0xffe1;  // Start of exif block.
9
10 var EXIF_ALIGN_LITTLE = 0x4949;  // Indicates little endian exif data.
11 var EXIF_ALIGN_BIG = 0x4d4d;  // Indicates big endian exif data.
12
13 var EXIF_TAG_TIFF = 0x002a;  // First directory containing TIFF data.
14 var EXIF_TAG_GPSDATA = 0x8825;  // Pointer from TIFF to the GPS directory.
15 var EXIF_TAG_EXIFDATA = 0x8769;  // Pointer from TIFF to the EXIF IFD.
16 var EXIF_TAG_SUBIFD = 0x014a;  // Pointer from TIFF to "Extra" IFDs.
17
18 var EXIF_TAG_JPG_THUMB_OFFSET = 0x0201;  // Pointer from TIFF to thumbnail.
19 var EXIF_TAG_JPG_THUMB_LENGTH = 0x0202;  // Length of thumbnail data.
20
21 var EXIF_TAG_ORIENTATION = 0x0112;
22 var EXIF_TAG_X_DIMENSION = 0xA002;
23 var EXIF_TAG_Y_DIMENSION = 0xA003;
24
25 function ExifParser(parent) {
26   ImageParser.call(this, parent, 'jpeg', /\.jpe?g$/i);
27 }
28
29 ExifParser.prototype = {__proto__: ImageParser.prototype};
30
31 /**
32  * @param {File} file File object to parse.
33  * @param {Object} metadata Metadata object for the file.
34  * @param {function} callback Callback to be called on success.
35  * @param {function} errorCallback Error callback.
36  */
37 ExifParser.prototype.parse = function(file, metadata, callback, errorCallback) {
38   this.requestSlice(file, callback, errorCallback, metadata, 0);
39 };
40
41 /**
42  * @param {File} file File object to parse.
43  * @param {function} callback Callback to be called on success.
44  * @param {function} errorCallback Error callback.
45  * @param {Object} metadata Metadata object.
46  * @param {number} filePos Position to slice at.
47  * @param {number=} opt_length Number of bytes to slice. By default 1 KB.
48  */
49 ExifParser.prototype.requestSlice = function(
50     file, callback, errorCallback, metadata, filePos, opt_length) {
51   // Read at least 1Kb so that we do not issue too many read requests.
52   opt_length = Math.max(1024, opt_length || 0);
53
54   var self = this;
55   var reader = new FileReader();
56   reader.onerror = errorCallback;
57   reader.onload = function() { self.parseSlice(
58       file, callback, errorCallback, metadata, filePos, reader.result);
59   };
60   reader.readAsArrayBuffer(file.slice(filePos, filePos + opt_length));
61 };
62
63 /**
64  * @param {File} file File object to parse.
65  * @param {function} callback Callback to be called on success.
66  * @param {function} errorCallback Error callback.
67  * @param {Object} metadata Metadata object.
68  * @param {number} filePos Position to slice at.
69  * @param {ArrayBuffer} buf Buffer to be parsed.
70  */
71 ExifParser.prototype.parseSlice = function(
72     file, callback, errorCallback, metadata, filePos, buf) {
73   try {
74     var br = new ByteReader(buf);
75
76     if (!br.canRead(4)) {
77       // We never ask for less than 4 bytes. This can only mean we reached EOF.
78       throw new Error('Unexpected EOF @' + (filePos + buf.byteLength));
79     }
80
81     if (filePos == 0) {
82       // First slice, check for the SOI mark.
83       var firstMark = this.readMark(br);
84       if (firstMark != EXIF_MARK_SOI)
85         throw new Error('Invalid file header: ' + firstMark.toString(16));
86     }
87
88     var self = this;
89     var reread = function(opt_offset, opt_bytes) {
90       self.requestSlice(file, callback, errorCallback, metadata,
91           filePos + br.tell() + (opt_offset || 0), opt_bytes);
92     };
93
94     while (true) {
95       if (!br.canRead(4)) {
96         // Cannot read the mark and the length, request a minimum-size slice.
97         reread();
98         return;
99       }
100
101       var mark = this.readMark(br);
102       if (mark == EXIF_MARK_SOS)
103         throw new Error('SOS marker found before SOF');
104
105       var markLength = this.readMarkLength(br);
106
107       var nextSectionStart = br.tell() + markLength;
108       if (!br.canRead(markLength)) {
109         // Get the entire section.
110         if (filePos + br.tell() + markLength > file.size) {
111           throw new Error(
112               'Invalid section length @' + (filePos + br.tell() - 2));
113         }
114         reread(-4, markLength + 4);
115         return;
116       }
117
118       if (mark == EXIF_MARK_EXIF) {
119         this.parseExifSection(metadata, buf, br);
120       } else if (ExifParser.isSOF_(mark)) {
121         // The most reliable size information is encoded in the SOF section.
122         br.seek(1, ByteReader.SEEK_CUR); // Skip the precision byte.
123         var height = br.readScalar(2);
124         var width = br.readScalar(2);
125         ExifParser.setImageSize(metadata, width, height);
126         callback(metadata);  // We are done!
127         return;
128       }
129
130       br.seek(nextSectionStart, ByteReader.SEEK_BEG);
131     }
132   } catch (e) {
133     errorCallback(e.toString());
134   }
135 };
136
137 /**
138  * @private
139  * @param {number} mark Mark to be checked.
140  * @return {boolean} True if the mark is SOF.
141  */
142 ExifParser.isSOF_ = function(mark) {
143   // There are 13 variants of SOF fragment format distinguished by the last
144   // hex digit of the mark, but the part we want is always the same.
145   if ((mark & ~0xF) != EXIF_MARK_SOF) return false;
146
147   // If the last digit is 4, 8 or 12 it is not really a SOF.
148   var type = mark & 0xF;
149   return (type != 4 && type != 8 && type != 12);
150 };
151
152 /**
153  * @param {Object} metadata Metadata object.
154  * @param {ArrayBuffer} buf Buffer to be parsed.
155  * @param {ByteReader} br Byte reader to be used.
156  */
157 ExifParser.prototype.parseExifSection = function(metadata, buf, br) {
158   var magic = br.readString(6);
159   if (magic != 'Exif\0\0') {
160     // Some JPEG files may have sections marked with EXIF_MARK_EXIF
161     // but containing something else (e.g. XML text). Ignore such sections.
162     this.vlog('Invalid EXIF magic: ' + magic + br.readString(100));
163     return;
164   }
165
166   // Offsets inside the EXIF block are based after the magic string.
167   // Create a new ByteReader based on the current position to make offset
168   // calculations simpler.
169   br = new ByteReader(buf, br.tell());
170
171   var order = br.readScalar(2);
172   if (order == EXIF_ALIGN_LITTLE) {
173     br.setByteOrder(ByteReader.LITTLE_ENDIAN);
174   } else if (order != EXIF_ALIGN_BIG) {
175     this.log('Invalid alignment value: ' + order.toString(16));
176     return;
177   }
178
179   var tag = br.readScalar(2);
180   if (tag != EXIF_TAG_TIFF) {
181     this.log('Invalid TIFF tag: ' + tag.toString(16));
182     return;
183   }
184
185   metadata.littleEndian = (order == EXIF_ALIGN_LITTLE);
186   metadata.ifd = {
187     image: {},
188     thumbnail: {}
189   };
190   var directoryOffset = br.readScalar(4);
191
192   // Image directory.
193   this.vlog('Read image directory.');
194   br.seek(directoryOffset);
195   directoryOffset = this.readDirectory(br, metadata.ifd.image);
196   metadata.imageTransform = this.parseOrientation(metadata.ifd.image);
197
198   // Thumbnail Directory chained from the end of the image directory.
199   if (directoryOffset) {
200     this.vlog('Read thumbnail directory.');
201     br.seek(directoryOffset);
202     this.readDirectory(br, metadata.ifd.thumbnail);
203     // If no thumbnail orientation is encoded, assume same orientation as
204     // the primary image.
205     metadata.thumbnailTransform =
206         this.parseOrientation(metadata.ifd.thumbnail) ||
207         metadata.imageTransform;
208   }
209
210   // EXIF Directory may be specified as a tag in the image directory.
211   if (EXIF_TAG_EXIFDATA in metadata.ifd.image) {
212     this.vlog('Read EXIF directory.');
213     directoryOffset = metadata.ifd.image[EXIF_TAG_EXIFDATA].value;
214     br.seek(directoryOffset);
215     metadata.ifd.exif = {};
216     this.readDirectory(br, metadata.ifd.exif);
217   }
218
219   // GPS Directory may also be linked from the image directory.
220   if (EXIF_TAG_GPSDATA in metadata.ifd.image) {
221     this.vlog('Read GPS directory.');
222     directoryOffset = metadata.ifd.image[EXIF_TAG_GPSDATA].value;
223     br.seek(directoryOffset);
224     metadata.ifd.gps = {};
225     this.readDirectory(br, metadata.ifd.gps);
226   }
227
228   // Thumbnail may be linked from the image directory.
229   if (EXIF_TAG_JPG_THUMB_OFFSET in metadata.ifd.thumbnail &&
230       EXIF_TAG_JPG_THUMB_LENGTH in metadata.ifd.thumbnail) {
231     this.vlog('Read thumbnail image.');
232     br.seek(metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_OFFSET].value);
233     metadata.thumbnailURL = br.readImage(
234         metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_LENGTH].value);
235   } else {
236     this.vlog('Image has EXIF data, but no JPG thumbnail.');
237   }
238 };
239
240 /**
241  * @param {Object} metadata Metadata object.
242  * @param {number} width Width in pixels.
243  * @param {number} height Height in pixels.
244  */
245 ExifParser.setImageSize = function(metadata, width, height) {
246   if (metadata.imageTransform && metadata.imageTransform.rotate90) {
247     metadata.width = height;
248     metadata.height = width;
249   } else {
250     metadata.width = width;
251     metadata.height = height;
252   }
253 };
254
255 /**
256  * @param {ByteReader} br Byte reader to be used for reading.
257  * @return {number} Mark value.
258  */
259 ExifParser.prototype.readMark = function(br) {
260   return br.readScalar(2);
261 };
262
263 /**
264  * @param {ByteReader} br Bye reader to be used for reading.
265  * @return {number} Size of the mark at the current position.
266  */
267 ExifParser.prototype.readMarkLength = function(br) {
268   // Length includes the 2 bytes used to store the length.
269   return br.readScalar(2) - 2;
270 };
271
272 /**
273  * @param {ByteReader} br Byte reader to be used for reading.
274  * @param {Array.<Object>} tags Array of tags to be written to.
275  * @return {number} Directory offset.
276  */
277 ExifParser.prototype.readDirectory = function(br, tags) {
278   var entryCount = br.readScalar(2);
279   for (var i = 0; i < entryCount; i++) {
280     var tagId = br.readScalar(2);
281     var tag = tags[tagId] = {id: tagId};
282     tag.format = br.readScalar(2);
283     tag.componentCount = br.readScalar(4);
284     this.readTagValue(br, tag);
285   }
286
287   return br.readScalar(4);
288 };
289
290 /**
291  * @param {ByteReader} br Byte reader to be used for reading.
292  * @param {Object} tag Tag object.
293  */
294 ExifParser.prototype.readTagValue = function(br, tag) {
295   var self = this;
296
297   function safeRead(size, readFunction, signed) {
298     try {
299       unsafeRead(size, readFunction, signed);
300     } catch (ex) {
301       self.log('error reading tag 0x' + tag.id.toString(16) + '/' +
302                tag.format + ', size ' + tag.componentCount + '*' + size + ' ' +
303                (ex.stack || '<no stack>') + ': ' + ex);
304       tag.value = null;
305     }
306   }
307
308   function unsafeRead(size, readFunction, signed) {
309     if (!readFunction)
310       readFunction = function(size) { return br.readScalar(size, signed) };
311
312     var totalSize = tag.componentCount * size;
313     if (totalSize < 1) {
314       // This is probably invalid exif data, skip it.
315       tag.componentCount = 1;
316       tag.value = br.readScalar(4);
317       return;
318     }
319
320     if (totalSize > 4) {
321       // If the total size is > 4, the next 4 bytes will be a pointer to the
322       // actual data.
323       br.pushSeek(br.readScalar(4));
324     }
325
326     if (tag.componentCount == 1) {
327       tag.value = readFunction(size);
328     } else {
329       // Read multiple components into an array.
330       tag.value = [];
331       for (var i = 0; i < tag.componentCount; i++)
332         tag.value[i] = readFunction(size);
333     }
334
335     if (totalSize > 4) {
336       // Go back to the previous position if we had to jump to the data.
337       br.popSeek();
338     } else if (totalSize < 4) {
339       // Otherwise, if the value wasn't exactly 4 bytes, skip over the
340       // unread data.
341       br.seek(4 - totalSize, ByteReader.SEEK_CUR);
342     }
343   }
344
345   switch (tag.format) {
346     case 1: // Byte
347     case 7: // Undefined
348       safeRead(1);
349       break;
350
351     case 2: // String
352       safeRead(1);
353       if (tag.componentCount == 0) {
354         tag.value = '';
355       } else if (tag.componentCount == 1) {
356         tag.value = String.fromCharCode(tag.value);
357       } else {
358         tag.value = String.fromCharCode.apply(null, tag.value);
359       }
360       break;
361
362     case 3: // Short
363       safeRead(2);
364       break;
365
366     case 4: // Long
367       safeRead(4);
368       break;
369
370     case 9: // Signed Long
371       safeRead(4, null, true);
372       break;
373
374     case 5: // Rational
375       safeRead(8, function() {
376         return [br.readScalar(4), br.readScalar(4)];
377       });
378       break;
379
380     case 10: // Signed Rational
381       safeRead(8, function() {
382         return [br.readScalar(4, true), br.readScalar(4, true)];
383       });
384       break;
385
386     default: // ???
387       this.vlog('Unknown tag format 0x' + Number(tag.id).toString(16) +
388                 ': ' + tag.format);
389       safeRead(4);
390       break;
391   }
392
393   this.vlog('Read tag: 0x' + tag.id.toString(16) + '/' + tag.format + ': ' +
394             tag.value);
395 };
396
397 /**
398  * Map from the exif orientation value to the horizontal scale value.
399  * @const
400  * @type {Array.<number>}
401  */
402 ExifParser.SCALEX = [1, -1, -1, 1, 1, 1, -1, -1];
403
404 /**
405  * Map from the exif orientation value to the vertical scale value.
406  * @const
407  * @type {Array.<number>}
408  */
409 ExifParser.SCALEY = [1, 1, -1, -1, -1, 1, 1, -1];
410
411 /**
412  * Map from the exit orientation value to the rotation value.
413  * @const
414  * @type {Array.<number>}
415  */
416 ExifParser.ROTATE90 = [0, 0, 0, 0, 1, 1, 1, 1];
417
418 /**
419  * Transform exif-encoded orientation into a set of parameters compatible with
420  * CSS and canvas transforms (scaleX, scaleY, rotation).
421  *
422  * @param {Object} ifd Exif property dictionary (image or thumbnail).
423  * @return {Object} Orientation object.
424  */
425 ExifParser.prototype.parseOrientation = function(ifd) {
426   if (ifd[EXIF_TAG_ORIENTATION]) {
427     var index = (ifd[EXIF_TAG_ORIENTATION].value || 1) - 1;
428     return {
429       scaleX: ExifParser.SCALEX[index],
430       scaleY: ExifParser.SCALEY[index],
431       rotate90: ExifParser.ROTATE90[index]
432     };
433   }
434   return null;
435 };
436
437 MetadataDispatcher.registerParserClass(ExifParser);