Renderers & Custom Layout

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Every layout element you add to an iText 9 Document is only a description: a Paragraph knows its text and its properties, but not where on the page it will end up or how tall it will be. The work of measuring, splitting and painting is done by a second tree of objects, the renderers, which implement the IRenderer interface of com.itextpdf.layout.renderer. Most documents never touch them, because each element creates a suitable renderer on its own. They become important the moment the built-in behaviour is not what you need: a coloured badge behind a paragraph, a frame with two lines, a paragraph that must never be split across pages, a layout that flows text into several columns. This page explains the renderer tree, shows how to replace or subclass a renderer, how a renderer reports the result of a layout pass, how to flow content into columns, and how to draw rounded corners and custom borders. Versions are stated as of 2026-10-09: iText Core 9.8.0 on Java 21. Every example was compiled and run against exactly that version, and the output PDFs were rendered and inspected.

The elements themselves are described in Layout Elements, the document and writer objects the renderers draw onto in Document Lifecycle, and the table-specific use of renderers, in particular the cell and table renderers that replaced the old table events, in Tables. The running scenario is the Bookshelf Billing invoice. Page headers, footers and watermarks are drawn from page events rather than from renderers, and are covered in the page on page events of this section.

What the old API did and what replaces it

The classic text iText in Action (Manning, 2006/2007, consulted for the concept order of this page and not reproduced) was written for iText 1.4 and com.lowagie.text. It builds multi-column pages with ColumnText, which you feed with phrases and paragraphs and then ask to go() into a rectangle, and with MultiColumnText, which owns a list of column rectangles. Both classes are part of the iText 5 and OpenPDF API only: they are absent from iText 7, 8 and 9, which are a rewrite of the library (finding O5 of this section’s source analysis, the part about ColumnText and MultiColumnText). The current approach has three parts:

  • the layout engine itself is a renderer tree, so "put this content into this rectangle and tell me what did not fit" is what every renderer’s layout method does, with a LayoutResult instead of the old go() status flags;

  • multiple columns are a property of the document’s own renderer: you give the Document a ColumnDocumentRenderer built from an array of column rectangles, and every element you add afterwards flows from column to column and page to page;

  • custom drawing is done by subclassing the renderer of the element, not by registering table, cell or page events on the element.

The old Chapter, Section and HeaderFooter classes, and the PdfPTable event interfaces, are replaced by plain layout elements, page event handlers and, again, renderers (see Tables for the table part).

Java moved on as well (finding O28). The book’s listings are Java 1.3 to 5 code without generics, with close() calls in the middle of a method. All examples on this page are Java 21: they use switch expressions, var, String.formatted, repeat and try-with-resources, which closes the Document and the PdfDocument in the correct order even when an exception is thrown. The Maven dependencies, restated here so that the coordinates are complete on this page, are iText Core as a POM aggregator and, because the same project usually also signs or encrypts, one Bouncy Castle adapter. iText is licensed under the AGPLv3 or under a commercial licence (see the licensing discussion in the Introduction & Approaches page of this section).

<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itext-core</artifactId>
    <version>9.8.0</version>
    <type>pom</type>
</dependency>
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>bouncy-castle-adapter</artifactId>
    <version>9.8.0</version>
</dependency>

From elements to renderers

The model of iText 9 separates three things, which the figure below shows for a table with one row (the larger picture is in iText Architecture & Modules):

  • the element tree: Table, Cell, Paragraph, Text. These classes hold content and properties (font, margins, borders, colours) and nothing else. They implement IElement;

  • the renderer tree: TableRenderer, CellRenderer, ParagraphRenderer, TextRenderer. A renderer holds a reference to its model element (getModelElement()), its children (getChildRenderers()), and, after layout, the area it occupies (getOccupiedArea()). Renderers are created by the element for each layout and are not meant to be reused;

  • the document renderer, DocumentRenderer, the root of the tree, which owns the list of pages and decides where the next piece of content goes.

Element tree and renderer tree: each element creates one renderer, and the document renderer drives layout then draw

