Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source. Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
1777 lines
63 KiB
JavaScript
1777 lines
63 KiB
JavaScript
/* exported BlobView */
|
|
'use strict';
|
|
|
|
var BlobView = (function() {
|
|
function fail(msg) {
|
|
throw Error(msg);
|
|
}
|
|
|
|
var decoderCache = {};
|
|
function getDecoder(encoding) {
|
|
if (encoding in decoderCache) {
|
|
return decoderCache[encoding];
|
|
}
|
|
var decoder = decoderCache[encoding] = new TextDecoder(encoding);
|
|
return decoder;
|
|
}
|
|
|
|
// This constructor is for internal use only.
|
|
// Use the BlobView.get() factory function or the getMore instance method
|
|
// to obtain a BlobView object.
|
|
function BlobView(blob, sliceOffset, sliceLength, slice,
|
|
viewOffset, viewLength, littleEndian)
|
|
{
|
|
this.blob = blob; // The parent blob that the data is from
|
|
this.sliceOffset = sliceOffset; // The start address within the blob
|
|
this.sliceLength = sliceLength; // How long the slice is
|
|
this.slice = slice; // The ArrayBuffer of slice data
|
|
this.viewOffset = viewOffset; // The start of the view within the slice
|
|
this.viewLength = viewLength; // The length of the view
|
|
this.littleEndian = littleEndian; // Read little endian by default?
|
|
|
|
// DataView wrapper around the ArrayBuffer
|
|
this.view = new DataView(slice, viewOffset, viewLength);
|
|
|
|
// These fields mirror those of DataView
|
|
this.buffer = slice;
|
|
this.byteLength = viewLength;
|
|
this.byteOffset = viewOffset;
|
|
|
|
this.index = 0; // The read methods keep track of the read position
|
|
}
|
|
|
|
// Async factory function
|
|
BlobView.get = function(blob, offset, length, callback, littleEndian) {
|
|
if (offset < 0) {
|
|
fail('negative offset');
|
|
}
|
|
if (length < 0) {
|
|
fail('negative length');
|
|
}
|
|
if (offset > blob.size) {
|
|
fail('offset larger than blob size');
|
|
}
|
|
// Don't fail if the length is too big; just reduce the length
|
|
if (offset + length > blob.size) {
|
|
length = blob.size - offset;
|
|
}
|
|
var slice = blob.slice(offset, offset + length);
|
|
var reader = new FileReader();
|
|
reader.readAsArrayBuffer(slice);
|
|
reader.onloadend = function() {
|
|
var result = null;
|
|
if (reader.result) {
|
|
result = new BlobView(blob, offset, length, reader.result,
|
|
0, length, littleEndian || false);
|
|
}
|
|
callback(result, reader.error);
|
|
};
|
|
};
|
|
|
|
// Synchronous factory function for use when you have an array buffer and want
|
|
// to treat it as a BlobView (e.g. to use the readXYZ functions). We need
|
|
// this for the music app, which uses an array buffer to hold
|
|
// de-unsynchronized ID3 frames.
|
|
BlobView.getFromArrayBuffer = function(buffer, offset, length, littleEndian) {
|
|
return new BlobView(null, offset, length, buffer, offset, length,
|
|
littleEndian);
|
|
};
|
|
|
|
BlobView.prototype = {
|
|
constructor: BlobView,
|
|
|
|
// This instance method is like the BlobView.get() factory method,
|
|
// but it is here because if the current buffer includes the requested
|
|
// range of bytes, they can be passed directly to the callback without
|
|
// going back to the blob to read them
|
|
getMore: function(offset, length, callback) {
|
|
// If we made this BlobView from an array buffer, there's no blob backing
|
|
// it, and so it's impossible to get more data.
|
|
if (!this.blob) {
|
|
fail('no blob backing this BlobView');
|
|
}
|
|
|
|
if (offset >= this.sliceOffset &&
|
|
offset + length <= this.sliceOffset + this.sliceLength) {
|
|
// The quick case: we already have that region of the blob
|
|
callback(new BlobView(this.blob,
|
|
this.sliceOffset, this.sliceLength, this.slice,
|
|
offset - this.sliceOffset, length,
|
|
this.littleEndian));
|
|
}
|
|
else {
|
|
// Otherwise, we have to do an async read to get more bytes
|
|
BlobView.get(this.blob, offset, length, callback, this.littleEndian);
|
|
}
|
|
},
|
|
|
|
// Set the default endianness for the other methods
|
|
littleEndian: function() {
|
|
this.littleEndian = true;
|
|
},
|
|
bigEndian: function() {
|
|
this.littleEndian = false;
|
|
},
|
|
|
|
// These "get" methods are just copies of the DataView methods, except
|
|
// that they honor the default endianness
|
|
getUint8: function(offset) {
|
|
return this.view.getUint8(offset);
|
|
},
|
|
getInt8: function(offset) {
|
|
return this.view.getInt8(offset);
|
|
},
|
|
getUint16: function(offset, le) {
|
|
return this.view.getUint16(offset,
|
|
le !== undefined ? le : this.littleEndian);
|
|
},
|
|
getInt16: function(offset, le) {
|
|
return this.view.getInt16(offset,
|
|
le !== undefined ? le : this.littleEndian);
|
|
},
|
|
getUint32: function(offset, le) {
|
|
return this.view.getUint32(offset,
|
|
le !== undefined ? le : this.littleEndian);
|
|
},
|
|
getInt32: function(offset, le) {
|
|
return this.view.getInt32(offset,
|
|
le !== undefined ? le : this.littleEndian);
|
|
},
|
|
getFloat32: function(offset, le) {
|
|
return this.view.getFloat32(offset,
|
|
le !== undefined ? le : this.littleEndian);
|
|
},
|
|
getFloat64: function(offset, le) {
|
|
return this.view.getFloat64(offset,
|
|
le !== undefined ? le : this.littleEndian);
|
|
},
|
|
|
|
// These "read" methods read from the current position in the view and
|
|
// update that position accordingly
|
|
readByte: function() {
|
|
return this.view.getInt8(this.index++);
|
|
},
|
|
readUnsignedByte: function() {
|
|
return this.view.getUint8(this.index++);
|
|
},
|
|
readShort: function(le) {
|
|
var val = this.view.getInt16(this.index,
|
|
le !== undefined ? le : this.littleEndian);
|
|
this.index += 2;
|
|
return val;
|
|
},
|
|
readUnsignedShort: function(le) {
|
|
var val = this.view.getUint16(this.index,
|
|
le !== undefined ? le : this.littleEndian);
|
|
this.index += 2;
|
|
return val;
|
|
},
|
|
readInt: function(le) {
|
|
var val = this.view.getInt32(this.index,
|
|
le !== undefined ? le : this.littleEndian);
|
|
this.index += 4;
|
|
return val;
|
|
},
|
|
readUnsignedInt: function(le) {
|
|
var val = this.view.getUint32(this.index,
|
|
le !== undefined ? le : this.littleEndian);
|
|
this.index += 4;
|
|
return val;
|
|
},
|
|
readFloat: function(le) {
|
|
var val = this.view.getFloat32(this.index,
|
|
le !== undefined ? le : this.littleEndian);
|
|
this.index += 4;
|
|
return val;
|
|
},
|
|
readDouble: function(le) {
|
|
var val = this.view.getFloat64(this.index,
|
|
le !== undefined ? le : this.littleEndian);
|
|
this.index += 8;
|
|
return val;
|
|
},
|
|
|
|
// Methods to get and set the current position
|
|
tell: function() {
|
|
return this.index;
|
|
},
|
|
remaining: function() {
|
|
return this.byteLength - this.index;
|
|
},
|
|
seek: function(index) {
|
|
if (index < 0) {
|
|
fail('negative index');
|
|
}
|
|
if (index > this.byteLength) {
|
|
fail('index greater than buffer size');
|
|
}
|
|
this.index = index;
|
|
},
|
|
advance: function(n) {
|
|
var index = this.index + n;
|
|
if (index < 0) {
|
|
fail('advance past beginning of buffer');
|
|
}
|
|
// It's usual that when we finished reading one target view,
|
|
// the index is advanced to the start(previous end + 1) of next view,
|
|
// and the new index will be equal to byte length(the last index + 1),
|
|
// we will not fail on it because it means the reading is finished,
|
|
// or do we have to warn here?
|
|
if (index > this.byteLength) {
|
|
fail('advance past end of buffer');
|
|
}
|
|
this.index = index;
|
|
},
|
|
|
|
// Additional methods to read other useful things
|
|
getUnsignedByteArray: function(offset, n) {
|
|
return new Uint8Array(this.buffer, offset + this.viewOffset, n);
|
|
},
|
|
|
|
// Additional methods to read other useful things
|
|
readUnsignedByteArray: function(n) {
|
|
var val = new Uint8Array(this.buffer, this.index + this.viewOffset, n);
|
|
this.index += n;
|
|
return val;
|
|
},
|
|
|
|
getBit: function(offset, bit) {
|
|
var byte = this.view.getUint8(offset);
|
|
return (byte & (1 << bit)) !== 0;
|
|
},
|
|
|
|
getUint24: function(offset, le) {
|
|
var b1, b2, b3;
|
|
if (le !== undefined ? le : this.littleEndian) {
|
|
b1 = this.view.getUint8(offset);
|
|
b2 = this.view.getUint8(offset + 1);
|
|
b3 = this.view.getUint8(offset + 2);
|
|
}
|
|
else { // big end first
|
|
b3 = this.view.getUint8(offset);
|
|
b2 = this.view.getUint8(offset + 1);
|
|
b1 = this.view.getUint8(offset + 2);
|
|
}
|
|
|
|
return (b3 << 16) + (b2 << 8) + b1;
|
|
},
|
|
|
|
readUint24: function(le) {
|
|
var value = this.getUint24(this.index, le);
|
|
this.index += 3;
|
|
return value;
|
|
},
|
|
|
|
// There are lots of ways to read strings. We support binary (raw 8-bit
|
|
// codepoints), Latin-1, UTF-8, and UTF-16. For the latter three, we also
|
|
// support null-terminated versions.
|
|
|
|
getBinaryText: function(offset, len) {
|
|
var bytes = new Uint8Array(this.buffer, offset + this.viewOffset, len);
|
|
return String.fromCharCode.apply(String, bytes);
|
|
},
|
|
|
|
readBinaryText: function(len) {
|
|
var s = this.getBinaryText(this.index, len);
|
|
this.index += len;
|
|
return s;
|
|
},
|
|
|
|
getLatin1Text: function(offset, len) {
|
|
var bytes = new Uint8Array(this.buffer, offset + this.viewOffset, len);
|
|
return getDecoder('latin1').decode(bytes);
|
|
},
|
|
|
|
readLatin1Text: function(len) {
|
|
var s = this.getLatin1Text(this.index, len);
|
|
this.index += len;
|
|
return s;
|
|
},
|
|
|
|
getUTF8Text: function(offset, len) {
|
|
var bytes = new Uint8Array(this.buffer, offset + this.viewOffset, len);
|
|
return getDecoder('utf-8').decode(bytes);
|
|
},
|
|
|
|
readUTF8Text: function(len) {
|
|
var s = this.getUTF8Text(this.index, len);
|
|
this.index += len;
|
|
return s;
|
|
},
|
|
|
|
// Get UTF16 text. If le is not specified, expect a BOM to define
|
|
// endianness. If le is true, read UTF16LE, if false, UTF16BE.
|
|
getUTF16Text: function(offset, len, le) {
|
|
if (len % 2) {
|
|
fail('len must be a multiple of two');
|
|
}
|
|
|
|
var bytes = new Uint8Array(this.buffer, offset + this.viewOffset, len);
|
|
|
|
if (le === null || le === undefined) {
|
|
var BOM = (bytes[0] << 8) + bytes[1];
|
|
|
|
if (BOM === 0xFEFF) {
|
|
bytes = bytes.subarray(2);
|
|
le = false;
|
|
} else if (BOM === 0xFFFE) {
|
|
bytes = bytes.subarray(2);
|
|
le = true;
|
|
} else {
|
|
le = true;
|
|
}
|
|
}
|
|
|
|
var encoding = le ? 'utf-16le' : 'utf-16be';
|
|
return getDecoder(encoding).decode(bytes);
|
|
},
|
|
|
|
readUTF16Text: function(len, le) {
|
|
var s = this.getUTF16Text(this.index, len, le);
|
|
this.index += len;
|
|
return s;
|
|
},
|
|
|
|
// Read 4 bytes, ignore the high bit and combine them into a 28-bit
|
|
// big-endian unsigned integer.
|
|
// This format is used by the ID3v2 metadata.
|
|
getID3Uint28BE: function(offset) {
|
|
var b1 = this.view.getUint8(offset) & 0x7f;
|
|
var b2 = this.view.getUint8(offset + 1) & 0x7f;
|
|
var b3 = this.view.getUint8(offset + 2) & 0x7f;
|
|
var b4 = this.view.getUint8(offset + 3) & 0x7f;
|
|
return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4;
|
|
},
|
|
|
|
readID3Uint28BE: function() {
|
|
var value = this.getID3Uint28BE(this.index);
|
|
this.index += 4;
|
|
return value;
|
|
},
|
|
|
|
// Read bytes up to and including a null terminator, but never
|
|
// more than size bytes. And return as a Latin1 string. If advance_by_size
|
|
// is true, this will always seek ahead by `size` bytes, even if a null
|
|
// character was found earlier.
|
|
readNullTerminatedLatin1Text: function(size, advance_by_size = false) {
|
|
var bytes = new Uint8Array(this.buffer, this.viewOffset + this.index,
|
|
size);
|
|
|
|
var nil = bytes.indexOf(0);
|
|
if (nil !== -1) {
|
|
bytes = bytes.subarray(0, nil);
|
|
}
|
|
|
|
var s = getDecoder('latin1').decode(bytes);
|
|
if (nil === -1 || advance_by_size) {
|
|
this.index += size;
|
|
} else {
|
|
this.index += nil + 1;
|
|
}
|
|
return s;
|
|
},
|
|
|
|
// Read bytes up to and including a null terminator, but never
|
|
// more than size bytes. And return as a UTF8 string. If advance_by_size
|
|
// is true, this will always seek ahead by `size` bytes, even if a null
|
|
// character was found earlier.
|
|
readNullTerminatedUTF8Text: function(size, advance_by_size = false) {
|
|
var bytes = new Uint8Array(this.buffer, this.viewOffset + this.index,
|
|
size);
|
|
|
|
var nil = bytes.indexOf(0);
|
|
if (nil !== -1) {
|
|
bytes = bytes.subarray(0, nil);
|
|
}
|
|
|
|
var s = getDecoder('utf-8').decode(bytes);
|
|
if (nil === -1 || advance_by_size) {
|
|
this.index += size;
|
|
} else {
|
|
this.index += nil + 1;
|
|
}
|
|
return s;
|
|
},
|
|
|
|
// Read UTF16 text. If le is not specified, expect a BOM to define
|
|
// endianness. If le is true, read UTF16LE, if false, UTF16BE
|
|
// Read until we find a null-terminator, but never more than size bytes.
|
|
// If advance_by_size is true, this will always seek ahead by `size` bytes,
|
|
// even if a null character was found earlier.
|
|
readNullTerminatedUTF16Text: function(size, le, advance_by_size = false) {
|
|
if (size % 2) {
|
|
fail('size must be a multiple of two');
|
|
}
|
|
|
|
for (var len = 0; len < size; len += 2) {
|
|
if (this.getUint16(this.index + len, le) === 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
var s = this.getUTF16Text(this.index, len, le);
|
|
if (len === size || advance_by_size) {
|
|
this.index += size;
|
|
} else {
|
|
this.index += len + 2;
|
|
}
|
|
return s;
|
|
}
|
|
};
|
|
|
|
// We don't want users of this library to accidentally call the constructor
|
|
// instead of using one of the factory functions, so we return a dummy object
|
|
// instead of the real constructor. If someone really needs to get at the
|
|
// real constructor, the contructor property of the prototype refers to it.
|
|
return {
|
|
get: BlobView.get,
|
|
getFromArrayBuffer: BlobView.getFromArrayBuffer
|
|
};
|
|
}());
|
|
|
|
'use strict';
|
|
|
|
/* global BlobView */
|
|
/* exported parseJPEGMetadata */
|
|
//
|
|
// This file defines a single function that asynchronously reads a
|
|
// JPEG file (or blob) to determine its width and height and find the
|
|
// location and size of the embedded preview image, if it has one. If
|
|
// it succeeds, it passes an object containing this data to the
|
|
// specified callback function. If it fails, it passes an error message
|
|
// to the specified error function instead.
|
|
//
|
|
// This function is capable of parsing and returning EXIF data for a
|
|
// JPEG file, but for speed, it ignores all EXIF data except the embedded
|
|
// preview image and the image orientation.
|
|
//
|
|
// This function requires the BlobView utility class
|
|
//
|
|
function parseJPEGMetadata(file, metadataCallback, metadataError) {
|
|
// This is the object we'll pass to metadataCallback
|
|
var metadata = {};
|
|
|
|
// this is the start offset for the data in exif. Should be
|
|
// where to find the TIFF magic header.
|
|
var TIFFHeaderOffset = 0;
|
|
|
|
// Start off reading a 16kb slice of the JPEG file.
|
|
// Hopefully, this will be all we need and everything else will
|
|
// be synchronous
|
|
BlobView.get(file, 0, Math.min(16 * 1024, file.size), function(data) {
|
|
if (data.byteLength < 2 ||
|
|
data.getUint8(0) !== 0xFF ||
|
|
data.getUint8(1) !== 0xD8) {
|
|
metadataError('Not a JPEG file');
|
|
return;
|
|
}
|
|
|
|
// Now start reading JPEG segments
|
|
// getSegment() and segmentHandler() are defined below.
|
|
getSegment(data, 2, segmentHandler);
|
|
});
|
|
|
|
// Read the JPEG segment at the specified offset and
|
|
// pass it to the callback function.
|
|
// Offset is relative to the current data offsets.
|
|
// We assume that data has enough data in it that we can
|
|
// can determine the size of the segment, and we guarantee that
|
|
// we read extra bytes so the next call works
|
|
function getSegment(data, offset, callback) {
|
|
try {
|
|
var header = data.getUint8(offset);
|
|
if (header !== 0xFF) {
|
|
metadataError('Malformed JPEG file: bad segment header');
|
|
return;
|
|
}
|
|
|
|
var type = data.getUint8(offset + 1);
|
|
var size = data.getUint16(offset + 2) + 2;
|
|
|
|
// the absolute position of the segment
|
|
var start = data.sliceOffset + data.viewOffset + offset;
|
|
// If this isn't the last segment in the file, add 4 bytes
|
|
// so we can read the size of the next segment
|
|
var isLast = (start + size >= file.size);
|
|
var length = isLast ? size : size + 4;
|
|
|
|
data.getMore(start, length,
|
|
function(data) {
|
|
callback(type, size, data, isLast);
|
|
});
|
|
}
|
|
catch (e) {
|
|
metadataError(e.toString() + '\n' + e.stack);
|
|
}
|
|
}
|
|
|
|
// This is a callback function for getNextSegment that handles the
|
|
// various types of segments we expect to see in a jpeg file
|
|
function segmentHandler(type, size, data, isLastSegment) {
|
|
try {
|
|
switch (type) {
|
|
case 0xC0: // Some actual image data, including image dimensions
|
|
case 0xC1:
|
|
case 0xC2:
|
|
case 0xC3:
|
|
// Get image dimensions
|
|
metadata.height = data.getUint16(5);
|
|
metadata.width = data.getUint16(7);
|
|
|
|
if (type === 0xC2) {
|
|
// pjpeg files can't be efficiently downsampled while decoded
|
|
// so we need to distinguish them from regular jpegs
|
|
metadata.progressive = true;
|
|
}
|
|
|
|
// We're done. All the EXIF data will come before this segment
|
|
// So call the callback
|
|
metadataCallback(metadata);
|
|
break;
|
|
|
|
case 0xE1: // APP1 segment. Probably holds EXIF metadata
|
|
parseAPP1(data);
|
|
|
|
/* falls through */
|
|
default:
|
|
// A segment we don't care about, so just go on and read the next one
|
|
if (isLastSegment) {
|
|
metadataError('unexpected end of JPEG file');
|
|
return;
|
|
}
|
|
getSegment(data, size, segmentHandler);
|
|
}
|
|
}
|
|
catch (e) {
|
|
metadataError(e.toString() + '\n' + e.stack);
|
|
}
|
|
}
|
|
|
|
function parseAPP1(data) {
|
|
if (data.getUint32(4, false) === 0x45786966) { // "Exif"
|
|
var exif = parseEXIFData(data);
|
|
|
|
if (exif.THUMBNAIL && exif.THUMBNAILLENGTH) {
|
|
var start = data.sliceOffset + data.viewOffset + TIFFHeaderOffset +
|
|
exif.THUMBNAIL;
|
|
metadata.preview = {
|
|
start: start,
|
|
end: start + exif.THUMBNAILLENGTH
|
|
};
|
|
}
|
|
|
|
// map exif orientation flags for easy transforms
|
|
switch (exif.ORIENTATION) {
|
|
case 2:
|
|
metadata.rotation = 0;
|
|
metadata.mirrored = true;
|
|
break;
|
|
case 3:
|
|
metadata.rotation = 180;
|
|
metadata.mirrored = false;
|
|
break;
|
|
case 4:
|
|
metadata.rotation = 180;
|
|
metadata.mirrored = true;
|
|
break;
|
|
case 5:
|
|
metadata.rotation = 90;
|
|
metadata.mirrored = true;
|
|
break;
|
|
case 6:
|
|
metadata.rotation = 90;
|
|
metadata.mirrored = false;
|
|
break;
|
|
case 7:
|
|
metadata.rotation = 270;
|
|
metadata.mirrored = true;
|
|
break;
|
|
case 8:
|
|
metadata.rotation = 270;
|
|
metadata.mirrored = false;
|
|
break;
|
|
default:
|
|
// This is the default orientation. If it is properly encoded
|
|
// we will get 1 here. But sometimes it is undefined and some
|
|
// files have a 0 here as well.
|
|
metadata.rotation = 0;
|
|
metadata.mirrored = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse an EXIF segment from a JPEG file and return an object
|
|
// of metadata attributes. The argument must be a DataView object
|
|
function parseEXIFData(data) {
|
|
var exif = {};
|
|
|
|
var byteorder = data.getUint8(10);
|
|
if (byteorder === 0x4D) { // big endian
|
|
byteorder = false;
|
|
} else if (byteorder === 0x49) { // little endian
|
|
byteorder = true;
|
|
} else {
|
|
throw Error('invalid byteorder in EXIF segment');
|
|
}
|
|
|
|
// The TIFF magic started at +10.
|
|
TIFFHeaderOffset = 10;
|
|
|
|
if (data.getUint16(12, byteorder) !== 42) { // magic number
|
|
throw Error('bad magic number in EXIF segment');
|
|
}
|
|
|
|
var offset = data.getUint32(14, byteorder);
|
|
|
|
// This is how we would parse all EXIF metadata more generally.
|
|
// Especially need for iamge orientation
|
|
parseIFD(data, offset + TIFFHeaderOffset, byteorder, exif, true);
|
|
|
|
// I'm leaving this code in as a comment in case we need other EXIF
|
|
// data in the future.
|
|
// if (exif.EXIFIFD) {
|
|
// parseIFD(data, exif.EXIFIFD + TIFFHeaderOffset, byteorder, exif);
|
|
// delete exif.EXIFIFD;
|
|
// }
|
|
|
|
// if (exif.GPSIFD) {
|
|
// parseIFD(data, exif.GPSIFD + TIFFHeaderOffset, byteorder, exif);
|
|
// delete exif.GPSIFD;
|
|
// }
|
|
|
|
// Instead of a general purpose EXIF parse, we're going to drill
|
|
// down directly to the thumbnail image.
|
|
// We're in IFD0 here. We want the offset of IFD1
|
|
var ifd0entries = data.getUint16(offset + TIFFHeaderOffset, byteorder);
|
|
var ifd1 = data.getUint32(offset + 12 + 12 * ifd0entries, byteorder);
|
|
// If there is an offset for IFD1, parse that
|
|
if (ifd1 !== 0) {
|
|
parseIFD(data, ifd1 + TIFFHeaderOffset, byteorder, exif, true);
|
|
}
|
|
|
|
return exif;
|
|
}
|
|
|
|
function parseIFD(data, offset, byteorder, exif, onlyParseOne) {
|
|
var numentries = data.getUint16(offset, byteorder);
|
|
for (var i = 0; i < numentries; i++) {
|
|
parseEntry(data, offset + 2 + 12 * i, byteorder, exif);
|
|
}
|
|
|
|
if (onlyParseOne) {
|
|
return;
|
|
}
|
|
|
|
var next = data.getUint32(offset + 2 + 12 * numentries, byteorder);
|
|
if (next !== 0 && next < file.size) {
|
|
parseIFD(data, next + TIFFHeaderOffset, byteorder, exif);
|
|
}
|
|
}
|
|
|
|
// size, in bytes, of each TIFF data type
|
|
var typesize = [
|
|
0, // Unused
|
|
1, // BYTE
|
|
1, // ASCII
|
|
2, // SHORT
|
|
4, // LONG
|
|
8, // RATIONAL
|
|
1, // SBYTE
|
|
1, // UNDEFINED
|
|
2, // SSHORT
|
|
4, // SLONG
|
|
8, // SRATIONAL
|
|
4, // FLOAT
|
|
8 // DOUBLE
|
|
];
|
|
|
|
// This object maps EXIF tag numbers to their names.
|
|
// Only list the ones we want to bother parsing and returning.
|
|
// All others will be ignored.
|
|
var tagnames = {
|
|
/*
|
|
* We don't currently use any of these EXIF tags for anything.
|
|
*
|
|
*
|
|
'256': 'ImageWidth',
|
|
'257': 'ImageHeight',
|
|
'40962': 'PixelXDimension',
|
|
'40963': 'PixelYDimension',
|
|
'306': 'DateTime',
|
|
'315': 'Artist',
|
|
'33432': 'Copyright',
|
|
'36867': 'DateTimeOriginal',
|
|
'33434': 'ExposureTime',
|
|
'33437': 'FNumber',
|
|
'34850': 'ExposureProgram',
|
|
'34867': 'ISOSpeed',
|
|
'37377': 'ShutterSpeedValue',
|
|
'37378': 'ApertureValue',
|
|
'37379': 'BrightnessValue',
|
|
'37380': 'ExposureBiasValue',
|
|
'37382': 'SubjectDistance',
|
|
'37383': 'MeteringMode',
|
|
'37384': 'LightSource',
|
|
'37385': 'Flash',
|
|
'37386': 'FocalLength',
|
|
'41986': 'ExposureMode',
|
|
'41987': 'WhiteBalance',
|
|
'41991': 'GainControl',
|
|
'41992': 'Contrast',
|
|
'41993': 'Saturation',
|
|
'41994': 'Sharpness',
|
|
// These are special tags that we handle internally
|
|
'34665': 'EXIFIFD', // Offset of EXIF data
|
|
'34853': 'GPSIFD', // Offset of GPS data
|
|
*/
|
|
'274' : 'ORIENTATION',
|
|
'513': 'THUMBNAIL', // Offset of thumbnail
|
|
'514': 'THUMBNAILLENGTH' // Length of thumbnail
|
|
};
|
|
|
|
function parseEntry(data, offset, byteorder, exif) {
|
|
var tag = data.getUint16(offset, byteorder);
|
|
var tagname = tagnames[tag];
|
|
|
|
// If we don't know about this tag type or already processed it, skip it
|
|
if (!tagname || exif[tagname]) {
|
|
return;
|
|
}
|
|
|
|
var type = data.getUint16(offset + 2, byteorder);
|
|
var count = data.getUint32(offset + 4, byteorder);
|
|
|
|
var total = count * typesize[type];
|
|
var valueOffset = total <= 4 ? offset + 8 :
|
|
data.getUint32(offset + 8, byteorder) + TIFFHeaderOffset;
|
|
exif[tagname] = parseValue(data, valueOffset, type, count, byteorder);
|
|
}
|
|
|
|
function parseValue(data, offset, type, count, byteorder) {
|
|
var i;
|
|
if (type === 2) { // ASCII string
|
|
var codes = [];
|
|
for (i = 0; i < count - 1; i++) {
|
|
codes[i] = data.getUint8(offset + i);
|
|
}
|
|
return String.fromCharCode.apply(String, codes);
|
|
} else {
|
|
if (count == 1) {
|
|
return parseOneValue(data, offset, type, byteorder);
|
|
} else {
|
|
var values = [];
|
|
var size = typesize[type];
|
|
for (i = 0; i < count; i++) {
|
|
values[i] = parseOneValue(data, offset + size * i, type, byteorder);
|
|
}
|
|
return values;
|
|
}
|
|
}
|
|
}
|
|
|
|
function parseOneValue(data, offset, type, byteorder) {
|
|
switch (type) {
|
|
case 1: // BYTE
|
|
case 7: // UNDEFINED
|
|
return data.getUint8(offset);
|
|
case 2: // ASCII
|
|
// This case is handed in parseValue
|
|
return null;
|
|
case 3: // SHORT
|
|
return data.getUint16(offset, byteorder);
|
|
case 4: // LONG
|
|
return data.getUint32(offset, byteorder);
|
|
case 5: // RATIONAL
|
|
return data.getUint32(offset, byteorder) /
|
|
data.getUint32(offset + 4, byteorder);
|
|
case 6: // SBYTE
|
|
return data.getInt8(offset);
|
|
case 8: // SSHORT
|
|
return data.getInt16(offset, byteorder);
|
|
case 9: // SLONG
|
|
return data.getInt32(offset, byteorder);
|
|
case 10: // SRATIONAL
|
|
return data.getInt32(offset, byteorder) /
|
|
data.getInt32(offset + 4, byteorder);
|
|
case 11: // FLOAT
|
|
return data.getFloat32(offset, byteorder);
|
|
case 12: // DOUBLE
|
|
return data.getFloat64(offset, byteorder);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
'use strict';
|
|
/* global BlobView */
|
|
/* exported getVideoRotation */
|
|
|
|
//
|
|
// Given an MP4/Quicktime based video file as a blob, read through its
|
|
// atoms to find the track header "tkhd" atom and extract the rotation
|
|
// matrix from it. Convert the matrix value to rotation in degrees and
|
|
// pass that number to the specified callback function. If no value is
|
|
// found but the video file is valid, pass null to the callback. If
|
|
// any errors occur, pass an error message (a string) callback.
|
|
//
|
|
// See also:
|
|
// http://androidxref.com/4.0.4/xref/frameworks/base/media/libstagefright/MPEG4Writer.cpp
|
|
// https://developer.apple.com/library/mac/#documentation/QuickTime/QTFF/QTFFChap2/qtff2.html
|
|
//
|
|
function getVideoRotation(blob, rotationCallback) {
|
|
|
|
// A utility for traversing the tree of atoms in an MP4 file
|
|
function MP4Parser(blob, handlers) {
|
|
// Start off with a 1024 chunk from the start of the blob.
|
|
BlobView.get(blob, 0, Math.min(1024, blob.size), function(data, error) {
|
|
// Make sure that the blob is, in fact, some kind of MP4 file
|
|
if (data.byteLength <= 8 || data.getBinaryText(4, 4) !== 'ftyp') {
|
|
handlers.errorHandler('not an MP4 file');
|
|
return;
|
|
}
|
|
parseAtom(data);
|
|
});
|
|
|
|
// Call this with a BlobView object that includes the first 16 bytes of
|
|
// an atom. It doesn't matter whether the body of the atom is included.
|
|
function parseAtom(data) {
|
|
var offset = data.sliceOffset + data.viewOffset; // atom position in blob
|
|
var size = data.readUnsignedInt(); // atom length
|
|
var type = data.readBinaryText(4); // atom type
|
|
var contentOffset = 8; // position of content
|
|
|
|
if (size === 0) {
|
|
// Zero size means the rest of the file
|
|
size = blob.size - offset;
|
|
}
|
|
else if (size === 1) {
|
|
// A size of 1 means the size is in bytes 8-15
|
|
size = data.readUnsignedInt() * 4294967296 + data.readUnsignedInt();
|
|
contentOffset = 16;
|
|
}
|
|
|
|
var handler = handlers[type] || handlers.defaultHandler;
|
|
if (typeof handler === 'function') {
|
|
// If the handler is a function, pass that function a
|
|
// DataView object that contains the entire atom
|
|
// including size and type. Then use the return value
|
|
// of the function as instructions on what to do next.
|
|
data.getMore(data.sliceOffset + data.viewOffset, size, function(atom) {
|
|
// Pass the entire atom to the handler function
|
|
var rv = handler(atom);
|
|
|
|
// If the return value is 'done', stop parsing.
|
|
// Otherwise, continue with the next atom.
|
|
// XXX: For more general parsing we need a way to pop some
|
|
// stack levels. A return value that is an atom name should mean
|
|
// pop back up to this atom type and go on to the next atom
|
|
// after that.
|
|
if (rv !== 'done') {
|
|
parseAtomAt(data, offset + size);
|
|
}
|
|
});
|
|
}
|
|
else if (handler === 'children') {
|
|
// If the handler is this string, then assume that the atom is
|
|
// a container atom and do its next child atom next
|
|
var skip = (type === 'meta') ? 4 : 0; // special case for meta atoms
|
|
parseAtomAt(data, offset + contentOffset + skip);
|
|
}
|
|
else if (handler === 'skip' || !handler) {
|
|
// Skip the atom entirely and go on to the next one.
|
|
// If there is no next one, call the eofHandler or just return
|
|
parseAtomAt(data, offset + size);
|
|
}
|
|
else if (handler === 'done') {
|
|
// Stop parsing
|
|
return;
|
|
}
|
|
}
|
|
|
|
function parseAtomAt(data, offset) {
|
|
if (offset >= blob.size) {
|
|
if (handlers.eofHandler) {
|
|
handlers.eofHandler();
|
|
}
|
|
return;
|
|
}
|
|
else {
|
|
data.getMore(offset, 16, parseAtom);
|
|
}
|
|
}
|
|
}
|
|
|
|
// We want to loop through the top-level atoms until we find the 'moov'
|
|
// atom. Then, within this atom, there are one or more 'trak' atoms.
|
|
// Each 'trak' should begin with a 'tkhd' atom. The tkhd atom has
|
|
// a transformation matrix at byte 48. The matrix is 9 32 bit integers.
|
|
// We'll interpret those numbers as a rotation of 0, 90, 180 or 270.
|
|
// If the video has more than one track, we expect all of them to have
|
|
// the same rotation, so we'll only look at the first 'trak' atom that
|
|
// we find.
|
|
MP4Parser(blob, {
|
|
errorHandler: function(msg) { rotationCallback(msg); },
|
|
eofHandler: function() { rotationCallback(null); },
|
|
defaultHandler: 'skip', // Skip all atoms other than those listed below
|
|
moov: 'children', // Enumerate children of the moov atom
|
|
trak: 'children', // Enumerate children of the trak atom
|
|
tkhd: function(data) { // Pass the tkhd atom to this function
|
|
// The matrix begins at byte 48
|
|
data.advance(48);
|
|
|
|
var a = data.readUnsignedInt();
|
|
var b = data.readUnsignedInt();
|
|
data.advance(4); // we don't care about this number
|
|
var c = data.readUnsignedInt();
|
|
var d = data.readUnsignedInt();
|
|
|
|
if (a === 0 && d === 0) { // 90 or 270 degrees
|
|
if (b === 0x00010000 && c === 0xFFFF0000) {
|
|
rotationCallback(90);
|
|
}
|
|
else if (b === 0xFFFF0000 && c === 0x00010000) {
|
|
rotationCallback(270);
|
|
}
|
|
else {
|
|
rotationCallback('unexpected rotation matrix');
|
|
}
|
|
}
|
|
else if (b === 0 && c === 0) { // 0 or 180 degrees
|
|
if (a === 0x00010000 && d === 0x00010000) {
|
|
rotationCallback(0);
|
|
}
|
|
else if (a === 0xFFFF0000 && d === 0xFFFF0000) {
|
|
rotationCallback(180);
|
|
}
|
|
else {
|
|
rotationCallback('unexpected rotation matrix');
|
|
}
|
|
}
|
|
else {
|
|
rotationCallback('unexpected rotation matrix');
|
|
}
|
|
return 'done';
|
|
}
|
|
});
|
|
}
|
|
|
|
/* exported getImageSize */
|
|
/* global BlobView */
|
|
/* global parseJPEGMetadata */
|
|
|
|
/*
|
|
* Determine the pixel dimensions of an image without actually
|
|
* decoding the image. Passes an object of metadata to the callback
|
|
* function on success or an error message to the error function on
|
|
* failure. The metadata object will include type, width and height
|
|
* properties. Supported image types are GIF, PNG and JPEG. JPEG
|
|
* metadata may also include information about an EXIF preview image.
|
|
*
|
|
* Because of shortcomings in the way Gecko handles images, the
|
|
* Gallery app will crash with an OOM error if it attempts to decode
|
|
* and display an image that is too big. Images require 4 bytes per
|
|
* pixel, so a 10 megapixel photograph requires 40 megabytes of image
|
|
* memory. This function gives the gallery app a way to reject images
|
|
* that are too large.
|
|
*
|
|
* Requires the BlobView class from shared/js/blobview.js and the
|
|
* parseJPEGMetadata() function from shared/js/media/jpeg_metadata_parser.js
|
|
*/
|
|
function getImageSize(blob, callback, error) {
|
|
'use strict';
|
|
|
|
BlobView.get(blob, 0, Math.min(1024, blob.size), function(data) {
|
|
// Make sure we are at least 8 bytes long before reading the first 8 bytes
|
|
if (data.byteLength <= 8) {
|
|
error('corrupt image file');
|
|
return;
|
|
}
|
|
var magic = data.getBinaryText(0, 8);
|
|
if (magic.substring(0, 4) === 'GIF8') {
|
|
try {
|
|
callback({
|
|
type: 'gif',
|
|
width: data.getUint16(6, true),
|
|
height: data.getUint16(8, true)
|
|
});
|
|
}
|
|
catch (e) {
|
|
error(e.toString());
|
|
}
|
|
}
|
|
else if (magic.substring(0, 8) === '\x89PNG\r\n\x1A\n') {
|
|
try {
|
|
callback({
|
|
type: 'png',
|
|
width: data.getUint32(16, false),
|
|
height: data.getUint32(20, false)
|
|
});
|
|
}
|
|
catch (e) {
|
|
error(e.toString());
|
|
}
|
|
}
|
|
else if (magic.substring(0, 2) === 'BM' &&
|
|
data.getUint32(2, true) === blob.size) {
|
|
// This is a BMP file
|
|
try {
|
|
var width, height;
|
|
|
|
if (data.getUint16(14, true) === 12) { // check format version
|
|
width = data.getUint16(18, true); // 16-bit little endian width
|
|
height = data.getUint16(20, true); // 16-bit little endian height
|
|
}
|
|
else { // newer versions of the format use 32-bit ints
|
|
width = data.getUint32(18, true); // 32-bit little endian width
|
|
height = data.getUint32(22, true); // 32-bit little endian height
|
|
}
|
|
|
|
callback({
|
|
type: 'bmp',
|
|
width: width,
|
|
height: height
|
|
});
|
|
}
|
|
catch (e) {
|
|
error(e.toString());
|
|
}
|
|
}
|
|
else if (magic.substring(0, 2) === '\xFF\xD8') {
|
|
parseJPEGMetadata(blob,
|
|
function(metadata) {
|
|
if (metadata.progressive) {
|
|
// If the jpeg parser tells us that this is
|
|
// a progressive jpeg, then treat that as a
|
|
// distinct image type because pjpegs have
|
|
// such different memory requirements than
|
|
// regular jpegs.
|
|
delete metadata.progressive;
|
|
metadata.type = 'pjpeg';
|
|
}
|
|
else {
|
|
metadata.type = 'jpeg';
|
|
}
|
|
callback(metadata);
|
|
},
|
|
error);
|
|
}
|
|
else {
|
|
error('unknown image type');
|
|
}
|
|
});
|
|
}
|
|
|
|
'use strict';
|
|
/* global
|
|
CONFIG_MAX_IMAGE_PIXEL_SIZE,
|
|
CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT,
|
|
CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH,
|
|
cropResizeRotate,
|
|
Downsample,
|
|
getImageSize,
|
|
getVideoRotation,
|
|
isPhone,
|
|
parseJPEGMetadata,
|
|
videostorage
|
|
*/
|
|
/* exported
|
|
metadataParser
|
|
*/
|
|
|
|
//
|
|
// This file defines a single metadataParsers object. The two
|
|
// properties of this object are metadata parsing functions for image
|
|
// files and video files, intended for use with the MediaDB class.
|
|
//
|
|
// This file depends on JPEGMetadataParser.js and blobview.js
|
|
//
|
|
var metadataParser = (function() {
|
|
// If we generate our own thumbnails, aim for this size.
|
|
// Calculate needed size from longer side of the screen.
|
|
var THUMBNAIL_WIDTH = computeThumbnailWidth();
|
|
var THUMBNAIL_HEIGHT = THUMBNAIL_WIDTH;
|
|
|
|
function computeThumbnailWidth() {
|
|
// Make sure this works regardless of current device orientation
|
|
var width = window.innerWidth || screen.width;
|
|
var height = window.innerHeight || screen.height;
|
|
var portraitWidth = Math.max(1, Math.min(width, height));
|
|
var landscapeWidth = Math.max(1, Math.max(width, height));
|
|
var thumbnailsPerRowPortrait = isPhone ? 3 : 4;
|
|
var thumbnailsPerRowLandscape = isPhone ? 4 : 6;
|
|
return Math.round(window.devicePixelRatio *
|
|
Math.max(portraitWidth / thumbnailsPerRowPortrait,
|
|
landscapeWidth / thumbnailsPerRowLandscape));
|
|
}
|
|
|
|
var thumbnailSize = {
|
|
width: THUMBNAIL_WIDTH,
|
|
height: THUMBNAIL_HEIGHT
|
|
};
|
|
|
|
// Don't try to decode image files of unknown type if bigger than this
|
|
var MAX_UNKNOWN_IMAGE_FILE_SIZE = 0.5 * 1024 * 1024; // half a megabyte
|
|
|
|
// If a jpeg does not have a suitable EXIF preview and has more than
|
|
// this number of pixels, then we create an external preview for it
|
|
var MAX_PREVIEWLESS_JPEG_SIZE = 5 * 1024 * 1024; // 5 megapixels
|
|
|
|
// An <img> element for loading images
|
|
var offscreenImage = new Image();
|
|
|
|
// The screen size. Preview images must be at least this big
|
|
// Note that we use screen.width instead of window.innerWidth because
|
|
// the window size is different when we are invoked for a pick activity
|
|
// (non-fullscreen) and when we are invoked normally (fullscreen).
|
|
var sw = screen.width * window.devicePixelRatio;
|
|
var sh = screen.height * window.devicePixelRatio;
|
|
|
|
// Create a thumbnail size canvas, copy the <img> or <video> into it
|
|
// cropping the edges as needed to make it fit, and then extract the
|
|
// thumbnail image as a blob and pass it to the callback.
|
|
// This utility function is used by both the image and video metadata parsers
|
|
function createThumbnailFromElement(elt, video, rotation,
|
|
mirrored, callback, error) {
|
|
try {
|
|
// Create a thumbnail image
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = THUMBNAIL_WIDTH;
|
|
canvas.height = THUMBNAIL_HEIGHT;
|
|
var context = canvas.getContext('2d', { willReadFrequently: true });
|
|
var eltwidth = elt.width;
|
|
var eltheight = elt.height;
|
|
var scalex = canvas.width / eltwidth;
|
|
var scaley = canvas.height / eltheight;
|
|
|
|
// Take the larger of the two scales: we crop the image to the thumbnail
|
|
var scale = Math.max(scalex, scaley);
|
|
|
|
// Calculate the region of the image that will be copied to the
|
|
// canvas to create the thumbnail
|
|
var w = Math.round(THUMBNAIL_WIDTH / scale);
|
|
var h = Math.round(THUMBNAIL_HEIGHT / scale);
|
|
var x = Math.round((eltwidth - w) / 2);
|
|
var y = Math.round((eltheight - h) / 2);
|
|
|
|
var centerX = Math.floor(THUMBNAIL_WIDTH / 2);
|
|
var centerY = Math.floor(THUMBNAIL_HEIGHT / 2);
|
|
|
|
// If a orientation is specified, rotate/mirroring the canvas context.
|
|
if (rotation || mirrored) {
|
|
context.save();
|
|
// All transformation are applied to the center of the thumbnail.
|
|
context.translate(centerX, centerY);
|
|
}
|
|
if (mirrored) {
|
|
context.scale(-1, 1);
|
|
}
|
|
if (rotation) {
|
|
switch (rotation) {
|
|
case 90:
|
|
context.rotate(Math.PI / 2);
|
|
break;
|
|
case 180:
|
|
context.rotate(Math.PI);
|
|
break;
|
|
case 270:
|
|
context.rotate(-Math.PI / 2);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (rotation || mirrored) {
|
|
context.translate(-centerX, -centerY);
|
|
}
|
|
|
|
// Draw that region of the image into the canvas, scaling it down
|
|
context.drawImage(elt, x, y, w, h,
|
|
0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
|
|
|
|
// Restore the default rotation so the play arrow comes out correctly
|
|
if (rotation || mirrored) {
|
|
context.restore();
|
|
}
|
|
|
|
// If this is a video, superimpose a translucent play button over
|
|
// the captured video frame to distinguish it from a still photo thumbnail
|
|
if (video) {
|
|
// First draw a transparent gray circle
|
|
context.fillStyle = 'rgba(0, 0, 0, .2)';
|
|
context.beginPath();
|
|
context.arc(THUMBNAIL_WIDTH / 2, THUMBNAIL_HEIGHT / 2,
|
|
THUMBNAIL_HEIGHT / 5, 0, 2 * Math.PI, false);
|
|
context.fill();
|
|
|
|
// Now outline the circle in white
|
|
context.strokeStyle = 'rgba(255,255,255,.6)';
|
|
context.lineWidth = 2;
|
|
context.stroke();
|
|
|
|
// And add a white play arrow.
|
|
context.beginPath();
|
|
context.fillStyle = 'rgba(255,255,255,.6)';
|
|
// The height of an equilateral triangle is sqrt(3)/2 times the side
|
|
var side = THUMBNAIL_HEIGHT / 5;
|
|
var triangle_height = side * Math.sqrt(3) / 2;
|
|
context.moveTo(THUMBNAIL_WIDTH / 2 + triangle_height * 2 / 3,
|
|
THUMBNAIL_HEIGHT / 2);
|
|
context.lineTo(THUMBNAIL_WIDTH / 2 - triangle_height / 3,
|
|
THUMBNAIL_HEIGHT / 2 - side / 2);
|
|
context.lineTo(THUMBNAIL_WIDTH / 2 - triangle_height / 3,
|
|
THUMBNAIL_HEIGHT / 2 + side / 2);
|
|
context.closePath();
|
|
context.fill();
|
|
}
|
|
|
|
canvas.toBlob(function(blob) {
|
|
context = null;
|
|
canvas.width = canvas.height = 0;
|
|
canvas = null;
|
|
callback(blob);
|
|
}, 'image/jpeg');
|
|
} catch (ex) {
|
|
// An error may be thrown when the drawImage decodes a broken/trancated
|
|
// image.
|
|
// The elt may be a offscreen image. So, the image metadata is parsed, and
|
|
// the image data is loaded but not decoded. The drawImage triggers the
|
|
// image decoder to decode the image data. And an error may be thrown.
|
|
error('createThumbnailFromElement:' + ex.message);
|
|
}
|
|
}
|
|
|
|
var VIDEOFILE = /DCIM\/\d{3}MZLLA\/VID_\d{4}\.jpg/;
|
|
|
|
function metadataParser(file, metadataCallback, metadataError, bigFile) {
|
|
// If the file is a poster image for a video file, then we've want
|
|
// video metadata, not image metadata
|
|
if (VIDEOFILE.test(file.name)) {
|
|
videoMetadataParser(file, metadataCallback, metadataError);
|
|
return;
|
|
}
|
|
|
|
// Figure out how big the image is if we can. For JPEG files this
|
|
// calls the JPEG parser and returns the EXIF preview if there is one.
|
|
getImageSize(file, gotImageSize, gotImageSizeError);
|
|
|
|
function gotImageSizeError(errmsg) {
|
|
// The image is not a JPEG, PNG, GIF or BMP file. We may still be
|
|
// able to decode and display it but we don't know the image
|
|
// size, so we won't even try if the file is too big.
|
|
if (file.size > MAX_UNKNOWN_IMAGE_FILE_SIZE) {
|
|
metadataError('Ignoring large file ' + file.name);
|
|
return;
|
|
}
|
|
|
|
// If the file is too small to be an image, ignore it
|
|
if (file.size < 32) {
|
|
metadataError('Ignoring small file ' + file.name);
|
|
return;
|
|
}
|
|
|
|
// If the error message is anything other than unknown image type
|
|
// it means we've got a corrupt image file, or the image metdata parser
|
|
// can't handle the file for some reason. Log a warning but keep going
|
|
// in case the image is good and the metadata parser is buggy.
|
|
if (errmsg !== 'unknown image type') {
|
|
console.warn('getImageSize', errmsg, file.name);
|
|
}
|
|
|
|
// If it is not too big create a preview and thumbnail.
|
|
createThumbnailAndPreview(file,
|
|
metadataCallback,
|
|
metadataError,
|
|
false,
|
|
bigFile,
|
|
{});
|
|
}
|
|
|
|
function gotImageSize(metadata) {
|
|
//
|
|
// If the image is too big, reject it now so we don't have
|
|
// memory trouble later.
|
|
//
|
|
// CONFIG_MAX_IMAGE_PIXEL_SIZE is maximum image resolution we
|
|
// can handle. It's from config.js which is generated at build
|
|
// time (see build/application-data.js).
|
|
//
|
|
// For jpeg images we can downsample while decoding which means that
|
|
// we can handle images that are quite a bit larger.
|
|
//
|
|
// getImageSize() distinguishes pjpeg files from jpeg, because
|
|
// progressive jpeg files are quite different than regular jpegs.
|
|
// Not only do pjpegs not benefit from using #-moz-samplesize, they
|
|
// actually require more memory to decode than other images of the same
|
|
// resolution, so we actually need to reduce the image size limit for
|
|
// this image type.
|
|
//
|
|
var imagesizelimit = CONFIG_MAX_IMAGE_PIXEL_SIZE;
|
|
if (metadata.type === 'jpeg') {
|
|
imagesizelimit *= Downsample.MAX_AREA_REDUCTION;
|
|
}
|
|
else if (metadata.type === 'pjpeg') {
|
|
// As a semi-educated guess, we'll say that we can handle pjpegs
|
|
// 2/3rds the size of the largest PNG we can handle.
|
|
imagesizelimit *= 2 / 3;
|
|
}
|
|
|
|
//
|
|
// Even if we can downsample an image while decoding it, we still
|
|
// have to read the entire image file. If the file is particularly
|
|
// large we might also have memory problems. (See bug 1008834: a 20mb
|
|
// 80mp jpeg file will cause an OOM on Tarako even though we can
|
|
// decode it at < 2mp). Rather than adding another build-time config
|
|
// variable to specify the maximum file size, however, we'll just
|
|
// base the file size limit on CONFIG_MAX_IMAGE_PIXEL_SIZE.
|
|
// So if that variable is set to 2M, then we might use up to 12Mb of
|
|
// memory. 2 * 2M bytes for the image file and 4 bytes times 2M pixels
|
|
// for the decoded image. A 4mb file size limit should accomodate
|
|
// most JPEG files up to 12 or 16 megapixels.
|
|
//
|
|
// Note that this size limit is just a guess. If we continue to see
|
|
// OOMs with large files then we should change the number 2 below
|
|
// to something smaller.
|
|
//
|
|
var filesizelimit = 2 * CONFIG_MAX_IMAGE_PIXEL_SIZE;
|
|
|
|
if (metadata.width * metadata.height > imagesizelimit ||
|
|
file.size > filesizelimit) {
|
|
metadataError('Ignoring high-resolution image ' + file.name);
|
|
return;
|
|
}
|
|
|
|
// If the file included a preview image, see if it is big enough
|
|
if (metadata.preview) {
|
|
// Create a blob that is just the preview image
|
|
var previewblob = file.slice(metadata.preview.start,
|
|
metadata.preview.end,
|
|
'image/jpeg');
|
|
|
|
// Check to see if the preview is big enough to use in MediaFrame
|
|
parseJPEGMetadata(previewblob, previewsuccess, previewerror);
|
|
}
|
|
else {
|
|
// If there wasn't a preview image, then generate a preview and
|
|
// thumbnail from the full size image.
|
|
useFullsizeImage();
|
|
}
|
|
|
|
function previewerror(msg) {
|
|
// The preview isn't a valid jpeg file, so use the full image to
|
|
// create a preview and a thumbnail
|
|
console.error(msg);
|
|
useFullsizeImage();
|
|
}
|
|
|
|
// If we can't find a valid embedded EXIF preview image then we come here
|
|
function useFullsizeImage() {
|
|
// If the full size image is a JPEG then we'll just skip the
|
|
// preview generation because we can downsample and decode it
|
|
// at preview size when needed. So in this case, we just need
|
|
// to create a thumbnail for the image. On the other hand, if
|
|
// this is not a JPEG image then we have to create both a
|
|
// thumbnail and an external preview image. Note that progressive
|
|
// jpegs will have a type 'pjpeg' and will be handled below
|
|
// like png images.
|
|
|
|
// For jpeg images with image sizes more than 5MP, we need
|
|
// to create preview on sdcard to avoid flash while opening image.
|
|
// See bug 1181290.
|
|
// This constraint can be removed when gecko supports downscale during
|
|
// decode by canvas size for offscreen images. This will
|
|
// facilitate removing #moz-sample-size and using ddd.
|
|
// See bug 1206206.
|
|
|
|
if (metadata.type === 'jpeg') {
|
|
if (metadata.width * metadata.height <= MAX_PREVIEWLESS_JPEG_SIZE) {
|
|
// If the jpeg image isn't too big we don't need a preview
|
|
// because we can decode it quickly enough at screen size.
|
|
// So in this case we just need to create a thumbnail.
|
|
cropResizeRotate(file, null, thumbnailSize, null, metadata,
|
|
function gotThumbnail(error, thumbnailBlob) {
|
|
if (error) {
|
|
var msg = 'Failed to create thumbnail for' +
|
|
file.name;
|
|
console.error(msg);
|
|
metadataError(msg);
|
|
return;
|
|
}
|
|
|
|
metadata.preview = null;
|
|
metadata.thumbnail = thumbnailBlob;
|
|
metadataCallback(metadata);
|
|
});
|
|
}
|
|
else {
|
|
// Otherwise, this is a high-resolution jpeg that does not
|
|
// have an EXIF preview big enough for the screen. We need
|
|
// to create both a thumbnail and a preview image but we can
|
|
// do that with efficient downsampling using cropResizeRotate().
|
|
createJPEGThumbnailAndPreview(file, metadata,
|
|
metadataCallback,
|
|
metadataError);
|
|
}
|
|
}
|
|
else {
|
|
// In this case we have no preview and the image is not a jpeg
|
|
// that can be downsampled with #-moz-samplesize
|
|
createThumbnailAndPreview(file,
|
|
metadataCallback,
|
|
metadataError,
|
|
false,
|
|
bigFile,
|
|
metadata);
|
|
}
|
|
}
|
|
|
|
function previewsuccess(previewmetadata) {
|
|
// Size of the preview image
|
|
var pw = previewmetadata.width;
|
|
var ph = previewmetadata.height;
|
|
// optional configuration specifying minimum size
|
|
var mw = CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH;
|
|
var mh = CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT;
|
|
|
|
var bigenough;
|
|
|
|
// If config.js specifies a minimum required preview size,
|
|
// then this preview is big enough if both dimensions are
|
|
// larger than that configured minimum. Otherwise, the preview
|
|
// is big enough if at least one dimension is >= the screen
|
|
// size in both portait and landscape mode.
|
|
if (mw && mh) {
|
|
bigenough =
|
|
Math.max(pw, ph) >= Math.max(mw, mh) &&
|
|
Math.min(pw, ph) >= Math.min(mw, mh);
|
|
}
|
|
else {
|
|
bigenough = (pw >= sw || ph >= sh) && (pw >= sh || ph >= sw);
|
|
}
|
|
|
|
// Does the aspect ratio of the preview match the aspect ratio of
|
|
// the full-size image? If not we have to assume it is distorted
|
|
// and should not be used.
|
|
var aspectRatioMatches =
|
|
Math.abs(pw / ph - metadata.width / metadata.height) < 0.01;
|
|
|
|
// If the preview is good, use it to create a thumbnail.
|
|
if (bigenough && aspectRatioMatches) {
|
|
metadata.preview.width = pw;
|
|
metadata.preview.height = ph;
|
|
|
|
// This is the most common case. We've got a JPEG image with a
|
|
// JPEG preview. All we need is a thumbnail, and we can create that
|
|
// most efficiently with cropResizeRotate which will use
|
|
// #-moz-samplesize to save memory while decoding the image.
|
|
// Note that we apply the EXIF orientation information from
|
|
// the full size image to the preview image.
|
|
previewmetadata.rotation = metadata.rotation;
|
|
previewmetadata.mirrored = metadata.mirrored;
|
|
cropResizeRotate(previewblob, null, thumbnailSize, null,
|
|
previewmetadata,
|
|
function gotThumbnailBlob(error, thumbnailBlob) {
|
|
if (error) {
|
|
previewerror(error);
|
|
return;
|
|
}
|
|
metadata.thumbnail = thumbnailBlob;
|
|
metadataCallback(metadata);
|
|
});
|
|
} else {
|
|
// Preview isn't big enough so get one the hard way
|
|
useFullsizeImage();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// This function is called for JPEG images that do not have a suitable
|
|
// EXIF preview image and that have more than MAX_PREVIEWLESS_JPEG_SIZE
|
|
// pixels. It creates and saves a preview image and a thumbnail.
|
|
// Unlike createThumbnailAndPreview(), it uses cropResizeRotate() which
|
|
// downsamples jpeg images while decoding them to save memory.
|
|
function createJPEGThumbnailAndPreview(file, metadata,
|
|
metadataCallback, metadataError)
|
|
{
|
|
|
|
// Figure out the preview size.
|
|
var scale = Math.max(Math.min(sw / metadata.width,
|
|
sh / metadata.height, 1),
|
|
Math.min(sh / metadata.width,
|
|
sw / metadata.height, 1));
|
|
var previewSize = {
|
|
width: Math.round(metadata.width * scale),
|
|
height: Math.round(metadata.height * scale)
|
|
};
|
|
|
|
// Create a preview image.
|
|
// Note that we can't use the full metadata object here
|
|
// because we don't want the preview image to be rotated.
|
|
// We want the same orientation as the original image
|
|
cropResizeRotate(file, null, previewSize, null,
|
|
{ width: metadata.width, height: metadata.height },
|
|
function gotPreview(error, previewBlob) {
|
|
if (error) {
|
|
var msg = 'Failed to create preview for' +
|
|
file.name;
|
|
console.error(msg);
|
|
metadataError(msg);
|
|
return;
|
|
}
|
|
|
|
// Save this preview to the sdcard
|
|
savePreview(file, previewBlob, metadataError, function(filename) {
|
|
// Record the preview image in the metadata
|
|
metadata.preview = {
|
|
filename: filename,
|
|
width: previewSize.width,
|
|
height: previewSize.height
|
|
};
|
|
|
|
// Now create the thumbnail from this smaller preview image
|
|
// In this case we need to pass the size of the preview
|
|
// and the rotation of the original image.
|
|
cropResizeRotate(previewBlob, null, thumbnailSize, null,
|
|
{
|
|
width: previewSize.width,
|
|
height: previewSize.height,
|
|
rotation: metadata.rotation
|
|
},
|
|
function gotThumbnail(error, thumbnailBlob) {
|
|
// If thumbnail creation failed report the error
|
|
if (error) {
|
|
metadataError(error);
|
|
return;
|
|
}
|
|
|
|
// Otherwise, remember thumbnail and we're done
|
|
metadata.thumbnail = thumbnailBlob;
|
|
metadataCallback(metadata);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// Load an image from a file into an <img> tag, and then use that
|
|
// to get its dimensions and create a thumbnail. Store these values in
|
|
// a metadata object, and pass the object to the callback function.
|
|
// If anything goes wrong, pass an error message to the error function.
|
|
// If it is a large image, create and save a preview for it as well.
|
|
//
|
|
// We used to call this function for every image. Now that gecko supports
|
|
// the #-moz-samplesize media fragment, however, we handle jpeg images
|
|
// with cropResizeRotate() for memory efficiency, and this function is
|
|
// used only for non-jpeg files.
|
|
function createThumbnailAndPreview(file, callback, error, nopreview,
|
|
bigFile, metadata) {
|
|
var url = URL.createObjectURL(file);
|
|
offscreenImage.src = url;
|
|
|
|
offscreenImage.onerror = function() {
|
|
URL.revokeObjectURL(url);
|
|
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
|
|
error('createThumbnailAndPreview: Image failed to load');
|
|
};
|
|
|
|
offscreenImage.onload = function() {
|
|
URL.revokeObjectURL(url);
|
|
|
|
var iw = offscreenImage.width;
|
|
var ih = offscreenImage.height;
|
|
|
|
// Don't overwrite the metadata in the case we read a previewblob.
|
|
if (!nopreview) {
|
|
metadata.width = iw;
|
|
metadata.height = ih;
|
|
}
|
|
|
|
// If this is a big image, then decoding it takes a lot of memory.
|
|
// We set this flag to prevent the user from zooming in on other
|
|
// images at the same time because that also takes a lot of memory
|
|
// and we don't want to crash with an OOM. If we find one big image
|
|
// we assume that there may be others, so the flag remains set until
|
|
// the current scan is complete.
|
|
//
|
|
// XXX: When bug 854795 is fixed, we'll be able to create previews
|
|
// for large images without using so much memory, and we can remove
|
|
// this flag then.
|
|
if (iw * ih > 2 * 1024 * 1024 && bigFile) {
|
|
bigFile();
|
|
}
|
|
|
|
// If the image was already thumbnail size, it is its own thumbnail
|
|
// and it does not need a preview
|
|
if (metadata.width <= THUMBNAIL_WIDTH &&
|
|
metadata.height <= THUMBNAIL_HEIGHT) {
|
|
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
|
|
metadata.thumbnail = file;
|
|
callback(metadata);
|
|
}
|
|
else {
|
|
createThumbnailFromElement(
|
|
offscreenImage,
|
|
false,
|
|
metadata.rotation || 0,
|
|
metadata.mirrored || false,
|
|
gotThumbnail,
|
|
error);
|
|
}
|
|
|
|
function gotThumbnail(thumbnail) {
|
|
metadata.thumbnail = thumbnail;
|
|
// If no preview was requested, or if if the image was less
|
|
// than half a megapixel then it does not need a preview
|
|
// image, and we can call the callback right away
|
|
if (nopreview || metadata.width * metadata.height < 512 * 1024) {
|
|
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
|
|
callback(metadata);
|
|
}
|
|
else {
|
|
// Otherwise, this was a big image and we need to create a
|
|
// preview for it so we can avoid decoding the full size
|
|
// image again when possible
|
|
createAndSavePreview();
|
|
}
|
|
}
|
|
|
|
function createAndSavePreview() {
|
|
// Figure out the preview size.
|
|
// Make sure the size is big enough for both landscape and portrait
|
|
var scale = Math.max(Math.min(sw / iw, sh / ih, 1),
|
|
Math.min(sh / iw, sw / ih, 1));
|
|
// preview width and height
|
|
var pw = Math.round(iw * scale);
|
|
var ph = Math.round(ih * scale);
|
|
|
|
// Create the preview in a canvas
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = pw;
|
|
canvas.height = ph;
|
|
var context = canvas.getContext('2d', { willReadFrequently: true });
|
|
context.drawImage(offscreenImage, 0, 0, iw, ih, 0, 0, pw, ph);
|
|
canvas.toBlob(function(blob) {
|
|
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
|
|
canvas.width = canvas.height = 0;
|
|
savePreview(file, blob, error, function(filename) {
|
|
metadata.preview = {
|
|
filename: filename,
|
|
width: pw,
|
|
height: ph
|
|
};
|
|
callback(metadata);
|
|
});
|
|
}, 'image/jpeg');
|
|
}
|
|
};
|
|
}
|
|
|
|
// Given an image file and a preview blob for it, this function saves
|
|
// the preview to a file in .gallery/previews/ and then passes the filename
|
|
// to the specified callback. Calls errorCallback if an error occurs.
|
|
// This is used by createThumbnailAndPreview() and by
|
|
// createJPEGThumbnailAndPreview().
|
|
function savePreview(file, previewblob, errorCallback, filenameCallback) {
|
|
var storage = navigator.getDeviceStorage('pictures');
|
|
var filename;
|
|
if (file.name[0] === '/') {
|
|
// We expect file.name to be a fully qualified name (perhaps
|
|
// something like /sdcard/DCIM/100MZLLA/IMG_0001.jpg).
|
|
var slashIndex = file.name.indexOf('/', 1);
|
|
if (slashIndex < 0) {
|
|
errorCallback('savePreview: Bad filename: \'' + file.name + '\'');
|
|
return;
|
|
}
|
|
filename =
|
|
file.name.substring(0, slashIndex) + // storageName (i.e. /sdcard)
|
|
|
|
'/.gallery/previews' +
|
|
file.name.substring(slashIndex); // rest of path (i,e, /DCIM/...)
|
|
} else {
|
|
// On non-composite storage areas (e.g. desktop), file.name will be
|
|
// a relative path.
|
|
filename = '.gallery/previews/' + file.name;
|
|
}
|
|
|
|
// Add a '.jpeg' extension to all preview files. This is necessary
|
|
// because all previews get saved as jpegs even if the original
|
|
// image is not a jpeg. But DeviceStorage uses the filename extension
|
|
// to determine the file type.
|
|
filename += '.jpeg';
|
|
|
|
// Delete any existing preview by this name
|
|
var delreq = storage.delete(filename);
|
|
delreq.onsuccess = delreq.onerror = save;
|
|
|
|
function save() {
|
|
var savereq = storage.addNamed(previewblob, filename);
|
|
savereq.onerror = function() {
|
|
console.error('Could not save preview image', filename);
|
|
};
|
|
|
|
// Don't actually wait for the save to complete.
|
|
filenameCallback(filename);
|
|
}
|
|
}
|
|
|
|
function videoMetadataParser(file, metadataCallback, errorCallback) {
|
|
var metadata = {};
|
|
var extensions = ['webm', '3gp', 'mp4'];
|
|
function findVideo() {
|
|
var videofilename = file.name.replace(/\.jpg$/, '.' + extensions.shift());
|
|
var getreq = videostorage.get(videofilename);
|
|
getreq.onerror = function() {
|
|
if (extensions.length) findVideo();
|
|
else errorCallback('cannot get video file: ' + videofilename);
|
|
};
|
|
getreq.onsuccess = function() {
|
|
metadata.video = videofilename;
|
|
if (/\.webm$/.test(videofilename)) {
|
|
metadata.rotation = 0;
|
|
getVideoThumbnailAndSize();
|
|
return;
|
|
}
|
|
getVideoRotation(getreq.result, function(rotation) {
|
|
if (typeof rotation === 'number') {
|
|
metadata.rotation = rotation;
|
|
getVideoThumbnailAndSize();
|
|
} else errorCallback('Video rotation: ' + rotation);
|
|
});
|
|
};
|
|
}
|
|
findVideo();
|
|
|
|
function getVideoThumbnailAndSize() {
|
|
var url = URL.createObjectURL(file);
|
|
offscreenImage.src = url;
|
|
|
|
offscreenImage.onerror = function() {
|
|
URL.revokeObjectURL(url);
|
|
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
|
|
errorCallback('getVideoThumanailAndSize: Image failed to load');
|
|
};
|
|
|
|
offscreenImage.onload = function() {
|
|
URL.revokeObjectURL(url);
|
|
|
|
// We store the unrotated size of the poster image. The video
|
|
// itself might be larger or smaller than this, but it will
|
|
// have the same orientation and aspect ratio.
|
|
metadata.width = offscreenImage.width;
|
|
metadata.height = offscreenImage.height;
|
|
|
|
createThumbnailFromElement(offscreenImage,
|
|
true,
|
|
metadata.rotation,
|
|
false,
|
|
function(thumbnail) {
|
|
metadata.thumbnail = thumbnail;
|
|
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
|
|
metadataCallback(metadata);
|
|
},
|
|
errorCallback);
|
|
};
|
|
}
|
|
}
|
|
|
|
return metadataParser;
|
|
}());
|