0

I'm a server-side dev who's new to front-end development. I've been tinkering around with vanilla Javascript and need to clarify some concepts.

The usecase I'm experimenting with is handling image upload (and mid-air JS-facilitated compression before said upload to server) via JS.

Currently I'm stuck on step one. Imagine the following simple set up:

<form method="POST" action="some_url">
<input type="file" id="browse_image" onchange="compressImage(event)">
<input type="submit" value="Submit">
</form>

My question is:

At what step do I try to pass the image to a JS function (given my goal is to compress it and send it to the server)? Would this happen at the time of image selection (i.e. pressing the browse button), or at the point of pressing Submit? Where do I put the event and how do I proceed from there? A quick illustrative answer with a an example would be great!


I've been trying to do it at the point of image selection (to no avail):

function compressImage(e){

     var reader = new FileReader();
     reader.readAsDataURL(e.target.files[0]);
     console.log(reader);
}

Would be great to get a conceptual walkthrough, alongwith a quick illustrative example. Vanilla JS only, I'm not going to touch JQuery before I get the hang of JS.

Hassan Baig
  • 15,055
  • 27
  • 102
  • 205
  • You have a slight typo in your `input type="submit"` (need a quote after `value="Submit`), but otherwise the code as given works just fine for me. What specific problem are you having? – Hamms Jan 30 '18 at 01:16
  • @Hamms: good catch. Well the `console.log` statement spews out `FileReader { readyState: 1, result: null, error: null, onloadstart: null, onprogress: null, onload: null, onabort: null, onerror: null, onloadend: null }`. This tells me nothing got captured. I'm in two (or three) minds regarding what to do next, thus the question. – Hassan Baig Jan 30 '18 at 01:18

1 Answers1

1