When you call document.add(element), the document asks the element for a renderer with createRendererSubTree(). The default implementation calls the protected factory makeNewRenderer() of the element, which returns the right renderer class, and then recursively adds the renderers of the children with addChild. The root renderer then calls layout on the new subtree, handing it the current free area, and draw on the result, once the page is known. The next example performs the same steps by hand for a one-row table so that the tree can be printed. It uses a LayoutResult whose getStatus() is one of three integer constants, FULL, PARTIAL and NOTHING, described in the layout results section below. Note the Java 21 switch expression on those constants.

package com.example.billing;

import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Cell;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import com.itextpdf.layout.layout.LayoutArea;
import com.itextpdf.layout.layout.LayoutContext;
import com.itextpdf.layout.layout.LayoutResult;
import com.itextpdf.layout.renderer.IRenderer;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class RendererTreeDump {

    public static void main(String[] args) throws IOException {
        Table table = new Table(2);
        table.addCell(new Cell().add(new Paragraph("Clean Code")));
        table.addCell(new Cell().add(new Paragraph("29.90")));

        // A renderer tree is built from the element tree; nothing is drawn yet
        IRenderer root = table.createRendererSubTree();
        // Layout needs a PdfDocument only for fonts; an in-memory one is enough
        try (PdfDocument pdf = new PdfDocument(new PdfWriter(new ByteArrayOutputStream()))) {
            pdf.addNewPage(PageSize.A4);
            Rectangle area = new Rectangle(36, 36, 523, 770);
            LayoutResult result = root.setParent(new Document(pdf).getRenderer())
                    .layout(new LayoutContext(new LayoutArea(1, area)));
            System.out.println("status = " + switch (result.getStatus()) {
                case LayoutResult.FULL -> "FULL";
                case LayoutResult.PARTIAL -> "PARTIAL";
                default -> "NOTHING";
            });
            System.out.println("occupied = " + result.getOccupiedArea().getBBox());
            dump(root, 0);
        }
    }

    static void dump(IRenderer renderer, int depth) {
        System.out.println("  ".repeat(depth) + renderer.getClass().getSimpleName()
                + " <- " + renderer.getModelElement().getClass().getSimpleName());
        renderer.getChildRenderers().forEach(child -> dump(child, depth + 1));
    }
}

Running it prints the following tree. Header and footer rows of a table are laid out by their own sub-renderers, which is why the example uses a table without them.

status = FULL
occupied = Rectangle: 102.90401x22.981995
TableRenderer <- Table
  CellRenderer <- Cell
    ParagraphRenderer <- Paragraph
      TextRenderer <- Text
  CellRenderer <- Cell
    ParagraphRenderer <- Paragraph
      TextRenderer <- Text

This example derives from the renderer chapter of the iText tutorial, Chapter 3: Using renderers and event handlers, and from the API documentation of IRenderer. The tutorial and many forum answers were written for iText 7.0 and show some older signatures; the code above is the compiled truth for 9.8.0 and the classes it names are the ones in the 9.8.0 API.

Replacing a renderer: setNextRenderer and getNextRenderer

Every element has a setNextRenderer(IRenderer) method. It tells the element to use your renderer for its next layout, instead of the default one. The usual recipe has three steps:

  1. subclass the renderer that matches the element: ParagraphRenderer, CellRenderer, TableRenderer, DivRenderer, ImageRenderer, TextRenderer, and so on;

  2. override draw(DrawContext) (or one of its finer-grained helpers, drawBackground, drawBorder, drawChildren) to paint what you need before or after calling super;

  3. override getNextRenderer() so that it returns a new instance of your own class. iText calls it whenever the element is split over several areas (for instance a paragraph that continues on the next page): the overflow part is created through getNextRenderer(), and if you do not override it you silently get the stock renderer for the continuation. This is the most common mistake in custom renderers.

The next example paints a rounded green "PAID" badge behind a paragraph, and logs the layout result of each cell of a table by overriding layout. In draw, the canvas comes from the DrawContext and the position from getOccupiedAreaBBox(), which is valid because draw always runs after layout. saveState() and restoreState() isolate the colour change so that the text drawn by super.draw is not affected.

