Note that every image with a blue border in this article launches a swf example. Read its caption to understand how it works.
One of the most interesting sessions at MAX 2008 was Creating a Next-Generation Desktop News Reader by Jeremy Clark, Daniel Wabyick and Justin Van Slembrouck. It demonstrated a sophisticated and dynamic AIR news reader application using the International Herald Tribune's feeds and gave a taste of what to expect in the news delivery of the future. It also showcased the greatly improved text management of the latest Flash player 10.
In addition, Adobe Labs released in December 2008 the Text Layout Framework (previously known as Vellum then Text Component ActionScript Library) built on top of the FTE. With it you can use or extend text components built on top of the text engine for both Flex and Flash. The Text Layout Framework deserves an article of its own as it will be used by most developers but the intent of this article is to give an understanding of how the engine itself works.
The new text engine flash.text.engine was built from the ground-up. It co-exists with the current TextField object but works differently: it is a low-level access, highly flexible text layout engine. Device fonts can now be manipulated as if they were embedded. In fact, a lot of the same effects can be applied to device and embedded fonts. The text is print-quality typography for the web.
The engine supports many languages and alphabeths including the ones using right-to-left layout (Arabic and Hebrew) or vertical layout (Chinese, Japanese and Korean) but also complex ones (Tate-chu-yoko) which have horizontal blocks in vertical text. Additionally a combination of scripts, such as Japanese or Arabic, is possible.
The following classes are used to create, format and control your text (in addition, many other classes manage under the hood core text information such as Font or TextRenderer).
- the TextBlock - the factory for building a paragraph of text
- the textLine - a line of text for the textBlock and a new displayObject
- the contentElement - holds the content (text or graphic) of the textBlock
- the ElementFormat - defines the format of the contentElement
- the fontDescription - defines properties of the font applied to the elementFormat
This is some simple code using these classes to display text with all default settings:
import flash.text.engine.*;
var fd:FontDescription = new FontDescription();
var ef:ElementFormat = new ElementFormat(fd);
var te:TextElement = new TextElement("Hello world", ef);
var tb:TextBlock = new TextBlock();
tb.content = te;
var tl:TextLine = tb.createTextLine(null, 200);
addChild(tl);
FontDescription
This is where you define the font family fontName you want to use. For device font, you can select font styling such as italize fontPosture and bold fontWeight. For embedded font, you can take advantage of the enhanced screen rendering especially for small sizes renderingMode.CFF and strong horizontal stems to snap to a sub pixel grid especially important on LCD displays cffHinting. In RenderingMode.CFF mode, instead of the standard Flash renderer, "anti-alias for animation", used for vector art, Adobe's special glyph rendering technology is applied.

Note that fonts of type EMBEDDED can only be used by the TextField object (still maintained in the future but non longer improved). Only fonts of type EMBEDDED_CFF (Compact Font Format) can take advantage of the FTE features. In addtion to font outlines and information for Unicode mapping, they include a subset of OpenType tables needed to do things such as ligature substitutions, contextual writing systems like Arabic or Hebrew and GPOS/GSUB (Glyph Positioning table and Glyph Substitution table).
To see which fonts are available on your machine and in the swf, use the enumerateFonts method of the Font class. The method isFontCompatible(fontName, fontWeight, fontPosture) returns the availability of an embedded font in any style. Note that, for device font, the player has no interface to determine if the font provided by the OS has bold or italic face.
import flash.text.Font;
import flash.text.engine.FontDescription;
// all fonts
var myFonts:Array = Font.enumerateFonts(true);
for (var i:int = 0; i < myFonts.length; i++) {
if (myFonts[i].fontType == "device") {
trace("I am a device font and my name is", myFonts[i].fontName);
}
}
// embedded fonts only
var myEmbeddedFonts:Array = Font.enumerateFonts(false);
var f:Font = myEmbeddedFonts[0];
trace(FontDescription.isFontCompatible(f.fontName, "normal", "normal"));
trace(FontDescription.isFontCompatible(f.fontName, "bold", "normal"));
trace(FontDescription.isFontCompatible(f.fontName, "normal", "italic"));
Embedding fonts can be tricky. In Flex, embed the font and store it using DefineFont4 and font subsetting currently only supported by Gumbo. A future version of Flash will support it but in the meantime, a Gumbo SWC with the embedded font must be created then adding to your Flash CS4 project.
[Embed(source="assets/GaramondPremrPro.otf",
fontFamily="GaramondPremiere",
mimeType="application/x-font",
cff="true")]
private const GaramondPremiere:Class;
var fd = new FontDescription();
fd.fontLookup = FontLookup.EMBEDDED_CFF;
fd.fontName = "GaramondPremiere";
To display non-Roman text, Unicode character codes are converted to Strings. Unicode is a standard to represent text in most systems providing character information in an abstract way rather than graphically by assigning a unique number for every character in a language. The Flash player uses the font information to determine how to map the number to a glyph.
var text:String = "abc" + String.fromCharCode(0x05D0, 0x05D1, 0x05D2, 0x5185, 0x95A3, 0x5E9C);

