Creating tables with Ibex

John Farrow, Visual Programming Limited

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="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">
    <root>
      <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">
        </flow>
      </page-sequence>
    </root>
  </xsl:template>
</xsl:stylesheet>

[Download examples/tabular/layout1.xsl]



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 to only execute the XSL transformation process and create the FO file.

By running this command:

ibex35 -transform bookdata.xml layout1.xsl layout1.fo

We can produce the FO file, which looks like this:

<?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" />
  </page-sequence>
</root>

[Download examples/tabular/layout1.fo]



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:

ibex35 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:

ibex35 -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:

<table>          
  <table-body>
    <table-row>
      <table-cell>
        <block/>
      </table-cell>
    </table-row>
  </table-body>
</table>
        

The table rows are contained inside the 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.

 <table>
  <table-body>
   <xsl:apply-templates select="book"/>
  </table-body>
 </table>

The template which creates a row for each book element looks like this:

 <xsl:template match="book">
  <table-row>
   <table-cell>
    <block>
     <xsl:value-of select="isbn/text()"/>
    </block>
   </table-cell>
   <table-cell>
    <block>
     <xsl:value-of select="title/text()"/>
    </block>
   </table-cell>
   <table-cell>
    <block>
     <xsl:value-of select="price/text()"/>
    </block>
   </table-cell>
  </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="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">
    <root>
      <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>
              <xsl:apply-templates select="book" />
            </table-body>
          </table>
        </flow>
      </page-sequence>
    </root>
  </xsl:template>
  <xsl:template match="book">
    <table-row>
      <table-cell>
        <block>
          <xsl:value-of select="isbn/text()" />
        </block>
      </table-cell>
      <table-cell>
        <block>
          <xsl:value-of select="title/text()" />
        </block>
      </table-cell>
      <table-cell>
        <block>
          <xsl:value-of select="price/text()" />
        </block>
      </table-cell>
    </table-row>
  </xsl:template>
</xsl:stylesheet>

[Download examples/tabular/layout2.xsl]



And the complete FO now including the table of book data looks like this:

<?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>
            <table-cell>
              <block>978-1592001170</block>
            </table-cell>
            <table-cell>
              <block>Inspired 3D Short Film Production</block>
            </table-cell>
            <table-cell>
              <block>31.50</block>
            </table-cell>
          </table-row>
          <table-row>
            <table-cell>
              <block>978-0321316318</block>
            </table-cell>
            <table-cell>
              <block>Digital Lighting and Rendering</block>
            </table-cell>
            <table-cell>
              <block>34.65</block>
            </table-cell>
          </table-row>
          <table-row>
            <table-cell>
              <block>978-0571202287</block>
            </table-cell>
            <table-cell>
              <block>The Animator's Survival Kit</block>
            </table-cell>
            <table-cell>
              <block>19.80</block>
            </table-cell>
          </table-row>
        </table-body>
      </table>
    </flow>
  </page-sequence>
</root>

[Download examples/tabular/layout2.fo]



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">
  <table-row>
   <table-cell border="1pt solid black" padding="4pt">
    <block>
     <xsl:value-of select="isbn/text()"/>
    </block>
   </table-cell>
   <table-cell border="1pt solid black" padding="4pt">
    <block>
     <xsl:value-of select="title/text()"/>
    </block>
   </table-cell>
   <table-cell border="1pt solid black" padding="4pt" text-align="right">
    <block>
     <xsl:value-of select="price/text()"/>
    </block>
   </table-cell>
  </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:

 <table>
  <table-column column-number="1" column-width="25%"/>
  <table-column column-number="2" column-width="60%"/>
  <table-column column-number="3" column-width="15%"/>
  <table-body>
   <xsl:apply-templates select="book"/>
  </table-body>
 </table>

The complete stylesheet now looks like this:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns="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">
    <root>
      <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-column column-number="1" column-width="25%" />
            <table-column column-number="2" column-width="60%" />
            <table-column column-number="3" column-width="15%" />
            <table-body>
              <xsl:apply-templates select="book" />
            </table-body>
          </table>
        </flow>
      </page-sequence>
    </root>
  </xsl:template>
  <xsl:template match="book">
    <table-row>
      <table-cell border="1pt solid black" padding="4pt">
        <block>
          <xsl:value-of select="isbn/text()" />
        </block>
      </table-cell>
      <table-cell border="1pt solid black" padding="4pt">
        <block>
          <xsl:value-of select="title/text()" />
        </block>
      </table-cell>
      <table-cell border="1pt solid black" padding="4pt" text-align="right">
        <block>
          <xsl:value-of select="price/text()" />
        </block>
      </table-cell>
    </table-row>
  </xsl:template>
</xsl:stylesheet>

[Download examples/tabular/layout4.xsl]



With the columns widths specified the table in the PDF file looks like this:

Table headers

