image smaller than the original on canvas to make drawImage() - javascript

I am developing a system for image filter with html5 canvas, however, as I am at the beginning I have emerged me some doubts and mistakes.
Here's what I've developed so far:
$(document).ready(function() {
$("#uploadImagem").change(function(e) {
var _URL = window.URL || window.webkitURL,
arquivo = e.target.files[0],
tipoImagem = /image.*/,
reader,
imagem;
if(!arquivo.type.match(tipoImagem)) {
alert("Somente imagens são aceitas!");
return;
}
imagem = new Image();
imagem.onload = function() {
if(this.width > 600 || this.height > 400) {
alert("Somente imagens com largura máxima de 600px e altura máxima de 400px");
return;
} else {
$("#filtrarImagem").width(this.width).height(this.height);
}
};
imagem.src = _URL.createObjectURL(arquivo);
reader = new FileReader();
reader.onload = fileOnload;
reader.readAsDataURL(arquivo);
});
function fileOnload(e) {
var $img = $("<img>", {src: e.target.result}),
canvas = $("#filtrarImagem")[0],
context = canvas.getContext("2d");
$img.load(function() {
context.drawImage(this, 0, 0);
});
}
});
When I do imagem.onload... I wish, if the image pixels were greater than 600 and 400 he gave the alert and stop there, but even so the image appears on the canvas.
In the same imagem.onload... function, if the image pixels correspond to the canvas required (id = "filtrarImagem") is the size of the image, but the image to the canvas becomes smaller than the original uploaded, and she was to occupy all the canvas and get the original size.
How to continue?

