Theoretically, there would be way to leverage a bit the browser’s processing, by extracting these values from arrayBuffer representations of the uploaded files.
Most image formats have readable metadata containing the dimensions of the media, so we can access it without asking the browser to actually parse and compute the image’s data (uncompress, decode etc.).
Here is a really rough proof of concept, using ExifReader lib that I don’t have tested too much, for jpeg images.
/*
Rough proof of concept of getting image files width & height
by reading their metadata directly in arrayBuffer, instead of loading it
Should support most jpeg png gif and bmp image files
(though all versions of these formats have NOT been tested)
@input A fileList.
@output A promise
whose fulfillment handler receives an Array containing successfully parsed files.
*/
function getImageSizes(files) {
/* Attaches a buffer of the size specified to the File object */
function getBuffer(fileList, size) {
return new Promise((resolve, reject) => {
const fr = new FileReader();
const toLoad = fileList.length;
if (!toLoad) { // an empty list
resolve(fileList);
return;
}
let arr = [];
let loaded = 0;
let current = fileList[loaded];
let chunk = current.slice(0, size || current.size); // get only the required bytes
fr.onload = e => {
fileList[loaded].buf = fr.result;
if (++loaded < toLoad) {
current = fileList[loaded];
chunk = current.slice(0, size || current.size);
fr.readAsArrayBuffer(chunk);
} else { // once all the list has been treated
resolve(fileList);
}
};
fr.readAsArrayBuffer(chunk);
});
}
/* png is easy, IHDR starts at 16b, and 8 first bytes are 32bit width & height */
// You can read https://www.w3.org/TR/PNG-Chunks.html for more info on each numeric value
function getPNGSizes(pngArray) {
let view;
// Little endian only
function readInt16(offset) {
return view[offset] << 24 |
view[offset + 1] << 16 |
view[offset + 2] << 8 |
view[offset + 3];
}
pngArray.forEach(o => {
view = new Uint8Array(o.buf);
o.meta = {
width: readInt16(16),
height: readInt16(20),
bitDepth: view[24],
colorType: view[25],
compressionMethod: view[26],
filterMethod: view[27],
interlaceMethod: view[28]
};
o.width = o.meta.width;
o.height = o.meta.height;
});
return pngArray;
}
function getJPEGSizes(jpegArray) {
/* the EXIF library seems to have some difficulties */
let failed = [];
let retry = [];
let success = [];
// EXIF data can be anywhere in the file, so we need to get the full arrayBuffer
return getBuffer(jpegArray).then(jpegArray => {
jpegArray.forEach(o => {
try {
const tags = ExifReader.load(o.buf);
if (!tags || !tags.PixelXDimension) {
throw 'no EXIF';
}
o.meta = tags; // since OP said he wanted it
o.width = tags.PixelXDimension.value;
o.height = tags.PixelYDimension.value;
success.push(o);
} catch (e) {
failed.push(o);
return;
}
});
// if some have failed, we will retry with the ol'good img way
retry = failed.map((o) => {
return new Promise((resolve, reject) => {
let img = new Image();
img.onload = e => {
URL.revokeObjectURL(img.src);
o.width = img.width;
o.height = img.height;
resolve(o);
};
img.onerror = e => {
URL.revokeObjectURL(img.src);
reject(o);
};
img.src = URL.createObjectURL(o);
});
});
return Promise.all(retry)
// concatenate the no-exif ones with the exif ones.
.then(arr => success.concat(arr))
});
}
function getGIFSizes(gifArray) {
gifArray.forEach(o => {
let view = new Uint8Array(o.buf);
o.width = view[6] | view[7] << 8;
o.height = view[8] | view[9] << 8;
});
return gifArray;
}
function getBMPSizes(bmpArray) {
let view;
function readInt(offset) {
// I probably have something wrong in here...
return Math.abs(view[offset] |
view[offset + 1] << 8 |
view[offset + 2] << 16 |
view[offset + 3] << 24
);
}
bmpArray.forEach(o => {
view = new Uint8Array(o.buf);
o.meta = {
width: readInt(18),
height: readInt(22)
}
o.width = o.meta.width;
o.height = o.meta.height;
});
return bmpArray;
}
// only based on MIME-type string, to avoid all non-images
function simpleImageFilter(files) {
return Promise.resolve(
Array.prototype.filter.call(files, f => f.type.indexOf('image/') === 0)
);
}
function filterType(list, requestedType) {
// A more robust MIME-type check
// see http://stackoverflow.com/questions/18299806/how-to-check-file-mime-type-with-javascript-before-upload
function getHeader(buf) {
let type="unknown";
let header = Array.prototype.map.call(
new Uint8Array(buf.slice(0, 4)),
v => v.toString(16)
).join('')
switch (header) {
case "89504e47":
case "0D0A1A0A":
type = "image/png";
break;
case "47494638":
type = "image/gif";
break;
case "ffd8ffe0":
case "ffd8ffe1":
case "ffd8ffe2":
type = "image/jpeg";
break;
default:
switch (header.substr(0, 4)) {
case "424d":
type="image/bmp";
break;
}
break;
}
return type;
}
return Array.prototype.filter.call(
list,
o => getHeader(o.buf) === requestedType
);
}
function getSizes(fileArray) {
return getJPEGSizes(filterType(fileArray, 'image/jpeg'))
.then(jpegs => {
let pngs = getPNGSizes(filterType(fileArray, 'image/png'));
let gifs = getGIFSizes(filterType(fileArray, 'image/gif'));
let bmps = getBMPSizes(filterType(fileArray, 'image/bmp'));
return gifs.concat(pngs.concat(bmps.concat(jpegs)));
});
}
return simpleImageFilter(files)
.then(images => getBuffer(images, 30))
.then(getSizes);
}
// our callback
function sort(arr) {
arr.sort(function(a, b) {
return a.width * a.height - b.width * b.height;
});
output.innerHTML = '';
arr.forEach(f => {
// ugly table generation
let t="<td>",
tt="</td>" + t,
ttt="</td></tr>";
output.innerHTML += '<tr>' + t + f.name + tt + f.width + tt + f.height + ttt;
})
}
f.onchange = e => {
getImageSizes(f.files)
.then(sort)
.catch(e => console.log(e));
output.innerHTML = '<tr><td colspan="3">Processing, please wait...</td></tr>';
}
table {
margin-top: 12px;
border-collapse: collapse;
}
td,
th {
border: 1px solid #000;
padding: 2px 6px;
}
tr {
border: 0;
margin: 0;
}
<script src="https://rawgit.com/mattiasw/ExifReader/master/dist/exif-reader.js"></script>
<input type="file" id="f" webkitdirectory accepts="image/*">
<table>
<thead>
<tr>
<th>file name</th>
<th>width</th>
<th>height</th>
</tr>
</thead>
<tbody id="output">
<tr>
<td colspan="3">Please choose a folder to upload</td>
</tr>
</tbody>
</table>