Entering and Formatting Text

In my previous posts, I’ve shown how to draw objects in Illustrator, InDesign, and Photoshop. The idea was to create generic functions that “wrap” the application-specific code. Each function takes a set of coordinate locations, and uses those coordinates to draw a path. You don’t need to know the details of the application objects properties, and methods needed to draw the path—just send an array to the function, and the function will take care of drawing the path. The resulting paths will be as close to identical as we can make them, given the differences between the applications.

In this blog post, I’ll try to do the same thing for text. I’ll introduce a set of functions for entering text and doing some minor typesetting tasks in Illustrator, InDesign, and Photoshop. Send the function a string of text, a location (as a coordinate pair), a point size, and a font name, and the application-specific code in each “flavor” of the function will take care of creating and formatting the text. Again, I’ll try to have all three applications produce the same result.

While we’re at it, I’ll show how to get a list of fonts from the application and display the font names in a a ComboBox control in the example plug-in. Think of it as a bonus.

The example extension will create a sample document and position the text at the center of the document, but you can use the example makeText functions to draw text wherever you want.

You can find the example project for this post here:

text

Update: This project is now available via Import>Adobe Creative Suite Extension Builder>Remote Creative Suite SDK examples.

What’s the Problem?

The problem that I’m trying to solve in these posts is that Illustrator, InDesign, and Photoshop all use different application-specific code to do some very basic things. The applications can’t agree on how to open a document, for crying out loud!

While I firmly believe that most of the work—and most of the benefit of automation—in Creative Suite extensions will take place within individual applications, and use application-specific code, I also think it’s useful to be able to do basic things in the same way across all applications (or the “Big Three,” at least). If I can provide functions for creating most basic objects that work the same way for all applications, then we’re a step closer to creating something like a common programming interface for the supported applications. It’s not an ideal approach, because the more application-specific code I add, the more I’ll have to maintain in the future. Ideally, we’d provide common scripting interfaces for doing simple things in the products themselves.

Building the Creative Suite Extension

I’ll use the CS Extension Builder to quickly create an extension that will run in the three (or four, if you count Photoshop Extended as a separate application) applications we’re interested in. After that I’ll set up the extension using the same structure as I used for the drawing example I presented earlier. This entails adding the AppModel and AppController classes as shown in the illustration below.

Next, I’ll edit the main.mxml file to make it look like this:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" historyManagementEnabled="false" creationComplete="onCreationComplete()">
	<mx:Script>
		<![CDATA[
			import com.adobe.csxs.core.CSXSInterface;			
			import sample.text.AppController;
			import sample.text.AppModel;
			[Bindable]
			private var model:AppModel = AppModel.getInstance();
			private var controller:AppController = AppController.getInstance();
			private function onCreationComplete():void
			{
				model.readyState();
				controller.getFontNames();
			}
			private function handleUI(event:Event):void{
				switch(event.currentTarget.id){
					case "textField":
						model.textToEnter = textField.text;
						break;
					case "pointSizeField":
						model.pointSize = Number(pointSizeField.text);
						break;
					case "makeTextButton":
						controller.makeText();
					case "fontComboBox":
						model.fontIndex = fontComboBox.selectedIndex;
						model.fontName = String(fontComboBox.selectedItem);
						break;
				}
			}
		]]>
	</mx:Script>
	<mx:VBox height="100%" width="100%" verticalAlign="middle" horizontalAlign="center">
		<mx:VBox>
			<mx:HBox>
				<mx:Label
					id="textFieldLabel"
					text="Text:"
					width="100" textAlign="right" paddingTop="2"/>				
				<mx:TextInput 
					id="textField" 
					width="120" 
					textAlign="left"
					focusOut="handleUI(event)"
					text="Hello World"/>
			</mx:HBox>
			<mx:HBox>
				<mx:Label
					text="Font:"
					width="100" textAlign="right" paddingTop="2"/>				
				<mx:ComboBox 
					id="fontComboBox" 
					width="200"
					dataProvider = "{model.fontNames}"
					selectedIndex="{model.fontIndex}"
					change="handleUI(event)"/>
			</mx:HBox>
			<mx:HBox>
				<mx:Label
					text="Size:"
					width="100" textAlign="right" paddingTop="2"/>				
				<mx:TextInput 
					id="pointSizeField" 
					width="60" 
					textAlign="right"
					restrict="0123456789-" 
					focusOut="handleUI(event)"
					text="72"/>
				<mx:Label
					text="points"
					width="100" textAlign="left" paddingTop="2"/>				
			</mx:HBox>
		</mx:VBox>
		<mx:Button id="makeTextButton" label="Enter Text" click="handleUI(event)"/>
	</mx:VBox>
