How to adjust the page height to the content height?

6k views Asked by At

I'm using iTextPDF + FreeMarker for my project. Basically I load and fill an HTML template with FreeMarker and then render it to pdf with iTextPDF's XMLWorker.

The template is:

<html>
    <body style="font-family; ${fontName}">
        <table>
            <tr>
                <td style="text-align: right">${timestampLabel}&nbsp;</td>
                <td><b>${timestampValue}</b></td>
            </tr>
            <tr>
                <td style="text-align: right">${errorIdLabel}&nbsp;</td>
                <td><b>${errorIdValue}</b></td>
            </tr>
            <tr>
                <td style="text-align: right">${systemIdLabel}&nbsp;</td>
                <td><b>${systemIdValue}</b></td>
            </tr>
            <tr>
                <td style="text-align: right">${descriptionLabel}&nbsp;</td>
                <td><b>${descriptionValue}</b></td>
            </tr>
        </table>
    </body>
</html>

And this is my code:

SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");

String errorId = "ERROR-01";
String systemId = "SYSTEM-01";
String description = "A SHORT DESCRIPTION OF THE ISSUE";

Map<String, String> parametersMap = new HashMap<String, String>();
parametersMap.put("fontName", fontName);  //valid font name

parametersMap.put("timestampLabel",   "   TIMESTAMP:");
parametersMap.put("errorIdLabel",     "    ERROR ID:");
parametersMap.put("systemIdLabel",    "   SYSTEM ID:");
parametersMap.put("descriptionLabel", " DESCRIPTION:");

parametersMap.put("timestampValue", DATE_FORMAT.format(new Date()));
parametersMap.put("errorIdValue", errorId);
parametersMap.put("systemIdValue", systemId);
parametersMap.put("descriptionValue", description);

FreeMarkerRenderer renderer = new FreeMarkerRenderer(); //A utility class
renderer.loadTemplate(errorTemplateFile);               //the file exists
String rendered = renderer.render(parametersMap);

File temp = File.createTempFile("document", ".pdf");
file = new FileOutputStream(temp);
Document document = new Document();

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
document.setPageSize(new Rectangle(290f, 150f));
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

document.setMargins(10, 10, 10, 10);
PdfWriter writer = PdfWriter.getInstance(document, file);
document.open();
InputStream is = new ByteArrayInputStream(rendered.getBytes());


XMLWorkerFontProvider provider = new XMLWorkerFontProvider();
provider.register(fontFile); //the file exists
FontFactory.setFontImp(provider);
byte[] errorStyle = getErrorStyleByteArray(); //returns a byte array from a css file (works)

XMLWorkerHelper helper = XMLWorkerHelper.getInstance();
helper.parseXHtml(writer, document, is, new ByteArrayInputStream(errorStyle), provider);

document.close();
file.close();

This code works fine, but With the fixed height is a problem.

I.E. Let's say that:

errorId = "ERROR-01"
systemId = "SYSTEM-01"
description = "A SHORT DESCRIPTION OF THE ISSUE"

The produced document is:

enter image description here

If instead I use

errorId = "ERROR-01"
systemId = "SYSTEM-01"
description = "A SHORT DESCRIPTION OF THE ISSUE. THIS IS MULTILINE AND IT SHOULD STAY ALL IN THE SAME PDF PAGE."

The produced document is:

enter image description here

As you can see, in the last document I have two pages. I would like to have only one page which changes its height according to the content height.

Is something like this possible with iText?

1

There are 1 answers

6
Bruno Lowagie On BEST ANSWER

You can not change the page size after you have added content to that page. One way to work around this, would be to create the document in two passes: first create a document to add the content, then manipulate the document to change the page size. That would have been my first reply if I had time to answer immediately.

Now that I've taken more time to think about it, I've found a better solution that doesn't require two passes. Take a look at HtmlAdjustPageSize

In this example, I first parse the content to a list of Element objects using this method:

