I've been working on this way for people to draw stuff on canvas and upload it to the server.
It works fine except for people being able to insert their own images (my friend tested it)
Here's what it does:
Person clicks "submit" their canvas is saved into base64 and is sent using $.post()
php file in the $.post() runs and saves the file to a file in the server
Is there any way from preventing the user to be able to submit their own images, I already check image dimensions and so on but they'd just resize it and submit it. (I don't think php's ability to draw image rects would work due to my small servers)
Is there any way from preventing the user to be able to submit their own images
Nope.
Anyone can upload whatever they want by completely bypassing your client-side code. There's nothing you can do about this, outside of hacky heuristics. (Have they been on the page awhile? Were mouse movements or screen touches detected? Did they actually draw something?) These sorts of things can also be faked, it's just a hair more hassle to hack around. Don't bother with this, you'll create more problems for yourself than you will solve.
If, as I understand it, your app is an simple drawing tool, then one easy way would be to only send a JSON containing all the user gestures, instead of saving the image per se.
This way, even if one could still bypass your app, and produce images out of you control (i.e programmatically), they wouldn't be able to save e.g pr0n images on your server.
This would also allow you to perform a sanity check of the data structure server side, before saving it, and to implement an cancel feature in your app.
One counter-side effect however, would be a consequent increase of data to save on large drawings (but a decrease on smaller ones).
const ctx = canvas.getContext('2d');
let drawing = false,
rect = canvas.getBoundingClientRect(),
paths = [];
let savedData = '[]';
save_btn.onclick = _ => {
savedData = JSON.stringify(paths);
console.clear();
console.log(savedData);
// here send this JSON data to the server
};
load_btn.onclick = _ => {
// easy to grab from server too
paths = JSON.parse(savedData);
draw();
};
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// at each draw, we loop over all our paths
paths.forEach(p => {
ctx.lineWidth = p.strokeWidth;
ctx.strokeStyle = p.color;
ctx.beginPath();
const l = p.list;
ctx.moveTo(l[0], l[1]);
for (let i = 2; i < l.length; i += 2) {
ctx.lineTo(l[i], l[i + 1]);
}
ctx.stroke();
});
}
// returns a new path object
function makePath() {
return {
color: randCol(),
list: [],
strokeWidth: (Math.random() * 10) + 1
};
}
canvas.onmouseup = canvas.onmouseleave = e => {
drawing = false;
};
canvas.onmousedown = e => {
paths.push(makePath());
drawing = true;
}
canvas.onmousemove = throttle(e => {
if (!drawing) return;
// to minimize the size of our JSON data
// we fix the coordinates to precision(2)
let x = ~~((e.clientX - rect.left) *10)/10;
let y = ~~((e.clientY - rect.top) *10)/10;
paths[paths.length - 1].list.push(x, y);
draw();
});
window.onresize = window.onscroll = throttle(e => rect = canvas.getBoundingClientRect());
function throttle(callback) {
let active = false;
let evt;
const handler = function() {
active = false;
callback(evt);
}
return function handleEvent(e) {
evt = e;
if (!active) {
active = true;
requestAnimationFrame(handler);
};
};
}
function randCol() {
const letters = '0123456789ABCDEF'.split('');
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.round(Math.random() * 15)];
}
return color;
}
canvas{
border: 1px solid;
}
<button id="save_btn">save</button>
<button id="load_btn">load last saved</button><br>
<canvas id="canvas"></canvas>
You can check image data such as the image height ,width, possibly even image size and color depending on what is rendered in the canvas. For an example let's say the canvas is 250 x 250 pixels and it renders 2D squares only using the colors blue, red, and green. If there are more than three colors, if the colors are not only blue, red, and green, or if the canvas is not 250 x 250 you deny it. You could also check the "referrer" value in the user agent, this can be easily changed though.
Related
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'));
I am creating a simple animation program in p5.js. When a user clicks the save button, I want to download a video of the animation.
I have an object called frames where each key is labelled frame_1, frame_2 and so on. The value associated with each key is an array of line segments that makes up that frame.
I am trying to think of an approach to take this data and create an mp4 video. p5.js has a built in save function that I thought might be helpful but it is not a full solution on its own. I could save each frame as an individual image and then somehow stitch those images together on the client side but I have yet to find a solution to this.
Any other approaches would be great as well. The only requirement is that it is done client side.
Since p5.js is built on the Canvas API, in modern browsers, you can use a MediaRecorder to do this job.
const btn = document.querySelector('button'),
chunks = [];
function record() {
chunks.length = 0;
let stream = document.querySelector('canvas').captureStream(30),
recorder = new MediaRecorder(stream);
recorder.ondataavailable = e => {
if (e.data.size) {
chunks.push(e.data);
}
};
recorder.onstop = exportVideo;
btn.onclick = e => {
recorder.stop();
btn.textContent = 'start recording';
btn.onclick = record;
};
recorder.start();
btn.textContent = 'stop recording';
}
function exportVideo(e) {
var blob = new Blob(chunks);
var vid = document.createElement('video');
vid.id = 'recorded'
vid.controls = true;
vid.src = URL.createObjectURL(blob);
document.body.appendChild(vid);
vid.play();
}
btn.onclick = record;
// taken from pr.js docs
var x, y;
function setup() {
createCanvas(300, 200);
// Starts in the middle
x = width / 2;
y = height;
}
function draw() {
background(200);
// Draw a circle
stroke(50);
fill(100);
ellipse(x, y, 24, 24);
// Jiggling randomly on the horizontal axis
x = x + random(-1, 1);
// Moving up at a constant speed
y = y - 1;
// Reset to the bottom
if (y < 0) {
y = height;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.7/p5.min.js"></script>
<button>start recording</button><br>
ccapture works well with p5.js to achieve the goal of recording what's displaying on a canvas.
Here is a demo of ccapture working with p5.js. The source code comes with the demo.
This method won't output laggy videos because it is not recording what you see on the screen, which can be laggy. Instead, it writes every frame into the video and tells the videos to play at a fixed frame rate. So even if it takes seconds to calculate just one frame, the output video will play smoothly without showing any delay between frames.
However, there is one caveat though. This method only works with Chrome.
As you specified in the comments that a gif would also work, here is a solution:
Below is a sample p5 sketch that records canvas animation and turns it into a gif, using gif.js.
Works in browsers supporting: Web Workers, File API and Typed Arrays.
I've provided this code so you can get an idea of how to use this library because not much documentation is provided for it and I had a hard time myself figuring it out.
var cnv;
var gif, recording = false;
function setup() {
cnv = createCanvas(400, 400);
var start_rec = createButton("Start Recording");
start_rec.mousePressed(saveVid);
var stop_rec = createButton("Stop Recording");
stop_rec.mousePressed(saveVid);
start_rec.position(500, 500);
stop_rec.position(650, 500);
setupGIF();
}
function saveVid() {
recording = !recording;
if (!recording) {
gif.render();
}
}
var x = 0;
var y = 0;
function draw() {
background(51);
fill(255);
ellipse(x, y, 20, 20);
x++;
y++;
if (recording) {
gif.addFrame(cnv.elt, {
delay: 1,
copy: true
});
}
}
function setupGIF() {
gif = new GIF({
workers: 5,
quality: 20
});
gif.on('finished', function(blob) {
window.open(URL.createObjectURL(blob));
});
}
More Info :
This sketch starts recording frames when you click start_rec and stops when you hit stop_rec, in your sketch you might want to control things differently, but keep in mind that addFrame only adds one frame to the gif so you need to call it in the draw function to add multiple frames, you can pass in an ImageElement, a CanvasElement or a CanvasContext along with other optional parameters.
In the gif.on function, you can specify a callback function to do whatever you like with the gif.
If you want to fine tune settings of the gif, like quality, repeat, background, you can read more here. Hope this helps!
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.
Long story short in another portion of the program I make canvases, convert them to DataURLs, then pass them over to the following portion to use as the icon image of the buttons. Whenever I set this.icon = "/path/to/image.jpg", it pulls it correctly, but since these images are not on disk, I am unsure how to
arrowHandler: function (arrow) {
var list = [];
var library = Ext.getCmp("library");
var buttons = Ext.getCmp("numbered").menu.buttons; //where the dataURLs are pushed in another portion of the program
function btn(num) {
var image = new Image;
image.src = buttons[num].dataURL;
this.xtype = "button";
this.height = 50;
this.width = 50;
this.icon = image; //where putting an actual path works correctly, but this code doesn't
this.num = num;
this.handler = function (btn) {
btn.up("button").menu.Style = this.num;
btn.up("button").fireEvent("selected", this.num);
};
}
for (var i = 0; i <= 0; i++)
library.items.items.push(new btn(i));
},
I am aware the loop is only going thru index 0 - it's like that purposefully for testing.
SOLUTION
The selected correct answer did provide the right way to set the icon with a DataURI, but it wasn't the fix to my issue. Turns out instead of doing
library.items.items.push(new btn(i));
I needed to be doing
library.add(new btn(i));
The error I kept encountering with pushing was "c.render() is not a function". I mention that solely to make it hopefully searchable for anyone else who encounters that error.
Should be the same as data uri, you'll have to convert it first.
https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL
var dataURL = canvas.toDataURL();
Here is a button fiddle:
https://fiddle.sencha.com/#view/editor&fiddle/1og6
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!