More Basics: Importing and Exporting Files

One of my co-workers (let’s call him “Zak”) recently referred to this series of blog postings as “an expanded Hello World.” Zak wasn’t being critical; he thinks this is a good thing, and I agree. The point is to get developers past the “what do I do now?” hump—the one that follows immediately after you’ve set up your development environment and worked through your first (usually trivial) tutorial example extension.

What I’m trying to do is make sure that new developers don’t get stuck because of some application-specific peculiarity of one of the Creative Suite applications. I know that this can happen, because it’s happened to me. Often.

In this post, we’ll turn back to the “Big Three” applications of the Creative Suite: Illustrator, InDesign, and Photoshop. All three applications can import, or “place” documents, and can export or save files in a wide variety of file formats. Each program has an idiosyncratic way of doing this basic task, so I’ll create a generic wrapper function and encapsulate (hide) all of the application-specific details.

This post follows in the footsteps of earlier posts, notably ”Drawing Paths,” ”Entering and Formatting Text,” and ”Watching the Detections,” and continues to build our basic Creative Suite SDK construction kit.

You can find the project for this example here:
importexport

Setting Up

I’ve used the same structure as in the other examples in this series: a very minimal main.mxml file, an AppController class (where the non-application-specific action takes place), an AppModel class (for common variables), the DocumentWatcher class (which monitors CSXS events), and separate ActionScript files for each application.

<?xml version="1.0" encoding="utf-8"?>
<csxs:CSXSWindowedApplication
	xmlns:csxs="com.adobe.csxs.core.*" 
	xmlns:mx="http://www.adobe.com/2006/mxml" 
	layout="absolute" 
	historyManagementEnabled="false" 
	creationComplete="onCreationComplete()"
	applicationComplete="onApplicationComplete()" 
	close="onClose()"
	>
	<mx:Script>
		<![CDATA[
			import importexport.*
				
				[Bindable]
			private var model:AppModel = null; 
			private var controller:AppController = null;
			private var watcher:DocumentWatcher = null;
			private function onCreationComplete():void{
				model = AppModel.getInstance();				
				model.readyState();
			}
			private function onApplicationComplete():void{
				controller = AppController.getInstance();
				watcher = DocumentWatcher.getInstance();
				controller.attach();
			}
			private function onCheckboxClick():void{
				if(model.exportSelection == true){
					model.exportSelection = false;
				}
				else{
					model.exportSelection = true;
				}
			}
			private function selectFormat(event:Event):void{
				model.formatName = model.formatNames[FormatsComboBox.selectedIndex];
				//Have the controller update the user interface to activate/deactivate the checkbox, if necessary.
				controller.handleEvent();
			}
			private function onClose():void{
				controller.detach();
			}
		]]>
	</mx:Script>
	<mx:VBox height="100%" width="100%" verticalAlign="middle" horizontalAlign="center">
		<mx:HBox>
			<mx:Button label="Import File" click="controller.importFile()" enabled="{model.documentOpen}" width="100"/>			
		</mx:HBox>
		<mx:HRule width="100%"/>
		<mx:HBox>
			<mx:Label 
				text="Export Format:" 
				width="100" textAlign="right"/>
			<mx:ComboBox 
				enabled="{model.documentOpen}"
				width="80"
				id="FormatsComboBox"
				dataProvider="{model.formatNames}" 
				change="selectFormat(event)"/>
		</mx:HBox>
		<mx:HBox>
			<mx:CheckBox label="Export Selected" visible="{model.showCheckbox}" enabled="{model.enableCheckbox}" buttonMode="{model.exportSelection}" click = "onCheckboxClick()" />			
		</mx:HBox>
		<mx:HBox>
			<mx:Button label="Export File" click="controller.exportFile()" enabled="{model.documentOpen == true}" width="100"/>			
		</mx:HBox>
	</mx:VBox>
</csxs:CSXSWindowedApplication>