In my mind (but it's a bit subjective), you would do it in both places.

  1. User selects a File from your input
  2. You process the file through js
  3. If your processing failed (e.g the file was not an image / corrupted / who knows) you can let the user know directly.
  4. If the processing succeeded, when user clicks submit, you overwrite the default behavior of your form, and send a FormData containing your new File/Blob instead of the original one.

var toSend = null, // here we will store our processed File/Blob (the one to send)
browse_image = document.getElementById('browse_image');

// when user selects a new File
browse_image.onchange = process_user_file;
// when user decides to send it to server
document.querySelector('form').onsubmit = overwrite_default_submit;

// grab the file from the input and process it
function process_user_file(evt) {
  // since we attached the event listener through elem.onevent,
  // 'this' refers to the input
  var file = this.files[0];
  // here do your compression, for demo, we'll just check it's a png file
  var reader = new FileReader();
  // a FileReader is async, so we pass the actual checking script as the onload handler
  reader.onload = checkMagicNumber;
  reader.readAsArrayBuffer(file.slice(0,4));
}

// we don't want the real form to be submitted, but our processed File/Blob
function overwrite_default_submit(evt) {
  // block the form's submit default behavior
  evt.preventDefault();
  // create a new form result from scratch
  var form = new FormData();
  // add our File/Blob
  form.append("myfile", toSend, browse_image.files[0].name);
  // create a new AJAX request that will do the same as original form's behavior
  var xhr = new XMLHttpRequest();
  xhr.open('POST', evt.target.action);
//  xhr.send(form); // uncomment to really send the request
  console.log('sent', toSend);
}

// simply checks if it's really a png file
// for you, it will be your own compression code,
//  which implementation can not be discussed in this answer
function checkMagicNumber(evt) {
  var PNG = '89504e47';
  var arr = new Uint8Array(evt.target.result);
  var header = "";
  for(var i = 0; i < arr.length; i++) {
     header += arr[i].toString(16);
  }
  // some user friendly actions
  if(header !== PNG) {
    alert('not a png file'); // let the user know it didn't work
    browse_image.value = ""; // remove invalid File
    sub.disabled = true; // avoid the form's submission
    toSend = null; // nothing to send
  }
  else {
    toSend = browse_image.files[0]; // for demo we don't actually modify it...
    sub.disabled = false; // allow form's submission
  }
}
<form method="POST" action="some_url">
  <label>Please select a .png file</label>
  <input type="file" id="browse_image" name="myfile">
  <input type="submit" value="Submit" disabled id="sub">
</form>

Ps: note that even your original code wouldn't have sent anything, since no input in your form had a name attribute.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • 1
    @HassanBaig well it's up to you to decide when you do it, but this allows to e.g tell before hand if what you got is correct, and IMM for better UX, it's better to let know the User as soon as possible, and hence, to do the processing as soon as possible. But some may disagree, since the problem of this approach is that your user may well change its mind between the first time they selected a File, and the time they click submit, and in this case, you would have uselessly processed some files. So really, it's a matter of opinions. – Kaiido Jan 30 '18 at 03:03
  • 1
    A even better way would be to make default checks at user selection, and real processing only when user decides to send it. The only really required action being to overwrite the default behavior of your form. – Kaiido Jan 30 '18 at 03:05
  • 1
    @HassanBaig I added some comments to the code, hoping this will help you better understand what happens there. For the `this.files` issue, are you sure you added the event handler like I did? To get the image's `naturalWidth` and `naturalHeight`, you would have to either grab it from the file data yourself (the hard but fast way), or to use a `` element (the easy but slow way). But this is far beyond the scope of this question, and [has already been answered](https://stackoverflow.com/questions/42540903/sort-by-image-resolution-in-gallery/42649964#42649964). – Kaiido Jan 30 '18 at 04:29
  • 1
    @HassanBaig it is slower because with the `` approach, not only will the browser parse the image's metadata, but it will also unpack its content, apply necessary ColorProfiles if any, and even actually render the image in some browsers (e.g chrome), and also store an uncompressed version in memory so that it can render faster next time. While when we parse it ourselves, we only request the file, and then, in best case, we just need to read 20 bytes of the data. Nothing in memory, very little I/O impact. – Kaiido Jan 30 '18 at 06:35
  • trying to wrap my head around the PNG mime-type detection in your answer. Wanted a clarification regarding that. You create an array like so: `var arr = new Uint8Array(e.target.result);` in `function checkMagicNumber()`. However, I've added a separately excerpt at the end of my question, where it's `var arr = (new Uint8Array(e.target.result)).subarray(0, 4);`. Have a look. Now both of you guys end up comparing to the same magic number for PNG detection. Then howcome the initial array is formed differently? Being new to JS, I guess I'm going through the inevitable learning curve. – Hassan Baig Jan 30 '18 at 22:41
  • @HassanBaig unfortunately this has nothing to do with the current question, so I would prefer you to rollback your question to what it was originally. The differrence is that his code reads uselessly the whole file and then grab only the first 4 bytes, while mine reads only the first 4 bytes directly, thanks to Blob.slice method. – Kaiido Jan 30 '18 at 23:01
  • Yep, I only tacked it on to show it to you. Now doesn't `reader.onload = checkMagicNumber;` execute *before* `reader.readAsArrayBuffer(file.slice(0,4))` in your code? In other words, while inside `checkMagicNumber`, the blob hasn't been sliced yet? **OR**, I'm wrong because of the way asynch calls work? I guess that's what I really need to wrap my head around. – Hassan Baig Jan 30 '18 at 23:05
  • 1
    CheckMagicNumber is an event handler, and will be executed only when readAsArrayBuffer async code finishes. What readAsArrayBuffer reads in my version is a 4bytes slice of the file, while in the other version, it reads the whole file. – Kaiido Jan 30 '18 at 23:25
  • I'm seeing an anomaly whereby reducing the dimensions of a `.jpg` (in pure JS) is causing its file size to inflate. Given how well you understand these concepts a a conceptual level, it'll be great if you can have a quick look: https://stackoverflow.com/q/48632459/4936905 It's fine otherwise too. – Hassan Baig Feb 05 '18 at 23:08