The Shape Of The Word

The first time I was paid to develop software, coincided with the first Browser War at the end of the 1990's. I worked for three summers in a row as part of a team developing on-line learning software, a lot of which was web based. Whilst those years saw dramatic improvements in web technology, trying to develop a web application that would work reliably and consistently across the different browsers that were available was a a real nightmare. The problem was that browser developers tended to add new features to attract users rather than focusing on correctly implementing the existing web standards. Fortunately the world has moved on in the decade or so since then and most web browsers will now render the same, standards complaint, page in the same way (assuming that we ignore the abomination that is Internet Explorer 6). Unfortunately I was reminded of browser inconsistency recently, when I found that, just like HTML pages during the Browser War, SVG images can appear completely differently depending on the application used to render them.

SVG images, for those who don't already know, are vector rather than raster images. A raster image encodes the colour of each pixel within the image, where as a vector image is a list of instructions for drawing the image. SVG images, therefore, scale well as the co-ordinates etc. within the image can be scaled before drawing. For this reason I've been using SVG images in the applications I've been developing for a while now, as they give me the flexibility to change the interface without having to re-draw toolbar images etc. For those of you who have been reading this blog for a while might remember I translate the SVG images into Java2D code using SVGRoundTrip. I developed SVGRoundTrip out of some code from the now abandoned Flamingo component suite, and have known for a while that it has a few limitations. Until recently though those limitations haven't actually caused me any problems and so I've not been motivated to fix them; if it ain't broke don't fix it!

The main limitation was that text within SVG images wasn't really supported. I did add some code that would render the text to a PNG image, and then load and render the image in the right place when drawing the generated Java2D code. While this works, the whole point of using SVG images is to allow for scaling the images without loss of quality and scaling a PNG as necessary definitely breaks that philosophy. To get around this, one option is to convert the text to curves when editing the image in Inkscape, which works well. The problem is that this makes it impossible to edit the text at a later date. If you might need to edit the image then you need to keep two copies one with the text as text and one with it as curves, which seems a little silly.

Some of the recent development work I've done (as part of my day job) on GATE has involved GUI changes that have required new icons. Given that you never know how a UI might change in the future I decided to use SVG images for all the new icons and slipped SVGRoundTrip into the build process. In the new plugin manager UI I needed a copy of the main application icon, so once I had a usable SVG image (I converted the G to curves in Inkscape) I started to use it throughout the UI for consistency. This worked perfectly until one of my colleagues decided that it would be useful, when the icon was used in a larger version (a dock icon etc.), to include version information within the icon. The current development version of GATE is 7.1-SNAPSHOT and so he created the icon seen to the left of this paragraph. Converting the text to curves in Inkscape produced an SVG file that I could render correctly with SVGRoundTrip, but I decided to have a go at adding support for text to SVGRoundTrip instead.

I started out with a simpler SVG file that just contained a single piece of text and after reading through the API for Apache Batik (the SVG parser underlying SVGRoundTrip) I discovered that actually the following (fairly simple method) would suffice.
private void transcodeTextNode(TextNode node, String comment)
{  
  //this is needed otherwise we get null text runs
  node.getTextPainter().getOutline(node);
     
  @SuppressWarnings("unchecked")
  List runs = node.getTextRuns();
  if (runs != null) {
    for (TextRun run : runs) {
      transcodeShape(run.getLayout().getGlyphVector().getOutline());
   
      TextPaintInfo paintInfo =
          (TextPaintInfo)run.getACI().getAttribute(TextNode.PAINT_INFO);

      // fill the text if we need to
      if (paintInfo.fillPaint != null) {
        transcodePaint(paintInfo.fillPaint);
        printWriter.println("g.setPaint(paint);");
        printWriter.println("g.fill(shape);");
      }
   
      // draw the outline if we need to
      if (paintInfo.strokeStroke != null &&
          paintInfo.strokePaint != null) {
        transcodePaint(paintInfo.strokePaint);
        transcodeStroke(paintInfo.strokeStroke);
        printWriter.println("g.setStroke(stroke);");
        printWriter.println("g.setPaint(paint);");
        printWriter.println("g.draw(shape);");
      }
    }
  }
}
Essentially this method gets each TextRun (i.e. a contguous run of text without line breaks and which is in the same font and style), converts the shape of the text into Java2D code and then converts the fill and stroke commands as well. After initial success I tried varying the fonts and style (including rotating the text) and everything seemed to work. So I moved on to trying to convert the GATE icon.