The font description is initially applied to an elementFormat (covered next) and locked. To make changes, a clone must be created:
import flash.text.engine.*;
import flash.text.Font;
var fd = new FontDescription("PALATINO", FontWeight.BOLD);
fd.fontLookup = FontLookup.DEVICE;
var ef = new ElementFormat(fd);
var te = new TextElement("Hello world", ef);
var tb = new TextBlock();
tb.content = te;
var firstLine = tb.createTextLine(null, 300);
firstLine.y = 50;
addChild(firstLine);
// fd.fontWeight = FontWeight.NORMAL; // will throw an error
// Error 2185: FontDescription object is locked and cannot be modified
// clone the font description
var fdCloned = (fd.locked) ? fd.clone() : fd;
fdCloned. fontWeight = FontWeight.NORMAL;
var ef2 = new ElementFormat(fdCloned);
tb.content.elementFormat = ef2;
var secondLine = tb. createTextLine (null, 300);
addChild(secondLine);
secondLine.y = 100;
ElementFormat
This is where you define text styling and some of the layout of your content (ContentElement discussed next). They applied to both device and embedded fonts.
Some styling properties are self-explanatory such as alpha, color, fontSize and typographicCase. Others, maybe less familiar, take their root in typography digitCase, digitWidth, ligatureLevel. The layout properties affects horizontal spacing between atoms kerning, trackingRight, trackingLeft, vertical positioning baselineShift, alignmentBaseline, dominantBaseline, and textRotation. breakOpportunity determines which characters to break when wrapping text over several lines.
The image below shows several elementFormats with different settings. Note that the intent of some of the ElementFormat properties is to accommodate for vertical or right-to-left scripts rather than design.

