Thursday, June 7, 2007

GWT Canvas: Rendering rotated text

Chronoscope operates on a Canvas abstraction in GWT. Think of it as a mirror of the Safari Javascript Canvas API, with extensions for text rendering, reading back coordinate transforms, layers, fast-clear, and frame/display list capture.

A typical bit of Chronoscope Canvas code will look like:


Canvas canvas = getCanvas();
canvas.beginFrame();
Layer layer = canvas.getLayer("foo");
layer.setStrokeColor(Color.black);
layer.moveTo(sx,sy);
layer.lineTo(ex, ey);
layer.stroke();

Layer textLayer = canvas.getLayer("bar");
textLayer.setStrokeColor(Color.red);
textLayer.drawText("Hello World", x, y,
gssProperties);

canvas.endFrame();



The begin/end frame abstraction permits capture of display list for faster rendering (Opera lockCanvas), as well as shipping a scenegraph to Flash, Silverlight, or SVG. Layers permit compositing and fast-clear.

Rendering Text

Users of Javascript Canvas dream of support for text rendering. For some unknown reason, Apple (who has awesome text rendering capability in the Quartz canvas) left it out. This has forced many developers to roll their own text rendering, until the day when everyone supports the WHATWG Canvas.

Some of the typical hacks used to get text rendering include:

  • Placing DIV tags over the Canvas with text
  • Using pre-rendered or on-the-fly server-rendered images
  • Extracting glyph vectors from true type fonts, sending them encoded to the browser, and rendering them with Canvas moveto/lineto calls.

The latter two options are the only ones capable of dealing with rotated text.

Chronoscope uses the first solution, even for rotated text, however the individual letters do not get rotated, leading to ugliness. The existing Chronoscope demo shows this on the vertical axis chart labels. Fast-clear comes into play when I want to redraw. I can blow away dozens of hundreds of DIV tags with something like "layerElem.innerHTML=''".


GWT1.4 ClippedImage and FontBook Rendering

I recently began experimenting with the idea of rendering an entire font at the desired 2D transformation and font-properties, and then using GWT1.4 ClippedImage to render letters.

A few hours later, I have an implementation that exceeded my wildest dreams in quality, here's how I did it:

  1. Create a GWT RPC Service called getFontMetrics(transform, fontProperties, color)
  2. Servlet calculates FontMetrics (ascent, descent, leading, advance, etc) for first 256 characters of the Font (most European languages)
  3. Returned metrics includes a URL to a generated font book
  4. FontRenderer servlet renders 16x16 array of characters with specified transform, color, font, etc
  5. new drawTransformedText() method of Canvas first renders according to the old "ugly" method mentioned above and kicks off RPC and Image load
  6. On load completed, "ugly" transformed text replaced with text rendered with clipped characters from the font book image


Devil's in the drawTransformedText() details


So how does this method work? The server returns a 256-element array of advance values (essentially character width), along with maxAscent, maxDescent, and leading obtained from Java2D FontMetrics. The returned RenderedFontMetrics RPC object has a method called 'getBounds(char c, Bounds b)' that for any character, returns a clipping bounding box into the second argument (avoiding excessive object creation in the main loop), which is the position of the rendered character in the font book image.

The main loop essentially obtains the bounding box for each character to be drawn, creates a ClippedImage that will display it, and positions the IMG tag along the font-baseline according to the current transform (coordinate system). It uses the 'advance' values for each character to know how far along the baseline to place each successive clipped image.

Screen Shot





Performance



Font Books can be cached, and rendering a font book on the server takes about 10 milliseconds. The resulting image is about 20kb. After that, text rendering performance is pretty much the same as the old "ugly" DIV method.

Caveats



BIDI not supported. Non-ISO-8859 character sets a pain. The issue is, I can't very well send down font-book with 65k characters. However, statistics are on our side. For example, in a language like Chinese, you'll find that most people only use a few thousand characters in day to day use.

We can compute a histogram on a corpus of Chinese text, and render a fontbook containing the most used characters in descending order. We can render several such tiles, in a monotonically descending fashion, on-demand, and cache them. In most applications, you'll probably infrequently request the second-tier characters, and very very infrequently request characters 3 standard deviations away.

Once Chronoscope is out in the wild, I hope some non-ISO-8859-1 native speakers will help contribute improved fontbook rendering for CJK characters and other languages.

p.s. I'll post a demo up on timepedia.org later, after I can verify all of the code is working properly, since I also moved the code base from 1.3 to 1.4.

-Ray

1 comment:

Sebastian said...

Hi, i am very interessted in your font-redering. You said you want to show some demo-code. Is it ready yet?