1

I'm a server-side dev learning the ropes of vanilla JS. I need to clear my concepts regarding sending an Ajax POST request for an image object I'm creating in JS - this question is about that.

Imagine a web app where users upload photos for others to see. At the point of each image's upload, I use vanilla JS to confirm the image's mime-type (via interpreting magic numbers), and then resize the image for optimization purposes.

After resizing, I do:

    var canvas = document.createElement('canvas');
    canvas.width = resized_width;
    canvas.height = resized_height;
    var ctx = canvas.getContext("2d");
    ctx.drawImage(source_img, 0, 0, resized_width, resized_height);
    var resized_img = new Image();
    resized_img.src = canvas.toDataURL("image/jpeg",0.7);
    return resized_img;

The image object returned has to be sent to the backend via an Ajax request. Something like:

function overwrite_default_submit(e) {
  e.preventDefault();
  var form = new FormData();
  form.append("myfile", resized_img, img_name);
  var xhr = new XMLHttpRequest();
  xhr.open('POST', e.target.action);
//  xhr.send(form); // uncomment to really send the request
}

However, the image object returned after resizing is essentially an HTML element like so <img src="data:image/jpeg;base64>. Whereas the object expected in the FormData object ought to be a File object, e.g. something like: File { name: "example.jpg", lastModified: 1500117303000, lastModifiedDate: Date 2017-07-15T11:15:03.000Z, webkitRelativePath: "", size: 115711, type: "image/jpeg" }.

So what do I do to fix this issue? Would prefer to learn the most efficient way of doing things here.

Btw, I've seen an example on SO of using the JS FILE object, but I'd prefer a more cross-browser method, given File garnered support from Safari, Opera Mobile and built-in Android browsers relatively recently.

Moreover, only want pure JS solutions since I'm using this as an exercise to learn the ropes. JQuery is on my radar, but for later.


The rest of my code is as follows (only included JPEG processing for brevity):

var max_img_width = 400;
var wranges = [max_img_width, Math.round(0.8*max_img_width), Math.round(0.6*max_img_width),Math.round(0.4*max_img_width),Math.round(0.2*max_img_width)];

// grab the file from the input and process it
function process_user_file(e) {
  var file = e.target.files[0];
  var reader = new FileReader();
  reader.onload = process_image;
  reader.readAsArrayBuffer(file.slice(0,25));
}

// checking file type programmatically (via magic numbers), getting dimensions and returning a compressed image
function process_image(e) {
  var img_width;
  var img_height;
  var view = new Uint8Array(e.target.result);
  var arr = view.subarray(0, 4);
  var header = "";
  for(var i = 0; i < arr.length; i++) {
     header += arr[i].toString(16);
  }
  switch (header) {
    case "ffd8ffe0":
    case "ffd8ffe1":
    case "ffd8ffe2":
    case "ffd8ffe3":
    case "ffd8ffe8":
        // magic numbers represent type = "image/jpeg";
        // use the 'slow' method to get the dimensions of the media
        img_file = browse_image_btn.files[0];
        var fr = new FileReader();
        fr.onload = function(){
          var dataURL = fr.result;
          var img = new Image();
          img.onload = function() {
              img_width = this.width;
              img_height = this.height;
              resized_img = resize_and_compress(this, img_width, img_height, 80);
            }
          img.src = dataURL;
        };
        fr.readAsDataURL(img_file);
        to_send = browse_image_btn.files[0];
        load_rest = true;
        subform.disabled = false;
        break;
    default:
        // type = "unknown"; // Or one can use the blob.type as fallback
        load_rest = false;
        subform.disabled = true;
        browse_image_btn.value = "";
        to_send = null;
        break;
    }
}

// resizing (& compressing) image
function resize_and_compress(source_img, img_width, img_height, quality){
    var new_width;
    switch (true) {
      case img_width < wranges[4]:
         new_width = wranges[4];
         break;
      case img_width < wranges[3]:
         new_width = wranges[4];
         break;
      case img_width < wranges[2]:
         new_width = wranges[3];
         break;
      case img_width < wranges[1]:
         new_width = wranges[2];
         break;
      case img_width < wranges[0]:
         new_width = wranges[1];
         break;
      default:
         new_width = wranges[0];
         break;
    }
    var wpercent = (new_width/img_width);
    var new_height = Math.round(img_height*wpercent);

    var canvas = document.createElement('canvas');
    canvas.width = new_width;
    canvas.height = new_height;
    var ctx = canvas.getContext("2d");
    ctx.drawImage(source_img, 0, 0, new_width, new_height);
    console.log(ctx);
    var resized_img = new Image();
    resized_img.src = canvas.toDataURL("image/jpeg",quality/100);
    return resized_img;
}

