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.
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.
10 var EXIF_ALIGN_LITTLE = 0x4949; // Indicates little endian exif data.
11 var EXIF_ALIGN_BIG = 0x4d4d; // Indicates big endian exif data.
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.
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.
21 var EXIF_TAG_ORIENTATION = 0x0112;
22 var EXIF_TAG_X_DIMENSION = 0xA002;
23 var EXIF_TAG_Y_DIMENSION = 0xA003;
25 function ExifParser(parent) {
26 ImageParser.call(this, parent, 'jpeg', /\.jpe?g$/i);
29 ExifParser.prototype = {__proto__: ImageParser.prototype};
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.
37 ExifParser.prototype.parse = function(file, metadata, callback, errorCallback) {
38 this.requestSlice(file, callback, errorCallback, metadata, 0);
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.
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);
55 var reader = new FileReader();
56 reader.onerror = errorCallback;
57 reader.onload = function() { self.parseSlice(
58 file, callback, errorCallback, metadata, filePos, reader.result);
60 reader.readAsArrayBuffer(file.slice(filePos, filePos + opt_length));
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.
71 ExifParser.prototype.parseSlice = function(
72 file, callback, errorCallback, metadata, filePos, buf) {
74 var br = new ByteReader(buf);
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));
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));
89 var reread = function(opt_offset, opt_bytes) {
90 self.requestSlice(file, callback, errorCallback, metadata,
91 filePos + br.tell() + (opt_offset || 0), opt_bytes);
96 // Cannot read the mark and the length, request a minimum-size slice.
101 var mark = this.readMark(br);
102 if (mark == EXIF_MARK_SOS)
103 throw new Error('SOS marker found before SOF');
105 var markLength = this.readMarkLength(br);
107 var nextSectionStart = br.tell() + markLength;
108 if (!br.canRead(markLength)) {
109 // Get the entire section.
110 if (filePos + br.tell() + markLength > file.size) {
112 'Invalid section length @' + (filePos + br.tell() - 2));
114 reread(-4, markLength + 4);
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!
130 br.seek(nextSectionStart, ByteReader.SEEK_BEG);
133 errorCallback(e.toString());
139 * @param {number} mark Mark to be checked.
140 * @return {boolean} True if the mark is SOF.
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;
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);
153 * @param {Object} metadata Metadata object.
154 * @param {ArrayBuffer} buf Buffer to be parsed.
155 * @param {ByteReader} br Byte reader to be used.
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));
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());
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));
179 var tag = br.readScalar(2);
180 if (tag != EXIF_TAG_TIFF) {
181 this.log('Invalid TIFF tag: ' + tag.toString(16));
185 metadata.littleEndian = (order == EXIF_ALIGN_LITTLE);
190 var directoryOffset = br.readScalar(4);
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);
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;
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);
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);
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);
236 this.vlog('Image has EXIF data, but no JPG thumbnail.');
241 * @param {Object} metadata Metadata object.
242 * @param {number} width Width in pixels.
243 * @param {number} height Height in pixels.
245 ExifParser.setImageSize = function(metadata, width, height) {
246 if (metadata.imageTransform && metadata.imageTransform.rotate90) {
247 metadata.width = height;
248 metadata.height = width;
250 metadata.width = width;
251 metadata.height = height;
256 * @param {ByteReader} br Byte reader to be used for reading.
257 * @return {number} Mark value.
259 ExifParser.prototype.readMark = function(br) {
260 return br.readScalar(2);
264 * @param {ByteReader} br Bye reader to be used for reading.
265 * @return {number} Size of the mark at the current position.
267 ExifParser.prototype.readMarkLength = function(br) {
268 // Length includes the 2 bytes used to store the length.
269 return br.readScalar(2) - 2;
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.
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);
287 return br.readScalar(4);
291 * @param {ByteReader} br Byte reader to be used for reading.
292 * @param {Object} tag Tag object.
294 ExifParser.prototype.readTagValue = function(br, tag) {
297 function safeRead(size, readFunction, signed) {
299 unsafeRead(size, readFunction, signed);
301 self.log('error reading tag 0x' + tag.id.toString(16) + '/' +
302 tag.format + ', size ' + tag.componentCount + '*' + size + ' ' +
303 (ex.stack || '<no stack>') + ': ' + ex);
308 function unsafeRead(size, readFunction, signed) {
310 readFunction = function(size) { return br.readScalar(size, signed) };
312 var totalSize = tag.componentCount * size;
314 // This is probably invalid exif data, skip it.
315 tag.componentCount = 1;
316 tag.value = br.readScalar(4);
321 // If the total size is > 4, the next 4 bytes will be a pointer to the
323 br.pushSeek(br.readScalar(4));
326 if (tag.componentCount == 1) {
327 tag.value = readFunction(size);
329 // Read multiple components into an array.
331 for (var i = 0; i < tag.componentCount; i++)
332 tag.value[i] = readFunction(size);
336 // Go back to the previous position if we had to jump to the data.
338 } else if (totalSize < 4) {
339 // Otherwise, if the value wasn't exactly 4 bytes, skip over the
341 br.seek(4 - totalSize, ByteReader.SEEK_CUR);
345 switch (tag.format) {
353 if (tag.componentCount == 0) {
355 } else if (tag.componentCount == 1) {
356 tag.value = String.fromCharCode(tag.value);
358 tag.value = String.fromCharCode.apply(null, tag.value);
370 case 9: // Signed Long
371 safeRead(4, null, true);
375 safeRead(8, function() {
376 return [br.readScalar(4), br.readScalar(4)];
380 case 10: // Signed Rational
381 safeRead(8, function() {
382 return [br.readScalar(4, true), br.readScalar(4, true)];
387 this.vlog('Unknown tag format 0x' + Number(tag.id).toString(16) +
393 this.vlog('Read tag: 0x' + tag.id.toString(16) + '/' + tag.format + ': ' +
398 * Map from the exif orientation value to the horizontal scale value.
400 * @type {Array.<number>}
402 ExifParser.SCALEX = [1, -1, -1, 1, 1, 1, -1, -1];
405 * Map from the exif orientation value to the vertical scale value.
407 * @type {Array.<number>}
409 ExifParser.SCALEY = [1, 1, -1, -1, -1, 1, 1, -1];
412 * Map from the exit orientation value to the rotation value.
414 * @type {Array.<number>}
416 ExifParser.ROTATE90 = [0, 0, 0, 0, 1, 1, 1, 1];
419 * Transform exif-encoded orientation into a set of parameters compatible with
420 * CSS and canvas transforms (scaleX, scaleY, rotation).
422 * @param {Object} ifd Exif property dictionary (image or thumbnail).
423 * @return {Object} Orientation object.
425 ExifParser.prototype.parseOrientation = function(ifd) {
426 if (ifd[EXIF_TAG_ORIENTATION]) {
427 var index = (ifd[EXIF_TAG_ORIENTATION].value || 1) - 1;
429 scaleX: ExifParser.SCALEX[index],
430 scaleY: ExifParser.SCALEY[index],
431 rotate90: ExifParser.ROTATE90[index]
437 MetadataDispatcher.registerParserClass(ExifParser);