Migrating from Apache FOP 0.20.5 to 0.94 or 0.95 (beta)
05 April 08 09:09 | Andreas Mengel | mit no comments

 

Introduction

At a customer project we have many PDF Reports we produce with TVDxORA and Apache FOP, alas in version 0.20.5. Knowing, there are newer versions available, I recently migrated the existing reports' stylesheets to version 0.94 (they also work fine with version 0.95 beta which came out last month).

 

Changes in the .fo-file

The calls of the processor remain the same for my case, but I had to rewrite my XSLT-stylesheets. The following is what I had to change, the first item of the list below can also be found at the Apache FOP web site (upgrading). In order to find problematic regions in your stylesheets, it is handy to produce the .fo file first and to render it with Apache FOP afterwards, otherwise (using only one call for transformation and rendering) you won't get line numbers for the spots that cause problems.

  • <fo:table-cell/> or <fo:table-cell></fo:table-cell> cannot be empty any more, you must have a <fo:block/> inside: <fo:table-cell><fo:block/></fo:table-cell>
  • the order of <fo:region-before>, <fo:region-body>, <fo:region-after> matters now: <fo:region-body> must be the first element
  • when specifying attribute table-layout="fixed" in <fo:table> you should also include width="100%" to avoid warning messages of the processor
  • <fo:instream-foreign-object xmlns:svg="http://www.w3.org/2000/svg"> must be included within an <fo:block> element now
  • cell, column and table borders have to be reviewed, in my case I had to change the level where I defined them

 

Drawbacks

The primary intention of upgrading the stylesheets for my project was to keep up-to-date with the actual release of FOP. Yet, at the moment I am not sure whether I shall recommend an upgrade of the FOP to 0.94 or 0.94 to my customer for the following reasons:

  • there are no issues with the old version in production
  • the time for rendering the PDFs has increased by 60%
  • the size of the files has increased by 300%

 

Any hints on that issue?

Abgelegt unter: , , , ,
Distributing an Odd Number of Elements Across a Structure preferring Even Numbers with XSLT
19 März 08 10:32 | Andreas Mengel | mit no comments

In an output management project we recently faced the problem that we wanted to distribute a given number of elements - odd or even - to a table-like structure of two named columns. As a table it would look like this:

a    b
c    d
e    d

 

In XML we needed this kind of format:

<table>
  <row>
    <left_column>a</left_column>
    <right_column>b</right_column>
  </row>
  <row>
    <left_column>c</left_column>
    <right_column>d</right_column>
  </row>
  <row>
    <left_column>e</left_column>
    <right_column>f</right_column>
  </row>
</table>

 

Suppose this would be our XML input file:

<?xml version="1.0" encoding="UTF-8"?>
<sequence>
  <item>a</item>
  <item>b</item>
  <item>c</item>
  <item>d</item>
  <item>e</item>
</sequence>

 

With this task you face the challenge that you need to determine the position of the actual item and write (and open or close) the appropriate element.

Thus, in a first attempt your XSLT would look like this

<xsl:for-each select="item">
  <xsl:choose>
    <xsl:when test="position() mod  2">
      <row>
        <left_column>
    </xsl:when>
    <xsl:otherwise>
        <right_column>
    </xsl:otherwise>
  </xsl:choose>
  <xsl:value-of select="text()"/>
    ...

and thus be invalid XSLT because the <row>, the <left_column> and the <right_column> elements are not closed inside their parent elements. What you have to do in order to overcome this problem is conceal the code by using CDATA tags.

<xsl:for-each select="item">
  <xsl:choose>
    <xsl:when test="position() mod  2">
      <![CDATA[
      <row>
        <left_column>

      ]]>
    </xsl:when>
    <xsl:otherwise>
      <![CDATA[
      <right_column>
      ]]>
    </xsl:otherwise>
  </xsl:choose>
  <xsl:value-of select="text()"/>
    ...

 

