javascripthtmlcanvassvghtml5-canvas

How to use Google fonts in Canvas when Drawing DOM objects in SVG?


As per Mozilla's documentation, you can draw complex HTML on Canvas like this.

What I can't figure out is a way to make Google fonts work with it.

See this example below:

var canvas = document.getElementById('canvas');
    var ctx    = canvas.getContext('2d');
    
    var data   = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">' +
                   '<foreignObject width="100%" height="100%">' +
                     '<div xmlns="http://www.w3.org/1999/xhtml" style="font-size:40px;font-family:Pangolin">' +
                       'test' +
                     '</div>' +
                   '</foreignObject>' +
                 '</svg>';
    
    var DOMURL = window.URL || window.webkitURL || window;
    
    var img = new Image();
    var svg = new Blob([data], {type: 'image/svg+xml;charset=utf-8'});
    var url = DOMURL.createObjectURL(svg);
    
    img.onload = function () {
      ctx.drawImage(img, 0, 0);
      DOMURL.revokeObjectURL(url);
    }
    
    img.src = url;
<link href="https://fonts.googleapis.com/css?family=Pangolin" rel="stylesheet">

<div style="font-size:40px;font-family:Pangolin">test</div><hr>
<canvas id="canvas" style="border:2px solid black;" width="200" height="200"></canvas>


Solution

  • This has been already asked a few times, but never really as precise as it about Google Fonts.

    So the general ideas are that :

    So for a font, you'll need to append a <style> element, and replace font's src in between url(...), with the dataURI version.

    Google fonts embed documents like the one you use are actually just css files which will then point to the actual font files. So we need to fetch not only the first level CSS doc, but also the actual font files.

    Here is a annotated and working (?) proof of concept, written with ES6 syntax, so which will require a modern browser, but it could be transpiled quite easily since all the methods in it can be polyfiled.

    /*
      Only tested on a really limited set of fonts, can very well not work
      This should be taken as an proof of concept rather than a solid script.
    	
      @Params : an url pointing to an embed Google Font stylesheet
      @Returns : a Promise, fulfiled with all the cssRules converted to dataURI as an Array
    */
    function GFontToDataURI(url) {
      return fetch(url) // first fecth the embed stylesheet page
        .then(resp => resp.text()) // we only need the text of it
        .then(text => {
          // now we need to parse the CSSruleSets contained
          // but chrome doesn't support styleSheets in DOMParsed docs...
          let s = document.createElement('style');
          s.innerHTML = text;
          document.head.appendChild(s);
          let styleSheet = s.sheet
    
          // this will help us to keep track of the rules and the original urls
          let FontRule = rule => {
            let src = rule.style.getPropertyValue('src') || rule.style.cssText.match(/url\(.*?\)/g)[0];
            if (!src) return null;
            let url = src.split('url(')[1].split(')')[0];
            return {
              rule: rule,
              src: src,
              url: url.replace(/\"/g, '')
            };
          };
          let fontRules = [],
            fontProms = [];
    
          // iterate through all the cssRules of the embedded doc
          // Edge doesn't make CSSRuleList enumerable...
          for (let i = 0; i < styleSheet.cssRules.length; i++) {
            let r = styleSheet.cssRules[i];
            let fR = FontRule(r);
            if (!fR) {
              continue;
            }
            fontRules.push(fR);
            fontProms.push(
              fetch(fR.url) // fetch the actual font-file (.woff)
              .then(resp => resp.blob())
              .then(blob => {
                return new Promise(resolve => {
                  // we have to return it as a dataURI
                  //   because for whatever reason, 
                  //   browser are afraid of blobURI in <img> too...
                  let f = new FileReader();
                  f.onload = e => resolve(f.result);
                  f.readAsDataURL(blob);
                })
              })
              .then(dataURL => {
                // now that we have our dataURI version,
                //  we can replace the original URI with it
                //  and we return the full rule's cssText
                return fR.rule.cssText.replace(fR.url, dataURL);
              })
            )
          }
          document.head.removeChild(s); // clean up
          return Promise.all(fontProms); // wait for all this has been done
        });
    }
    
    /* Demo Code */
    
    const ctx = canvas.getContext('2d');
    let svgData = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">' +
      '<foreignObject width="100%" height="100%">' +
      '<div xmlns="http://www.w3.org/1999/xhtml" style="font-size:40px;font-family:Pangolin">' +
      'test' +
      '</div>' +
      '</foreignObject>' +
      '</svg>';
    // I'll use a DOMParser because it's easier to do DOM manipulation for me
    let svgDoc = new DOMParser().parseFromString(svgData, 'image/svg+xml');
    // request our dataURI version
    GFontToDataURI('https://fonts.googleapis.com/css?family=Pangolin')
      .then(cssRules => { // we've got our array with all the cssRules
        let svgNS = "http://www.w3.org/2000/svg";
        // so let's append it in our svg node
        let defs = svgDoc.createElementNS(svgNS, 'defs');
        let style = svgDoc.createElementNS(svgNS, 'style');
        style.innerHTML = cssRules.join('\n');
        defs.appendChild(style);
        svgDoc.documentElement.appendChild(defs);
        // now we're good to create our string representation of the svg node
        let str = new XMLSerializer().serializeToString(svgDoc.documentElement);
        // Edge throws when blobURIs load dataURIs from https doc...
        // So we'll use only dataURIs all the way...
        let uri = 'data:image/svg+xml;charset=utf8,' + encodeURIComponent(str);
    
        let img = new Image();
        img.onload = function(e) {
          URL.revokeObjectURL(this.src);
          canvas.width = this.width;
          canvas.height = this.height;
          ctx.drawImage(this, 0, 0);
        }
        img.src = uri;
      })
      .catch(reason => console.log(reason)) // if something went wrong, it'll go here
    <canvas id="canvas"></canvas>