I nearly have the theory down but am getting stuck wit the fine details. An image sizing function, given a var $data (which is the event object of a file input after changing) will return a var dataurl back (which is a dataurl jpeg).
My issue was getting dataurl to recognise it was updated inside a nested function. The code, as I show it below, was logging data:image/jpg;base64 to the console, showing dataurl was not updated.
Research led me to find that this is due to the asynchronous calls and this makes sense. The var wasn't changed in time before moving on and printing to the console. I've also learned that this is a standard situation to issue a callback to update the var.
Having made a few attempts at doing so, I can't find a clean (or working) method to update the var using a callback. I'll expand on this after I show my current code:
function clProcessImages($data) {
var filesToUpload = $data.target.files;
var file = filesToUpload[0];
var canvas = document.createElement('canvas');
var dataurl = 'data:image/jpg;base64';
var img = document.createElement("img");
var reader = new FileReader();
reader.onload = function(e, callback) {
img.onload = function () {
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
var MAX_WIDTH = 800;
var MAX_HEIGHT = 600;
var width = img.width;
var height = img.height;
if (width > height) {
if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width;
width = MAX_WIDTH;
}
} else {
if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height;
height = MAX_HEIGHT;
}
}
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
dataurl = canvas.toDataURL("image/jpeg",0.8);
callback(dataurl);
}
img.src = e.target.result
}
reader.readAsDataURL(file);
console.log(dataurl); // console log mentioned in question
return dataurl;
};
This is the code as it stands, somewhere in the middle of figuring out how to pull off the callback. I'm aware that in its shown state the callback isn't called.
My first hurdle was that I (seemingly) couldn't later call the reader.onload function again, as it appears to be an event specifically to be fired when the object is loaded, not something to call on manually at any time. My best guess was to create a new function that reader.onload would trigger, would contain all of the the current reader.onload function and I could specify directly later, to issue the callback, skipping referring to the onload event.
Seemed like a sound theory, but then I got stuck with the event object that the onload function passes through. Suppose I apply the above theory and the onload event calls the new function readerOnLoad(e). It would pass through the event object fine. However, when wanting to perform a callback later, how could I manually call readerOnLoad(e) as I don't see a way to pass on the event object from the same FileReader in an elegant way. It feels a little like I'm straying into very messy code* to solve whereas it feels like there should be something a little bit more elegant for a situation that doesn't feel too niche.
*The original $data event object variable was obtained by storing a global variable when a file input detected a change. In theory, I could create a global variable to store the event object for the FileReader onload. It just feels quite convoluted, in that the difference between my original code and a code that functions this way is quite severe and inelegant when the only difference is updating a var that wasn't ready in time.
When working with asynchronous function, you would usually want to pass a function as callback. You can use NodeJS style callback like this:
// asyncTask is an asynchronous function that takes 3 arguments, the last one being a callback
asyncTask(arg1, arg2, function(err, result) {
if (err) return handleError(err);
handleResult(result);
})
So in your case, you can do:
clProcessImages($data, function(dataurl) {
console.log(dataurl);
// Do you other thing here
});
function clProcessImages($data, onProcessedCallback) {
var filesToUpload = $data.target.files;
var file = filesToUpload[0];
var canvas = document.createElement('canvas');
var dataurl = 'data:image/jpg;base64';
var img = document.createElement("img");
var reader = new FileReader();
reader.onload = function(e) {
img.onload = function () {
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
var MAX_WIDTH = 800;
var MAX_HEIGHT = 600;
var width = img.width;
var height = img.height;
if (width > height) {
if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width;
width = MAX_WIDTH;
}
} else {
if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height;
height = MAX_HEIGHT;
}
}
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
dataurl = canvas.toDataURL("image/jpeg",0.8);
onProcessedCallback(dataurl);
}
img.src = e.target.result
}
reader.readAsDataURL(file);
return dataurl;
};
You might also want to pass error object to the callback like the first example, and handle it appropriately.
In short, always add a callback function as an argument to your own asynchronous function.
Related
I am currently building an application, in which a user can upload several images and work with them simultaneously. Since some processes performed poorly due to large image files, I want to resize them, before the user gets them.
In my resizer() function I try to resize using canvas. It works, but since the 'canvas.toDataURL()' is inside the img.onload function, I don't know how to return the value and parse it to the handleFiles() function.
Also...I tried some cases in which I got some code from the handleFiles() - like:
var preview = document.getElementById("img"+count);
var surface = document.getElementById("cubface"+count);
var count = counterImg();
preview.src = resizer(e.target.result, count);
surface.src = resizer(e.target.result, count);
And put them in the end of the img.onload function like
var preview = document.getElementById("img"+number);
var surface = document.getElementById("cubface"+number);
preview.src = canvas.toDataURL;
surface.src = canvas.toDataURL;
I got the resize, but I got only the last image from the loop to be processed.
So the questions are:
In the resizer() function, how to return the canvas.toDataURL value which is in img.onload?
Why does the loop cover only the last instance and not every image and how to solve that?
Full code:
JavaScript:
function resizer(base64, number){
// Max size for thumbnail
if(typeof(maxWidth) === 'undefined') maxWidth = 1200;
if(typeof(maxHeight) === 'undefined') maxHeight = 1200;
var img = new Image();
img.src = base64;
// Create and initialize two canvas
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var canvasCopy = document.createElement("canvas");
var copyContext = canvasCopy.getContext("2d");
img.onload = function() {
// Determine new ratio based on max size
var ratio = 1;
if (img.width > maxWidth)
ratio = maxWidth / img.width;
else if (img.height > maxHeight)
ratio = maxHeight / img.height;
// Draw original image in second canvas
canvasCopy.width = img.width;
canvasCopy.height = img.height;
copyContext.drawImage(img, 0, 0);
// Copy and resize second canvas to first canvas
canvas.width = img.width * ratio;
canvas.height = img.height * ratio;
ctx.drawImage(canvasCopy, 0, 0, canvasCopy.width, canvasCopy.height, 0, 0, canvas.width, canvas.height);
return canvas.toDataURL();
};
return img.onload;
}
function handleFiles(files) {
for (var i = 0; i < files.length; i++) {
var file = files[i];
var count = counterImg();
var preview = document.getElementById("img"+count);
var surface = document.getElementById("cubface"+count);
var reader = new FileReader();
reader.onload = (function (preview, surface) {
return function (e) {
var newimage = resizer(e.target.result, count);
preview.src = newimage;
surface.src = newimage;
$('#image-cropper'+count).cropit('imageSrc', e.target.result);
}
})(preview, surface);
reader.readAsDataURL(file);
}
}
The variable count is not scoped properly.
There's a delay between the time you declare the function and the time you use it, therefore, each execution ends up with the same value. So the jquery selector will always return the same element. Which explains why only the last image is modified.
Here's a jsfiddle that demonstrate the execution.
https://jsfiddle.net/Lc6bngv5/1/
To fix this, simply pass count to the function that encapsulate your preview and surface :
reader.onload = (function (preview, surface, count) {
return function (e) {
var newimage = resizer(e.target.result, count);
preview.src = newimage;
surface.src = newimage;
$('#image-cropper'+count).cropit('imageSrc', e.target.result);
}
})(preview, surface, count);
For the second question :
The resize function returns a function. It does not return the result of that function. To get the url properly, I would use a callback function :
reader.onload = (function (preview, surface, count) {
return function (e) {
var newimage = resizer(e.target.result, count, function(url){
preview.src = url;
surface.src = url;
$('#image-cropper'+count).cropit('imageSrc', e.target.result);
});
}
})(preview, surface, count);
And you would have to do the following changes in resize :
function resizer(base64, number, cb){
...
img.onload = function() {
...
// return canvas.toDataURL();
cb(canvas.toDataURL());
};
}
with this code I try to update a canvas with a drawn image when imageUpdated is true.
I use a timetag so the image isnt loaded from the browser cache.
But when i use timetags the canvas stays empty.
It works without the timetags.
var imageUpdated = true;
if(imageUpdated)
{
imageUpdated = false;
var heightImg = new Image;
heightImg.src = '/path/to/image.png?' + new Date().getTime().toString();
this.canvas = $('<canvas />')[0];
this.canvas.width = this.width;
this.canvas.height = this.height;
this.canvas.getContext('2d').drawImage(heightImg, 0, 0, this.width, this.height);
}
/*rest of event happens here*/
Thanks!
Probably it's because you're drawing the image before it is loaded.
I guess that's also the reason it works without the timetag, it's already in cache so can instantly be drawn.
A solution would be to draw after the image is loaded. So, add an eventListener on the image:
image.addEventListener('load', callback);
Within the callback, draw the image on the canvas.
For example:
// add eventlistener
heightImg.addEventListener('load', function(e) {
var width = heightImg.width;
var height = heightImg.height;
var canvas = document.querySelector('canvas');
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(heightImg, 0, 0, width, height);
});
// trigger the loading
heightImg.src = url + '?time=' + new Date().getTime().toString();
And a Fiddle
I googled enough examples and found solution how to resize images:
function imageToDataUri(img, width, height) {
// create an off-screen canvas
var canvas = document.createElement('canvas'),ctx = canvas.getContext('2d');
// set its dimension to target size
canvas.width = width;
canvas.height = height;
// draw source image into the off-screen canvas:
ctx.drawImage(img, 0, 0, width, height);
// encode image to data-uri with base64 version of compressed image
return canvas.toDataURL();
}
function resizeImage() {
var newDataUri = imageToDataUri(this, 10, 10); // resize to 10x10
return newDataUri;
}
and this is my problem:
I have list of objects where each Item has based 64 image string: group.mAvatarImgBase64Str. That string is greater then 10x10 and I need replace it with new String.
So far I did:
if (group.mAvatarImgBase64Str !== 'none') {
var img = new Image();
img.onload = resizeImage;
img.src = group.mAvatarImgBase64Str;
// group.mAvatarImgBase64Str = ??
}
If I'll print out new value in resizeImage method - It resized properly but:
How do I add new String back to group.mAvatarImgBase64Str?
onload task is async and I need something like:
group.mAvatarImgBase64Str = resize(group.mAvatarImgBase64Str, 10, 10);
Thanks,
You should be able to do it this way:
if (group.mAvatarImgBase64Str !== 'none') {
(function(group) {
var img = new Image();
img.onload = function() {
group.mAvatarImgBase64Str = resizeImage.call(this);
}
img.src = group.mAvatarImgBase64Str;
})(group);
}
Additional IIFE is needed in case if you use for loop to iterate over array of object. If you use forEach loop, it's not needed.
I am struggling with an issue that I can't define. I've pieced together this JQuery for uploading an image. When generating the preview, I'm using FileReader to get the image dimensions in order to control the resizing of the preview. As far as I can tell, that is working fine when I do console.log ($preview); inside img.onload = function() {};. But If I do that outside the function, it is undefined. I need access to the variable in the following functions. I've been studying scope, and from what I understand if I don't declare the variable inside the function like var variable = x; it shouldn't be local. I've also searched and searched the internet and SO for a solution but nothing seems to fit.
Is this an asynchronous issue or is it an issue with scope even though $preview is defined initially outside these functions? Can I return it from inside the function like in PHP?
var $preview;
if (previewsOn) {
if (isImgFile(ui.file)) {
var reader = new FileReader;
reader.onload = function() {
var img = new Image;
img.onload = function() {
if (img.width > 120 || img.height > 100) {
var maxWidth = 120;
var maxHeight = 100;
var ratio = 0;
var width = img.width;
var height = img.height;
if (width > maxWidth){
ratio = maxWidth / width;
$preview = $('<img/>').css("width", maxWidth);
$preview = $('<img/>').css("height", height * ratio);
height = height * ratio;
width = width * ratio;
}
if (height > maxHeight){
ratio = maxHeight / height;
$preview = $('<img/>').css("height", maxHeight);
$preview = $('<img/>').css("width", width * ratio);
width = width * ratio;
height = height * ratio;
}
} else {
$preview = $('<img/>');
}
};
img.src = reader.result;
};
reader.readAsDataURL(ui.file);
ui.readAs('DataURL', function(e) {
$preview.attr('src', e.target.result);
});
} else {
$preview = $('<i/>');
ui.readAs('Text', function(e) {
$preview.text(e.target.result.substr(0, 15) + '...');
});
}
} else {
$preview = $('<i>no preview</i>');
}
$($previewImg).empty(); $($preview).appendTo($previewImg);
$('<td/>').text(Math.round(ui.file.size / 1024) + ' KB').appendTo($row);
$('<td/>').append($pbWrapper).appendTo($row);
$('<td/>').append($cancelBtn).appendTo($row);
return $progressBar;
};
You assign value to $preview in .onload handler, but it is called some time later. According to your code following change could help:
var $preview;
if (previewsOn) {
if (isImgFile(ui.file)) {
$preview = $("<img/>");
(added initialization code after if statement)
Then replace lines like
$preview = $('<img/>').css("width", maxWidth);
with
$preview.css("width", maxWidth);
And remove
} else {
$preview = $('<img/>');
So variable will not be re-assigned.
answer to comment
Failure scenario timeline:
you code registers onload handler
you code continues execution, trying to do smth with $preview, which has no value yet
image finally loaded and onload handler triggered, assigning value to $preview
What is wrong with this function?
window.LoadImage = function(el, canvasId){
var canvas = document.getElementById(canvasId);
var context = canvas.getContext("2d");
var dialogCanvas = document.getElementsByClassName('dialogCanvas');
var dialogContext = dialogCanvas[0].getContext("2d");
var reader = new FileReader();
reader.onload = function(event){
var img = new Image();
img.onload = function(){
var w = 545;
var imgW = img.width;
var imgH = img.height;
var dialogW = dialogCanvas[0].width;
var dialogH = dialogCanvas[0].height;
h = (imgH / imgW) * w;
dialogH = (imgH / imgW) * dialogW;
context.clearRect(0, 0, w, imgH);
dialogContext.clearRect(0,0, dialogW, 250);
canvas.width = w;
canvas. height = h;
context.drawImage(img,0,0,w,h);
dialogContext.drawImage(img,0,0,dialogW, dialogH);
}
img.src = event.target.result;
}
reader.readAsDataURL(event.target.files[0]);
}
I am trying to draw 2 different canvas with this function. Neither work on Firefox. And the damn thing works on Chrome and IE.
The "dialogCanvas" is a preview which is inside a jquery modal box and the other, reached using the "canvasId" parameter, has it's display = "none", in the page.
I'm not getting any errors on firefox. Actually, I can't even debug it.
Thanks in advance.
Edit
jsfiddle = http://jsfiddle.net/cgEv8/
This line:
reader.readAsDataURL(event.target.files[0]);
refers to "event". Where's that? In some browsers it's a global object, but not in Firefox.
As already pointed out the event is undefined. In Firefox I get this error on the fiddle:
ReferenceError: event is not defined. Try to handle event
You should rather handle the event using:
$('#t1-1-img-header').on('change', LoadImage);
Then in you handler you can use
function LoadImage(event) {
var el = this; /// this will be the calling element
...
}
(and of course you need to remove the onchange attribute in the element itself).
For the canvas ID you need to either pass it in directly or use some other means to store it so it is available to the callback scope.
Modified fiddle here