package com.example.billing;

import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.colors.ColorConstants;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Cell;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import com.itextpdf.layout.layout.LayoutContext;
import com.itextpdf.layout.layout.LayoutResult;
import com.itextpdf.layout.properties.UnitValue;
import com.itextpdf.layout.renderer.CellRenderer;
import com.itextpdf.layout.renderer.DrawContext;
import com.itextpdf.layout.renderer.IRenderer;
import com.itextpdf.layout.renderer.ParagraphRenderer;

import java.io.IOException;
import java.util.Locale;

public class InvoiceRenderers {

    /** Paints a rounded green badge behind a paragraph, then lets the normal drawing run. */
    static class BadgeRenderer extends ParagraphRenderer {
        BadgeRenderer(Paragraph modelElement) {
            super(modelElement);
        }

        @Override
        public IRenderer getNextRenderer() {
            // Required: iText calls this when the paragraph is split across areas
            return new BadgeRenderer((Paragraph) modelElement);
        }

        @Override
        public void draw(DrawContext drawContext) {
            Rectangle box = getOccupiedAreaBBox();
            PdfCanvas canvas = drawContext.getCanvas();
            canvas.saveState()
                    .setFillColor(ColorConstants.GREEN)
                    .roundRectangle(box.getX() - 4, box.getY() - 3, box.getWidth() + 8, box.getHeight() + 6, 6)
                    .fill()
                    .restoreState();
            super.draw(drawContext);
        }
    }

    /** Logs the layout result of every cell of a table, to show what layout() returns. */
    static class LoggingCellRenderer extends CellRenderer {
        LoggingCellRenderer(Cell modelElement) {
            super(modelElement);
        }

        @Override
        public LayoutResult layout(LayoutContext layoutContext) {
            LayoutResult result = super.layout(layoutContext);
            String status = switch (result.getStatus()) {
                case LayoutResult.FULL -> "FULL";
                case LayoutResult.PARTIAL -> "PARTIAL";
                default -> "NOTHING";
            };
            System.out.printf(Locale.ROOT, "cell (%d,%d) -> %s, height %.1f%n", ((Cell) getModelElement()).getRow(),
                    ((Cell) getModelElement()).getCol(), status, result.getOccupiedArea().getBBox().getHeight());
            return result;
        }

        @Override
        public IRenderer getNextRenderer() {
            return new LoggingCellRenderer((Cell) modelElement);
        }
    }

    public static void main(String[] args) throws IOException {
        try (PdfDocument pdf = new PdfDocument(new PdfWriter("renderers.pdf"));
             Document document = new Document(pdf)) {

            PdfFont bold = PdfFontFactory.createFont(StandardFonts.HELVETICA_BOLD);
            Paragraph paid = new Paragraph("PAID").setFont(bold);
            paid.setNextRenderer(new BadgeRenderer(paid));
            document.add(new Paragraph("Invoice BB-2026-0042"));
            document.add(paid);

            Table table = new Table(UnitValue.createPercentArray(new float[] {7, 3})).useAllAvailableWidth();
            for (String text : new String[] {"Clean Code", "29.90", "Effective Java", "39.90"}) {
                Cell cell = new Cell().add(new Paragraph(text));
                cell.setNextRenderer(new LoggingCellRenderer(cell));
                table.addCell(cell);
            }
            document.add(table);
        }
    }
}

The program prints one line per cell, for example cell (0,0) -> FULL, height 22.0. The rendered page shows the green rounded badge behind the word PAID and a two-column table below it. The pattern derives from the KB tutorial Chapter 3: Using renderers and event handlers and the API documents ParagraphRenderer, CellRenderer and DrawContext.

A renderer is not limited to decorating. AbstractRenderer offers getProperty, setProperty and getModelElement, so a renderer can read custom properties set on its element, and can wrap or replace the children it draws. When you only want a different look for many elements of the same kind, a renderer is still the right tool; when you only want a different colour or border for one element, use the element’s own setters.

Layout results and custom layout

