Formatting Text Ranges in Photoshop

In response to an earlier post, “Entering and Formatting Text” (here) a couple of folks asked if there was a way to apply formatting to ranges of text in a Photoshop text layer. It’s certainly not obvious from the Photoshop scripting object model how one would go about doing this—in fact, I don’t think it’s possible, and said so in my response to the comments.

This exchange got me thinking. So it’s impossible—how do you do it? I knew that Photoshop has another way of scripting (in addition to the object model), the Action Manager. I made a mental note to pursue this course, and promptly got distracted by other work.

In the comments section of the earlier post, Jeremy Knudsen kindly posted a pointer to an exchange from the (excellent) PS-Scripts forum, in which (excellent) Photoshop scripter xbytor described a way to work with text ranges in Photoshop. xbytor’s (excellent) xtools Photoshop scripting package for ExtendScript looked like it might solve the problem. I considered asking xbytor for permission to port the package to ActionScript, but decided to try solving the problem myself. There’s a lot of code in the xTools package, and I was looking for a lighter-weight solution.

This led me back to Photoshop, the Action Manager, and the Script Listener plug-in.

You can find the example project here.

Taking Action

If a task can be recorded using Photoshop Actions, it can be scripted using the Action Manager. This, in some cases, means that we can do things that Photoshop’s “normal” scripting model doesn’t support. The question was: could changing the font of a character in a text layer be recorded?

To find out, I created a text layer and entered the word “text.” I then created a new Action and started recording. I selected the last character, and applied a font change, then stopped recording. I then created a new document, created a text layer, entered the word “text,” and ran the Action. Sure enough, the Action appeared to be formatting to the last character in the text layer. But I smelled a rat. I then replaced the text in the text layer with the characters “abcd” and ran the Action again. The Action happily replaced “abcd” with “text,” including the formatted “t” character.

I’d thought that I’d recorded an Action that would select the fourth or last character in a text layer and apply formatting to it; what I got instead was an action that would re-enter all of the text in a text layer, including the formatted character “t.”

Clearly, this wasn’t what I wanted to do.

I’m an optimist, however, so I turned to the Script Listener plug-in.

Listening to the Script Listener

The Script Listener is a Photoshop plug-in that records actions as JavaScript (or VBScript) code. For more on the Script Listener, including installation instructions, refer to “The Script Listener Plug-In” in the Adobe Photoshop CS5 Scripting Guide PDF (which you can find in the Documentation folder inside the Scripting folder in your Photoshop folder).

The code generated by the Script Listener isn’t exactly easy to read—at least compared to standard object model scripting. Here’s a chunk of output from the Script Listener:

// ======================================================= 
var idslct = charIDToTypeID( "slct" ); 
 	var desc4 = new ActionDescriptor(); 
 	var idnull = charIDToTypeID( "null" ); 
 	 	var ref2 = new ActionReference(); 
 	 	var idmoveTool = stringIDToTypeID( "moveTool" ); 
 	 	ref2.putClass( idmoveTool ); 
 	desc4.putReference( idnull, ref2 ); 
 	var iddontRecord = stringIDToTypeID( "dontRecord" ); 
 	desc4.putBoolean( iddontRecord, true ); 
 	var idforceNotify = stringIDToTypeID( "forceNotify" ); 
 	desc4.putBoolean( idforceNotify, true ); 
executeAction( idslct, desc4, DialogModes.NO );


Recorded actions use four-character codes to represent the various things going on in the action. It’s possible to guess what some of them are—”slct,” looks a lot like “select,” and so on. For most of them, however, you really need a magic decoder ring. Luckily, a partial listing exists in “Appendix A: Event ID Codes,” at the end of the Adobe Photoshop CS5 JavaScript Scripting Reference PDF (in the Documents folder of the Scripting folder inside your Photoshop folder). As I said, however, this is only a partial reference—when you look at the log file, you’ll see lots of four-character codes that don’t appear in the Appendix.

After installing the Script Listener plug-in, I went through the steps I detailed above, then looked at the ScriptingListenerJS.log file on my desktop. Not only had this one action generated 666 lines of code (an ominous number), but the recorded script did essentially the same thing as the Action: re-created the text layer from scratch, then re-applied all of the formatting.

By (somewhat intelligent) trial and error, I found which sections could be omitted, which sections did the sorts of things I was looking for, and which changes would crash Photoshop, or make the Photoshop scripting engine stop working, and so on. Over the course of a day or two, I was able to figure out which lines I needed to keep, and which to throw away. I still don’t know exactly how it works, but it does work.

The process of porting the ExtendScript from the Script Listener’s log file to ActionScript is straightforward—mostly just a matter of adding the relevant ActionScript types.

Setting Up

