Getting data from multiple images with one canvas - javascript

This is a specific problem so I'm going to do my best explaining it.
I have a single canvas on my page. The user uploads a batch of files taken from the same camera angle. The canvas displays one of the images and the user draws over the image in a way that depicts an area of interest. The program then goes over this area of interest and extracts relevant RGB data from the pixels inside the area.
Now, what I'm having an issue with is then switching the canvas over to the next image to process that one as well. I have the area of interest still specified as an array of indexes inside the area. But, when trying to obtain the RGB data from the rest of the images, it only gives me the info about the first image.
Below are some relevant code snippets as well as their function.
This function loads up the next image in the fileUpload to the canvas
function readNextImage(i) {
if (fileUpload.files && fileUpload.files[i]) {
var FR = new FileReader();
FR.onload = function(e) {
fabric.Image.fromURL(e.target.result, function(img) {
img.set({
left: 0,
top: 0,
evented: false
});
img.scaleToWidth(canvas.width);
img.setCoords();
canvas.add(img);
})
};
FR.readAsDataURL(fileUpload.files[i]);
}
}
This snippet loads up an array with the image data.
var imgData = context.getImageData(0,0,canvas.width,canvas.height);
var data = imgData.data;
So currently, I'm able to go through 'data' the first time and obtain the relevant RGB info I need. After I do that, I call the readNextImage() function, it loads it into the canvas, then I make another call to
var imgData = context.getImageData(0,0,canvas.width,canvas.height);
var data = imgData.data;
to hopefully load the arrays with the new values. This is where it seems to fail as the values are the same as the previous iteration.
Is there a chance the program isn't given enough time between loading the canvas with the new image and trying to get the new data values? Is there something else that's going on with the canvas that I'm unaware of?
Here's where I'm actually calling the readNextImage() function
for (var image_num=0; image_num<fileUpload.files.length; image_num+=1){
imgData = context.getImageData(0,0,canvas.width,canvas.height);
data = imgData.data;
/*Lines of code that parses through the data object specified above*/
readNextImage(image_num);
}
I'm hoping the problem is clear, let me know if anything is confusing.

Edit 2:
links to articles about promises: promises 1 promises 2 promises 3
Edit: Instead of using a global canvas, use a local one created for each image instance:
for (var image_num=0; image_num<fileUpload.files.length; image_num+=1){
var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
readNextImage(image_num,canvas);
imgData = context.getImageData(0,0,canvas.width,canvas.height);
data = imgData.data;
/*Lines of code that parses through the data object specified above*/
}
function readNextImage(i,canvas) {
if (fileUpload.files && fileUpload.files[i]) {
var FR = new FileReader();
FR.onload = function(e) {
fabric.Image.fromURL(e.target.result, function(img) {
img.set({
left: 0,
top: 0,
evented: false
});
img.scaleToWidth(canvas.width);
img.setCoords();
canvas.add(img);
})
};
FR.readAsDataURL(fileUpload.files[i]);
}
}
The problem could be here (the previous image is still attached to the canvas):
canvas.add(img);
try removing the previous child(ren) before adding another one by replacing that line with:
while (canvas.firstChild) {
canvas.removeChild(canvas.firstChild);
}
canvas.add(img);
canvas.getContext("2d").drawImage(img, 0, 0);

Related

Loaded SVG paths are not immediately pushed into an array

I'm trying to load paths from different SVG files and keep them in an array. I don't really want them in the canvas, since I'm mostly using them to create new paths from them. However, I do want from time to time to actually add them to the canvas to be rendered.
Here's a code snippet that shows my problem:
var canvas = new fabric.Canvas('c', {
backgroundColor: 'skyblue'
});
canvas.renderAll();
var orig_shapes = []; // I want to store the paths here
function loadShape(url){
fabric.loadSVGFromURL(url, function(paths, options){
let shape = paths[0];
orig_shapes.push(shape);
});
}
loadShape('path0.svg');
loadShape('path1.svg');
// Add all the loaded shapes to the canvas
console.log(orig_shapes.length);
orig_shapes.forEach(function(shape, index, arr) {
canvas.add(orig_shapes[index]);
});
canvas.renderAll();
This script is loaded at the end of the <body> element inside the web page.
I expected to see the paths rendered and a 2 in the console. Unfortunately, all I get is a blue background and a 0.
Despite this, if I check orig_shapes.length() inside the console, I do get a 2 back; so apparently the paths are eventually pushed to the array (but not when I need to). I can even add the paths to the canvas writing canvas.add(orig_shapes[i]) in the console. They are rendered with no problems.
So what's the problem? Why isn't this working as expected?
As Durga commented, loadSVGFromURL is an asynchronous function, so I needed to check first that the SVGs had been loaded. Here's a version of the script that uses Promises to make sure of this:
var canvas = new fabric.Canvas('c', {
backgroundColor: 'skyblue'
});
canvas.renderAll();
var orig_shapes = [];
function loadShape(url){
return new Promise(function(resolve, reject){
fabric.loadSVGFromURL(url, function(paths, options){
let shape = paths[0];
orig_shapes.push(shape);
resolve(shape);
});
});
}
function loadShapes(urls, shape_callback) {
let promises = [];
urls.forEach(function(url) {
promises.push(loadShape(url).then(shape_callback));
});
return Promise.all(promises);
}
function addToCanvas(object) {
canvas.add(object);
}
loadShapes(['path0.svg', 'path1.svg'])
.then(function(objs) {
objs.forEach(addToCanvas);
});
loadShape and loadShapes both allow me to set especific actions to execute when a single shape is loaded and when all shapes have been loaded, respectively.

