From java.awt.print.Printable to PDF

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:
  1. import java.awt.Font;
  2. import java.awt.Graphics2D;
  3. import java.awt.print.PageFormat;
  4. import java.awt.print.Paper;
  5. import java.awt.print.Printable;
  6. import java.io.ByteArrayOutputStream;
  7. import java.io.InputStream;
  8.  
  9. import com.itextpdf.text.Document;
  10. import com.itextpdf.text.PageSize;
  11. import com.itextpdf.text.Rectangle;
  12. import com.itextpdf.text.pdf.BaseFont;
  13. import com.itextpdf.text.pdf.FontMapper;
  14. import com.itextpdf.text.pdf.PdfContentByte;
  15. import com.itextpdf.text.pdf.PdfWriter;
  16.  
  17. /**
  18.  * 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.
  19.  *
  20.  * @author G.J. Schouten
  21.  *
  22.  */
  23. public class PdfPrinter {
  24.  
  25. /**
  26.   * Prints the given {@link Printable} to a pdf file and returns the result as a byte-array. The size of the document is A4.
  27.   *
  28.   * @param printable The {@link Printable} that has to be printed.
  29.   * @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.
  30.   * @param orientation {@link PageFormat}.PORTRAIT or {@link PageFormat}.LANDSCAPE.
  31.   * @return A byte-array with the pdf file.
  32.   */
  33. public static byte[] printToPdf(Printable printable, Printable clone, int orientation) {
  34.  
  35. Rectangle pageSize;
  36. if(orientation == PageFormat.PORTRAIT) {
  37. pageSize = PageSize.A4;
  38. } else {
  39. pageSize = PageSize.A4.rotate();
  40. }
  41.  
  42. //The bytes to be returned
  43. byte[] bytes = null;
  44.  
  45. //We will count the number of pages that have to be printed
  46. int numberOfPages = 0;
  47.  
  48. //We will print the document twice, once to count the number of pages and once for real
  49. for(int i=0; i<2; i++) {
  50. try {
  51. Printable usedPrintable = i==0 ? printable : clone; //First time, use the printable, second time, use the clone
  52.  
  53.  
  54. Document document = new Document(pageSize); //This determines the size of the pdf document
  55. PdfWriter writer = PdfWriter.getInstance(document, bos);
  56. document.open();
  57. PdfContentByte contentByte = writer.getDirectContent();
  58.  
  59. //These lines do not influence the pdf document, but are there to tell the Printable how to print
  60. double a4WidthInch = 8.26771654; //Equals 210mm
  61. double a4HeightInch = 11.6929134; //Equals 297mm
  62. Paper paper = new Paper();
  63. paper.setSize(a4WidthInch*72, a4HeightInch*72); //72 DPI
  64. paper.setImageableArea(72, 72, a4WidthInch*72 - 144, a4HeightInch*72 - 144); //1 inch margins
  65. PageFormat pageFormat = new PageFormat();
  66. pageFormat.setPaper(paper);
  67. pageFormat.setOrientation(orientation);
  68.  
  69. float width = ((float) pageFormat.getWidth());
  70. float height = ((float) pageFormat.getHeight());
  71.  
  72. //First time, don't use numberOfPages, since we don't know it yet
  73. for(int j=0; j<numberOfPages || i==0; j++) {
  74. Graphics2D g2d = contentByte.createGraphics(width, height, new PdfFontMapper());
  75. int pageReturn = usedPrintable.print(g2d, pageFormat, j);
  76. g2d.dispose();
  77.  
  78. //The page that we just printed, actually existed; we only know this afterwards
  79. if(pageReturn == Printable.PAGE_EXISTS) {
  80. 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
  81. if(i == 0) { //First run, count the pages
  82. numberOfPages++;
  83. }
  84. } else {
  85. break;
  86. }
  87. }
  88. document.close();
  89. writer.close();
  90.  
  91. bytes = bos.toByteArray();
  92. } catch(Exception e) {
  93. //We expect no Exceptions, so any Exception that occurs is a technical one and should be a RuntimeException
  94. throw new RuntimeException(e);
  95. }
  96. }
  97. return bytes;
  98. }
  99.  
  100. /**
  101.   * 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.
  102.   *
  103.   * @author G.J. Schouten
  104.   *
  105.   */
  106. private static class PdfFontMapper implements FontMapper {
  107.  
  108. public BaseFont awtToPdf(Font font) {
  109.  
  110. try {
  111. if(font.getFamily().equalsIgnoreCase("Verdana")) {
  112. if(font.isBold()) {
  113. if(font.isItalic()) {
  114. return this.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANAZ.TTF");
  115. }
  116. return this.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANAB.TTF");
  117. } else if(font.isItalic()) {
  118. return this.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANAI.TTF");
  119. } else {
  120. return this.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANA.TTF");
  121. }
  122. } else { //Times new Roman is default
  123. if(font.isBold()) {
  124. if(font.isItalic()) {
  125. return this.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMESBI.TTF");
  126. }
  127. return this.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMESBD.TTF");
  128. } else if(font.isItalic()) {
  129. return this.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMESI.TTF");
  130. } else {
  131. return this.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMES.TTF");
  132. }
  133. }
  134.  
  135. } catch(Exception e) {
  136. throw new RuntimeException(e);
  137. }
  138. }
  139.  
  140. public Font pdfToAwt(BaseFont baseFont, int size) {
  141.  
  142. }
  143.  
  144. /**
  145.   * 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
  146.   *
  147.   * @param directory
  148.   * @param filename
  149.   * @return
  150.   * @throws Exception
  151.   */
  152. private BaseFont getBaseFontFromFile(String directory, String filename) throws Exception {
  153.  
  154. InputStream is = null;
  155. try {
  156. is = PdfPrinter.class.getResourceAsStream(directory + filename);
  157.  
  158. byte[] buf = new byte[1024];
  159. while(true) {
  160. int size = is.read(buf);
  161. if(size < 0) {
  162. break;
  163. }
  164. bos.write(buf, 0, size);
  165. }
  166. buf = bos.toByteArray();
  167. BaseFont bf = BaseFont.createFont(filename, BaseFont.WINANSI, BaseFont.NOT_EMBEDDED, BaseFont.NOT_CACHED, buf, null);
  168. return bf;
  169. } finally {
  170. if(is != null) {
  171. is.close();
  172. }
  173. }
  174. }
  175. }
  176. }