This time, I won’t bother with the AppModel and AppController classes that I’ve used in previous posts. I created the Photoshop-only project using the Creative Suite Extension Builder’s wizard. You can create the same files using the Creative Suite SDK without CS Extension Builder. Here’s main.mxml.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" historyManagementEnabled="false">
	<mx:Script>
		<![CDATA[

			[Bindable]
			private var hostName:String = HostObject.mainExtension;
			
		]]>
	</mx:Script>
	<mx:VBox height="100%" width="100%" verticalAlign="middle" horizontalAlign="center">
		<mx:Button label="Run PS code" click="PSTypesetting.run()" enabled="{hostName.indexOf('photoshop') > -1}"/>
	</mx:VBox>
</mx:Application>


This creates a panel with a single button, “Run PS Code.”

The Fun is in the Function

The setFormatting function wraps the somewhat unruly code that I got from the Script Listener plug-in. For the most part, you don’t really need to know what’s going on inside the function—you can simply give it the character range you want to format, the font and font style, the point size, and the character color, and it will take care of the rest. That said, I’ve tried to use variable names and comments to try to explain what’s going on. If you want to add other formatting features, it should be fairly easy to do so—but be prepared to wade through lengthy log files to find the ids and parameters you’ll need.

/**
* The setFormatting function sets the font, font style, point size, and RGB color of specified
* characters in a Photoshop text layer.
*
* @param start (int) the index of the insertion point *before* the character you want.,
* @param end (int) the index of the insertion point following the character.
* @param fontName is a string for the font name.
* @param fontStyle is a string for the font style.
* @param fontSize (Number) the point size of the text.
* @param colorArray (Array) is the RGB color to be applied to the text.
*/		
private static function setFormatting(start:int, end:int, fontName:String, fontStyle:String, fontSize:Number, colorArray:Array):void{	
	//Sanity checking: is the active layer a text layer?
	if(app.activeDocument.activeLayer is ArtLayer){
		var activeLayer:ArtLayer = app.activeDocument.activeLayer as ArtLayer;
		if(activeLayer.kind == LayerKind.TEXT){
			//More checking: does the text layer have content, and are start and end set to reasonable values?
			if((activeLayer.textItem.contents != "")&&(start >= 0)&&(end <= activeLayer.textItem.contents.length)){
				var idsetd:Number = app.charIDToTypeID( "setd" );
				var action:ActionDescriptor = new ActionDescriptor();
				var idnull:Number = app.charIDToTypeID( "null" );
				//The action reference specifies the active text layer.
				var reference:ActionReference = new ActionReference();
				var idTxLr:Number = app.charIDToTypeID( "TxLr" );
				var idOrdn:Number = app.charIDToTypeID( "Ordn" );
				var idTrgt:Number = app.charIDToTypeID( "Trgt" );
				reference.putEnumerated( idTxLr, idOrdn, idTrgt );
				action.putReference( idnull, reference );
				var idT:Number = app.charIDToTypeID( "T   " );
				var textAction:ActionDescriptor = new ActionDescriptor();
				var idTxtt:Number = app.charIDToTypeID( "Txtt" );
				//actionList contains the sequence of formatting actions.
				var actionList:ActionList = new ActionList();
				//textRange sets the range of characters to format.
				var textRange:ActionDescriptor = new ActionDescriptor();
				var idFrom:Number = app.charIDToTypeID( "From" );
				textRange.putInteger( idFrom, start );
				textRange.putInteger( idT, end );
				var idTxtS:Number = app.charIDToTypeID( "TxtS" );
				//The "formatting" ActionDescriptor holds the formatting. It should be clear that you can
				//add other attributes here--just get the relevant lines (usually 2) from the Script Listener 
				//output and graft them into this section.
				var formatting:ActionDescriptor = new ActionDescriptor();
				//Font name.
				var idFntN:Number = app.charIDToTypeID( "FntN" );
				formatting.putString( idFntN, fontName );
				//Font style.
				var idFntS:Number = app.charIDToTypeID( "FntS" );
				formatting.putString( idFntS, fontStyle );
				//Font size.
				var idSz:Number = app.charIDToTypeID( "Sz  " );
				var idPnt:Number = app.charIDToTypeID( "#Pnt" );
				formatting.putUnitDouble( idSz, idPnt, fontSize );
				//Fill color (as an RGB color).
				var idClr:Number = app.charIDToTypeID( "Clr " );
				var colorAction:ActionDescriptor = new ActionDescriptor();
				var idRd:Number = app.charIDToTypeID( "Rd  " );
				colorAction.putDouble( idRd, colorArray[0] );
				var idGrn:Number = app.charIDToTypeID( "Grn " );
				colorAction.putDouble( idGrn, colorArray[1]);
				var idBl:Number = app.charIDToTypeID( "Bl  " );
				colorAction.putDouble( idBl, colorArray[2] );
				var idRGBC:Number = app.charIDToTypeID( "RGBC" );
				formatting.putObject( idClr, idRGBC, colorAction );
				//end color.
				//
				textRange.putObject( idTxtS, idTxtS, formatting );
				actionList.putObject( idTxtt, textRange );
				textAction.putList( idTxtt, actionList );
				action.putObject( idT, idTxLr, textAction );
				app.executeAction( idsetd, action, DialogModes.NO );
			}
		}
	}
}