This example features a fancier user interface than most of the examples I’ve presented to date—look, there’s a providing a horizontal line to separate the import and export sections of the panel. We’ll have glowing, throbbing buttons before you know it.

I’ve added the DocumentWatcher class that we’ve used before. This module takes care of notifying the extension on particular events in the host application: file open, close, and so on—we need this to manage the enabled/disabled state of the buttons in the panel’s user interface. See ”Watching the Detections” for a more complete description of this class.

Note, again, that this extension will start the application-specific event listeners in response to the ApplicationComplete event to avoid crashes that can occur when the same event listeners are loaded after CreationComplete (see the discussion of event timing in ”Path Effects”).

The AppModel class is roughly the same as I’ve presented before—it’s mostly made up of our standard routine for determining the host application of the panel. In this one, I’ve defined a few global variables for the other modules to call on.

public var hostName:String = "";
[Bindable]
public var documentOpen:Boolean = false;
public var itemSelected:Boolean = false;
public var formatNames:Array = new Array("JPEG", "SWF", "PDF");
public var formatName:String = "JPEG";
public var enableCheckbox:Boolean = false;
public var showCheckbox:Boolean = false;
public var exportSelection:Boolean = false;


The AppController class takes care of getting file references for import/export, then dispatches the file to the relevant host application. This class takes care of showing the Export Selected checkbox in the panel’s user interface when the host application is InDesign, and changes the an export file type if the host application is Photoshop.

package importexport
{
	import flash.events.Event;
	import flash.filesystem.File;
	import flash.net.FileReference;
	
	public class AppController
	{
		import importexport.*;
		private static var instance:AppController;
		private var model:AppModel = AppModel.getInstance();
		
		public static function getInstance() : AppController 
		{
			if (instance == null)
			{
				instance = new AppController();
			}
			return instance;
		}
		public function importFile():void 
		{
			//Get a file to import.
			var file:File = new File();
			file.addEventListener(Event.SELECT,handleImportEvent);
			file.browseForOpen("Select a File");
		}
		public function exportFile():void 
		{
			//Get a file to export.
			var file:File = new File();
			file.addEventListener(Event.SELECT,handleExportEvent);
			file.browseForSave("Save File As");
		}
		private function handleImportEvent(event:Event):void
		{
			switch(model.hostName){
				case "indesign":
					importexportInDesign.importFile(event.target);
					break;
				case "illustrator":
					importexportIllustrator.importFile(event.target);
					break;
				case "photoshop":
					importexportPhotoshop.importFile(event.target);
					break;
			}			
		}
		public function handleExportEvent(event:Event):void 
		{
			switch(model.hostName){
				case "indesign":
					importexportInDesign.exportFile(event.target);
					break;
				case "illustrator":
					importexportIllustrator.exportFile(event.target);
					break;
				case "photoshop":
					importexportPhotoshop.exportFile(event.target);
					break;
			}
		}
		public function attach():void 
		{
			switch(model.hostName){
				case "indesign":
					model.showCheckbox = true;
					importexportInDesign.attach();
					break;
				case "illustrator":
					model.showCheckbox = true;
					importexportIllustrator.attach();
					break;
				case "photoshop":
					//No Photoshop-specific events.
					//Photoshop can’t save/export as SWF, so change the second option to TIFF.
					model.formatNames[1] = "TIFF";
					break;
			}
		}
		
		public function detach():void 
		{
			switch(model.hostName){
				case "indesign":
					importexportInDesign.detach();
					break;
				case "illustrator":
					importexportIllustrator.detach();
					break;
				case "photoshop":
					//No Photoshop-specific events.
					break;
			}
		}
		
		public function handleEvent():void
		{
			switch(model.hostName){
				case "indesign":
					importexportInDesign.handleEvent();
					break;
				case "illustrator":
					importexportIllustrator.handleEvent();
					break;
				case "photoshop":
					importexportPhotoshop.handleEvent();
					break;
			}
		}
	}
}


