abitofcode

Export Flash library items as PNG’s with JSFL

After last weeks news from Adobe I’m happy I held back on upgrading from Flash CS3. Flash though is still a valuable tool in the development process allowing the quick prototyping of game mechanics.

What I’ve ended up doing on more than one occasion however is prototyping something in Flash, then finding I need to get the information out of flash into a form I can use elsewhere e.g for iOS development. Flash comes with an extension language called JSFL (JavaScript Flash) that allows scripted manipulation of the authoring environment, Adobe in their infinite wisdom decided that producing decent documentation for this killer feature was not worth the effort. Fortunately Keith Peters and Todd Yard filled the void with the book ‘Extending Flash MX 2004‘ it’s still a valid book today.

In a recent project I needed to run through an .fla used to generate a swc and export all the items in the library that had a linkageIdentifier as separate PNG images. It took longer than it should have as I made all the mistakes I’d made on previous JSFL projects, so this time I commented the code and jotted down the issues for future reference. Another source of useful information is Writing a JSFL component for Flash AS3 where there is a link to a comprehensive API specification for JSFL in pdf format.

The following JSFL scripts have been tested with Flash CS3 on Lion (OSX)

1. Getting started

Scripts saved with a .jsfl extension in the correct directory will show up in the Commands menu within the flash IDE. You do not need to restart flash for any changes or new scripts to be detected so I tend to open the directory in finder and use textmate (use your editor of choice) to build the scripts.

>cd ~/Library/Application\ Support/Adobe/Flash\ CS3/en/Configuration/Commands
>touch test.jsfl
>open .

Here the backslashes in the path are escaping the spaces, and the tilda (~) is a shortcut to the users home directory.
‘touch test.jsfl’ creates an empty file called ‘test.jsfl’
‘open .’ opens a finder window in the current directory from the terminal window.

The following file loops through all the library items and writes their names to the output window, if you have no library items, you’ll have no output.

Download the file here: [download id=”6″ format=”2″]

// Clear the output panel 
fl.outputPanel.clear()
 
// Get the Document object of the currently active
// document (FLA file)
var doc = fl.getDocumentDOM();
 
// Loop through all the items in the library
for (idx in doc.library.items) {
 
    // get a reference to the current item
    var currentItem = doc.library.items[idx];
 
    fl.trace('item:' + currentItem.name);
}

2. Export library items with a linkage as PNG’s

The following script is a modified version of one I found a while back from http://www.johnnystorm.com with comments added and simplified to do what I needed.

It loops through all the library items, the copies each item that has a linkage to the stage before selecting all the items on the stage and cutting to the clipboard. A publish profile is generated before each image is published that has the width, height and file path set for the current library item.

A new temporary document is then created and the clipboard is pasted onto it. The width and height of the new document are adjusted to the size of the item and the item is placed correctly within the bounds of the document. The document is then published using the profile generated earlier which result in an image (PNG) being produced.

Finally we close the new document before going onto the next library item.

Download the file here: [download id=”7″ format=”2″]

// Create a reference to the publishing profile we generate for each file
var profpath = fl.configURI + 'Publish%20Profiles/png.xml';
 
// Clear the output panel 
fl.outputPanel.clear()
 
// Get a location to export images to. trim the file:/// from it using join and split
var folderURI = decodeURI(fl.browseForFolderURL("Select a folder.").split("file:///").join(""));
 
// split path into an array on the forward slash
var folderItems = folderURI.split("/");
 
// On the Mac we need to remove the first item in the path
var saveDir = "/" + folderItems.slice(1,folderItems.length).join("/") + "/";
 
// if there is a save Directory specified then process the library items
if (saveDir) {
    // Get the Document object of the currently active document (FLA file)
    var doc = fl.getDocumentDOM();
 
    // Loop through all the items in the library
    for (idx in doc.library.items) {
 
        // get a reference to the current item
        var currentItem = doc.library.items[idx];
 
        // Check of the linkageIdentifier property of the current item <span class="hiddenGrammarError" pre="movie "><span class="hiddenGrammarError" pre="movie ">is
        // set</span></span>, if not ignore the item.
        if(currentItem.linkageIdentifier != undefined) {
 
            // export the current item as a png
            exportItemAsPng(currentItem)
        }
    }
}
 
