I'm trying to valdiate a multiple image upload using jQuery. The validation proccess get the images dimensions and check if it is greater than 338x450. But I know very little about javascript. Can someone help me? Here's the code I'm trying:
HTML:
<input class="form-control" id="ads-photos" name="ads-photos[]" type="file" accept="image/jpeg, image/jpg" multiple />
javascript
var isFilesOk;
$('#ads-photos').change(function (evt) {
validateDimensions();
});
function validateDimensions() {
var fi = document.getElementById('ads-photos');
if (fi.files.length > 0) {
for (var i = 0; i <= fi.files.length - 1; i++) {
var fileName, fileExtension, fileSize, fileType, dateModified;
fileName = fi.files.item(i).name;
fileExtension = fileName.replace(/^.*\./, '');
if (fileExtension == 'jpg' || fileExtension == 'jpeg') {
if (readImageFile(fi.files.item(i))) {
alert(isFilesOk);
} else {
alert(isFilesOk);
}
}
}
}
// GET THE IMAGE WIDTH AND HEIGHT USING fileReader() API.
function readImageFile(file) {
var reader = new FileReader(); // CREATE AN NEW INSTANCE.
reader.onload = function (e) {
var img = new Image();
img.src = e.target.result;
img.onload = function () {
var w = this.width;
var h = this.height;
if (this.width >= 338 && this.height >= 450) {
isFilesOk = true;
} else {
isFilesOk = false;
}
}
};
reader.readAsDataURL(file);
return isFilesOk;
}
}
A example link
The above code needs analyze all the images sended by the user to know if they all have minimum required dimensions. But I'm not getting it to work.
The issue here is that the readImageFile method is asynchronous. The work it's doing happens later, after it's executed when the onload event is triggered on the file.
The best solution will depend a little on the environment you're targeting (eg: whether you need to support old IE, etc), but if you're able to use modern JS with Promises, this will be easier.
Basically, for each file that's selected, you want to get a Promise indicating whether the file is okay or not. A good way to do this is map the array to an array of Promises and use Promise.all to get a callback when they're all done.
Here's the method to get that for a single file:
function fileIsValid(file) {
return new Promise((resolve) => {
var reader = new FileReader(); // CREATE AN NEW INSTANCE.
reader.onload = function (e) {
var img = new Image();
img.src = e.target.result;
img.onload = function () {
var w = this.width;
var h = this.height;
const isValid = w >= 338 && h >= 450;
resolve(isValid);
};
};
reader.readAsDataURL(file);
});
}
And then using this method:
const fi = document.getElementById('ads-photos');
const promises = [];
for (let i = 0; i <= fi.files.length - 1; i++) {
const file = fi.files.item(i);
if (/\.jpe?g$/.test(file.name)) {
promises.push(fileIsValid(file));
}
}
Promise.all(promises).then((results) => {
// results is an array of booleans
const allValid = results.every(Boolean);
if (allValid) {
alert('Yeah!');
} else {
alert('Nope');
}
});
There's a few things to note here:
you could make it shortcut once a single error is found by making the promise reject if it's invalid, and Promise.all then won't wait for any other pending promises. This is a bit of a style question about whether failed validation should result in a rejected promise. Up to you on that. In this case, it's reading from a local file system and I presume they'll be selecting a relatively small number of files, so short-circuiting probably won't make any noticeable difference.
it doesn't handle actual errors (eg: what if a file is unable to be loaded?).
Related
Actually I have a form where with image input. I want to validate image for three condition like
extension should be png, jpg
size should be less than 2048kb
less than 200px x 200px is consider as dimension
I wrote an function and solve 1 and 2 issue. To solve issue 3 , I use image reader inside onload listener and when I clicked it can not prevent submit also if I remove 3, then it works fine ! Is there any solution in JS that solve above issue?
Here is a slide of my code in below.
function isImageValid(idName) {
var fileUpload = document.getElementById(idName);
var fileName = document.getElementById(idName).value;
if (typeof (fileUpload.files) != "undefined") {
for (var i=0; i<fileUpload.files.length;i++)
{
// console.log(fileUpload.files[i]);
var valid_dimension = 0;
var reader = new FileReader();
//Read the contents of Image File.
reader.readAsDataURL(fileUpload.files[0]);
reader.onload = function (e) {
//Initiate the JavaScript Image object.
var image = new Image();
//Set the Base64 string return from FileReader as source.
image.src = e.target.result;
//Validate the File Height and Width.
image.onload = function () {
var height = this.height;
var width = this.width;
if (height>200||width>200) {
valid_dimension =1;
// alert("Height and Width must not exceed 200px.");
return false;
}
// alert("Uploaded image has valid Height and Width.");
return true;
};
};
var size = parseFloat(fileUpload.files[0].size / 1024).toFixed(2);
var extension = fileName.split('.').pop();
if( valid_dimension ==1||size>2048||(extension!='jpg'&&extension!='JPG'&&extension!='JPEG'&&extension!='jpeg'&&extension!='png'&&extension!='PNG'))
return false;
else
return true;
}
} else {
return false;
}
}
And,
const form = document.getElementById('employee_form');
form.addEventListener('submit', (e)=>{
var is_avatar_img_valid = isImageValid('avatar');
if(is_avatar_img_valid==false){
e.preventDefault();
document.getElementById("avatar").style.borderColor = "red";
document.getElementById('avatar_validator_message').innerHTML = 'Invalid image';
}
else{
document.getElementById("avatar").style.borderColor = "black";
document.getElementById('avatar_validator_message').innerHTML = '';
}
}
The problem is reader.onload and image.onload functions are async in nature. So your form submits before these onload methods execute.
To solve this you need to follow the below steps
Prevent default in submit event handler
Pass callbacks for valid and invalid image to the isImageValid function
Manually submit the form if image is valid
Below is the code. Please mark the answer as accepted, if it helps
function isImageValid(idName, onValidCallback, onInvalidCallback) {
var fileUpload = document.getElementById(idName);
var fileName = document.getElementById(idName).value;
if (typeof (fileUpload.files) != "undefined") {
for (var i = 0; i < fileUpload.files.length; i++) {
// console.log(fileUpload.files[i]);
//--------------------
const allowedExtension = ['jpg', 'JPG', 'JPEG', 'jpeg', 'png', 'PNG'];
const maxAllowedSize = 2048;
const maxAllowedHeight = 200;
const maxAllowedWidth = 200;
const size = parseFloat(fileUpload.files[i].size / 1024).toFixed(2);
const extension = fileName.split('.').pop();
console.log({ size, extension });
//Check for valid extension and size limit
if (allowedExtension.some(ext => ext === extension) && size <= maxAllowedSize) {
//Extension and size are valid
// Now check for valid dimensions
const reader = new FileReader();
reader.readAsDataURL(fileUpload.files[i]);
reader.onload = function (e) {
//Initiate the JavaScript Image object.
var image = new Image();
//Set the Base64 string return from FileReader as source.
image.src = e.target.result;
//Validate the File Height and Width.
image.onload = function () {
const height = this.height;
const width = this.width;
console.log({ height, width });
if (height > maxAllowedHeight || width > maxAllowedWidth) {
// alert("Height and Width must not exceed 200px.");
//File does not meet the dimensions guidline
if (onInvalidCallback)
onInvalidCallback();
return false;
}
// alert("Uploaded image has valid Height and Width.");
//Everything is fine, form canbe submitted now
if (onValidCallback)
onValidCallback();
};
};
}
break;
}
}
else {
// There are no files selected
if (onInvalidCallback)
onInvalidCallback();
}
}
const form = document.getElementById('employee_form');
form.addEventListener('submit', (e) => {
e.preventDefault();
isImageValid('avatar', () => {
alert('going to submit');
document.getElementById("avatar").style.borderColor = "black";
document.getElementById('avatar_validator_message').innerHTML = '';
//Manually submit the form
form.submit();
},
() => {
alert('stop submit');
document.getElementById("avatar").style.borderColor = "red";
document.getElementById('avatar_validator_message').innerHTML = 'Invalid image';
}
);
return false;
});
Inside the form eventlister you need to use
e.preventDefault()
The full code looks like this
const form = document.getElementById('employee_form');
form.addEventListener('submit', (e) => {
e.preventDefault()
var is_avatar_img_valid = isImageValid('avatar');
if (is_avatar_img_valid == false) {
e.preventDefault();
document.getElementById("avatar").style.borderColor = "red";
document.getElementById('avatar_validator_message').innerHTML = 'Invalid image';
}
else {
document.getElementById("avatar").style.borderColor = "black";
document.getElementById('avatar_validator_message').innerHTML = '';
}
});
I have vue component like this :
<template>
<section>
...
</section>
</template>
<script>
export default {
...
data() {
return {
allowableTypes: ['jpg', 'jpeg', 'png'],
maximumSize: 4000000
}
},
methods: {
...
onFileChange(e) {
if (this.validate(e.target.files[0])) {
let files = e.target.files,
reader = new FileReader()
// if any values
if (files.length){
reader.onload = (e) => {
this.image = e.target.result
}
reader.readAsDataURL(files[0])
}
}
},
validate(image) {
// validation file type
if (!this.allowableTypes.includes(image.name.split(".").pop().toLowerCase())) {
return false
}
// validation file size
if (image.size > this.maximumSize) {
return false
}
// validation image resolution
let img = new Image()
img.src = window.URL.createObjectURL(image)
let self = this
img.onload = function() {
let width = img.naturalWidth,
height = img.naturalHeight
window.URL.revokeObjectURL(img.src)
if(width != 850 && height != 350) {
return false
}
}
return true
}
}
}
</script>
If user upload image, it will call onFileChange method. Before displaying the image, it will call method validate to validate the image.
I try to validate file size and file type and it works. The problem here is validating the resolution.
From my code, it seems my code is true
But when I try like this:
I upload image with width = 100, height = 100, from the code, should it return `false``.
But when I run my code, it returns true.
Seems return is not working in the img.onload
How can I solve this problem?
A nice way to handle asynchronous validation is by using Promises :
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
If you are targeting Internet Explorer, make sure to use a polyfill such as this one :
https://github.com/stefanpenner/es6-promise
Your code would then look like this :
onFileChange(e) {
let self = this
this.validate(e.target.files[0])
.then(function() {
let files = e.target.files,
reader = new FileReader()
// if any values
if (files.length) {
reader.onload = (e) => {
self.image = e.target.result
}
reader.readAsDataURL(files[0])
}
})
.catch(function() {
// do something in the case where the image is not valid
})
},
validate(image) {
let self = this
return new Promise(function(resolve, reject) {
// validation file type
if (!self.allowableTypes.includes(image.name.split(".").pop().toLowerCase())) {
reject()
}
// validation file size
if (image.size > self.maximumSize) {
reject()
}
// validation image resolution
let img = new Image()
img.src = window.URL.createObjectURL(image)
img.onload = function() {
let width = img.naturalWidth,
height = img.naturalHeight
window.URL.revokeObjectURL(img.src)
if (width != 850 && height != 350) {
reject()
} else {
resolve()
}
}
})
}
If you do not want or cannot use Promises you could use a Callback to achieve the same behaviour :
onFileChange(e) {
let self = this
let validCallback = function() {
let files = e.target.files,
reader = new FileReader()
// if any values
if (files.length) {
reader.onload = (e) => {
self.image = e.target.result
}
reader.readAsDataURL(files[0])
}
}
let unvalidCallback = function() {
// do something in the case where the image is not valid
}
this.validate(e.target.files[0], validCallback, unvalidCallback)
},
validate(image, validCallback, unvalidCallback) {
// validation file type
if (!this.allowableTypes.includes(image.name.split(".").pop().toLowerCase())) {
unvalidCallback()
return
}
// validation file size
if (image.size > this.maximumSize) {
unvalidCallback()
return
}
// validation image resolution
let img = new Image()
img.src = window.URL.createObjectURL(image)
let self = this
img.onload = function() {
let width = img.naturalWidth,
height = img.naturalHeight
window.URL.revokeObjectURL(img.src)
if (width != 850 && height != 350) {
unvalidCallback()
return
} else {
validCallback()
}
}
}
It's onloadend not onload.
Change your code to this:
let self = this;
var reader = new FileReader();
reader.onloadend = () => {
// you logic here (use self, not this)
}
I am attempting to access the dimensions from my uploaded image before I post it to the post upload endpoint. If you look, there is a variable issueArrived that is supposed to determine if the image is too small and if so negate the upload process. Unfortunately, the image doesn't access the dimensions check until after the upload to the server commences. Any help would be appreciated.
// internal function that creates an input element
var file = newElement("input", 0, "");
// sets the input element as type file
file.type = "file";
file.multiple = "multiple";
file.name = "photoupload[]";
file.accept = "image/*";
var issueArrived = false;
file.onchange = function(e) {
// internal function that creates an image element
var img = newElement("img", 0, "");
img.onload = function(){
var cw = img.width;
var ch = img.height;
if(cw < 500 || ch < 500) {
alert("Photo must be atleast 500x500");
issueArrived = true;
return false;
}
}
img.src = URL.createObjectURL(this);
if (issueArrived) return;
var formdata = new FormData();
formdata.append("photoupload[]", this);
var x = new XMLHttpRequest();
x.open("POST", "/media/uploadphotos", true);
x.send(formdata);
}
file.click();
onchange method is async. You can't expect to set the value from issueArrived inside it an access it outside.
You can rewrite this way:
file.onchange=function(e){
// internal function that creates an image element
var img=newElement("img",0,"");
img.onload=function(){
var cw=img.width;
var ch=img.height;
if(cw<500||ch<500){
alert("Photo must be atleast 500x500");
} else {
var formdata=new FormData();
formdata.append("photoupload[]",this);
var x=new XMLHttpRequest();
x.open("POST","/media/uploadphotos",true);
x.send(formdata);
}
}
img.src=URL.createObjectURL(this);
}
file.click();
or, better, you can create a function that gets the file and returns a promise.
function checkSize(fileToCheck) {
return new Promise( function(resolve, reject) {
var img = newElement("img",0,"");
img.onload=function(){
var cw=img.width;
var ch=img.height;
cw<500||ch<500 ? reject() : resolve()
}
img.src=URL.createObjectURL(fileToCheck);
})
}
...
file.onchange=function(e){
checkSize(this)
.then( function() { /* success handler */ } )
.catch( function() { /* error handler */ } )
}
The following are the variables i have declared in javascript script tag.
The Variables are declared globally and initialized in the inner function which is inside another function (outer).
The problem is; when i try to check the variables using console.log i get "undefined". I don't know why.
I have read the variable hoisting, scope etc of Javascript variables. But i am unable to solve it. Please help.
var width;
var height;
var ratio;
var nwidth;
var nheight;
var wbool;
var imgsrc;
var canvas;
$("#myimage").change(function(){
readURL(this);
});
function readURL(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
var image = new Image();
image.src = e.target.result;
image.onload = function(){
if(this.width > this.height){
ratio = this.width/this.height;
nheight = parseInt(Math.ceil(1024/ratio));
wbool = true;
}
else{
ratio = this.height/this.width;
window.nwidth = parseInt(Math.ceil(768*ratio));
wbool = false;
}
}
console.log(window.nwidth + "" + wbool);
You're console.log()'ing nwidth before it has been assigned.
image.onload is an asynchronous operation (think like a callback). the console.log() is firing before the variable data has been assigned.
Try moving the log into the onload function.
var width;
var height;
var ratio;
var nwidth;
var nheight;
var wbool;
var imgsrc;
var canvas;
$("#myimage").change(function(){
readURL(this);
});
function readURL(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
var image = new Image();
image.src = e.target.result;
image.onload = function(){
if(this.width > this.height){
ratio = this.width/this.height;
nheight = parseInt(Math.ceil(1024/ratio));
wbool = true;
}
else{
ratio = this.height/this.width;
window.nwidth = parseInt(Math.ceil(768*ratio));
wbool = false;
// it will be defined here, because you are doing
// it inside the async operation.
console.log(window.nwidth);
}
}
// this will be undefined, because the var has not been set
// by the time this line executes.
console.log(window.nwidth + "" + wbool);
why does this happen?
async operations, like ajax calls ( or file loading in your code), take time to complete. Since javascript runs on a single thread, if the app were to stop and wait for the operation to complete, then the browser would lock up. Not good!
So, async operations are skipped over, and the callback portion is executed once the operation completes. This means your code is executing like this:
start loading the file
console.log() // undefined
loading complete, and the variable has been defined.
To trigger a custom event, you could do something like this:
$("#myimage").change(function(){
readURL(this);
});
// SET UP LISTENER FOR YOUR CUSTOM EVENT
$(document).on('fileWasLoaded', function () {
console.log(window.nwidth + "" + wbool);
});
function readURL(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
var image = new Image();
image.src = e.target.result;
image.onload = function(){
if(this.width > this.height){
ratio = this.width/this.height;
nheight = parseInt(Math.ceil(1024/ratio));
wbool = true;
}
else{
ratio = this.height/this.width;
window.nwidth = parseInt(Math.ceil(768*ratio));
wbool = false;
// TRIGGER YOUR CUSTOM EVENT WHEN THE IMAGE IS LOADED
$(document).trigger('fileWasLoaded');
}
}
In this page http://www.html5rocks.com/en/tutorials/file/dndfiles/ if you scroll down to example "Example: Slicing a file. Try it!" you will see uses of readAsBinaryString API to read bytes of local files.
I've seen IE (My case its IE11) doesn't support readAsBinaryString.
Even this code mentioned in post HTML5 File API read as text and binary breaks at readAsBinaryString in IE11.
I have seen some post in stack overflow, it suggests use of ReadAsArrayBuffer(). But it is also not working. It returns undefined.
My question is what are the options if I have to run it on IE11? Is it possible to write another IE compatible JS function which will do the JOB of readAsBinaryString().
I combine #Jack answer with my comment to show a complete working example.
In the <head> section I added this script to add FileReader.readAsBinaryString function in IE11
if (FileReader.prototype.readAsBinaryString === undefined) {
FileReader.prototype.readAsBinaryString = function (fileData) {
var binary = "";
var pt = this;
var reader = new FileReader();
reader.onload = function (e) {
var bytes = new Uint8Array(reader.result);
var length = bytes.byteLength;
for (var i = 0; i < length; i++) {
binary += String.fromCharCode(bytes[i]);
}
//pt.result - readonly so assign content to another property
pt.content = binary;
pt.onload(); // thanks to #Denis comment
}
reader.readAsArrayBuffer(fileData);
}
}
Then I needed to slightly modify my original script code because target.result has no value when using this fallback function.
var reader = new FileReader();
reader.onload = function (e) {
// ADDED CODE
if (!e) {
var data = reader.content;
}
else {
var data = e.target.result;
}
// business code
};
reader.readAsBinaryString(myFile);
This is my solution.
var reader = new FileReader();
reader.readAsBinaryString(fileData);
reader.onload = function(e) {
if (reader.result) reader.content = reader.result;
var base64Data = btoa(reader.content);
//...
}
//extend FileReader
if (!FileReader.prototype.readAsBinaryString) {
FileReader.prototype.readAsBinaryString = function (fileData) {
var binary = "";
var pt = this;
var reader = new FileReader();
reader.onload = function (e) {
var bytes = new Uint8Array(reader.result);
var length = bytes.byteLength;
for (var i = 0; i < length; i++) {
binary += String.fromCharCode(bytes[i]);
}
//pt.result - readonly so assign binary
pt.content = binary;
$(pt).trigger('onload');
}
reader.readAsArrayBuffer(fileData);
}
}
FileReader.readAsBinaryString is a non-standard function and has been deprecated.
FileReader.readAsArrayBuffer should be used instead.
MDN
For IE 11 you can use this XHR trick:
function blobToBinaryStringIE11(blob) {
var blobURL = URL.createObjectURL(blob);
var xhr = new XMLHttpRequest;
xhr.open("get", blobURL);
xhr.overrideMimeType("text/plain; charset=x-user-defined");
xhr.onload = function () {
var binary = xhr.response;
// do stuff
};
xhr.send();
}
It's 20x faster than the Uint8Array + fromCharCode route and as fast as readAsBinaryString.
Replace
reader.readAsBinaryString(blob);
with:
reader.readAsText(blob);
it's works well in cross browser.
I had some problems with the answers here and ended up making a few slight changes.
Instead of assigning to pt.content, my solution is to send a custom object to the prototype's onload, that receiver can specifically look for, I named this property msieContent so it will be very specific.
Also I used other accepted answer for converting Uint8Array to string in more robust way, you can see full details of it here:
https://stackoverflow.com/a/12713326/213050
Polyfill
if (FileReader.prototype.readAsBinaryString === undefined) {
// https://stackoverflow.com/a/12713326/213050
function Uint8ToString(u8a: Uint8Array) {
const CHUNK_SZ = 0x8000;
let c = [];
for (let i = 0; i < u8a.length; i += CHUNK_SZ) {
c.push(String.fromCharCode.apply(null, u8a.subarray(i, i + CHUNK_SZ)));
}
return c.join('');
}
FileReader.prototype.readAsBinaryString = function (fileData) {
const reader = new FileReader();
reader.onload = () => this.onload({
msieContent: Uint8ToString(new Uint8Array(<any>reader.result))
});
reader.readAsArrayBuffer(fileData);
}
}
Usage
private _handleTextFile(file: File) {
const reader = new FileReader();
reader.onload = (e) => {
// support for msie, see polyfills.ts
const readResult: string = (<any>e).msieContent || <string>e.target.result;
};
reader.readAsBinaryString(file);
}