How to parse multiple HTML files into a single PDF?

Category: 
Tags: XMLWorkerXHTMLiText 5

I want to use iText to convert a series of html file to PDF. For instance: I have these files: page1.html, page2.html, page3.html,... Now I want to create a single PDF file, where page1.html is the first page, page2.html is the second page, and so on... I know how to convert a single HTML file to a PDF, but I don't know how to combine these different PDFs resulting from this operation into a single PDF.

Posted on StackOverflow on January 6, 2015 by kyzh101

There are two answers and answer #2 is generally better than answer #1, but I'm giving both options because there may be specific cases where answer #1 is better.

Test data: I have created 3 simple HTML files, each containing some info about a State in the US:

We are going to use XML Worker to parse these three files and we want a single PDF file as a result.

Answer #1: see ParseMultipleHtmlFiles1 for the full code sample and multiple_html_pages1.pdf for the resulting PDF.

You say that you already succeeded in converting one HTML file into one PDF files. It is assumed that you did it like this:

public byte[] parseHtml(String html) throws DocumentException, IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    // step 1
    Document document = new Document();
    // step 2
    PdfWriter writer = PdfWriter.getInstance(document, baos);
    // step 3
    document.open();
    // step 4
    XMLWorkerHelper.getInstance().parseXHtml(writer, document,
            new FileInputStream(html));
    // step 5
    document.close();
    // return the bytes of the PDF
    return baos.toByteArray();
}

This is not the most efficient way to parse an HTML file (there are other examples on the web site), but it's the simplest way.

As you can see, this method parse an HTML into a PDF file and returns that PDF file in the form of a byte[]. As we want to create a single PDF, we can feed this byte array to a PdfCopy instance, so that we can concatenate multiple documents.

Suppose that we have three documents:

public static final String[] HTML = {
    "resources/xml/page1.html",
    "resources/xml/page2.html",
    "resources/xml/page3.html"
};

We can loop over these three documents, parse them one by one to a byte[], create a PdfReader instance with the PDF bytes, and add the document to the PdfCopy instance using the addDocument() method:

public void createPdf(String file) throws IOException, DocumentException {
    Document document = new Document();
    PdfCopy copy = new PdfCopy(document, new FileOutputStream(file));
    document.open();
    PdfReader reader;
    for (String html : HTML) {
        reader = new PdfReader(parseHtml(html));
        copy.addDocument(reader);
        reader.close();
    }
    document.close();
} 

This solves your problem, but suppose that you need to use a special font that needs to be embedded. In that case, every separate PDF file will contain a subset of that font. Different files will require different font subsets, and PdfCopy (nor PdfSmartCopy for that matter) can merge font subsets. This could result in a bloated PDF file with way too many font subsets of the same font. How can we avoid this? That's explained in answer #2.

Answer #2: See ParseMultipleHtmlFiles2 for the full code sample and multiple_html_pages2.pdf for the resulting PDF. You already see the difference in file size: 4.61 KB versus 5.05 KB (and we didn't even introduce embedded fonts).

In this case, we don't parse the HTML to a PDF file the way we did in the parseHtml() method from answer #1. Instead, we parse the HTML to an iText ElementList using the parseToElementList() method. This method requires two Strings. One containing the HTML code, the other one containing CSS values. We use a utility method to read the HTML file into a String. As for the CSS value, we could pass null to parseToElementList(), but in that case, default styles will be ignored. You'll notice that the <h1> tag we introduced in our HTML will look completely different if you don't pass the default.css that is shipped with XML Worker. This is the code:

public void createPdf(String file) throws IOException, DocumentException {
    Document document = new Document();
    PdfWriter.getInstance(document, new FileOutputStream(file));
    document.open();
    String css = readCSS();
    for (String htmlfile : HTML) {
        String html = Utilities.readFileToString(htmlfile);
        ElementList list = XMLWorkerHelper.parseToElementList(html, css);
        for (Element e : list) {
            document.add(e);
        }
        document.newPage();
    }
    document.close();
}

We create a single Document and a single PdfWriter instance. We parse the different HTML files into ElementLists one by one, and we add all the elements to the Document.

As you want a new page, each time a new HTML file is parsed, I introduced a document.newPage(). If you remove this line, you can add the three HTML pages on a single page (which wouldn't be possible if you would opt for answer #1).