| OLD | NEW |
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 const EXIF_MARK_SOI = 0xffd8; // Start of image data. | 5 const EXIF_MARK_SOI = 0xffd8; // Start of image data. |
| 6 const EXIF_MARK_SOS = 0xffda; // Start of "stream" (the actual image data). | 6 const EXIF_MARK_SOS = 0xffda; // Start of "stream" (the actual image data). |
| 7 const EXIF_MARK_SOF = 0xffc0; // Start of "frame" | 7 const EXIF_MARK_SOF = 0xffc0; // Start of "frame" |
| 8 const EXIF_MARK_EXIF = 0xffe1; // Start of exif block. | 8 const EXIF_MARK_EXIF = 0xffe1; // Start of exif block. |
| 9 | 9 |
| 10 const EXIF_ALIGN_LITTLE = 0x4949; // Indicates little endian exif data. | 10 const EXIF_ALIGN_LITTLE = 0x4949; // Indicates little endian exif data. |
| (...skipping 11 matching lines...) Expand all Loading... |
| 22 const EXIF_TAG_X_DIMENSION = 0xA002; | 22 const EXIF_TAG_X_DIMENSION = 0xA002; |
| 23 const EXIF_TAG_Y_DIMENSION = 0xA003; | 23 const EXIF_TAG_Y_DIMENSION = 0xA003; |
| 24 | 24 |
| 25 function ExifParser(parent) { | 25 function ExifParser(parent) { |
| 26 ImageParser.call(this, parent, 'jpeg', /\.jpe?g$/i); | 26 ImageParser.call(this, parent, 'jpeg', /\.jpe?g$/i); |
| 27 } | 27 } |
| 28 | 28 |
| 29 ExifParser.prototype = {__proto__: ImageParser.prototype}; | 29 ExifParser.prototype = {__proto__: ImageParser.prototype}; |
| 30 | 30 |
| 31 ExifParser.prototype.parse = function(file, callback, errorCallback) { | 31 ExifParser.prototype.parse = function(file, callback, errorCallback) { |
| 32 var metadata = this.createDefaultMetadata(); |
| 33 this.requestSlice(file, callback, errorCallback, metadata, 0); |
| 34 }; |
| 35 |
| 36 ExifParser.prototype.requestSlice = function ( |
| 37 file, callback, errorCallback, metadata, filePos, opt_length) { |
| 38 // Read at least 1Kb so that we do not issue too many read requests. |
| 39 opt_length = Math.max(1024, opt_length || 0); |
| 40 |
| 32 var self = this; | 41 var self = this; |
| 33 var currentStep = -1; | 42 var reader = new FileReader(); |
| 43 reader.onerror = errorCallback; |
| 44 reader.onload = function() { self.parseSlice( |
| 45 file, callback, errorCallback, metadata, filePos, reader.result); |
| 46 }; |
| 47 reader.readAsArrayBuffer(file.webkitSlice(filePos, filePos + opt_length)); |
| 48 }; |
| 34 | 49 |
| 35 function nextStep(var_args) { | 50 ExifParser.prototype.parseSlice = function( |
| 36 self.vlog('exif nextStep: ' + steps[currentStep + 1].name); | 51 file, callback, errorCallback, metadata, filePos, buf) { |
| 37 try { | 52 try { |
| 38 steps[++currentStep].apply(null, arguments); | 53 var br = new ByteReader(buf); |
| 39 } catch(e) { | 54 |
| 40 onError(e.stack || e.toString()); | 55 if (!br.canRead(4)) { |
| 56 // We never ask for less than 4 bytes. This can only mean we reached EOF. |
| 57 throw new Error('Unexpected EOF @' + (filePos + buf.byteLength)); |
| 41 } | 58 } |
| 59 |
| 60 if (filePos == 0) { |
| 61 // First slice, check for the SOI mark. |
| 62 var firstMark = this.readMark(br); |
| 63 if (firstMark != EXIF_MARK_SOI) |
| 64 throw new Error('Invalid file header: ' + firstMark.toString(16)); |
| 65 } |
| 66 |
| 67 var self = this; |
| 68 function reread(opt_offset, opt_bytes) { |
| 69 self.requestSlice(file, callback, errorCallback, metadata, |
| 70 filePos + br.tell() + (opt_offset || 0), opt_bytes); |
| 71 } |
| 72 |
| 73 while (true) { |
| 74 if (!br.canRead(4)) { |
| 75 // Cannot read the mark and the length, request a minimum-size slice. |
| 76 reread(); |
| 77 return; |
| 78 } |
| 79 |
| 80 var mark = this.readMark(br); |
| 81 if (mark == EXIF_MARK_SOS) |
| 82 throw new Error('SOS marker found before SOF'); |
| 83 |
| 84 var markLength = this.readMarkLength(br); |
| 85 |
| 86 var nextSectionStart = br.tell() + markLength; |
| 87 if (!br.canRead(markLength)) { |
| 88 // Get the entire section. |
| 89 reread(-4, markLength + 4); |
| 90 return; |
| 91 } |
| 92 |
| 93 if (mark == EXIF_MARK_EXIF) { |
| 94 this.parseExifSection(metadata, buf, br); |
| 95 } else if (mark == EXIF_MARK_SOF) { |
| 96 // The most reliable size information is encoded in the SOF section. |
| 97 br.seek(1, ByteReader.SEEK_CUR); // Skip the precision byte. |
| 98 var height = br.readScalar(2); |
| 99 var width = br.readScalar(2); |
| 100 ExifParser.setImageSize(metadata, width, height); |
| 101 callback(metadata); // We are done! |
| 102 return; |
| 103 } |
| 104 |
| 105 br.seek(nextSectionStart, ByteReader.SEEK_BEG); |
| 106 } |
| 107 } catch (e) { |
| 108 errorCallback(e.toString()); |
| 109 } |
| 110 }; |
| 111 |
| 112 ExifParser.prototype.parseExifSection = function(metadata, buf, br) { |
| 113 var magic = br.readString(6); |
| 114 if (magic != 'Exif\0\0') { |
| 115 // Some JPEG files may have sections marked with EXIF_MARK_EXIF |
| 116 // but containing something else (e.g. XML text). Ignore such sections. |
| 117 this.vlog('Invalid EXIF magic: ' + magic + br.readString(100)); |
| 118 return; |
| 42 } | 119 } |
| 43 | 120 |
| 44 function onError(err) { | 121 // Offsets inside the EXIF block are based after the magic string. |
| 45 errorCallback(err, steps[currentStep].name); | 122 // Create a new ByteReader based on the current position to make offset |
| 123 // calculations simpler. |
| 124 br = new ByteReader(buf, br.tell()); |
| 125 |
| 126 var order = br.readScalar(2); |
| 127 if (order == EXIF_ALIGN_LITTLE) { |
| 128 br.setByteOrder(ByteReader.LITTLE_ENDIAN); |
| 129 } else if (order != EXIF_ALIGN_BIG) { |
| 130 this.log('Invalid alignment value: ' + order.toString(16)); |
| 131 return; |
| 46 } | 132 } |
| 47 | 133 |
| 48 var steps = | 134 var tag = br.readScalar(2); |
| 49 [ // Step one, read the file header into a byte array. | 135 if (tag != EXIF_TAG_TIFF) { |
| 50 function readHeader(file) { | 136 this.log('Invalid TIFF tag: ' + tag.toString(16)); |
| 51 var reader = new FileReader(); | 137 return; |
| 52 reader.onerror = onError; | 138 } |
| 53 reader.onload = function(event) { nextStep(file, reader.result) }; | |
| 54 reader.readAsArrayBuffer(file.webkitSlice(0, 1024)); | |
| 55 }, | |
| 56 | 139 |
| 57 // Step two, find the exif marker and read all exif data. | 140 metadata.littleEndian = (order == EXIF_ALIGN_LITTLE); |
| 58 function findExif(file, buf) { | 141 metadata.ifd = { |
| 59 var br = new ByteReader(buf); | 142 image: {}, |
| 60 var mark = self.readMark(br); | 143 thumbnail: {} |
| 61 if (mark != EXIF_MARK_SOI) | 144 }; |
| 62 return onError('Invalid file header: ' + mark.toString(16)); | 145 var directoryOffset = br.readScalar(4); |
| 63 | 146 |
| 64 while (true) { | 147 // Image directory. |
| 65 if (mark == EXIF_MARK_SOS || br.eof()) { | 148 this.vlog('Read image directory.'); |
| 66 return onError('Unable to find EXIF or SOF marker'); | 149 br.seek(directoryOffset); |
| 67 } | 150 directoryOffset = this.readDirectory(br, metadata.ifd.image); |
| 151 metadata.imageTransform = this.parseOrientation(metadata.ifd.image); |
| 68 | 152 |
| 69 mark = self.readMark(br); | 153 // Thumbnail Directory chained from the end of the image directory. |
| 70 if (mark == EXIF_MARK_SOF) { | 154 if (directoryOffset) { |
| 71 // If we reached this section first then there is no EXIF data. | 155 this.vlog('Read thumbnail directory.'); |
| 72 // Extract image dimensions and return. | 156 br.seek(directoryOffset); |
| 157 this.readDirectory(br, metadata.ifd.thumbnail); |
| 158 // If no thumbnail orientation is encoded, assume same orientation as |
| 159 // the primary image. |
| 160 metadata.thumbnailTransform = |
| 161 this.parseOrientation(metadata.ifd.thumbnail) || |
| 162 metadata.imageTransform; |
| 163 } |
| 73 | 164 |
| 74 // TODO(kaznacheev) Here we are assuming that SOF section lies within | 165 // EXIF Directory may be specified as a tag in the image directory. |
| 75 // first 1024 bytes. This must be true for any normal JPEG file | 166 if (EXIF_TAG_EXIFDATA in metadata.ifd.image) { |
| 76 // with no EXIF data. Still might want to handle this more carefully. | 167 this.vlog('Read EXIF directory.'); |
| 77 if (br.tell() + 7 < buf.byteLength) { | 168 directoryOffset = metadata.ifd.image[EXIF_TAG_EXIFDATA].value; |
| 78 br.seek(3, ByteReader.SEEK_CUR); | 169 br.seek(directoryOffset); |
| 79 var metadata = self.createDefaultMetadata(); | 170 metadata.ifd.exif = {}; |
| 80 metadata.width = br.readScalar(2); | 171 this.readDirectory(br, metadata.ifd.exif); |
| 81 metadata.height = br.readScalar(2); | 172 } |
| 82 callback(metadata); | |
| 83 return; | |
| 84 } | |
| 85 } | |
| 86 if (mark == EXIF_MARK_EXIF) { | |
| 87 var length = self.readMarkLength(br); | |
| 88 | 173 |
| 89 // Offsets inside the EXIF block are based after this bit of | 174 // GPS Directory may also be linked from the image directory. |
| 90 // magic, so we verify and discard it here, before exif parsing, | 175 if (EXIF_TAG_GPSDATA in metadata.ifd.image) { |
| 91 // to make offset calculations simpler. | 176 this.vlog('Read GPS directory.'); |
| 92 var magic = br.readString(6); | 177 directoryOffset = metadata.ifd.image[EXIF_TAG_GPSDATA].value; |
| 93 if (magic != 'Exif\0\0') | 178 br.seek(directoryOffset); |
| 94 return onError('Invalid EXIF magic: ' + magic.toString(16)); | 179 metadata.ifd.gps = {}; |
| 180 this.readDirectory(br, metadata.ifd.gps); |
| 181 } |
| 95 | 182 |
| 96 var pos = br.tell(); | 183 // Thumbnail may be linked from the image directory. |
| 97 var reader = new FileReader(); | 184 if (EXIF_TAG_JPG_THUMB_OFFSET in metadata.ifd.thumbnail && |
| 98 reader.onerror = onError; | 185 EXIF_TAG_JPG_THUMB_LENGTH in metadata.ifd.thumbnail) { |
| 99 reader.onload = function(event) { nextStep(file, reader.result) }; | 186 this.vlog('Read thumbnail image.'); |
| 100 reader.readAsArrayBuffer(file.webkitSlice(pos, pos + length - 6)); | 187 br.seek(metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_OFFSET].value); |
| 101 return; | 188 metadata.thumbnailURL = br.readImage( |
| 102 } | 189 metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_LENGTH].value); |
| 190 } else { |
| 191 this.vlog('Image has EXIF data, but no JPG thumbnail.'); |
| 192 } |
| 193 }; |
| 103 | 194 |
| 104 self.skipMarkData(br); | 195 ExifParser.setImageSize = function(metadata, width, height) { |
| 105 } | 196 if (metadata.imageTransform && metadata.imageTransform.rotate90) { |
| 106 }, | 197 metadata.width = height; |
| 107 | 198 metadata.height = width; |
| 108 // Step three, parse the exif data. | 199 } else { |
| 109 function readDirectories(file, buf) { | 200 metadata.width = width; |
| 110 var br = new ByteReader(buf); | 201 metadata.height = height; |
| 111 var order = br.readScalar(2); | 202 } |
| 112 if (order == EXIF_ALIGN_LITTLE) { | |
| 113 br.setByteOrder(ByteReader.LITTLE_ENDIAN); | |
| 114 } else if (order != EXIF_ALIGN_BIG) { | |
| 115 return onError('Invalid alignment value: ' + order.toString(16)); | |
| 116 } | |
| 117 | |
| 118 var tag = br.readScalar(2); | |
| 119 if (tag != EXIF_TAG_TIFF) | |
| 120 return onError('Invalid TIFF tag: ' + tag.toString(16)); | |
| 121 | |
| 122 var metadata = self.createDefaultMetadata(); | |
| 123 metadata.littleEndian = (order == EXIF_ALIGN_LITTLE); | |
| 124 metadata.ifd = { | |
| 125 image: {}, | |
| 126 thumbnail: {} | |
| 127 }; | |
| 128 var directoryOffset = br.readScalar(4); | |
| 129 | |
| 130 // Image directory. | |
| 131 self.vlog('Read image directory.'); | |
| 132 br.seek(directoryOffset); | |
| 133 directoryOffset = self.readDirectory(br, metadata.ifd.image); | |
| 134 metadata.imageTransform = self.parseOrientation(metadata.ifd.image); | |
| 135 | |
| 136 // Thumbnail Directory chained from the end of the image directory. | |
| 137 if (directoryOffset) { | |
| 138 self.vlog('Read thumbnail directory.'); | |
| 139 br.seek(directoryOffset); | |
| 140 self.readDirectory(br, metadata.ifd.thumbnail); | |
| 141 // If no thumbnail orientation is encoded, assume same orientation as | |
| 142 // the primary image. | |
| 143 metadata.thumbnailTransform = | |
| 144 self.parseOrientation(metadata.ifd.thumbnail) || | |
| 145 metadata.imageTransform; | |
| 146 } | |
| 147 | |
| 148 // EXIF Directory may be specified as a tag in the image directory. | |
| 149 if (EXIF_TAG_EXIFDATA in metadata.ifd.image) { | |
| 150 self.vlog('Read EXIF directory.'); | |
| 151 directoryOffset = metadata.ifd.image[EXIF_TAG_EXIFDATA].value; | |
| 152 br.seek(directoryOffset); | |
| 153 metadata.ifd.exif = {}; | |
| 154 self.readDirectory(br, metadata.ifd.exif); | |
| 155 | |
| 156 if (EXIF_TAG_X_DIMENSION in metadata.ifd.exif && | |
| 157 EXIF_TAG_Y_DIMENSION in metadata.ifd.exif) { | |
| 158 if (metadata.imageTransform && metadata.imageTransform.rotate90) { | |
| 159 metadata.width = metadata.ifd.exif[EXIF_TAG_Y_DIMENSION].value; | |
| 160 metadata.height = metadata.ifd.exif[EXIF_TAG_X_DIMENSION].value; | |
| 161 } else { | |
| 162 metadata.width = metadata.ifd.exif[EXIF_TAG_X_DIMENSION].value; | |
| 163 metadata.height = metadata.ifd.exif[EXIF_TAG_Y_DIMENSION].value; | |
| 164 } | |
| 165 } | |
| 166 } | |
| 167 | |
| 168 // GPS Directory may also be linked from the image directory. | |
| 169 if (EXIF_TAG_GPSDATA in metadata.ifd.image) { | |
| 170 self.vlog('Read GPS directory.'); | |
| 171 directoryOffset = metadata.ifd.image[EXIF_TAG_GPSDATA].value; | |
| 172 br.seek(directoryOffset); | |
| 173 metadata.ifd.gps = {}; | |
| 174 self.readDirectory(br, metadata.ifd.gps); | |
| 175 } | |
| 176 | |
| 177 // Thumbnail may be linked from the image directory. | |
| 178 if (EXIF_TAG_JPG_THUMB_OFFSET in metadata.ifd.thumbnail && | |
| 179 EXIF_TAG_JPG_THUMB_LENGTH in metadata.ifd.thumbnail) { | |
| 180 self.vlog('Read thumbnail image.'); | |
| 181 br.seek(metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_OFFSET].value); | |
| 182 metadata.thumbnailURL = br.readImage( | |
| 183 metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_LENGTH].value); | |
| 184 } else { | |
| 185 self.vlog('Image has EXIF data, but no JPG thumbnail.'); | |
| 186 } | |
| 187 | |
| 188 nextStep(metadata); | |
| 189 }, | |
| 190 | |
| 191 // Step four, we're done. | |
| 192 callback | |
| 193 ]; | |
| 194 | |
| 195 nextStep(file); | |
| 196 }; | 203 }; |
| 197 | 204 |
| 198 ExifParser.prototype.readMark = function(br) { | 205 ExifParser.prototype.readMark = function(br) { |
| 199 return br.readScalar(2); | 206 return br.readScalar(2); |
| 200 }; | 207 }; |
| 201 | 208 |
| 202 ExifParser.prototype.readMarkLength = function(br) { | 209 ExifParser.prototype.readMarkLength = function(br) { |
| 203 // Length includes the 2 bytes used to store the length. | 210 // Length includes the 2 bytes used to store the length. |
| 204 return br.readScalar(2) - 2; | 211 return br.readScalar(2) - 2; |
| 205 }; | 212 }; |
| 206 | 213 |
| 207 ExifParser.prototype.readMarkData = function(br) { | |
| 208 var length = this.readMarkLength(br); | |
| 209 return br.readSlice(length); | |
| 210 }; | |
| 211 | |
| 212 ExifParser.prototype.skipMarkData = function(br) { | |
| 213 br.seek(this.readMarkLength(br), ByteReader.SEEK_CUR); | |
| 214 }; | |
| 215 | |
| 216 ExifParser.prototype.readDirectory = function(br, tags) { | 214 ExifParser.prototype.readDirectory = function(br, tags) { |
| 217 var entryCount = br.readScalar(2); | 215 var entryCount = br.readScalar(2); |
| 218 for (var i = 0; i < entryCount; i++) { | 216 for (var i = 0; i < entryCount; i++) { |
| 219 var tagId = br.readScalar(2); | 217 var tagId = br.readScalar(2); |
| 220 var tag = tags[tagId] = {id: tagId}; | 218 var tag = tags[tagId] = {id: tagId}; |
| 221 tag.format = br.readScalar(2); | 219 tag.format = br.readScalar(2); |
| 222 tag.componentCount = br.readScalar(4); | 220 tag.componentCount = br.readScalar(4); |
| 223 this.readTagValue(br, tag); | 221 this.readTagValue(br, tag); |
| 224 } | 222 } |
| 225 | 223 |
| (...skipping 119 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 345 return { | 343 return { |
| 346 scaleX: ExifParser.SCALEX[index], | 344 scaleX: ExifParser.SCALEX[index], |
| 347 scaleY: ExifParser.SCALEY[index], | 345 scaleY: ExifParser.SCALEY[index], |
| 348 rotate90: ExifParser.ROTATE90[index] | 346 rotate90: ExifParser.ROTATE90[index] |
| 349 } | 347 } |
| 350 } | 348 } |
| 351 return null; | 349 return null; |
| 352 }; | 350 }; |
| 353 | 351 |
| 354 MetadataDispatcher.registerParserClass(ExifParser); | 352 MetadataDispatcher.registerParserClass(ExifParser); |
| OLD | NEW |