The result of converting the GATE icon (shown to the right) unfortunately wasn't quite what I was expecting. Clearly it had converted the text into curves, but it looked as if it was drawing each letter in the wrong place. Essentially SVGRoundTrip parses the SVG file and converts it into Java2D commands, so there was really only three possible places things could go wrong:
  1. Batik was parsing the SVG file incorrectly
  2. SVGRoundTrip was incorrectly converting the shape into Java2D commands
  3. There was a problem with the SVG file
After some debugging it was clear that "7.1" and "SNAPSHOT" were being treated as a single shapes and so it was unlikely that the problem was occurring within the SVGRoundTrip code. I didn't really want to start digging into the Batik source code so I went back to have a look at the SVG file.

Both pieces of text were, according to Inkscape, displayed using the DejaVu Sans font using the Bold Semi-Condensed style. After some experimentation I found that if I just set the style to Bold then the image generated by SVGRoundTrip was the same as with Bold Semi-Condensed. This was definitely useful progress! It turns out that when the font information is broken down into CSS for inclusion in the SVG file it actually produces the following XML snippet (I've simplified this to just focus on the font attributes):
<text 
    y="263.66687"
    x="117.51996"
    style="font-size:6px;
           font-style:normal;
           font-variant:normal;
           font-weight:bold;
           font-stretch:semi-condensed;
           font-family:DejaVu Sans;">
  <tspan>SNAPSHOT</tspan>
</text>
Now I'd never heard of font-stretch before so I looked it up in the SVG specification which simply pointed me to the CSS specification which states:

The 'font-stretch' property selects a normal, condensed, or extended face from a font family. Absolute keyword values have the following ordering, from narrowest to widest: ultra-condensed, extra-condensed, condensed, semi-condensed, normal, semi-expanded, expanded, extra-expanded, ultra-expanded.
Now to my ears that sounds more like a hint to how to select a font rather than a direct instruction, especially as while most (if not all) fonts will have bold and italic variants I don't imagine that they will all have nine different condensed versions. To check what versions of the DejaVu Sans font I had installed I had a look at the font options in LibreOffice (a open-source Office package), which lists DejaVu Sans, DejaVu Sans Condensed, DejaVu Sans Light and DejaVu Sans Mono. So no semi-condensed variant is present on my machine. A quick manual tweak to the SVG file to use the condensed version instead and suddenly the text rendered properly in both Inkscape and via SVGRoundTrip! Unfortunately if you then started editing the text (the whole reason for this entire article) in Inkscape it seemed to lose the font-stretch setting altogether which wasn't exactly ideal.

By this time I'd come to the decision that the font-stretch property was something I'd rather avoid, as there didn't seem anyway to ensure that the hint it provided was rendered consistently. Fortunately there is a way to simulate something similar using negative values for the letter-spacing CSS property. The nice thing about this property is that I can give it pixel values which should be interpreted the same by any renderer.


Unfortunately as you can see from the above images the icon still isn't rendered consistently across different applications. Fortunately it renders the same in Inkscape (the left hand image) and SVGRoundTrip (the centre image), and is editable in Inkscape without any problems. The third image shows what happens if I try loading the SVG image into GIMP (an open-source image editor) -- I really don't know what's gone wrong here, it almost looks as if it's picked the wrong font size. Fortunately I don't need to use GIMP to edit or render the image so I'm calling this success.

So SVGRoundTrip now has support for converting text to Java2D curve commands. Because there seems to be quite a few issues with how text is rendered across different SVG tools I'm currently calling this code experimental, so you need to enable it with the -e command line switch. I haven't done a full release of SVGRoundTrip but you can grab this improved version from SVN.

0 comments:

Post a Comment