0

I'm using fabric.js to design an Image editor. I'm able to convert the canvas to JSON object and send it to the backend.

In the backend I'm able to render the Image from NodeJS server to view it as a static image.

Here is my front end code:

var json = canvas.toJSON();

The json object is sent to server.

Code in my backend server to render the image via URL.

app.get('/image', function(req, res) {

let data = {"version":"4.6.0","objects":[{"type":"rect","version":"4.6.0","originX":"left","originY":"top","left":87,"top":24,"width":20,"height":20,"fill":"blue","stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeDashOffset":0,"strokeLineJoin":"miter","strokeUniform":false,"strokeMiterLimit":4,"scaleX":16.48,"scaleY":16.48,"angle":0,"flipX":false,"flipY":false,"opacity":1,"shadow":null,"visible":true,"backgroundColor":"","fillRule":"nonzero","paintFirst":"fill","globalCompositeOperation":"source-over","skewX":0,"skewY":0,"rx":0,"ry":0},{"type":"textbox","version":"4.6.0","originX":"left","originY":"top","left":148,"top":134,"width":237,"height":45.2,"fill":"rgb(0,0,0)","stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeDashOffset":0,"strokeLineJoin":"miter","strokeUniform":false,"strokeMiterLimit":4,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"shadow":null,"visible":true,"backgroundColor":"","fillRule":"nonzero","paintFirst":"fill","globalCompositeOperation":"source-over","skewX":0,"skewY":0,"fontFamily":"Fontdiner Swanky","fontWeight":"normal","fontSize":40,"text":"Demo Text","underline":false,"overline":false,"linethrough":false,"textAlign":"left","fontStyle":"normal","lineHeight":1.16,"textBackgroundColor":"","charSpacing":0,"styles":{},"direction":"ltr","path":null,"pathStartOffset":0,"pathSide":"left","minWidth":20,"splitByGrapheme":false}]}

res.writeHead(200, { 'Content-Type': 'image/png' });

var canvas = new fabric.StaticCanvas(null, { width: 800, height: 400 });

canvas.loadFromJSON(data, function() {
    canvas.renderAll();

    var stream = canvas.createPNGStream();
    stream.on('data', function(chunk) {
        res.write(chunk);
    });
    stream.on('end', function() {
        res.end();
    });
  });


});

Here is how the image renders

enter image description here

I'm little confused on how to send data to backend when it comes to GIFs.

I was able to add GIF on to the canvas using gifuct-js package. When I convert the canvas to JSON object, the size is more than 10mb. I felt this was not the right way to pass information to backend. Also, how do I render it from backend?

Anirudh
  • 2,767
  • 5
  • 69
  • 119

3 Answers3

0

What you're trying to achieve sounds reasonable, but you went down the wrong path.

You should load the elements/images/gifs/whatevs all by themselves and process them afterwards in the canvas.

And that already is the answer to your question: don't save the canvas but the images and when needed load the images onto the canvas.

If you need to create new elements for the canvas save each element on its own and load it appropriately, don't save the full canvas.

Bonus: you get layers for free on your image editor

Andy
  • 81
  • 6
0

You may avoid this entire problem (and save some bandwidth) by doing the rendering on the client side. The function below is just enough for that:

function render(canvas, mimeType = 'image/png') {
    const target = document.createElement('canvas');
    target.width = canvas.getWidth();
    target.height = canvas.getHeight();
    
    var wasInteractive = canvas.interactive;
    try {
        // kludge to disable rendering interactive elements
        canvas.interactive = false;
        canvas.renderCanvas(target.getContext('2d'), canvas.getObjects());
    } finally {
        canvas.interactive = wasInteractive;
    }

    return new Promise((accept, reject) => {
        try {
            target.toBlob(blob => accept(blob), mimeType);
        } catch (e) {
            reject(e);
        }
    });
}

The above will return a promise resolving to a Blob, which you can then for example offer for download. Be aware, however, that a privacy-conscious browser (like Firefox) may refuse to convert canvas contents into a Blob in order to thwart fingerprinting attempts: unlocking the functionality may require interacting with the page first and/or answering a browser prompt. Browsers may also refuse to render canvas contents if any of the images are not cross-origin clean.


If you insist on rendering the canvas server-side though, you should have noticed the canvas data only contains URIs of the images, so the issue becomes whether the server will be able to resolve them. If all the URIs point to resources downloadable from the public Internet, this should not be a problem: the server can just download the images itself. Server-side fabric.js relies on JSDOM as the DOM implementation: this library defines a class, ResourceLoader, which is responsible for resolving URIs into resources. By default, the DOM environment used by fabric.js is configured to allow fetching external images.

On the other hand, if some of the images are file uploads from the local computer, their contents will have to be bundled with the uploaded canvas data.

You may for example try something like:

function* walkObjects(data) {
    for (const obj of data.objects) {
        yield obj;
        if (obj.type === 'group')
            yield* walkObjects(obj);
    }
}

function resolveBlob(blobURI) {
    if (!/^blob:/.test(blobURI))
        return null;

    /* look up in a private cache of blob URIs */
}

const formData = new FormData;
const data = canvas.toObject();
formData.append('canvas', JSON.stringify(data));

let counter = 0;
for (const obj of walkObjects(data)) {
    if (obj.type !== 'image')
        continue;
    const blob = resolveBlob(obj.src);
    if (!blob)
        continue;
    formData.append('bundle' + counter, blob);
    obj.src = 'example:attached/bundle' + counter;
    counter++;
}

const response = await fetch('/image', {
    method: 'post',
    data: formData,
});

/* e.g. convert into a Blob URI and attach it to <img> */

On the server side, you can supply your own ResourceLoader, so that the substituted URLs resolve correctly, and tell fabric.js to use it.

user3840170
  • 26,597
  • 4
  • 30
  • 62
-1

You need to split the GIF into individual frames and then pass it on to fabric to render/animate.

Code found here might be useful.

knicola
  • 337
  • 1
  • 2