Table headers are created using the table-header element, which appears inside the table element just before the 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:

 <table>
  <table-column column-number="1" column-width="25%"/>
  <table-column column-number="2" column-width="60%"/>
  <table-column column-number="3" column-width="15%"/>
  <table-header font-weight="bold">
   <table-row>
    <table-cell border="1pt solid black" padding="4pt">
     <block>ISBN Number
     </block>
    </table-cell>
    <table-cell border="1pt solid black" padding="4pt">
     <block>Title
     </block>
    </table-cell>
    <table-cell border="1pt solid black" padding="4pt" text-align="right">
     <block>Price
     </block>
    </table-cell>
   </table-row>
  </table-header>
  <table-body>
   <xsl:apply-templates select="book"/>
  </table-body>
 </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="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">
    <root>
      <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-column column-number="1" column-width="25%" />
            <table-column column-number="2" column-width="60%" />
            <table-column column-number="3" column-width="15%" />
            <table-header font-weight="bold">
              <table-row>
                <table-cell border="1pt solid black" padding="4pt">
                  <block>ISBN Number</block>
                </table-cell>
                <table-cell border="1pt solid black" padding="4pt">
                  <block>Title</block>
                </table-cell>
                <table-cell border="1pt solid black" padding="4pt" text-align="right">
                  <block>Price</block>
                </table-cell>
              </table-row>
            </table-header>
            <table-body>
              <xsl:apply-templates select="book" />
            </table-body>
          </table>
        </flow>
      </page-sequence>
    </root>
  </xsl:template>
  <xsl:template match="book">
    <table-row>
      <table-cell border="1pt solid black" padding="4pt">
        <block>
          <xsl:value-of select="isbn/text()" />
        </block>
      </table-cell>
      <table-cell border="1pt solid black" padding="4pt">
        <block>
          <xsl:value-of select="title/text()" />
        </block>
      </table-cell>
      <table-cell border="1pt solid black" padding="4pt" text-align="right">
        <block>
          <xsl:value-of select="price/text()" />
        </block>
      </table-cell>
    </table-row>
  </xsl:template>
</xsl:stylesheet>

[Download examples/tabular/layout5.xsl]



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:

 <table>
  <table-column column-number="1" column-width="25%"/>
  <table-column column-number="2" column-width="60%"/>
  <table-column column-number="3" column-width="15%"/>
  <table-header font-weight="bold">
   <table-row>
    <table-cell border="1pt solid black" padding="4pt">
     <block>ISBN Number
     </block>
    </table-cell>
    <table-cell border="1pt solid black" padding="4pt">
     <block>Title
     </block>
    </table-cell>
    <table-cell border="1pt solid black" padding="4pt" text-align="right">
     <block>Price
     </block>
    </table-cell>
   </table-row>
  </table-header>
  <table-footer font-weight="bold">
   <table-row>
    <table-cell number-columns-spanned="2" border="1pt solid black" padding="4pt">
     <block>Total Price
     </block>
    </table-cell>
    <table-cell border="1pt solid black" padding="4pt" text-align="right">
     <block>
      <xsl:value-of select="sum(book/price)"/>
     </block>
    </table-cell>
   </table-row>
  </table-footer>
  <table-body>
   <xsl:apply-templates select="book"/>
  </table-body>
 </table>

The complete XSL now looks like this:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns="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">
    <root>
      <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-column column-number="1" column-width="25%" />
            <table-column column-number="2" column-width="60%" />
            <table-column column-number="3" column-width="15%" />
            <table-header font-weight="bold">
              <table-row>
                <table-cell border="1pt solid black" padding="4pt">
                  <block>ISBN Number</block>
                </table-cell>
                <table-cell border="1pt solid black" padding="4pt">
                  <block>Title</block>
                </table-cell>
                <table-cell border="1pt solid black" padding="4pt" text-align="right">
                  <block>Price</block>
                </table-cell>
              </table-row>
            </table-header>
            <table-footer font-weight="bold">
              <table-row>
                <table-cell number-columns-spanned="2" border="1pt solid black" padding="4pt">
                  <block>Total Price</block>
                </table-cell>
                <table-cell border="1pt solid black" padding="4pt" text-align="right">
                  <block>
                    <xsl:value-of select="sum(book/price)" />
                  </block>
                </table-cell>
              </table-row>
            </table-footer>
            <table-body>
              <xsl:apply-templates select="book" />
            </table-body>
          </table>
        </flow>
      </page-sequence>
    </root>
  </xsl:template>
  <xsl:template match="book">
    <table-row>
      <table-cell border="1pt solid black" padding="4pt">
        <block>
          <xsl:value-of select="isbn/text()" />
        </block>
      </table-cell>
      <table-cell border="1pt solid black" padding="4pt">
        <block>
          <xsl:value-of select="title/text()" />
        </block>
      </table-cell>
      <table-cell border="1pt solid black" padding="4pt" text-align="right">
        <block>
          <xsl:value-of select="price/text()" />
        </block>
      </table-cell>
    </table-row>
  </xsl:template>
</xsl:stylesheet>

[Download examples/tabular/layout6.xsl]



With the footer added the table in the PDF file looks like this:

Repeating footers: repeating 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.

Copyright (c) 2002-2016 Visual Programming Limited