在SVG中顯示LaTex數學式子

用MathJax可以讓網頁顯示LaTex數學式子,但SVG中的LaTex數學式子要也能顯示,這就得動點手腳了
How to Include MathJax Equations in SVG With Less Than 100 Lines of JavaScript!

How to Include MathJax Equations in SVG With Less Than 100 Lines of JavaScript!

Jason Sachs●May 23, 2014 [source]

Today’s short and tangential note is an account of how I dug myself out of Documentation Despair. I’ve been working on some block diagrams. You know, this sort of thing, to describe feedback control systems:

And I had a problem. How do I draw diagrams like this?

I don’t have Visio and I don’t like Visio. I used to like Visio. But then it got Microsofted.

I can use MATLAB and Simulink, which are great for drawing block diagrams. Normally you use them to create a model for simulation, but you can just use them for illustrations. Except that Simulink’s support for TeX output is very limited and not very easy to use. You have to create a masked subsystem and use icon drawing commands with (..., 'texmode', 'on'). Basically it’s just a bunch of basic math characters, and the ability to use subscripts and superscripts. You can’t even draw simple rational transfer functions like H(s)=1s2+2Kbs+b2H(s)=1s2+2Kbs+b2 because MATLAB’s TeX support hasn’t done anything new in at least the last 12 years, and use of the TeX \frac{}{} macro isn’t supported.

I found the blockdiag program, which is Python-based (yay!!!), and kind of like the dot program in graphviz, but a little bit better-suited to block diagrams. But it doesn’t have equation support either.

Argh! So I turned to the Internet and looked up to see how to put MathJax equations in SVG. MathJax is the client-side JavaScript framework that is the de facto standard for math typesetting in HTML. I use it all the time in my articles. You can do stuff like this without lifting an eyelash:

 

γ=limn→∞(2n+1−−−−−√−∑k=1ntan−11k−−√)=2.157782996659446…γ=limn→∞(2n+1−∑k=1ntan−1⁡1k)=2.157782996659446…

Here’s how MathJax works: it goes in like a bunch of mini-math ninjas and rips through your HTML and looks for text in special delimiters \( \) or \[ \] or $$ $$, and merrily interprets it as TeX math mode, typesetting the results as a huge ugly nested tree of HTML <span> elements to place math text characters in exactly the right places so it looks like a professionally-typeset math equation. It’s not a bitmap! You can zoom in on equations to your heart’s content and it gets sharper-looking.

But it doesn’t work in SVG, at least not without some hackery. One approach is to use the SVG <foreignObject> tag, and I tried it and couldn’t get that to work either. Then I found this post on StackOverflow which uses a different approach. Basically all it does is to render MathJax equations normally, outside of the desired SVG elements, and render the equations as SVG, then copy the resulting SVG nodes into the relevant place inside the desired SVG elements. It took me a little while to figure out what it was doing. The author uses CoffeeScript and jQuery to create equations in SVG. Which is great, but I don’t know CoffeeScript, and I don’t want to mess around with something I don’t know unless I’m going to use it a lot, and it doesn’t seem like these dependencies are really necessary to get things done.

So I wrote my own method, which is pretty simple. You just add a few lines of Javascript at the top of your HTML file, and pretend that MathJax markup works properly in SVG:

Here’s how it works:

  • Include the MathJax scripts with SVG rendering
  • Before MathJax runs:
    • For each <text> element inside a <svg> element, where the <text> node’s content consists entirely of a MathJax equation (with optional spaces outside the equation delimiters):
      • Copy the text content of the <text> element to a temporary <div> element elsewhere on-screen, so that MathJax can typeset the equation properly.
      • Copy these two elements (the original <text> element and the temporary <div> element) to a temporary list in Javascript.
  • After MathJax runs:
    • For each pair of elements in the temporary list:
      • Clone the resulting elements and replace the original <text> element in the SVG with the results of MathJax typesetting.
    • Remove the temporary <div> elements.

That’s it! Excluding the Apache License notice at the top of the file, the Javascript source is less than 100 lines.

I’m happy with the results; I’ve been using this technique to add blocks to Simulink diagrams which have MathJax equations in them.

Here’s an example of a Simulink diagram with the raw MathJax markup: (sorry, it’s only a PNG file so you can’t zoom in)

