Fabric.js - how to save canvas on server with custom attributes

DatabaseJsonSavechangesFabricjs

Database Problem Overview


I'd like to be able to save the current canvas' state to a server-side database, probably as a JSON string, and then later restore it with loadFromJSON. Typically, this is easily accomplished using:

var canvas = new fabric.Canvas();
function saveCanvas() {
	// convert canvas to a json string
	var json = JSON.stringify( canvas.toJSON() );

	// save via xhr
	$.post('/save', { json : json }, function(resp){ 
		// do whatever ...
	}, 'json');
}

And then

function loadCanvas(json) {

  // parse the data into the canvas
  canvas.loadFromJSON(json);

  // re-render the canvas
  canvas.renderAll();

  // optional
  canvas.calculateOffset();
}

However, I've also been setting a few custom attributes on the fabric objects I'm adding to the canvas using the builtin Object#set method:

// get some item from the canvas
var item = canvas.item(0);

// add misc properties
item.set('wizard', 'gandalf');
item.set('hobbit', 'samwise');

// save current state
saveCanvas();

The problem is that when I check the request on the server-side, I see that my custom attributes were not parsed from the canvas and sent along with everything else. This probably has to do with how toObject method removes anything that's not a default attribute in the object class. What would be a good way to tackle this issue, such that I'll be able to both save and restore the canvas from a JSON string sent by the server, and the restored canvas will also include my custom attributes? thanks.

Database Solutions


Solution 1 - Database

Good question.

If you're adding custom properties to objects, those objects are likely "special" in some way. It seems like subclassing them would be a reasonable solution.

For example, here's how we would subclass a fabric.Image into a named image. Those image objects could then have names like "Gandalf" or "Samwise".

fabric.NamedImage = fabric.util.createClass(fabric.Image, {
  
  type: 'named-image',

  initialize: function(element, options) {
    this.callSuper('initialize', element, options);
    options && this.set('name', options.name);
  },

  toObject: function() {
    return fabric.util.object.extend(this.callSuper('toObject'), { name: this.name });
  }
});

First, we give these objects a type. This type is used by loadFromJSON to automatically invoke fabric.<type>.fromObject method. In this case it would be fabric.NamedImage.fromObject.

Then we overwrite initialize (constructor) instance method, to also set "name" property when initializing an object (if that property is given).

Then we overwrite toObject instance method to include "name" in returned object (this is a cornerstone of object serialization in fabric).

Finally, we'll also need to implement that fabric.NamedImage.fromObject that I mentioned earlier, so that loadFromJSON would know which method to invoke during JSON parsing:

fabric.NamedImage.fromObject = function(object, callback) {
  fabric.util.loadImage(object.src, function(img) {
    callback && callback(new fabric.NamedImage(img, object));
  });
};

We're loading an image here (from "object.src"), then creating an instance of fabric.NamedImage out of it. Note how at that point, constructor will already take care of "name" setting, since we overwrote "initialize" method earlier.

And we'll also need to specify that fabric.NamedImage is an asynchronous "class", meanining that its fromObject does not return an instance, but passes it to a callback:

fabric.NamedImage.async = true;

And now we can try this all out:

// create image element
var img = document.createElement('img');
img.src = 'https://www.google.com/images/srpr/logo3w.png';

// create an instance of named image
var namedImg = new fabric.NamedImage(img, { name: 'foobar' });

// add it to canvas
canvas.add(namedImg);

// save json
var json = JSON.stringify(canvas);

// clear canvas
canvas.clear();

// and load everything from the same json
canvas.loadFromJSON(json, function() {

  // making sure to render canvas at the end
  canvas.renderAll();

  // and checking if object's "name" is preserved
  console.log(canvas.item(0).name);
});

Solution 2 - Database

Wow. Am I missing something here?

I've done this plenty of times and it doesn't need any fancy subclassing.

The docs cover it: http://fabricjs.com/docs/fabric.Canvas.html#toJSON

Just include an array of property names as strings in your call to toJSON().

Eg

canvas.toJSON(['wizard','hobbit']);

Hopefully.... for bonus points you can add a reviver function which will rehydrate your custom attributes.

Again this is covered in the docs and has an example.

Solution 3 - Database

I had the same issue but I didn't want to extend the fabric.js classes.

I wrote a function that takes the fabric canvas in parameter and returns a stringified version with my special attributes:

function stringifyCanvas(canvas)
{
	//array of the attributes not saved by default that I want to save
    var additionalFields = ['selectable', 'uid', 'custom']; 

    sCanvas = JSON.stringify(canvas);
    oCanvas = JSON.parse(sCanvas) ;
    $.each(oCanvas.objects, function(n, object) {
        $.each(additionalFields, function(m, field) {
            oCanvas.objects[n][field] = canvas.item(n)[field];
        });
    });
    
    return JSON.stringify(oCanvas);     
}

The special attributes seems properly imported when I use canvas.loadFromJSON(), I'm using fabric 1.7.2.

Solution 4 - Database

A more simple approach would be to add the properties post-stringify:

var stringJson = JSON.stringify(this.canvas);
var objectJson = JSON.parse(string.Json);

//remove property1 property
delete objectJson.property1;

//add property2 property
delete objectJson.property2;

// stringify the object again
stringJson = JSON.stringify(objectJson);

// at this point stringJson is ready to be sent over to the server
$http.post('http://serverurl/',stringJson);

Solution 5 - Database

If you don't want to specify the custom attributes you are using every time you call canvas.toJSON(), and you don't want to use a complicated subclassing approach, here is a very simple way to extend Fabric's toObject method.

//EXTEND THE PROPS FABRIC WILL EXPORT TO JSON
fabric.Object.prototype.toObject = (function(toObject) {
    return function() {
        return fabric.util.object.extend(toObject.call(this), {
            id: this.id,
            wizard: this.wizard,
            hobbit: this.hobbit,
        });
    };
})(fabric.Object.prototype.toObject);

Then you can set custom properties on Fabric objects.

item.set("wizard","gandalf");
item.set("hobbit","bilbo");

And when you call canvas.toJSON() those properties will persist to the output. If you then use canvas.loadFromJSON() with your JSON output, the custom attributes will be imported and applied to the Fabric objects.

Solution 6 - Database

http://fabricjs.com/fabric-intro-part-3#serialization

var rect = new fabric.Rect();

rect.toObject = (function(toObject) {
  return function() {
    return fabric.util.object.extend(toObject.call(this), {
      name: this.name
    });
  };
})(rect.toObject);

canvas.add(rect);

rect.name = 'trololo';

console.log(JSON.stringify(canvas));

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionsa125View Question on Stackoverflow
Solution 1 - DatabasekangaxView Answer on Stackoverflow
Solution 2 - DatabasekbcoolView Answer on Stackoverflow
Solution 3 - Databaseadrien54View Answer on Stackoverflow
Solution 4 - DatabaseDan OchianaView Answer on Stackoverflow
Solution 5 - DatabaseNeil VanLandinghamView Answer on Stackoverflow
Solution 6 - DatabaseVipin ChoubeyView Answer on Stackoverflow