The layout(LayoutContext) method of a renderer is where measuring happens. The LayoutContext carries a LayoutArea: the page number and a Rectangle with the space still free. The method returns a LayoutResult, whose status tells the caller what happened:

Status Meaning What the caller does

LayoutResult.FULL

The whole renderer fitted into the area.

Keeps the occupied area and goes on with the next element.

LayoutResult.PARTIAL

Part of the content fitted. The result carries the split renderer (what fitted) and the overflow renderer (what is left).

Draws the split renderer, starts a new area (next column or next page) and lays out the overflow renderer there.

LayoutResult.NOTHING

Nothing fitted. The result carries only an overflow renderer.

Moves to the next area and lays out the whole renderer again.

The factory arguments of LayoutResult are status, the occupied area, the split renderer, the overflow renderer, and, for NOTHING, optionally the renderer that caused the failure. Because the layout engine only looks at that result, overriding layout lets you change the splitting policy. The example below makes a paragraph indivisible: if a normal layout would split it (PARTIAL), it reports NOTHING with itself as the overflow, so the entire paragraph moves to the next page. This is what the setKeepTogether(true) property of the element does for the simple case; writing the renderer yourself is useful when the rule is more specific, for instance "keep together only if at most six lines".

package com.example.billing;

import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.layout.LayoutContext;
import com.itextpdf.layout.layout.LayoutResult;
import com.itextpdf.layout.renderer.IRenderer;
import com.itextpdf.layout.renderer.ParagraphRenderer;

import java.io.IOException;

public class NoSplit {

    /** A paragraph that is never split: if it only partly fits, the whole paragraph moves on. */
    static class NoSplitParagraphRenderer extends ParagraphRenderer {
        NoSplitParagraphRenderer(Paragraph modelElement) {
            super(modelElement);
        }

        @Override
        public LayoutResult layout(LayoutContext layoutContext) {
            LayoutResult result = super.layout(layoutContext);
            if (result.getStatus() == LayoutResult.PARTIAL) {
                // NOTHING: no split part, the overflow renderer is the whole of this renderer
                return new LayoutResult(LayoutResult.NOTHING, null, null, this);
            }
            return result;
        }

        @Override
        public IRenderer getNextRenderer() {
            return new NoSplitParagraphRenderer((Paragraph) modelElement);
        }
    }

    public static void main(String[] args) throws IOException {
        try (PdfDocument pdf = new PdfDocument(new PdfWriter("no-split.pdf"));
             Document document = new Document(pdf)) {
            for (int i = 1; i <= 26; i++) {
                document.add(new Paragraph("Filler line " + i));
            }
            Paragraph clause = new Paragraph("Clause 9. ".repeat(60));
            clause.setNextRenderer(new NoSplitParagraphRenderer(clause));
            document.add(clause);
            System.out.println("pages = " + pdf.getNumberOfPages());
        }
    }
}

With 26 filler lines the page has room for four more lines. Without the custom renderer, four lines of the clause stay on page 1 and the rest continues on page 2; with it, the clause starts on page 2 and page 1 ends after the filler. The mechanism is described in the API documentation of LayoutResult and LayoutContext.

Two further customisation points belong to the same family. A renderer can override getOccupiedAreaBBox()-related helpers such as applyMargins, applyPaddings and applyBorderBox when its drawing must respect the box model, as the border example below does. And the root renderer can be replaced, which is the subject of the next section.

Multi-column layout with ColumnDocumentRenderer

ColumnDocumentRenderer (package com.itextpdf.layout, not com.itextpdf.layout.renderer: a frequent import slip, because DocumentRenderer sits in the renderer package) is a DocumentRenderer whose areas are not the whole page but a list of rectangles that you provide. The renderer fills the first rectangle, then the second, and when the last one is full it starts a new page and begins again with the first. It replaces both ColumnText and MultiColumnText of the old API: content is added with the ordinary document.add(...), text is split between columns by the same LayoutResult mechanism, and a paragraph can continue from one column to the next.

