Figure 8-5. Generated JPEG files for XML <title> elements
These files are title1.jpg and title8.jpg, respectively. Our extension function has taken the text of the appropriate <title> elements, drawn it on top of the background graphic, then written the new image out as a JPEG graphic.
Let's take a look at the call to our extension function:
<xsl:value-of
select="jpeg:createJPEG(title, 'bg.jpg',
concat('title', position(), '.jpg'),
'Swiss 721 Bold Condensed', 'BOLD', 22, 52, 35)"/>
First of all, look at the call itself. What we've written here is jpeg:createJPEG as the name of the function. The namespace prefix jpeg is defined in the stylesheet. We associated this prefix with the string xalan://JPEGWriter; this string tells Xalan that any function invoked with this prefix should be treated as a method of the named class JPEGWriter. If you use an XSLT processor other than Xalan, the way you define and invoke an extension function will probably vary.
Next, let's look at the parameters to the function. We're passing eight different parameters:
-
The text that should be written in the JPEG image. This text is passed in as a NodeList, one of the datatypes available to us in the Xalan API. In our previous example, we're selecting all <title> elements contained in the current node.
-
The filename of the background image that should be used. This filename is passed in as a String.
-
The filename of the created JPEG image. The image will be created, then written out to this filename. Notice that in our example, we generate the filename by concatenating the string "title", the position of the current node, and the string ".jpg". This procedure ensures that all our title graphics have unique filenames. It also makes it easy to determine which JPEG matches a given <title> element.
-
The name of the font we want to use. This name is a String.
-
The font style we want to use. We've written our function to accept three different values: BOLD, ITALIC, and BOLDITALIC. These values mirror the three values used by the Java Font class.
-
The point size of the font. Notice that this font size is passed to our extension function as a Java Double; XPath and XSLT do not define an Integer type. The first thing our function does is convert the Double values into ints to simplify our arithmetic instructions.
-
The x-offset where the text should begin. We're using a Java Canvas object, whose coordinate system begins in the upper left corner. The value of x-offset determines where we should start drawing the text on the background JPEG. As with the font size, this value is a Double that we convert to an int.
-
The y-offset where the text should begin.
You could certainly modify this function to support other options, such as the color of the text, the depth of the shadow effects on the text, the location of the shadow, etc. You could also create different versions of the function with different method signatures, allowing some calls to the createJPEG function to default certain parameters. The benefit of this approach is that you can access a wide range of behaviors in your extension function by changing your XSLT stylesheet.
Here's the code for the extension function itself:
import com.sun.image.codec.jpeg.ImageFormatException;
import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGImageDecoder;
import com.sun.image.codec.jpeg.JPEGImageEncoder;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.image.BufferedImage;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import org.apache.xpath.objects.XNodeSet;
import org.w3c.dom.NodeList;
public class JPEGWriter
{
public static void createJPEG(NodeList nodes, String backgroundFilename,
String outputFilename, String fontName,
String fontAttributes, Double dFontSize,
Double dXOffset, Double dYOffset)
throws IOException, FileNotFoundException, ImageFormatException
{
try
{
int fontSize = dFontSize.intValue();
int xOffset = dXOffset.intValue();
int yOffset = dYOffset.intValue();
String jpegText = (new XNodeSet(nodes.item(1))).str();
FileInputStream fis = new FileInputStream(backgroundFilename);
JPEGImageDecoder northDecoder = JPEGCodec.createJPEGDecoder(fis);
BufferedImage bi = northDecoder.decodeAsBufferedImage();
int fa = Font.PLAIN;
if (fontAttributes.equalsIgnoreCase("BOLD"))
fa = Font.BOLD;
else if (fontAttributes.equalsIgnoreCase("ITALIC"))
fa = Font.ITALIC;
else if (fontAttributes.equalsIgnoreCase("BOLDITALIC"))
fa = Font.BOLD & Font.ITALIC;
Graphics2D g = bi.createGraphics();
int maxTextWidth = bi.getWidth() - xOffset - 5;
GraphicsEnvironment ge = GraphicsEnvironment.
getLocalGraphicsEnvironment();
Font allFonts[] = ge.getAllFonts();
Font chosenFont = new Font("Arial", fa, fontSize);
int i = 0;
boolean fontNotFound = true;
while (fontNotFound && (i < allFonts.length))
{
if (allFonts[i].getFontName().equalsIgnoreCase(fontName))
{
chosenFont = allFonts[i].deriveFont(fa, fontSize);
if (!chosenFont.getFontName().equalsIgnoreCase(fontName))
{
fa = Font.PLAIN;
chosenFont = allFonts[i].deriveFont(fa, fontSize);
}
g.setFont(chosenFont);
FontMetrics fm = g.getFontMetrics();
int textWidth = fm.stringWidth(jpegText);
while (textWidth > maxTextWidth && fontSize > 1)
{
fontSize -= 2;
chosenFont = allFonts[i].deriveFont(fa, fontSize);
g.setFont(chosenFont);
fm = g.getFontMetrics();
textWidth = fm.stringWidth(jpegText);
}
if (fontSize < 1)
chosenFont = allFonts[i].deriveFont(fa, 12);
g.setFont(chosenFont);
fontNotFound = false;
}
else
i++;
}
g.setColor(Color.black);
g.drawString(jpegText, xOffset, yOffset);
g.setColor(Color.gray);
g.drawString(jpegText, xOffset - 1, yOffset - 1);
FileOutputStream fos = new FileOutputStream(outputFilename);
JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(fos);
encoder.encode(bi);
fos.flush();
fos.close();
}
catch (FileNotFoundException fnfe)
{
System.err.println(fnfe);
}
catch (IOException ioe)
{
System.err.println(ioe);
}
}
}
Notice that we use a while loop to check the font size. If drawing the text string in the current font size won't fit inside the graphic, we'll try to reduce the font size until it does. Given this <chapter> element:
<chapter>
<title>A chapter in which the title is so very long, most people
don't bother reading it</title>
<para>If this chapter had any text, it would appear here.</para>
</chapter>