</mx:Application>


Note that the above panel isn’t intended as a “finished” CS Extension. It’s intentionally very simple—just enough to demonstrate the functions I’ll be presenting.

I’ll use the AppModel class to store various variables, and the AppController class will contain all of the non-application-specific functions. The textIllustrator, textInDesign, and textPhotoshop classes will contain the application-specific code.

In the AppModel class, I’ll define the following variables (most of these have to do with setting up the example document):

public var hostName:String;
public var textToEnter:String = "Hello World";
public var sampleDocument:Boolean = true;
public var sampleWidth:Number = 612;
public var sampleHeight:Number = 792;
public var sampleCenterX:Number = sampleWidth/2;
public var sampleCenterY:Number = sampleHeight/2;
public var pointSize:Number = 72;
public var center:Array = new Array(sampleWidth/2, sampleHeight/2);
public var fontNames:ArrayCollection = new ArrayCollection(["Update"]);
public var fontName:String = "";
public var fontIndex:int = 0;


The AppModel class also includes the getHostName function for determining the name of the host application (the application displaying the example panel).

The AppController class passes program control to application-specific functions based on the name of the current host application. As you can see, the makeText functions are generic—they take the same parameters. This example plug-in also includes a few utility functions for creating sample documents.

switch(model.hostName){
	case "indesign":
		textInDesign.makeText(model.textToEnter, new Array(model.sampleCenterX,model.sampleCenterY), model.fontName, model.pointSize);
		break;
	case "illustrator":
		textIllustrator.makeText(model.textToEnter, new Array(model.sampleCenterX,model.sampleCenterY), model.fontName, model.pointSize);
		break;
	case "photoshop":
		textPhotoshop.makeText(model.textToEnter, new Array(model.sampleCenterX,model.sampleCenterY), model.fontName, model.pointSize);
		break;
}


Application-Specific Code

In Illustrator and InDesign, you can create TextFrame objects, but the objects differ in behavior (in the user interface), capabilities, and in their properties and sub-elements. In Photoshop, text exists on text layers, and a TextItem contains the properties of the text on the layer.

Illustrator

Here’s the makeText function for Illustrator:

public static function makeText(content:String, array:Array, fontName:String, pointSize:Number):void{
	if(app.documents.length > 0){
		var document:Document = app.documents.index(0);
		var textFrame:TextFrame = document.textFrames.add();
		textFrame.position = array;
		textFrame.contents = content;
		textFrame.paragraphs.index(0).characterAttributes.size = pointSize;
		textFrame.paragraphs.index(0).paragraphAttributes.justification = Justification.CENTER;
		textFrame.paragraphs.index(0).characterAttributes.textFont = app.textFonts.getByName(fontName);
	}
}


I want to center the text at a given location, and Illustrator makes this very easy—just set textFrame.position property to an array containing a pair of coordinates, and set the alignment of the paragraph to Justification.CENTERED. Note that most text formatting properties are found in the characterAttributes and paragraphAttributes objects.

Here’s the function for updating the font list (this one isn’t generic, and won’t work unless you have an ArrayCollection named fontNames in an instance of a class model):

public static function updateMenuArray():void{
	model.fontNames.removeAll();
	for(var counter:int = 0; counter < app.textFonts.length; counter++){
		var fontName:String = app.textFonts.index(counter).name;
		model.fontNames.addItem(fontName);
	}
}


