Search code examples
itextitext7

Multi-Line Text Fitting in IText 7


This question is a follow-up from this After the previous post I managed to create the following method that fits text in certain spaces in paragraphs.

public static void getPlainFill2(String str, Document doc, PdfDocument document, Paragraph root,
    Paragraph space, boolean isCentred) {
// System.out.println("prevText: "+prev.getText());
float width = doc.getPageEffectiveArea(PageSize.A4).getWidth();
float height = doc.getPageEffectiveArea(PageSize.A4).getHeight();
if (str.isEmpty() || str.isBlank()) {
    str = "________";
}
IRenderer spaceRenderer = space.createRendererSubTree().setParent(doc.getRenderer());

LayoutResult spaceResult = spaceRenderer
    .layout(new LayoutContext(new LayoutArea(1, new Rectangle(width, height))));

Rectangle rectSpaceBox = ((ParagraphRenderer) spaceRenderer).getOccupiedArea().getBBox();

float writingWidth = rectSpaceBox.getWidth();
float writingHeight = rectSpaceBox.getHeight();

Rectangle remaining = doc.getRenderer().getCurrentArea().getBBox();
float yReal = remaining.getTop() + 2f;// orig 4f

float sizet = 0;
for (int i = 0; i < root.getChildren().size(); i++) {
    IElement e = root.getChildren().get(i);

    if (e.equals(space)) {

    break;
    }

    IRenderer ss = e.createRendererSubTree().setParent(doc.getRenderer());
    
    LayoutResult ss2 = ss.layout(new LayoutContext(new LayoutArea(1, new Rectangle(width, height))));

    sizet += ss.getOccupiedArea().getBBox().getWidth();

    System.out.println("width: " + width + " current: " + sizet);

}
float start =  sizet+doc.getLeftMargin();
 if(isCentred) 
     start = (width - getRealWidth(doc, root,width,height))/2+doc.getLeftMargin()+sizet;
 


Rectangle towr = new Rectangle(start, yReal, writingWidth, writingHeight);// sizet+doc.getLeftMargin()

PdfCanvas pdfcanvas = new PdfCanvas(document.getFirstPage());
Canvas canvas = new Canvas(pdfcanvas, towr);
canvas.setTextAlignment(TextAlignment.CENTER);
canvas.setHorizontalAlignment(HorizontalAlignment.CENTER);

Paragraph paragraph = new Paragraph(str).setTextAlignment(TextAlignment.CENTER).setBold();//.setMultipliedLeading(0.9f);
Div lineDiv = new Div();
lineDiv.setVerticalAlignment(VerticalAlignment.MIDDLE);
lineDiv.add(paragraph);

float fontSizeL = 1f;
float fontSizeR = 12;
int adjust = 0;
while (Math.abs(fontSizeL - fontSizeR) > 1e-1) {
    float curFontSize = (fontSizeL + fontSizeR) / 2;
    lineDiv.setFontSize(curFontSize);
    // It is important to set parent for the current element renderer to a root
    // renderer
    IRenderer renderer = lineDiv.createRendererSubTree().setParent(canvas.getRenderer());
    LayoutContext context = new LayoutContext(new LayoutArea(1, towr));
    if (renderer.layout(context).getStatus() == LayoutResult.FULL) {
    // we can fit all the text with curFontSize
    fontSizeL = curFontSize;
    } else {
    fontSizeR = curFontSize;
    }
    if(adjust>=2) {
    writingHeight -=1.3f;
    yReal += 1.4f;
    adjust= 0;
    }
}

lineDiv.setFontSize(fontSizeL);
canvas.add(lineDiv);
// border
// PdfCanvas(document.getFirstPage()).rectangle(towr).setStrokeColor(ColorConstants.BLACK).stroke();

canvas.close();

}




public static float getRealWidth (Document doc, Paragraph root,float width,float height) {
 float sizet = 0;
    
 for(int  i = 0;i<root.getChildren().size();i++) {
     IElement e =  root.getChildren().get(i);
     
    
        IRenderer ss = e.createRendererSubTree().setParent(doc.getRenderer());
    LayoutResult ss2 = ss.layout(new LayoutContext(new LayoutArea(1, new Rectangle(width,height))));

    sizet +=ss.getOccupiedArea().getBBox().getWidth();


        
    }
return sizet;}

Now this works almost decent, there are minor issues when text scales to lower sizes and it goes like: https://i.ibb.co/MkxfwjQ/Screenshot-from-2021-06-14-18-27-09.png (I can't post images because I have no rep.) but the main issue is that you have to write Paragraphs line by line to work. As next example:

