Friday, April 19, 2013

Printing multiple Printables in Java

Java is a great language that let's you concentrate on features to implement instead of figuring out how to manage the memory addresses for example. But sometimes, you can be challenged by the runtime and have to get dirty to create some advance functions.

Printing in Java is quite easy. But it can be a bit to simplified for your needs and you'll end up coding at a low level to get the desired result. Basically, when you want to print, you have to calculate the space required for each word, handle any carriage return and draw the result on a a Graphics. For images, you'll have to resize the image to fit the paper size, draw lines for underlined text, etc... You know the drill.

Of course, there are tons of libraries that will handle complex document printing like JasperReport. With these tools, you design your templates, invoke a database and print the result on your printer or a PDF file.

For simpler documents, you can populate a JTable and print it directly quite easily. The same goes for a JTextArea or a JTextPane. But you can only print one component in a PrinterJob at a time. There is no way to print a JTable and a JTextArea on the same page or in the same PrinterJob. Even combining both in a Book (Pageable class) will result as each component being printed on a new page instead of being printed one after the other.

Here's an example:




This is a case where you'll have to rely on an external library or do some low level coding... At least, until now...

The issue is that PrinterJob.print() will call the Printable object with a page index. The index will be 0 on the JTable, and 1 on the JTextArea in a Pageable object (Book). Only the JTable will be printed. If you manage manually the page index, calling 0 on both, you will have no other choice then printing on two different pages as there is no way to invoke the second printing on the same page, following the first one.

Basically, PageFormat is your friend as you can use it to specify here to print the component on your page. All you need to do is manage your page index and modify the PageFormat (Imageable Area) to indicate where you can start printing the next component. The problem is that you don't know where the printing operation ended on the Y axis. If you table heigh is know, there is a work around that can be used, but if your JTable height is greater than one page, you're stuck. You'll have to implement complex calculation to find how much was printed based on the paper size, paper orientation, cell size and so on. It can become quickly a nightmare if you plan printing several Printable components in a single PrinterJob.

I've search the Internet and that issue has been confusing Java coders for the last decade. Either you implement a low level printing algorithm or you use an external library and implements a totally different design just to be able to print tables and texts. JTable, JTextArea and JTextPane can handle printing by themselves and provide excellent results without much effort. But not together...

I finally found a clean solution to make them work together. The main issue is to find how much space is left when a component has finished printing. Using a BufferedImage to do a pre-printing is how I was able to do it. You'll have to pre-print anyway to find out the total page count required if you want to build a table of content, so it's not a big deal.

Here's the code, a simple Printable class, wrapping the Printable object of the component. This wrapper will handle the amount of space left after the printing. All you need to do is check how much space is left and create a new page when needed and restart the page index at 0 for each new page.

/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package printer;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.awt.print.PageFormat;
import java.awt.print.Paper;
import java.awt.print.Printable;
import java.awt.print.PrinterException;

/**
*
* @author pballeux
*/
public class MyPrintable implements Printable {

Printable delegate;
double spaceLeft = 0;
double minimumRequired = 72;
static int debugindex = 0;

public MyPrintable(Printable p) {
this.delegate = p;
}

public MyPrintable(Printable p, double minimumHeight) {
this.delegate = p;
this.minimumRequired = minimumHeight;
}

public void setMinimumRequired(double height) {
minimumRequired = height;
}

public double getMinimumRequired() {
return minimumRequired;
}


public double getSpaceLeft() {
return spaceLeft;
}

private int detectLastLine(BufferedImage img) {
int lastIndex = 0;
int[] data = ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
for (int i = data.length - 1; i > 0; i--) {
if (data[i] != 0) {
lastIndex = i;
break;
}
}
return (lastIndex / img.getWidth());
}

@Override
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
BufferedImage img = new BufferedImage((int) pageFormat.getWidth(), (int) pageFormat.getHeight(), BufferedImage.TRANSLUCENT);
Graphics2D g = img.createGraphics();
int retValue = delegate.print(g, pageFormat, pageIndex);
if (retValue == PAGE_EXISTS) {
//Find out bound of printing...
//System.out.println("Last Line drawn is : " + detectLastLine(img));
// try {
// ImageIO.write(img, "jpg", new File("img" + debugindex++ + ".jpg"));
// } catch (IOException ex) {
// }
spaceLeft = (pageFormat.getImageableY() + pageFormat.getImageableHeight()) - detectLastLine(img);
retValue = delegate.print(graphics, pageFormat, pageIndex);
//Updating paper after the hardprint
Paper paper = pageFormat.getPaper();
paper.setImageableArea(paper.getImageableX(), paper.getImageableY() + paper.getImageableHeight() - spaceLeft, paper.getImageableWidth(), spaceLeft);
pageFormat.setPaper(paper);
}
return retValue;
}
}


For each Printable, JTable.getPrintable(...), wrap it with MyPrintable object and then print the wrapper.


MyPrintable prt = new MyPrintable(table.getPrintable(...))
...
while (prt.print(graphics,pf,pageIndex) == PAGE_EXISTS){
if (prt.getSpaceLeft() < 100){
// create a new page or a new graphics
// reset page index to 0
}
pageIndex++;
}
// looping to the next Printable...



Have fun!