In Illustrator, you can get a list of font names to populate the font menu in the panel. You can use the name of the selected font to refer to a font object using the getByName method. Once you have a reference to a font, you can use it to apply the font to the text.

InDesign

Here’s the makeText function for InDesign:

public static function makeText(content:String, array:Array, fontName:String, pointSize:Number):void{
	var document:Document = app.documents.item(0);
	//The active window can be either a layout window or a story window. If the
	//user did not create a sample document, and has a story window open, we 
	//shouldn't do anything.
	if(app.activeWindow is LayoutWindow){
		var window:LayoutWindow = LayoutWindow(app.activeWindow);
		var page:Page = window.activePage;
		var textFrame:TextFrame = page.textFrames.add();
		var topLeft:Array = page.resolve(AnchorPoint.TOP_LEFT_ANCHOR, CoordinateSpaces.SPREAD_COORDINATES)[0];
		var bottomRight:Array = page.resolve(AnchorPoint.BOTTOM_RIGHT_ANCHOR, CoordinateSpaces.SPREAD_COORDINATES)[0];
		textFrame.reframe(CoordinateSpaces.SPREAD_COORDINATES, new Array(topLeft, bottomRight));
		textFrame.texts.item(0).contents = model.textToEnter;
		textFrame.texts.item(0).pointSize = pointSize;
		textFrame.texts.item(0).appliedFont = app.fonts.itemByName(fontName);
		textFrame.fit(FitOptions.FRAME_TO_CONTENT);
		var textFrameCenter:Array = textFrame.resolve(AnchorPoint.CENTER_ANCHOR, CoordinateSpaces.INNER_COORDINATES)[0];
		var transformationMatrix:TransformationMatrix = app.transformationMatrices.add();
		var xTranslate:Number = array[0] - textFrameCenter[0];
		var yTranslate:Number = array[1] - textFrameCenter[1];
		transformationMatrix = transformationMatrix.translateMatrix(xTranslate, yTranslate);
		//Move the center point of the text frame to the specified location.
		textFrame.transform(CoordinateSpaces.INNER_COORDINATES, AnchorPoint.CENTER_ANCHOR, transformationMatrix);
	}			
}


There are lots of different ways to set the location of a text frame in InDesign. I’ll use an approach that will work regardless of the orientation or size of the page, and regardless of the current units of measurement, zero point location, and ruler origin. To do that, I’ll use the resolve method to get the corners of the page, then I’ll resize the text frame using those values (thereby creating a text frame the size of the page, which should be sufficient to contain the text). After I enter and format the text, I’ll fit the frame to the text. Once that’s done, the script will move the center point of the text frame to the specified location using the transform method.

Here’s the function for updating the font list (this one isn’t generic, and won’t work unless you have an ArrayCollection named fontNames in an instance of a class model):

public static function updateMenuArray():void{
	model.fontNames.removeAll();
	for(var counter:int = 0; counter < app.fonts.length; counter++){
		var fontName:String = app.fonts.item(counter).name;
		model.fontNames.addItem(fontName);
	}
}


In InDesign, as in Illustrator, you can get a list of font names to populate the font menu in the panel. You can use the name of the selected font to refer to a font object using the itemByName method. Once you have a reference to a font, you can use it to apply the font to the text.

Photoshop

Here’s the makeText function for Photoshop:

public static function makeText(content:String, array:Array, fontName:String, pointSize:Number):void{
	if(app.documents.length > 0){
		var document:Document = app.documents.index(0);
		var textLayer:ArtLayer = document.artLayers.add();
		textLayer.kind = LayerKind.TEXT;
		textLayer.textItem.contents = content;
		var xCenter:Number = array[0] - Number(textLayer.textItem.width)/2;
		var yCenter:Number  = array[1] - Number(textLayer.textItem.height)/2;
		textLayer.textItem.size = pointSize;
		textLayer.textItem.font = fontName;
		textLayer.textItem.justification = Justification.CENTER;
		textLayer.textItem.position = new Array(xCenter, yCenter);
	}
}


