How to return function with img.onload? - javascript

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

Related

Can't redraw image on canvas with css filters [duplicate]

This question already has answers here:
Callback/scope understanding
(3 answers)
Closed 1 year ago.
I am having trouble drawing an image with CSS filters onto a canvas. I want to apply the CSS filters with sliders/range inputs that I have. The image that the filters will be applied to is an image that you upload from your files. This image is put into a variable in a function I have that will upload the image. The function for the slider is separate, and therefore I can't apply the filters and draw the image again in the function that is activated when the sliders are being dragged.
I need it to be drawn instead of just putting the filters on the canvas itself so that I can download the image with filters.
Here is the javascript code:
var imageLoader = document.getElementById('imageLoader');
imageLoader.addEventListener('change', handleImage, false);
var canvas = document.getElementById('imageCanvas');
var ctx = canvas.getContext('2d');
function handleImage(e) {
var reader = new FileReader();
reader.onload = function(event) {
var img = new Image();
img.onload = function() {
var ratio = this.height / this.width;
canvas.height = canvas.width * ratio;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
}
img.src = event.target.result;
}
reader.readAsDataURL(e.target.files[0]);
}
var container = document.getElementById("container")
var download = document.getElementById("download")
var adjustments = document.getElementById("adjustments")
download.addEventListener("click", function() {
var link = document.createElement('a');
link.download = 'filename.png';
link.href = canvas.toDataURL();
link.click();
});
var prop1 = document.getElementById("prop1");
var open = true;
adjustments.addEventListener("click", function() {
if (open === true) {
prop1.style = "margin-left: -240px;";
adjustments.classList.remove("line");
open = false;
} else if (open === false) {
prop1.style = "margin-left: 50px;";
adjustments.classList.add("line");
open = true;
}
});
var contrast = document.getElementById("contrast");
var brightness = document.getElementById("brightness");
var slider1 = contrast.value;
var slider2 = brightness.value;
function effect() {
slider1 = contrast.value;
slider2 = brightness.value;
ctx.filter = "contrast(" + slider1 + ") brightness(" + slider2 + ")";
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
}
contrast.oninput = function() {
effect()
};
brightness.oninput = function() {
effect()
};
You could try clearing the canvas with ctx.clearRect() then redraw everything on the canvas. I'd also recommend creating a function to redraw the items just for easier reading.

How to call an asynchronous JavaScript function synchronously?

