Creating tables with Ibex
This example shows how to create a PDF document which contains data laid out using tables. This example includes details on how to define page layout and format tables including repeating headers and footers.
The XML Data
The data used for this example is a list of books, including their ISBN numbers and prices. This data is contained in an XML file called bookdata.xml, and looks like this:
<?xml version="1.0" encoding="utf-8"?>
<bookdata>
<book>
<isbn>978-1592001170</isbn>
<title>Inspired 3D Short Film Production</title>
<author>Jeremy Cantor and Pepe Valencia</author>
<price>31.50</price>
</book>
<book>
<isbn>978-0321316318</isbn>
<title>Digital Lighting and Rendering</title>
<author>Jeremy Birn</author>
<price>34.65</price>
</book>
<book>
<isbn>978-0571202287</isbn>
<title>The Animator's Survival Kit</title>
<author>Richard Williams </author>
<price>19.80</price>
</book>
</bookdata>
Page Layout
For this example we use a single page size, specifically 11" high by 8.5" wide. The layout of the page and positioning of all contents of the PDF file is defined in an XSL stylesheet. This XSL transforms the data XML into XML in the XSL-FO namespace, which Ibex then converts into a PDF file.
Although it is possible to perform the XSL transformation externally to Ibex, in this example we will pass Ibex the XML and XSL files and Ibex will perform the transformation.
A simple stylesheet which defines the page layout but does nothing with the data looks like this:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes" />
<xsl:strip-space elements="*" />
<xsl:template match="bookdata">
<fo:root>
<fo:layout-master-set>
<fo:simple-page-master master-name="page-layout" page-height="11in" page-width="8in">
<fo:region-body margin="1in" region-name="body" background-color="#dddddd" />
</fo:simple-page-master>
</fo:layout-master-set>
<fo:page-sequence master-reference="page-layout">
<fo:flow flow-name="body">
</fo:flow>
</fo:page-sequence>
</fo:root>
</xsl:template>
</xsl:stylesheet>
In the next sections we will extend this stylesheet, adding table creation templates.
Running Ibex
Creating the FO file
We can run Ibex from the command line to create the PDF file, or just execute the XSL transformation process and create the FO file by using this command:
ibex -transform bookdata.xml layout1.xsl layout1.fo
We can produce the FO file, which looks like this:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes" />
<xsl:strip-space elements="*" />
<xsl:template match="bookdata">
<fo:root>
<fo:layout-master-set>
<fo:simple-page-master master-name="page-layout" page-height="11in" page-width="8in">
<fo:region-body margin="1in" region-name="body" background-color="#dddddd" />
</fo:simple-page-master>
</fo:layout-master-set>
<fo:page-sequence master-reference="page-layout">
<fo:flow flow-name="body">
</fo:flow>
</fo:page-sequence>
</fo:root>
</xsl:template>
</xsl:stylesheet>
When learning to use Ibex it can be very useful to create and examine the FO file to make sure that the results of the XSL transformation are as expected.
Creating the PDF file
We can tell Ibex to create the PDF file from the FO file using this command:
ibex layout1.fo layout1.pdf
This will create a PDF file with a single blank page. We haven't referenced any of the XML data from our stylesheet, so there will no data in the PDF file.
Instead of creating the FO file and passing it to Ibex as a two-step process, we can create the PDF file with a single command like this:
ibex -xsl layout1.xsl bookdata.xml layout1.pdf
Adding data to the page
We add the data to the PDF file by creating a table element and then adding a row to that table for each book element in the data.
The basic structure of an FO table looks like this:
<fo:table>
<fo:table-body>
<fo:table-row>
<fo:table-cell>
<fo:block/>
</fo:table-cell>
</fo:table-row>
</fo:table-body>
</fo:table>
The table rows are contained inside the fo:table-body element. A table can contain any number of rows each containing any number of cells.
Within the flow element we add a table like this, including a call to xsl:apply-templates which will add a table-row for each book element in the XML.
<fo:table>
<fo:table-body>
<xsl:apply-templates select="book"/>
</fo:table-body>
</fo:table>
The template which creates a row for each book element looks like this:
<xsl:template match="book">
<fo:table-row>
<fo:table-cell>
<fo:block>
<xsl:value-of select="isbn/text()"/>
</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block>
<xsl:value-of select="title/text()"/>
</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block>
<xsl:value-of select="price/text()"/>
</fo:block>
</fo:table-cell>
</fo:table-row>
</xsl:template>
The full stylesheet to display the data in a table looks like this:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:fo="http://www.w3.org/1999/XSL/Format"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes" />
<xsl:strip-space elements="*" />
<xsl:template match="bookdata">
<fo:root>
<fo:layout-master-set>
<fo:simple-page-master master-name="page-layout" page-height="11in" page-width="8in">
<fo:region-body margin="1in" region-name="body" background-color="#dddddd" />
</fo:simple-page-master>
</fo:layout-master-set>
<fo:page-sequence master-reference="page-layout">
<fo:flow flow-name="body">
<fo:table>
<fo:table-body>
<xsl:apply-templates select="book" />
</fo:table-body>
</fo:table>
</fo:flow>
</fo:page-sequence>
</fo:root>
</xsl:template>
<xsl:template match="book">
<fo:table-row>
<fo:table-cell>
<fo:block>
<xsl:value-of select="isbn/text()" />
</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block>
<xsl:value-of select="title/text()" />
</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block>
<xsl:value-of select="price/text()" />
</fo:block>
</fo:table-cell>
</fo:table-row>
</xsl:template>
</xsl:stylesheet>
And the complete FO now including the table of book data looks like this:
<?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="page-layout" page-height="11in" page-width="8in">
<fo:region-body margin="1in" region-name="body" background-color="#dddddd" />
</fo:simple-page-master>
</fo:layout-master-set>
<fo:page-sequence master-reference="page-layout">
<fo:flow flow-name="body">
<fo:table>
<fo:table-body>
<fo:table-row>
<fo:table-cell>
<fo:block>978-1592001170</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block>Inspired 3D Short Film Production</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block>31.50</fo:block>
</fo:table-cell>
</fo:table-row>
<fo:table-row>
<fo:table-cell>
<fo:block>978-0321316318</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block>Digital Lighting and Rendering</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block>34.65</fo:block>
</fo:table-cell>
</fo:table-row>
<fo:table-row>
<fo:table-cell>
<fo:block>978-0571202287</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block>The Animator's Survival Kit</fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block>19.80</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-body>
</fo:table>
</fo:flow>
</fo:page-sequence>
</fo:root>
Borders and padding
The table in the PDF file looks like this:
We can add cell borders by using the border attribute on each cell. We also want to add some padding so that there is some spacing between the cell contents and the borders. To do this we change the stylesheet, adding border and padding attributes to each cell:
<xsl:template match="book">
<fo:table-row>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>
<xsl:value-of select="isbn/text()"/>
</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>
<xsl:value-of select="title/text()"/>
</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt" text-align="right">
<fo:block>
<xsl:value-of select="price/text()"/>
</fo:block>
</fo:table-cell>
</fo:table-row>
</xsl:template>
With the borders and padding added the table in the PDF file looks like this:
Column widths
If column widths are not specified on the table then Ibex attempts automatic layout, determining the width of each column by looking at the content of the first rows in the table. Sometimes this produces an acceptable result, but if possible it should not be relied on in production systems. The fact that the layout of the table changes depending on the data means you need to test for a wide range of possible data values. A better practice is to specify the column widths so that the table always looks the same.
Column widths are specified using the table-column element. These elements are placed in the table element before any table-header or table-body elements. Each table-column element should specify the number of the column it applies to (starting at 1) and the width of that column.
Column widths can be specified as fixed amounts such as "2cm" or proportional amounts such as "20%".
The columns for this example are specified like this:
<fo:table>
<fo:table-column column-number="1" column-width="25%"/>
<fo:table-column column-number="2" column-width="60%"/>
<fo:table-column column-number="3" column-width="15%"/>
<fo:table-body>
<xsl:apply-templates select="book"/>
</fo:table-body>
</fo:table>
The complete stylesheet now looks like this:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:fo="http://www.w3.org/1999/XSL/Format"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes" />
<xsl:strip-space elements="*" />
<xsl:template match="bookdata">
<fo:root>
<fo:layout-master-set>
<fo:simple-page-master master-name="page-layout" page-height="11in" page-width="8in">
<fo:region-body margin="1in" region-name="body" background-color="#dddddd" />
</fo:simple-page-master>
</fo:layout-master-set>
<fo:page-sequence master-reference="page-layout">
<fo:flow flow-name="body">
<fo:table>
<fo:table-column column-number="1" column-width="25%" />
<fo:table-column column-number="2" column-width="60%" />
<fo:table-column column-number="3" column-width="15%" />
<fo:table-body>
<xsl:apply-templates select="book" />
</fo:table-body>
</fo:table>
</fo:flow>
</fo:page-sequence>
</fo:root>
</xsl:template>
<xsl:template match="book">
<fo:table-row>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>
<xsl:value-of select="isbn/text()" />
</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>
<xsl:value-of select="title/text()" />
</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt" text-align="right">
<fo:block>
<xsl:value-of select="price/text()" />
</fo:block>
</fo:table-cell>
</fo:table-row>
</xsl:template>
</xsl:stylesheet>
With the columns widths specified the table in the PDF file looks like this:
Table headers
Table headers are created using the fo:table-header element, which appears inside the fo:table element just before the fo:table-body element. The table-header is similar in structure to a table-body in that it contains table-row elements.
The XSL for creating a table with a header looks like this:
<fo:table>
<fo:table-column column-number="1" column-width="25%"/>
<fo:table-column column-number="2" column-width="60%"/>
<fo:table-column column-number="3" column-width="15%"/>
<fo:table-header font-weight="bold">
<fo:table-row>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>ISBN Number
</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>Title
</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt" text-align="right">
<fo:block>Price
</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-header>
<fo:table-body>
<xsl:apply-templates select="book"/>
</fo:table-body>
</fo:table>
Note we have added font-weight attribute to the header to make it stand out. The complete XSL now looks like this:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:fo="http://www.w3.org/1999/XSL/Format"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes" />
<xsl:strip-space elements="*" />
<xsl:template match="bookdata">
<fo:root>
<fo:layout-master-set>
<fo:simple-page-master master-name="page-layout" page-height="11in" page-width="8in">
<fo:region-body margin="1in" region-name="body" background-color="#dddddd" />
</fo:simple-page-master>
</fo:layout-master-set>
<fo:page-sequence master-reference="page-layout">
<fo:flow flow-name="body">
<fo:table>
<fo:table-column column-number="1" column-width="25%" />
<fo:table-column column-number="2" column-width="60%" />
<fo:table-column column-number="3" column-width="15%" />
<fo:table-header font-weight="bold">
<fo:table-row>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>ISBN Number</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>Title</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt" text-align="right">
<fo:block>Price</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-header>
<fo:table-body>
<xsl:apply-templates select="book" />
</fo:table-body>
</fo:table>
</fo:flow>
</fo:page-sequence>
</fo:root>
</xsl:template>
<xsl:template match="book">
<fo:table-row>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>
<xsl:value-of select="isbn/text()" />
</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>
<xsl:value-of select="title/text()" />
</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt" text-align="right">
<fo:block>
<xsl:value-of select="price/text()" />
</fo:block>
</fo:table-cell>
</fo:table-row>
</xsl:template>
</xsl:stylesheet>
With the header added the table in the PDF file looks like this:
Repeating headers
Repeating the table header after each page break is the default behaviour of Ibex. To prevent the header being repeated specify table-omit-header-at-break="true" on the table element.
Table footers
Table footers are created using the table-footer element, which appears inside the table element, after the optional table-header element and just before the table-body element. The table-footer is similar in structure to a table-body in that it contains table-row elements.
The XSL for creating a table with a footer with the total of the price elements looks like this:<
<fo:table>
<fo:table-column column-number="1" column-width="25%"/>
<fo:table-column column-number="2" column-width="60%"/>
<fo:table-column column-number="3" column-width="15%"/>
<fo:table-header font-weight="bold">
<fo:table-row>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>ISBN Number
</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>Title
</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt" text-align="right">
<fo:block>Price
</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-header>
<fo:table-footer font-weight="bold">
<fo:table-row>
<fo:table-cell number-columns-spanned="2" border="1pt solid black" padding="4pt">
<fo:block>Total Price
</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt" text-align="right">
<fo:block>
<xsl:value-of select="sum(book/price)"/>
</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-footer>
<fo:table-body>
<xsl:apply-templates select="book"/>
</fo:table-body>
</fo:table>
The complete XSL now looks like this:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:fo="http://www.w3.org/1999/XSL/Format"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes" />
<xsl:strip-space elements="*" />
<xsl:template match="bookdata">
<fo:root>
<fo:layout-master-set>
<fo:simple-page-master master-name="page-layout" page-height="11in" page-width="8in">
<fo:region-body margin="1in" region-name="body" background-color="#dddddd" />
</fo:simple-page-master>
</fo:layout-master-set>
<fo:page-sequence master-reference="page-layout">
<fo:flow flow-name="body">
<fo:table>
<fo:table-column column-number="1" column-width="25%" />
<fo:table-column column-number="2" column-width="60%" />
<fo:table-column column-number="3" column-width="15%" />
<fo:table-header font-weight="bold">
<fo:table-row>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>ISBN Number</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>Title</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt" text-align="right">
<fo:block>Price</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-header>
<fo:table-footer font-weight="bold">
<fo:table-row>
<fo:table-cell number-columns-spanned="2" border="1pt solid black" padding="4pt">
<fo:block>Total Price</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt" text-align="right">
<fo:block>
<xsl:value-of select="sum(book/price)" />
</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-footer>
<fo:table-body>
<xsl:apply-templates select="book" />
</fo:table-body>
</fo:table>
</fo:flow>
</fo:page-sequence>
</fo:root>
</xsl:template>
<xsl:template match="book">
<fo:table-row>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>
<xsl:value-of select="isbn/text()" />
</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>
<xsl:value-of select="title/text()" />
</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt" text-align="right">
<fo:block>
<xsl:value-of select="price/text()" />
</fo:block>
</fo:table-cell>
</fo:table-row>
</xsl:template>
</xsl:stylesheet>
With the footer added the table in the PDF file looks like this:
Repeating footers
Rrepeating the table footer before after each page break is the default behaviour of Ibex. To prevent the footer being repeated specify table-omit-footer-at-break="true" on the table element.
Conditional footers
Conditional footers are footers which may or may not appear depending on certain conditions. The traditional example is a footer which says "continued on next page", and appears on every page a table is on except the last one.
Conditional footers are implemented in two parts: a marker element which specifies the content to appear in the footer, and retrieve-table-marker element which appears inside the footer. Typically there are many marker elements, only one of which is retrieved for display inside the footer. Which marker is selected depends on the attributes specified on the retrieve-table-marker.
In the most common case a marker is associated with the first row of the table, and contains the message "continued on next page". This is the marker we want displayed for all pages the table appears on except the last.
A second empty marker element is placed on the last row of the table. On pages in which the does not appear, the "continued on next page" marker will be displayed in the footer. On the last page of the table the empty marker is retrieved and no message is displayed in the footer.
The template to insert a marker in the first and last rows of the table looks like this:
<xsl:template match="book">
<fo:table-row>
<xsl:if test="position() = 1">
<fo:marker retrieve-class-name="booklist">
<fo:block>continued on next page</fo:block>
</fo:marker>
</xsl:if>
<xsl:if test="position() = last()">
<fo:marker retrieve-class-name="booklist"/>
</xsl:if>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>
<xsl:value-of select="isbn/text()"/>
</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt">
<fo:block>
<xsl:value-of select="title/text()"/>
</fo:block>
</fo:table-cell>
<fo:table-cell border="1pt solid black" padding="4pt" text-align="right">
<fo:block>
<xsl:value-of select="price/text()"/>
</fo:block>
</fo:table-cell>
</fo:table-row>
</xsl:template>
In order to create an effective example we need enought data to fill more than one page, so we have created bookdata2.xml which contains data for many books.
The XSL created by this template is shown below - we can see the marker elements on the first and last rows.
<?xml version="1.0" encoding="utf-8"?>
<root xmlns="http://www.w3.org/1999/XSL/Format">
<layout-master-set>
<simple-page-master master-name="page-layout" page-height="11in" page-width="8in">
<region-body margin="1in" region-name="body" background-color="#dddddd" />
</simple-page-master>
</layout-master-set>
<page-sequence master-reference="page-layout">
<flow flow-name="body">
<table>
<table-body>
<table-row>
<marker retrieve-class-name="booklist">
<block>continued on next page</block>
</marker>
<table-cell border="1pt solid black" padding="4pt">
<block>978-1592001170</block>
</table-cell>
<table-cell border="1pt solid black" padding="4pt">
<block>Inspired 3D Short Film Production</block>
</table-cell>
<table-cell border="1pt solid black" padding="4pt" text-align="right">
<block>31.50</block>
</table-cell>
</table-row>
<table-row>
<table-cell border="1pt solid black" padding="4pt">
<block>978-0321316318</block>
</table-cell>
<table-cell border="1pt solid black" padding="4pt">
<block>Digital Lighting and Rendering</block>
</table-cell>
<table-cell border="1pt solid black" padding="4pt" text-align="right">
<block>34.65</block>
</table-cell>
</table-row>
<table-row>
<marker retrieve-class-name="booklist" />
<table-cell border="1pt solid black" padding="4pt">
<block>978-0571202287</block>
</table-cell>
<table-cell border="1pt solid black" padding="4pt">
<block>The Animator's Survival Kit</block>
</table-cell>
<table-cell border="1pt solid black" padding="4pt" text-align="right">
<block>19.80</block>
</table-cell>
</table-row>
</table-body>
</table>
</flow>
</page-sequence>
</root>