In Photoshop, you control the location of the textItem using the position property. If the justification (alignment) of the text is set to Justification.CENTER, then the center point of the text will fall at the coordinates you use to set the position property.

Here’s the function for updating the font list (this one isn’t generic, and won’t work unless you have an ArrayCollection named fontNames in an instance of a class model):

public static function updateMenuArray():void{
	var fontName:String;
	model.fontNames.removeAll();
	for(var counter:int = 0; counter < app.fonts.length; counter++){
		fontName = app.fonts.index(counter).postScriptName;
		model.fontNames.addItem(fontName);
	}			
}


When I saw that textItem.font required a string (and not a font object), I knew that I needed a font name, but I didn’t know which font name I needed. The font object has name, postScriptName, and family properties. I tried getting a list of the name properties first, but using that string to apply the formatting didn’t work for all fonts. As it turns out, Photoshop wants you to use the string you get from the postScriptName property to set the font. Sometimes, development is a bit like playing “Battleship.”

Testing the Panel

If you’re using the Creative Suite Extension Builder, you can select the project and run or debug the panel in the program of your choice. If you’re not using the Extension Builder, you can build and install the plug-in. Either way, there’s not much to testing the plug-in: open the plug in, enter some text, select a font, enter a point size, and click the Make Text button. The host application will create a new document and enter and format the text.



As you can see, each application has a slightly different idea about the vertical location of the centered text. You might want to experiment with positioning—moving the InDesign text frame up by one third of the text height or so. Vertical centering of text is highly subjective, so you should make the adjustments that look best to you.

Note that the Photoshop version of the panel will cause an error in Windows if you run it once, then run it again. This is because the Windows version of Photoshop can’t set the active document via scripting, and can’t add objects to a document unless the document is the active document. But the function will still work in other contexts—it’s only in the context of the sample documents created by this panel that this limitation beomes a problem.

More Basics to Come

Next time, I’ll be back with more generic functions for doing basic things in Illustrator, InDesign, and Photoshop. I’m not sure exactly which tasks I’ll focus on, but I have a few ideas. If there’s a particular area—some feature that all three programs support—you’d like to see covered, please let me know via the comments.

6 Responses to Entering and Formatting Text

  1. Kevin says:

    Hi, this has been a great help. I’ve been playing around with text for photoshop and am I right in thinking there is no way to control individual characters, size and font within a textItem?

  2. Olav Kvern says:

    Hi Kevin,

    I don’t think that there is a way to set the formatting of an individual character in a textItem–it seems like there *must* be a way, but I haven’t found it yet. Since you can do it in the UI, you’d think it would be supported via scripting…. All I’ve been able to come up with are ugly workarounds (creating separate text layers, etc.). I’m not the Photoshop scripting expert, though, so I’ll ask around. I’m hoping that someone has found a way, since it seems like a serious limitation.

    Thanks,

    Ole

  3. Jeremy Knudsen says:

    Hi Olav,

    Thank you so much for this article!

    I have exactly the same request as Kevin. In Photoshop, you can edit and copy text and its styling to the clipboard and paste it into another text layer and it its formatting is retained. Therefore, it would seem possible to get at and read the per-character styling data.

    Being able to get at or read the style data of a text layer on a per-character basis would solve one of the problems I currently have in one of my Photoshop panel projects. Please do ask around…I would even appreciate an Adobe-approved workaround if there is one. :-)

    Thanks!
    Jeremy

  4. Jeremy Knudsen says:

    I posted the same request on PS-Scripts and xbytor found a way to do this in Photoshop. It looks like it needs a little work, but I’ve been able to get it to control the setting of individual characters:

    http://www.ps-scripts.com/bb/viewtopic.php?f=9&t=3752&start=0&sid=68343c96c289e6591cacce511c9e4ec8

  5. Pingback: Formatting Text Ranges in Photoshop « Creative Suite SDK Team

  6. Pingback: More Basics: Importing and Exporting Files « Creative Suite SDK Team