User-friendly image saving from the canvas

After the previous few canvas exercises, I wanted a simple way to allow users to save images from a canvas tag to their local machine. Ideally, without any server-side dependencies. While the whole right-click-the-canvas-and-open-a-png-in-a-new-tab is a handy pro-tip, I really wanted a nicer and easy user experience. Something I wouldn’t have to explain to my mom how to use. I figured there were a few solutions out there, so I began poking around. I ended up with something of my own :)

For starters, it’s really simple to get the bytecode for an image from the canvas tag. No magic necessary; canvas.toDataURL() to the rescue. This function accepts a MIME-type as an optional parameter (think: ‘image/png’), and returns a gianormous base64 encoded string containing all the pixel data.

Some quick google searches revealed that you can take this string and change its MIME-type to trick the browser into thinking it is a file for download. Some of the more popular canvas-to-image utilities (mainly the aptly named canvas2image) do this. While canvas2image is a lot more robust (it let’s you save to a bunch of different image formats), the whole gist of it’s approach focuses on client side mime-type switching like:

var data = canvas.toDataURL("image/png").replace("image/png", "image/octet-stream");
window.location.href = data;

This is sooooo close to what I was after! However, I just can’t get down with not being able to set a filename for the downloaded file. Safari and Chrome do a decent job of not freaking out the user by saving the files with the name ‘Download’. No file extension or anything, but it’s readable. However, I knew I needed a different solution after testing out how FireFox handled it:

Yikes. To the average user, it looks like this thing is about to eat their hard drive! What the heck is '...z6zSHmu/vFec+...'?! Well, us smarties know its just the image bytecode, but the average joe would be left wondering if it’s time to cancel that operation. It would be much better to have it say something like mypicture.png.

Needless to say, image bytecode is orders of magnitude above what I am able to comprehend. After laughably trying in vain to pass a filename into the byte-code string, I gave up on a client-side only approach. Since my hackish toying, I don’t even think file names are stored in the byte-code. I ran a diff of the byte code between a file called ‘Download’ and a close renamed ‘Download.png’ and both files were exactly the same.

Anyway, to the server side we go! I kludged together simple php script to accept the bytecode string from the toDataURL() function, and decode it back into an image. All while setting the files’ MIME type as an image, making it a download, and giving it a custom filename. It looks like this:

<?php
	# we are a PNG image
	header('Content-type: image/png');
	
	# we are an attachment (eg download), and we have a name
	header('Content-Disposition: attachment; filename="' . $_POST['name'] .'"');
	
	#capture, replace any spaces w/ plusses, and decode
	$encoded = $_POST['imgdata'];
	$encoded = str_replace(' ', '+', $encoded);
	$decoded = base64_decode($encoded);
	
	#write decoded data
	echo $decoded;
?>

Ok. Server side down. Let’s move it back to the client side. Enter the CanvasSaver object. Now we can more easily implement the client side portion of this process.

function CanvasSaver(url) {
	
	this.url = url;
	
	this.savePNG = function(cnvs, fname) {
		if(!cnvs || !url) return;
		fname = fname || 'picture';
		
		var data = cnvs.toDataURL("image/png");
		data = data.substr(data.indexOf(',') + 1).toString();
		
		var dataInput = document.createElement("input") ;
		dataInput.setAttribute("name", 'imgdata') ;
		dataInput.setAttribute("value", data);
		dataInput.setAttribute("type", "hidden");
		
		var nameInput = document.createElement("input") ;
		nameInput.setAttribute("name", 'name') ;
		nameInput.setAttribute("value", fname + '.png');
		
		var myForm = document.createElement("form");
		myForm.method = 'post';
		myForm.action = url;
		myForm.appendChild(dataInput);
		myForm.appendChild(nameInput);
		
		document.body.appendChild(myForm) ;
		myForm.submit() ;
		document.body.removeChild(myForm) ;
	};
	
	this.generateButton = function (label, cnvs, fname) {
		var btn = document.createElement('button'), scope = this;
		btn.innerHTML = label;
		btn.style['class'] = 'canvassaver';
		btn.addEventListener('click', function(){scope.savePNG(cnvs, fname);}, false);
		
		document.body.appendChild(btn);
		
		return btn;
	};
}

When creating a new CanvasSaver, I pass in the URL to the saveme.php file. Then, I generate a simple button to allow a user to save a PNG from the canvas with the generateButtion() method. I could skip the button generation, and opt for manually calling the savePNG function arbitrarily, but the button is nice enough.

Here is an example of implementing the CanvasSaver in a project:

