Search code examples
opacityitext7

iText Background Opacity


I want to overlay a text with semi-transparent background over an existing text using iText 7. Setting the background opacity for a text element doesn't seem to work (line 1), I can only set it for the whole paragraph (line 2):

enter image description here

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.element.Paragraph;
import com.itextpdf.layout.element.Text;
import com.itextpdf.layout.property.TextAlignment;
import com.itextpdf.layout.property.VerticalAlignment;
import java.io.IOException;

public class TextBackgroundOpacityTest {

    public static void main(String[] args) throws IOException {

        try (Document doc = new Document( new PdfDocument(new PdfWriter("TextBackgroundOpacityTest.pdf")))) {
            doc.add(new Paragraph(new String(new char[130]).replace("\0", "A")));

            // opacity doesn't work for text element
            doc.showTextAligned(new Paragraph(new Text("missing background transparency").setBackgroundColor(ColorConstants.WHITE, .8f)), 500, 805, 0, TextAlignment.RIGHT, VerticalAlignment.TOP, 0);

            // opacity for the whole paragraph works, but this is not what I want
            doc.showTextAligned(new Paragraph("whole pharagraph background transparancy").setBackgroundColor(ColorConstants.WHITE, .8f), 500, 785, 0, TextAlignment.RIGHT, VerticalAlignment.TOP, 0);
        }
    }    
}

How can I overlay a text with a semi-transparent background as show in line 2, but just for the overlayed text, not the whole paragraph? Desired output: enter image description here


Solution

  • To work around the solution you can use custom renderers. If you look at the BlockRenderer#drawBackground which is called in case you set transparent background to a paragraph you can see the following lines there:

    TransparentColor backgroundColor = new TransparentColor(background.getColor(), background.getOpacity());
    drawContext.getCanvas().saveState().setFillColor(backgroundColor.getColor());
    backgroundColor.applyFillTransparency(drawContext.getCanvas());
    

    TextRenderer, however, has its own implementation and does not respect transparent background. But we can customize the renderer implementation. We'll need to copy-paste quite a bit of code from the current TextRenderer implementation, but the good news is that we don't need to change a lot of code. Just insert two lines in the right place:

    TransparentColor backgroundColor = new TransparentColor(background.getColor(), background.getOpacity());
    backgroundColor.applyFillTransparency(drawContext.getCanvas());
    

    Overall we get the following implementation:

    private static class TextRendererWithBackgroundOpacity extends TextRenderer {
        public TextRendererWithBackgroundOpacity(Text textElement) {
            super(textElement);
        }
    
        @Override
        public void drawBackground(DrawContext drawContext) {
            Background background = this.<Background>getProperty(Property.BACKGROUND);
            Float textRise = this.getPropertyAsFloat(Property.TEXT_RISE);
            Rectangle bBox = getOccupiedAreaBBox();
            Rectangle backgroundArea = applyMargins(bBox, false);
            float bottomBBoxY = backgroundArea.getY();
            float leftBBoxX = backgroundArea.getX();
            if (background != null) {
                boolean isTagged = drawContext.isTaggingEnabled();
                PdfCanvas canvas = drawContext.getCanvas();
                if (isTagged) {
                    canvas.openTag(new CanvasArtifact());
                }
                boolean backgroundAreaIsClipped = clipBackgroundArea(drawContext, backgroundArea);
    
                canvas.saveState().setFillColor(background.getColor());
                TransparentColor backgroundColor = new TransparentColor(background.getColor(), background.getOpacity());
                backgroundColor.applyFillTransparency(drawContext.getCanvas());
    
                canvas.rectangle(leftBBoxX - background.getExtraLeft(), bottomBBoxY + (float) textRise - background.getExtraBottom(),
                        backgroundArea.getWidth() + background.getExtraLeft() + background.getExtraRight(),
                        backgroundArea.getHeight() - (float) textRise + background.getExtraTop() + background.getExtraBottom());
                canvas.fill().restoreState();
                if (backgroundAreaIsClipped) {
                    drawContext.getCanvas().restoreState();
                }
                if (isTagged) {
                    canvas.closeTag();
                }
            }
        }
    
        @Override
        public IRenderer getNextRenderer() {
            return new TextRendererWithBackgroundOpacity((Text)modelElement);
        }
    }
    

    To make Text element use the custom renderer implementation just call setNextRenderer method:

    Text customTextElement = new Text("missing background transparency");
    customTextElement.setNextRenderer(new TextRendererWithBackgroundOpacity(customTextElement));
    

    By the way you are very welcome to file the fix as a pull request to iText (please follow the contribution guidelines though). The repository is located at https://github.com/itext/itext7