10

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>
supersan
  • 5,671
  • 3
  • 45
  • 64
  • 2
    You'd have to include the font in the svg markup as a link to a data uri. – Robert Longson Feb 22 '17 at 22:01
  • I think this sounds like the most practical solution because after much research I can confirm it won't let me load external woff files needed for it. This site looks the best place to start: http://sosweetcreative.com/2613/font-face-and-base64-data-uri – supersan Feb 22 '17 at 22:52

3 Answers3

29

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('\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>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • I made sample code too. http://jsdo.it/defghi1977/K53D The idea of this code is same as Kaiido's. – defghi1977 Feb 23 '17 at 03:00
  • Kudos on making it work and explaining it so well! This is exactly what I was after. Thanks. – supersan Feb 23 '17 at 18:18
  • Is this approach only working for .woff files or also for .eot files? – NiZa Feb 15 '18 at 08:57
  • @NiZa the FontRule should grab the font url without regard to the format, so eot should be grabbed too. – Kaiido Feb 15 '18 at 09:02
  • Ok thx, because I do get it worked for a woff font, but not for the other. – NiZa Feb 15 '18 at 09:07
  • Just found out that it doesn't work on Edge. The 'src' property is unknown in the CSSStyleDeclaration object. Does someone know a work around? – NiZa Feb 22 '18 at 10:22
  • 1
    @NiZa ah I have to admit I didn't tried on Edge, I'll try to have a look tomorrow or during the weekend. – Kaiido Feb 22 '18 at 11:45
  • @NiZa so I had a quick look at it: Indeed Edge doesn't expose `src` for CSSFontFaceRule... had to use an ugly regex that I am not sure how strong it is. Also, they don't make CSSRuleList iterable, so gone back to an ol'school forloop. But it seems to work now in Edge, at least with the font in the example. – Kaiido Feb 23 '18 at 09:01
  • Thanks, I appreciate your solution! But for me it still seems not to work in Edge. I see just the fallback font now. – NiZa Feb 23 '18 at 09:24
  • @Niza in the answer's snippet? I tried the code locally on a VM maybe something goes wrong when ran in iframe... I'll try again during the we. – Kaiido Feb 23 '18 at 09:26
  • I also running it locally, but actually the change that you did works for getting the src value. Only still the wrong font is showing up, I will do some investigate too. Thanks! – NiZa Feb 23 '18 at 09:54
  • @Kaiido How sure are you that it works on Edge? Because it is fetching the font, but not rendering it. – NiZa Feb 26 '18 at 10:48
  • @NiZa I finally had two minutes to check what's going on: it worked on my localhost, but neither on SO not on jsfiddle because Edge won't load data:// fonts on an HTTPS scheme (it says HTTPS security is compromised by data:font/woff2;...). That certainly is a bug, and I don't know any workaround unfortunately... [Working fiddle with non-secure HTTP scheme](http://jsfiddle.net/cmuvp2k0/) – Kaiido Mar 02 '18 at 07:21
  • @NiZa actually I found one, but I don't really like it. Edge faces this bug only from blobURIs, so if we use a dataURI for the img's src, Edge is happy... – Kaiido Mar 02 '18 at 07:28
  • 1. thanks for the detailed description. 2. works well in chrome+ff but not in safari 11: `TypeError: Cross origin requests are only supported for HTTP.` – daniel Jul 10 '18 at 09:28
  • this is awesome. thanks so much for writing this and sharing it. – Crashalot Jan 18 '21 at 05:04
  • @Crashalot yes, to be able to draw an svg image we have to pass through an , from where we can't do any request to external resources, so even for resources used by we need to inline them. – Kaiido Jan 18 '21 at 06:42
  • thanks for the fast reply. is it possible to inline a blob URL for a font file instead? a blob URL might avoid potential complexities of needing to inline a font file. – Crashalot Jan 18 '21 at 06:49
  • @Crashalot unfortunately not. That is I think a specs overthinking since they would lead to the same security issues as data: URLs, but that's how it is now... – Kaiido Jan 18 '21 at 06:59
  • thanks for the reply. so frustrating. this would have been so much easier if browsers supported converting the DOM to an image for same-origin frames. (blanking frames from different origins would address security fears from programmatic DOM screenshots.) – Crashalot Jan 18 '21 at 08:06
  • @Crashalot Yes, well there is the [Screen-Capture API](https://w3c.github.io/mediacapture-screen-share/#dom-mediadevices-getdisplaymedia) that does exist, which allows to get a screen-cast. The UX might be a bit complicated for everyday users, but it might be an option too (just grab the mediastream from there, and either use [ImageCapture](https://developer.mozilla.org/en-US/docs/Web/API/ImageCapture) where available, or pass it to a video that you'll draw to a canvas. – Kaiido Jan 18 '21 at 08:13
  • 1
    thanks for the suggestion. yes, we looked into this, but asking users for permission to record their screens would scare off too many people. it's unfortunate that spec authors didn't consider protecting native DOM screenshots with the same-origin policy. this decision has caused so many needless headaches. anyway, thanks for your help. – Crashalot Jan 18 '21 at 08:43
  • forgot to ask: have you found any issues with this script? it may be the final piece for converting DOM animations to a WebM video, all from javascript. – Crashalot Jan 18 '21 at 09:08
  • @Crashalot As said in the heading comment, I didn't do any serious testings against this script. Be very careful using it. – Kaiido Jan 18 '21 at 09:10
  • ok, was hoping you had heard from others or done more testing since 2018. thanks again for your help. – Crashalot Jan 18 '21 at 09:23
  • last question if you don't mind: assuming this script works and fonts can get inlined, do you foresee other complications with rendering CSS animations onto canvas to create webm videos? – Crashalot Jan 18 '21 at 09:24
  • @Crashalot You'd need a way to freeze the CSS state because CSS animated svgs only get the first frame rendered on canvas. And regarding the testings, some bugs were found by SO users in this exact feed. – Kaiido Jan 18 '21 at 09:26
  • thanks again for all your help. to clarify, not SVG animations, but normal DOM elements animated via CSS and jQuery/javascript. – Crashalot Jan 18 '21 at 09:27
  • @Crashalot Yes, the same rule applies to any animated image when painted on the canvas. – Kaiido Jan 18 '21 at 09:29
  • @defghi1977 The link doesn't work anymore. – Ziyuan Aug 14 '23 at 13:42
1

The simple javascript code to add custom font in SVG would be.

function loadFont(target) {
            //(1)get css as a text.
            const request = new XMLHttpRequest();
            request.open("get", "https://fonts.googleapis.com/css2?family=Satisfy&display=swap"); //font url
            request.responseType = "text";
            request.send();
            request.onloadend = () => {

                //(2)find all font urls.
                let css = request.response;
                const fontURLs = css.match(/https?:\/\/[^ \)]+/g);
                let loaded = 0;
                console.log(fontURLs)
                fontURLs.forEach(url => {

                    //(3)get each font binary.
                    const request = new XMLHttpRequest();
                    request.open("get", url);
                    request.responseType = "blob";
                    request.onloadend = () => {

                        //(4)conver font blob to binary string.
                        const reader = new FileReader();
                        reader.onloadend = () => {
                            //(5)replace font url by binary string.
                            css = css.replace(new RegExp(url), reader.result);
                            loaded++;
                            //check all fonts are replaced.
                            if (loaded === fontURLs.length){
                                const styleEl = document.createElement('style');
                                styleEl.appendChild(document.createTextNode(css));
                                target.querySelector('svg').appendChild(styleEl)
                                // get all svg from your div.
                            }
                        };
                        reader.readAsDataURL(request.response);
                    };
                    request.send();
                });
            };
        }
loadFont($('#capture_div'));
<!--- add your canvas Code -->

And the HTML code

<div id="capture_div">
----other code ----
 <svg viewBox="20 -100 500 200">
                <text style="fill: #000;
                        font-family: 'Satisfy', cursive;
                        stroke: #fff;
                        stroke-width: 25px;
                        font-size: 120px;
                        width: 100%;
                        stroke-linejoin: round;
                        paint-order: stroke;" x="20" y="59">capture</text>
                <text x="70" y="80">my image!</text>
            </svg>
-2

The first thing you can try is to use Google web font loader because you are generating the svg before the font is loaded by the browser

so you need to make sure that the fonts are loaded and then generate the svg/image

if that doesn't work you can create text tags in your svg and try these alternatives for fonts https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/SVG_fonts

Ieltxu Algañarás
  • 4,654
  • 3
  • 10
  • 7