Template Transformation

Today I am disclosing a trick that will allow you to apply a transformation to your template.  But first a disclaimer: this technique is not a product feature.  It is just a trick that takes advantage of some side effect behaviours.  Ok, ok, it’s a hack.  But it’s potentially a very useful hack.

Motivation

Have you ever wanted to do apply a global change to your template?  Change the fonts of your fields.  Set a colour scheme.  Add a script to all your fields.  You know by now that it can be very tedious to do these things. 

Transformation Techniques

What options do you have today?  For very simple changes, some people will go to their XML Source tab and do a global search/replace.  That’s a bit scary.  I do it when I rename anything that is referenced in script.  But it’s hard to use when you want to be selective in your changes — e.g. apply a font change to all numeric fields.  There is also XSLT.  We have some very technical customers who will save their form as an XDP and apply an XSLT script to make global changes.  These customers have my very humble respect.  XSLT is a very powerful tool, but a very difficult technology to master.

Script

There are a couple of ways I’d like to transform a template.  One choice would be some form of style sheet definition.  My other choice (today’s topic) would be to write JavaScript that updates the template.  We write tons of script that modifies the form DOM at runtime.  Wouldn’t it be great if we could write script that modified the template at design time?  Well, turns out you can.  But as I think I may have mentioned — it’s a bit of a hack.

I’ll explain how it works, but first some background to help you understand the technique.

PDF Generation Process

When you edit a dynamic template and save as a PDF, Designer gives you the illusion that PDF is the native format for the template.  In reality, the native format is XDP.  When Designer opens a dynamic PDF, it extracts the XDP stream stored inside and discards the PDF container.  Then when you save, Designer re-generates the PDF from the XDP.  That would explain the phrasing of the messages you see in the log tab of Designer when you save:

Generating PDF Document…
PDF generated successfully.

0 warnings/errors reported.

The PDF generation process that Designer goes through during save is almost exactly the same process LiveCycle Forms goes through when it generates a PDF from an XDP on the server.  Among the steps during PDF generation:

  1. Embed all required fonts
  2. Embed referenced images
  3. Generate XMP metadata
  4. Run server-side scripts

Hopefully #4 caught your attention.  When Designer saves a dynamic PDF, one of the things it does is execute script. 

Scripting the Template

When any script runs in an XFA form, the default context is the Form DOM.  The Form DOM is a transient DOM that gets created when you open the form and goes away when you close the form.  You can script against the Form DOM as much as you like.  However, in Reader you cannot modify the template DOM.  The template DOM is the source code.  It is what gets saved back in the XDP stream in the <template> element.   For security reasons, we don’t allow forms in Reader to modify their template.  But, this is a Reader-only restriction.  Forms running in the context of the server can modify their template.  This means that script run by Designer when it saves your dynamic PDF has full write-access to the template and any changes the script makes will be preserved in the generated PDF.

Setting up a save-time script

Take a simple example.  This script will update the author information metadata in my template (note that the script is set to run at server):

