// Copyright (c) 2011 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.
var exif = {
verbose: false,
messageHandlers: {
"init": function() {
this.log('thumbnailer initialized');
},
"get-exif": function(fileURL) {
this.processOneFile(fileURL, function callback(metadata) {
postMessage({verb: 'give-exif',
arguments: [fileURL, metadata]});
});
},
},
processOneFile: function(fileURL, callback) {
var self = this;
var currentStep = -1;
function nextStep(var_args) {
self.vlog('nextStep: ' + steps[currentStep + 1].name);
steps[++currentStep].apply(self, arguments);
}
function onError(err) {
self.vlog('Error processing: ' + fileURL + ': step: ' +
steps[currentStep].name + ": " + err);
postMessage({verb: 'give-exif-error',
arguments: [fileURL, steps[currentStep].name, err]});
}
var steps =
[ // Step one, turn the url into an entry.
function getEntry() {
webkitResolveLocalFileSystemURL(fileURL,
function(entry) { nextStep(entry) },
onError);
},
// Step two, turn the entry into a file.
function getFile(entry) {
entry.file(function(file) { nextStep(file) }, onError);
},
// Step three, read the file header into a byte array.
function readHeader(file) {
var reader = new FileReader(file.webkitSlice(0, 1024));
reader.onerror = onError;
reader.onload = function(event) { nextStep(file, reader.result) };
reader.readAsArrayBuffer(file);
},
// Step four, find the exif marker and read all exif data.
function findExif(file, buf) {
var br = new exif.BufferReader(buf);
var mark = br.readMark();
if (mark != exif.MARK_SOI)
return onError('Invalid file header: ' + mark.toString(16));
while (true) {
if (mark == exif.MARK_SOS || br.eof()) {
return onError('Unable to find EXIF marker');
}
mark = br.readMark();
if (mark == exif.MARK_EXIF) {
var length = br.readMarkLength();
// Offsets inside the EXIF block are based after this bit of
// magic, so we verify and discard it here, before exif parsing,
// to make offset calculations simpler.
var magic = br.readString(6);
if (magic != 'Exif\0\0')
return onError('Invalid EXIF magic: ' + magic.toString(16));
var pos = br.tell();
var reader = new FileReader();
reader.onerror = onError;
reader.onload = function(event) { nextStep(file, reader.result) };
reader.readAsArrayBuffer(file.webkitSlice(pos, pos + length - 6));
return;
}
br.skipMarkData();
}
},
// Step five, parse the exif data.
function parseExif(file, buf) {
var br = new exif.BufferReader(buf);
var order = br.readScalar(2);
if (order == exif.ALIGN_LITTLE) {
br.setByteOrder(exif.BufferReader.LITTLE_ENDIAN);
} else if (order != exif.ALIGN_BIG) {
return onError('Invalid alignment value: ' + order.toString(16));
}
var tag = br.readScalar(2);
if (tag != exif.TAG_TIFF)
return onError('Invalid TIFF tag: ' + tag.toString(16));
var tags = {};
var directoryOffset = br.readScalar(4);
while (directoryOffset) {
br.seek(directoryOffset);
var entryCount = br.readScalar(2);
for (var i = 0; i < entryCount; i++) {
var tag = tags[br.readScalar(2)] = {};
tag.format = br.readScalar(2);
tag.componentCount = br.readScalar(4);
tag.value = br.readScalar(4);
};
directoryOffset = br.readScalar(4);
}
var metadata = { rawTags: tags };
if (exif.TAG_JPG_THUMB_OFFSET in tags &&
exif.TAG_JPG_THUMB_LENGTH in tags) {
br.seek(tags[exif.TAG_JPG_THUMB_OFFSET].value);
var b64 = br.readBase64(tags[exif.TAG_JPG_THUMB_LENGTH].value);
metadata.thumbnailURL = 'data:image/jpeg;base64,' + b64;
} else {
self.vlog('Image has EXIF data, but no JPG thumbnail.');
}
if (exif.TAG_EXIF_IMAGE_WIDTH in tags)
metadata.exifImageWidth = tags[exif.TAG_IMAGE_WIDTH];
if (exif.TAG_EXIF_IMAGE_HEIGHT in tags)
metadata.exifImageHeight = tags[exif.TAG_IMAGE_HEIGHT];
nextStep(metadata);
},
// Step six, we're done.
callback
];
nextStep();
},
onMessage: function(event) {
var data = event.data;
if (this.messageHandlers.hasOwnProperty(data.verb)) {
//this.log('dispatching: ' + data.verb + ': ' + data.arguments);
this.messageHandlers[data.verb].apply(this, data.arguments);
} else {
this.log('Unknown message from client: ' + data.verb, data);
}
},
log: function(var_args) {
var ary = Array.apply(null, arguments);
postMessage({verb: 'log', arguments: ary});
},
vlog: function(var_args) {
if (this.verbose)
this.log.apply(this, arguments);
}
};
exif.MARK_SOI = 0xffd8; // Start of image data.
exif.MARK_SOS = 0xffda; // Start of "stream" (the actual image data).
exif.MARK_EXIF = 0xffe1; // Start of exif block.
exif.ALIGN_LITTLE = 0x4949; // Indicates little endian alignment of exif data.
exif.ALIGN_BIG = 0x4d4d; // Indicates big endian alignment of exif data.
exif.TAG_TIFF = 0x002a; // First tag in the exif data.
exif.TAG_JPG_THUMB_OFFSET = 0x0201;
exif.TAG_JPG_THUMB_LENGTH = 0x0202;
exif.TAG_EXIF_IMAGE_WIDTH = 0xa002;
exif.TAG_EXIF_IMAGE_HEIGHT = 0xa003;
exif.BufferReader = function(buf) {
this.buf_ = buf;
this.ary_ = new Uint8Array(buf);
this.pos_ = 0;
this.setByteOrder(exif.BufferReader.BIG_ENDIAN);
};
exif.BufferReader.LITTLE_ENDIAN = 0; // Intel, 0x1234 is [0x34, 0x12]
exif.BufferReader.BIG_ENDIAN = 1; // Motorola, 0x002a is [0x12, 0x34]
exif.BufferReader.prototype = {
setByteOrder: function(order) {
this.order_ = order;
if (order == exif.BufferReader.LITTLE_ENDIAN) {
this.readScalar = this.readLittle;
} else {
this.readScalar = this.readBig;
}
},
eof: function() {
return this.pos_ >= this.ary_.length;
},
readScalar: null, // Either readLittle or readBig, according to byte order.
/**
* Big endian read. Most significant bytes come first.
*/
readBig: function(width) {
var rv = 0;
switch(width) {
case 4:
rv = this.ary_[this.pos_++] << 24;
case 3:
rv |= this.ary_[this.pos_++] << 16;
case 2:
rv |= this.ary_[this.pos_++] << 8;
case 1:
rv |= this.ary_[this.pos_++];
}
return rv;
},
/**
* Little endian read. Least significant bytes come first.
*/
readLittle: function(width) {
var rv = 0;
switch(width) {
case 4:
rv = this.ary_[this.pos_ + 3] << 24;
case 3:
rv |= this.ary_[this.pos_ + 2] << 16;
case 2:
rv |= this.ary_[this.pos_+ 1] << 8;
case 1:
rv |= this.ary_[this.pos_];
}
this.pos_ += width;
return rv;
},
readString: function(length) {
var chars = [];
for (var i = 0; i < length; i++) {
chars[i] = String.fromCharCode(this.ary_[this.pos_++]);
}
return chars.join('');
},
base64Alphabet_: ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
'abcdefghijklmnopqrstuvwxyz' +
'0123456789+/').split(''),
readBase64: function(length) {
var rv = [];
var chars = [];
var padding = 0;
for (var i = 0; i < length; /* incremented inside */) {
var bits = this.ary_[this.pos_ + i++] << 16;
if (i < length) {
bits |= this.ary_[this.pos_ + i++] << 8;
if (i < length) {
bits |= this.ary_[this.pos_ + i++];
} else {
padding = 1;
}
} else {
padding = 2;
}
chars[3] = this.base64Alphabet_[bits & 63];
chars[2] = this.base64Alphabet_[(bits >> 6) & 63];
chars[1] = this.base64Alphabet_[(bits >> 12) & 63];
chars[0] = this.base64Alphabet_[(bits >> 18) & 63];
rv.push.apply(rv, chars);
}
this.pos_ += i;
if (padding > 0)
chars[chars.length - 1] = '=';
if (padding > 1)
chars[chars.length - 2] = '=';
return rv.join('');
},
readMark: function() {
return this.readScalar(2);
},
readMarkLength: function() {
// Length includes the 2 bytes used to store the length.
return this.readScalar(2) - 2;
},
readMarkData: function(opt_arrayConstructor) {
var arrayConstructor = opt_arrayConstructor || Uint8Array;
var length = this.readMarkLength();
var slice = new arrayConstructor(this.buf_, this.pos_, length);
this.pos_ += length;
return slice;
},
skipMarkData: function() {
this.skip(this.readMarkLength());
},
seek: function(pos) {
this.pos_ = pos;
},
skip: function(count) {
this.pos_ += count;
},
tell: function() {
return this.pos_;
}
};
var onmessage = exif.onMessage.bind(exif);