At least your XSLT is valid now, but the XSLT processor might escape the output and print "<row>" as "&lt;row&gt;". If you are unhappy with this, you need to disable output-escaping of the CDATA content by wrapping it into an <xsl:text> element with disable-output-escaping set to "no".

So this is the resulting stylesheet:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0">
  <xsl:template match="/sequence">
    <table>
      <xsl:for-each select="item">
        <xsl:choose>
          <xsl:when test="position() mod  2">
            <xsl:text disable-output-escaping="yes">
              <![CDATA[
                <row>
                  <left_column>
              ]]>
            </xsl:text>
          </xsl:when>
          <xsl:otherwise>
            <xsl:text disable-output-escaping="yes">
              <![CDATA[
                <right_column>
              ]]>
            </xsl:text>
          </xsl:otherwise>
        </xsl:choose>
        <xsl:value-of select="text()"/>
        <xsl:choose>
          <xsl:when test="position() mod 2">
            <xsl:text disable-output-escaping="yes">
              <![CDATA[
                </left_column>
              ]]>
            </xsl:text>
            <xsl:if test="position() = last()">
              <xsl:text disable-output-escaping="yes">
                <![CDATA[
                  </row>
                ]]>
              </xsl:text>
            </xsl:if>
          </xsl:when>
          <xsl:otherwise>
            <xsl:text disable-output-escaping="yes">
              <![CDATA[
                  </right_column>
                </row>
              ]]>
            </xsl:text>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:for-each>
    </table>
  </xsl:template>
</xsl:stylesheet>

The result is:

<table>
  <row>
    <left_column>a</left_column>
    <right_column>b</right_column>
  </row>
  <row>
    <left_column>c</left_column>
    <right_column>d</right_column>
  </row>
  <row>
    <left_column>e</left_column>
  </row>
</table>
Abgelegt unter: , ,
Generating SVG Pie Charts in PDF Documents with XSLT and Apache FOP
14 März 08 10:14 | Andreas Mengel | mit no comments

This article outlines how to draw a pretty print pie chart in a PDF document with XSLT and Apache FOP.

 

Background

In some of our projects with TVDxORA (a lightweight web reporting framework) we produce PDF, some of them populated with pie charts. Our input is XML - representing the result of a query - which is generated from the Oracle database. We transform this output with XSLT (inside the DB or outside with xalan-j) and produce an XSL-FO document which again is rendered to PDF by Apache FOP.

Having SVGs embedded in PDF gives you high quality graphical output: Opening the PDF you can

  • zoom in without loosing quality
  • copy parts of the document or graphic to another application at the resolution level you desire, either by zooming in as much as you wish or by setting the resolution to a fixed dpi value (Preferences -> Page Display -> Custom Resolution in Adobe Reader 8).

 

SVG and PDF

In general - using Apache FOP - it is relatively easy to produce drawings from XML since you can embed SVG (Scalable Vector Graphics) within an fo:instream-foreign-object element (cf. example):

  <fo:instream-foreign-object>
    <svg:svg width="8" height="8" viewBox="0 0 8 8">
      <svg:rect stroke="black" stroke-width="0.5" x="0" y="0" width="8"
height="8" fill="orangered"/>
    </svg:svg>
  </fo:instream-foreign-object>

 

Challenges

The tricky thing is the pie chart itself. Whereas drawing bar-charts is rather simple and straigh forward, there is no "pie-command" or "slice-instruction" in SVG that lets you just enter the coordinate and no further preparation is required. You rather have to draw each individual slice yourself.

On the Making a pie chart page the general aspects of how to draw arcs and basic trigonometric aspects are introduced.

This is where I started. Yet, I had to do it in XSLT and I wanted to start at 12 o'clock. And I used XSLT 1.0. this meant I needed a recursive template. The general idea is that I start with the last (clockwise) and smallest segment, draw the full circle in the color of that segment, then go to the last but one, draw a filled arc with an angle of 360° minus the last segment, then the last but two with 360° minus the sum of the previous two, and so on.

pie

As one can see, I also added some figures and in case the angle of the slice becomes too narrow, I draw a line and add the statistical detail outside the pie.

