PlayFOP User Guide


PlayFOP is a library for generating PDFs, images, and other types of output in Play Framework applications. PlayFOP accepts XSL-FO that an application has generated—via a Play Twirl template, with the scala-xml library, or as a String—and processes it with Apache FOP.

For information on PlayFOP beyond this user guide, see its homepage.

This guide applies to PlayFOP 1.0.

Package Structure


PlayFOP's public API is divided into three packages. For a given application, package com.dmanchester.playfop.api is relevant, as well as either com.dmanchester.playfop.sapi or com.dmanchester.playfop.japi, depending on the application's language:

Package Relevant to Applications in... Documentation
com.dmanchester.playfop.api Scala or Java Scaladoc
com.dmanchester.playfop.sapi Scala Scaladoc
com.dmanchester.playfop.japi Java Javadoc

Using PlayFOP


PlayFOP is available in Maven Central/the Central Repository. To use it, add PlayFOP as a dependency to your application's build.sbt:

libraryDependencies ++= Seq(
...,
"com.dmanchester" %% "playfop" % "1.0"
)

Then, inject a PlayFop object via dependency injection (DI), following the guidance for your scenario (guidance assumes you are using constructor-based DI):

Scenario Guidance Example
Scala Application, Runtime DI
  • In your controller, add a PlayFop constructor parameter (package com.dmanchester.playfop.sapi).
  • In application.conf, add: play.modules.enabled += "com.dmanchester.playfop.sapi.PlayFopModule"
Scala sample application, "master" branch
Scala Application, Compile-Time DI
  • In your controller, add a PlayFop constructor parameter (package com.dmanchester.playfop.sapi).
  • In your "components" class, add PlayFopComponents (package com.dmanchester.playfop.sapi) to the list of traits you mix in via with. Pass playFop to the controller constructor.
Scala sample application, alternate branch
Java Application, Runtime DI
  • In your controller, add a PlayFop constructor parameter and field (package com.dmanchester.playfop.japi). Set the field from the constructor parameter.
  • In application.conf, add: play.modules.enabled += "com.dmanchester.playfop.japi.PlayFopModule"
Java sample application, "master" branch
Java Application, Compile-Time DI
  • In your controller, add a PlayFop constructor parameter and field (package com.dmanchester.playfop.japi). Set the field from the constructor parameter.
  • In your "components" class, add PlayFopComponents (package com.dmanchester.playfop.japi) to the list of interfaces your class implements. Invoke playFop() and pass the resulting value to the controller constructor.
Java sample application, alternate branch

Generating PDFs, Images, and Other Output: Using the PlayFop Object

With the PlayFop object you inject into your application, you can generate the various kinds of output supported by Apache FOP.

At the point in your code where you wish to generate output, invoke one of the PlayFop object's processing methods: processTwirlXml or processStringXml, or processScalaXml (Scala API only). The method will return the output as a byte array, which can be returned in an HTTP response, saved to a file, etc.

The simplest processing invocations involve two arguments:

For example, given someTwirlTemplate.scala.xml...

@(text: String)<?xml version='1.0' encoding='utf-8'?>
<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
  <fo:layout-master-set>
    <fo:simple-page-master master-name="label">
      <fo:region-body region-name="xsl-region-body"/>
    </fo:simple-page-master>
  </fo:layout-master-set>
  <fo:page-sequence master-reference="label">
    <fo:flow flow-name="xsl-region-body">
      <fo:block font-size="120pt">@text</fo:block>
    </fo:flow>
  </fo:page-sequence>
</fo:root>

...you could generate a PNG image as follows:

Simple Processing Example, Twirl Template, Scala

val png: Array[Byte] = playFop.processTwirlXml(
  views.xml.someTwirlTemplate.render("Hello world."),
  MimeConstants.MIME_PNG
)

Simple Processing Example, Twirl Template, Java

byte[] png = playFop.processTwirlXml(
    views.xml.someTwirlTemplate.render("Hello world."),
    MimeConstants.MIME_PNG
);

The resulting PNG (Scala or Java):

You can also pass to a process... method either or both of the following options:

In Scala applications, you typically supply these options as named arguments. The FOUserAgent block is an FOUserAgent-accepting function.

In Java applications, you supply these items via a builder. The FOUserAgent block is an implementation of the single-method FOUserAgentBlock interface.

The following code samples present the syntax for using autoDetectFontsForPDF and an FOUserAgent block. They presume a String literal "xslfo", equivalent to the above Twirl template rendered with the text, "Hello again."

Complex Processing Example, String Literal, Scala

val myFOUserAgentBlock = { foUserAgent: FOUserAgent =>
  foUserAgent.setAuthor("PlayFOP Sample Code")
}

val pdf: Array[Byte] = playFop.processStringXml(
  xslfo,
  MimeConstants.MIME_PDF,
  autoDetectFontsForPDF = true,
  foUserAgentBlock = myFOUserAgentBlock
)

Complex Processing Example, String Literal, Java

FOUserAgentBlock myFOUserAgentBlock = new FOUserAgentBlock() {
    @Override
    public void withFOUserAgent(FOUserAgent foUserAgent) {
        foUserAgent.setAuthor("PlayFOP Sample Code");
    }
};

ProcessOptions processOptions = new ProcessOptions.Builder().
        autoDetectFontsForPDF(true).
        foUserAgentBlock(myFOUserAgentBlock).
        build();

byte[] pdf = playFop.processStringXml(
    xslfo,
    MimeConstants.MIME_PDF,
    processOptions
);

The resulting PDF (Scala or Java):

Applying Apache FOP Units to Values: the Units Class

To apply a numeric value to an XSL-FO element, you generally place the value in an attribute of the element. The value is labeled with the unit of measure (if any), without any intervening space: 29.7cm, 9pt, etc.

