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)
})
})