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
- You can only change a file input value with another list
here is how: https://stackoverflow.com/a/52079109/1008999 (also in the example) - Using the FileReader is a waste of time, CPU, Encoding & decoding and RAM…
use URL.createObjectURL instead - Don’t use canvas.toDataURL… use canvas.toBlob instead
- 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- First try to see if the image is in a reasonable size first
- 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. - 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) }) })