On almost every topic
Using Alfresco Composite Rendition to Render PDF

This is a follow-up to the post Add an XML to PDF Transformer to Alfresco. In this post I looked into the options to add and use an XSL-FO processor using configuration files and some JavaScript. The post ended with the remark that Composite Renditions are not exposed in the JavaScript API. This post provides an example of a custom action that creates a PDF rendition based on XSL-FO.

Note: in order to make these examples work, you need to follow the configuration steps in the previous post. This post assumes you are familiar with customizing Alfresco using Java.

Composite Rendition

A Composite Rendition allows you to create a rendition from one mimetype to another using one or more intermediate transformations. With XSL-FO for example you would normally first transform the XML document into XSL-FO and then transform the result to the desired output format, for example PDF.

Custom Action

To create PDF renditions for XML files using XSL-FO you can write a custom action in Java that creates a Composite Rendition to first transform the XML document into an XSL-FO document and then transforms the result into a PDF. The PDF is then stored as a rendition associated to the XML document.

The following Java class demonstrates this approach. It is a custom action that takes a single parameter to pass the stylesheet:

package com.someco.action;

import java.util.List;

import org.alfresco.repo.action.ParameterDefinitionImpl;
import org.alfresco.repo.action.executer.ActionExecuterAbstractBase;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.rendition.executer.ReformatRenderingEngine;
import org.alfresco.repo.rendition.executer.XSLTRenderingEngine;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ParameterDefinition;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.rendition.CompositeRenditionDefinition;
import org.alfresco.service.cmr.rendition.RenditionDefinition;
import org.alfresco.service.cmr.rendition.RenditionService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;

public class FoRenditionActionExecuter extends ActionExecuterAbstractBase {

  public static final String NAME = "fo";

  public static final String PARAM_TEMPLATE_REF = "template";

  RenditionService renditionService;

  @Override
  protected void executeImpl(Action action, NodeRef actionedUponNodeRef) {

    RenditionDefinition xslDefinition = renditionService
      .createRenditionDefinition(QName.createQName(
        NamespaceService.CONTENT_MODEL_1_0_URI,
        "xslRenderingDefinition"), XSLTRenderingEngine.NAME);

    NodeRef template = (NodeRef) action
      .getParameterValue(PARAM_TEMPLATE_REF);

    xslDefinition.setParameterValue(
      XSLTRenderingEngine.PARAM_TEMPLATE_NODE, template);
    xslDefinition.setParameterValue(
      ReformatRenderingEngine.PARAM_MIME_TYPE, "text/xsl");

    RenditionDefinition pdfDefinition = renditionService
      .createRenditionDefinition(QName.createQName(
        NamespaceService.CONTENT_MODEL_1_0_URI,
        "pdfRenderingDefinition"), ReformatRenderingEngine.NAME);
		
    pdfDefinition.setParameterValue(
      ReformatRenderingEngine.PARAM_MIME_TYPE,
      MimetypeMap.MIMETYPE_PDF);

    CompositeRenditionDefinition compositeDefinition = renditionService
      .createCompositeRenditionDefinition(QName.createQName(
        NamespaceService.CONTENT_MODEL_1_0_URI,
        "compRenderingDefinition"));

    compositeDefinition.addAction(xslDefinition);
    compositeDefinition.addAction(pdfDefinition);

    renditionService.render(actionedUponNodeRef, compositeDefinition);
  }

  @Override
  protected void addParameterDefinitions(List paramList) {
    paramList.add(new ParameterDefinitionImpl(PARAM_TEMPLATE_REF,
      DataTypeDefinition.NODE_REF, true,
      getParamDisplayLabel(PARAM_TEMPLATE_REF)));
  }

  public RenditionService getRenditionService() {
		return renditionService;
  }

  public void setRenditionService(RenditionService renditionService) {
		this.renditionService = renditionService;
  }

}

In order to make the custom action available in Alfresco we need to add it as a Spring Bean:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 
  'http://www.springframework.org/dtd/spring-beans.dtd'>
  