PlayFOP's Units class handles this formatting. To use it:

  1. Create an instance for the desired unit of measure, passing the label and the precision (the number of decimal places to display when formatting a value).
  2. Call the instance's format method, passing the value to be formatted.

Units Example, Inside a Twirl Template

...beginning of a Twirl template that accepts a fontSizeInPoints argument...

@import com.dmanchester.playfop.api.Units
@pt = @{new Units("pt", 0)}

<fo:block-container font-size="@pt.format(fontSizeInPoints)" ... >

...rest of template...

Units instances are thread-safe.

Preserving Whitespace: the Formatters

As XSL-FO is an XML dialect, Apache FOP ignores some kinds of whitespace when processing XSL-FO documents. In particular:

PlayFOP's Formatters object offers methods for preserving both kinds of whitespace.

The preserveSpaces method replaces "regular" spaces with no-break ones. (No-break spaces are not subject to collapsing.)

The newline-preservation methods (preserveNewlinesForTwirlXml, preserveNewlinesForScalaXml, and preserveNewlinesForStringXml) take newline-terminated runs of characters and wrap each one in an <fo:block> element. With the characters wrapped in this fashion, the newlines are preserved in output.

All Formatters methods accept a String argument. preserveSpaces returns a String, while the newline-preservation methods return an object of the type suggested by the method name:

Method Returns
preserveNewlinesForTwirlXml a Twirl Xml instance
preserveNewlinesForScalaXml a scala-xml Seq[Node]
preserveNewlinesForStringXml a String

So, if you want both kinds of whitespace preservation, invoke preserveSpaces first and pass its return value to a newline-preservation method.

Formatters Example

Invoking the following code block with text set to the string "H e l l o\n\n w o r l d"...

import com.dmanchester.playfop.api.Formatters

val formattedText = Formatters.preserveNewlinesForScalaXml(
  Formatters.preserveSpaces(text)
)

val xslfo =
<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
  <fo:layout-master-set>
    <fo:simple-page-master master-name="label">
      <fo:region-body region-name="xsl-region-body"/>
    </fo:simple-page-master>
  </fo:layout-master-set>
  <fo:page-sequence master-reference="label">
    <fo:flow flow-name="xsl-region-body">
      <fo:block font-family="Courier" font-size="24pt">{formattedText}</fo:block>
    </fo:flow>
  </fo:page-sequence>
</fo:root>

...and rendering xslfo as a PDF leads to:

Logging

You can configure logging for PlayFOP and Apache FOP via your logback.xml file. PlayFOP logs to the com.dmanchester.playfop logger; Apache FOP, to the org.apache.fop and org.apache.xmlgraphics loggers. So, your logging configuration might include:

<logger name="com.dmanchester.playfop" level="INFO" />
<logger name="org.apache.fop" level="ERROR" />
<logger name="org.apache.xmlgraphics" level="ERROR" />

Thread Safety

PlayFOP seeks to provide thread safety everywhere it makes sense to do so, and classes' thread safety is documented in the Scaladoc and Javadoc. However, there is an open question around the thread safety of Apache FOP itself. The Apache FOP documentation has long included the following statement:

"Apache FOP may currently not be completely thread safe. The code has not been fully tested for multi-threading issues, yet. If you encounter any suspicious behaviour, please notify us."

Reviews of the Apache FOP issue tracker and the "FOP Users" mailing list do not turn up recent trouble reports around multi-threaded Apache FOP use, and this thread mentions good experiences.

PlayFOP therefore takes a pragmatic approach: If an application uses PlayFOP in a multi-threaded fashion, PlayFOP's use of Apache FOP is multi-threaded as well.

If PlayFOP's multi-threading of Apache FOP causes problems for your application, synchronizing your access to your PlayFop object should resolve them. Please also comment on the related placeholder issue with details of the problems you've encountered.

Developing PlayFOP


PlayFOP is built with sbt. It includes a full suite of tests (specs2 for Scala code, JUnit for Java code).

With sbt's cross-building support, PlayFOP can target Scala 2.11 and 2.12.

To begin developing changes to PlayFOP, update build.sbt with a "...-SNAPSHOT" version number. (Doing so will ensure locally published JARs and POMs are not undesirably cached by applications that use them.) Update the version number in UserGuide.scalatex as well.

As you develop your changes, you'll likely make use of a combination of the sbt commands in the table below. By default, commands target Scala 2.12. To target both Scala 2.11 and 2.12, prefix a command with "+".

Command Description
clean Deletes previous compilation results.
compile Compiles the source code.
test Runs the tests.
publishLocal Compiles the source code and places the resulting JAR and POM file in your local Ivy repository.
scaladocOnly/doc Generates the PlayFOP Scaladoc and places it at library/target-scaladoc/scala-2.12/api/index.html.
javadocOnly/doc Generates the PlayFOP Javadoc and places it at library/target-javadoc/scala-2.12/api/index.html.
userguide/run Generates the user guide and places it at library/userguide/target/scalatex/index.html.

When your changes are final and you wish to obtain a distribution, change to a non-snapshot version number in build.sbt and UserGuide.scalatex and execute the commands in the table below. (Cross-building for Scala 2.11 and 2.12 with "+" is essential for a complete distribution.)

Command Description
+cleanAll Deletes the library/dist_... directories, as well as previous compilation results, previously generated Scaladoc and Javadoc, and the previously generated user guide (if applicable).
+publishAll Publishes to each of the library/dist_... directories a full set of PlayFOP artifacts:
  • the PlayFOP JAR and POM
  • JARs of the Scaladoc and Javadoc
  • a ZIP of the user guide
  • a JAR of the source code