When you want to convert a java.awt.print.Printable to a pdf file, your first course of action might be to Google for "java printable pdf". If you do that though, all you get is links about how to send a pdf file to the (hardware) printer in java or how to "read" a pdf file in Java. No one seemed to have written an article about how to convert a java.awt.print.Printable to a pdf file. This is that article.
The java code in this article requires the single itext jar to work. I used itext-5.0.2.jar, but I'm sure newer versions will also work.
Update: Some people informed me that as of version 5, itext does not have a business-friendly license anymore. Therefore, I also tested this solution with itext-2.1.7.jar and I am happy to report that this works just fine, the 2.1.7 jar is drop-in compatible. End of update.
This example works for A4 documents only, but once you understand the code, it is fairly trivial to adapt it to any size you want.
So, without further ado, here's the code! I'm going to explain 2 fundamental elements that make it work afterwards.
PdfPrinter.java:
FontCreator.java:
import com.itextpdf.text.PageSize; import com.itextpdf.text.pdf.BaseFont; import com.itextpdf.text.pdf.FontMapper; import com.itextpdf.text.pdf.PdfContentByte; import com.itextpdf.text.pdf.PdfWriter; /** * With this class, you can print a {@link Printable} to a pdf. You get the pdf as a byte-array, that can be stored in a file or sent as an email attachment. * * @author G.J. Schouten * */ public class PdfPrinter { /** * Prints the given {@link Printable} to a pdf file and returns the result as a byte-array. The size of the document is A4. * * @param printable The {@link Printable} that has to be printed. * @param clone A clone of the first {@link Printable}. Needed because internally, it must be printed twice and the Printable may keep state and may not be re-usable. * @param orientation {@link PageFormat}.PORTRAIT or {@link PageFormat}.LANDSCAPE. * @return A byte-array with the pdf file. */ Rectangle pageSize; pageSize = PageSize.A4; } else { pageSize = PageSize.A4.rotate(); } //The bytes to be returned byte[] bytes = null; //We will count the number of pages that have to be printed int numberOfPages = 0; //We will print the document twice, once to count the number of pages and once for real for(int i=0; i<2; i++) { try { Printable usedPrintable = i==0 ? printable : clone; //First time, use the printable, second time, use the clone PdfWriter writer = PdfWriter.getInstance(document, bos); document.open(); PdfContentByte contentByte = writer.getDirectContent(); //These lines do not influence the pdf document, but are there to tell the Printable how to print double a4WidthInch = 8.26771654; //Equals 210mm double a4HeightInch = 11.6929134; //Equals 297mm paper.setSize(a4WidthInch*72, a4HeightInch*72); //72 DPI paper.setImageableArea(72, 72, a4WidthInch*72 - 144, a4HeightInch*72 - 144); //1 inch margins pageFormat.setPaper(paper); pageFormat.setOrientation(orientation); float width = ((float) pageFormat.getWidth()); float height = ((float) pageFormat.getHeight()); //First time, don't use numberOfPages, since we don't know it yet for(int j=0; j<numberOfPages || i==0; j++) { int pageReturn = usedPrintable.print(g2d, pageFormat, j); g2d.dispose(); //The page that we just printed, actually existed; we only know this afterwards document.newPage(); //We have to create a newPage for the next page, even if we don't yet know if it exists, hence the second run where we do know if(i == 0) { //First run, count the pages numberOfPages++; } } else { break; } } document.close(); writer.close(); bytes = bos.toByteArray(); //We expect no Exceptions, so any Exception that occurs is a technical one and should be a RuntimeException } } return bytes; } /** * This class maps {@link java.awt.Font}s to {@link com.itextpdf.text.pdf.BaseFont}s. It gets the fonts from files, so that the pdf looks identical on all platforms. * * @author G.J. Schouten * */ private static class PdfFontMapper implements FontMapper { try { if(font.getFamily().equalsIgnoreCase("Verdana")) { if(font.isBold()) { if(font.isItalic()) { return this.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANAZ.TTF"); } return this.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANAB.TTF"); } else if(font.isItalic()) { return this.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANAI.TTF"); } else { return this.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANA.TTF"); } } else { //Times new Roman is default if(font.isBold()) { if(font.isItalic()) { return this.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMESBI.TTF"); } return this.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMESBD.TTF"); } else if(font.isItalic()) { return this.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMESI.TTF"); } else { return this.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMES.TTF"); } } } } } /** * To get a {@link BaseFont} from a file on the filesystem or in a jar. See: http://www.mail-archive.com/itext-questions@lists.sourceforge.net/msg02691.html * * @param directory * @param filename * @return * @throws Exception */ InputStream is = null; try { is = PdfPrinter.class.getResourceAsStream(directory + filename); byte[] buf = new byte[1024]; while(true) { int size = is.read(buf); if(size < 0) { break; } bos.write(buf, 0, size); } buf = bos.toByteArray(); BaseFont bf = BaseFont.createFont(filename, BaseFont.WINANSI, BaseFont.NOT_EMBEDDED, BaseFont.NOT_CACHED, buf, null); return bf; } finally { if(is != null) { is.close(); } } } } }
Okay, so what makes this code work? If you've studied it, you will have noticed 2 things: the fact that we print the document twice and the fact that we get the fonts from files. I'm going to explain both of them.
/** * With this class, you can create a {@link java.awt.Font} from TTF-files that are packaged with the jar. * * @author G.J. Schouten * */ public class FontCreator { try { Font baseFont; if(name.equalsIgnoreCase("Verdana")) { baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANAZ.TTF"); } baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANAB.TTF"); baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANAI.TTF"); } else { baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANA.TTF"); } } else { //Times new Roman is default baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMESBI.TTF"); } baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMESBD.TTF"); baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMESI.TTF"); } else { baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMES.TTF"); } } return derivedFont; } } /** * To get a {@link Font} from a file on the filesystem or in a jar. * * @param directory * @param filename * @return * @throws Exception */ InputStream is = null; try { is = FontCreator.class.getResourceAsStream(directory + filename); return font; } finally { if(is != null) { is.close(); } } } }
Running the print twice
A characteristic of the java.awt.print.Printable class is that the return value of the print method tells the caller whether the print that was just requested (by calling the method) was actually valid (PAGE_EXISTS) or that the document has ended (NO_SUCH_PAGE). In other words, we only know whether a page existed after we've printed it. The Document class from itext however, requires a new page to be created before it is printed. So, we have to create a new page in the pdf Document before we know whether it even exists! This way, we always end up with a blank page at the end of a document. Therefore, the entire print sequence is run twice. Once to count the number of pages and once to use that number to prevent an extra page from being created. Granted, the first run didn't have to include everything, but since an extra millisecond didn't really matter in my case, I kept the code as short as possible. If you want to optimize it, then by all means do!
Getting the fonts from files
A nasty thing that I ran into was that itext treats fonts just a little bit different from how java.awt.print.Printable treats them. The widths and heights of the characters are just a little bit different between them, even if using the exact same parameters (style, size, bold/italic, etc). This leads to problems when you use FontMetrics.stringWidth(), for example.
The solution here is to make both of them not use their own fonts but the fonts from font-files that are packaged with the jar. In your implementation of java.awt.print.Printable, you have to use the FontCreator to create the java.awt.Fonts from the files instead of just using the java.awt.Font constructor. We also need the PdfFontMapper in the PdfPrinter to map those java.awt.Fonts to itext's own BaseFont class. The PdfFontMapper uses the same font-files to create the itext BaseFonts, so that itext and java.awt.print.Printable now use exactly the same fonts. Problem solved!
The code that retrieves the file names for the different fonts is a little duplicated between the two. If you have many fonts that you want to use, you may want to optimize this.
So that's it. Now, you can easily convert your java.awt.print.Printables to pdf files and use them for whatever you like!
Back to blog-index
Pingback: JavaPins
I am happy that i got this article. I guess the code written in this article will be fruitful to every javaine. I am introducing this javaine word to those who are eating/driking/sleeping/doing everything in the language of Java.
I am thankful to the code writer and the article writer both.
In this case the writer of the article and the writer of the code is the same person, me! But thanks, I appreciate your comment!
Thank you for the article… It will help me for my BI project development!
As a side note iText changed license policy starting from version 5 and has two license offers, AGPL and a commercial/payed one. AGPL doesn’t allow to be included in projects non AGPL compatible. Just to point out that you can’t simply take iText jars and include them in your commercial/closed product (like you would do with Apache License).
Thanks for your replies! To be honest, I haven’t tried this solution with older, GPL compatible versions of itext. When I have some time, I might! :-)
Hello all,
I’ve tested my solution with itext-2.1.7.jar and I am happy to report that this works just fine, the 2.1.7 jar is drop-in compatible.