Cell cell3 = new Cell();
        LineCountingParagraph line3 = new LineCountingParagraph("");
        Text ch07 = new Text("Paragraph Prev ");
        line3.add(ch07);
        Paragraph nrZile =  getEmptySpace(15);
        line3.add(nrZile);
        Text ch08 = new Text("afterStr, textasdsadasdas ");
        line3.add(ch08);
        Paragraph data =  getEmptySpace(18);
        line3.add(data);
        Text ch09 = new Text(".\n");
        line3.add(ch09);
        line3.setTextAlignment(TextAlignment.CENTER);
        cell3.add(line3);
        doc.add(cell3);
        getPlainFill2("thisisalongstring", doc, document, line3, nrZile, true);
        getPlainFill2("1333", doc, document, line3, data, true);
         
           Cell cell4 =  new Cell();
            LineCountingParagraph line4 =  new LineCountingParagraph("");
            Paragraph loc2  = getEmptySpace(30);
            line4.add(loc2);
            Text pr32 = new Text(" aasdbsadasd ");
            line4.add(pr32);
            Paragraph nr2 = getEmptySpace(8);
            line4.add(nr2);
            Text pr33 =  new Text(" asdasdasdasd.\n");
            line4.add(pr33);
            line4.setTextAlignment(TextAlignment.CENTER);
            cell4.add(line4);
            doc.add(cell4);
         
            getPlainFill2("1333", doc, document, line4, nr2, true);

If you need more code, I'll upload it somewhere. Now is there a way to insert text within the same paragraph on multiple lines ? because there seems I cannot find a way to detect line break in IText 7.1.11.

Full code:

package pdfFill;

import java.io.File;
import java.io.IOException;

import com.itextpdf.kernel.colors.ColorConstants;
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.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.element.Cell;
import com.itextpdf.layout.element.Div;
import com.itextpdf.layout.element.IElement;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Text;
import com.itextpdf.layout.layout.LayoutArea;
import com.itextpdf.layout.layout.LayoutContext;
import com.itextpdf.layout.layout.LayoutResult;
import com.itextpdf.layout.property.HorizontalAlignment;
import com.itextpdf.layout.property.TextAlignment;
import com.itextpdf.layout.property.VerticalAlignment;
import com.itextpdf.layout.renderer.DrawContext;
import com.itextpdf.layout.renderer.IRenderer;
import com.itextpdf.layout.renderer.ParagraphRenderer;



public class Newway4 {

