Listing graphics in order of appearance – Part 2

Since we want a list of graphics, there are a couple of ways to approach this. First, we can write our report at the same time as we are collecting the data. Or, we can collect all of the data first, and then write the report. I prefer the second approach because it tends to make the code simpler to understand.

What kind of data should we collect? It all depends on our needs but it usually doesn’t hurt to collect more than we need in case we need it later. Here is what I would suggest collecting:

  • The absolute path to the imported graphic.
  • The absolute path to the FrameMaker document containing the graphic.
  • The page number.
  • The unique ID of the anchored frame containing the graphic. This will allow us to build a hypertext link from the report to the graphic if desired.

Here is a function we can use to create a data structure to hold the graphic data. There are several ways that this could be done, but this approach uses a series of parallel lists to hold the data.

Set oImageData = GetDataStructure{};

Function GetDataStructure
//
Local Result(eUtl.EStructure{});

// Add a string list to hold the graphic paths.
Result.Graphics = eUtl.StringList{};

// Add a string list to hold the document paths.
Result.Docs = eUtl.StringList{};

// Add a string list to hold the page numbers.
Result.Pages = eUtl.StringList{};

// Add an integer list to hold the unique ids of the anchored frames.
Result.Uniques = eUtl.IntList{};
//
EndFunc //--------------------------------------------------------------------

This function will show you how to add to the data structure for each anchored frame.

Function ProcessAFrame oAFrame oImageData oDoc
//
Local oGraphic(0);

// Loop through the graphics in the anchored frame.
Set oGraphic = oAFrame.FirstGraphicInFrame;
Loop While(oGraphic)
  // Test for an imported graphic.
  If oGraphic.ObjectName = 'Inset'
    // See if it was imported by reference.
    If oGraphic.InsetFile <> ''
      // Add the data to the data structure.
      Add Member(oGraphic.InsetFile) To(oImageData.Graphics);
      Add Member(oDoc.Name) To(oImageData.Docs);
      Add Member(oAFrame.Page.PageNumString) To(oImageData.Pages);
      Add Member(oAFrame.Unique) To(oImageData.Uniques);
    EndIf
  EndIf
  Set oGraphic = oGraphic.NextGraphicInFrame;
EndLoop
//
EndFunc //--------------------------------------------------------------------

You will have to pass this oImageData object into your functions as you navigate the paragraphs and anchored frames in your document (or book). Let me backtrack and rework the code from the last post. I am just going to just post shells of the existing functions and point out what is different. See if you can put the pieces together.

// Test for an active document or book.
If (ActiveDoc = 0) and (ActiveBook = 0)
  MsgBox 'Please open a document or book.    ';
  LeaveSub; // Exit the script.
EndIf

// Call the function to make the empty data strucure.
Set oImageData = GetDataStructure{};

If ActiveBook
  // Book code can be added later.
  // Set iResult = ProcessBook{ActiveBook,oImageData};
Else
  Set iResult = ProcessDoc{ActiveDoc,oImageData};
EndIf

// Now write the report with the data.
// ... Call a report writing function here ...

Function ProcessDoc oDoc oImageData
//
Local i(0), tTextList(0), iResult(0);

// Lines 4 through 11 of the second code listing in the previous post go here.
// Line 8 will be replaced with this:
Set iResult = ProcessParagraph{oPgf,oImageData,oDoc};
//
EndFunc //--------------------------------------------------------------------

Function ProcessParagraph oPgf oImageData oDoc
//
Local tTextList(0), i(0), oAFrame(0), iResult(0);

// Lines 3 through 10 of the ProcessParagraph function in the previous post
// go here. Line 9 will be replaced with a call to the ProcessAFrame
// function in this post (above).
Set iResult = ProcessAFrame{oAFrame,oImageData,oDoc};
//
EndFunc //--------------------------------------------------------------------

Function ProcessTable oTbl oImageData oDoc
//
// This is the function from lines 17 to 45 of the second code listing in the
// previous post. Everything is the same, except we added a couple of
// parameters to line 17. Line 19 will now be:
Local oCell(0), oPgf(0), iResult(0);

// Lines 24, 32, and 40 will now contain this:
Set iResult = ProcessParagraph{oPgf,oImageData,oDoc};
//
EndFunc //--------------------------------------------------------------------