TextRotation on a elementFormat is only applied at a 90 degree angle so for a more detailed rotation such as moving text on a path, use the TextLine rotation. For alpha and textRotation, the final effect is the multiplication of several objects' property value.
Like the fontDescription, once a ElementFormat is assigned, it is locked. It can however be cloned.
import flash.text.engine.*;
import flash.text.Font;
var fd = new FontDescription("PALATINO");
fd.fontLookup = FontLookup.DEVICE;
var ef = new ElementFormat(fd);
ef.fontSize = 30;
var te = new TextElement("Hello world", ef);
var tb = new TextBlock();
tb.content = te;
var firstLine = tb.createTextLine(null, 300);
firstLine.y = 50;
addChild(firstLine);
// ef.fontSize = 50; // will throw an error
// Error 2184: The ElementFormat object is locked and cannot be modified
// clone the element format
var efClone = (ef.locked) ? ef.clone() : ef;
efClone.fontSize = 50;
// applies the new format to the existing content
tb.content.elementFormat = efClone;
var secondLine = tb. createTextLine (null, 300);
addChild(secondLine);
secondLine.y = 100;
ContentElement
is the content (text and graphic) stored in a block of text and is independent of how it is distributed over lines. The contentElement is an abstract base class and cannot be instantianted. Use one of its subclasses instead: TextElement, GraphicElement or GroupElement.
import flash.text.engine.*;
var fd:FontDescription = new FontDescription();
var ef:ElementFormat = new ElementFormat(fd);
var te:TextElement = new TextElement("This is the content to display", ef);
var tb:TextBlock = new TextBlock();
tb.content = te;
var tl:TextLine = tb.createTextLine(null, 200);
addChild(tl);
import flash.text.engine.*;
private var loader:Loader;
private function loadImage():void {
loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, done);
var url:String = "http://www.google.com/images/nav_logo.png";
var urlReq:URLRequest = new URLRequest(url);
loader.load(urlReq);
}
private function done(e:Event):void {
var graphicElement:GraphicElement = new GraphicElement(loader, loader.width, loader.height);
addChild(loader);
textBlock.content = graphicElement;
}
import flash.text.engine.*;
// create a ContentElement vector to store multiple textElements
var content:Vector.<ContentElement> = new Vector.<ContentElement>();
var fd:FontDescription = new FontDescription();
var el1:ElementFormat = new ElementFormat(fd, 10);
content.push(new TextElement("I a the first piece of content. ", el1));
var el2:ElementFormat = new ElementFormat(fd, 15);
content.push(new TextElement("I am the second ", el2));
var el3:ElementFormat = new ElementFormat(fd, 20);
content.push(new TextElement("And I am the third one.", el3));
var tb:TextBlock = new TextBlock(new GroupElement(content));
var tbc = textBlock.content;
trace(tbc); // [object GroupElement]
trace(tbc.elementCount); // 3
trace(tbc.getElementAt(0)); // [object TextElement]
createTextLines(tb); // create lines using the text line content
Each element type has its specific mechanism. TextElement can replace its text. GraphicElement can modify the height and width allocated to the graphic on the line elementHeight and elementWidth. GroupElement manages its various elements in different ways such as replace, merge and split.
If the content is changed after it was initially created, the elementFormat doesn't need to be defined again or cloned but the lines containing the content need to be broken and recreated (discussed under TextBlock).
The contentElement read-only information is its text, its elementFormat, its groupElement if it is part of a group as well as its textBlock and its textBlockBeginIndex (index for this particular contentElement in the textBlock). The userData provides a way to associate data with the element. The eventMirror object mirrors the events dispatched to the textLine and can be used to add logic and interactivity to your text (example under TextLine).
TextLine
is a new DisplayObject used to render one line of content according to a variable width. A textLine can only be created via the textBlock function createTextLine().
The TextLine is made of "atoms”: characters, groups of characters, graphic elements and indivisible elements. Methods to retrieve atoms information are available but it is important to flushAtomData() afterwards to avoid memory overhead.
The textLine can determine on mouseDown the atom selected by calling getAtomIndexAtPoint(e.stageX, e.stageY) but it has no knowledge of the content (text or/and graphic) corresponding to the atom. This information can be retrieved via the textblock.content.text and passing it the index position of the atom within the textBlock getAtomTextBlockBeginIndex().
private function createLine(tb:TextBlock, x:int, y:int, w:int, cont):void {
// create the textLine and a mouseEvent listener
var tl:TextLine = tb.createTextLine (null, w);
tl.x = x;
tl.y = y;
cont.addChild(tl);
tl.addEventListener(MouseEvent.CLICK, showMe);
}
// get the atom clicked by point position
private function showMe(e:MouseEvent):void {
var tl:TextLine = e.target as TextLine;
var atom:int = e.target.getAtomIndexAtPoint(e.stageX, e.stageY);
// draw a yellow box above the atom
var bounds:Rectangle = tl.getAtomBounds(atom);
box.graphics.beginFill(0xFFFF00, 0.4);
box.graphics.drawRect(bounds.left, bounds.top, bounds.width, bounds.height);
box.graphics.endFill();
addChild(box);
// get the atom content via the textBlock.content.text
var index:int = textLine.getAtomTextBlockBeginIndex(i);
trace( textLine.textBlock.content.text.charAt(index) );
}
The contentElement, covered earlier, can defines an event dispatcher as a mirrorRegion to the TextLine so that it receives events received by the TextLine. This option is helpful to add interactivity (click, mouseOver and mouseOut only). The code below uses this mechanism to make some text linkable.
var dispatcher:EventDispatcher = new EventDispatcher();
dispatcher.addEventListener("click", clickHandler);
var te1:TextElement = new TextElement("Here are more of", someFormat);
var te2:TextElement = new TextElement("Einstein's quotes", someFormat);
te2.eventMirror = dispatcher;
te2.userData = "http://rescomp.stanford.edu/~cheshire/EinsteinQuotes.html";
// code here to make the TextElements as content of the TextBlock
// and create TextLines
private function clickHandler(event:MouseEvent):void {
var line:TextLine = event.target as TextLine;
var region:TextLineMirrorRegion = line.getMirrorRegion(dispatcher);
selected = region.element as TextElement;
navigateToURL(selected.userdata);
}
The text engine doesn't provide an interface to select text as with a traditional selectable text field. The (partial) code and swf below show how to add the cut, copy and paste functionality to the TextLine using the standard key commands. If you are developing for an AIR application, storing the text cut, copied or pasted is much simpler because you can take advantage of the Clipboard.setData() and Clipboard.getData() methods.
// Vector to store textBlock index position of selected text
private var selected:Vector.<int> = new Vector.<int>();
// Vector to store cut or copied text
private var stored:Vector.<String> = new Vector.<String>();
// displayObject used to draw red marker and blue selection
private var box:Sprite = new Sprite();
// store cursor position to paste text on keyCommand
private var lastAtom:int = -1;
textLine.addEventListener(MouseEvent.MOUSE_DOWN, trackSelection);
textLine.addEventListener(MouseEvent.MOUSE_UP, stopTracking);
textLine.addEventListener(KeyboardEvent.KEY_DOWN, keyDownListener);
private function trackSelection(e:MouseEvent):void {
e.target.removeEventListener(MouseEvent.MOUSE_DOWN, trackSelection);
// clear previous selection
selected.splice(0, selected.length);
box.graphics.clear();
var tl:TextLine = e.target as TextLine;
var atom:int = tl.getAtomIndexAtPoint(e.stageX, e.stageY);
var bounds:Rectangle = tl.getAtomBounds(atom);
originX = bounds.x + tl.x;
originY = bounds.y + tl.y;
originW = 1;
originH = tl.height;
// draw a red selection marker
box.graphics.lineStyle(1, 0xFF0000, 1);
box.graphics.moveTo(originX, originY);
box.graphics.lineTo(originX, originY+originH);
// nows listen to the mouseMove
e.target.addEventListener(MouseEvent.MOUSE_MOVE, drawSelection);
}
private function drawSelection(e:MouseEvent):void {
var tl:TextLine = e.target as TextLine;
// get atom index in the line
var atom:int = tl.getAtomIndexAtPoint(e.stageX, e.stageY);
// get atom index in the TextBlock to copy text between all lines
var atomT:int = tl.getAtomTextBlockBeginIndex(atom);
// only store the atom once
if (atomT == oldAtom) {
return;
}
oldAtom = atomT;
// draw the blue selection based on the atom bounds and previous selection
var bounds:Rectangle = tl.getAtomBounds(atom);
box.graphics.clear();
box.graphics.beginFill(0x7FBBF3, 0.4);
box.graphics.drawRect(originX, originY, originW+bounds.width, originH);
originW = originW+bounds.width;
box.graphics.endFill();
selected.push(atomT);
}
private function stopTracking(e:MouseEvent):void {
e.target.removeEventListener(MouseEvent.MOUSE_MOVE, drawSelection);
e.target.addEventListener(MouseEvent.MOUSE_DOWN, trackSelection);
var tl:TextLine = e.target as TextLine;
var atom:int = tl.getAtomIndexAtPoint(e.stageX, e.stageY);
lastAtom = tl.getAtomTextBlockBeginIndex(atom);
}
// cut, copy or paste text
private function keyDownListener(e:KeyboardEvent):void {
if (e.ctrlKey == true) {
switch(e.keyCode) {
case 88:
if (selected.length > 0) {
stored.splice(0, stored.length);
var cutString:String = "";
for (var i:int = 0; i < selected.length; i++) {
// store text cut for future use
stored[i] = textBlock.content.text.charAt(selected[i]);
cutString += textBlock.content.text.charAt(selected[i]);
}
var c:TextElement = textBlock.content as TextElement;
// remove text from textBlock.content
c.replaceText(selected[0], cutString.length + selected[0], null);
createLines();
}
break;
case 67 :
if (selected.length > 0) {
stored.splice(0, stored.length);
for (var i:int = 0; i < selected.length; i++) {
// store text for future use
stored[i] = textBlock.content.text.charAt(selected[i]);
}
}
break;
case 86:
if (lastAtom != -1) {
var pasteString:String = "";
for (var i:int = 0; i < stored.length; i++) {
pasteString += stored[i];
}
var c:TextElement = textBlock.content as TextElement;
// add text to textBlock.content
c.replaceText(lastAtom, lastAtom, pasteString);
createLines();
}
break;
default:
}
}
}
Note that the
TextBlock
is the factory to create one single paragraph (the algorithms for formats such as directional and line-break only work on one paragraph).
This is where the direction (or directions if mixed) of the text is set up bidiLevel, the tab offset tabStops, the text justified including for Asian scripts textJustifier and EastAsianJustifier. The baseline property baselineZero can be the Roman baseline, its ascent or descent and for scripts such as Chinese, ideographic top, center or bottom. The lineRotation can be modified by a 90 degree increment lineRotation and a special screen appearance turned on applyNonLinearFontScaling. The userData property is a handy way to associate some customized data to the textBlock like the text creation date or the name of the author.
With all these settings, the textBlock uses its content to create text, one textLine at a time at a given width. For each line created, a textLineCreationResult of type Success, Complete or Insufficient_Width is fired. The textLines must be added to the display list independently.
private function displayLines(tb:TextBlock, sp:Sprite, width:int):void {
var prevLine:TextLine;
var tl:TextLine = tb.createTextLine(prevLine, width);
while (tl != null) {
tl.y = prevLine ? prevLine.y+tl.height : tl.ascent;
sp.addChild(textLine);
prevLine = tl;
tl = tb.createTextLine(prevLine, width);
}
}
After the initial creation, if the container's width changes or if a fontDescription, a textFormat or an elementContent is modified, the lines must be "broken" again.
// photoW and photoH are the dimensions of the image
private var offset:int = 13; // offset from top left corner
// small layout
var prevLine:TextLine;
var textLine:TextLine = _textBlock.createTextLine(null, photoW);
while (textLine) {
textLine.x = prevLine ? textLine.ascent : offset;
textLine.y = prevLine ? prevLine.y + textLine.height : offset;
// add a gutter to the first line below the image
if (textLine.previousLine == _textBlock.firstLine) {
textLine.y = prevLine.y + textLine.height + photoH + margin;
}
prevLine = textLine;
addChild(textLine);
textLine = _textBlock.createTextLine(textLine, photoW);
}
// medium layout
var isSecond:Boolean = false;
var prevLine:TextLine;
var currentWidth:Number = photoW -margin*10;
var refLine:TextLine;
while (textLine) {
// lines along the side of the image
if (isSecond == false) {
textLine.x = prevLine ? prevLine.x : 13;
textLine.y = prevLine ? prevLine.y + textLine.height : 13;
// first line after the image
if (textLine.previousLine == _textBlock.firstLine) {
textLine.x = prevLine.x + photoW + margin;
textLine.y = prevLine.y + textLine.height;
}
// line reaching bottom of image
if (textLine.y > _textBlock.firstLine.height && isSecond == false) {
isSecond = true;
currentWidth = photoW + photoW-(margin*10) + margin;
refLine = textLine;
}
// lines below the image
} else {
textLine.x = _textBlock.firstLine.x;
textLine.y = prevLine.y + textLine.height;
// add a gutter to the first line below the image
if (refLine == textLine.previousLine) {
textLine.y = prevLine.y + textLine.height+margin/3;
}
}
prevLine = textLine;
addChild(textLine);
textLine = _textBlock.createTextLine(textLine, currentWidth);
}
// large layout
var isThird:Boolean = false;
var prevLine:TextLine;
var textLine:TextLine = _textBlock.createTextLine (null, photoW);
while (textLine) {
textLine.x = prevLine ? prevLine.x : offset;
textLine.y = prevLine ? prevLine.y + textLine.height : offset;
// first line after the image - create a second row
if (textLine.previousLine == _textBlock.firstLine) {
textLine.x = prevLine.x + photoW + margin;
}
// create a third row
if (textLine.y > _textBlock.firstLine.height && isThird == false) {
textLine.y = _textBlock.firstLine.nextLine.y;
textLine.x = textLine.previousLine.x + textLine.previousLine.width + margin;
isThird = true;
}
prevLine = textLine;
addChild(textLine);
textLine = _textBlock.createTextLine(textLine, photoW);
}
}
Thank you to Nabeel Al-Shamma, Senior Director, Engineering at Adobe, for his information on fonts and to the late Albert Einstein and Ernst Haeckel for the memorable quotes and the beautiful jellyfish drawing.