    public static void main(String[] args) {
        PdfWriter writer;

        try {
            writer = new PdfWriter(new File("test4.pdf"));

            PdfDocument document = new PdfDocument(writer);
             document.getDocumentInfo().addCreationDate();
             document.getDocumentInfo().setAuthor("Piri");
             document.getDocumentInfo().setTitle("Test_Stackoverflow");
             document.setDefaultPageSize(PageSize.A4);
             Document doc =  new Document(document);
             doc.setFontSize(12);
          

            
            
        
            final Paragraph titlu = new Paragraph();
            final Text t1 = new Text("\n\n\n\nTest Stackoverflow\n\n\n").setBold().setUnderline();
            titlu.setHorizontalAlignment(HorizontalAlignment.CENTER);
            titlu.setTextAlignment(TextAlignment.CENTER);
            titlu.add(t1).setBold();
            doc.add(titlu);
            

            Cell cell1 = new Cell();
            LineCountingParagraph line1 = new LineCountingParagraph("");
            line1.add( addTab());
            Text ch01 = new Text("This is the 1st example ");
            line1.add(ch01);
            Paragraph name =  getEmptySpace(42);
            line1.add(name);// cnp new line
            Text ch02 = new Text(" that works ");
            line1.add(ch02);
            Paragraph domiciliu =  getEmptySpace(63);
            line1.add(domiciliu);
            /* Text ch03 = new Text("\njudet");
            line1.add(ch03);
            Paragraph judet =  getEmptySpace(12);
            line1.add(judet);*/
           Text ch031 = new Text("\n");
            line1.add(ch031);
            cell1.add(line1);
            doc.add(cell1);
             getPlainFill2("with insertion str", doc, document, line1, name, false);
             getPlainFill2("because is writtin line by line", doc, document, line1, domiciliu, false);
          


             
             
             Cell cell2 =  new Cell();
                LineCountingParagraph line2 =  new LineCountingParagraph("");
                Text p51 = new Text("as you can see in this");
                line2.add(p51);
                Paragraph localitatea = getEmptySpace(30);
                line2.add(localitatea);
                Text p7 = new Text(" and ");
                line2.add(p7);
                Paragraph nrCasa =getEmptySpace(8);
                line2.add(nrCasa);
                Text p09 = new Text(" of text scalling ");
                line2.add(p09);
                Paragraph telefon = getEmptySpace(22);
                line2.add(telefon);
                Text p11 =  new Text(".");
                line2.add(p11);
                line2.setTextAlignment(TextAlignment.CENTER);
                cell2.add(line2);
                doc.add(cell2);
                getPlainFill2("sentence", doc, document, line2, localitatea, true);
                getPlainFill2("example", doc, document, line2, nrCasa, true);
                getPlainFill2("text scalling bla bla", doc, document, line2, telefon, true);
             
             
             doc.add(new Paragraph("\n\n\n"));
             
             
             LineCountingParagraph paragraphTest =  new LineCountingParagraph("");
             paragraphTest.add(addTab());
             Text testch01 =  new Text("This is the 2nd example ");
             paragraphTest.add(testch01);
             Paragraph emptyTest01 =  getEmptySpace(42);
             paragraphTest.add(emptyTest01);
             Text testch02 =  new Text(" that doesn't work ");
             paragraphTest.add(testch02);
             Paragraph  emptyTest02 =  getEmptySpace(53);
             paragraphTest.add(emptyTest02);
             Text testch04 =  new Text(" this next goes to the next line but ");
             paragraphTest.add(testch04);
             Paragraph emptyTest03 =  getEmptySpace(42);
             paragraphTest.add(emptyTest03);
             Text testch05 =  new Text(" won't appear !!");
             paragraphTest.add(testch05);
             doc.add(paragraphTest);
             getPlainFill2("with insertion str", doc, document, paragraphTest, emptyTest01, false);
             getPlainFill2("because next text goes next line", doc, document, paragraphTest, emptyTest02, false);
             getPlainFill2("this text", doc, document, paragraphTest, emptyTest03, false);
             
            
             
             
        
            doc.close();
            writer.flush();


        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public static String getStrWithDots(final int dots, final String str) {
        final int strSize = str.length();
        final StringBuilder sb = new StringBuilder();
        int dotsRemained;
        if (strSize > dots) {
            dotsRemained = 0;
        } else {
            dotsRemained = dots - strSize;
        }
        for (int i = 0; i < dotsRemained; ++i) {
            if (i == dotsRemained / 2) {
            sb.append(str);
            }
            sb.append(".");
        }
        return sb.toString();
        }
    
      public static void getPlainFill2(String str, Document doc, PdfDocument document, Paragraph root,
                Paragraph space, boolean isCentred) {
            // System.out.println("prevText: "+prev.getText());
            float width = doc.getPageEffectiveArea(PageSize.A4).getWidth();
            float height = doc.getPageEffectiveArea(PageSize.A4).getHeight();
            if (str.isEmpty() || str.isBlank()) {
                str = "________";
            }
            IRenderer spaceRenderer = space.createRendererSubTree().setParent(doc.getRenderer());

            LayoutResult spaceResult = spaceRenderer
                .layout(new LayoutContext(new LayoutArea(1, new Rectangle(width, height))));

            Rectangle rectSpaceBox = ((ParagraphRenderer) spaceRenderer).getOccupiedArea().getBBox();

            float writingWidth = rectSpaceBox.getWidth();
            float writingHeight = rectSpaceBox.getHeight();

            Rectangle remaining = doc.getRenderer().getCurrentArea().getBBox();
            float yReal = remaining.getTop() + 2f;// orig 4f

            float sizet = 0;
            for (int i = 0; i < root.getChildren().size(); i++) {
                IElement e = root.getChildren().get(i);

                if (e.equals(space)) {

                break;
                }

                IRenderer ss = e.createRendererSubTree().setParent(doc.getRenderer());
                
                LayoutResult ss2 = ss.layout(new LayoutContext(new LayoutArea(1, new Rectangle(width, height))));

                sizet += ss.getOccupiedArea().getBBox().getWidth();


            }
            float start =  sizet+doc.getLeftMargin();
             if(isCentred) 
                 start = (width - getRealWidth(doc, root,width,height))/2+doc.getLeftMargin()+sizet;
             
            
            
            Rectangle towr = new Rectangle(start, yReal, writingWidth, writingHeight);// sizet+doc.getLeftMargin()

            PdfCanvas pdfcanvas = new PdfCanvas(document.getFirstPage());
            Canvas canvas = new Canvas(pdfcanvas, towr);
            canvas.setTextAlignment(TextAlignment.CENTER);
            canvas.setHorizontalAlignment(HorizontalAlignment.CENTER);

            Paragraph paragraph = new Paragraph(str).setTextAlignment(TextAlignment.CENTER).setBold();//.setMultipliedLeading(0.9f);//setbold oprtional
            Div lineDiv = new Div();
            lineDiv.setVerticalAlignment(VerticalAlignment.MIDDLE);
            lineDiv.add(paragraph);

            float fontSizeL = 0.0001f, fontSizeR= 10000;
            int adjust = 0;
         
            while (Math.abs(fontSizeL - fontSizeR) > 1e-1) {
                float curFontSize = (fontSizeL + fontSizeR) / 2;
                lineDiv.setFontSize(curFontSize);
                // It is important to set parent for the current element renderer to a root
                // renderer
                IRenderer renderer = lineDiv.createRendererSubTree().setParent(canvas.getRenderer());
                LayoutContext context = new LayoutContext(new LayoutArea(1, towr));
                if (renderer.layout(context).getStatus() == LayoutResult.FULL) {
                // we can fit all the text with curFontSize
                fontSizeL = curFontSize;
                
                   if (++adjust>1)
                       towr.setHeight(towr.getHeight()-0.90f);
                } else {
                fontSizeR = curFontSize;
                }
              
            }

            lineDiv.setFontSize(fontSizeL);
            canvas.add(lineDiv);
    
             new PdfCanvas(document.getFirstPage()).rectangle(towr).setStrokeColor(ColorConstants.BLACK).stroke();

            canvas.close();

            }
   
    public static Text addTab() {
        StringBuilder sb =  new StringBuilder();
        for(int i = 0;i<8;i++)
            sb.append("\u00a0");
        return new Text(sb.toString());
    }
    
    
    
    public static float getRealWidth (Document doc, Paragraph root,float width,float height) {
         float sizet = 0;
            
         for(int  i = 0;i<root.getChildren().size();i++) {
             IElement e =  root.getChildren().get(i);
             
            
                IRenderer ss = e.createRendererSubTree().setParent(doc.getRenderer());
            LayoutResult ss2 = ss.layout(new LayoutContext(new LayoutArea(1, new Rectangle(width,height))));
      
            sizet +=ss.getOccupiedArea().getBBox().getWidth();

        
                
            }
        return sizet;
    }
     
     
   
    

     
     private static Paragraph getEmptySpace(int size) {
          Paragraph space = new Paragraph();
            space.setMaxWidth(size);
            for(int i=0;i<size;i++) {
            //    par.add("\u00a0");
                space.add("\u00a0");
            }
            return space;
     }
     
     
     
     private static class LineCountingParagraph extends Paragraph {
            private int linesWritten = 0;

            public LineCountingParagraph(String text) {
                super(text);
            }

            public void addWrittenLines(int toAdd) {
                linesWritten += toAdd;
            }

            public int getNumberOfWrittenLines() {
                return linesWritten;
            }

            @Override
            protected IRenderer makeNewRenderer() {
                return new LineCountingParagraphRenderer(this);
            }
        }

        private static class LineCountingParagraphRenderer extends ParagraphRenderer {
            public LineCountingParagraphRenderer(LineCountingParagraph modelElement) {
                super(modelElement);
            }

            @Override
            public void drawChildren(DrawContext drawContext) {
                ((LineCountingParagraph)modelElement).addWrittenLines(lines.size());
                super.drawChildren(drawContext);
            }

            @Override
            public IRenderer getNextRenderer() {
                return new LineCountingParagraphRenderer((LineCountingParagraph) modelElement);
            }
        }

}

The issue: in the top half of the PDF you can see the result of two LineCountingParagraph instances being created, one per line. In the bottom half of the PDF you can see the result when only one instance of LineCountingParagraph is created. So fitting the text in boxes does not work well in case content of the paragraph wraps to the next line.

issue


Solution

  • You have unnecessarily complicated things in such a way that we have to start from scratch :)