The FO-code of one of these arcs looks like this. For reasons of convenience I rounded figures in the box below:

<svg:svg width="300" height="300" viewBox="0 0 500 500">

  <svg:path stroke="black" stroke-width="1" stroke-linejoin="round" fill="blueviolet"

            d="M 200,200 l 0,-150 a150,150 0 1,1 -0.019,0.000 z"/>

  <svg:path stroke="black" stroke-width="1" stroke-linejoin="round" fill="none"

            d="M 195.964,80.067 L 194.451,35.093 H 184.451"/>

  <svg:text text-anchor="end" font-size="8" x="183.451" y="36.093">1.07</svg:text>

  <svg:path stroke="black" stroke-width="1" stroke-linejoin="round" fill="lightgreen"

            d="M 200,200 l 0,-150 a150,150 0 1,1 -10.080,0.339 z"/>

  <svg:text text-anchor="middle" font-size="8" x="178.351" y="81.968">3.63</svg:text>

  <svg:path stroke="black" stroke-width="1" stroke-linejoin="round" fill="yellow"

            d="M 200,200 l 0,-150 a150,150 0 1,1 -43.680,6.500 z"/>

  <svg:text text-anchor="middle" font-size="8" x="101.236" y="131.840">21.37</svg:text>

  <svg:path stroke="black" stroke-width="1" stroke-linejoin="round" fill="cornflowerblue"

            d="M 200,200 l 0,-150 a150,150 0 1,1 -149.661,160.075 z"/>

  <svg:text text-anchor="middle" font-size="8" x="145.671" y="306.997">32.91</svg:text>

  <svg:path stroke="black" stroke-width="1" stroke-linejoin="round" fill="orangered"

            d="M 200,200 l 0,-150 a150,150 0 0,1 80.176,276.774 z"/>

  <svg:text text-anchor="middle" font-size="8" x="315.259" y="166.603">41.03</svg:text>

</svg:svg>

 

This would be the XML input for two companies i.e. pie charts:

<?xml version="1.0" encoding="UTF-8"?>
<ROWSET>
  <ROW>
    <P2>company a</P2><P14>192</P14>
  </ROW>
  <ROW>
    <P2>company a</P2><P14>154</P14>
  </ROW>
  <ROW>
    <P2>company a</P2><P14>100</P14>
  </ROW>
  <ROW>
    <P2>company a</P2><P14>17</P14>
  </ROW>
  <ROW>
    <P2>company a</P2><P14>5</P14>
  </ROW>
  <ROW>
    <P2>company b</P2><P14>200</P14>
  </ROW>
  <ROW>
    <P2>company b</P2><P14>175</P14>
  </ROW>
  <ROW>
    <P2>company b</P2><P14>89</P14>
  </ROW>
  <ROW>
    <P2>company b</P2><P14>88</P14>
  </ROW>
  <ROW>
    <P2>company b</P2><P14>10</P14>
  </ROW>
</ROWSET>

 

And this is the XSLT code fragment:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="
http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:svg="http://www.w3.org/2000/svg" xmlns:math="xalan://java.lang.Math" extension-element-prefixes="math">
  <xsl:output method="xml"/>
  <xsl:template match="/">
    <xsl:call-template name="pie_chart"/>
  </xsl:template>
  <xsl:template name="pie_chart">
    <!-- draw the pie for every company-->
    <xsl:for-each select="/ROWSET/ROW[preceding-sibling::ROW[1]/P2/text() != P2/text() or position() = 1]">
      <xsl:variable name="company_name" select="P2/text()"/>
      <xsl:variable name="no_products" select="count(//ROW[P2/text()=$company_name])"/>
      <fo:block space-before="2cm" text-align="center">
        <fo:instream-foreign-object xmlns:svg="
