Fabric.js: Text squished onto single line when using custom font with fabricjs/node-canvas in AWS Lambda

Created on 19 Jun 2020  路  25Comments  路  Source: fabricjs/fabric.js

Version

3.6.0

Information about environment

Nodejs / AWS Lambda

Steps to reproduce

When loading a custom font using the registerFont method, the text gets rendered to online instead of breaking correctly like text without a custom font applied.

Expected Behavior

Text should break onto new lines as per a textbox without a custom font family applied.

Screenshot from 2020-06-19 17-47-40

Actual Behavior

Text is squished onto a single line... :D

Screenshot from 2020-06-19 17-46-11

Does anyone have any ideas as to what is causing this? I've tried ttf, woff and woff2 fonts and ttf loads correctly but the text ends up squished onto a single line.

When I figure this out I'll let everyone know the resolution :)

All 25 comments

@asturur I鈥檓 thinking that this could be to do with not having pango lib in my lambda layers. Any idea on this one as it鈥檚 been driving me insane :D

Can you put pango on lambda? The only 100% reliable way to render stuff with fabric on a server is headless chrome as of today.
If node-canvas would improve more myabe things could change.

Or if someone would take the firefox canvas code, make a library for it with node bindings.

I'm going to go down the headless chrome route. In theory, the canvas should behave exactly as it does in a browser?

it does.
It can be a little be slower because missing hw accelerated gpus, i use it with decent results.

@asturur what does your stack look like for this? Do you use puppeteer / puppeteer-core / aws-chrome-lambda ?

I made just some experiments locally with node.
At work we use it a bit as a proof of concept to render on the servers, but we are not using it yet, we are not sure we need it.
If we use it we have containers on aws on normal machines, non lambda. So with my docker file i can install whatever is missing.

@asturur oh nice. I got custom fonts working eventually with puppeteer/headless chrome on AWS Lambda. Was a pain to say the very least. I've now got the issue again with images not loading before the canvas is exported to a png. Textboxes with custom fonts looks 99.9% identical to those in the browser.

What is your image loading issue? there is a problem with fabric code and chromium 83+ that needs to be solved and influence images with crossOrigin

Hi @asturur, the canvas images are not loading before the image of the canvas is extracted (so I get a blank canvas). I had a custom promise for the loadFromJSON method but since I've got to run this in headless chrome now, the async/await features don't work (looking at compiling with babel). I've also seen others customize the image object to make sure that fromObject loads the image first...

I did try what @radiolondra did but to no success yet:
https://github.com/radiolondra/Server-side-FabricJs-using-Puppeteer/blob/master/testpuppetgithub/libs/fabfunctions.js#L125-L139

@melchiar @asturur Any idea how I can wait for all images in my input JSON to load before trying to render the canvas and extract an image?

I鈥檓 thinking that maybe I should download the images beforehand and add the base64 image data to the objects. Then customise the fromObject function for images to load the base64 data instead. Hopefully this then won鈥檛 result in a blank canvas. The image is generated fine if I remove the images- it鈥檚 just the waiting for the images to download that is tripping me up!

The loadFromJSON() method contains a callback so you should be fine if you execute your code within there.

The loadFromJSON() method contains a callback so you should be fine if you execute your code within there.

That鈥檚 what I鈥檓 doing at the moment but if my JSON has an image then it Returns a blank canvas. I had this issue before and I had a custom promise that was resolved when the callback was called but it鈥檚 no longer working with puppeteer for some reason

I got this work but not without a fair bit of hacking around! I customized the toObject method for images in the end.

if fromObject is not waiting for the image to load there is a bug.
The reason why fromObjet has a callback is JUST image loading otherwise would be a sync operation.

  /**
   * Creates an instance of fabric.Image from its object representation
   * @static
   * @param {Object} object Object to create an instance from
   * @param {Function} callback Callback to invoke when an image instance is created
   */
  fabric.Image.fromObject = function(_object, callback) {
    var object = fabric.util.object.clone(_object);
    fabric.util.loadImage(object.src, function(img, isError) {
      if (isError) {
        callback && callback(null, true);
        return;
      }
      fabric.Image.prototype._initFilters.call(object, object.filters, function(filters) {
        object.filters = filters || [];
        fabric.Image.prototype._initFilters.call(object, [object.resizeFilter], function(resizeFilters) {
          object.resizeFilter = resizeFilters[0];
          fabric.util.enlivenObjects([object.clipPath], function(enlivedProps) {
            object.clipPath = enlivedProps[0];
            var image = new fabric.Image(img, object);
            callback(image, false);
          });
        });
      });
    }, null, object.crossOrigin);
  };