Update: I'm employing the following:

// converting image data uri to a blob object
function dataURItoBlob(dataURI,mime_type) {
  var byteString = atob(dataURI.split(',')[1]);
  var ab = new ArrayBuffer(byteString.length);
  var ia = new Uint8Array(ab);
  for (var i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); }
  return new Blob([ab], { type: mime_type });
}

Where the dataURI parameter is canvas.toDataURL(mime_type,quality/100)

Hassan Baig
  • 15,055
  • 27
  • 102
  • 205

2 Answers2

0

See to this SO post: How to get base64 encoded data from html image

I think you need to call 'canvas.toDataURL()' to get the actual base64 stream of the image.

var image = canvas.toDataURL();

Then upload it with a Form: Upload a base64 encoded image using FormData?

var data = new FormData();
data.append("image_data", image);

Untested, but this should be about it.

mvermand
  • 5,829
  • 7
  • 48
  • 74
  • Oh, I didn't know I get post up the base64 stream directly. I do have that data, let me give it a quick shot and I'll get back to you. – Hassan Baig Feb 01 '18 at 15:18
  • You could transform to a Blob: https://stackoverflow.com/questions/35845623/uploading-a-canvas-image-to-facebook-as-blob-of-multipart-form-data-type – mvermand Feb 01 '18 at 15:27
  • 1
    Something like `canvas.toBlob(canvas.toDataURL(),'image/jpeg', 0.7)`? – Hassan Baig Feb 01 '18 at 15:54
  • Update: for sure doesn't work with just `canvas.toDataURL()`, I get `TypeError: Argument 2 of FormData.append is not an object`. I'll try the blob approach now. – Hassan Baig Feb 01 '18 at 16:14
0

You should call the canvas.toBlob() to get the binary instead of using a base64 string.

it's async so you would have to add a callback to it.

canvas.toBlob(function(blob) {
  resized_img.onload = function() {
    // no longer need to read the blob so it's revoked
    URL.revokeObjectURL(this.url);
  };

  // Preview the image using createObjectURL
  resized_img.src = URL.createObjectURL(blob);

  // Attach the blob to the FormData
  var form = new FormData();
  form.append("myfile", blob, img_name);
}, "image/jpeg", 0.7);
Endless
  • 34,080
  • 13
  • 108
  • 131
  • Interesting. I hadn't read your answer, and instead have written somewhat different code - I've added that as an **update** at the end of my question. Could you have a look at that and qualitatively compare it with your approach? Which one is superior (and why)? Would love to know the pros and cons, if any. – Hassan Baig Feb 02 '18 at 09:02
  • base64 is ~3x larger in size. Strings in browser are utf16 not utf8 so it actually takes up 2x more in memory. not to mention that you need the blob and the base64 string at the same time. The conversion it has to do to and from base64 takes longer. the function `dataURItoBlob` use more code. – Endless Feb 02 '18 at 10:28
  • What about from a browser support angle? I've read that canvas.toBlob doesn't enjoy broad support across browsers (e.g. Chrome needs a polyfill, src: https://github.com/blueimp/JavaScript-Canvas-to-Blob) – Hassan Baig Feb 02 '18 at 10:47
  • Chrome don't need a polyfill, it's quite cross compitable in the newer browser. See here: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Browser_compatibility – Endless Feb 02 '18 at 13:08
  • Well, I think the issue is with *mobile* browser compability, apprently there's a polyfill required for that which falls back on using `toDataUrl`. I think the perfect solution would be something that utilizes both your suggestion and `toDataUrl` (in case of fallback). I think that should definitely be part of your otherwise excellent answer. Check out: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Browser_compatibility – Hassan Baig Feb 02 '18 at 23:10