    So the goal is to be able to create paragraphs with boxed intrusions of fixed width, where we need to copy-fit (place) some text, making sure the font size is selected in such a way that the text fits into that box.

    The result should look similar to this picture: goal - result

    The idea is that we will add paragraphs of fixed width into our main paragraph (iText allows adding block elements and a paragraph is a block element - into paragraphs). The fixed width will be guaranteed by the contents of our paragraph - it will just contain non-breakable spaces. Our paragraph will actually be backed by another paragraph with the actual content we want to fit into our wrapping paragraph. During the layout of the wrapping paragraph we will know its effective boundary and we will just use that area to determine the right font size for our content paragraph using binary search algorithm. Once the right font size has been determined we will just make sure the paragraph with the content gets drawn right next to our wrapping paragraph.

    The code for our wrapping paragraph is pretty simple. It just expects the underlying paragraph with real content as the parameter. As always with iText layout, we should customize the renderer of our autoscaling paragraph:

    private static class AutoScalingParagraph extends Paragraph {
        Paragraph innerParagraph;
    
        public AutoScalingParagraph(Paragraph innerParagraph) {
            this.innerParagraph = innerParagraph;
        }
    
        @Override
        protected IRenderer makeNewRenderer() {
            return new AutoScalingParagraphRenderer(this);
        }
    }
    