See if you can put it all together from both posts and make it work. You can test it by adding this line after line 18 in the previous code listing:

Display oImageData.Graphics;

This should display a list of the graphic names in a message box. Note that if you have a ton of graphics in your document, you might not see the entire dialog box. You should be able to press Enter or Escape to dismiss it if you can’t see the OK/Cancel buttons. Alternatively, you can write the list to the FrameMaker Console window by using this:

Write Console oImageData.Graphics;

In another post, we can explore how to write the report. Comments or questions are welcome.

Listing graphics in order of appearance

Jonathan asked on the FrameScript list on yahoo groups about getting a list of graphic files in a document in order of appearance. I decided to answer it here so I can discuss it in some detail and because yahoo groups doesn’t format code listings very well. There are a couple of possibilities to consider. First, if you don’t have any graphics in tables, you can do something simple like this:

// Set a variable for the active document.
Set oDoc = ActiveDoc;

// Loop through all of the anchored frames in the main text flow.
Get TextList InObject(oDoc.MainFlowInDoc) FrameAnchor NewVar(tTextList);
Loop While(i <= tTextList.Count) LoopVar(i) Init(1) Incr(1)
  // Get each anchored frame.
  Set oAFrame = tTextList[i].TextData;
  // Get the information on the imported graphic(s) inside the text frame here.
  // ...
EndLoop

If you have anchored frames inside of tables, then things get a little more complicated. Here is a basic loop for processing all of the paragraphs in document order, including those in tables:

// Set a variable for the active document.
Set oDoc = ActiveDoc;

// Set a variable for the first paragraph in the main text flow.
Set oPgf = oDoc.MainFlowInDoc.FirstPgfInFlow;
// Loop through the paragraphs in the main text flow.
Loop While(oPgf)
  
  // Process any tables in the paragraph.
  Get TextList InObject(oPgf) TblAnchor NewVar(tTextList);
  Loop While(i <= tTextList.Count) LoopVar(i) Init(1) Incr(1)
    Set iResult = ProcessTable{tTextList[i].TextData};
  EndLoop
  Set oPgf = oPgf.NextPgfInFlow;
EndLoop

Function ProcessTable oTbl
//
Local oCell(0), oPgf(0);

If oTbl.TblTitlePosition = TblTitleAbove
  Set oPgf = oTbl.FirstPgf;
  Loop While(oPgf)
    
    Set oPgf = oPgf.NextPgfInFlow;
  EndLoop
EndIf
Set oCell = oTbl.FirstRowInTbl.FirstCellInRow;
Loop While(oCell)
  Set oPgf = oCell.FirstPgf;
  Loop While(oPgf)
  
    Set oPgf = oPgf.NextPgfInFlow;
  EndLoop
  Set oCell = oCell.NextCellInTbl;
EndLoop
If oTbl.TblTitlePosition = TblTitleBelow
  Set oPgf = oTbl.FirstPgf;
  Loop While(oPgf)
    
    Set oPgf = oPgf.NextPgfInFlow;
  EndLoop
EndIf
//
EndFunc //--------------------------------------------------------------------

This is a general purpose script for processing paragraphs in document order. Lines 8, 24, 32, and 40 are where you do something with each paragraph. In this case, we can call a function on each of these lines to get any anchored frames that are in each paragraph. The function would be similar to the first code example, except that it would get any anchored frames in each paragraph instead of the entire main flow.

Function ProcessParagraph oPgf
//
// Loop through all of the anchored frames in the paragraph.
Get TextList InObject(oPgf) FrameAnchor NewVar(tTextList);
Loop While(i <= tTextList.Count) LoopVar(i) Init(1) Incr(1)
  // Get each anchored frame.
  Set oAFrame = tTextList[i].TextData;
  // Get the information on the imported graphic(s) inside the text frame here.
  // ...
EndLoop
//
EndFunc //--------------------------------------------------------------------

There are some unfinished pieces here, like how to query each anchored frame for imported graphic information. Also, you will need a way to collect the graphic data. But this will get you started. Questions and comments are welcome. If there is enough discussion generated, we can go further with this.

Naming Variables and Functions in Scripts