You install the renderer with document.setRenderer(...). Elements added before the call keep the previous flow, and you can switch back with a plain DocumentRenderer later, which is useful for a title across the full width followed by body text in columns, or the other way round. The example below writes a title in a single column, switches to three columns for the terms and conditions of the store, and then returns to a single column on a fresh page for a signature line. The column rectangles are in default user space units (points), with the origin at the bottom left of the page. The first two arguments of each rectangle are the lower-left corner, then width and height, and the height reserves room for the title above the columns.

package com.example.billing;

import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.AreaBreak;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.properties.AreaBreakType;
import com.itextpdf.layout.properties.TextAlignment;
import com.itextpdf.layout.ColumnDocumentRenderer;
import com.itextpdf.layout.renderer.DocumentRenderer;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class Columns {

    public static void main(String[] args) throws IOException {
        try (PdfDocument pdf = new PdfDocument(new PdfWriter("columns.pdf"));
             Document document = new Document(pdf, PageSize.A4)) {

            document.add(new Paragraph("Bookshelf: terms and conditions").setFontSize(16));

            // Switch from the default one-column renderer to a three-column one
            float gutter = 20;
            Rectangle page = PageSize.A4;
            float columnWidth = (page.getWidth() - 72 - 2 * gutter) / 3;
            List<Rectangle> columns = new ArrayList<>();
            for (int i = 0; i < 3; i++) {
                columns.add(new Rectangle(36 + i * (columnWidth + gutter), 36, columnWidth, page.getHeight() - 36 - 90));
            }
            document.setRenderer(new ColumnDocumentRenderer(document, columns.toArray(new Rectangle[0])));
            for (int i = 1; i <= 40; i++) {
                document.add(new Paragraph(("Clause %d. The buyer may return unopened books within 14 days of delivery "
                        + "for a full refund of the purchase price.").formatted(i))
                        .setTextAlignment(TextAlignment.JUSTIFIED).setFontSize(9));
            }

            // Go back to the single-column flow on a fresh page
            document.add(new AreaBreak(AreaBreakType.NEXT_PAGE));
            document.setRenderer(new DocumentRenderer(document));
            document.add(new Paragraph("Signature: ______________________"));
        }
    }
}

With the 40 clauses of the example, the three columns of the first page fill with clauses 1 to 35 and the remaining five continue at the top of the first column of page 2; the signature line is on page 3. The two-argument constructor ColumnDocumentRenderer(Document, Rectangle[]) creates new pages using the document’s page size; the three-argument form takes a boolean immediateFlush as second argument, which writes pages to the output as they complete in large documents. The behaviour derives from the API documentation of ColumnDocumentRenderer and DocumentRenderer.

For anything more irregular than equal columns, subclass DocumentRenderer or ColumnDocumentRenderer and override updateCurrentArea(LayoutResult), which decides the next area every time the current one is full; the rectangles may differ from page to page, for instance to leave room for an image on the first page. The areas stay rectangles, which is the main difference from the free-form column shapes that the old ColumnText supported.

The 9.8.0 property set also contains Property.COLUMN_COUNT and Property.COLUMN_GAP, and the renderer package has a MulticolRenderer, which are aimed at CSS multi-column content. They take effect on a MulticolContainer (in com.itextpdf.layout.element), the Div subclass whose renderer is the MulticolRenderer, which is what pdfHTML uses for CSS multi-column layout. In the test for this page a plain layout Div given only those two properties was not flowed into columns, because a plain Div does not use that renderer, so this page relies on ColumnDocumentRenderer for programmatic layouts and leaves multi-column CSS to HTML conversion.

Rounded corners and custom borders

The easy case needs no renderer. In iText 9 block elements such as Div, Paragraph and Cell support setBorderRadius(BorderRadius), and the radius is applied to the border and the background of the element. Use new BorderRadius(8) for a uniform radius in points, or the per-corner properties (Property.BORDER_TOP_LEFT_RADIUS and its three siblings) when the corners differ. The first Div of the next example is a total box with a blue border, a rounded outline and a translucent grey background.