You have two separate handlers for image loading. There is no need to set two image sources with the same URL, just reuse a single image for both checking size and setting canvas size as well as draw it into the canvas.
I would suggest the following steps (you tagged the question with jQuery but I would highly recommend working in vanilla JavaScript when you work with non-DOM oriented elements such as canvas).
Example
if (typeof window.URL === "undefined") window.URL = window.webkitURL;
$("input")[0].onchange = function(e) {
var arquivo = e.target.files[0],
imagem = new Image();
if (!arquivo.type.match(/image.*/)) {
alert("Somente imagens são aceitas!");
return;
}
// STEP 1: load as image
imagem.onload = doSetup;
imagem.src = URL.createObjectURL(arquivo)
};
// STEP 2: now that we have the image loaded we can check size and setup canvas
function doSetup() {
URL.revokeObjectURL(this.src); // clean up memory for object-URL
if (this.width > 600 || this.height > 400) {
alert("Too big!");
return; // exit!
}
// image OK, setup canvas
var c = $("canvas")[0]; // work with the element itself, not the jQuery object
c.width = this.width; // important: use canvas.width, not style or css
c.height = this.height;
// draw in image
var ctx = c.getContext("2d");
ctx.drawImage(this, 0, 0);
// NEXT: ... from here, invoke filter etc.
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input type="file"><br><canvas></canvas>

The jQuery width() and height() functions use CSS to change the size of the canvas which does not do what you would expect it to do. Change the width and height attributes of the canvas element directly.
The reason why your check for the size does not prevent the image from being drawn, is because your check is in the handler for imagem.onload in $("#uploadImagem").change while the drawing happens in $img.onload assigned from reader.onload. These two events are processed independently. One of them returning prematurely does not prevent the other from being executed.

Related

Firefox drawing blank Canvas when restoring from saved base64

I am working with a single canvas that allows the user to click on a window pane in a window image. The idea is to show where the user has clicked. The image will then be modified (by drawing a grill on the window) and then saved to in JPEG. I am saving the canvas image prior to the click function because I don't want the selection box to show in the final image. However, Firefox often displays a blank canvas when restoring the canvas where IE and Chrome do not. This works perfectly in Chrome and IE. Any suggestions? Does Firefox have a problem with toDataURL()? Maybe some async issue going on here? I am also aware that saving a canvas in this fashion is memory intensive and there may be a better way to do this but I'm working with what I have.
Code:
/**
* Restores canvas from drawingView.canvasRestorePoint if there are any restores saved
*/
restoreCanvas:function()
{
var inverseScale = (1/drawingView.scaleFactor);
var canvas = document.getElementById("drawPop.canvasOne");
var c = canvas.getContext("2d");
if (drawingView.canvasRestorePoint[0]!=null)
{
c.clearRect(0,0,canvas.width,canvas.height);
var img = new Image();
img.src = drawingView.canvasRestorePoint.pop();
c.scale(inverseScale,inverseScale);
c.drawImage(img, 0, 0);
c.scale(drawingView.scaleFactor, drawingView.scaleFactor);
}
},
/**
* Pushes canvas into drawingView.canvasRestorePoint
*/
saveCanvas:function()
{
var canvas = document.getElementById("drawPop.canvasOne");
var urlData = canvas.toDataURL();
drawingView.canvasRestorePoint.push(urlData);
},
EXAMPLE OF USE:
readGrillInputs:function()
{
var glassNum = ir.get("drawPop.grillGlassNum").value;
var panelNum = ir.get("drawPop.grillPanelNum").value;
drawingView.restoreCanvas();
drawEngine.drawGrill(glassNum, panelNum,null);
drawingView.saveCanvas();
},
sortClick:function(event)
{
..... //Sorts where user has clicked and generates panel/glass num
.....
drawingView.showClick(panelNum, glassNum);
},
showClick:function(panelNum, glassNum)
{
var glass = item.panels[panelNum].glasses[glassNum];
var c = drawEngine.context;
drawingView.restoreCanvas();
drawingView.saveCanvas();
c.strokeStyle = "red";
c.strokeRect(glass.x, glass.y, glass.w, glass.h);
},
By just looking at the code setting the img.src is an async action to retrieve the image, so when you try to draw it 2 lines later to the canvas, it probably hasn't been loaded yet (having it in cache will make it return fast enough that it might work).
You should instead use an img.onload function to draw the image when it has loaded.
restoreCanvas:function()
{
var inverseScale = (1/drawingView.scaleFactor);
var canvas = document.getElementById("drawPop.canvasOne");
var c = canvas.getContext("2d");
if (drawingView.canvasRestorePoint[0]!=null)
{
c.clearRect(0,0,canvas.width,canvas.height);
var img = new Image();
img.onload = function() {
c.scale(inverseScale,inverseScale);
c.drawImage(img, 0, 0);
c.scale(drawingView.scaleFactor, drawingView.scaleFactor);
};
img.src = drawingView.canvasRestorePoint.pop();
}
},

How should I crop an image at client side using jcrop and upload it?

I am working on a component in which there is file-upload HTML control, upon selecting an image using the file-upload element, the image would be rendered on the HTML5 Canvas element.
Here is JSFiddle with sample code: https://jsfiddle.net/govi20/spmc7ymp/
id=target => selector for jcrop element
id=photograph => selector for fileupload element
id=preview => selector for canvas element
id=clear_selection => selector for a button which would clear the canvas
Third-party JS libraries used:
<script src="./js/jquery.min.js"></script>
<script src="./js/jquery.Jcrop.js"></script>
<script src="./js/jquery.color.js"></script>
Setting up the JCrop:
<script type="text/javascript">
jQuery(function($){
var api;
$('#target').Jcrop({
// start off with jcrop-light class
bgOpacity: 0.5,
keySupport: false,
bgColor: 'black',
minSize:[240,320],
maxSize:[480,640],
onChange : updatePreview,
onSelect : updatePreview,
height:160,
width:120,
addClass: 'jcrop-normal'
},function(){
api = this;
api.setSelect([0,0,240,320]);
api.setOptions({ bgFade: true });
api.ui.selection.addClass('jcrop-selection');
});
});
clear canvas event which will be triggered on clear button click event:
jQuery('#clear_selection').click(function(){
$('#target').Jcrop({
setSelect: [0,0,0,0],
});
});
code that renders image on HTML5 Canvas:
function readURL(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
$('#target').attr('src', e.target.result);
setProperties();
}
reader.readAsDataURL(input.files[0]);
}
}
function setProperties(){
$('#target').Jcrop({
setSelect: [0,0,240,320]
});
}
$("#photograph").change(function(){
readURL(this);
});
code to crop and render an image on the canvas:
var canvas = document.getElementById('preview'),
context = canvas.getContext('2d');
make_base();
function updatePreview(c) {
console.log("called");
if(parseInt(c.w) > 0) {
// Show image preview
var imageObj = $("#target")[0];
var canvas = $("#preview")[0];
var context = canvas.getContext("2d");
context.drawImage(imageObj, c.x, c.y, c.w, c.h, 0, 0, canvas.width, canvas.height);
}
};
function make_base() {
console.log("make_base called");
var base_image = new Image();
base_image.src = '';
base_image.onload = function () {
context.drawImage(base_image, 0, 0);
}
}
Here are a bunch of issues I am facing with the above setup:
updatePreview function is not getting called on selection, hence the canvas is not getting rendered.
crop selection box is not draggable (I am using bootstrap CSS, I suspect it is due to missing/mismatching dependency).
Canvas is HTML5 element, which means the end-user must have an HTML5 compatible browser, I am working on an app that has millions of users. Forcing users to use the latest browser is not a feasible option. What should be the fallback mechanism here?
Here's basic html 5 code:
https://jsfiddle.net/zm7e0jev/
This code crops the image, shows a preview and sets the value of an input element to the base64 encoded cropped image.
You can fetch the image file in php the following way:
//File destination
$destination = "/folder/cropped_image.png";
//Get convertable base64 image string
$image_base64 = $_POST["png"];
$image_base64 = str_replace("data:image/png;base64,", "", $image_base64);
$image_base64 = str_replace(" ", "+", $image_base64);
//Convert base64 string to image data
$image = base64_decode($image_base64);
//Save image to final destination
file_put_contents($destination, $image);
Submitting base64 image string as a post variable has it's server post size limits and base64 encoding makes the cropped image file size even bigger (~33%) then the raw data of the cropped image would be which makes the upload take even longer.
To set the post size limit: What is the size limit of a post request?
Keep in mind that an increased post size limit can be abused for a DoS attack as example.
Instead I suggest converting the base64 cropped image to a data blob and then add it to the form on submit as a file:
https://jsfiddle.net/g3ysk6sf/
Then you can fetch the image file in php the following way:
//File destination
$destination = "/folder/cropped_image.png";
//Get uploaded image file it's temporary name
$image_tmp_name = $_FILES["cropped_image"]["tmp_name"][0];
//Move temporary file to final destination
move_uploaded_file($image_tmp_name, $destination);
Update:
FormData() is only partially support in IE10 and not supported in older versions of IE
So I suggest sending the base64 string as a fallback, though this will cause problems with bigger images so it needs to check the filesize and show an error popup when the image is above a specific size.
I'll post an update with the fallback code below when I've got it working.
Update 2:
I added a fallback for IE10 and below:
https://jsfiddle.net/oupxo3pu/
The only limitation is the image size that can be submitted when using IE10 and below, in case the image size is too big the js code will throw an error. The maximum size to work for post values is different between each server, the js code has a variable to set the maximum size.
The php code below is adapted to work with above fallback:
//File destination
$destination = "/folder/cropped_image.png";
if($_POST["png"]) {//IE10 and below
//Get convertable base64 image string
$image_base64 = $_POST["png"];
$image_base64 = str_replace("data:image/png;base64,", "", $image_base64);
$image_base64 = str_replace(" ", "+", $image_base64);
//Convert base64 string to image data
$image = base64_decode($image_base64);
//Save image to final destination
file_put_contents($destination, $image);
} else if($_FILES["cropped_image"]) {//IE11+ and modern browsers
//Get uploaded image file it's temporary name
$image_tmp_name = $_FILES["cropped_image"]["tmp_name"][0];
//Move temporary file to final destination
move_uploaded_file($image_tmp_name, $destination);
}
There is no fallback code for the canvas element yet, I'm looking into it.
The post size limitation in the fallback for older browsers is one of the reasons I dropped support for older browsers myself.
Update 3:
The fallback I recommend for the canvas element in IE8:
http://flashcanvas.net/
It supports all the canvas functions the cropping code needs.
Keep in mind it requires flash. There is a canvas fallback (explorercanvas) that does not require flash but it does not support the function toDataURL() which we need to save our cropped image.
Seahorsepip's answer is fantastic. I made a lot of improvements on the non-fallback answer.
http://jsfiddle.net/w1Lh4w2t/
I would recommend not doing that strange hidden png thing, when an Image object works just as well (so long as we're not supporting fallbacks).
var jcrop_api;
var canvas;
var context;
var image;
var prefsize;
Though even then we are, you're better off getting that data out of the canvas at the end and putting it in that field only at the end.
function loadImage(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
image = new Image();
image.src = e.target.result;
validateImage();
}
reader.readAsDataURL(input.files[0]);
}
}
But, if you want more functions than just crop, if we attach the jcrop to an inserted canvas (which we destroy with the jcrop on refresh). We can easily do anything we can do with a canvas, then validateImage() again and have the updated image visible in place.
function validateImage() {
if (canvas != null) {
image = new Image();
image.src = canvas.toDataURL('image/png');
}
if (jcrop_api != null) {
jcrop_api.destroy();
}
$("#views").empty();
$("#views").append("<canvas id=\"canvas\">");
canvas = $("#canvas")[0];
context = canvas.getContext("2d");
canvas.width = image.width;
canvas.height = image.height;
context.drawImage(image, 0, 0);
$("#canvas").Jcrop({
onSelect: selectcanvas,
onRelease: clearcanvas,
boxWidth: crop_max_width,
boxHeight: crop_max_height
}, function() {
jcrop_api = this;
});
clearcanvas();
}
Then on submit we submit any pending operations, like applyCrop() or applyScale(), adding data into hidden fields for fallback stuff, if we have those things needed. We then have a system we can easily just modify the canvas, in any way, then when we submit the canvas data gets sent properly.
function applyCrop() {
canvas.width = prefsize.w;
canvas.height = prefsize.h;
context.drawImage(image, prefsize.x, prefsize.y, prefsize.w, prefsize.h, 0, 0, canvas.width, canvas.height);
validateImage();
}
The canvas is added to a div views.
<div id="views"></div>
To catch the attached file in PHP (drupal), I used something like:
function makeFileManaged() {
if (!isset($_FILES['croppedfile']))
return NULL;
$path = $_FILES['croppedfile']['tmp_name'];
if (!file_exists($path))
return NULL;
$result_filename = $_FILES['croppedfile']['name'];
$uri = file_unmanaged_move($path, 'private://' . $result_filename, FILE_EXISTS_RENAME);
if ($uri == FALSE)
return NULL;
$file = File::Create([
'uri' => $uri,
]);
$file->save();
return $file->id();
}