<beans>
 
  <bean id="fo" class="com.someco.action.FoRenditionActionExecuter"
    parent="action-executer">
    <property name="publicAction">
      <value>false</value>
    </property>
    <property name="renditionService">
      <ref bean="RenditionService" />
    </property>
  </bean>

</beans>

Execute the action

Next restart Alfresco. You can test the custom action by writing a small JavaScript that executes the action by providing an XML document and an XSL stylesheet that creates XSL-FO output. The FOP distribution provides some examples that you can use. The following code demonstrates how to execute the action using JavaScript:

var fo = actions.create("fo");
fo.parameters.template = companyhome.childByNamePath("/XML/projectteam2fo.xsl");
fo.execute(document);

This code creates a PDF rendition for the given document (in this case a file called projectteam.xml, an example provided by FOP). The rendition is stored as a child of the source document.

Custom Transformer Java Implementation

Once you have started writing Java code, you might as well want to replace the transformer configuration with a Java implementation. The Apache FOP Java library is already on Alfresco’s class path, so a Java implementation does not require a separate install of Apache FOP.

The following class is a basic implementation to provide the custom transformer in Java to replace the transformer defined in the file custom-transformer-context.xml:

package com.someco.transform;

import java.io.BufferedOutputStream;
import java.io.OutputStream;

import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamSource;

import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.transform.AbstractContentTransformer2;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.TransformationOptions;
import org.apache.fop.apps.Fop;
import org.apache.fop.apps.FopFactory;
import org.apache.fop.apps.MimeConstants;

public class FoContentTransformer extends AbstractContentTransformer2 {

  @Override
  public boolean isTransformable(String sourceMimetype,
      String targetMimetype, TransformationOptions options) {
    if (sourceMimetype.equalsIgnoreCase(MimeConstants.MIME_XSL_FO)
        && targetMimetype.equalsIgnoreCase(MimetypeMap.MIMETYPE_PDF))
      return true;
    return false;
  }

  @Override
  protected void transformInternal(ContentReader reader,
      ContentWriter writer, TransformationOptions options)
      throws Exception {

    FopFactory fopFactory = FopFactory.newInstance();

    OutputStream out = new BufferedOutputStream(
        writer.getContentOutputStream());

    try {
      Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, out);

      TransformerFactory factory = TransformerFactory.newInstance();
      Transformer transformer = factory.newTransformer();

      Source src = new StreamSource(reader.getContentInputStream());

      Result res = new SAXResult(fop.getDefaultHandler());

      transformer.transform(src, res);

    } finally {
      out.close();
    }
  }

}

Next update the file custom-transformer-context.xml. Replace the current transformer with a Spring Brean that loads the Java implementation:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 
  'http://www.springframework.org/dtd/spring-beans.dtd'>
<beans>

  <bean id="transformer.FOP.PDF"
    class="com.someco.transform.FoContentTransformer"
    parent="baseContentTransformer" />

</beans>

Restart Alfresco and try to run the JavaScript code to create the rendition to PDF.

Read the follow-up post Persisting Alfresco Renditions to learn how to persist renditions to allow Alfresco to update the rendition on any properties update. 

Last updated: October 14, 2011

Add an XML to PDF Transformer to Alfresco

Alfresco provides the ability to add custom transformers to transform a given source document to a different document format. There are several transformers available out of the box, but there is no transformer that let’s you transform an XML document to PDF based on XSL-FO. 