The second Div uses a renderer to add something no property provides, a second thin frame inside the border. It overrides drawBorder, calls super.drawBorder to paint the normal border (with its own radius), and then draws the inner frame on the same canvas. Because getOccupiedAreaBBox() includes the margins of the element, the code removes them with applyMargins(rect, false) before computing the inner rectangle; false means "subtract" and true means "add". Skipping that step is the usual reason for a custom frame that appears shifted by the margin.

package com.example.billing;

import com.itextpdf.kernel.colors.ColorConstants;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.borders.SolidBorder;
import com.itextpdf.layout.element.Div;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.properties.BorderRadius;
import com.itextpdf.layout.renderer.DivRenderer;
import com.itextpdf.layout.renderer.DrawContext;
import com.itextpdf.layout.renderer.IRenderer;

import java.io.IOException;

public class Rounded {

    /** Draws a second, inner frame after the normal border. */
    static class DoubleFrameRenderer extends DivRenderer {
        DoubleFrameRenderer(Div modelElement) {
            super(modelElement);
        }

        @Override
        public IRenderer getNextRenderer() {
            return new DoubleFrameRenderer((Div) modelElement);
        }

        @Override
        public void drawBorder(DrawContext drawContext) {
            super.drawBorder(drawContext);
            var box = applyMargins(getOccupiedAreaBBox().clone(), false);
            drawContext.getCanvas().saveState()
                    .setStrokeColor(ColorConstants.DARK_GRAY).setLineWidth(0.5f)
                    .roundRectangle(box.getX() + 4, box.getY() + 4, box.getWidth() - 8, box.getHeight() - 8, 4)
                    .stroke().restoreState();
        }
    }

    public static void main(String[] args) throws IOException {
        try (PdfDocument pdf = new PdfDocument(new PdfWriter("rounded.pdf"));
             Document document = new Document(pdf)) {

            Div card = new Div().add(new Paragraph("Total due: 69.80 EUR"))
                    .setBorder(new SolidBorder(ColorConstants.BLUE, 1.5f))
                    .setBorderRadius(new BorderRadius(8))
                    .setBackgroundColor(ColorConstants.LIGHT_GRAY, 0.3f)
                    .setPadding(12);
            document.add(card);

            Div framed = new Div().add(new Paragraph("Thank you for your order"))
                    .setBorder(new SolidBorder(1)).setPadding(12).setMarginTop(20)
                    .setBorderRadius(new BorderRadius(10));
            framed.setNextRenderer(new DoubleFrameRenderer(framed));
            document.add(framed);
        }
    }
}

Rendered, the first box has rounded blue corners and a light background and the second one a rounded black border with a thin double frame inside. For borders that are a different kind of line, rather than an additional drawing, subclass the abstract class Border: it declares draw(PdfCanvas, ...) for the four sides, drawCellBorder for table cells and getType(); the ready-made SolidBorder, DashedBorder, DottedBorder, DoubleBorder, RoundDotsBorder and the groove, ridge, inset and outset borders are examples to copy. A custom Border is assigned with setBorder like any other, so the element needs no renderer at all. The table-specific variant, drawing different borders for header cells or for the outer frame, is shown in the KB page Table and cell events to draw borders, which still talks about "events" but whose iText 7+ code is a cell or table renderer exactly as above. The classes used are documented in BorderRadius and AbstractRenderer.

Choosing the right extension point

You want to Use Why

A different colour, border, margin or font for one element

The element’s setters

No renderer needed.

Rounded corners

setBorderRadius(new BorderRadius(r))

Built in; no renderer needed.

Extra painting behind or around an element

A renderer subclass overriding draw, drawBackground or drawBorder

You get the canvas and the final occupied area.

A different splitting rule

A renderer subclass overriding layout

The LayoutResult is the only thing the engine looks at.

Text in several columns

ColumnDocumentRenderer

Replaces ColumnText and MultiColumnText.

Irregular areas, or different columns per page

A DocumentRenderer subclass overriding updateCurrentArea

The root renderer chooses every next area.

Header, footer, "page X of Y", watermark

A page event handler

Not tied to an element; see the page events page of this section.

A line style that is not solid, dashed or dotted

A Border subclass

It is reusable for every side and for table cells.

References