Skip to content
Advertisement

Upload compressed image file from client-side using JavaScript

I am trying to compress images on client side using JavaScript on some low bandwidth devices and I’m currently stuck in a limbo using the HTML5 File API. I’m new to this, please bear with me if I’m missing something important.

I have some input tags which should ideally open the mobile camera, capture single image, compress and send files to the backend. Although this can be done with a single input field with multiple uploads enabled but I need the multiple image fields to segregate images based on some categories.

Here’s the input boxes:

<input type="file" name="file1" id="file1" capture="camera" accept="image/*">
<input type="file" name="file2" id="file2" capture="camera" accept="image/*">...

Here’s the image compression logic:

    // Takes upload element id ("file1") and a maxSize to resize, ideally on a change event
    window.resizePhotos = function(id, maxSize){
            var file = document.getElementById(id).files[0];

        // Ensuring it's an image
        if(file.type.match(/image.*/)) {
            // Loading the image
            var reader = new FileReader();
            reader.onload = function (readerEvent) {
                var image = new Image();
                image.onload = function (imageEvent) {

                    // Resizing the image and keeping its aspect ratio
                    var canvas = document.createElement("canvas"),
                        max_size = maxSize,
                        width = image.width,
                        height = image.height;
                    if (width > height) {
                        if (width > max_size) {
                            height *= max_size / width;
                            width = max_size;
                        }
                    } else {
                        if (height > max_size) {
                            width *= max_size / height;
                            height = max_size;
                        }
                    }
                    canvas.width = width;
                    canvas.height = height;
                    canvas.getContext("2d").drawImage(image, 0, 0, width, height);
                    var dataUrl = canvas.toDataURL("image/jpeg");
                    var resizedImage = dataURLToBlob(dataUrl);
                    $.event.trigger({
                        type: "imageResized",
                        blob: resizedImage,
                        url: dataUrl
                    });
                }
                image.src = readerEvent.target.result;
            }
            reader.readAsDataURL(file);
        }
    };

    // Function to convert a canvas to a BLOB
    var dataURLToBlob = function(dataURL) {
        var BASE64_MARKER = ';base64,';
        if (dataURL.indexOf(BASE64_MARKER) == -1) {
            var parts = dataURL.split(',');
            var contentType = parts[0].split(':')[1];
            var raw = parts[1];

            return new Blob([raw], {type: contentType});
        }

        var parts = dataURL.split(BASE64_MARKER);
        var contentType = parts[0].split(':')[1];
        var raw = window.atob(parts[1]);
        var rawLength = raw.length;

        var uInt8Array = new Uint8Array(rawLength);

        for (var i = 0; i < rawLength; ++i) {
            uInt8Array[i] = raw.charCodeAt(i);
        }

        return new Blob([uInt8Array], {type: contentType});
    }

    // Handling image resized events
    $(document).on("imageResized", function (event) {
        if (event.blob && event.url) {
            document.getElementById('file1').files[0] = event.url; // --> Tried this, did not work
            document.getElementById('file1').files[0].value = (URL || webkitURL).createObjectURL(event.blob); // --> Tried doing this looking at some other answers but did not work
            console.log(document.getElementById('file1').files[0]); // Original file is loading fine
            console.log(event.url); // Image compression is working correctly and producing the base64 data
        }
    });

    $(window).on("load", function() {
        // Resets the value to when navigating away from the page and choosing to upload the same file (extra feature)
        $("#file1").on("click touchstart" , function(){
            $(this).val("");
        });

        // Action triggers when user has selected any file 
        $("#file1").change(function(e) {
            resizePhotos("file1", 1024)
        });
    });

In PHP script, I’d usually try to catch files from the POST request like:

$file1 = $_FILES["file1"]["tmp_name"];
$file2 = $_FILES["file2"]["tmp_name"];
...

But this doesn’t work because it looks for the original user selected file at a tmp directory (e.g. the actual temporary file in my case is C:xampptmpphp25CB.tmp )

One thing I’ve tried is put the input fields outside of the form tags, enabled the click behaviour using a button and created new input field with the modified data within the form like:

var newinput = document.createElement("input");
newinput.type = 'file';
newinput.name = 'file1';
newinput.files[0] = event.url;
document.getElementById('parentdiv').appendChild(newinput);

Needless to say, this had no effect and the PHP script could not identify any file.

Please guide me and suggest any changes required in the JavaScript/PHP script so I can accept the modified file and not the original user uploaded file from the input field.

Advertisement

Answer

  1. You can only change a file input value with another list
    here is how: https://stackoverflow.com/a/52079109/1008999 (also in the example)
  2. Using the FileReader is a waste of time, CPU, Encoding & decoding and RAM…
    use URL.createObjectURL instead
  3. Don’t use canvas.toDataURL… use canvas.toBlob instead
  4. Canvas have bad compression, read earlier comment and see the jsfiddle proff
    If you insist on using canvas to try and squeeze the size down then
    1. First try to see if the image is in a reasonable size first
    2. Compare if the pre existing image file.size is smaller than what the canvas.toBlob provides and choose if you want the old or the new one instead.
    3. If resizing the image isn’t enough have a look at this solution that change the quality until a desired file size & image aspect have been meet.

Without any testing, this is how i would have refactor your code too:

/**
 * @params {File[]} files Array of files to add to the FileList
 * @return {FileList}
 */
function fileListItems (files) {
  var b = new ClipboardEvent('').clipboardData || new DataTransfer()
  for (var i = 0, len = files.length; i<len; i++) b.items.add(files[i])
  return b.files
}

// Takes upload element id ("file1") and a maxSize to resize, ideally on a change event
window.resizePhotos = async function resizePhotos (input, maxSize) {
  const file = input.files
  
  if (!file || !file.type.match(/image.*/)) return
  
  const image = new Image()
  const canvas = document.createElement('canvas')
  const max_size = maxSize
  image.src = URL.createObjectURL(file)
  await image.decode()
  let width = image.width
  let height = image.height
  
  // Resizing the image and keeping its aspect ratio
  if (width > height) {
    if (width > max_size) {
        height *= max_size / width
        width = max_size
    }
  } else {
      if (height > max_size) {
          width *= max_size / height
          height = max_size
      }
  }
  canvas.width = width
  canvas.height = height
  canvas.getContext('2d').drawImage(image, 0, 0, width, height)
  const resizedImage = await new Promise(rs => canvas.toBlob(rs, 'image/jpeg', 1))
  
  // PS: You might have to disable the event listener as this might case a change event
  // and triggering this function over and over in a loop otherwise

  input.files = fileListItems([
    new File([resizedImage], file.name, { type: resizedImage.type })
  ])
}


jQuery($ => {
  // Resets the value to when navigating away from the page and choosing to upload the same file (extra feature)
  $('#file1').on('click touchstart' , function(){
    $(this).val('')
  })

  // Action triggers when user has selected any file 
  $('#file1').change(function(e) {
    resizePhotos(this, 1024)
  })
})
User contributions licensed under: CC BY-SA
4 People found this is helpful
Advertisement