A friend recently asked me about how I name variables in my FrameScript scripts. I used to use a “v” prefix on all of my variables names. For example, for a paragraph I would use:

Set vDoc = ActiveDoc;
Set vPgf = vDoc.MainFlowInDoc.FirstPgfInFlow;

The “v” prefix would simply indicate that I was setting a variable. This is the convention I used in my 2003 FrameScript book FrameScript: A Crash Course. Later, I decided to make the prefixes reflect the data type that the variable represents. I replaced the “v” with another letter indicating the data type. For example,

Set oDoc = ActiveDoc;
Set oPgf = oDoc.MainFlowInDoc.FirstPgfInFlow;

The “o” prefix means “object” since both lines set variables for FrameMaker objects. I use “s” for strings, “i” for integers, “r” for real numbers, “m” for metric values, etc. These prefixes help me see at a glance what kind of data type the variable represents. This is important when I am looking at a script that I may not have worked on in awhile. Prefixes like this are also helpful when you are reusing functions. For example, here is a function that applies a named paragraph format to a paragraph.

Function ApplyPgfFmt oPgf sName oDoc
//
Local oPgfFmt(0);

Get Object Type(PgfFmt) Name(sName) DocObject(oDoc) NewVar(oPgfFmt);
If oPgfFmt
  Set oPgf.Properties = oPgfFmt.Properties;
Else
  Set oPgf.Name = sName;
EndIf
//
EndFunc //-------------------------------------------

Even if I don’t know exactly how this function works, I can see at a glance that it takes three parameters: a paragraph object (oPgf), a string indicating the paragraph format name (sName), and a document object (oDoc). The prefixes help me to quickly see the data types of each parameter.

As far as the variable name itself, I try to use the object name that the variable represents. In the ApplyPgfFmt function, I use the Get Object command to get a PgfFmt object. So, I use oPgfFmt as the variable name. Using this convention lets me quickly see that oPgfFmt represents a PgfFmt object. The sName parameter gets matched up with Name in the Get Object command and so on. I find that this method helped me memorize the FrameMaker object model because I have closely associated my variable names with the built-in FrameMaker object names.

When naming functions, the common sense approach is to use some kind of a verb form that describes what the function does. When you look at the function name and its parameters, it should be evident what the function does without looking at the actual function code.

With ExtendScript, I do things a little bit different. I don’t use the prefixes, I simply use the object name but with a lowercase first letter. Here is the example I showed earlier:

var doc = ActiveDoc;
var pgf = doc.MainFlowInDoc.FirstTextFrameInFlow.FirstPgf;

ExtendScript (JavaScript) is a case sensitive language, so pgf is different from Pgf. For function names (and longer variable names) I use the “camel case” convention. I don’t use the datatype prefixes, mainly because this is not a normal convention for JavaScript programmers. I suppose it is a bit vain, but I don’t want my ExtendScript code to look too out-of-the-ordinary. Here is the ExtendScript version of the ApplyPgfFmt function:

function applyPgfFmt(pgf,name,doc) {

  var pgfFmt = 0, props = 0;

  pgfFmt = doc.GetNamedPgfFmt(name);
  if (pgfFmt.ObjectValid()) {
    props = pgfFmt.GetProps();
    pgf.SetProps(props);
  }
  else {
    pgf.Name = name;
  }
}

Running a FrameScript Script from DITA-FMx

DITA-FMx is a great plugin for working with DITA in FrameMaker. One of its best features is the ability to create a complete FrameMaker book from a ditamap. In some situations you may want to run a script on the book before creating the PDF. Scott Prentice, the developer of DITA-FMx, has a blog post explaining how you can call an ExtendScript script from DITA-FMx. This article will show you how to call a FrameScript script from DITA-FMx.

To set this up in DITA-FMx, you will need to edit the BookBuildOverrides section of the book-build INI file that you are using with DITA-FMx. Here are the three keys that need to be edited:

[BookBuildOverrides]
...
RunScript=1
ScriptName=fsl
ScriptArgs=myEventScript

RunScript is a 0 or 1 value. Setting it to a 1 tells DITA-FMx that you want run one or more scripts or FDK clients. ScriptName for FrameScript is fsl. The ScriptArgs value is the name of the installed FrameScript “event script” that you want to run.