You can check the available transformers in Alfresco. Since version 3.4 there is an Administration Web Script (http://localhost:8080/alfresco/service/mimetypes) that lists all mimetypes and the transformers between these mimetypes.

Custom Transformer

You can write the transformer using Java, but you can also add a custom transformer by adding a couple of configuration files. It might not provide you with all the bells and whistles you can think of, but at least it shows how you easily add XSL-FO support without writing any code.

The transformer allows you to transform an XSL-FO document to PDF using a Run Action, a Content Rule or using the following line in a JavaScript:

document.transformDocument("application/pdf");

An XSL-FO document is an XML file containing the content that should be published in the resulting document combined with lay-out instructions. The lay-out instructions are somewhat similar to applying a CSS stylesheet to an HTML document. In Alfresco you can use a Freemarker template or an XSLT stylesheet to transform a source node into an XSF-FO document.

Download and install FOP

The first step is to download and install FOP, the apache XSL-FO processor. You can find it at http://xmlgraphics.apache.org/fop/. The next thing you need to do is to add it to the path to allow Alfresco to locate the executable files. Using Windows you can do this by adding the path to FOP to the PATH environment variable.

Configure the Transformer

The next step is to configure the transformer. Create a file custom-content transformer-context.xml in the folder tomcat/shared/classes/alfresco/extension and add the following lines:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 
  'http://www.springframework.org/dtd/spring-beans.dtd'>
<beans>
  <bean id="transformer.worker.FOP.PDF" 
    class="org.alfresco.repo.content.transform.RuntimeExecutableContentTransformerWorker">
    <property name="mimetypeService">
      <ref bean="mimetypeService" />
    </property>
    <property name="checkCommand">
      <bean class="org.alfresco.util.exec.RuntimeExec">
        <property name="commandsAndArguments">
          <map>
            <entry key=".*">
              <list>
                <value>fop.bat</value>
                <value>-version</value>
              </list>
            </entry>
          </map>
        </property>
      </bean>
    </property>
    <property name="transformCommand">
      <bean class="org.alfresco.util.exec.RuntimeExec">
        <property name="commandsAndArguments">
          <map>
            <entry key=".*">
              <list>
                <value>fop.bat</value>
                <value>${source}</value>
                <value>${target}</value>
              </list>
            </entry>
          </map>
        </property>
        <property name="errorCodes">
          <value>1,2</value>
        </property>
      </bean>
    </property>
    <property name="explicitTransformations">
      <list>
        <bean class="org.alfresco.repo.content.transform.ExplictTransformationDetails">
          <property name="sourceMimetype">
        <value>text/xsl</value>
        </property>
          <property name="targetMimetype">
        <value>application/pdf</value>
      </property>
        </bean>
      </list>
    </property>
  </bean>
  <bean id="transformer.FOP.PDF" 
    class="org.alfresco.repo.content.transform.ProxyContentTransformer" 
    parent="baseContentTransformer">
    <property name="worker">
      <ref bean="transformer.worker.FOP.PDF" />
    </property>
  </bean>
</beans>

I the configuration above I only added the Windows command-line. If you run on Linux, you should remove the .bat extension.

Add the Mimetype

In order to allow Alfresco to recognize the XSL-FO source document, you need to add a new mimetype to Alfresco. There is no official mimetype for XSL-FO, but FOP lists an unregistered one called text/xsl. First add a file mimetypes-extension.xml in the Alfresco extension folder and add the following lines to the file:

<alfresco-config area="mimetype-map">
  <config evaluator="string-compare" condition="Mimetype Map">
    <mimetypes>
      <mimetype mimetype="text/xsl" display="XSL-FO">
        <extension>fo</extension>
      </mimetype>
    </mimetypes>
  </config>
</alfresco-config>

Next add a file custom-mimetype-map-context.xml to the extension folder in order to load the new mimetype during the system bootstrap.

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE beans PUBLIC '-//SPRING//DTD BEAN//EN' 
  'http://www.springframework.org/dtd/spring-beans.dtd'>
<beans>
  <bean id="mimetypeConfigService"
  class="org.springframework.extensions.config.xml.XMLConfigService"
  init-method="init">
    <constructor-arg>
      <bean class="org.springframework.extensions.config.source.UrlConfigSource">
        <constructor-arg>
          <list>
            <value>classpath:alfresco/mimetype/mimetype-map.xml</value>
            <value>classpath:alfresco/mimetype/mimetype-map-openoffice.xml</value>
            <value>classpath:alfresco/extension/mimetypes-extension.xml</value>
          </list>
        </constructor-arg>
      </bean>
    </constructor-arg>
  </bean>
</beans>

The last step is to add the transformer to the web-client-config-custom.xml:

<config evaluator="string-compare" condition="Action Wizards">
  <subtypes>
  </subtypes>
  <specialise-types>
  </specialise-types>
  <aspects>
  </aspects>
  <transformers>
    <transformer name="text/xsl" />
  </transformers>		
</config>

Restart Alfresco

When you restart Alfresco you should now be able to find the mimetype text/xsl listed in the Administration Web Script mentioned earlier. When you go to the details you should see that it is transformable to application/pdf.

Test the transformer

Now add a file called hello-world.fo to Alfresco with the following content:

<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
  <fo:layout-master-set>
    <fo:simple-page-master master-name="A4-portrait"
      page-height="29.7cm" page-width="21.0cm" margin="2cm">
      <fo:region-body/>
    </fo:simple-page-master>
  </fo:layout-master-set>
  <fo:page-sequence master-reference="A4-portrait">
    <fo:flow flow-name="xsl-region-body">
      <fo:block>Hello World!>/fo:block>
    </fo:flow>
  </fo:page-sequence>
</fo:root>

Go to the document’s detail view and click Run Action and select Transform and Copy Content. The required format shall be Adobe PDF Document. Once you click OK you should be able to find a document called hello-world.pdf in the selected destination space.

JavaScript example

You can use the content transformer action in JavaScript to transform an XSL-FO document to PDF:

var action = actions.create("transform"); 
action.parameters["destination-folder"] = document.parent; 
action.parameters["assoc-type"] = "{http://www.alfresco.org/model/content/1.0}contains"; 
action.parameters["assoc-name"] = document.name + "transformed"; 
action.parameters["mime-type"] = "application/pdf"; 
action.execute(document);

Creating XSL-FO output

There are a couple of options to create XSL-FO output in Alfresco. If you have XML documents that need to be transformed to PDF you can either use a Freemarker template or an XSLT stylesheet. You can use Alfresco’s XSLT Rendition Service to transform XML documents using XSLT.

Freemarker example

Create a file hello_world_fo.ftl in the space Data Dictionary/Presentation Templates and add the folowing lines to the file:

<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
  <fo:layout-master-set>
    <fo:simple-page-master master-name="A4-portrait"
      page-height="29.7cm" page-width="21.0cm" margin="2cm">
      <fo:region-body/>
    </fo:simple-page-master>
  </fo:layout-master-set>
  <fo:page-sequence master-reference="A4-portrait">
    <fo:flow flow-name="xsl-region-body">
      <fo:block>${document.name}</fo:block>
    </fo:flow>
  </fo:page-sequence>
</fo:root>

Next create a file hello-world.js in Data Dictionary/Scripts and add the following content:

var template = companyhome.childByNamePath("/Data Dictionary/Presentation Templates/hello_world_fo.ftl");

if (template != null) {
  var result = document.processTemplate(template);

  var parent = document.parent;
  var name = document.name.replace(".txt",".fo");

  var output = parent.childByNamePath(name);

  if (output ==null) {
    output = parent.createFile(name);
  }
  output.content = result;

  output.transformDocument("application/pdf");
}

When you execute the script using a Run Action on a document it creates a PDF file with the document name in the same space as the source document.

Reformat Rendering Engine

As with all transformations you can also use the XSL-FO transformer to create a rendition using the Reformat Rendering Engine. The following JavaScript code snippet provides an example:

var renditionDef = renditionService.createRenditionDefinition('cm:pdfRenditionDef', 'reformat');
renditionDef.parameters['mime-type'] = 'application/pdf';
var pdfRendition= renditionService.render(document, renditionDef);

This code will create a PDF rendition for the given XSL-FO document.

Using XSLT

You can use Alfresco’s rendition service to transform documents using XSLT in Alfresco. Using JavaScript you can for example transform a document with code similar to this:

var template = companyhome.childByNamePath("/Data Dictionary/XSLT Templates/hello-world.xsl");
var renditionDef = renditionService.createRenditionDefinition("cm:xsltRenditionDef", "xsltRenderingEngine");
renditionDef.parameters["template_node"] = template;
renditionDef.parameters["mime-type"] = "text/xsl";

var doc = renditionService.render(document, renditionDef);

You can then extend the rendition with a transformation to PDF using the Reformat Rendering Engine:

var template = companyhome.childByNamePath("/Data Dictionary/XSLT Templates/foptestxsl.xsl");

var renditionDef = renditionService.createRenditionDefinition("vlc:xslRenditionDef", "xsltRenderingEngine");
renditionDef.parameters["template_node"] = template;
renditionDef.parameters["mime-type"] = "text/xsl";

var doc = renditionService.render(document, renditionDef);

// add a destination to make the rendition visible
var destination = document.displayPath + "/" + document.name + "-rendered.pdf";

renditionDef = renditionService.createRenditionDefinition("cm:foRenditionDef", "reformat");
renditionDef.parameters['mime-type'] = "application/pdf";
renditionDef.parameters["destination-path-template"] = destination;

var pdfRendition= renditionService.render(doc, renditionDef);

This example first creates a rendition that applies an XSLT stylesheet to an XML document to produce an XSL-FO document and then creates a reformat rendition to transform the result to PDF using our Apache FOP based custom transformer.

Composite Rendering Engine

There is also a Composite Rendering Engine that is able to just persist the PDF which is actually what you want to do in this case. For example:

var template = companyhome.childByNamePath("/Data Dictionary/XSLT Templates/foptestxsl.xsl");

// the following method is not supported in the JavaScript renditionService
var compositeDefinition = renditionService.createCompositeRenditionDefinition("cm:compositeDefinition");

var xslRenditionDef = renditionService.createRenditionDefinition("cm:xslRenditionDef", "xsltRenderingEngine");
xslRenditionDef.parameters["template-node"] = template;
xslRenditionDef.parameters["mime-type"] = "text/xsl";

pdfRenditionDef = renditionService.createRenditionDefinition("cm:pdfRenditionDef", "reformat");
pdfRenditionDef.parameters['mime-type'] = 'application/pdf';

compositeDefinition.addAction(xslRenditionDef);
compositeDefinition.addAction(pdfRenditionDef);

var pdfRendition = renditionService.render(document, compositeRenditionDef);

This will not work as the Composite Rendering Engine does not seem to be exposed in the JavaScript API. The purpose of this post was to demonstrate what you can do without having to code Java. In a follow-up post I will show how to do this in Java.

Follow-up post

Read the follow-up post Using Alfresco Composite Rendition to Render PDF for examples how to use the Composite Rendition in Java and how to add the custom transformer in Java to use the FOP library already available in Alfresco in stead of having a separate installation of Apache FOP on the server.

Last updated: October 12, 2011

Component Content Management with Componize and Alfresco

Belle de Mai

Last week I attended the Componize Customization Training at the offices of Componize in Marseille. They are located in the Media Park building Belle de Mai in the center of Marseille that houses companies that work in the creative sector. 

They offer a framework for component content management on top of Alfresco. It is all well designed and developed. You can use it to produce, manage and publish product documentation, training guides, legal documents or other publications. It allows you to maintain your content as a single source and publish in different configurations and output formats.

Output Processing Dialog

Where possible they implemented open standards. Besides XML these include DITA (Darwin Information Typing Architecture), XProc, XLink, XSLT 2.0 and RDF (Resource Description Framework). Out of the box it supports the DITA standard and DocBook, but you can configure the system to use any schema.

It offers extensive support for XML metadata management and link management and you can configure complex pipelines to process output. Processes that potentially use a lot of resources run in the background allowing the user to continue his or her work while the back-end system executes the required jobs. 

Link Management

I used to do a lot of XML related projects for publishers, but in recent years I got more and more involved in general content management projects, so I am quite happy that this solution brings both worlds together.