suushiemaniac
suushiemaniac

Reputation: 47

itext7 - Maximum font size without clipping content

EDIT in bold

I have a table where several cells contain Paragraphs with some arbitrarily long text. The table width is determined via useAllAvailableWidth, and I am also invoking setAutoLayout.

I am using the Renderer framework to set the Paragraph's font size to the largest possible value without clipping off any content.

In particular I wish to achieve similar results to iText Maximum Font Size, however that questions was written for itext5. I am using itext7.

I have read this sample, and I have come up with the following (partial) solution thanks to a previous answer:

class FontSizeRenderer(val content: Paragraph) : ParagraphRenderer(content) {
    override fun getNextRenderer() = FontSizeRenderer(content)

    override fun layout(layoutContext: LayoutContext?): LayoutResult {
        val currentFontSize = content.getProperty<UnitValue>(Property.FONT_SIZE).value

        return layoutBinarySearch(layoutContext, 1f, currentFontSize, 20)
    }

    private tailrec fun layoutBinarySearch(layoutContext: LayoutContext?, minFontSize: Float, maxFontSize: Float, iterationThreshold: Int): LayoutResult {
        val currentLayout = super.layout(layoutContext)

        if (iterationThreshold <= 0) {
            return currentLayout
        }

        val currentFontSize = content.getProperty<UnitValue>(Property.FONT_SIZE).value

        return if (currentLayout.status == LayoutResult.FULL) {
            val increment = (currentFontSize + maxFontSize) / 2
            content.setFontSize(increment)

            layoutBinarySearch(layoutContext, currentFontSize, maxFontSize, iterationThreshold - 1)
        } else {
            val decrement = (minFontSize + currentFontSize) / 2
            content.setFontSize(decrement)

            layoutBinarySearch(layoutContext, minFontSize, currentFontSize, iterationThreshold - 1)
        }
    }
}

When using this renderer in a fully-fledged table, it "kinda works" in a sense that it starts to recurse but it stops too early.

broken end result

The expected output string in the bottom cell on the first page is Scramble: R' U' F R F2 D2 R' B2 U2 R F2 R' B2 R' B F U' L2 B' R' B' U' R F2 R' U' F. The full code sample can be inspected (and downloaded) at this repository, under webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf, files FmcSolutionSheet.kt and util/FooRenderer.kt.

How can I adjust my Renderer to prevent the Cell from overflowing?

Upvotes: 0

Views: 864

Answers (1)

Alexey Subach
Alexey Subach

Reputation: 12312

Having a layout element you can create a renderer subtree out of it and try to lay it out in a given area. Then depending on whether the element fit completely in the area or there was not enough space you can increase or decrease your font size and try again.

To test out different font sizes, binary search is our best friend and allows a giant speed up.

First off, a helper function to set the font size of the element recursively:

private void setFontSizeRecursively(IElement element, float size) {
    element.setProperty(Property.FONT_SIZE, UnitValue.createPointValue(size));
    if (element instanceof com.itextpdf.layout.element.AbstractElement) {
        for (Object child : ((AbstractElement) element).getChildren()) {
            setFontSizeRecursively((IElement) child, size);
        }
    }
}

Now the meaty part of the code. In the binary search body we scale all the font sizes of the elements and emulate layout to see if our root element (table) fits into the given area. Then we shift the left or right search boundary and try again until we converge. You can tweak the number of iterations (set at 20 in the code) to your own taste for the accuracy/speed trade-off. You can also tweak left and right bounds of the binary search if you can make such judgment depending on the expected input. Finally, we set the font size scale we converged at and do the final layout.

PdfDocument pdfDocument = new PdfDocument(new PdfWriter(outFileName));
Document document = new Document(pdfDocument);

Table table = new Table(UnitValue.createPercentArray(new float[] {20, 40, 40}));
for (int i = 0; i < 9; i++) {
    table.addCell(new Cell().add(new Paragraph("Hello random text")));
}

float lFontSize = 1f;
float rFontSize = 100f;

float desiredWidth = 300;
float desiredHeight = 400;

table.setWidth(UnitValue.createPointValue(desiredWidth));

for (int i = 0; i < 20; i++) {
    float midFontSize = (lFontSize + rFontSize) / 2;
    setFontSizeRecursively(table, midFontSize);

    IRenderer tableRenderer = table.createRendererSubTree().setParent(document.getRenderer());
    LayoutResult result = tableRenderer.layout(new LayoutContext(new LayoutArea(1,
            new Rectangle(0, 0, desiredWidth, desiredHeight)))); // You can tweak desired size to fit the table in

    if (result.getStatus() == LayoutResult.FULL) {
        lFontSize = midFontSize;
    } else {
        rFontSize = midFontSize;
    }
}

setFontSizeRecursively(table, lFontSize);
document.add(table);

document.close();

Upvotes: 2

Related Questions