function exportItemAsPng(item) {
 
    // build a filename using the linkageIdentifier 
    var pngName = saveDir + item.linkageIdentifier +".png";
 
    // selects the specified library item (true = replace current selection)
    doc.library.selectItem(item.name, true);
 
    // gets an array of all currently selected items in the library.
    var selectedItems = doc.library.getSelectedItems();
 
    // Add the current library item to the stage
    doc.library.addItemToDocument({x:0, y:0});
 
    // array of selected items in the document
    var w = doc.selection[0].width;
    var h = doc.selection[0].height;
 
    // create a publishing profile for this output with the filename, width and height set
    createProfile(item.linkageIdentifier, w, h, saveDir);
 
    // cuts the current selection from the document and writes it to the Clipboard.
    doc.clipCut();
 
    // Create a new temporary document to paste the clip held in the clipboard 
    fl.createDocument();    
 
    // get a handle on the currently focused document (the temporary one)
    exportdoc = fl.getDocumentDOM();
 
    // Setup the document to use the profile we generated for it useing 
    // the tenmplate
    exportdoc.importPublishProfile(profpath);        
    exportdoc.currentPublishProfile = "png";
 
    // pastes the contents of the Clipboard into the document, defaults to 
    // adding it at the center of the document    
    exportdoc.clipPaste();
 
    // Selects all items on the Stage
    exportdoc.selectAll();
 
    // We are only adding one item to each document so we only need to get the 
    // first item in the array of selected items
    var selectedItem = exportdoc.selection[0];
 
    // set the dimensions of the output movie to match the dimensions of our selected clip
    exportdoc.width = Math.floor(w);
    exportdoc.height = Math.floor(h);
 
    // Move the selection to fit the dimensions of the export movie. 
    exportdoc.moveSelectionBy({x:-selectedItem.left, y:-selectedItem.top});
 
    // Deselect the selected item
    exportdoc.selectNone();    
 
    // Publish the current document using the png profile we set up earlier
    exportdoc.publish(); 
 
    // access the document that is currently focused (the temporary one) and
    // close it. Do not promt the user to save changes
    exportdoc.close(false);    
 
    // trace its name in the output panel
    fl.trace('saving:' + pngName);
}
 