form1.#subform[0].updateMeta::initialize – (JavaScript, server)
updateMetaData("creator", "John Brinkman");
updateMetaData("contact", "
http://blogs.adobe.com/formfeed/");
updateMetaData("usage",   "No rights reserved");

function updateMetaData(vName, vValue)
{
    var vDesc = xfa.template.["#subform"].desc;
    if (vDesc.nodes.namedItem(vName) == null)
    {
        vDesc.nodes.append(xfa.template.createNode("text", vName));
        vDesc[vName].value = vValue;
    }
}

Now when I save the form and go look at the file/Form Properties/Info I’ll see… no change.  Oops.  The problem is that once Designer saves, it doesn’t re-open the PDF it saved.  It continues operating on the XDP it has in memory.  To see the change you need to save *and* close.  Then when you re-open and look at your file properties you will see the author and contact information updated. (You won’t see the "usage" element in this screen, but it does get embedded in the PDF metadata).

Debugging

For those of you who get their JavaScript right the first time, read no further.  Those like me who need to liberally sprinkle their code with console.println() messages will discover that the console API doesn’t work.  That’s because console is an Acrobat-only object. You can’t code against it on the server.  Instead, you will want to familiarize yourself with the xfa.log object.  To dump a message to the log file, you can code:

xfa.log.message(0, "your message here");

This message will show up in the log tab of designer.  A couple things you’ll want to know about using this command effectively:

  • The log file suppresses messages that have the same text as the previous message
  • For a message to appear with proper line breaks in the log tab, you need to insert carriage return/line feed combinations in your string.  i.e. instead of "line1\nline2" you need to code: "line1\r\nline2".
  • Before saving you should clear the log.

Cleanup

Once your transformation script has run, you don’t need it anymore, and it would be bad form to leave it around.  Add a couple lines to your script so that after it has done its work it will remove itself.

With a log message and cleanup, the sample above becomes:

updateMetaData("creator", "John Brinkman");
updateMetaData("contact", "
http://blogs.adobe.com/formfeed/");
updateMetaData("usage",   "No rights reserved");

// Insert a message in the Designer log tab
xfa.log.message(0, "updated form metadata");

// remove this script once it has run on-save
var vNode = xfa.template.resolveNode("$.." + this.name);
vNode.parent.nodes.remove(vNode);

function updateMetaData(vName, vValue)
{
    var vDesc = xfa.template.["#subform"].desc;
    if (vDesc.nodes.namedItem(vName) == null)
    {
        vDesc.nodes.append(xfa.template.createNode("text", vName));
        vDesc[vName].value = vValue;
    }
}

Of course, you’ll probably want to leave the cleanup code commented out until you have fully debugged your script :-).  It would be a good idea to make sure your imported subform has a visual appearance so that you won’t forget to deal with it.

Once it is working, the updateMeta subform can be added to your fragment library.  When you want to update your metadata, you follow these steps:

  • import the updateMeta subform fragment
  • save
  • close
  • re-open

And if that’s less effort than re-typing the metadata, then you’re in a happy space.

I have attached a sample that does a couple of interesting things:

  • Update all the field widget borders to be .01in thick, solid and blue.
  • Add enter and exit scripts to each field to highlight the border while the field is active.

To use the sample:

  • Open the form in Designer
  • Modify the first line of the script and change it from if (false) to if (true)
  • Save
  • Close
  • Open

When re-opened you’ll see that the subform hosting the update script is gone.  The borders are now solid blue and when you preview and tab through the fields you’ll see that the border changes colour when the field is active.  You’ll also notice that in the sample I use a slightly updated version of the scMessage scripts to display the server message — fixing the carriage return/line feed issue.

Futures

I have been saying that this technique applies to Dynamic XFA-based PDFs.  In fact it works with static as well.  But it does not work for artwork PDFs.  It also does not work when you save as .XDP.  For those environments there is no solution yet.

The technique described here is admittedly awkward, but it does point toward some exciting possible enhancement areas for Designer.  Notably style sheets and Design-time macros.  Nobody is making any commitments, but these are areas of interest to us as we move forward.

6 Responses to Template Transformation

  1. Keith Gross says:

    We currently sometimes use XSLT when we need to make changes throughout a bunch of forms. I thought this might be a interesting option for some changes that are difficult to express that way. I worked up a very small initial form just to see that I understood and could make it work for myself but I’m getting a bit of a odd result.The form has a single field and I’m filling in it’s value using a server script on the forms initialize method. The code is as follows.var v = xfa.template.createNode(“value”);xfa.template.form1.Page1.TextField1.nodes.append(v);v.text = “Hello World”;What’s strange is that if I save the form as PDF and open it with Reader the default value is unchanged. If I open it with Designer though the default value has changed to “Hello World”. If now after opening with designer I save again I can now open with Reader and the default value has changed to “Hello World”. It seems like I have to save, close, reopen and save a second time before the change takes effect.On the theory that there is something special about value I’m going to try a number of other attributes. I downloaded and tried your PDF and it works as I’d expect.

  2. Keith:As you suspected, “value” is special. When the PDF is generated on the server, we embed the data that is generated from the form. When the form was initially created, those fields had an empty value, and so we stored a dataset with empty values. When you opened in Reader, the fields were initialized with their default values, but the embedded data trumped the default values. Then when you brought the form into designer, the embedded data was discarded.By the way, when setting the value property, .rawValue is available on the template — rather than explicitly creating nodes. Setting rawValue should create the right type based on the ui element. e.g. creates for .John

  3. Keith Gross says:

    I was doing some experimentation on Template Transforms for a new project when I noticed some odd behavior. I get xfa.log statement executing even for events that are not marked as server. This only seems to happen under specific circumstances.So Here’s what I do- Create a new blank untitled form in designer.- add One text field to the form.- place a xfa.log(0, “”) statement in every possible event of that text field. Each should have a unique string for the second argument and you don’t mark any as server. Setting up all the event isn’t required. It just shows all the events that can possibly get invoked.- Save the form as a xdp- Switch to the preview tab for the form.You should get output similar to what I have below in the log view. This only seems to happen on preview. If you instead do a “Save as…” to PDF only events actually marked as server appear.In practice i can work around this but it can sometimes make the log ambiguous since events are being invoked that you didn’t really want invoked.——————————–in text field initializein text field calculatein text field validatein text field ready:formin text field ready:layoutin text field docReadyin text field docClose——————————–

  4. Keith:There is an explanation for this…In Designer, look at Form Properties/Preview where it says: “Preview XML Form As”.Your choices are “Static PDF Form” and “Dynamic XML Form”.If you preview as static, then all the events will fire. That’s because we need to do a full-blown render to create the PDF page content of the static form.If you preview as dynamic, then we create a simpler PDF with only the embedded XFA definition — the page content gets created when you open in Acrobat/Reader.When creating the dynamic form we explicitly run the server scripts and assume the client scripts will run later — at the client.John

  5. Marcus says:

    Hello,

    i was testing your borders sample form. Very nice.
    Now I want to use it to show and highlight the buttom edge (underline) only, therefore I modied your vScript to:

    var vScript = xfa.template.createNode(“script”);
    vScript.contentType=”application/x-javascript”;
    // script to color the border
    vScript.value = “for (var i=0; i<4; i++)\n " +
    // We only want to show the underline (edge 2)
    "if(i == 2) {this.ui.oneOfChild.border.getElement(\"edge\",i).presence = \"visible\"; \n" +
    "this.ui.oneOfChild.border.getElement(\"edge\",i).color.value = \"255,0,0\";} ";

    This works as expected, but I where not able to set the edge 0,1 and 3 to hidden when defining the new borders.
    When I use
    vBorder.edge.presence = "hidden";
    all 4 edges are initially hidden and the underline becomes only visible when I enter the fields once.

    I think I have to use a for loop on the line
    vNode.ui.oneOfChild.nodes.append(vBorder);
    but I were not able to figure it out.

    Do you have any hints for me? Thanks in advance.

    • John Brinkman says:

      Marcus:

      The techniques described in this post are a bit dated by now. If you want to programatically modify your template, you should use macros:
      http://blogs.adobe.com/formfeed/2010/01/designer_es2_macros.html

      If you want to add enter/exit events to all fields, you should use propagating events:
      http://blogs.adobe.com/formfeed/2009/03/xfa_30_event_propagation.html

      Having said all that, if you want to define your field to have an initial appearance of a blue underline, this code should work:

      vNode.ui.oneOfChild.nodes.remove(vNode.ui.oneOfChild.border);
      var vBorder = xfa.template.createNode(“border”);
      for (var i=0; i<4; i++) {
        var vEdge = xfa.template.createNode(“edge”);
        vEdge.thickness = “.01in”;
        vEdge.color.value = “0,0,255”;
        if (i !== 2) {
          vEdge.presence = “hidden”;
        }
        vBorder.nodes.append(vEdge);
      }
      vNode.ui.oneOfChild.nodes.append(vBorder);