Add image to HTML5 canvas on file upload issue

Allright, this is going to be quite hard to explain but I will try to explain my problem as clear as possible.
I currently have a HTML5 Canvas with a couple of preloaded shapes which I changed to images. I am using a modified version of this example. On the canvas I have now I can drag, drop and select the shapes which are preloaded.
I have implemented an option to upload a new image to the canvas using this:
var imageLoader = document.getElementById('uploader');
imageLoader.addEventListener('change', handleImage, false);
var canvas = document.getElementById('canvas1');
var ctx = canvas.getContext('2d');
function handleImage(e){
var reader = new FileReader();
reader.onload = function(event){
var imgNew = new Image();
imgNew.onload = function(){
ctx.drawImage(imgNew, 0, 0);
}
imgNew.src = event.target.result;
}
reader.readAsDataURL(e.target.files[0]);
}
This will now just put an image in the top-left corner of the canvas and when I select another shape this image will disappear. All shapes that are already in the canvas are in an array called 'shapes'. In the example there is an option to add a new shape when you dubbelclick on the canvas, this is the code:
// double click for making new shapes
canvas.addEventListener('dblclick', function(e) {
var mouse = myState.getMouse(e);
myState.addShape(new Shape(mouse.x - 10, mouse.y - 10, 20, 20, 'rgba(0,255,0,.6)'));
}, true);
This is the addShape part which is called when double clicking:
CanvasState.prototype.addShape = function(shape) {
this.shapes.push(shape);
this.valid = false;
}
What I am trying to do is when I upload an image using the file upload the image should be added to the 'shapes' array so I can also drag,drop and select the uploaded image. (basically I need to do the same with the uploaded image as I can do with the preloaded ones) My guess is that it should be done something like the doubleclick method and addShape but I can't get this to work.
A working version of my current version can be found here:
http://codepen.io/anon/pen/qLuCy/
If anyone knows how I can get this to work it would be great!
first the result, here : http://codepen.io/anon/pen/qBmnC/
Your shape object can only draw one single hard-coded image, so change this and have shape be either a color or an image. In fact for this you just have to change shape.draw since the 'type' of the fill does not matter in the constructor.
So just test the fill and draw accordingly :
// Tekent de Shape
Shape.prototype.draw = function(ctx) {
var locx = this.x;
var locy = this.y;
var fill = this.fill ;
if (typeof fill == 'string' )
{
ctx.fillStyle = fill;
ctx.fillRect(locx, locy, this.w, this.h);
}
else
{
ctx.drawImage(fill, locx, locy, this.w, this.h);
}
}
then on image load i just add a shape that has this image as fill :
function handleImage(e){
var reader = new FileReader();
reader.onload = function(event){
var imgNew = new Image();
imgNew.onload = function(){
s.addShape(new Shape(60,60,imgNew.width,imgNew.height,imgNew));
}
imgNew.src = event.target.result;
}
reader.readAsDataURL(e.target.files[0]);
}
I had to put the shape collection, s, as a global var.
You can also see that i used the same scheme in init() to
add an image once it's loaded.