Before we go further, let me give a little background on FrameScript scripts. FrameScript has two kinds of scripts: Standard scripts and Event scripts. A standard script can consist of functions, subroutines, and events, but it always has an entry point that is not inside of a function, subroutine, etc. Typically, you “run” a Standard script, it loads into memory, runs to completion, then is flushed from memory.

Event scripts are not run directly; they are “installed” first and then “wait” for some kind of event to happen; for example, a menu command, a FrameMaker event, an outside call, etc. All of the code in an event script must be inside of a function, subroutine, or event. The entry point for an event script is some kind of an event inside of the script. One point that is pertinent to this post is that an installed Event script has a name, and this name is the value you use for the ScriptArgs key.

Instead of installing your event script manually, it is best to install it automatically with an “Initial Script”, which runs automatically whenever you start FrameMaker. That way, your event script will installed automatically when you start FrameMaker. Here is an example Initial Script:

// InitialScript.fsl Version 0.1b August 26, 2013

Local sPath('');

// Get the path to this script.
Set sPath = eSys.ExtractPathName{ThisScript};

// Install the event script that will receive the DITA-FMx call.
Install Script File(sPath+'Script1.fsl') Name('myEventScript');

This command will install the script “Script1.fsl” that is in the same folder as the Initial Script (InitialScript.fsl). The important parameter on the Install Script command is Name; the name supplied must match the name you give to the ScriptArgs key in DITA-FMx’s book-build INI file. Here we are using “myEventScript”.

To run this script automatically whenever FrameMaker starts, choose FrameScript > Options and click the Browse button next to the Initial Script Name field. Navigate to the InitialScript.fsl file and select it, and then click Save to close the Options dialog box.

FrameScript Options dialog box

Before you quit and restart FrameMaker, you will need to have Script1.fsl in the same folder as the InitialScript.fsl file. Here is a shell you can use for this script:

// Script1.fsl Version 0.1b August 26, 2013

Event NoteClientCall
//	
If ActiveBook
  Set iResult = ProcessBook{ActiveBook};
EndIf
//
EndEvent

Function ProcessBook oBook
//
//... Do something with the book here ...
//
EndFunc

The NoteClientCall event is a built-in event that “waits” for the outside call; in this case, from DITA-FMx. We test to make sure that there is an active book, which should be the book that DITA-FMx just created from the ditamap. If there is an active book, we call the ProcessBook function, which is where we process our book with FrameScript code. We could have our book code right in the ProcessBook function, or we could use this function to call other scripts or functions.

Please let me know if you have any questions or comments about calling FrameScript scripts with DITA-FMx.

Autonumber Problems

Here is part of a recent post on the Framers list:

Single-file doc with four Sections. Sections and sub-sections numbered 1, 1.1, 1.1.1 etc.

Pgf numbering formats:

Heading1 S:<n+>.< =0>< =0>< =0>
Heading2 S:<n>.<n+>< =0>< =0>
Heading3 S:<n>.<n>.<n+>< =0>

But I’m getting Sections 1 and 2 (with subsections), then 5 and 6 (which should be 3 and 4)….Pix and tables are numbered with F:<n+> and T:<n+> respectively so shouldn’t be affecting anything.

Autonumbering in FrameMaker is one feature that is rock-solid. So these problems are always caused by intervening paragraphs that have unintended autonumber counters. The trick is finding the offending paragraphs. I use a FrameScript script to make an “autonumber report” of a document or book. With this report, the cause of autonumber problems can be quickly found.

The author of the Framers post sent me the problem document and in seconds I generated the following spreadsheet (click the image to see it full size):

It’s pretty easy to see the problem paragraphs, which I have highlighted. The key to finding them is to look for the last good number, which is in row 13. Next, look for the first bad number, which is in row 20. Now we know that the problem paragraph or paragraphs is between these two rows in the spreadsheet. At this point, it’s best to scan the Autonumber String column, and look for the S: series label, which brings us to rows 15 and 17.

As it turns out, in this document the two offending paragraphs were very small and at the top of the page, which is why they were difficult to find. But like many other tasks, FrameScript makes it easy. If you are interested in purchasing this script, please let me know. Thank you very much.

– Rick