function createProfile(name, w, h, path) {
	profile = 	'<?xml version="1.0"?>\n' + 
	'<flash_profile version="1.0" name="Default">\n' + 
	'  <PublishFormatProperties enabled="true">\n' + 
	'    <defaultNames>0</defaultNames>\n' + 
	'    <flash>0</flash>\n' + 
	'    <generator>0</generator>\n' + 
	'    <projectorWin>0</projectorWin>\n' + 
	'    <projectorMac>0</projectorMac>\n' + 
	'    <html>0</html>\n' + 
	'    <gif>0</gif>\n' + 
	'    <jpeg>0</jpeg>\n' + 
	'    <png>1</png>\n' + 
	'    <qt>0</qt>\n' + 
	'    <rnwk>0</rnwk>\n' + 
	'    <flashDefaultName>0</flashDefaultName>\n' + 
	'    <generatorDefaultName>1</generatorDefaultName>\n' + 
	'    <projectorWinDefaultName>1</projectorWinDefaultName>\n' + 
	'    <projectorMacDefaultName>1</projectorMacDefaultName>\n' + 
	'    <htmlDefaultName>1</htmlDefaultName>\n' + 
	'    <gifDefaultName>1</gifDefaultName>\n' + 
	'    <jpegDefaultName>0</jpegDefaultName>\n' + 
	'    <pngDefaultName>0</pngDefaultName>\n' + 
	'    <qtDefaultName>1</qtDefaultName>\n' + 
	'    <rnwkDefaultName>1</rnwkDefaultName>\n' + 
	'    <flashFileName>' + path + name + '.swf</flashFileName>\n' + 
	'    <generatorFileName>' + path + name + '.swt</generatorFileName>\n' + 
	'    <projectorWinFileName>' + path + name + '.exe</projectorWinFileName>\n' + 
	'    <projectorMacFileName>' + path + name + '.app</projectorMacFileName>\n' + 
	'    <htmlFileName>' + path + name + '.html</htmlFileName>\n' + 
	'    <gifFileName>' + path + name + '.gif</gifFileName>\n' + 
	'    <jpegFileName>' + path + name + '.jpg</jpegFileName>\n' + 
	'    <pngFileName>' + path + name + '.png</pngFileName>\n' + 
	'    <qtFileName>' + path + name + '.mov</qtFileName>\n' + 
	'    <rnwkFileName>' + path + name + '.smil</rnwkFileName>\n' + 
	'  </PublishFormatProperties>\n' + 
	'  <PublishHtmlProperties enabled="true">\n' + 
	'    <VersionDetectionIfAvailable>0</VersionDetectionIfAvailable>\n' + 
	'    <VersionInfo>9,0,115,0;8,0,24,0;7,0,14,0;6,0,79,0;5,0,58,0;4,0,32,0;3,0,8,0;2,0,1,12;1,0,0,1;</VersionInfo>\n' + 
	'    <UsingDefaultContentFilename>1</UsingDefaultContentFilename>\n' + 
	'    <UsingDefaultAlternateFilename>1</UsingDefaultAlternateFilename>\n' + 
	'    <ContentFilename>'+ name + '.html</ContentFilename>\n' + 
	'    <AlternateFilename>'+ name + 'html</AlternateFilename>\n' + 
	'    <UsingOwnAlternateFile>0</UsingOwnAlternateFile>\n' + 
	'    <OwnAlternateFilename></OwnAlternateFilename>\n' + 
    '    <Width>' + w + '</Width>\n'+
    '    <Height>' + h + '</Height>\n'+
	'    <Align>0</Align>\n' + 
	'    <Units>0</Units>\n' + 
	'    <Loop>1</Loop>\n' + 
	'    <StartPaused>0</StartPaused>\n' + 
	'    <Scale>0</Scale>\n' + 
	'    <HorizontalAlignment>1</HorizontalAlignment>\n' + 
	'    <VerticalAlignment>1</VerticalAlignment>\n' + 
	'    <Quality>4</Quality>\n' + 
	'    <WindowMode>0</WindowMode>\n' + 
	'    <DisplayMenu>1</DisplayMenu>\n' + 
	'    <DeviceFont>0</DeviceFont>\n' + 
	'    <TemplateFileName>/Users/abitofcode/Library/Application Support/Adobe/Flash CS3/en/Configuration/HTML/Default.html</TemplateFileName>\n' + 
	'    <showTagWarnMsg>1</showTagWarnMsg>\n' + 
	'  </PublishHtmlProperties>\n' + 
	'  <PublishFlashProperties enabled="true">\n' + 
	'    <TopDown>0</TopDown>\n' + 
	'    <Report>0</Report>\n' + 
	'    <Protect>0</Protect>\n' + 
	'    <OmitTraceActions>0</OmitTraceActions>\n' + 
	'    <Quality>100</Quality>\n' + 
	'    <StreamFormat>0</StreamFormat>\n' + 
	'    <StreamCompress>7</StreamCompress>\n' + 
	'    <EventFormat>0</EventFormat>\n' + 
	'    <EventCompress>7</EventCompress>\n' + 
	'    <OverrideSounds>0</OverrideSounds>\n' + 
	'    <Version>8</Version>\n' + 
	'    <ExternalPlayer></ExternalPlayer>\n' + 
	'    <ActionScriptVersion>1</ActionScriptVersion>\n' + 
	'    <PackageExportFrame>1</PackageExportFrame>\n' + 
	'    <PackagePaths></PackagePaths>\n' + 
	'    <AS3PackagePaths></AS3PackagePaths>\n' + 
	'    <DebuggingPermitted>0</DebuggingPermitted>\n' + 
	'    <DebuggingPassword></DebuggingPassword>\n' + 
	'    <CompressMovie>1</CompressMovie>\n' + 
	'    <FireFox>0</FireFox>\n' + 
	'    <InvisibleLayer>1</InvisibleLayer>\n' + 
	'    <DeviceSound>0</DeviceSound>\n' + 
	'    <StreamUse8kSampleRate>0</StreamUse8kSampleRate>\n' + 
	'    <EventUse8kSampleRate>0</EventUse8kSampleRate>\n' + 
	'    <UseNetwork>0</UseNetwork>\n' + 
	'    <DocumentClass></DocumentClass>\n' + 
	'    <AS3Strict>1</AS3Strict>\n' + 
	'    <AS3Coach>1</AS3Coach>\n' + 
	'    <AS3AutoDeclare>1</AS3AutoDeclare>\n' + 
	'    <AS3Dialect>AS3</AS3Dialect>\n' + 
	'    <AS3ExportFrame>1</AS3ExportFrame>\n' + 
	'    <AS3Optimize>1</AS3Optimize>\n' + 
	'    <ExportSwc>0</ExportSwc>\n' + 
	'    <ScriptStuckDelay>15</ScriptStuckDelay>\n' + 
	'  </PublishFlashProperties>\n' + 
	'  <PublishJpegProperties enabled="true">\n' + 
    '    <Width>' + w + '</Width>\n'+
    '    <Height>' + h + '</Height>\n'+
	'    <Progressive>0</Progressive>\n' + 
	'    <DPI>4718592</DPI>\n' + 
	'    <Size>0</Size>\n' + 
	'    <Quality>80</Quality>\n' + 
	'    <MatchMovieDim>1</MatchMovieDim>\n' + 
	'  </PublishJpegProperties>\n' + 
	'  <PublishRNWKProperties enabled="true">\n' + 
	'    <exportFlash>1</exportFlash>\n' + 
	'    <flashBitRate>0</flashBitRate>\n' + 
	'    <exportAudio>1</exportAudio>\n' + 
	'    <audioFormat>0</audioFormat>\n' + 
	'    <singleRateAudio>0</singleRateAudio>\n' + 
	'    <realVideoRate>100000</realVideoRate>\n' + 
	'    <speed28K>1</speed28K>\n' + 
	'    <speed56K>1</speed56K>\n' + 
	'    <speedSingleISDN>0</speedSingleISDN>\n' + 
	'    <speedDualISDN>0</speedDualISDN>\n' + 
	'    <speedCorporateLAN>0</speedCorporateLAN>\n' + 
	'    <speed256K>0</speed256K>\n' + 
	'    <speed384K>0</speed384K>\n' + 
	'    <speed512K>0</speed512K>\n' + 
	'    <exportSMIL>1</exportSMIL>\n' + 
	'  </PublishRNWKProperties>\n' + 
	'  <PublishGifProperties enabled="true">\n' + 
    '    <Width>' + w + '</Width>\n'+
    '    <Height>' + h + '</Height>\n'+
	'    <Animated>0</Animated>\n' + 
	'    <MatchMovieDim>1</MatchMovieDim>\n' + 
	'    <Loop>1</Loop>\n' + 
	'    <LoopCount></LoopCount>\n' + 
	'    <OptimizeColors>1</OptimizeColors>\n' + 
	'    <Interlace>0</Interlace>\n' + 
	'    <Smooth>1</Smooth>\n' + 
	'    <DitherSolids>0</DitherSolids>\n' + 
	'    <RemoveGradients>0</RemoveGradients>\n' + 
	'    <TransparentOption></TransparentOption>\n' + 
	'    <TransparentAlpha>128</TransparentAlpha>\n' + 
	'    <DitherOption></DitherOption>\n' + 
	'    <PaletteOption></PaletteOption>\n' + 
	'    <MaxColors>255</MaxColors>\n' + 
	'    <PaletteName></PaletteName>\n' + 
	'  </PublishGifProperties>\n' + 
	'  <PublishPNGProperties enabled="true">\n' + 
    '    <Width>' + w + '</Width>\n'+
    '    <Height>' + h + '</Height>\n'+
	'    <OptimizeColors>1</OptimizeColors>\n' + 
	'    <Interlace>0</Interlace>\n' + 
	'    <Transparent>0</Transparent>\n' + 
	'    <Smooth>1</Smooth>\n' + 
	'    <DitherSolids>0</DitherSolids>\n' + 
	'    <RemoveGradients>0</RemoveGradients>\n' + 
	'    <MatchMovieDim>1</MatchMovieDim>\n' + 
	'    <DitherOption>None</DitherOption>\n' + 
	'    <FilterOption>None</FilterOption>\n' + 
	'    <PaletteOption>Web 216</PaletteOption>\n' + 
	'    <BitDepth>24-bit with Alpha</BitDepth>\n' + 
	'    <MaxColors>255</MaxColors>\n' + 
	'    <PaletteName></PaletteName>\n' + 
	'  </PublishPNGProperties>\n' + 
	'  <PublishQTProperties enabled="true">\n' + 
    '    <Width>' + w + '</Width>\n'+
    '    <Height>' + h + '</Height>\n'+
	'    <MatchMovieDim>1</MatchMovieDim>\n' + 
	'    <UseQTSoundCompression>0</UseQTSoundCompression>\n' + 
	'    <AlphaOption></AlphaOption>\n' + 
	'    <LayerOption></LayerOption>\n' + 
	'    <QTSndSettings>00000000</QTSndSettings>\n' + 
	'    <ControllerOption>0</ControllerOption>\n' + 
	'    <Looping>0</Looping>\n' + 
	'    <PausedAtStart>0</PausedAtStart>\n' + 
	'    <PlayEveryFrame>0</PlayEveryFrame>\n' + 
	'    <Flatten>1</Flatten>\n' + 
	'  </PublishQTProperties>\n' + 
	'</flash_profile>\n'
 
    // Clear the output window
    fl.outputPanel.clear()
    // write the profile xml (above) into the output window
    fl.trace(profile);
    // save the contents of the output window to the path stored in propath
    fl.outputPanel.save(profpath);
    // clear the output window
    fl.outputPanel.clear()
}