How to convert multiple images to base64 with canvas

I am storing base64 encoded images, and at the moment I can only create one code (i'm attempting to create two, but it appears the second is being overwritten). I don't get the over-arching concept of canvas drawing, so I believe that is the root of my issue when trying to solve this problem.
current behavior: It stores the same DataUrl in local storage twice. It does log the correct info. the favicon-green is getting stored, just not red
How do I encode multiple base64 images with canvas?
html:
<head>
...
<link id="favicon" rel="icon" src="/assets/favicon-red-16x16.ico.png">
</head>
<body>
...
<!-- hidden images to store -->
<img id="favicon-green" rel="icon" src="/assets/favicon-green-16x16.ico.png" width="16" height="16" />
<img id="favicon-red" rel="icon" src="/assets/favicon-red-16x16.ico.png" width="16" height="16" />
...
</body>
js:
// cache images
function storeImages() {
// my sorry attempt to create two canvas elements for two image encodings
var canvasGreen = document.createElement('canvas');
var canvasRed = document.createElement('canvas');
// painting both images
var ctxGreen = canvasGreen.getContext('2d');
var ctxRed = canvasRed.getContext('2d');
// getting both images from DOM
var favGreen = document.getElementById('favicon-green');
var favRed = document.getElementById('favicon-red');
// checking if images are already stored
var base64Green = localStorage.getItem('greenFavicon');
var base64Red = localStorage.getItem('redFavicon');
console.log('storing...')
if (base64Green == null && window.navigator.onLine || base64Red == null && window.navigator.onLine) {
ctxGreen.drawImage(favGreen, 0, 0);
ctxRed.drawImage(favRed, 0, 0);
// getting images (the DataUrl is currently the same for both)
base64Green = canvasGreen.toDataURL();
base64Red = canvasRed.toDataURL();
localStorage.setItem('greenFavicon', base64Green);
localStorage.setItem('redFavicon', base64Red);
console.log("are they equal : ", base64Green == base64Red); // returns true
}
}
storeImages();
I don't see anything particularly wrong with your code. If the code isn't a direct copy and paste, I would look through it with a fine-tooth come to make sure you don't switch any red and green around.
There shouldn't be any surprising mechanisms when it comes to converting canvases to data URLs.
Here is a quick example of two:
const a = document.createElement('canvas');
const b = document.createElement('canvas');
const aCtx = a.getContext('2d');
const bCtx = b.getContext('2d');
aCtx.fillStyle = '#000';
aCtx.fillRect(0, 0, a.width, a.height);
const aUrl = a.toDataURL();
const bUrl = b.toDataURL();
console.log(aUrl == bUrl, aUrl, bUrl);
console.log('First difference index:', Array.prototype.findIndex.call(aUrl, (aChar, index) => aChar !== bUrl[index]));
Notice that they are different. However, notice that they also start out very similar, and you have to go quite a ways over to start seeing differences (in my example, character 70). I would double-check that they are actually the same (by comparing them like I did). It could be it just looks the same.
Another thing you might do, which is more of a code style thing, but could also help with accidentally green and red mixups, is make a function to save just one, then call it twice.
const saveImage = (imageId, key) => {
if (localStorage.getItem(key)) {
return; // already saved
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const image = document.getElementById(imageId);
ctx.drawImage(image, 0, 0);
if (window.navigator.onLine) {
localStorage.setItem(key, canvas.toDataURL());
}
}
saveImage('favicon-green', 'greenFavicon');
saveImage('favicon-red', 'redFavicon');
Not only does that clean up your code and keep it DRY, but it also helps avoid accidental mix-ups between red and green in your function.
After some comments back and forth, I realized another possibility is you are trying to draw the images to the canvas before the images are loaded. This will cause it to draw blank images, but otherwise act like it is working fine.
You can quickly test this by console logging this:
console.log(image.width === 0);
after setting the image variable. If the value is true, then the image isn't loaded yet (before loading, images will have a width and height of 0). You need to make sure to wait until the image is loaded before trying to save it.
The best way to do this is with an addEventListener():
document.getElementById('favicon-green').addEventListener('load', () => {
saveImage('favicon-green', 'greenFavicon');
});
There is one more catch with this, in that if the image is somehow already loaded by the time that code runs, it'll never trigger. You need to look at the width of the image as well. Here is a function that does this all for you, and returns a Promise so you know it's done:
const saveImage = (imageId, key) => new Promise(resolve => {
if (localStorage.getItem(key)) {
return resolve(); // already saved
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const image = document.getElementById(imageId);
const onImageLoaded = () => {
ctx.drawImage(image, 0, 0);
if (window.navigator.onLine) {
localStorage.setItem(key, canvas.toDataURL());
}
resolve();
}
if (image.width > 0) {
onImageLoaded();
} else {
image.addEventListener('load', onImageLoaded);
}
});
saveImage('favicon-green', 'greenFavicon').then(() => console.log('saved'));

JavaScript: initialize different image if an error occurs

I have a folder with the contents like the following (giving a minimal example)
ex1_1.png
ex1_2.png
ex2_1.png
page.html
and I've written the following JavaScript code. The HTML file has two canvas elements that are designed to draw ex1_1.png and ex2_1.png and each canvas element has an associated "Next" button. If the first one is clicked it erases the first canvas element and draws ex_2.png. What I want is for the Next button to cycle through all my images, going back to the start when the last image is exceeded. The following JavaScript accomplishes this, except for the part where it cycles back. When it reaches the image with source ex1_3.png (which doesn't exist in the folder), I get a crash, but on the draw() command--which tells me that for whatever reason, it's not cycling the source back to ex1_1.png before attempting to draw.
To the best of my ability to debug this, something is going wrong with the img.onerror part, or how its implemented with the global variable window.indicator. When I cycle through using the next button, the indicator shows true then false if I print the value from within the img.onerror function. But if I print from within the next() function, it never shows false. This sounds like some kind of an issue with the window.indicator keeping its value globally.
// Variable to indicate whether the most recently generated image
// was valid.
window.indicator = true;
// Give the file base-name and index as stored in the local address.
// Return the corresponding Image() object.
function initImg(name, ind) {
var img = new Image();
// The file is local, the image is always of the form
// baseName_i.png
window.indicator = true;
img.src = name + "_" + ind + ".png";
img.onerror = function() {
// Find the appropriate canvas state element, and
// update its state back to 1.
for (i = 0; i < canStates.length; i++) {
var n = canStates[i][0].getAttribute("id");
n = n.split("_")[1];
if (name == canStates[i][0].getAttribute("id")) {
canStates[i][1] = 1;
}
window.indicator = false;
}
};
return img;
}
// Give the canvas context and image objects. Draw the image to
// the context, no return value.
function draw(ctx, img) {
// Check that the image is loaded before writing. Keep
// checking every 50 milliseconds.
if (!img.complete) {
setTimeout( function() {
draw(ctx, img);
}, 50);
}
// Clear the current image and draw the new.
ctx.clearRect(0,0, 200,200);
ctx.drawImage(img, 0,0, 200,200);
}
// Give the string canvas id and string base-name, create the
// canvas object and draw the first image to the canvas.
function slideShow(canId, name) {
var can = document.getElementById(canId);
can.width = 300;
can.height = 300;
var ctx = can.getContext('2d');
var img = initImg(name, 1);
draw(ctx, img);
}
// Next button function. Give the name of the canvas, draw the
// next image or cycle to the start.
function next(button) {
var name = button.getAttribute("name");
// Find the appropriate canvas element.
for (i = 0; i < canStates.length; i++) {
var id = canStates[i][0].getAttribute("id");
id = id.split("_")[1];
if (id == name) {
// Use the states to produce an image, and update the
// states.
canStates[i][1] += 1;
var img = initImg(name, canStates[i][1]);
if (!window.indicator) {
img = initImg(name, 1);
}
// Draw to the canvas.
draw(canStates[i][0].getContext('2d'),img);
}
}
}
// Create a global variable tracking all states of "Next" buttons.
// Stored as a list, each element is a list, the left coordinate is
// a canvas and the right coordinate is its state (image index).
// Also initialize all slide shows.
// The variable r stores the canvases and states, initialized
// outside the function in order to pass-by-reference so as to act
// as a global variable.
var canStates = new Array();
window.onload = function() {
var cans = document.getElementsByTagName("canvas");
for (i=0; i < cans.length; i++) {
var c = cans[i];
var n = c.getAttribute("id").split("_")[1];
slideShow("can_"+n, n);
}
for (i = 0; i < cans.length; i++) {
canStates[i] = [cans[i],1];
}
}
I could switch strategies completely here. I've heard that PHP is a decent way to server-side look at the files in a directory, and I could use that, but I don't know how to make a PHP script execute when a browser is loaded, or how to take its results and hand them over to the JavaScript file.

SVG tained canvas IE security error toDataURL [duplicate]

This question already has answers here:
Tainted Canvas, due to CORS and SVG?
(2 answers)
Closed 4 years ago.
I have a directive in angular JS that allows exporting SVGs to a PNG. This works fine in other browsers, but in IE I get a security error.
I have tried all sorts of things, but it just doesn't seem to work.
I read somewhere that if I base64 encode it, then that would work, but it doesn't.
The code I have for drawing the SVG on the canvas is this:
// Private function for drawing our images
var drawImage = function (canvas, ctx, svgContainer) {
// Defer our promise
var deferred = $q.defer();
// Remove hidden layers
removeHidden(angular.element(clone));
// Create our data
var clone = angular.element(svgContainer[0].cloneNode(true)),
child = clone.children()[0];
// Remove hidden layers
removeHidden(angular.element(clone));
var s = new XMLSerializer(),
t = s.serializeToString(child),
base64 = 'data:image/svg+xml;base64,' + window.btoa(t);
console.log(base64);
var img = new Image();
// When the image has loaded
img.onload = function () {
// Create our dimensions
var viewBox = child.getAttribute('viewBox').split(' ');
console.log(viewBox);
var dimensions = {
width: viewBox[2],
height: viewBox[3]
};
console.log(img.width);
// Get our location
getNextLocation(canvas.width, canvas.height, dimensions);
// Draw our image using the context
ctx.drawImage(img, 0, 0, dimensions.width / 2, dimensions.height, location.x, location.y, location.width, location.height);
// Resolve our promise
deferred.resolve(location);
};
// Set the URL of the image
img.src = base64;
// Return our promise
return deferred.promise;
};
Before this I was creating a blob but that also caused the security error.
My main bit of code, is this bit here:
// Public function to generate the image
self.generateImage = function (onSuccess) {
// Reset our location
location = null;
counter = 0;
// Get our SVG
var target = document.getElementById(self.options.targets.containerId),
svgContainers = angular.element(target.getElementsByClassName(self.options.targets.svgContainerClassName)),
itemCount = svgContainers.length;
// Get our context
var canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d');
// Set our canvas height and width
setCanvasDimensions(canvas, itemCount);
// Create our array of promises
var promises = [];
// Draw our header and footer
drawHeader(canvas, ctx);
//// For each container
//for (var i = 0; i < itemCount; i++) {
// // Get our elements
// var svgContainer = svgContainers[i];
// // Draw our image and push our promise to the array
// promises.push(draw(canvas, ctx, svgContainer));
//}
promises.push(draw(canvas, ctx, svgContainers[0]));
// Finally add our footer to our promises array
promises.push(drawFooter(canvas, ctx));
// When all promises have resolve
$q.all(promises).then(function () {
console.log('all promises completed');
// Get our URL as a base64 string
var dataURL = canvas.toDataURL("image/png");
console.log('we have the image');
// Create our model
var model = {
teamName: self.options.team.name,
sport: self.options.team.sport,
data: dataURL
};
// Create our preview
self.create(model).then(function (response) {
console.log('saved to the database');
// Invoke our success callback
onSuccess(response);
});
})
};
As you can see I loop through each svg container and draw the SVG for each one. I have commented that out in this bit of code and just draw the first image.
The console.log directive after canvas.toDateURL never get's invoked and I get a security error.
The SVG is inline. From what I have read the issue might be due to the xlns declaration, but removing it still gives me the issue:
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 259.5 131" enable-background="new 0 0 259.5 131" xml:space="preserve">
Does anyone know how I can solve this issue?
You need to set
img.crossOrigin= 'anonymous';
before
img.src=...
However, to make this work, you will also need to have
access-control-allow-origin: *
set in the response header of your image.
to try the solution, you can use 'http://fabricjs.com/assets/printio.png'(which has the proper header set) for example

Problems getting context while using a javascript library to handle image sizing

I'm using a good library on here to handle some large images coming in through the iphone camera in order to avoid the whole subsampling drama here.
My draw code:
function imageLoaded(img, frontCamera) {
element = document.getElementById("canvas1");
var mpImg= new MegaPixImage(img);
// read the width and height of the canvas- scaled down
width = element.width; //188 94x2
height = element.height; //125
//used for side by side comparison of images
w2 = width / 2;
// stamp the image on the left of the canvas
if (frontCamera) {
mpImg.render(element, {maxWidth:94, maxHeight:125});} else{
mpImg.render(element, {maxWidth:94, maxHeight:125});}
//at this point, i want to grab the imageData drawn to the canvas using
//MegaPixImage and continue to do some more image processing, which normally
//would happen by declaring ctx=element.getContext("2d");
//more stuff here
}
The image is drawing fine,...but I cannot seem to find a way of then doing image processing on that image subsequently. How would I get a new context after having drawn that image on the canvas?
Maybe I would either have to run further image processing from within that library so I have context access or strip the context drawing out of the library.
Thanks for the help!
I had a similar issue, and actually found a helpful function to detect subsampling and only use MegaPixImage when subsampling was found.
In my case, for local file reading (iPhone camera, in your case), I called a handleFileSelect function when a <input type="file"> value is changed (i.e. when a file is selected to populate this input). Inside this function, I called a general populateImage JS function that draws the image data to the canvas.
Here's the handleFileSelect function and input binding:
$("#my_file_input").bind('change', function (event) {
handleFileSelect(event);
});
function handleFileSelect(event) {
var reader,
tmp,
file = event.target.files[0];
try {
reader = new FileReader();
reader.onload = function (e) {
tmp = e.target.result.toString();
// In my case, some image data (from Androids, mostly) didn't contain necessary image data, so I added it in
if (tmp.search("image/jpeg") === -1 && tmp.search("data:base64") !== -1) {
tmp = tmp.replace("data:", "data:image/jpeg;");
}
populateImage(tmp);
};
reader.onerror = function (err) {
// Handle error as you need
};
reader.readAsDataURL(file);
} catch (error) {
// Handle error as you need
}
}
Then, my populateImage function (called in the reader.onload function above):
function populateImage(imageURL) {
var tmpImage = new Image();
$(tmpImage).load(function () {
var mpImg, mpImgData;
// If subsampling found, render using MegaPixImage fix, grab image data, and re-populate so we can use non-subsampled image.
// Note: imageCanvas is my canvas element.
if (detectSubsampling(this)) {
mpImg = new MegaPixImage(this);
mpImg.render(imageCanvas, {maxWidth: 94, maxHeight: 125});
mpImgData = imageCanvas.toDataURL("image/jpg");
populateImage(mpImgData);
return;
}
// Insert regular code to draw image to the canvas
// Note: ctx is my canvas element's context
ctx.drawImage(tmpImage, 0, 0, 94, 125); // Or whatever x/y/width/height values you need
});
$(tmpImage).error(function (event) {
// Handle error as you need
});
tmpImage.src = imageURL;
}
And last but not least, the detectSubsampling function. Note that this method was found from another source and isn't my own.
function detectSubsampling(img) {
var iw = img.naturalWidth,
ih = img.naturalHeight,
ssCanvas,
ssCTX;
if (iw * ih > 1024 * 1024) { // Subsampling may happen over megapixel image
ssCanvas = document.createElement('canvas');
ssCanvas.width = ssCanvas.height = 1;
ssCTX = ssCanvas.getContext('2d');
ssCTX.drawImage(img, -iw + 1, 0);
// Subsampled image becomes half smaller in rendering size.
// Check alpha channel value to confirm image is covering edge pixel or not.
// If alpha value is 0 image is not covering, hence subsampled.
return ssCTX.getImageData(0, 0, 1, 1).data[3] === 0;
}
return false;
}
This may be more than you bargained for, but like I said, I ran into a similar issue and this solution proved to work across all browsers/devices that were canvas supported.
Hope it helps!

Categories

Resources