public ElementList parseHtml(String html, String css) throws IOException {
    // CSS
    CSSResolver cssResolver = new StyleAttrCSSResolver();
    CssFile cssFile = XMLWorkerHelper.getCSS(new ByteArrayInputStream(css.getBytes()));
    cssResolver.addCss(cssFile);

    // HTML
    CssAppliers cssAppliers = new CssAppliersImpl(FontFactory.getFontImp());
    HtmlPipelineContext htmlContext = new HtmlPipelineContext(cssAppliers);
    htmlContext.setTagFactory(Tags.getHtmlTagProcessorFactory());
    htmlContext.autoBookmark(false);

    // Pipelines
    ElementList elements = new ElementList();
    ElementHandlerPipeline end = new ElementHandlerPipeline(elements, null);
    HtmlPipeline htmlPipeline = new HtmlPipeline(htmlContext, end);
    CssResolverPipeline cssPipeline = new CssResolverPipeline(cssResolver, htmlPipeline);

    // XML Worker
    XMLWorker worker = new XMLWorker(cssPipeline, true);
    XMLParser p = new XMLParser(worker);
    p.parse(new ByteArrayInputStream(html.getBytes()));

    return elements;
}

Note: I've been copy/pasting this method so many times that I decided to make it a static method in the XMLWorkerHelper class. It will be available in the next iText release.

Important: I have done what I promised, this method is now available in the XML Worker release.

For testing purposes, I used static String values for HTML and CSS:

public static final String HTML = "<table>" +
    "<tr><td class=\"ra\">TIMESTAMP</td><td><b>2014-11-28 11:06:09</b></td></tr>" +
    "<tr><td class=\"ra\">ERROR ID</td><td><b>ERROR-01</b></td></tr>" +
    "<tr><td class=\"ra\">SYSTEM ID</td><td><b>SYSTEM-01</b></td></tr>" +
    "<tr><td class=\"ra\">DESCRIPTION</td><td><b>TEST WITH A VERY, VERY LONG DESCRIPTION LINE THAT NEEDS MULTIPLE LINES</b></td></tr>" +
    "</table>";
public static final String CSS = "table {width: 200pt; } .ra { text-align: right; }";
public static final String DEST = "results/xmlworker/html_page_size.pdf";

You can see that I took HTML that looks more or less like the HTML you are dealing with.

I parse this HTML and CSS to an ElementList:

ElementList el = parseHtml(HTML, CSS);

Or, starting with XML Worker 5.5.4:

ElementList el = XMLWorkerHelper.parseToElementList(HTML, CSS);

So far, so good. I haven't told you anything that you didn't already know, except this: I am now going to use this el twice:

  1. I'll add the list to a ColumnText in simulation mode. This ColumnText isn't tied to any document or writer yet. The sole purpose to do this, is to know how much space I need vertically.
  2. I'll add the list to a ColumnText for real. This ColumnText will fit exactly on a page of a size that I define using the results obtained in simulation mode.

Some code will clarify what I mean:

// I define a width of 200pt
float width = 200;
// I define the height as 10000pt (which is much more than I'll ever need)
float max = 10000;
// I create a column without a `writer` (strange, but it works)
ColumnText ct = new ColumnText(null);
ct.setSimpleColumn(new Rectangle(width, max));
for (Element e : el) {
    ct.addElement(e);
}
// I add content in simulation mode
ct.go(true);
// Now I ask the column for its Y position
float y = ct.getYLine();

The above code is useful for only one things: getting the y value that will be used to define the page size of the Document and the column dimension of the ColumnText that will be added for real:

Rectangle pagesize = new Rectangle(width, max - y);
// step 1
Document document = new Document(pagesize, 0, 0, 0, 0);
// step 2
PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(file));
// step 3
document.open();
// step 4
ct = new ColumnText(writer.getDirectContent());
ct.setSimpleColumn(pagesize);
for (Element e : el) {
    ct.addElement(e);
}
ct.go();
// step 5
document.close();

Please download the full HtmlAdjustPageSize.java code and change the value of HTML. You'll see that this leads to different page sizes.