When you apply formatting using this function, you should be aware that the attributes that you don’t set (things like stroke color, kerning, etc.) will be formatted using the document’s default formatting, not the formatting of the other text in the text layer. For the purpose of this example, that’s not a problem, but you may need to add more to the formatting section in the function.

Note that you could create separate functions to set the font, the point size, or the color of the text. If you do this, the minimal amount of code would looks something like this:

/**
* The setFontSize function sets the size of specified
* characters in a Photoshop text layer.
*
* @param start (int) index of the insertion point *before* the character you want.,
* @param end (int) index of the the insertion point following the character.
* @param fontSize (Number) the point size of the text.
*/
private static function setFontSize(start:int, end:int, fontSize:Number):void{	
	//Sanity checking: is the active layer a text layer?
	if(app.activeDocument.activeLayer is ArtLayer){
		var activeLayer:ArtLayer = app.activeDocument.activeLayer as ArtLayer;
		if(activeLayer.kind == LayerKind.TEXT){
			//More checking: does the text layer have content, and are start and end set to reasonable values?
			if((activeLayer.textItem.contents != "")&&(start >= 0)&&(end <= activeLayer.textItem.contents.length)){
				//The indentation that the Script Listerner gives you is weird, but it’s useful.
				var idsetd:Number = app.charIDToTypeID( "setd" );
				var action:ActionDescriptor = new ActionDescriptor();
				var idnull:Number = app.charIDToTypeID( "null" );
				//The action reference specifies the active text layer.
				var reference:ActionReference = new ActionReference();
				var idTxLr:Number = app.charIDToTypeID( "TxLr" );
				var idOrdn:Number = app.charIDToTypeID( "Ordn" );
				var idTrgt:Number = app.charIDToTypeID( "Trgt" );
				reference.putEnumerated( idTxLr, idOrdn, idTrgt );
				action.putReference( idnull, reference );
				var idT:Number = app.charIDToTypeID( "T   " );
				var textAction:ActionDescriptor = new ActionDescriptor();
				var idTxtt:Number = app.charIDToTypeID( "Txtt" );
				//actionList contains the sequence of formatting actions.
				var actionList:ActionList = new ActionList();
				//textRange sets the range of characters to format.
				var textRange:ActionDescriptor = new ActionDescriptor();
				var idFrom:Number = app.charIDToTypeID( "From" );
				textRange.putInteger( idFrom, start );
				textRange.putInteger( idT, end );
				var idTxtS:Number = app.charIDToTypeID( "TxtS" );
				//The "formatting" ActionDescriptor holds the formatting. It should be clear that you can
				//add other attributes here--just get the relevant lines (usually 2) from the Script Listener 
				//output and graft them into this section.
				var formatting:ActionDescriptor = new ActionDescriptor();
				//Font size.
				var idSz:Number = app.charIDToTypeID( "Sz  " );
				var idPnt:Number = app.charIDToTypeID( "#Pnt" );
				formatting.putUnitDouble( idSz, idPnt, fontSize );
				textRange.putObject( idTxtS, idTxtS, formatting );
				actionList.putObject( idTxtt, textRange );
				textAction.putList( idTxtt, actionList );
				action.putObject( idT, idTxLr, textAction );
				app.executeAction( idsetd, action, DialogModes.NO );
			}
		}
	}
}


As you can see, most of the lines in the above function have to do with setting the target (the layer and the characters to format) and constructing the action. Only two lines do the actual work of applying the font size change. The essential mechanism for applying the formatting is all of the other stuff—which takes care of setting the target for the action: the characters inside the text layer. You can use the shell of this function to apply any sort of text formatting that can be recorded.

When you click the Run PS Code button in the example panel, the following function creates a new document, creates a text layer, enters text, and then formats the text using the functions shown above.

