Measuring text in IPython using the Javascript interface.

This notebook is an experiment in using the IPython notebook to do precise layout. For better or worse, SVG's facilities for text handling are general enough to handle modern variable-width fonts. This means that it is tricky to work out how wide text is when displayed. The solution is to make use of IPython's Javascript interface, which gives everything that is needed, in moderately simple form.

The width calculations do not apply exactly to other display media, but the before and after SVGs look good when opened in Safari or Chrome. Neither Inkscape nor the Unix convert tool do the same as the web browsers.

Create a simple piece of SVG with text and a rectangle that is only a rough estimate of the right size to fit tightly around the text.

In [1]:
from IPython.display import display,SVG,Javascript
In [2]:
def create_svg(text, name1="mySVG", tname='myText', rname='myRect'):
    """
    Create an SVG and assign names to the components.
    """
    svg = SVG("""<svg version="1.1"  x="0" y="0" width="400" height="200"

    baseProfile="full"  
    xmlns="http://www.w3.org/2000/svg" id="%s">  

    <text id="%s" name="textName" x="10" y="125" font-size="18" 
    style="font-family: impact, georgia, times, serif; font-weight: normal; font-style: normal"
    text-anchor="start" fill="blue">%s</text>
    <rect id="%s" x="10" y="107" stroke="grey"
        fill="None" height="18" width="%d"></rect>
    </svg>""" % (name1,tname,text,rname,9*len(text)))
    return svg

create_svg('Exercise caution when entering the building')
Out[2]:
Exercise caution when entering the building

The next cell finds the text and its surrounding rectangle, then adjusts the height and width of the rectangle to match those of the text that it is supposed to contain. For now, the names of the text and the rectangle are hard-wired.

In [3]:
%%javascript 
var mytext = document.getElementById('myText');
var bb1 = mytext.getBBox();
var myrect = document.getElementById('myRect');
var bb2 = myrect.getBBox();
myrect.setAttribute('width',bb1.width);
myrect.setAttribute('height',bb1.height);

The next cell uses IPython's kernel interface to create a string from the SVG element tree, then assign it to a Python variable. XMLSerializer seems to work smoothly for Safari. It is not certain that the API for the IPython kernel will persist into later versions, since the team are working hard on Javascript interaction.

The two cells after the one with the Javascript cell magic verify that display works, and that the raw svg is as expected.

In [4]:
%%javascript
var kernel = IPython.notebook.kernel;
var mySVG = document.getElementById('mySVG');
var s = new XMLSerializer()
var pp = s.serializeToString(mySVG);
var command = 'svgs = """' + pp + '"""'
kernel.execute(command);
In [5]:
SVG(data=svgs)
Out[5]:
Exercise caution when entering the building
In [6]:
print svgs
<svg baseProfile="full" height="200" id="mySVG" version="1.1" width="400" x="0" xmlns="http://www.w3.org/2000/svg" y="0">  

    <text fill="blue" font-size="18" id="myText" name="textName" style="font-family: impact, georgia, times, serif; font-weight: normal; font-style: normal" text-anchor="start" x="10" y="125">Exercise caution when entering the building</text>
    <rect fill="None" height="23" id="myRect" stroke="grey" width="314" x="10" y="107"></rect>
    </svg>
In [7]:
# output the saved SVG to a file.
outf = open('sample.svg','w')
outf.write(svgs);
outf.close()

Now try the same thing, but using a different interface to execute the Javascript, which makes it possible to dynamically build the string that is executed by Javascript. This allows flexibility in naming the rectangle and the text box.

In [8]:
def move_rectangle(tname, rname):
    '''
    Change the height and width of the surrounding rectangle
    to match those of the text box.
    '''
    return Javascript('''
        var mytext = document.getElementById("%s");
        var bb1 = mytext.getBBox();
        var myrect = document.getElementById("%s");
        var bb2 = myrect.getBBox();
        myrect.setAttribute('width',bb1.width);
        myrect.setAttribute('height',bb1.height);
    ''' % (tname,rname))

If you run the next cell using IPython 1.0 on Safari, it will show the rectangle only loosely around the text

In [9]:
svg = create_svg('An infinite number of screaming monkeys.')
display(svg)
An infinite number of screaming monkeys.
In [10]:
svg = create_svg('An infinite number of screaming monkeys.',name1='bazonka',tname='xya',rname='abd')
display(svg)
move_rectangle('xya','abd')
An infinite number of screaming monkeys.
Out[10]:

Save the original svg data, which has not changed.

In [11]:
print svg.data
<svg baseProfile="full" height="200" id="bazonka" version="1.1" width="400" x="0" xmlns="http://www.w3.org/2000/svg" y="0">  

    <text fill="blue" font-size="18" id="xya" name="textName" style="font-family: impact, georgia, times, serif; font-weight: normal; font-style: normal" text-anchor="start" x="10" y="125">An infinite number of screaming monkeys.</text>
    <rect fill="None" height="18" id="abd" stroke="grey" width="360" x="10" y="107"/>
    </svg>
In [12]:
# output the saved SVG to a file.
outf = open('before.svg','w')
outf.write(svg.data);
outf.close()

Recover the current state of the transformed svg, and save the result to a file.

In [13]:
def recover_svg(name):
    """
    obtain the string value of an SVG with a particular ID.
    """
    display(Javascript('''
    var kernel = IPython.notebook.kernel;
    var mySVG = document.getElementById('%s');
    var s = new XMLSerializer()
    var pp = s.serializeToString(mySVG);
    var command = 'svgs = """' + pp + '"""'
    kernel.execute(command);''' % (name,)))
    return svgs



svgs = recover_svg('bazonka')
In [14]:
# output the saved SVG to a file.
outf = open('after.svg','w')
outf.write(svgs);
outf.close()
In [15]:
print svgs
<svg baseProfile="full" height="200" id="bazonka" version="1.1" width="400" x="0" xmlns="http://www.w3.org/2000/svg" y="0">  

    <text fill="blue" font-size="18" id="xya" name="textName" style="font-family: impact, georgia, times, serif; font-weight: normal; font-style: normal" text-anchor="start" x="10" y="125">An infinite number of screaming monkeys.</text>
    <rect fill="None" height="23" id="abd" stroke="grey" width="302" x="10" y="107"></rect>
    </svg>
In [16]:
SVG(svgs)
Out[16]:
An infinite number of screaming monkeys.