10

I have a problem with .toDataURL() for large canvas. I want to enconde in base64 and decode on php file but if I have a large canvas the strDataURI variable is empty.

My code:

var strDataURI = canvas.toDataURL();
strDataURI = strDataURI.substr(22, strDataURI.length);
$.post("save.php",
{ 
   str: strDataURI
};

Is there any alternative to .toDataURL() or some way to change the size limit?

Thanks.

ptCoder
  • 2,229
  • 3
  • 24
  • 38
  • Which browser, or is it all of them? IE has limits on the size of a data URL. – RichieHindle Apr 22 '13 at 20:48
  • What browser are you using, and what size does your canvas actually have? – Bergi Apr 22 '13 at 20:48
  • My browser is Google Chrome, but I have tested in others browsers. I don't know what is my canvas size, but the dimensions are 20000x20000 pixels. Thank You. – ptCoder Apr 22 '13 at 20:55
  • 4
    A 20000x20000 pixel image will result in a data URL about 2 gigabytes in size. *edit* wait, more like 4 gigabytes, as JavaScript strings use UTF-16. – Pointy Apr 22 '13 at 21:01
  • 1
    You might have better luck using a _Blob_ in this instance, rather than a data uri; [`HTMLCanvasElement.toBlob`](https://developer.mozilla.org/en-US/docs/DOM/HTMLCanvasElement#Methods) – Paul S. Apr 22 '13 at 21:08
  • Is there any other option? Like save image directly with jquery? The problem is because I have a canvas like 2000x2000 pixels, but I need to resize all canvas elements and pixels because I need to convert the image to 300 DPI (for example)... – ptCoder Apr 22 '13 at 21:39
  • I thnk that problem is related to size limit, like this http://stackoverflow.com/questions/6081483/maximum-size-of-a-canvas-element – ptCoder Apr 23 '13 at 13:42
  • Exporting the canvas content as SVG wouldn't have done it? Vectors can be rendered at any size... once it's done you simply output a PNG from the SVG when you need a real image. – Jeff Noel Jan 23 '15 at 18:29

4 Answers4

18

I'm not sure if there are limitation to canvas dimensions, but data urls have limitations depending on the browser: Data URL size limitations.

What you could try is using Node.js + node-canvas (server side) to recreate the canvas. I've been using these for creating printable images from canvas elements, and didn't have any problems/limitations using toDataURL so far.

Are you using the fabric.js library? I noticed you posted on their forum as well. Fabric.js can be used in Node.js and has a toDataURLWithMultiplier method, which scales the canvas/context allowing you to change the dataurl image size. You can check the method source to see how this is done.

Edit:

Since you're using fabric.js I would suggest using Node.js to handle the canvas to image processing on the server. You'll find more info on how to use fabric.js on Node.js here.

Here is a simple server using Node.js and express:

var express = require('express'),
    fs = require('fs'),
    fabric = require('fabric').fabric,
    app = express(),
    port = 3000;

var allowCrossDomain = function (req, res, next) {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'POST, OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type');
    next();
}

app.configure(function() {
    app.use(express.bodyParser());
    app.use(allowCrossDomain);
});

app.options('/', function(req, res) {
    res.send(200);
});

app.post('/', function(req, res) {
    var canvas = fabric.createCanvasForNode(req.body.width, req.body.height);
    
    console.log('> Loading JSON ...');
    canvas.loadFromJSON(req.body.json, function() {
        canvas.renderAll();
        
        console.log('> Getting PNG data ... (this can take a while)');
        var dataUrl = canvas.toDataURLWithMultiplier('png', req.body.multiplier),
            data = dataUrl.replace(/^data:image\/png;base64,/, '');
        
        console.log('> Saving PNG to file ...');
        var filePath = __dirname + '/test.png';
        fs.writeFile(filePath, data, 'base64', function(err) {
            if (err) {
                console.log('! Error saving PNG: ' + err);
                res.json(200, { error: 'Error saving PNG: ' + err });
            } else {
                console.log('> PNG file saved to: ' + filePath);
                res.json(200, { success: 'PNG file saved to: ' + filePath });
            }
        });
    });
});

app.listen(port);
console.log('> Server listening on port ' + port);

When the server is running you can send data to it (postData). The server expects json, width and height to recreate the canvas, and a multiplier to scale the data url image. The client side code would look something like this:

var postData = {
    json: canvas.toJSON(),
    width: canvas.getWidth(),
    height: canvas.getHeight(),
    multiplier: 2
};

$.ajax({
    url: 'http://localhost:3000',
    type: 'POST',
    contentType: 'application/json; charset=utf-8',
    data: JSON.stringify(postData),
    dataType: 'json',
    success: function(data) {
        console.log(data);
    },
    error: function(err) {
        console.log(err);
    }
});
Community
  • 1
  • 1
sn3p
  • 726
  • 4
  • 13
  • Thank You for your reply. Node.js only working in Unix servers, right? – ptCoder Apr 23 '13 at 16:09
  • 1
    You'll find Windows and MacOSX installers on the [Node.js download page](http://nodejs.org/download/) – sn3p Apr 23 '13 at 16:31
  • Yes. My project involves export very large canvas to jpg or png image. But I think that canvas don't support large dimensions. You recommend me any option? I'm working in small canvas like 1000x1000 (for example), then I need to scale the canvas object to converto to 300 DPI via Imagick. Thank You. – ptCoder Apr 23 '13 at 16:37
  • I've updated my answer with a server and client example. Hope it helps. – sn3p Apr 23 '13 at 20:38
4

You should first consider this: the size of the upload is limited. The limit depends on browser, OS and server environment. You can have a look at this article: http://www.motobit.com/help/scptutl/pa98.htm

In general you can try something like this: first we need a function to convert the dataURI to a blob:

function convertDataURItoBlob(dataURI) {
        'use strict'

        var byteString,
            mimestring

        if(dataURI.split(',')[0].indexOf('base64') !== -1 ) {
            byteString = atob(dataURI.split(',')[1])
        } else {
            byteString = decodeURI(dataURI.split(',')[1])
        }

        mimestring = dataURI.split(',')[0].split(':')[1].split(';')[0]


        var content = new Array();
        for (var i = 0; i < byteString.length; i++) {
            content[i] = byteString.charCodeAt(i)
        }
        var rawContent = new Uint8Array(content),
            returnBlob = new Blob([rawContent], {type: mimestring})

        return returnBlob;

}

and next a function for the upload of the file, using XMLHttpRequest2:

function upload(blob) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/yourServerEndPoint', true);
  xhr.onload = function(e) { ... };

  xhr.send(blob);
}

