2

I convert HTML to PDF using iText7 with the convertToPDF() method of pdfHTML. I would like to change the page orientation for a few specific pages in my PDF document. The content of these pages is dynamic, and we cannot guess how many pages that should be in landscape (i.e. content of dynamic table could take more than one page)

Current situation: I create a custom worker (implements ITagWorker) which landscape the page that following the tag <landscape/>

public byte[] generatePDF(String html) {
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    PdfWriter pdfWriter = new PdfWriter(byteArrayOutputStream);
    PdfDocument pdfDocument = new PdfDocument(pdfWriter);
    try {

        ConverterProperties properties = new ConverterProperties();

        properties.setTagWorkerFactory(
                new DefaultTagWorkerFactory() {
                    @Override
                    public ITagWorker getCustomTagWorker(
                            IElementNode tag, ProcessorContext context) {
                        if ("landscape".equalsIgnoreCase(tag.name())) {
                            return new LandscapeDivTagWorker();
                        }
                        return null;
                    }
                } );

        MediaDeviceDescription mediaDeviceDescription = new MediaDeviceDescription(MediaType.PRINT);
        properties.setMediaDeviceDescription(mediaDeviceDescription);

        HtmlConverter.convertToPdf(html, pdfDocument, properties);
    } catch (IOException e) {
        e.printStackTrace();
    }
    pdfDocument.close();
    return byteArrayOutputStream.toByteArray();
}

The custom worker :

public class LandscapeDivTagWorker implements ITagWorker {

    @Override
    public void processEnd(IElementNode element, ProcessorContext context) {
    }

    @Override
    public boolean processContent(String content, ProcessorContext context) {
        return false;
    }

    @Override
    public boolean processTagChild(ITagWorker childTagWorker, ProcessorContext context) {
        return false;
    }

    @Override
    public IPropertyContainer getElementResult() {
        return new AreaBreak(new PageSize(PageSize.A4).rotate());
    }
}

Is there a way to define all the content that should be displayed in landscape?

Something like:

<p>Display in portrait</p>
<landscape>
<div>
<p>display in landscape</p>
…
<table>
..
</table>
</div>
</landscape>

or with a CSS class :

<p>Display in portrait</p>
<div class="landscape">
<p>display in landscape</p>
…
<table>
..
</table>
</div>

Result => 1 page in portrait and other pages in landscape (All the div content should be in landscape)

PS: I follow this hint Change page orientation for only some pages in the resulting PDF (created out of html) by using a custom CssApplierFactory but the result was the same => just the first page where the landscape class is used was in landscape and the other content of the table was in portrait

Alexey Subach
  • 11,903
  • 7
  • 34
  • 60
helmut
  • 23
  • 3

1 Answers1

2

Doing it is actually quite tricky, but the whole mechanism is still flexible enough to accommodate this requirement.

We will be working on supporting the following syntax:

<p>Display in portrait</p>
<landscape>
<div>
<p>display in landscape</p>
<p>content</p>
.....
<p>content</p>
</div>
</landscape>
<p> After portrait </p>

First off, we will need to convert the HTML content into elements first and then add those elements into a document instead direct HTML -> PDF conversion. This is needed because in case of HTML there is a separate mechanism for page size handling as dictated by CSS specification and it's not flexible enough to accommodate your requirement so we will be using native iText Layout mechanism for that.

The idea is that apart from customizing the new page size by passing an argument to AreaBreak we will also change the default page size for PdfDocument so that all the consequent pages are created with that custom new page size. For that we will need to pass PdfDocument all along. The high-level code looks as follows:

PdfDocument pdfDocument = new PdfDocument(new PdfWriter(outFilePath));
ConverterProperties properties = new ConverterProperties();
properties.setTagWorkerFactory(new CustomTagWorkerFactory(pdfDocument));