I'm still running v3.6x in my application and haven't had any issues with images not preloading. If you're in v4.x, you could perhaps try switching to v3 to see if there's a difference (if so then it probably is a bug as @asturur suggested)

If your hack/workaround involves serializing the images in base64, consider the possible memory and storage consequences of that (longer loading times since the images won't cache, storing images in your dB, and possibly even duplicating data when the same image is used more than once in a design)

@asturur @melchiar OK you caught me ! I might have just added the images as base64 in my dynamo dB table. If the base64 version doesn鈥檛 exist then I fetch the image and covert to base64 and set that as the src. I鈥檝e tried literally everything to get loadFromJSON to work with the image src set to a URL - custom loadFromJSON promise, cross origin etc. I鈥檓 sure this is a bug - would be hard to replicate my environment though! I鈥檓 using chromium 81 with fabric 4 12 beta with puppeteer on AWS lambda.

@asturur is that a customised fromObject function or is that the one that ships with Fabric 4?

Thanks for all your help !!!

Best regards,

Kyle

@melchiar could a workaround for this be to just download images on lambda prior and covert to base64 and then load onto the canvas ? That way I鈥檓 not storing the base64 images in the dB but it still loads images into the canvas... what do you mean by the images won鈥檛 cache? The images extracted from the canvas would cache on my CDN no?

That is the standard image one.
I use it in my project and it definitely waits.
Can you share your custom fromObejct promise? Maybe to fresh eyes is more clear.

That is the standard image one.
I use it in my project and it definitely waits.
Can you share your custom fromObejct promise? Maybe to fresh eyes is more clear.

Sure! So I had this promise for loadFromJSON working with the node-canvas setup (but had to switch to headless chrome for custom font rendering). I've tried using the loadFromJSON callback to then render and extract the image but no matter what I do, the image extracted from the canvas returns as a blank (canvas). Looking at this project (https://github.com/radiolondra/Server-side-FabricJs-using-Puppeteer/blob/master/testpuppetgithub/libs/maintest.js#L162-L172) it appears that the only way he got it working was to also use the dataURL's...

              let loadJSONPromise = (data) =>
                new Promise((resolve, reject) => {
                  try {
                    canvas.loadFromJSON(data, resolve);
                  } catch (e) {
                    reject(e);
                  }
                });
await loadJSONPromise(canvasJSON).then(async () => {
....
});

I've also tried to use a custom reviver which appends a promise for the loading of an image but had no luck (https://stackoverflow.com/questions/57661991/loading-images-for-canvas-object-asynchronously):

const promiseArray = []

canvasobj.loadFromJSON(template, () => {}, 
(o, object) => {
  const p = new Promise((resolve) => {
   //...
    Canvas.loadImage(binds.image).then((image) => {
      //...
      fabric.Image.fromURL(c.toDataURL(), (img) => {
        //...
        resolve()
      })
    })
  })
  promiseArray.push(p)
})

Promise.all(promiseArray)
  .then(() => {
    // ...
  })

Bottom line is that the only solution that is currently working for me is to convert the images to a base64 data URL and then swap the object src for the data URL before building, rendering and extracting the canvas images. This seems very weird that the dataURL representations of images are loaded correctly but the image URL's do not. As I've said before I've tried the crossOrigin suggestions and even downgraded to chromium 81 as per your suggestion previously.

Because dataurl are fast enough that maybe seem sync. But they are not sync, so you may end up in trouble anyway.

The reviver isn't waited for sure, so if you have some logic in the reviver that could be the problem.

Because dataurl are fast enough that maybe seem sync. But they are not sync, so you may end up in trouble anyway.

The reviver isn't waited for sure, so if you have some logic in the reviver that could be the problem.

Any suggestions on how I can get around this?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

eugene-g13 picture eugene-g13  路  3Comments

medialwerk picture medialwerk  路  5Comments

bhaskardas9475 picture bhaskardas9475  路  4Comments

guettli picture guettli  路  4Comments

AbhijitParate picture AbhijitParate  路  3Comments