/* 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 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 or