var cs = new CanvasSaver('http://greenethumb.com/canvas/lib/saveme.php')
cs.generateButton('save an image!', cnvs, 'myimage');

And how about a working demo:



Hopefully, I’ll be refining this over the next few days. It needs more work on the implementation side of things, but it’s working. And it seems more user-friendly then client-side only saving. Hopefully you find this useful!

It’s been a good experiment. When exploring a new technology, reinventing the wheel is underrated

Update: Fork, clone and push away on github: CanvasSaver

44 Responses to 'User-friendly image saving from the canvas'

  1. Nice stuff Jon! Very interesting and cool. Definitely worth re-inventing the wheel to learn a new technology.

  2. Solenoid says:

    The part about no file naming on client side is a real deal breaker for me, but since you went ahead and did the research on this I won’t have to, thanks.

  3. ANNA says:

    Thanks very match.. This is exactly what I was lookig for!!!

  4. GrooN says:

    Hi, interesting article.
    It’s a shame that there isn’t a way to let the user download it client-side. Sending all the data to the host before being able to download it is a waste of, not only bandwidth, but also time for the client.

  5. João says:

    Thanks!

  6. Delapouite says:

    Hi Jonathan.

    Did you have a look at the new download attribute introduced recently in Chrome that let you choose the file name?

    http://html5-demos.appspot.com/static/a.download.html

  7. Jacques says:

    Found the error
    In your php it says
    header(‘Content-type: image/png’);
    but i’m sending jpegs so ive changed ot to
    header(‘Content-type: image/jpg’);
    now it works
    Awesome

  8. Jacques says:

    No it was not that sorry. still have the same problem Jon

  9. Bob says:

    Great stuff!

  10. Russ says:

    This is exactly what I’m after! But is there a way of getting something like an Adobe Edge animation or even Flash to sit in the canvas and capture that?

  11. Russ says:

    This is exactly what I was looking for! But is there a way of placing an Adobe Edge animation or even Flash on the canvas and capturing that?

  12. duncan says:

    Help – File stops at 393,216 bytes ???

    That’s really really good. one thing though – after experimenting with this, if i make the canvas bigger (or the image way more complex) the file always stops saving at exactly 393,216 bytes. if this is enough to include the whole image then I have a working file – if the image is more than 393,216 bytes then i only get some of a file and it’s not usable (for example, on my mac I can see in the bounds of the file in the quicklook preview but the bottom whatever % will be blank because its stopped at 393,216 bytes). Any ideas why this would be ????

  13. Erick says:

    So I made a little program where you can take multiple pictures thanks to “getusermedia” using chrome. Im at the point where i try to save the images taken using “” etc.. YES, an array… the problem is that php throws a $_FILES error 4 which is: “No File uploaded”… I could let $_POST handle the encoded data, i just need to add more data to the post_max_limit in the php.ini but i heard that it can be a bad idea because DOS attacks.. so please point a finger … How do I save an array of images to php? thank you..

  14. Kendall says:

    Doesn’t work on Android 2.3 stock browser

  15. Djordje says:

    This .php script doesn’t work for me. When I use
    var cs = new CanvasSaver(‘http://greenethumb.com/canva/lib/saveme.php&#8217;) everything works fine, but when I download code for script and use
    var cs = new CanvasSaver(‘saveme.php’)I can’t open the image.

  16. Igor says:

    Great job, man! Very useful stuff. Tnx!

  17. knospe says:

    First of all, thank you Jonathan for this great example.

    I have been playing around with fabric JavaScript library and have used your example to save canvas as an image. I have stumbled upon a problem that was already described by duncan and here is my solution for it;

    To get to save the image larger than 393,216 B in Chrome, set the type of the input for imgdata to “hidden” -> in canvassaver.js by

    var dataInput = document.createElement(“input”);
    dataInput.setAttribute(“name”, ‘imgdata’);
    dataInput.setAttribute(“value”, data);

    set value for the “type” attribute -> dataInput.setAttribute(“type”, ‘hidden’);

    If this attribute is not set, the default value “text” is used. Each browser has a different limitation on this input type. That is why for example image that was saved looks ok in Firefox, but in Chrome gets like cut off at the bottom.

    I hope this helps.

  18. Ingmar says:

    Hi Jonathan,

    Thank you very much for this! I’m really happy I found your script, this was exactly what I was looking for!

    One question: your script doesn’t seem to work on Android. The image is saved, but it’s empty (0 bytes).

    Might you know how this can be solved?

    With kind regards,

    Ingmar

  19. Borja says:

    Awesome stuff!!Very great examples :)

  20. Jonathan Greene says:

    Hey Guys, not sure why I never linked this before. But I just updated the article with the git hub repo address. Thanks!

  21. Thanks for this great article. I encountered a gotcha that confounded me for hours that I’ll pass along. Initially, the script wasn’t working for “large” images …. so I checked, and re-checked my httpd.conf and php.ini for the ususal suspects :
    * LimitRequestBody
    * upload_max_filesize
    * post_max_size

    All of them were set to large thresholds, and still, I was getting a blank $_POST array even though the data was correctly posting from the browser to my server according firebug.

    Turns out the issue was the suhosin security settings. So, for those running php with the suhosin security patch (pretty much all of you), you’ll need to take a look at suhosin.ini (debian -> /etc/php5/conf.d/suhosin.ini). You’re looking for the following field :

    ; suhosin.post.max_value_length = 1000000

    That limits the value of any non-file-upload post variable (such as content in an input field) to under 1MB. My canvas “screenshot” data was about 1.2MB of base64 encoded data, which exceeded that default threshold. After adjusting the threshold upward (to 3 MB), my problems diappeared! Hope this helps someone…

  22. spacestud says:

    The script is really nice !, saved me :)

  23. ppyyo says:

    While trying the example locally (.js and .php files in the same folder as example.html) with firefox I couldn’t save the image but the saveme.php. Could you anyone tell me why? Has this to do with permissions? Thanks in advance.
    Regards.

  24. Ivo Ilic says:

    I keep getting this warning in the JS console
    “Resource interpreted as Document but transferred with MIME type image/png”
    Anyone know how I can get rid of this/ what this is?

  25. Madan says:

    Thank Jonathan for great solution and knospe for helping to save large files saved life

  26. John says:

    Just what I needed for a project I am working on. Excellent article and code. Thanks.

  27. calzinispaiati says:

    how do I give a color background to the canvas?

    thanks

    • calzinispaiati says:

      Is there a way to save it in jpg? I can not change the color to the canvas, or rather it can change, but when it does the screenshot, reads another canvas generated by a js that can not be edited. I think I would save it in jpg chance to have a white background.

      Thanks

  28. Fred says:

    Thank you! This has worked so far (in Chrome)–a relief to be able to save the images with a halfway-decent filename.

  29. okta via says:

    thank you ….
    This is really what I needed and was very helpful to me.

  30. Laura says:

    Very nice!

    I found it preferable, myself, to replace the this.generateButton code with this:

    this.generateEvent = function(cnvs, fname) { this.savePNG(cnvs, fname); };

    so that I could call it directly from anywhere in the javascript:

    var cs = new CanvasSaver(‘saveme.php’)
    cs.generateEvent($(“#canvas”)0, ‘canvas’);

    • Laura says:

      Note that where the blog software has interpreted a footnote, there is supposed to be an array index of 0. Not that it really matters; do it however you normally reference your canvas.

  31. Akash says:

    Finally got a perfect script i wanted. Thanks.

  32. arie says:

    I don’t know how to integrate this code with Google chart API code..PLEASE SOMEBODY..!!!!HELP ME…!! T___T

  33. Echeban says:

    Can you write the PHP code in C# ?
    I wrote a JS script inside a ASP.NET page using WebGL to create an image.
    I am able to save it as PNG but it does not have the extension PNG (same problem everybody is having) so my users are freaking out about this extensionless file being saved in their client.
    I do not know PHP and even less how to insert PHP code inside C#
    Please help me.

  34. Tejas Suthar says:

    Thanks man!
    It’s working perfect as expected. Earlier I did exported with canvas2Image library. But it was not working with safari.

    Now its working with safari as well.

    Many thanks for your library. It’s just working awesome.

    - Tejas Suthar

  35. Somehow I can’t get this example to work. The data is sent to PHP, then the PNG downloads, but when finished, the PNG file is 0kb.

  36. google says:

    Kimmal and outlines basic but seldom know procedures that
    avoid intense SEO. They might keep visiting it over and over
    or a lot of them might mention it to their friends
    and relatives. ‘ Voicemail Transcription – Keeps a log and reads what
    the voicemail says.

Trackbacks/Pingbacks
  1. [...] User-friendly image saving from the canvas – greenethumb Share this:TwitterFacebook This entry was posted in Uncategorized by zeezali52. Bookmark the permalink. [...]

  2. [...] Special thanks to: Lars Jung, for the js QR Generator and CanvasSaver by greenethumb. [...]

Leave a Reply

Your email address will not be published. Required fields are marked *

*