| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| 'use strict'; |
| |
| var EXIF_MARK_SOI = 0xffd8; // Start of image data. |
| var EXIF_MARK_SOS = 0xffda; // Start of "stream" (the actual image data). |
| var EXIF_MARK_SOF = 0xffc0; // Start of "frame" |
| var EXIF_MARK_EXIF = 0xffe1; // Start of exif block. |
| |
| var EXIF_ALIGN_LITTLE = 0x4949; // Indicates little endian exif data. |
| var EXIF_ALIGN_BIG = 0x4d4d; // Indicates big endian exif data. |
| |
| var EXIF_TAG_TIFF = 0x002a; // First directory containing TIFF data. |
| var EXIF_TAG_GPSDATA = 0x8825; // Pointer from TIFF to the GPS directory. |
| var EXIF_TAG_EXIFDATA = 0x8769; // Pointer from TIFF to the EXIF IFD. |
| var EXIF_TAG_SUBIFD = 0x014a; // Pointer from TIFF to "Extra" IFDs. |
| |
| var EXIF_TAG_JPG_THUMB_OFFSET = 0x0201; // Pointer from TIFF to thumbnail. |
| var EXIF_TAG_JPG_THUMB_LENGTH = 0x0202; // Length of thumbnail data. |
| |
| var EXIF_TAG_ORIENTATION = 0x0112; |
| var EXIF_TAG_X_DIMENSION = 0xA002; |
| var EXIF_TAG_Y_DIMENSION = 0xA003; |
| |
| function ExifParser(parent) { |
| ImageParser.call(this, parent, 'jpeg', /\.jpe?g$/i); |
| } |
| |
| ExifParser.prototype = {__proto__: ImageParser.prototype}; |
| |
| /** |
| * @param {File} file // TODO(JSDOC). |
| * @param {Object} metadata // TODO(JSDOC). |
| * @param {function} callback // TODO(JSDOC). |
| * @param {function} errorCallback // TODO(JSDOC). |
| */ |
| ExifParser.prototype.parse = function(file, metadata, callback, errorCallback) { |
| this.requestSlice(file, callback, errorCallback, metadata, 0); |
| }; |
| |
| /** |
| * @param {File} file // TODO(JSDOC). |
| * @param {function} callback // TODO(JSDOC). |
| * @param {function} errorCallback // TODO(JSDOC). |
| * @param {Object} metadata // TODO(JSDOC). |
| * @param {number} filePos // TODO(JSDOC). |
| * @param {number=} opt_length // TODO(JSDOC). |
| */ |
| ExifParser.prototype.requestSlice = function( |
| file, callback, errorCallback, metadata, filePos, opt_length) { |
| // Read at least 1Kb so that we do not issue too many read requests. |
| opt_length = Math.max(1024, opt_length || 0); |
| |
| var self = this; |
| var reader = new FileReader(); |
| reader.onerror = errorCallback; |
| reader.onload = function() { self.parseSlice( |
| file, callback, errorCallback, metadata, filePos, reader.result); |
| }; |
| reader.readAsArrayBuffer(file.slice(filePos, filePos + opt_length)); |
| }; |
| |
| /** |
| * @param {File} file // TODO(JSDOC). |
| * @param {function} callback // TODO(JSDOC). |
| * @param {function} errorCallback // TODO(JSDOC). |
| * @param {Object} metadata // TODO(JSDOC). |
| * @param {number} filePos // TODO(JSDOC). |
| * @param {ArrayBuffer} buf // TODO(JSDOC). |
| */ |
| ExifParser.prototype.parseSlice = function( |
| file, callback, errorCallback, metadata, filePos, buf) { |
| try { |
| var br = new ByteReader(buf); |
| |
| if (!br.canRead(4)) { |
| // We never ask for less than 4 bytes. This can only mean we reached EOF. |
| throw new Error('Unexpected EOF @' + (filePos + buf.byteLength)); |
| } |
| |
| if (filePos == 0) { |
| // First slice, check for the SOI mark. |
| var firstMark = this.readMark(br); |
| if (firstMark != EXIF_MARK_SOI) |
| throw new Error('Invalid file header: ' + firstMark.toString(16)); |
| } |
| |
| var self = this; |
| var reread = function(opt_offset, opt_bytes) { |
| self.requestSlice(file, callback, errorCallback, metadata, |
| filePos + br.tell() + (opt_offset || 0), opt_bytes); |
| }; |
| |
| while (true) { |
| if (!br.canRead(4)) { |
| // Cannot read the mark and the length, request a minimum-size slice. |
| reread(); |
| return; |
| } |
| |
| var mark = this.readMark(br); |
| if (mark == EXIF_MARK_SOS) |
| throw new Error('SOS marker found before SOF'); |
| |
| var markLength = this.readMarkLength(br); |
| |
| var nextSectionStart = br.tell() + markLength; |
| if (!br.canRead(markLength)) { |
| // Get the entire section. |
| if (filePos + br.tell() + markLength > file.size) { |
| throw new Error( |
| 'Invalid section length @' + (filePos + br.tell() - 2)); |
| } |
| reread(-4, markLength + 4); |
| return; |
| } |
| |
| if (mark == EXIF_MARK_EXIF) { |
| this.parseExifSection(metadata, buf, br); |
| } else if (ExifParser.isSOF_(mark)) { |
| // The most reliable size information is encoded in the SOF section. |
| br.seek(1, ByteReader.SEEK_CUR); // Skip the precision byte. |
| var height = br.readScalar(2); |
| var width = br.readScalar(2); |
| ExifParser.setImageSize(metadata, width, height); |
| callback(metadata); // We are done! |
| return; |
| } |
| |
| br.seek(nextSectionStart, ByteReader.SEEK_BEG); |
| } |
| } catch (e) { |
| errorCallback(e.toString()); |
| } |
| }; |
| |
| /** |
| * @private |
| * @param {number} mark // TODO(JSDOC). |
| * @return {boolean} // TODO(JSDOC). |
| */ |
| ExifParser.isSOF_ = function(mark) { |
| // There are 13 variants of SOF fragment format distinguished by the last |
| // hex digit of the mark, but the part we want is always the same. |
| if ((mark & ~0xF) != EXIF_MARK_SOF) return false; |
| |
| // If the last digit is 4, 8 or 12 it is not really a SOF. |
| var type = mark & 0xF; |
| return (type != 4 && type != 8 && type != 12); |
| }; |
| |
| /** |
| * @param {Object} metadata // TODO(JSDOC). |
| * @param {ArrayBuffer} buf // TODO(JSDOC). |
| * @param {ByteReader} br // TODO(JSDOC). |
| */ |
| ExifParser.prototype.parseExifSection = function(metadata, buf, br) { |
| var magic = br.readString(6); |
| if (magic != 'Exif\0\0') { |
| // Some JPEG files may have sections marked with EXIF_MARK_EXIF |
| // but containing something else (e.g. XML text). Ignore such sections. |
| this.vlog('Invalid EXIF magic: ' + magic + br.readString(100)); |
| return; |
| } |
| |
| // Offsets inside the EXIF block are based after the magic string. |
| // Create a new ByteReader based on the current position to make offset |
| // calculations simpler. |
| br = new ByteReader(buf, br.tell()); |
| |
| var order = br.readScalar(2); |
| if (order == EXIF_ALIGN_LITTLE) { |
| br.setByteOrder(ByteReader.LITTLE_ENDIAN); |
| } else if (order != EXIF_ALIGN_BIG) { |
| this.log('Invalid alignment value: ' + order.toString(16)); |
| return; |
| } |
| |
| var tag = br.readScalar(2); |
| if (tag != EXIF_TAG_TIFF) { |
| this.log('Invalid TIFF tag: ' + tag.toString(16)); |
| return; |
| } |
| |
| metadata.littleEndian = (order == EXIF_ALIGN_LITTLE); |
| metadata.ifd = { |
| image: {}, |
| thumbnail: {} |
| }; |
| var directoryOffset = br.readScalar(4); |
| |
| // Image directory. |
| this.vlog('Read image directory.'); |
| br.seek(directoryOffset); |
| directoryOffset = this.readDirectory(br, metadata.ifd.image); |
| metadata.imageTransform = this.parseOrientation(metadata.ifd.image); |
| |
| // Thumbnail Directory chained from the end of the image directory. |
| if (directoryOffset) { |
| this.vlog('Read thumbnail directory.'); |
| br.seek(directoryOffset); |
| this.readDirectory(br, metadata.ifd.thumbnail); |
| // If no thumbnail orientation is encoded, assume same orientation as |
| // the primary image. |
| metadata.thumbnailTransform = |
| this.parseOrientation(metadata.ifd.thumbnail) || |
| metadata.imageTransform; |
| } |
| |
| // EXIF Directory may be specified as a tag in the image directory. |
| if (EXIF_TAG_EXIFDATA in metadata.ifd.image) { |
| this.vlog('Read EXIF directory.'); |
| directoryOffset = metadata.ifd.image[EXIF_TAG_EXIFDATA].value; |
| br.seek(directoryOffset); |
| metadata.ifd.exif = {}; |
| this.readDirectory(br, metadata.ifd.exif); |
| } |
| |
| // GPS Directory may also be linked from the image directory. |
| if (EXIF_TAG_GPSDATA in metadata.ifd.image) { |
| this.vlog('Read GPS directory.'); |
| directoryOffset = metadata.ifd.image[EXIF_TAG_GPSDATA].value; |
| br.seek(directoryOffset); |
| metadata.ifd.gps = {}; |
| this.readDirectory(br, metadata.ifd.gps); |
| } |
| |
| // Thumbnail may be linked from the image directory. |
| if (EXIF_TAG_JPG_THUMB_OFFSET in metadata.ifd.thumbnail && |
| EXIF_TAG_JPG_THUMB_LENGTH in metadata.ifd.thumbnail) { |
| this.vlog('Read thumbnail image.'); |
| br.seek(metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_OFFSET].value); |
| metadata.thumbnailURL = br.readImage( |
| metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_LENGTH].value); |
| } else { |
| this.vlog('Image has EXIF data, but no JPG thumbnail.'); |
| } |
| }; |
| |
| /** |
| * @param {Object} metadata // TODO(JSDOC). |
| * @param {number} width // TODO(JSDOC). |
| * @param {number} height // TODO(JSDOC). |
| */ |
| ExifParser.setImageSize = function(metadata, width, height) { |
| if (metadata.imageTransform && metadata.imageTransform.rotate90) { |
| metadata.width = height; |
| metadata.height = width; |
| } else { |
| metadata.width = width; |
| metadata.height = height; |
| } |
| }; |
| |
| /** |
| * @param {ByteReader} br // TODO(JSDOC). |
| * @return {number} // TODO(JSDOC). |
| */ |
| ExifParser.prototype.readMark = function(br) { |
| return br.readScalar(2); |
| }; |
| |
| /** |
| * @param {ByteReader} br // TODO(JSDOC). |
| * @return {number} // TODO(JSDOC). |
| */ |
| ExifParser.prototype.readMarkLength = function(br) { |
| // Length includes the 2 bytes used to store the length. |
| return br.readScalar(2) - 2; |
| }; |
| |
| /** |
| * @param {ByteReader} br // TODO(JSDOC). |
| * @param {Array.<Object>} tags // TODO(JSDOC). |
| * @return {number} // TODO(JSDOC). |
| */ |
| ExifParser.prototype.readDirectory = function(br, tags) { |
| var entryCount = br.readScalar(2); |
| for (var i = 0; i < entryCount; i++) { |
| var tagId = br.readScalar(2); |
| var tag = tags[tagId] = {id: tagId}; |
| tag.format = br.readScalar(2); |
| tag.componentCount = br.readScalar(4); |
| this.readTagValue(br, tag); |
| } |
| |
| return br.readScalar(4); |
| }; |
| |
| /** |
| * @param {ByteReader} br // TODO(JSDOC). |
| * @param {Object} tag // TODO(JSDOC). |
| */ |
| ExifParser.prototype.readTagValue = function(br, tag) { |
| var self = this; |
| |
| function safeRead(size, readFunction, signed) { |
| try { |
| unsafeRead(size, readFunction, signed); |
| } catch (ex) { |
| self.log('error reading tag 0x' + tag.id.toString(16) + '/' + |
| tag.format + ', size ' + tag.componentCount + '*' + size + ' ' + |
| (ex.stack || '<no stack>') + ': ' + ex); |
| tag.value = null; |
| } |
| } |
| |
| function unsafeRead(size, readFunction, signed) { |
| if (!readFunction) |
| readFunction = function(size) { return br.readScalar(size, signed) }; |
| |
| var totalSize = tag.componentCount * size; |
| if (totalSize < 1) { |
| // This is probably invalid exif data, skip it. |
| tag.componentCount = 1; |
| tag.value = br.readScalar(4); |
| return; |
| } |
| |
| if (totalSize > 4) { |
| // If the total size is > 4, the next 4 bytes will be a pointer to the |
| // actual data. |
| br.pushSeek(br.readScalar(4)); |
| } |
| |
| if (tag.componentCount == 1) { |
| tag.value = readFunction(size); |
| } else { |
| // Read multiple components into an array. |
| tag.value = []; |
| for (var i = 0; i < tag.componentCount; i++) |
| tag.value[i] = readFunction(size); |
| } |
| |
| if (totalSize > 4) { |
| // Go back to the previous position if we had to jump to the data. |
| br.popSeek(); |
| } else if (totalSize < 4) { |
| // Otherwise, if the value wasn't exactly 4 bytes, skip over the |
| // unread data. |
| br.seek(4 - totalSize, ByteReader.SEEK_CUR); |
| } |
| } |
| |
| switch (tag.format) { |
| case 1: // Byte |
| case 7: // Undefined |
| safeRead(1); |
| break; |
| |
| case 2: // String |
| safeRead(1); |
| if (tag.componentCount == 0) { |
| tag.value = ''; |
| } else if (tag.componentCount == 1) { |
| tag.value = String.fromCharCode(tag.value); |
| } else { |
| tag.value = String.fromCharCode.apply(null, tag.value); |
| } |
| break; |
| |
| case 3: // Short |
| safeRead(2); |
| break; |
| |
| case 4: // Long |
| safeRead(4); |
| break; |
| |
| case 9: // Signed Long |
| safeRead(4, null, true); |
| break; |
| |
| case 5: // Rational |
| safeRead(8, function() { |
| return [br.readScalar(4), br.readScalar(4)]; |
| }); |
| break; |
| |
| case 10: // Signed Rational |
| safeRead(8, function() { |
| return [br.readScalar(4, true), br.readScalar(4, true)]; |
| }); |
| break; |
| |
| default: // ??? |
| this.vlog('Unknown tag format 0x' + Number(tag.id).toString(16) + |
| ': ' + tag.format); |
| safeRead(4); |
| break; |
| } |
| |
| this.vlog('Read tag: 0x' + tag.id.toString(16) + '/' + tag.format + ': ' + |
| tag.value); |
| }; |
| |
| /** |
| * TODO(JSDOC) |
| * @const |
| * @type {Array.<number>} |
| */ |
| ExifParser.SCALEX = [1, -1, -1, 1, 1, 1, -1, -1]; |
| |
| /** |
| * TODO(JSDOC) |
| * @const |
| * @type {Array.<number>} |
| */ |
| ExifParser.SCALEY = [1, 1, -1, -1, -1, 1, 1, -1]; |
| |
| /** |
| * TODO(JSDOC) |
| * @const |
| * @type {Array.<number>} |
| */ |
| ExifParser.ROTATE90 = [0, 0, 0, 0, 1, 1, 1, 1]; |
| |
| /** |
| * Transform exif-encoded orientation into a set of parameters compatible with |
| * CSS and canvas transforms (scaleX, scaleY, rotation). |
| * |
| * @param {Object} ifd exif property dictionary (image or thumbnail). |
| * @return {Object} // TODO(JSDOC). |
| */ |
| ExifParser.prototype.parseOrientation = function(ifd) { |
| if (ifd[EXIF_TAG_ORIENTATION]) { |
| var index = (ifd[EXIF_TAG_ORIENTATION].value || 1) - 1; |
| return { |
| scaleX: ExifParser.SCALEX[index], |
| scaleY: ExifParser.SCALEY[index], |
| rotate90: ExifParser.ROTATE90[index] |
| }; |
| } |
| return null; |
| }; |
| |
| MetadataDispatcher.registerParserClass(ExifParser); |