    private static class AutoScalingParagraphRenderer extends ParagraphRenderer {
        private IRenderer innerRenderer;
    
        public AutoScalingParagraphRenderer(AutoScalingParagraph modelElement) {
            super(modelElement);
        }
    
        @Override
        public LayoutResult layout(LayoutContext layoutContext) {
            LayoutResult baseResult = super.layout(layoutContext);
            this.innerRenderer = ((AutoScalingParagraph)modelElement).innerParagraph.createRendererSubTree().setParent(this);
            if (baseResult.getStatus() == LayoutResult.FULL) {
                float fontSizeL = 0.0001f, fontSizeR= 10000;
    
                while (Math.abs(fontSizeL - fontSizeR) > 1e-1) {
                    float curFontSize = (fontSizeL + fontSizeR) / 2;
                    this.innerRenderer.setProperty(Property.FONT_SIZE, UnitValue.createPointValue(curFontSize));
    
                    if (this.innerRenderer.layout(new LayoutContext(getOccupiedArea().clone())).getStatus() == LayoutResult.FULL) {
                        // we can fit all the text with curFontSize
                        fontSizeL = curFontSize;
                    } else {
                        fontSizeR = curFontSize;
                    }
                }
                this.innerRenderer.setProperty(Property.FONT_SIZE, UnitValue.createPointValue(fontSizeL));
    
                this.innerRenderer.layout(new LayoutContext(getOccupiedArea().clone()));
            }
            return baseResult;
        }
    
        @Override
        public void drawChildren(DrawContext drawContext) {
            super.drawChildren(drawContext);
            innerRenderer.draw(drawContext);
        }
    
        @Override
        public IRenderer getNextRenderer() {
            return new AutoScalingParagraphRenderer((AutoScalingParagraph) modelElement);
        }
    }
    

    Now we add the helper function for creating our wrapper paragraphs that just accepts the desired paragraph width in spaces and the underlying content we want to fit into that space:

    private static Paragraph createAdjustableParagraph(int widthInSpaces, Paragraph innerContent) {
        AutoScalingParagraph paragraph = new AutoScalingParagraph(innerContent);
        paragraph.setBorder(new SolidBorder(1));
    
        StringBuilder sb = new StringBuilder();
        for(int i=0;i<widthInSpaces;i++) {
            sb.append("\u00a0");
        }
        paragraph.add(sb.toString());
        return paragraph;
    }
    

    Finally, the main code:

    PdfWriter writer = new PdfWriter(new File("test4.pdf"));
    
    PdfDocument document = new PdfDocument(writer);
    Document doc = new Document(document);
    
    Paragraph paragraphTest = new Paragraph();
    Text testch01 =  new Text("This is the 2nd example ");
    paragraphTest.add(testch01);
    paragraphTest.add(createAdjustableParagraph(42, new Paragraph("with insertion str")));
    Text testch02 =  new Text(" that doesn't work ");
    paragraphTest.add(testch02);
    paragraphTest.add(createAdjustableParagraph(53, new Paragraph("because next text goes next line")));
    Text testch04 =  new Text(" this next goes to the next line but ");
    paragraphTest.add(testch04);
    paragraphTest.add(createAdjustableParagraph(42, new Paragraph("this text")));
    Text testch05 =  new Text(" won't appear !!");
    paragraphTest.add(testch05);
    doc.add(paragraphTest);
    
    doc.close();
    

    Which gives us the following result:

    result

    So we just have the one main paragraph which contains some content and our paragraph wrappers, which in tern have the underlying content we want to fit.

    Hint: centering the text is very easy, you don't need to calculate coordinates etc. Just set the right property to the paragraph with the content that you feed to your wrapper paragraph:

    paragraphTest.add(createAdjustableParagraph(42, new Paragraph("this text").setTextAlignment(TextAlignment.CENTER)));
    

    And you get the result:

    centered result