Both the importFile and exportFile functions (which are triggered by clicking the buttons in the panel’s user interface) create a File object, then add an event listener to the object, and then display an open/save dialog box. When you specify a file in the dialog box and click the OK button, the File object fires the Event.SELECT event and the corresponding event listener runs the relevant event handler function (handleImportEvent or handleExportEvent).

Application Specifics

You’d think that, when it comes to something as fundamental as importing a file, these three applications might be able to agree on a common approach. Alas, that’s not the case; each does things its own way. But we can create our own function to simplify the process. In this case, I’ll show how to place a file in the current view of the front-most document. I’ve added some application-specific commentary in the following sections. When you use this function in a real extension, you’ll probably want to fine tune the file import/export options.

In the following examples, I use app.activeDocument several times. In general, it’s a good idea to check for app.activeDocument == null—if no documents are open, the application will throw an error on app.activeDocument. In this case, we don’t need to worry about it, because the buttons in the panel’s user interface (and, therefore, the related functions) won’t be available unless a document is open.

Illustrator

public static function importFile(file:Object):void{
	var placedItem:PlacedItem = app.activeDocument.placedItems.add();
	placedItem.file = file as File;
}


Import options for PDF files are stored in app.preferences.pdfFileOptions (the type of this object, oddly enough, is OpenOptionsPDF—Note to Yoda: the property name similar to the object type name make). The settings in this object are applied to PDF files you import. I haven’t found import options for other graphic file types in Illustrator’s scripting object model, but I’d assume that the current settings predominate when placing files.

When you export a file from Illustrator, the exportFile method takes a file reference, an export format, and export options as parameters. The format is an ExportType enumeration; the third parameter is an export options object (ExportOptionsJPEG or ExportOptionsFlash, for this example). For Illustrator, PDF is something you save, rather than export, so you use the saveAs method to write the PDF to disk.

public static function exportFile(file:Object):void{
	var format:ExportType;
	var exportOptions:Object;
	//Get the index of the active artboard.
	var index:int = app.activeDocument.artboards.getActiveArtboardIndex();
	switch(model.formatName){
		case "JPEG":
			format = ExportType.JPEG;
			exportOptions = new ExportOptionsJPEG();
			//Add other export options here.
			app.activeDocument.exportFile( file as File, format, exportOptions);
			break;
		case "SWF":
			format = ExportType.FLASH;
			exportOptions = new ExportOptionsFlash();
			//Add other export options here.
			exportOptions.saveMultipleArtboards = false;
			exportOptions.artboardRange = String(index);
			app.activeDocument.exportFile( file as File, format, exportOptions);
			break;
		case "PDF":
			//PDFs aren’t exported, they’re saved. Like Photoshop, the format of
			//a saved file depends on the save options parameter of the saveAs method.
			var saveOptions:PDFSaveOptions = new PDFSaveOptions();
			//Add other PDF save options here.
			saveOptions.artboardRange = String(index);
			app.activeDocument.saveAs(file as File, saveOptions);
			break;				
	}
}


In this example, I set the export/save preferences in the relevant preferences object (ExportOptionsJPEG, ExportOptionsFlash, or PDFSaveOptions), then export the file.

InDesign

public static function importFile(file:Object):void
{
	app.activeWindow.activePage.place(file);
}


In InDesign, you can place a file on a page, on a spread, inside a page item, or in text. For the purposes of this example, we’ll simply place the file on the current page. This example doesn’t deal with import options, but the current properties in the preferences object for the file type you’re importing (e.g., jpegImportPreferences, swfImportPreferences, or pdfImportPreferences—all on the app object) will be applied when you place the file.