And here’s what happens when I include it in an HTML file along with my SVG_MathJax script: Presto-chango!

I put the Javascript file svg_mathjax.js into a Bitbucket repository — feel free to use it yourself. Maybe someday soon, the MathJax people will make it obsolete by getting their excellent software to work properly when used in SVG elements.

In the meantime, happy typesetting!


© 2014 Jason M. Sachs, all rights reserved.

 


SVG_MathJax

[source] This replaces MathJax inline markup in an <svg> element with SVG-rendered MathJax.

Usage is easy:

<html>
    <head>
    <script type="text/javascript"
      src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_SVG">
    </script>
    <script type="text/javascript" src="svg_mathjax.js">
    </script>
    <script type="text/javascript">
        new Svg_MathJax().install();
    </script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300">
     <!-- center-align at x,y point: -->
     <text font-size="11" x="100" y="175">\( a^2 + b^2 = c^2 \)</text>
     <!-- left-align at x,y point: -->
     <text font-size="11" x="100" y="205">L\( a^2 + b^2 = c^2 \)</text>
     <!-- right-align at x,y point: -->
     <text font-size="11" x="100" y="235">R\( a^2 + b^2 = c^2 \)</text>
  </svg>
</body>
</html>

svg_mathjax.js

/*
 * SVG_MathJax
 * 
 * Copyright 2014 Jason M. Sachs
 * Based loosely on an approach outlined by Martin Clark
 * in http://stackoverflow.com/a/21923030/44330
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
Svg_MathJax = (function() {
    // apply a function to elements of an array x
    function forEach(x,f) { 
        var n = x.length; for (var i = 0; i < n; ++i) { f(x[i]); } 
    }

    // find all the SVG text elements that are delimited by 
    // \( \) or $ $ MathJax delimiters 
    // (with optional whitespace before/after)
    function findSVGMathJax(f, context)
    {
        var re = /^\s*([LlRrCc]?)(\\\(.*\\\)|\$.*\$)\s*$/;
        context = context || document;
        forEach(context.getElementsByTagName('svg'), function(svg) {
            forEach(svg.getElementsByTagName('text'), function(t) {
                var m = t.textContent.match(re);
                if (m)
                {
                    f(svg, t, m);
                }
            });
        });
    }

    function _install(options) {
        var items = [];

        // Move the raw MathJax items to a temporary element
        MathJax.Hub.Register.StartupHook("Begin Typeset",function () { 
            var mathbucket = document.createElement('div');
            mathbucket.setAttribute('id','mathjax_svg_bucket');
            document.body.appendChild(mathbucket);
            findSVGMathJax(function(svg, t, m) { 
                var d = document.createElement('div');
                mathbucket.appendChild(d);
                var mathmarkup = m[2].replace(/^\$(.*)\$$/,'\\($1\\)');
                d.appendChild(document.createTextNode(mathmarkup));
                t.textContent = '';	
                items.push([t,d,m[1]]);
            });
        });
        MathJax.Hub.Register.StartupHook("End Typeset",function() {
            forEach(items, function(x) {
                var svgdest = x[0]; 
                var mathjaxdiv = x[1]; 
                var justification = x[2];
                var svgmath = 
                     mathjaxdiv.getElementsByClassName('MathJax_SVG')[0]
                               .getElementsByTagName('svg')[0];
                var svgmathinfo = {
                  width: svgmath.viewBox.baseVal.width, 
                  height: svgmath.viewBox.baseVal.height
                };
                // get graphics nodes
                var gnodes = 
                    svgmath.getElementsByTagName('g')[0].cloneNode(true);
                var fontsize = svgdest.getAttribute('font-size');
                var scale = options.scale*fontsize;
                var x =  +svgdest.getAttribute('x');
                var y =  +svgdest.getAttribute('y');

                var x0 = x;
                var y0 = y;
                var x1;
                switch (justification.toUpperCase())
                {
                case 'L': x1 = 0; break;
                case 'R': x1 = -svgmathinfo.width; break;
                case 'C': // default to center
                default:  x1 = -svgmathinfo.width * 0.5; break;
                }
                var y1 = svgmathinfo.height*0;
                gnodes.setAttribute('transform', 'translate('+x0+' '+y0+')'
                     +' scale('+scale+') translate('+x1+' '+y1+')'
                     +' matrix(1 0 0 -1 0 0)');
                if (options.escape_clip)
                    svgdest.parentNode.removeAttribute('clip-path');
                svgdest.parentNode.replaceChild(gnodes,svgdest);
            });
            // remove the temporary items
            var mathbucket = document.getElementById('mathjax_svg_bucket');
            mathbucket.parentNode.removeChild(mathbucket);
        });
    }
    
    var F = function()
    {
        this.scale = 0.0016;
        this.escape_clip = false;
    };
    F.prototype.install = function() { _install(this); }
    return F;
})();

發現上述有些瑕疵,調整如下

Svg_MathJax = (function() {
    // apply a function to elements of an array x
    function forEach(x,f) { 
        var n = x.length; for (var i = 0; i < n; ++i) { f(x[i]); } 
    }

    // find all the SVG text elements that are delimited by 
    // \( \) or $ $ MathJax delimiters 
    // (with optional whitespace before/after)
    function findSVGMathJax(f, context)
    {
        var re = /^\s*([LlRrCc]?)(\\\(.*\\\)|\$.*\$)\s*$/;
        context = context || document;
        forEach(context.getElementsByTagName('svg'), function(svg) {
            forEach(svg.getElementsByTagName('text'), function(t) {
                var m = t.textContent.match(re);
                if (m)
                {
                    f(svg, t, m);
                }
            });
        });
    }

    function _install(options) {
        var items = [];

        // Move the raw MathJax items to a temporary element
        MathJax.Hub.Register.StartupHook("Begin Typeset",function () { 
            var mathbucket = document.createElement('div');
			$(mathbucket).hide()
            mathbucket.setAttribute('id','mathjax_svg_bucket');
            document.body.appendChild(mathbucket);
            findSVGMathJax(function(svg, t, m) { 
                var d = document.createElement('div');
                mathbucket.appendChild(d);
                var mathmarkup = m[2].replace(/^\$(.*)\$$/,'\\($1\\)');
                d.appendChild(document.createTextNode(mathmarkup));
                t.textContent = '';	
                items.push([t,d,m[1]]);
            });
        });
        MathJax.Hub.Register.StartupHook("End Typeset",function() {
            forEach(items, function(x) {
			//debugger
                var svgdest = x[0]; 
                var mathjaxdiv = x[1]; 
                var justification = x[2];
                var svgmath = mathjaxdiv.getElementsByClassName('MathJax_SVG')[0]
                               .getElementsByTagName('svg')[0];
                var svgmathinfo = {
                  width: svgmath.viewBox.baseVal.width, 
                  height: svgmath.viewBox.baseVal.height
                };
                // get graphics nodes
                var gnodes = 
                    svgmath.getElementsByTagName('g')[0].cloneNode(true);
                var fontsize = svgdest.getAttribute('font-size');
				
				if (fontsize==null){
				   // fix, by Michael
				   fontsize= parseFloat($(svgmath).css("font-size"))
				   }
				
                var scale = options.scale*fontsize;
                var x =  +svgdest.getAttribute('x');
                var y =  +svgdest.getAttribute('y');

                var x0 = x;
                var y0 = y;
                var x1;
                switch (justification.toUpperCase())
                {
                case 'L': x1 = 0; break;
                case 'R': x1 = -svgmathinfo.width; break;
                case 'C': // default to center
                default:  x1 = -svgmathinfo.width * 0.5; break;
                }
                var y1 = svgmathinfo.height*0;
                gnodes.setAttribute('transform', 'translate('+x0+' '+y0+')'
                     +' scale('+scale+') translate('+x1+' '+y1+')'
                     +' matrix(1 0 0 -1 0 0)');
                if (options.escape_clip)
                    svgdest.parentNode.removeAttribute('clip-path');
                svgdest.parentNode.replaceChild(gnodes,svgdest);
            });
            // remove the temporary items
            var mathbucket = document.getElementById('mathjax_svg_bucket');
            mathbucket.parentNode.removeChild(mathbucket);
        });
    }
    
    var F = function()
    {
        this.scale = 0.0016;
        this.escape_clip = false;
    };
    F.prototype.install = function() { _install(this); }
    return F;
})();