INDEX_SIZE_ERR when drawImage on canvas

I need to draw an Image object to a canvas but I've got an INDEX_SIZE_ERR exception in Firefox and IE10 but not in Chrome nor Safari...
According to the W3C: If one of the sw or sh arguments is zero, the implementation must raise an INDEX_SIZE_ERR exception..
Here is the code that causes the problem:
function writePhotoOnCanvas(data, width, height) {
// Get the canvas
var canvasGallery = document.getElementById("canvasGallery");
// Clear the canvas
canvasGallery.width = canvasGallery.width;
// Get its context
var ctxCapture = canvasGallery.getContext("2d");
// Create an image in order to draw in the canvas
var img = new Image();
// Set image width
img.width = width;
// Set image height
img.height = height;
// To do when the image is loaded
img.onload = function() {
console.log("img.width="+img.width+", img.height="+img.height);
console.log("width="+width+", height="+height+", canvasGallery.width="+canvasGallery.width+", canvasGallery.height="+canvasGallery.height);
// Draw the picture
try {
ctxCapture.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvasGallery.width, canvasGallery.height);
} catch(e) {
console.error(e.message);
}
};
// Set image content from specified photo
img.src = data;
}
The console shows:
img.width=640, img.height=480
width=640, height=480, canvasGallery.width=589, canvasGallery.height=440
Index or size is negative or greater than the allowed amount
What is the source of the problem?
Thanks
You are manually setting the width and height of the image (img.width = width; img.height = height;). I don't really understand why you are doing this, but it is likely unnecessary.
These should be calculated automatically from the image data you load. Try to remove them to see what the actual size of the data is.
The image width and height are read-only properties so they will cause the code to break in some browser.
If you absolutely want to set the width and height before loading the image you can do:
var img = new Image(width, height);
Then you can read img.width and img.height when image has loaded (or read img.naturalWidth and img.naturalHeight to get the original dimension).
There is no need though to do this. Simply call your drawImage() like this:
ctxCapture.drawImage(img, 0, 0, canvasGallery.width, canvasGallery.height);
This will use the full dimension of the image and scale it to the canvasGallery's dimension.
Tip: If you are using this function to load several images you will want to exchange img with this inside your onload handler.
Modified code:
function writePhotoOnCanvas(data, width, height) {
var canvasGallery = document.getElementById("canvasGallery");
var ctxCapture = canvasGallery.getContext("2d");
/// setting width to clear does not work in all browser, to be sure:
ctxCapture.clearRect(0, 0, canvasGallery.width, canvasGallery.height);
var img = new Image();
img.onload = function() {
// Draw the picture
try {
ctxCapture.drawImage(this, 0, 0,
canvasGallery.width, canvasGallery.height);
} catch(e) {
console.error(e.message);
}
};
// Set image content from specified photo
img.src = data;
}

