Wednesday, June 07, 2006

Printing HTML from Java

I've been working on a project that uses a JEditorPane to display HTML. We also need to be able to print this information. Luckily Swing components know how to render themselves and Sun even has some helpful sample code up that seems to be six years old, but is still useful. The parts I thought were most helpful were the classes Vista, JComponentVista, and JBrowser.

However this code has at least two significant shortcomings.

The first is that while you can scale the rendered image in several ways, the only way to change how much text goes on a line is to actually change the size of the JBrowser window with the mouse. I tried repeatedly to have an offscreen JEditorPane that I could set to an independent width in order to render it for printing without regard to the width of the onscreen window and was constantly frustrated. If anybody knows how to do this I'd be interested. My crappy solution was to set the window to a reasonable width on application launch and let the user shoot themselves in the foot if they mess with it.

The second problem was less annoying to me but was actually noticed by the client, so I had to solve it. Since the JEditorPane is painting itself onto the pages using Graphics2D, it really is just creating a big image and then cropping it into pages. This method ignores page breaks. So you could easily get a line of text at the bottom of the page with its bottom half chopped off. The bottom half would appear at the top of the next page. Generally this is not good for readability.

Now looking back I probably should have gotten the source for JEditorPane and the HTMLEditorKit and looked at how it renders things and added a way to set the page length. This is not what I did.

Instead I figured that the way to do this was to examine the image that was rendered, look for text near the bottom of the page, and then look of a relatively pixel free line near the bottom of the page and set that as the boundary of the lower crop.

Unfortunately Graphics2D does not provide a simple way of examining what you've just drawn. In order to look for blank horizontal lines I did the following:


   public PageFormat getPageFormat(int pageIndex) throws IndexOutOfBoundsException {


if (pageIndex >= mNumPages) {

throw new IndexOutOfBoundsException();

}
BufferedImage image = new BufferedImage(300,5000,BufferedImage.TYPE_INT_RGB);

Graphics2D imageGraphics = (Graphics2D)image.getGraphics();
double originX = (pageIndex % mNumPagesX) * mFormat.getImageableWidth();

//double originY = (pageIndex / mNumPagesX) * mFormat.getImageableHeight();
double originY = 0;

for (int i = 0; i < pageIndex; i++)
{
originY+=heights[i];
}
imageGraphics.translate(-originX/mScaleX, -originY/mScaleY);
//System.out.println("OrigX:"+originX+ " OrigY:"+originY);
//System.out.println("SOrigX:"+originX/mScaleX+ " SOrigY:"+originY/mScaleY);
mComponent.paint(imageGraphics);
Raster raster = image.getData();
DataBuffer db = raster.getDataBuffer();
SampleModel sm = raster.getSampleModel();
int[] pixels = new int[30000];
PageFormat pageFormat = getPageFormat();
//System.out.println("Printing page:"+pageIndex+ " Y: "+ pageFormat.getImageableHeight()/mScaleY);

pixels = sm.getPixels(100,(int) (pageFormat.getImageableHeight()/mScaleY )-50,200,50,pixels,db);


int bestRow = 49;
int bestCount = 0;

for (int row = 49; row >= 0; row--)
{
int count = 0;
for (int col = 0; col < 600; col ++)
{
count += pixels[row*600+col];
}
if (count > bestCount)
{
bestRow = row;
bestCount = count;
}
// System.out.println("Row:"+row +" Count:"+count);

}
/*
* The following print statements are quite
* useful for debugging but slow things down.
*
System.out.println("Best Row:"+bestRow +" Best Count:"+bestCount);

for (int i = 0; i < pixels.length; i+=3)
{

if (i%600 == 0)
{
System.out.print("\n");
if (i/600 == bestRow)
{
System.out.print("***");
}
else
{

if (i/600 < 100) System.out.print("0");
if (i/600 < 10 ) System.out.print("0");
System.out.print(i/600);
}
}
if (pixels[i]==0)
{
System.out.print("X");
}
else
{
System.out.print(" ");
}

}
*/
int diff = 50 - bestRow;

Paper newPaper = (Paper) pageFormat.getPaper().clone();
heights[pageIndex]= (int) (pageFormat.getImageableHeight()-diff*mScaleY);
newPaper.setImageableArea(72,72,pageFormat.getImageableWidth(), pageFormat.getImageableHeight()-diff*mScaleY);
pageFormat.setPaper(newPaper);
//System.out.print("\n");

return pageFormat;

}



So what does that do? First I create a BufferedImage in order to render to it. Then I figure out what page it is that is being rendered. I find the current position in the larger image by adding up the lengths of the previous pages. Note that these lengths will vary now. Then I translate the rendering context to that area and draw the image. Once I have it drawn I use a Raster, DataBuffer, and SampleModel to extract the pixels from the image. I then examine a small sample starting at the bottom and going up 50 lines. I am looking for the horizontal line with the least number of black pixels. If there is a tie I use the lower line. I've also got some commented out code there that outputs the last 50 lines of pixels as text. This was very useful for debugging purposes as it allowed me to see exactly what was happening.

This solution isn't perfect. It doesn't span the entire width of the page, which is something I could make it do pretty easily. It also does not calculate the total number of pages properly since some pages will be shorter now, which in some situations will mean that addtional pages must be printed. I should probably do all the pre-rendering when the number of pages is calculated. However it works well enough for now, and I thought it would be worthwhile to post this in this form since I was unable to find a solution to this problem on the web.

Feedback would be appreciated.

I should also mention that the client has suddenly decided to print white text on a blue background. This subverts the above code in an obvious way by making it actively try to split a line of text.

0 Comments:

Post a Comment

<< Home