Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
271 views
in Technique[技术] by (71.8m points)

javascript - 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>
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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

So the general ideas are that :

  • To draw an svg on a canvas, we need to load it in an <img> element first.
  • For security reasons, <img> inner documents can not make any external requests.
    This means that you'll have to embed all your external resources as dataURIs inside your svg markup itself, before loading it to the <img> element.

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('
');
    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>

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...