Document document = new Document(pdfDocument);
List<IElement> elements = HtmlConverter.convertToElements(new FileInputStream(inputHtmlPath), properties);
for (IElement element : elements) {
    if (element instanceof IBlockElement) {
        document.add((IBlockElement) element);
    }
}

pdfDocument.close();

The custom tag worker factory is also almost unchanged - it just passes PdfDocument along to the tag worker:

private static class CustomTagWorkerFactory extends DefaultTagWorkerFactory {
    PdfDocument pdfDocument;

    public CustomTagWorkerFactory(PdfDocument pdfDocument) {
        this.pdfDocument = pdfDocument;
    }

    @Override
    public ITagWorker getCustomTagWorker(IElementNode tag, ProcessorContext context) {
        if ("landscape".equalsIgnoreCase(tag.name())) {
            return new LandscapeDivTagWorker(tag, context, pdfDocument);
        }
        return null;
    }
}

The idea of LandscapeDivTagWorker is to create a Div wrapper and put there the inner content of <landscape> tag but also surround it with AreaBreak elements - the preceding one will force the new page break with landscape orientation and change the default page size for the whole document while the succeeding one will revert everything back - force the split to portrait page size and set default page size to portrait as well. Note that we are also setting a custom renderer for AreaBreak with setNextRenderer to actually set the default page size when it comes to that break:

private static class LandscapeDivTagWorker extends DivTagWorker {
    private PdfDocument pdfDocument;

    public LandscapeDivTagWorker(IElementNode element, ProcessorContext context, PdfDocument pdfDocument) {
        super(element, context);
        this.pdfDocument = pdfDocument;
    }

    @Override
    public IPropertyContainer getElementResult() {
        IPropertyContainer baseElementResult = super.getElementResult();
        if (baseElementResult instanceof Div) {
            Div div = new Div();
            AreaBreak landscapeAreaBreak = new AreaBreak(new PageSize(PageSize.A4).rotate());
            landscapeAreaBreak.setNextRenderer(new DefaultPageSizeChangingAreaBreakRenderer(landscapeAreaBreak, pdfDocument));
            div.add(landscapeAreaBreak);
            div.add((IBlockElement) baseElementResult);
            AreaBreak portraitAreaBreak = new AreaBreak(new PageSize(PageSize.A4));
            portraitAreaBreak.setNextRenderer(new DefaultPageSizeChangingAreaBreakRenderer(portraitAreaBreak, pdfDocument));
            div.add(portraitAreaBreak);
            baseElementResult = div;
        }
        return baseElementResult;
    }
}

The implementation of the custom area break renderer is quite straightforward - we only set the default page size to PdfDocument - the rest is done under the hood by default implementations which we extend from:

private static class DefaultPageSizeChangingAreaBreakRenderer extends AreaBreakRenderer {
    private PdfDocument pdfDocument;
    private AreaBreak areaBreak;

    public DefaultPageSizeChangingAreaBreakRenderer(AreaBreak areaBreak, PdfDocument pdfDocument) {
        super(areaBreak);
        this.pdfDocument = pdfDocument;
        this.areaBreak = areaBreak;
    }

    @Override
    public LayoutResult layout(LayoutContext layoutContext) {
        pdfDocument.setDefaultPageSize(areaBreak.getPageSize());
        return super.layout(layoutContext);
    }
}

As a result you will get the page set up similar to the one on the screenshot:

result

Alexey Subach
  • 11,903
  • 7
  • 34
  • 60
  • Many thanks for your response, it's worked fine :) however I sometimes have just an issue when I try to convert a template containing another one with landscape tag (like `
    `) I got a loop during adding elements into the document (issue with the element containing the landscape tag) After debugging I find the loop in the **addChild** method of the class **RootRenderer** in the second **for** loop. Do you have any Idea ?
    – helmut Jul 13 '20 at 16:04
  • I fixed the issue by removing a CSS class containing this style `page-break-inside:avoid;` – helmut Jul 14 '20 at 08:41