Sunday, 4 September 2011

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:
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.print.PageFormat;
import java.awt.print.Paper;
import java.awt.print.Printable;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;

import com.itextpdf.text.Document;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.Rectangle;
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.
     */
    public static byte[] printToPdf(Printable printable, Printable clone, int orientation) {
        
        Rectangle pageSize;
        if(orientation == PageFormat.PORTRAIT) {
            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
                
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                
                Document document = new Document(pageSize); //This determines the size of the pdf document
                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 paper = new Paper();
                paper.setSize(a4WidthInch*72, a4HeightInch*72); //72 DPI
                paper.setImageableArea(72, 72, a4WidthInch*72 - 144, a4HeightInch*72 - 144); //1 inch margins
                PageFormat pageFormat = new PageFormat();
                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++) {
                    Graphics2D g2d = contentByte.createGraphics(width, height, new PdfFontMapper());
                    int pageReturn = usedPrintable.print(g2d, pageFormat, j);
                    g2d.dispose();
                    
                    //The page that we just printed, actually existed; we only know this afterwards
                    if(pageReturn == Printable.PAGE_EXISTS) {
                        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();
            } catch(Exception e) {
                //We expect no Exceptions, so any Exception that occurs is a technical one and should be a RuntimeException
                throw new RuntimeException(e);
            }
        }
        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 {

        public BaseFont awtToPdf(Font font) {
            
            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");
                    }
                }
                
            } catch(Exception e) {
                throw new RuntimeException(e);
            }
        }

        public Font pdfToAwt(BaseFont baseFont, int size) {
            
            throw new UnsupportedOperationException();
        }
        
        /**
         * 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
         */
        private BaseFont getBaseFontFromFile(String directory, String filename) throws Exception {
            
           InputStream is = null;
           try {
               is = PdfPrinter.class.getResourceAsStream(directory + filename);
    
               ByteArrayOutputStream bos = new ByteArrayOutputStream();
               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();
               }
           }
        }
    }
}
FontCreator.java:
import java.awt.Font;
import java.io.InputStream;

/**
 * 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 {

    public static Font createFont(String name, int style, int size) {

        try {
            Font baseFont;
            
            if(name.equalsIgnoreCase("Verdana")) {
                if(style == Font.BOLD) {
                    if(style == Font.ITALIC) {
                        baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANAZ.TTF");
                    }
                    baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/verdana/", "VERDANAB.TTF");
                } else if(style == Font.ITALIC) {
                    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
                if(style == Font.BOLD) {
                    if(style == Font.ITALIC) {
                        baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMESBI.TTF");
                    }
                    baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMESBD.TTF");
                } else if(style == Font.ITALIC) {
                    baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMESI.TTF");
                } else {
                    baseFont = FontCreator.getBaseFontFromFile("/META-INF/fonts/timesnewroman/", "TIMES.TTF");
                }
            }
            
            Font derivedFont = baseFont.deriveFont(style, size);
            return derivedFont;
            
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * To get a {@link Font} from a file on the filesystem or in a jar.
     * 
     * @param directory
     * @param filename
     * @return
     * @throws Exception
     */
    private static Font getBaseFontFromFile(String directory, String filename) throws Exception {
        
       InputStream is = null;
       try {
           is = FontCreator.class.getResourceAsStream(directory + filename);
           
           Font font = Font.createFont(Font.TRUETYPE_FONT, is);
           return font;
       } 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.

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!

No comments:

Post a Comment