fl.browseForFolderURL returns a file with the prefix file:/// which when used as the file path in the publish profile causes the export to fail silently, we need to handle this.

Gotcha’s

Silent Fail – error in publish profile such as file:/// prefix on filename. The file path in the generated publish profile has to be correct or the file will not output and Flash will probably not tell you why. Opening the file built in the createProfile method and looking through should help solve any issues here.

To test I set up one item in the fla and then commented out the line that closed the document;

// exportdoc.close(false);

This left the new document open where I could try and publish it within the IDE. After fixing the file path I discovered the following error.

Silent Fail – Trying to export with the wrong version set.

Version errors can occur even though the only output is an image

Even though I was only publishing a PNG it insisted (silently) that I needed to have a version of the flash player set higher than 6. Rather than manually build up a profile that worked I changed the settings in the IDE and then used the Helper function further down to build most of the profile for me. There was some manual adjustment to add in the width, heights and filenames but as I’d only be doing this once I thought I’d leave it as is.

Having fixed this error I stared getting the following error;

When working with multiple documents make sure you have the right one before accessing properties on it

After some searching I realised I’d not closed the document after fixing the earlier error so I was in one document while trying to access properties of another one. Coupled with this, I now had an untitled document open that had no items in its library and I was running the script against it which of course produced no results.