public static function exportFile(file:Object):void
{
	var format:ExportFormat;
	switch(model.formatName)
	{
		case "JPEG":
			//Set the export page range to the current page.
			app.jpegExportPreferences.jpegExportRange = ExportRangeOrAllPages.EXPORT_RANGE;
			app.jpegExportPreferences.pageString = app.activeWindow.activePage.name;
			format = ExportFormat.JPG;
			break;
		case "SWF":
			if(model.exportSelection == true){
				app.swfExportPreferences.pageRange = PageRange.SELECTED_ITEMS;
			}
			else{
				app.swfExportPreferences.pageRange = app.activeWindow.activePage.name;
			}
			format = ExportFormat.SWF;
			break;
		case "PDF":
			if(model.exportSelection == true){
				app.pdfExportPreferences.pageRange = PageRange.SELECTED_ITEMS;
			}
			else{
				app.pdfExportPreferences.pageRange = app.activeWindow.activePage.name;
			}
			format = ExportFormat.PDF_TYPE;
			break;
	}
	
	app.activeDocument.exportFile(format, file as File, false);
}


InDesign can export the selected items when you’re exporting to a SWF, so I’ve added a checkbox to the panel that will become available when you have objects selected and have chosen SWF as your export format. Here’s the event listener that takes care of enabling/disabling this option (by setting the model.enableCheckbox variable):

public static function attach():void{
	IDScriptingEventAdapter.getInstance().addEventListener(Application.AFTER_SELECTION_CHANGED, handleIDEvent);
}
public static function detach():void 
{
	try{
		IDScriptingEventAdapter.getInstance().removeEventListener(Application.AFTER_SELECTION_CHANGED, handleIDEvent);
	}
	catch(error:Error){}
}
//InDesign event handlers expect an event object, so we’ll 
//create a dummy handler to strip the event and call the generic
//event handler.
public static function handleIDEvent(event:Event):void 
{
	handleEvent();
}		
public static function handleEvent():void 
{
	if(!app.modalState){
		var result:Boolean = false;
		if(app.documents.length > 0)
		{
			//A document is open, so enable the Import File button.
			model.documentOpen = true;
			if(app.selection.length > 0){
				for(var counter:int = 0; counter < app.selection.length; counter++){
					switch(true){
						case app.selection[counter] is Rectangle:
						case app.selection[counter] is Oval:
						case app.selection[counter] is Polygon:
						case app.selection[counter] is GraphicLine:
						case app.selection[counter] is TextFrame:
						case app.selection[counter] is Group:
							//If the current export format is SWF, enable the Export Selected checkbox.
							if(model.formatName == "SWF"){
								result = true;
							}
							break;
					}
					if(result == true){
						break;
					}
				}
			}
		}
		else{
			model.documentOpen = false;
		}
	}
	model.enableCheckbox = result;
}


Note: If you’re familiar with the InDesign scripting object model, you probably know that all page items have an exportFile method. This leads one (or, me, anyway) to think that each page item can be exported to a file, independent of other page items on the same page. This is not the case. When you use the exportFile method, the page range defined in the export preferences object (for the specific export format) defines what gets exported.

Next, the pdfExportPreferences object includes a pageRange property, and the documentation for the property states that it can be a PageRange enumerator. PageRange.SELECTED_ITEMS, however, does not work when you’re exporting PDF—only PageRange.ALL_PAGES.

In short: if you don’t see an option (exporting the selection, in this case) *somewhere* in the user interface, you probably can’t use that option in scripting. In this case, the Export PDF dialog box doesn’t show anything about exporting the selection, and scripting abides by that limitation.

That said, there are a number of ways that you could write a routine for exporting individual page items or collections of page items. Most of these would involve duplicating the items to a new page or document, then exporting from there. I may revisit this project at some point to add that feature.

Photoshop

I could not find a simple place method anywhere in Photoshop’s scripting object model, so I made my own using an Action Manager script. For more on scripting Photoshop using the Action Manager, see ”Formatting Text Ranges in Photoshop”.