I've checked every question related to my problem in Stack Overflow and couldn't find a way to fix it.
What I am trying to do is; User tries to upload images, they get resized in the client-side and then they get uploaded. I've used Pica library. Everything works fine for one file. However, when I changed it to multiple files, I get duplicates of the last image.
What's happening: Loop 1 thru N times -> resizeImg N times
Ideal Solution: Loop 1 -> resizeImg(1) -> Loop 2 -> resizeImg(2)
Any help would be appreciated.
My code is below:
function resizeImg(source) {
img = new Image;
img.src = source;
img.onload = function() {
width = img.naturalWidth;
height = img.naturalHeight;
ratio = Math.min(targetWidth / width, targetHeight / height);
resizer = window.pica();
canvas = document.createElement("canvas");
ctx = canvas.getContext("2d");
ctx.canvas.width = width * ratio;
ctx.canvas.height = height * ratio;
resizer.resize(img, canvas, {
quality: 3,
alpha: true,
unsharpAmount: 0
}).then(result => resizer.toBlob(result, 'image/jpeg', 0.90)).then(blob => imgBlobArray.push(blob)).then(function() {
console.log(i);
console.log(imgBlobArray);
});
};
}
document.getElementById('select').onchange = function(evt) {
for (i = 0; i < this.files.length; i++) {
resizeImg(window.URL.createObjectURL(this.files[i]));
}
}
The problem is that you don't have a separate binding of img for each call of resizeImg - you don't have var or const or let in front of the first use of img. You're implicitly creating a global variable. So, to the interpreter, it looks something like
var img;
function resizeImg(source) {
img = new Image;
img.src = source;
img.onload = function() {
img is getting continuously reassigned. So, after all iterations, img will end up being only the last img created with resizeImg - the references to the other Images have been lost.
So, always declare variables explicitly to ensure each call of resizeImg has a separate img binding. Do the same with all your other variables as well, or they'll be implicitly global.
function resizeImg(source) {
const img = new Image;
img.src = source;
img.onload = function() {
const width = img.naturalWidth;
const height = img.naturalHeight;
const ratio = Math.min(targetWidth / width, targetHeight / height);
const resizer = window.pica();
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
ctx.canvas.width = width * ratio;
ctx.canvas.height = height * ratio;
resizer.resize(img, canvas, {
quality: 3,
alpha: true,
unsharpAmount: 0
}).then(result => resizer.toBlob(result, 'image/jpeg', 0.90)).then(blob => imgBlobArray.push(blob)).then(function() {
console.log(i);
console.log(imgBlobArray);
});
};
}
document.getElementById('select').onchange = function(evt) {
for (let i = 0; i < this.files.length; i++) {
resizeImg(window.URL.createObjectURL(this.files[i]));
}
}
You could try defining a recursive function like so
function resizeImgRec(files, i) {
if (i >= files.length)
return;
...
img.onload = function() {
...
resizer.resize(img, canvas, {
...
}).then(result => resizer.toBlob(result, 'image/jpeg', 0.90)).then(blob => imgBlobArray.push(blob)).then(function() {
console.log(i);
console.log(imgBlobArray);
resizeImg(files, i + 1);
});
};
}
document.getElementById('select').onchange = function(evt) {
resizeImgRec(this.files, 0);
}
This way the next resizeImg will only execute after the last promise is resolved.

Adjusting canvas quality to achieve predetermined image file size

I want to resize (as in reduce the filesize if it exceeds limit) an Image using canvas. I see that the resultant image size drawn on canvas, keeping the width and height constant, results in different file size depending upon what has been drawn on canvas.
I am willing to reduce the resolution further provided I get the desired file size.
How do I make sure whatever the resolution I get file(image) size within the limit?
Refer to this fiddle
var el = document.getElementById('f');
el.addEventListener('change', onChange);
function onChange(e) {
var file = this.files[0];
console.log("CHANGING");
var fr = new FileReader();
fr.onload = function(e) {
drawOnCanvas(e.target.result);
}
fr.readAsDataURL(file);
}
function drawOnCanvas(imgSrc) {
var canv = document.createElement('canvas'),
img = new Image();
console.log("DRAWING");
canv.width = canv.height = 500;
ctx = canv.getContext('2d');
img.onload = function() {
console.log("IMAGE LOADED");
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canv.width, canv.height);
try {
document.body.removeChild(document.querySelector('canvas'));
} catch (e) {}
document.body.appendChild(canv);
blob = canvasToFile(canv.toDataURL());
alert("FILE SIZE " + blob.size);
}
img.src = imgSrc;
}
function canvasToFile(dataUrl) {
var encData = dataUrl.split(',')[1],
binString = atob(encData),
binData = new Uint8Array(binString.length),
i, blob;
for (i = 0; i < binString.length; i++) {
binData[i] = binString.charCodeAt(i);
}
blob = new Blob([binData], {
type: 'image/jpeg'
});
return blob;
}
<p>Try uploading different image file and see the difference in size for same output resolution:</p>
<input type="file" id="f">
What you are experiencing is that the default type argument for canvas.toDataURL(type, encoderOptions) is image/png, and also that it's actually a base64 encode of the image data.
For the former, most browsers do support image/jpeg as image type. This may reduce the data size, thanks to jpg compression, but this will above all enable the encoderOptions parameter of the method, which could be translated to quality parameter and requires a 0 to 1 float.
Note that you can't be sure that the data will be lighter if converted into jpg though : if the image is mainly composed of transparent pixels, then png will be lighter. Also, remember that jpeg compression will reduce the quality of your image, see the last snippet to see what 0 results in :)
For the later, ( you can find an explanation of the why here ), the solution is to transform back the dataURL to a blob.
Here is a snippet to demonstrate it :
var input = document.querySelector('input');
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d');
function draw(){
var img = new Image();
img.src = URL.createObjectURL(this.files[0]);
img.onload = function(){
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
ctx.drawImage(this, 0,0);
var noData = canvas.toDataURL();
png_data.innerHTML = 'dataURL length: '+noData.length;
var noBlob = dataURItoBlob(noData);
png_blob.innerHTML = 'blob size: '+ noBlob.size;
var jpgData10 = canvas.toDataURL('image/jpeg', 1),
jpgData5 = canvas.toDataURL('image/jpeg', 0.5),
jpgData0 = canvas.toDataURL('image/jpeg', 0);
jpg_data.innerHTML = 'quality = 1 : '+ jpgData10.length+'<br>';
jpg_data.innerHTML += 'quality = 0.5 : '+ jpgData5.length+'<br>';
jpg_data.innerHTML += 'quality = 0 :'+ jpgData0.length+'<br>';
var jpgBlob10 = dataURItoBlob(jpgData10);
jpg_blob.innerHTML = 'blob quality = 1 : '+jpgBlob10.size+'<br>';
var jpgBlob5 = dataURItoBlob(jpgData5);
jpg_blob.innerHTML += 'blob quality = .5 : '+jpgBlob5.size+'<br>';
var jpgBlob0 = dataURItoBlob(jpgData0);
jpg_blob.innerHTML += 'blob quality = 0 : '+jpgBlob0.size;
}
this.previousElementSibling.innerHTML = 'orginal size : '+this.files[0].size;
}
input.onchange = draw;
// Taken from https://stackoverflow.com/a/5100158/3702797
function dataURItoBlob(dataURI) {
var byteString;
if (dataURI.split(',')[0].indexOf('base64') >= 0)
byteString = atob(dataURI.split(',')[1]);
else
byteString = unescape(dataURI.split(',')[1]);
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
var ia = new Uint8Array(byteString.length);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ia], {type:mimeString});
}
<div></div>
<input type="file"/>
<br>PNG :
<div id="png_data"></div>
<div id="png_blob"></div>
<br>Jpeg :
<div id="jpg_data"></div>
<div id="jpg_blob"></div>
canvas.toDataURL('image/jpeg', 0) demo ?
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d');
document.querySelector('input').onchange = function(){
var img = new Image();
img.src = URL.createObjectURL(this.files[0]);
img.onload = function(){
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
ctx.drawImage(this, 0,0);
document.images[0].src = canvas.toDataURL('image/jpeg', 0);
}
}
<input type="file"/>
<img/>
Also, if you need to make your file lighter than a specific size, I wrote something on another question, which limited the data size to 3Mb (just change maxSize to your needs and remove the localStorage part).

