Object Refinery Ltd. makes two small but extremely useful Java libraries for producing vector graphics. JFreeSVG (free & commercial) supports Scalable Vector Graphics (SVG) and HTML5 Canvas output, and OrsonPDF (commercial with free demo) supports Adobe PDF output. Each provides a custom implementation of java.awt.Graphics2D, the 2D drawing surface for the Java AWT graphics library.
Both libraries offer an attractive and unusual option for drawing text. In addition to a handful of built-in fonts (PDF) or whatever fonts happen to be present on the client (SVG), they can render text as vector graphics using the line and curve primitives provided by the output format. This considerably bloats the resulting file so you wouldn’t want to use it for long documents, but it’s perfect for short bits that require specific fonts – individual phrases, equations, or diagrams. (Adobe PDF also supports font embedding but OrsonPDF 1.6 does not, so vector drawing is your only option. This does have the benefit of avoiding the licensing issues endemic with font embedding, though.)
Moreover, both libraries always use vector glyphs when drawing TextLayout objects. If you want the measuring features or text attributes provided by that class, you’ll get no other text rendering option. So I happily created vector-rendered text for a UML diagrammer application, but I soon noticed a problem: the glyph positioning was often incorrect. The following picture, created in Adobe Acrobat from a sample PDF as PNG at 1200 DPI, shows a few lines in Arial that demonstrate the problem:
The left side is the original output, drawn using one
TextLayout instance per line in a straightforward way. The right side is the corrected output that I eventually figured out, as explained below. SVG is visually indistinguishable from the showcased PDF on both sides, so I didn’t reproduce it here. Just in case you’re not seeing the defects clearly, the most visible ones are the following:
- “kynosarges.starchess” — “yn” is too close while most other glyphs are spaced too widely and inconsistently, especially “es.” Such completely wretched spacing is peculiar to boldface.
- “VIEW_INTERNAL” — “NT” and “NA” are almost touching whereas “TE” is too wide.
- “maxPositions” — “ma” is too close, “xP” are almost touching.
- “totalNanoTime” — “Na” and “me” are too close, “Ti” is too wide.
The exact defects vary between characters and fonts. I used Arial here to show that the issue is not peculiar to any “fancy” fonts for which vector rendering would be more important, and also to enable as many readers as possible to reproduce the sample directly. Rest assured that some defects cropped up regardless of the font I used!
Cause & Solution
As I eventually discovered, the root cause is the resolution that
TextLayout.draw uses for relative glyph positioning. With a
FontRenderContext obtained from the current
Graphics2D object, that resolution is identical to that object’s drawing resolution. And the drawing resolution is left at its output-dependent default by both JFreeSVG and OrsonPDF:
- SVG — one HTML pixel (px) which equals 1/96″, give or take browser adjustment.
- PDF — one typographical point (pt) which equals 1/72″, give or take reader adjustment.
For traditional bitmap font rendering, the output resolution defines a pixel grid which constrains glyph positions as well as their shapes. Low-resolution displays and bitmap images require careful adjustments to produce readable text, e.g. ensuring that a lower-case “i” which is just one pixel wide also has one pixel of whitespace to its left and right. But for high-resolution vector rendering with floating-point coordinates, the low-resolution grid of integral layout coordinates is quite meaningless. Using it for glyph positioning is wholly inappropriate and leads to the defects shown above.
Getting properly spaced glyphs when the positioning algorithm insists on using integral coordinates therefore requires scaling up the output resolution for text rendering. Fortunately
Graphics2D accepts the required scaling transformation, although its use is somewhat cumbersome. First you need to apply an
AffineTransform with a scaling factor of less than one. I’m using a floating-point variable
scale holding the inverse factor of one or greater, resulting in this scaling call:
Then, of course, all font sizes and drawing coordinates must be explicitly multiplied by
scale to correct for this transformation. You can download a small ZIP package with the complete source code and sample output in PDF and SVG format. The two sides of the sample output were obtained at a
scale of 1 and 20, respectively. This works out to a DPI resolution of 72 and 1440 DPI (PDF) or 96 and 1920 DPI (SVG). Scaling factor 20 produces a very respectable print-like resolution, ensuring that glyph positioning is visually correct on all conceivable displays and printers.
The first and best alternative solution would be to set the
Graphics2D rendering hint that allows fractional metrics when positioning individual character glyphs. If positions were calculated in fractional coordinates, the nominal resolution of the positioning grid wouldn’t matter. Unfortunately, neither JFreeSVG 1.9 nor OrsonPDF 1.6 seem to support this hint. Adding such support would be highly desirable (and render this entire article obsolete).
Another alternative I attempted was to create a custom FontRenderContext with the desired resolution, i.e. scaled by 20 compared to the original
Graphics2D surface. That worked fine… until I also enabled kerning. Java AWT, or at least
TextLayout, evidently use both resolutions specified by
FontRenderContext for kerning, so kerning pairs were wildly out of place compared to the remaining text when these resolutions differed. This may be an AWT bug.
Bonus Tip: Kerning & Ligatures
Speaking of kerning, I had some trouble enabling this feature, as well as ligatures. Both are supported as TextAttributes and so can be added to an AttributedString – but had no effect when I did so. Turns out that while the Java API documentation does not list them among the “primary” attributes that are replaced by explicit font selection, this is in fact precisely what happens. So when you use the FONT attribute to select a precreated font into your
AttributedString, you must enable kerning and ligatures on the font itself in order to have any effect. This requires calling the method deriveFont(Map), as demonstrated in Oracle’s AttributedText tutorial. My own sample code also uses this technique.
2014-08-04: With remarkable speed, David Gilbert has fixed glyph positioning in JFreeSVG 2.0. The solution turned out to be simple:
Graphics2D.getFontRenderContext now returns a new
FontRenderContext with fractional metrics enabled. This seems to work well and reproduces the effect of the equivalent rendering hint. Fractional metrics are now always enabled but that’s fine, as you would never want to not use them with vector text. An equivalent fix in OrsonPDF 1.7 is upcoming.
2015-10-04: The release of OrsonPDF 1.7 took a while longer but it’s available now and implements the same fix. Moreover, open source authors can now obtain and redistribute OrsonPDF under the GPLv3 license. A commercial license for non-GPL projects is still an option, too.