public static function importFile(file:Object):void{
	//I couldn’t find a way to place a file apart from using the Action Manager.
	//This action places the file on a new layer in the center of the current
	//document (just as the File>Place command does).
	var idPlc:Number = app.charIDToTypeID( "Plc " );
	var actionDescriptor:ActionDescriptor = new ActionDescriptor();
	var idnull:Number = app.charIDToTypeID( "null" );
	actionDescriptor.putPath( idnull, file as File);
	var idFTcs:Number = app.charIDToTypeID( "FTcs" );
	var idQCSt:Number = app.charIDToTypeID( "QCSt" );
	var idQcsa:Number = app.charIDToTypeID( "Qcsa" );
	actionDescriptor.putEnumerated( idFTcs, idQCSt, idQcsa );
	var idOfs:Number = app.charIDToTypeID( "Ofst" );
	var positionDescriptor:ActionDescriptor = new ActionDescriptor();
	var idHrzn:Number = app.charIDToTypeID( "Hrzn" );
	var idRlt:Number = app.charIDToTypeID( "#Rlt" );
	positionDescriptor.putUnitDouble( idHrzn, idRlt, -0.000000 );
	var idVrtc:Number = app.charIDToTypeID( "Vrtc" );
	idRlt = app.charIDToTypeID( "#Rlt" );
	positionDescriptor.putUnitDouble( idVrtc, idRlt, 0.000000 );
	var idOfst:Number = app.charIDToTypeID( "Ofst" );
	actionDescriptor.putObject( idOfst, idOfst, positionDescriptor );
	app.executeAction( idPlc, actionDescriptor, DialogModes.NO );			
}


For Photoshop, all of the operations related to this example are “save,” not “export” actions. When saving files, Photoshop works in roughly the same fashion as Illustrator: the saveAs method takes a save options parameter that defines the file format for the saved file.

public static function exportFile(file:Object):void{
	//In Photoshop, the type of the saveOptions paramter of the saveAs method
	//determines the saved file type. Each format has a different set of options.
	var saveOptions:Object;
	switch(model.formatName){
		case "JPEG":
			saveOptions = new JPEGSaveOptions();
			//Set JPEG save options here.
			saveOptions.quality = 12;
			saveOptions.formatOptions = FormatOptions.PROGRESSIVE;
			saveOptions.scans = 2;
			break;
		case "TIFF":
			saveOptions = new TiffSaveOptions();
			//Set TIFF save options here.
			saveOptions.layers = true;
			saveOptions.alphaChannels = true;
			saveOptions.imageCompression = TIFFEncoding.TIFFZIP;
			saveOptions.transparency = true;
			break;
		case "PDF":
			saveOptions = new PDFSaveOptions();
			//Set PDF save options here.
			saveOptions.view = false;
			//If you need to export to a particular PDF standard, you can use the
			//saveOptions.PDFStandard property, as shown in the line below.
			//saveOptions.PDFStandard = PDFStandard.PDFX42008;
			break;
	}
	app.activeDocument.saveAs(file as File, saveOptions, true);
}


Testing the Extension

When you click the Import File button, the extension displays a standard Open File dialog box. Select a file and click the OK button, and the host application will import the file and place it in the current view.

Open a file, select a file format from the pop-up menu, and click the Export File button. The extension will display a standard Save As dialog box. Enter a name for the exported file and click the OK button, and the host application will export the file. That’s all there is to it!

Another Step Toward a Common Object Model

At this point, we have a simple importFile function—feed it a file reference, and the host application will take care of getting the file into the current document. The exportFile function works the same way—give it a place to save a file, and the host application will export the current page to that location. We’ve added another (small) piece to our “common DOM” for Illustrator, InDesign, and Photoshop.

If you enjoyed this post, or have an idea for a future article, please drop me a line or write a comment. This blog is for you, after all; left to my own devices, I’d just put up more goofy scripts for creating geometric art.

3 Responses to More Basics: Importing and Exporting Files

  1. Pingback: Tweets that mention More Basics: Importing and Exporting Files « Creative Suite SDK Team -- Topsy.com

  2. Ilya says:

    Hi, thank you for good post.
    Where I can get the source code?