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 3// found in the LICENSE file. 4 5var exif = { 6 verbose: false, 7 8 messageHandlers: { 9 "init": function() { 10 this.log('thumbnailer initialized'); 11 }, 12 13 "get-exif": function(fileURL) { 14 this.processOneFile(fileURL, function callback(metadata) { 15 postMessage({verb: 'give-exif', 16 arguments: [fileURL, metadata]}); 17 }); 18 }, 19 }, 20 21 processOneFile: function(fileURL, callback) { 22 var self = this; 23 var currentStep = -1; 24 25 function nextStep(var_args) { 26 self.vlog('nextStep: ' + steps[currentStep + 1].name); 27 steps[++currentStep].apply(self, arguments); 28 } 29 30 function onError(err) { 31 self.vlog('Error processing: ' + fileURL + ': step: ' + 32 steps[currentStep].name + ": " + err); 33 34 postMessage({verb: 'give-exif-error', 35 arguments: [fileURL, steps[currentStep].name, err]}); 36 } 37 38 var steps = 39 [ // Step one, turn the url into an entry. 40 function getEntry() { 41 webkitResolveLocalFileSystemURL(fileURL, 42 function(entry) { nextStep(entry) }, 43 onError); 44 }, 45 46 // Step two, turn the entry into a file. 47 function getFile(entry) { 48 entry.file(function(file) { nextStep(file) }, onError); 49 }, 50 51 // Step three, read the file header into a byte array. 52 function readHeader(file) { 53 var reader = new FileReader(file.webkitSlice(0, 1024)); 54 reader.onerror = onError; 55 reader.onload = function(event) { nextStep(file, reader.result) }; 56 reader.readAsArrayBuffer(file); 57 }, 58 59 // Step four, find the exif marker and read all exif data. 60 function findExif(file, buf) { 61 var br = new exif.BufferReader(buf); 62 var mark = br.readMark(); 63 if (mark != exif.MARK_SOI) 64 return onError('Invalid file header: ' + mark.toString(16)); 65 66 while (true) { 67 if (mark == exif.MARK_SOS || br.eof()) { 68 return onError('Unable to find EXIF marker'); 69 } 70 71 mark = br.readMark(); 72 if (mark == exif.MARK_EXIF) { 73 var length = br.readMarkLength(); 74 75 // Offsets inside the EXIF block are based after this bit of 76 // magic, so we verify and discard it here, before exif parsing, 77 // to make offset calculations simpler. 78 var magic = br.readString(6); 79 if (magic != 'Exif\0\0') 80 return onError('Invalid EXIF magic: ' + magic.toString(16)); 81 82 var pos = br.tell(); 83 var reader = new FileReader(); 84 reader.onerror = onError; 85 reader.onload = function(event) { nextStep(file, reader.result) }; 86 reader.readAsArrayBuffer(file.webkitSlice(pos, pos + length - 6)); 87 return; 88 } 89 90 br.skipMarkData(); 91 } 92 }, 93 94 // Step five, parse the exif data. 95 function parseExif(file, buf) { 96 var br = new exif.BufferReader(buf); 97 var order = br.readScalar(2); 98 if (order == exif.ALIGN_LITTLE) { 99 br.setByteOrder(exif.BufferReader.LITTLE_ENDIAN); 100 } else if (order != exif.ALIGN_BIG) { 101 return onError('Invalid alignment value: ' + order.toString(16)); 102 } 103 104 var tag = br.readScalar(2); 105 if (tag != exif.TAG_TIFF) 106 return onError('Invalid TIFF tag: ' + tag.toString(16)); 107 108 var tags = {}; 109 var directoryOffset = br.readScalar(4); 110 111 while (directoryOffset) { 112 br.seek(directoryOffset); 113 var entryCount = br.readScalar(2); 114 for (var i = 0; i < entryCount; i++) { 115 var tag = tags[br.readScalar(2)] = {}; 116 tag.format = br.readScalar(2); 117 tag.componentCount = br.readScalar(4); 118 tag.value = br.readScalar(4); 119 }; 120 121 directoryOffset = br.readScalar(4); 122 } 123 124 var metadata = { rawTags: tags }; 125 126 if (exif.TAG_JPG_THUMB_OFFSET in tags && 127 exif.TAG_JPG_THUMB_LENGTH in tags) { 128 br.seek(tags[exif.TAG_JPG_THUMB_OFFSET].value); 129 var b64 = br.readBase64(tags[exif.TAG_JPG_THUMB_LENGTH].value); 130 metadata.thumbnailURL = 'data:image/jpeg;base64,' + b64; 131 } else { 132 self.vlog('Image has EXIF data, but no JPG thumbnail.'); 133 } 134 135 if (exif.TAG_EXIF_IMAGE_WIDTH in tags) 136 metadata.exifImageWidth = tags[exif.TAG_IMAGE_WIDTH]; 137 138 if (exif.TAG_EXIF_IMAGE_HEIGHT in tags) 139 metadata.exifImageHeight = tags[exif.TAG_IMAGE_HEIGHT]; 140 141 nextStep(metadata); 142 }, 143 144 // Step six, we're done. 145 callback 146 ]; 147 148 nextStep(); 149 }, 150 151 onMessage: function(event) { 152 var data = event.data; 153 154 if (this.messageHandlers.hasOwnProperty(data.verb)) { 155 //this.log('dispatching: ' + data.verb + ': ' + data.arguments); 156 this.messageHandlers[data.verb].apply(this, data.arguments); 157 } else { 158 this.log('Unknown message from client: ' + data.verb, data); 159 } 160 }, 161 162 log: function(var_args) { 163 var ary = Array.apply(null, arguments); 164 postMessage({verb: 'log', arguments: ary}); 165 }, 166 167 vlog: function(var_args) { 168 if (this.verbose) 169 this.log.apply(this, arguments); 170 } 171}; 172 173exif.MARK_SOI = 0xffd8; // Start of image data. 174exif.MARK_SOS = 0xffda; // Start of "stream" (the actual image data). 175exif.MARK_EXIF = 0xffe1; // Start of exif block. 176 177exif.ALIGN_LITTLE = 0x4949; // Indicates little endian alignment of exif data. 178exif.ALIGN_BIG = 0x4d4d; // Indicates big endian alignment of exif data. 179 180exif.TAG_TIFF = 0x002a; // First tag in the exif data. 181exif.TAG_JPG_THUMB_OFFSET = 0x0201; 182exif.TAG_JPG_THUMB_LENGTH = 0x0202; 183exif.TAG_EXIF_IMAGE_WIDTH = 0xa002; 184exif.TAG_EXIF_IMAGE_HEIGHT = 0xa003; 185 186exif.BufferReader = function(buf) { 187 this.buf_ = buf; 188 this.ary_ = new Uint8Array(buf); 189 this.pos_ = 0; 190 this.setByteOrder(exif.BufferReader.BIG_ENDIAN); 191}; 192 193exif.BufferReader.LITTLE_ENDIAN = 0; // Intel, 0x1234 is [0x34, 0x12] 194exif.BufferReader.BIG_ENDIAN = 1; // Motorola, 0x002a is [0x12, 0x34] 195 196exif.BufferReader.prototype = { 197 setByteOrder: function(order) { 198 this.order_ = order; 199 if (order == exif.BufferReader.LITTLE_ENDIAN) { 200 this.readScalar = this.readLittle; 201 } else { 202 this.readScalar = this.readBig; 203 } 204 }, 205 206 eof: function() { 207 return this.pos_ >= this.ary_.length; 208 }, 209 210 readScalar: null, // Either readLittle or readBig, according to byte order. 211 212 /** 213 * Big endian read. Most significant bytes come first. 214 */ 215 readBig: function(width) { 216 var rv = 0; 217 switch(width) { 218 case 4: 219 rv = this.ary_[this.pos_++] << 24; 220 case 3: 221 rv |= this.ary_[this.pos_++] << 16; 222 case 2: 223 rv |= this.ary_[this.pos_++] << 8; 224 case 1: 225 rv |= this.ary_[this.pos_++]; 226 } 227 228 return rv; 229 }, 230 231 /** 232 * Little endian read. Least significant bytes come first. 233 */ 234 readLittle: function(width) { 235 var rv = 0; 236 switch(width) { 237 case 4: 238 rv = this.ary_[this.pos_ + 3] << 24; 239 case 3: 240 rv |= this.ary_[this.pos_ + 2] << 16; 241 case 2: 242 rv |= this.ary_[this.pos_+ 1] << 8; 243 case 1: 244 rv |= this.ary_[this.pos_]; 245 } 246 247 this.pos_ += width; 248 return rv; 249 }, 250 251 readString: function(length) { 252 var chars = []; 253 for (var i = 0; i < length; i++) { 254 chars[i] = String.fromCharCode(this.ary_[this.pos_++]); 255 } 256 257 return chars.join(''); 258 }, 259 260 base64Alphabet_: ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 261 'abcdefghijklmnopqrstuvwxyz' + 262 '0123456789+/').split(''), 263 264 readBase64: function(length) { 265 var rv = []; 266 var chars = []; 267 var padding = 0; 268 269 for (var i = 0; i < length; /* incremented inside */) { 270 var bits = this.ary_[this.pos_ + i++] << 16; 271 272 if (i < length) { 273 bits |= this.ary_[this.pos_ + i++] << 8; 274 275 if (i < length) { 276 bits |= this.ary_[this.pos_ + i++]; 277 } else { 278 padding = 1; 279 } 280 } else { 281 padding = 2; 282 } 283 284 chars[3] = this.base64Alphabet_[bits & 63]; 285 chars[2] = this.base64Alphabet_[(bits >> 6) & 63]; 286 chars[1] = this.base64Alphabet_[(bits >> 12) & 63]; 287 chars[0] = this.base64Alphabet_[(bits >> 18) & 63]; 288 289 rv.push.apply(rv, chars); 290 } 291 292 this.pos_ += i; 293 294 if (padding > 0) 295 chars[chars.length - 1] = '='; 296 if (padding > 1) 297 chars[chars.length - 2] = '='; 298 299 return rv.join(''); 300 }, 301 302 readMark: function() { 303 return this.readScalar(2); 304 }, 305 306 readMarkLength: function() { 307 // Length includes the 2 bytes used to store the length. 308 return this.readScalar(2) - 2; 309 }, 310 311 readMarkData: function(opt_arrayConstructor) { 312 var arrayConstructor = opt_arrayConstructor || Uint8Array; 313 314 var length = this.readMarkLength(); 315 var slice = new arrayConstructor(this.buf_, this.pos_, length); 316 this.pos_ += length; 317 318 return slice; 319 }, 320 321 skipMarkData: function() { 322 this.skip(this.readMarkLength()); 323 }, 324 325 seek: function(pos) { 326 this.pos_ = pos; 327 }, 328 329 skip: function(count) { 330 this.pos_ += count; 331 }, 332 333 tell: function() { 334 return this.pos_; 335 } 336}; 337 338var onmessage = exif.onMessage.bind(exif); 339