Sort by image resolution in gallery

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>

Leave a Comment