/*
* This shared module defines a single ImageUtils object in the global scope.
*
* ImageUtils.getSizeAndType() takes a Blob as its argument and
* returns a Promise. If the promise resolves, the resolve method is
* passed an object with width, height, and type properties. Width and
* height specify the width and height of the image in pixels. Type
* specifies the MIME type of the image. If the promise is rejected,
* the reject method will be passed a string error message.
*
* Supported image types are JPEG, PNG, GIF and BMP. The MIME type
* strings returned when a promise resolves are "image/jpeg",
* "image/png", "image/gif", and "image/bmp".
*
* If you pass a blob that holds an image of some other type (or a
* blob that is not an image at all) then promise will be rejected.
*
* getSizeAndType() does not decode the image to determine its size
* because can require large amounts of memory. Instead, it parses the
* image file to determine its size and typically only needs to read a
* prefix of the file into memory.
*
* getSizeAndType() differs from the getImageSize() function defined
* in shared/js/media/immage_size.js. That function is similar but
* also parses EXIF metadata for JPEG images and is only suitable if
* you need to access EXIF data.
*/
(function(exports) {
'use strict';
/* global Promise */
var ImageUtils = exports.ImageUtils = {};
// Define constants for the MIME types we use
const JPEG = 'image/jpeg';
const PNG = 'image/png';
const GIF = 'image/gif';
const BMP = 'image/bmp';
// Export them
ImageUtils.JPEG = JPEG;
ImageUtils.PNG = PNG;
ImageUtils.GIF = GIF;
ImageUtils.BMP = BMP;
ImageUtils.getSizeAndType = function getSizeAndType(imageBlob) {
if (!(imageBlob instanceof Blob)) {
console.log('not blob');
return Promise.reject(new TypeError('argument is not a Blob'));
}
return new Promise(function(resolve, reject) {
if (imageBlob.size <= 16) {
reject('corrupt image file');
return;
}
// Start off assuming that we'll read the first 32kb of the file
var bytesToRead = 32 * 1024;
// But if the blob indicates that this is a PNG, GIF, or BMP, then
// we can read less since the size information is always close to the
// start of the file for those formats.
if (imageBlob.type === PNG ||
imageBlob.type === GIF ||
imageBlob.type === BMP) {
bytesToRead = 512;
}
// Try to find the information we want in the first bytes of the file
findSizeAndType(imageBlob, bytesToRead, success, tryagain);
// If we got the size and type data, resolve the promise
function success(data) {
resolve(data);
}
// If we didn't find it, try again for JPEG files
function tryagain(data) {
// If we were able to verify that this blob is a jpeg file
// then we will try again because in JPEG files, the size
// comes after the EXIF data so if there is a large preview image,
// the size might be more than 32kb into the file
if (data.type === JPEG) {
findSizeAndType(imageBlob, imageBlob.size, success, failure);
}
// For all other known image types the type and size data is
// near the start of the file, if we didn't find it on this first
// try we need to give up now.
else {
reject(data.error);
}
}
// If we didn't find the data even after a second attempt then reject.
function failure(data) {
reject(data.error);
}
});
// This is the internal helper function that actually reads the blob
// and finds type and size information.
function findSizeAndType(imageBlob, amountToRead, success, failure) {
var slice = imageBlob.slice(0, Math.min(amountToRead, imageBlob.size));
var reader = new FileReader();
reader.readAsArrayBuffer(slice);
reader.onloadend = function() {
parseImageData(reader.result);
};
function parseImageData(buffer) {
var header = new Uint8Array(buffer, 0, 16);
var view = new DataView(buffer);
if (header[0] === 0x89 &&
header[1] === 0x50 && /* P */
header[2] === 0x4e && /* N */
header[3] === 0x47 && /* G */
header[4] === 0x0d && /* \r */
header[5] === 0x0a && /* \n */
header[6] === 0x1A &&
header[7] === 0x0a &&
header[12] === 0x49 && /* I */
header[13] === 0x48 && /* H */
header[14] === 0x44 && /* D */
header[15] === 0x52) /* R */
{
// This is a PNG file
try {
success({
type: PNG,
width: view.getUint32(16, false), // 32 bit big endian width
height: view.getUint32(20, false) // 32 bit big endian height
});
}
catch (ex) {
failure({ error: ex.toString()});
}
}
else if (header[0] === 0x47 && /* G */
header[1] === 0x49 && /* I */
header[2] === 0x46 && /* F */
header[3] === 0x38 && /* 8 */
(header[4] === 0x37 || /* 7 */
header[4] === 0x39) && /* or 9 */
header[5] === 0x61) /* a */
{
// This is a GIF file
try {
success({
type: GIF,
width: view.getUint16(6, true), // 16-bit little endian width
height: view.getUint16(8, true) // 16-big little endian height
});
}
catch (ex) {
failure({ error: ex.toString() });
}
}
else if (header[0] === 0x42 && /* B */
header[1] === 0x4D && /* M */
view.getUint32(2, true) === imageBlob.size)
{
// This is a BMP file
try {
var width, height;
if (view.getUint16(14, true) === 12) { // check format version
width = view.getUint16(18, true); // 16-bit little endian width
height = view.getUint16(20, true); // 16-bit little endian height
}
else { // newer versions of the format use 32-bit ints
width = view.getUint32(18, true); // 32-bit little endian width
height = view.getUint32(22, true); // 32-bit little endian height
}
success({
type: BMP,
width: width,
height: height
});
}
catch (ex) {
failure({ error: ex.toString() });
}
}
else if (header[0] === 0xFF &&
header[1] === 0xD8) {
var value = {
type: JPEG
};
// This is a JPEG file. To find its width and height we need
// to skip past the EXIF data and find the start of image data.
try {
var offset = 2;
// Loop through the segments of the file until we find an SOF
// segment with image size or until we reach the end of our data.
for (;;) {
// The byte at the current offset should be 0xFF marking
// the start of a new segment.
if (view.getUint8(offset) !== 0xFF) {
failure({ error: 'corrupt JPEG file' });
}
var segmentType = view.getUint8(offset + 1);
var segmentSize = view.getUint16(offset + 2) + 2;
// If we found an SOF "Start of Frame" segment, we can get
// the image size from it.
if ((segmentType >= 0xC0 && segmentType <= 0xC3) ||
(segmentType >= 0xC5 && segmentType <= 0xC7) ||
(segmentType >= 0xC9 && segmentType <= 0xCB) ||
(segmentType >= 0xCD && segmentType <= 0xCF))
{
// 16-bit big-endian dimensions, height first
value.height = view.getUint16(offset + 5, false);
value.width = view.getUint16(offset + 7, false);
success(value);
break;
}
// Otherwise, move on to the next segment
offset += segmentSize;
// We may not have read the entire file into our array buffer
// so we have to make sure we have not gone past the end of
// the buffer before looping again. Note that in this case
// the object we pass to the failure callback includes the
// image type. This is a signal to getSizeAndType that it
// should try again with a larger ArrayBuffer.
if (offset + 9 > view.byteLength) {
value.error = 'corrupt JPEG file';
failure(value);
break;
}
}
}
catch (ex) {
failure({ error: ex.toString() });
}
}
else {
failure({ error: 'unknown image type' });
}
}
}
};
//
// Given a blob containing an image file, this function returns a
// Promise that will resolve to a blob for an image file that has
// the specified width and height. If the blob already has that
// size, it is returned unchanged. If the image is bigger than the
// specified size, then it will be shrunk and cropped as necessary
// to fit the specified dimensions. If the image is smaller than
// the specified size, it will be enlarged and cropped to fit. The
// cropping is done in the same way as with the CSS
// background-size:cover attribute.
//
// If the image is resized and outputType is "image/jpeg" or
// "image/png" then that type will be used for the resized image. If
// a JPEG or PNG image is resized and outputType is not specified,
// then the output image will have the same type as the input image.
// In all other cases, a resized image will be encoded as a PNG image.
// Note that when an image does not need to be resized its type is not
// changed, even if outputType is specified.
// 'encoderOptions' is passed 'as is' to the 'toBlob' method of canvas:
// A Number between 0 and 1 indicating image quality if the requested type
// is image/jpeg or image/webp. If this argument is anything else,
// the default value for image quality is used.
ImageUtils.resizeAndCropToCover = function(inputImageBlob,
outputWidth, outputHeight,
outputType, encoderOptions)
{
if (!outputWidth || !isFinite(outputWidth) || outputWidth <= 0 ||
!outputHeight || !isFinite(outputHeight) || outputHeight <= 0) {
return Promise.reject(new TypeError('invalid output dimensions'));
}
outputWidth = Math.round(outputWidth);
outputHeight = Math.round(outputHeight);
return ImageUtils.getSizeAndType(inputImageBlob).then(
function resolve(data) {
var inputWidth = data.width;
var inputHeight = data.height;
// If the image already has the desired size, just return it unchanged
if (inputWidth === outputWidth && inputHeight === outputHeight) {
return inputImageBlob;
}
// Otherwise, return a promise for the resized image
return resize(data);
},
function reject(error) {
// If we couldn't determine the image size and type for some
// reason we are still going to assume that it is a valid image,
// just one we don't know about. If gecko can decode it we'll
// use it and won't reject unless gecko refuses it.
return resize({});
}
);
// Decode the image in an
element, resize and/or crop it into a
//