/**
* The run function creates a sample document, adds a text layer, and formats the text in the text layer.
*/		
public static function run():void 
{
	app.preferences.rulerUnits = Units.POINTS;
	var document:Document = app.documents.add(600, 600);
	var textLayer:ArtLayer = document.artLayers.add();
	textLayer.kind = LayerKind.TEXT;
	textLayer.textItem.contents = "The Literate Hen: A Text Layer";
	textLayer.textItem.font = "Tekton Pro";
	textLayer.textItem.size = 36;
	//Make certain that the layer is the active layer.
	app.activeDocument.activeLayer = textLayer;
	setFormatting(0, 17, "Tekton Pro", "Bold Condensed", 48, new Array(192, 192, 192));
	//You can apply more formatting without disturbing the existing formatting.
	//We’ll make the text "Hen" little and red.
	setFormatting(13, 17, "Tekton Pro", "Bold Condensed", 36, new Array(255, 0, 0));
	//Set just the size of the last word.
	setFontSize(25, 30, 48);
}


The Moral of the Story

At first, I had the idea that the script as recorded by the Script Listener was the law—that is, if you wanted to have Photoshop perform an Action, you’d have to do it exactly as it was recorded. As it turned out, I was wrong—large parts of the Actions I recorded could be safely discarded and/or rearranged.

This is an area I’d love to spend a bit more time in, because there are some other things that you can’t do from Photoshop’s scripting object model that you might be able to do using the Action Manager. Sure, the ideal (at least from my point of view) would be to have Photoshop’s scripting object model extended to cover those cases. But, in the meantime, we can use tricks like the one described in this post to get some work done.

I hope you found this post useful. If you did, or if you didn’t, or if you have ideas for future posts, I’d love to hear from you.

9 Responses to Formatting Text Ranges in Photoshop

  1. Jeremy Knudsen says:

    Wow! Awesome post, Olav! Thank you for providing the community with this beautiful gem!

    It is great to see how you can *set* type using ranges.Now, would it be possible to *get* type (its content and formatting) using ranges? It would be great to both “get” and “set” type using ranges!!!

    Again, thank you so much! This is seriously awesome work.

    Jeremy

  2. Jaroslav Polakovič says:

    Great article, thank you!

    I’m trying to solve a problem closely related to this one – I need to _retrieve_ text ranges with different formatting from a text layer in PS. Any idea how to approach this?

    I thing having both set and get functions to manipulate text ranges would be a real breakthrough. I mean REAL breakthrough (… for a very few people :).

  3. Olav Kvern says:

    Hi Jaroslav,

    I agree that it would be very useful! I can think of a few approaches (all of them ugly)–give me a bit, and we’ll see what I can do.

    Thanks,

    Ole

  4. Jeremy Knudsen says:

    Count me among the few! Thanks again Ole for your hard work and dedication!

  5. Olav Kvern says:

    Hi Jeremy,

    Thanks for your kind words! I haven’t forgotten about this, but I had to put it aside for a bit (after running into dead ends in all of the approaches I could think of!). But it seems to me that there must be a way to do it.

    Ole

  6. Jeremy Knudsen says:

    Thanks Ole! In the meantime, I am appreciating your other posts. :)

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

  8. David says:

    I’m fiddling with the same problem: trying to *get* the formatting from Photoshop text layers. So far I’m the closest with this article, where I can see it’s possible to *set* these, but after a few hours of searching, I still have no idea about getting the data.
    Have you made any progress in this direction in the past one year? I really want to solve this, so if you could provide with any materials or ideas you have, that could be a big help for me.
    If I make any progress / can build a working script, I’ll certainly share it with you all.

  9. Lars says:

    Hey everyone!

    I’m also working on the problem of extracting text-format information of a PSD.
    If people are still interested, I found a way, that will definitely work, but will be very work-intensive:

    When you open a PSD in a texteditor you will see some XML in the top of the document. This contains information about the document that you can already get through javascript functions in photoshop. I.e.: the content of textlayers without any format information.
    So far so good, nothing gained yet. But further down in the document is the information we want. Search for “ParagraphSheet” and you will find some XML-like structure. This contains the format-information of all paragraphs. When you scroll to the end of the PragraphSheets you will find a line called “RunLengthArray”. This tells you how many characters there are per paragraph section. I.e:
    [5 10 4] means you have 3 paragraphs. The first has 5 cahracters, the second 10 and the last one 4. The format-information of the paragraphs are written in the respective ParagraphSheets.

    Below the ParagraphSheets there are the StyleSheets. This contains the actual character-format information. This works the same way. You have some StyleSheetData sections equivalent to the number of different character-formats you use. At the end of the whole section you again find a “RunLengthArray” which contains the information how many characters use the respective format.

    I’ll continue to look for a simpler and faster implemented way because writing a parser for this would take quite some time. But maybe it helps some of you if there isn’t another way or time isn’t an issue for you.

    Still – if someone has found another or easier way please leave a comment, it would be a ton of help!! :-)

    Cheers