Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(80)

Side by Side Diff: chrome/browser/resources/file_manager/js/metadata/exif_parser.js

Issue 39123003: [Files.app] Split the JavaScript files into subdirectories: common, background, and foreground (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: fixed test failure. Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
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);
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698