getImageData() returning all zeros

I'm trying to output the pixel values from an image. The image is loading correctly; another answer on here suggested that the browswer is trying to read the pixels before the image is finished loading, but I can see that it's loaded by the time the alert() fires.
function initContext(canvasID, contextType)
{
var canvas = document.getElementById(canvasID);
var context = canvas.getContext(contextType);
return context;
}
function loadImage(imageSource, context)
{
var imageObj = new Image();
imageObj.onload = function()
{
context.drawImage(imageObj, 0, 0);
};
imageObj.src = imageSource;
return imageObj;
}
function readImage(imageData)
{
console.log();
console.log(imageData.data[0]);
}
var context = initContext('canvas','2d');
var imageObj = loadImage('images/color-test.png',context);
var imageData = context.getImageData(0,0,10,10);
alert();
readImage(imageData);
Image.onload() is called asynchronously when the image has been loaded. That can happen after your current code calls context.getImageData().
The following code should work:
function initContext(canvasID, contextType)
{
var canvas = document.getElementById(canvasID);
var context = canvas.getContext(contextType);
return context;
}
function loadImage(imageSource, context)
{
var imageObj = new Image();
imageObj.onload = function()
{
context.drawImage(imageObj, 0, 0);
var imageData = context.getImageData(0,0,10,10);
readImage(imageData);
};
imageObj.src = imageSource;
return imageObj;
}
function readImage(imageData)
{
console.log();
console.log(imageData.data[0]);
}
var context = initContext('canvas','2d');
var imageObj = loadImage('images/color-test.png',context);
If you want to access the imageData value outside of loadImage(), you have 2 basic choices:
Store the imageData in a variable that is declared outside the function. In this case, the value will be unset until your onload() is called, so the outside code would have to test it for reasonableness before calling readImage(imageData).
Have the outside code register a callback function, and have your onload() call that function with the imageData value.
It is also possible you are evaluating a portion of an image that is transparent. I had this issue while scanning a PNG.
Share a situation I encountered when I used a loop to request multiple images at the same time and get their pixels respectively. Then I encountered this problem.
//my code looks like this
for(let i = 0; i < urls.length; ++i){
let img = new Image() // it will be destoryed every loop begin, even if use keyword 'var' insteads of 'let'.
img.onload = ()=>{
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
let imgData = ctx.getImageData(0, 0, 1024, 1024) // imgData will be all 0 or some other unexpected result.
}
img.src = urls[i]
}
// fixed bug
window.imgs = []
for(let i = 0; i < urls.length; ++i)
window.imgs[i] = new Image()
for(let i = 0; i < urls.length; ++i){
let img = window.imgs[i]
img.onload = ()=>{
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
let imgData = ctx.getImageData(0, 0, 1024, 1024)
}
img.src = urls[i]
}
The reason may be object reference, garbage collection and other problems. I don't know. Because I'm a beginner of js.

Fill canvas with images in javascript without having overlapping images

To fill a canvas with several images i am using the following code:
<canvas id="myCanvas"></canvas>
<script>
function loadImages(sources, callback) {
var images = {};
var loadedImages = 0;
var numImages = 0;
// get num of sources
for ( var src in sources) {
numImages++;
}
for ( var src in sources) {
images[src] = new Image();
images[src].onload = function() {
if (++loadedImages >= numImages) {
callback(images);
}
};
images[src].src = sources[src];
}
}
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
fitToContainer(canvas);
var sources = {
darthVader : 'http://www.html5canvastutorials.com/demos/assets/darth-vader.jpg',
yoda : 'http://www.html5canvastutorials.com/demos/assets/yoda.jpg'
};
loadImages(sources, function(images) {
context.drawImage(images.darthVader, 0,0);
context.drawImage(images.yoda, 0, 0);
});
function fitToContainer(canvas) {
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
}
</script>
Although this is in fact filling the canvas with images, i am not able to position the images in order not to overlap them. I dont know ahead of time the sizes of pictures. Is there any library or framework than can easily do this for me or what direction can i go?
And the canvas has a limited size since it is inside a parent html element.
Just keep track of the accumulated width of your images.
Then draw the next image to the right of that accumulated width
loadImages(sources, function(images) {
var nextX=0;
context.drawImage(images.darthVader, nextX,0);
nextX+=images.darthVader.width;
context.drawImage(images.yoda, nextX, 0);
nextX+=images.yoda.width;
// ...and so on...
});

Categories

Resources