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.
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 |
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 |
|
Scala sample application, "master" branch |
Scala Application, Compile-Time DI |
|
Scala sample application, alternate branch |
Java Application, Runtime DI |
|
Java sample application, "master" branch |
Java Application, Compile-Time DI |
|
Java sample application, alternate branch |
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:
scala-xml
Node
, or as a String
; andorg.apache.xmlgraphics.util.MimeConstants
.
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:
autoDetectFontsForPDF
: Whether, when creating PDF files, Apache FOP should automatically detect operating system fonts (and make them available for PDF files).FOUserAgent
block: A block of code that modifies the FOUserAgent
that Apache FOP will use.
Useful for setting a PDF file's subject, author, etc.
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):
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:
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.
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:
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" />
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.
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:
|