Search code examples
firebasefontsgoogle-cloud-functionsnode-modulesnode-canvas

registerFont() from node-canvas not parsing font on Firebase Cloud Functions


I am currently working on generating an image with node-canvas via Firebase Cloud Functions, then sending it as an email attachment with nodemailer.

The requirement of the image is that I need to input some string onto a template with a specific font.

So on Firebase Cloud Functions, here is what I did:

exports.test = functions.https.onRequest((request, response) => {
  cors(request, response, () => {
      let tempFilePath = path.join(os.tmpdir(), 'tnr.ttf');
      functions.logger.log('tempFilePath is', tempFilePath);

      bucket.file('tnr.ttf').download({destination: tempFilePath});
      functions.logger.log('Font downloaded locally to', tempFilePath);

      registerFont(path.resolve(tempFilePath), {family: 'tnr'});
      functions.logger.log('Font registered successfully');
  }
}

I have already verified repeatedly that the file 'tnr.ttf' exists on my project cloud and can successfully be loaded to that temp path.

At the registerFont function however, Firebase would always throw this error, no matter which font file I use:

Error: Could not parse font file
    at registerFont (/workspace/node_modules/canvas/index.js:48:17)

Could there possibly be a problem with how Firebase handles this package or function?


Solution

  • You are trying to use the font before it has finished downloading. By the time you realize the problem, open up a terminal and confirm that it's actually present, the download has finished.

    What you need to do is wait for either the Promise returned by download(...) to resolve or attach a callback to the download() method. Then after it's finished downloading, you can register the font.

    exports.test = functions.https.onRequest((request, response) => {
      cors(request, response, (corsErr) => {
        if (corsErr) {
          // this is rare, but still possible.
          functions.logger.error('Request rejected by CORS: ', corsErr);
          res.status(412).send('Rejected by CORS'); // 412 FAILED PRECONDITION
          return;
        }
    
        let tempFilePath = path.join(os.tmpdir(), 'tnr.ttf');
        functions.logger.log('tempFilePath is ', tempFilePath);
    
        bucket.file('tnr.ttf').download(
          {destination: tempFilePath},
          (err) => {
            if (err) {
              functions.logger.error('Failed to download font to local system: ', err);
              res.status(422).send('Failed to register font'); // 422 UNPROCESSABLE ENTITY
              return;
            }
    
            functions.logger.log('Font downloaded locally to ', tempFilePath);
    
            registerFont(path.resolve(tempFilePath), {family: 'tnr'});
            functions.logger.log('Font registered successfully');
    
            res.send('Registered font successfully');
          }
        );
      }
    }
    

    Notes:

    • Don't forget to properly handle errors and terminate requests properly.
    • The same function invocation may be invoked again, consider setting a "I already downloaded that" flag somewhere. In a "worst-case scenario", you may end up with 100s of the same font file.
    • Make sure to execute the registerFont method where you will be using it. You can't have separate /registerFonts and /drawWithFont endpoints, as you aren't guaranteed to be talking to the same function instance you were a moment ago.