This chapter’s introduction covered the mechanism for binding the stylesheet to the Java implementations, so this section concentrates on examples.
Chapter 2 showed how to convert numbers from base 10 to other bases (such as base 16 (hex)). You can implement a hex converter in Java easily:
package com.ora.xsltckbk.util; public class HexConverter { public static String toHex(String intString) { try { Integer temp = new Integer(intString) ; return new String("0x").concat(Integer.toHexString(temp.intValue( ))) ; } catch (Exception e) { return new String("0x0") ; } } }
You can probably tell by the way the return value is formatted with a
leading 0x
that this particular function will be
used in a code-generation application. The following example shows
how it might be used:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xalan="http://xml.apache.org/xslt" xmlns:hex="xalan://com.ora.xsltckbk.util.HexConverter" exclude-result-prefixes="hex xalan"> <xsl:template match="group"> enum <xsl:value-of select="@name"/> { <xsl:apply-templates mode="enum"/> } ; </xsl:template> <xsl:template match="constant" mode="enum"> <xsl:variable name="rep"> <xsl:call-template name="getRep"/> </xsl:variable> <xsl:value-of select="@name"/> = <xsl:value-of select="$rep"/> <xsl:if test="following-sibling::constant"> <xsl:text>,</xsl:text> </xsl:if> </xsl:template> <xsl:template match="constant"> <xsl:variable name="rep"> <xsl:call-template name="getRep"/> </xsl:variable> const int <xsl:value-of select="@name"/> = <xsl:value-of select="$rep"/> ; </xsl:template> <xsl:template name="getRep"> <xsl:choose> <xsl:when test="@rep = 'hex'"> <xsl:value-of select="hex:toHex(@value)"/> </xsl:when> <xsl:otherwise> <xsl:value-of select="@value"/> </xsl:otherwise> </xsl:choose> </xsl:template> </xsl:stylesheet>
The next example shows how you can construct Java objects and call their methods. Dealing with text layout is difficult when transforming XML to Scalable Vector Graphics. SVG gives you no way to determine how long a string will be when it is rendered. Fortunately, Java provides the functionality you need. The question is whether Java’s opinion of how long a string will be when rendered in a particular font matches the SVG engine opinion. Nevertheless, this idea is seductive enough to try:
package com.ora.xsltckbk.util ; import java.awt.* ; import java.awt.geom.* ; import java.awt.font.* ; import java.awt.image.*; public class SVGFontMetrics { public SVGFontMetrics(String fontName, int size) { m_font = new Font(fontName, Font.PLAIN, size) ; BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); m_graphics2D = bi.createGraphics( ) ; } public SVGFontMetrics(String fontName, int size, boolean bold, boolean italic) { m_font = new Font(fontName, style(bold,italic) , size) ; BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); m_graphics2D = bi.createGraphics( ) ; } public double stringWidth(String str) { FontRenderContext frc = m_graphics2D.getFontRenderContext( ); TextLayout layout = new TextLayout(str, m_font, frc); Rectangle2D rect = layout.getBounds( ) ; return rect.getWidth( ) ; } public double stringHeight(String str) { FontRenderContext frc = m_graphics2D.getFontRenderContext( ); TextLayout layout = new TextLayout(str, m_font, frc); Rectangle2D rect = layout.getBounds( ) ; return rect.getHeight( ) ; } static private int style(boolean bold, boolean italic) { int style = Font.PLAIN ; if (bold) { style |= Font.BOLD;} if (italic) { style |= Font.ITALIC;} return style ; } private Font m_font = null ; private Graphics2D m_graphics2D = null; }
Here Java 2’s (JDK 1.3.1)
Graphics2D
and TextLayout
classes provide the information you need. You implemented two public
constructors to support simple fonts and fonts that are either bold
or italic. Two public methods, stringWidth( )
and stringHeight( )
, get
dimensional information about a how a particular string would be
rendered in the font specified by the constructor. This technique is
generally accurate on most common fonts, but without precise
guarantees, you will have to experiment.
The following stylesheet tests the results:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xalan="http://xml.apache.org/xslt" xmlns:font="xalan://com.ora.xsltckbk.util.SVGFontMetrics" exclude-result-prefixes="font xalan"> <xsl:output method="xml"/> <xsl:template match="/"> <svg width="100%" height="100%"> <xsl:apply-templates/> </svg> </xsl:template> <xsl:template match="text"> <xsl:variable name="fontMetrics" select="font:new(@font, @size, boolean(@weight), boolean(@stytle))"/> <xsl:variable name="text" select="."/> <xsl:variable name="width" select="font:stringWidth($fontMetrics, $text)"/> <xsl:variable name="height" select="font:stringHeight($fontMetrics, $text)"/> <xsl:variable name="style"> <xsl:if test="@style"> <xsl:value-of select="concat('font-style:',@style)"/> </xsl:if> </xsl:variable> <xsl:variable name="weight"> <xsl:if test="@weight"> <xsl:value-of select="concat('font-weight:',@weight)"/> </xsl:if> </xsl:variable> <g style="font-family:{@font};font-size:{@size};{$style};{$weight}"> <!-- Use the SVGFontMetrics info render a rectangle that is --> <!-- slightly bigger than the expected size of the text --> <!-- Adjust the y position based on the previous text size. --> <rect x="10" y="{sum(preceding-sibling::text/@size) * 2}pt" width="{$width + 2}" height="{$height + 2}" style="fill:none;stroke: black;stroke-width:0.5;"/> <!-- Render the text so it is cenetered in the rectangle --> <text x="11" y="{sum(preceding-sibling::text/@size) * 2 + @size div 2 + 2}pt"> <xsl:value-of select="."/> </text> </g> </xsl:template> </xsl:stylesheet>
Your test run produced pretty good results on some commonly available fonts, as shown in Figure 12-1:
<TextWidthTest> <text font="Serif" size="9">M's are BIG; l's are small;</text> <text font="Serif" size="10">SVG makes handling text no fun at all</text> <text font="Helvetica" size="12">But if I cheat with a little Java</text> <text font="Arial" size="14" weight="bold">PROMISE ME YOU WON'T TELL MY MAMMA! </text> <text font="Century" size="16" style="italic">But if you do, I won't lose cheer. </text> <text font="Courier New" size="18" weight="bold" style="italic">Its really my tech editor that I fear!</text> </TextWidthTest>
The examples shown in the
“Solution” section work unchanged
with either Xalan or Saxon (despite the
xml.apache.org/xslt
namespace). It works because
you used the processors’ shortcut conventions for
encoding the Java class in the namespace.
Notice that constructors are accessed using a function with the name
new( )
and that the XSLT processors can figure out
which overloaded constructor to call based on the arguments. Member
functions of a Java class are called by passing an extra initial
argument corresponding to this
. The
HexConverter
example shows that static members are
called without the extra this
parameter.
The SVGFontMetrics
example does not work with
older versions of the JDK, but similar results can be obtained if you
use the
java.awt.FontMetrics
class in conjunction with the original
java.awt.Graphics
class:
package com.ora.xsltckbk.util ; import java.awt.* ; import java.awt.geom.* ; import java.lang.System ; public class FontMetrics { public FontMetrics(String fontName, int size) { //Any concrete component will do Label component = new Label( ) ; m_metrics = component.getFontMetrics( new Font(fontName, Font.PLAIN, size)) ; m_graphics = component.getGraphics( ) ; } public FontMetrics(String fontName, int size, boolean bold, boolean italic) { //Any concrete component will do Label component = new Label( ) ; m_metrics = component.getFontMetrics( new Font(fontName, style(bold,italic) , size)) ; m_graphics = component.getGraphics( ) ; } //Simple, but less accurate on some fonts public int stringWidth(String str) { return m_metrics.stringWidth(str) ; } //Better accuracy on most fonts public double stringWidthImproved(String str) { Rectangle2D rect = m_metrics.getStringBounds(str, m_graphics) ; return rect.getWidth( ) ; } static private int style(boolean bold, boolean italic) { int style = Font.PLAIN ; if (bold) { style |= Font.BOLD;} if (italic) { style |= Font.ITALIC;} return style ; } private java.awt.FontMetrics m_metrics = null; private java.awt.Graphics m_graphics = null ; }
Although these particular examples may not fulfill your immediate needs, they demonstrate the mechanisms by which you can harness your own Java-based extension functions. Other possibilities, in increasing level of difficulty, include:
Using Java’s Hashtable
instead of
xsl:key
. This allows better control over which
elements are indexed
and allows the index
to be changed during the execution. It overcomes the limitation,
whereas xsl:key
definitions cannot reference
variables. You can also use it to build a master index that spans
multiple documents.
Implementing a node-sorting function that can compensate for
xsl:sort
’s limitations, for
example, constructing a sort based on foreign language rules. Doug
Tidwell demonstrates this example with Saxon in
XSLT (O’Reilly, 2001).
Reads and writes multiple file formats in a single stylesheet. For example, it allows the stylesheet to read text files other than XML, such as CSV or proprietary binary files. XSLT 2.0 provides capabilities in this area, but you may not want to wait for it.
Processes compressed XML straight from a zip file
using
java.util.zip.ZipFile
. Studying the source code of
your XSLT processor’s document
function would be helpful.
Chapter 9 punted on the problem of laying out text
within your generated SVG tree nodes. You could use
SVGFontMetrics
as an ingredient in the solution.
Although not specifically related to XSLT extensions, developers interested in Java and SVG should check out Batik (http://xml.apache.org/batik/index.html).