OLD | NEW |
| (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); | |
OLD | NEW |