Search code examples
javapdfitextrendereritext7

Switch between ColumnDocumentRenderer and DocumentRenderer in same page?


I was testing few things with iText7 and I have a scenario where I need to have DocumentRenderer paragraph at the top and then start the ColumnDocumentRender with 2 columns right below it on the same page. The problem I am having is when I change the content on same page it overlaps content from DocumentRenderer with content from ColumnDocumentRenderer. I believe it is because one render does not know about the other render and contents starts from the top of the page. I followed this tutorial but it only shows how to add content to the next page. It does say

we'll have to instruct iText not to flush the content to the OutputStream

But can anyone show me exactly how we can achieve this?

public void createPdf(String dest) throws IOException {
    PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
    Document document = new Document(pdf);
    Paragraph p = new Paragraph()
        .add("Be prepared to read a story about a London lawyer "
        + "named Gabriel John Utterson who investigates strange "
        + "occurrences between his old friend, Dr. Henry Jekyll, "
        + "and the evil Edward Hyde.");
    document.add(p);
    document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
    ... // Define column areas
    document.setRenderer(new ColumnDocumentRenderer(document, columns));
    document.add(new AreaBreak(AreaBreakType.LAST_PAGE));   
    ... // Add novel in two columns
    document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
    document.setRenderer(new DocumentRenderer(document)); 
    document.add(new AreaBreak(AreaBreakType.LAST_PAGE));
    p = new Paragraph()
        .add("This was the story about the London lawyer "
        + "named Gabriel John Utterson who investigates strange "
        + "occurrences between his old friend, Dr. Henry Jekyll, "
        + "and the evil Edward Hyde. THE END!");
    document.add(p);
    document.close();
}

I need something like this:

iText7 Columns

Whenever you create a new DocumentRenderer, iText starts returns to the top of the document –that is: from the first page. This allows you to use different renderers on the same document next to each other on the same page. If that is needed, we'll have to instruct iText not to flush the content to the OutputStream; otherwise we won't have access to previous pages. In this case, we don't need to change anything on previous pages. We just want to switch to another renderer on the next page. Introducing a page break that goes to the last page will avoid that new content overwrites old content.


Solution

  • I have taken this code: C02E08_JekyllHydeV4

    And I updated it according to what you have in your question:

    //Initialize PDF document
    PdfDocument pdf = new PdfDocument(new PdfWriter(dest));
    // Initialize document
    Document document = new Document(pdf);
    Paragraph p = new Paragraph()
        .add("Be prepared to read a story about a London lawyer "
           + "named Gabriel John Utterson who investigates strange "
           + "occurrences between his old friend, Dr. Henry Jekyll, "
           + "and the evil Edward Hyde.");
    document.add(p);
    document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
    
    //Set column parameters
    ...
    //Define column areas
    ...
    document.setRenderer(new ColumnDocumentRenderer(document, columns)); 
    document.add(new AreaBreak(AreaBreakType.LAST_PAGE));   
    // Add the full Jekyl and Hyde text
    document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
    document.setRenderer(new DocumentRenderer(document)); 
    document.add(new AreaBreak(AreaBreakType.LAST_PAGE));
    p = new Paragraph()
        .add("This was the story about the London lawyer "
           + "named Gabriel John Utterson who investigates strange "
           + "occurrences between his old friend, Dr. Henry Jekyll, "
           + "and the evil Edward Hyde. THE END!");
    document.add(p);
    //Close document
    document.close();
    

    The result looks like this:

    enter image description here

    enter image description here

    enter image description here

    I think that's the behavior you are looking for. If it's not, please explain what goes wrong.

    UPDATE:

    After clarifying the question, it is clear that the above answer doesn't solve the problem. This is the solution to the actual problem:

    We need to create a custom ParagraphRenderer to determine a Y-position: class MyParagraphRenderer extends ParagraphRenderer {

        float y;
    
        public MyParagraphRenderer(Paragraph modelElement) {
            super(modelElement);
        }
    
        @Override
        public void drawBorder(DrawContext drawContext) {
            super.drawBorder(drawContext);
            y = getOccupiedAreaBBox().getBottom();
        }
    
        public float getY() {
            return y;
        }
    
    }
    

    When we add the first paragraph, we need to use this custom ParagraphRenderer:

    Paragraph p = new Paragraph()
            .add("Be prepared to read a story about a London lawyer "
            + "named Gabriel John Utterson who investigates strange "
            + "occurrences between his old friend, Dr. Henry Jekyll, "
            + "and the evil Edward Hyde.");
    MyParagraphRenderer renderer = new MyParagraphRenderer(p);
    p.setNextRenderer(renderer);
    document.add(p);
    

    We can now get the Y position we need like this: renderer.getY(); we use this Y position to define a first set of columns:

    float offSet = 36;
    float gutter = 23;
    float columnWidth = (PageSize.A4.getWidth() - offSet * 2) / 2 - gutter;
    float columnHeight1 = renderer.getY() - offSet * 2;
    Rectangle[] columns1 = {
        new Rectangle(offSet, offSet, columnWidth, columnHeight1),
        new Rectangle(offSet + columnWidth + gutter, offSet, columnWidth, columnHeight1)};
    

    We could use this set of columns to create a ColumnDocumentRenderer, but if more than one page is needed to render all the content, then the offset of the columns on the second page will be wrong, hence we also create a custom ColumnDocumentRenderer:

    class MyColumnDocumentRenderer extends ColumnDocumentRenderer {
    
        Rectangle[] columns2;
    
        public MyColumnDocumentRenderer(Document document, Rectangle[] columns1, Rectangle[] columns2) {
            super(document, columns1);
            this.columns2 = columns2;
        }
    
        @Override
        protected PageSize addNewPage(PageSize customPageSize) {
            PageSize size = super.addNewPage(customPageSize);
            columns = columns2;
            return size;
        }
    }
    

    This ColumnDocumentRenderer accepts two sets of columns, one set will be used on the first page, the second set will be used on all subsequent pages. This is how we define and apply the custom ColumnDocumentRenderer:

    float columnHeight2 = PageSize.A4.getHeight() - offSet * 2;
    Rectangle[] columns2 = {
        new Rectangle(offSet, offSet, columnWidth, columnHeight2),
        new Rectangle(offSet + columnWidth + gutter, offSet, columnWidth, columnHeight2)};
    document.setRenderer(new MyColumnDocumentRenderer(document, columns1, columns2));  
    

    Now the result looks like this:

    enter image description here

    Depending on the distance you want between the first page-wide paragraph and the subsequent content in the columns, you can adjust the value of renderer.getY() - offSet * 2.