http://www.w3.org/2000/svg">
        <!--set the display-->
          <svg:svg>
            <xsl:attribute name="width"><xsl:value-of select="300"/></xsl:attribute>
            <xsl:attribute name="height"><xsl:value-of select="300"/></xsl:attribute>
            <xsl:attribute name="viewBox"><xsl:value-of select="'0 0 500 500'"/></xsl:attribute>
            <!--call the template starting at the last slice-->
            <xsl:call-template name="pie_chart_slice">
              <xsl:with-param name="company_name" select="$company_name"/>
              <xsl:with-param name="sum" select="sum(//ROW[P2/text()=$company_name]/P14/text())"/>
              <xsl:with-param name="position" select="$no_products"/>
              <xsl:with-param name="no_products" select="$no_products"/>
              <xsl:with-param name="middle_x" select="200"/>
              <xsl:with-param name="middle_y" select="200"/>
              <xsl:with-param name="move_x" select="0"/>
              <xsl:with-param name="radius" select="150"/>
            </xsl:call-template>
          </svg:svg>
        </fo:instream-foreign-object>
      </fo:block>
    </xsl:for-each>
  </xsl:template>
  <xsl:template name="pie_chart_slice">
    <xsl:param name="company_name"/>
    <xsl:param name="sum"/>
    <xsl:param name="position"/>
    <xsl:param name="no_products"/>
    <xsl:param name="middle_x"/>
    <xsl:param name="middle_y"/>
    <xsl:param name="move_x"/>
    <xsl:param name="radius"/>
    <!--prepare the middle part of the arc command-->
    <xsl:variable name="middle" select="concat('M',' ',$middle_x,',',$middle_y)"/>
    <xsl:variable name="part" select="sum(//ROW[P2/text()=$company_name][position() &lt;= $position]/P14/text())"/>
    <xsl:variable name="angle" select="($part div $sum) * 360"/>
    <xsl:variable name="x" select="math:sin(3.1415292 * $angle div 180.0) * $radius"/>
    <xsl:variable name="y" select="math:cos(3.1415292 * $angle div 180.0) * $radius"/>
    <xsl:variable name="move_y" select="-$radius"/>
    <xsl:variable name="first_line" select="concat('l',' ',$move_x,',',$move_y)"/>
    <xsl:variable name="arc_move1" select="'0'"/>
    <xsl:variable name="arc_move2">
      <xsl:choose>
      <!--check the direction of the arc: inward or outward-->
        <xsl:when test="$angle &lt;=180">0</xsl:when>
        <xsl:otherwise>1</xsl:otherwise>
      </xsl:choose>
    </xsl:variable>
    <xsl:variable name="arc_move3" select="'1'"/>
    <xsl:variable name="arc_move" select="concat($arc_move1,' ',$arc_move2,',',$arc_move3)"/>
    <xsl:variable name="d" select="concat($middle,' ',$first_line,' ','a150,150',' ',$arc_move,' ',$x,',',$radius - $y,' ','z')"/>
    <!--put it all together-->
    <svg:path stroke="black" stroke-width="1" stroke-linejoin="round">
      <xsl:attribute name="fill"><xsl:call-template name="giveColor"><xsl:with-param name="i"><xsl:value-of select="$position"/></xsl:with-param></xsl:call-template></xsl:attribute>
      <xsl:attribute name="d"><xsl:value-of select="$d"/></xsl:attribute>
    </svg:path>
    <!--now the percentage-->
    <xsl:variable name="percentage" select="format-number(( //ROW[P2/text()=$company_name][position() = $position]/P14/text() div sum(//ROW[P2/text()=$company_name]/P14/text()) ) * 100,'###,###0.00')"/>
    <xsl:variable name="part_half" select="format-number(( //ROW[P2/text()=$company_name][position() = $position]/P14/text() div sum(//ROW[P2/text()=$company_name]/P14/text()) ) div 2 * 360,'###,###0.00')"/>
    <xsl:variable name="text_x" select="math:sin(3.1415292 * (($angle - $part_half ) div 180.0)) * ($radius * 0.8)"/>
    <xsl:variable name="text_y" select="math:cos(3.1415292 * (($angle - $part_half ) div 180.0)) * ($radius * 0.8)"/>
    <xsl:variable name="text_line_x" select="math:sin(3.1415292 * (($angle - $part_half ) div 180.0)) * ($radius * 1.1)"/>
    <xsl:variable name="text_line_y" select="math:cos(3.1415292 * (($angle - $part_half ) div 180.0)) * ($radius * 1.1)"/>
    <!--we either put it on the cream or have a line pointing into the slice-->
    <xsl:choose>
      <xsl:when test="$percentage >= 3">
      <!--on the cream-->
        <svg:text text-anchor="middle" font-size="8">
          <xsl:attribute name="x"><xsl:value-of select="$middle_x + $text_x"/></xsl:attribute>
          <xsl:attribute name="y"><xsl:value-of select="$middle_y - $text_y"/></xsl:attribute>
          <xsl:value-of select="$percentage"/>
        </svg:text>
      </xsl:when>
      <xsl:otherwise>
      <!--extra line pointing into the slice-->
        <svg:path stroke="black" stroke-width="1" stroke-linejoin="round">
          <xsl:attribute name="fill">none</xsl:attribute>
          <xsl:attribute name="d"><xsl:value-of select="concat('M',' ', $middle_x + $text_x,',',$middle_y - $text_y,' ','L',' ',$middle_x + $text_line_x,',',$middle_y - $text_line_y,' ','H',' ',$middle_x + $text_line_x - 10)"/></xsl:attribute>
        </svg:path>
        <svg:text text-anchor="end" font-size="8">
          <xsl:attribute name="x"><xsl:value-of select="$middle_x + $text_line_x - 11 "/></xsl:attribute>
          <xsl:attribute name="y"><xsl:value-of select="$middle_y - $text_line_y + 1"/></xsl:attribute>
          <xsl:value-of select="$percentage"/>
        </svg:text>
      </xsl:otherwise>
    </xsl:choose>
    <!--loop until we reach the first part-->
    <xsl:if test="$position > 1">
      <xsl:call-template name="pie_chart_slice">
        <xsl:with-param name="company_name" select="$company_name"/>
        <xsl:with-param name="sum" select="$sum"/>
        <xsl:with-param name="position" select="$position - 1"/>
        <xsl:with-param name="no_products" select="$no_products"/>
        <xsl:with-param name="middle_x" select="$middle_x"/>
        <xsl:with-param name="middle_y" select="$middle_y"/>
        <xsl:with-param name="move_x" select="$move_x"/>
        <xsl:with-param name="radius" select="$radius"/>
      </xsl:call-template>
    </xsl:if>
  </xsl:template>
  <xsl:template name="giveColor">
    <xsl:param name="i"/>
    <xsl:choose>
      <xsl:when test="$i=1">orangered</xsl:when>
      <xsl:when test="$i=2">cornflowerblue</xsl:when>
      <xsl:when test="$i=3">yellow</xsl:when>
      <xsl:when test="$i=4">lightgreen</xsl:when>
      <xsl:when test="$i=5">blueviolet</xsl:when>
      <xsl:when test="$i=6">chartreuse</xsl:when>
      <xsl:when test="$i=7">deepskyblue</xsl:when>
      <xsl:when test="$i=8">lightbrown</xsl:when>
      <xsl:when test="$i=9">aquamarine</xsl:when>
      <xsl:when test="$i=10">pink</xsl:when>
      <xsl:when test="$i=11">cornflowerblue</xsl:when>
      <xsl:when test="$i=12">lightyellow</xsl:when>
      <xsl:when test="$i=13">burlywood</xsl:when>
      <xsl:when test="$i=14">cornflowerblue</xsl:when>
      <xsl:when test="$i=15">cornsilk</xsl:when>
      <xsl:otherwise>black</xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>


For the different colors I just used a lookup template that provides up to 15 distinct colors, which should be enough in most cases, however, one could add more colors or just use a modulo operation in order to repeat that sequence in case the number of slices is greater that 15.
Abgelegt unter: , , , , ,

Dieser Blog

Ordnungsbegriffe

Syndikation