Now you can pass your strDataURI to the first function and then upload the file with the second function.

You can have a deeper look at XMLHTTPRequest2 here: http://www.html5rocks.com/en/tutorials/file/xhr2/ and about the blob constructor here: https://developer.mozilla.org/en-US/docs/DOM/Blob

Mimo
  • 6,015
  • 6
  • 36
  • 46
  • I don't need to upload. I need to convert my canvas in PNG. My problem is exporting canvas. For example: I have a canvas like 100x100 centimeters at 72 DPI resolution, but I need to resize to 300 dpi. The final canvas as about 30000x30000 pixels (100 centimeters * 300 is about 30000 pixels) and toDataURL function won't work. I thnk that problem is related to size limit, like this http://stackoverflow.com/questions/6081483/maximum-size-of-a-canvas-element Thank You for your reply. – ptCoder Apr 23 '13 at 13:41
2

You could always just break the image up into smaller sections and save those individually, which probably isn't a bad idea anyway. Basically you'd have a function that's something like

var largeCanvas = document.getElementById('yourGiantCanvas').getContext('2d'),
    slice = document.createElement('canvas').getContext('2d');

slice.canvas.width = 1000;
slice.canvas.height = 1000;

for (var y=0; y < canvas.height; y+=1000){
  for (var x=0; x < canvas.width; x+=1000){
    slice.clearRect(0, 0, slice.canvas.width, slice.canvas.height);
    slice.drawImage(largeCanvas.canvas, x, y, 1000, 1000, 0, 0, 1000, 1000);

    var imagePiece = slice.canvas.toDataURL();

    //Now just save the imagePiece however you normally were planning to
    //and you can build the image again using these slices. You can create 
    //a much better user experience this way too. 
  }
}
hobberwickey
  • 6,118
  • 4
  • 28
  • 29
  • What issue are you having? This isn't a full working function, just a template for how you would do something like this. – hobberwickey Apr 23 '13 at 16:41
  • Please sorry, probably this works. But I need your help. How to parse imagePiece var or a array to create one image like this: $.post("createImage.php", { str: imagePiece; }; How to join all imagePieces to make one image like this script: $urlUploadImages = 'images/'; $nameImage = 'test.png'; $data = base64_decode($_POST["str"]); $img = imagecreatefromstring($data); $width = imagesx($img); ... imagedestroy($img); Thank You very much. – ptCoder Apr 23 '13 at 17:18
  • I wouldn't bother joining all the images in php (unless you have to, and if you do check out this http://stackoverflow.com/questions/1719909/joining-multiple-images-into-one-with-a-php-script). I would just save all the images in some naming format i.e. ImagePiece001.png, ImagePiece002.png, etc... then when you load them back on the front-end just create a large canvas and tile the pieces back together. – hobberwickey Apr 24 '13 at 12:41
1

Have updated the code to split the canvas into smaller canvas objects. Works pretty good and have added a tracker also:

This Allows for tracking of the upload process and overall I think is better for the user. I use PHP to rejoin at a later stage.

It avoids the issues of size of canvas / browser etc.

My first post so hope it helps!

// pass in type for the file name

function sliceCanvas(type, canvasId){
var largeCanvas = document.getElementById(canvasId).getContext('2d');
var slice = document.createElement('canvas').getContext('2d');

var baseSize = 500;

fileH = largeCanvas.canvas.height / baseSize;
fileW = largeCanvas.canvas.width / baseSize;

slice.canvas.width = baseSize;
slice.canvas.height = baseSize; 
count = 1;

numFiles = Math.ceil(fileH) * Math.ceil(fileW);

for (var y=0; y < largeCanvas.canvas.height; y+=baseSize){
  for (var x=0; x < largeCanvas.canvas.width; x+=baseSize){
    slice.clearRect(0, 0, slice.canvas.width, slice.canvas.height);
    slice.drawImage(largeCanvas.canvas, x, y, baseSize, baseSize, 0, 0, baseSize, baseSize);

    var imagePiece = slice.canvas.toDataURL();

    typeFinal = type + count;

    exportSlice(typeFinal, imagePiece, numFiles);
    count++;


  } 
}
}

Ajax to upload:

function exportSlice(type, dataURL, fileNum){

percent = 0;
percentComplete = 0;

    $.ajax({
         type: "POST",
          url: YourServerSideFiletoSave,
          data: {image: dataURL, type: type}
        })
        .done(function( response ) {
            console.log(response);
            percent++;
            percentComplete = Math.ceil(Number(percent/fileNum*100));
           return true;
         })
          .fail(function(response) {
            console.log("Image FAILED");

            console.log(response);

            return false;

        })
        .always(function(response) {
          console.log( "Always");
        });

  }
Tim Hill
  • 41
  • 6