Make sure before you run the script that you are on the main stage and not within a movie clip you may have been editing or you may get an error to the effect ‘Cannot embed blah within an instance of itself’.

Finally: If your movie is set with Actionscript version: Actionscript 2 you will need to use linkageClassName instead of linkageBaseClass (AS3)

Hopefully this png extraction will make more sense once I have the next post up.

Helper function

Download the file here: [download id=”8″ format=”2″]

/****************************************************
 * Set the desired publish profile properties in    *
 * the flash IDE. Run this script from the Commands *
 * Menu and cut and paste the output from into the  *
 * export script                                    *
 * Some modification will need to be made to take   *
 * the width, height and name of each item. See the *
 * export script for an example                     *
 *                                                  *
 * Author: C.Wilson - abitofcode                    *
 * Date: 19-11-2011                                 *
 ****************************************************/
 
// clear the output window
fl.outputPanel.clear()
 
// Get a path to save the current profile information into
var saveDir = fl.browseForFolderURL("Save your profile template where:")
var profFile = saveDir + '/pubprofile_template.xml'
 
// Export the current publish profile
fl.getDocumentDOM().exportPublishProfile(profFile);
 
// Read the profile we have exported back into a string
var str = FLfile.read( profFile);
if (str) {
    // Split the profile XML by line break
    var lines = str.split("\n");
 
    // Start building the profile string that we'll use in 
    // the export script.
    var profileString = "\tprofile = ";
 
    // loop through each line formatting for use in the template
    for(line in lines){
        if(lines[line]) {
            profileString += "\t'" + lines[line] + "\\n' + \n";        
        }
    }
 
    // Output the profile string to the output window so we can
    // cut and paste into the export script
    fl.trace(profileString);
}

Comments are closed.