FontCreator.java:
  1. import java.awt.Font;
  2. import java.io.InputStream;
  3.  
  4. /**
  5.  * With this class, you can create a {@link java.awt.Font} from TTF-files that are packaged with the jar.
  6.  *
  7.  * @author G.J. Schouten
  8.  *
  9.  */
  10. public class FontCreator {
  11.  
  12. public static Font createFont(String name, int style, int size) {
  13.  
  14. try {
  15. Font baseFont;
  16.  
  17. if(name.equalsIgnoreCase("Verdana")) {
  18. if(style == Font.BOLD) {
  19. if(style == Font.ITALIC) {
  20. baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANAZ.TTF");
  21. }
  22. baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANAB.TTF");
  23. } else if(style == Font.ITALIC) {
  24. baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANAI.TTF");
  25. } else {
  26. baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANA.TTF");
  27. }
  28. } else { //Times new Roman is default
  29. if(style == Font.BOLD) {
  30. if(style == Font.ITALIC) {
  31. baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMESBI.TTF");
  32. }
  33. baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMESBD.TTF");
  34. } else if(style == Font.ITALIC) {
  35. baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMESI.TTF");
  36. } else {
  37. baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMES.TTF");
  38. }
  39. }
  40.  
  41. Font derivedFont = baseFont.deriveFont(style, size);
  42. return derivedFont;
  43.  
  44. } catch(Exception e) {
  45. throw new RuntimeException(e);
  46. }
  47. }
  48.  
  49. /**
  50.   * To get a {@link Font} from a file on the filesystem or in a jar.
  51.   *
  52.   * @param directory
  53.   * @param filename
  54.   * @return
  55.   * @throws Exception
  56.   */
  57. private static Font getBaseFontFromFile(String directory, String filename) throws Exception {
  58.  
  59. InputStream is = null;
  60. try {
  61. is = FontCreator.class.getResourceAsStream(directory + filename);
  62.  
  63. Font font = Font.createFont(Font.TRUETYPE_FONT, is);
  64. return font;
  65. } finally {
  66. if(is != null) {
  67. is.close();
  68. }
  69. }
  70. }
  71. }
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.

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!
Gert-Jan Schouten, MSc. is a Senior Software Architect / Engineer specialized in Java and Development Infrastructure. At this blog, he writes about his personal visions and thoughts.

Back to blog-index

Share |

9 thoughts on “From java.awt.print.Printable to PDF

  1. Pingback: JavaPins

  2. 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.

  3. 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).

  4. Thanks for your article.
    I am a newbie and need help. I am not able to figure out how to use/call your class “PdfPrinter” to print from a JPanel. Can you please show some code example. My JPanel is called “wrapper” which contains other panels and Jlabels. The height of the wrapper panel is 2000 (sometimes more) and therefore needs pagination. I want to produce a multipage pdf document from the content of my “wrapper” panel.
    Please help.

    Roman

  5. Hello Roman!

    Thanks for your question! Unfortunately, my article is about converting a java.awt.print.Printable to a PDF file. The java printing API, of which java.awt.print.Printable is a part, is completely separate from the Swing and AWT GUI classes, meaning that you cannot “print” a JPanel.

    I used to face the same problem in my own applications and the only solution was to display my data in two different ways: once in a JPanel, to show it to the user in the application and once in a java.awt.print.Printable, in order to print it.

    A good guide on how to print stuff with a java.awt.print.Printable can be found here: http://www.javaworld.com/article/2076214/java-se/printing-in-java–part-1.html

    Even though it’s NOT possible to print a JPanel, it IS possible to show a java.awt.print.Printable object in a JPanel. In order to achieve this, override the paint(Graphics g) method of the JPanel and use the Graphics object to paint the Printable to the JPanel. In order to do this, you need to create your own PageFormat object to pass to the print method of Printable. Please note that this is quite advanced and requires a deep understanding of both JPanel and Printable. If you’re a rookie, I recommend just writing both a JPanel and a Printable to display the data.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>