jQuery and Canvas loading behaviour

After being a long time lurker, this is my first post here! I've been RTFMing and searching everywhere for an answer to this question to no avail. I will try to be as informative as I can, hope you could help me.
This code is for my personal webpage.
I am trying to implement some sort of a modern click-map using HTML5 and jQuery.
In the website you would see the main image and a hidden canvas with the same size at the same coordinates with this picture drawn into it.
When the mouse hovers the main picture, it read the mouse pixel data (array of r,g,b,alpha) from the image drawn onto the canvas. When it sees the pixel color is black (in my case I only check the RED value, which in a black pixel would be 0) it knows the activate the relevant button.
(Originally, I got the idea from this article)
The reason I chose this method, is for the page to be responsive and dynamically change to fit different monitors and mobile devices. To achieve this, I call the DrawCanvas function every time the screen is re-sized, to redraw the canvas with the new dimensions.
Generally, this works OK. The thing is ,there seems to be an inconsistent behavior in Chrome and IE(9). When I initially open the page, I sometimes get no pixel data (0,0,0,0), until i re-size the browser. At first I figured there's some loading issues that are making this happen so I tried to hack it with setTimeout, it still doesn't work. I also tried to trigger the re-size event and call the drawCanvas function at document.ready, still didn't work.
What's bothering me is most, are the inconsistencies. Sometimes it works, sometimes is doesn't. Generally, it is more stable in chrome than in IE(9).
Here is the deprecated code:
<script type="text/javascript">
$(document).ready(function(){setTimeout(function() {
// Get main image object
var mapWrapper = document.getElementById('map_wrapper').getElementsByTagName('img').item(0);
// Create a hidden canvas the same size as the main image and append it to main div
var canvas = document.createElement('canvas');
canvas.height = mapWrapper.clientHeight;
canvas.width = mapWrapper.clientWidth;
canvas.fillStyle = 'rgb(255,255,255)';
canvas.style.display = 'none';
canvas.id = 'hiddencvs';
$('#map_wrapper').append(canvas);
// Draw the buttons image into the canvas
drawCanvas(null);
$("#map_wrapper").mousemove(function(e){
var canvas = document.getElementById('hiddencvs');
var context = canvas.getContext('2d');
var pos = findPos(this);
var x = e.pageX - pos.x;
var y = e.pageY - pos.y;
// Get pixel information array (red, green, blue, alpha)
var pixel = context.getImageData(x,y,1,1).data;
var red = pixel[0];
var main_img = document.getElementById('map_wrapper').getElementsByTagName('img').item(0);
if (red == 0)
{
...
}
else {
...
}
});
},3000);}); // End DOM Ready
function drawCanvas(e)
{
// Get context of hidden convas and set size according to main image
var cvs = document.getElementById('hiddencvs');
var ctx = cvs.getContext('2d');
var mapWrapper = document.getElementById('map_wrapper').getElementsByTagName('img').item(0);
cvs.width = mapWrapper.clientWidth;
cvs.height = mapWrapper.clientHeight;
// Create img element for buttons image
var img = document.createElement("img");
img.src = "img/main-page-buttons.png";
// Draw buttons image inside hidden canvas, strech it to canvas size
ctx.drawImage(img, 0, 0,cvs.width,cvs.height);
}
$(window).resize(function(e){
drawCanvas(e);
}
);
function findPos(obj)
{
...
}
</script>
I'd appreciate any help!
Thanks!
Ron.
You don't wait for the image to be loaded so, depending on the cache, you may draw an image or not in the canvas.
You should do this :
$(function(){
var img = document.createElement("img");
img.onload = function() {
var mapWrapper = document.getElementById('map_wrapper').getElementsByTagName('img').item(0);
...
// your whole code here !
...
}
img.src = "img/main-page-buttons.png";
});

Categories

Resources