Search code examples
itextitext7

Text element with graphical text effects


I want create Text elements (com.itextpdf.layout.element.Text) with different kind of additional graphical effects, for example a drop shadow or adding some kind of texture on the glyphs. (see DropShadow some 3D Effect) What is the best way of achiving this?

The best idea i had so far, is using the clipping Text Rendering mode. (Defined in PDF 32000-1 9.3.6; com.itextpdf.kernel.pdf.canvas.PdfCanvasConstants.TextRenderingMode). Drawing the Text as clipping boundary and the apply some kind of Texture or drawing an additional shadow "layer". However the clipping path is restored to the state before text drawing with canvas.restoreState(), which is called at the end in com.itextpdf.layout.renderer.TextRender#draw. Extending this to a custom TextRenderer could work, but the draw function is a big one with some calls to private functions of TextRenderer.

Any adivces for other possible methods?


Solution

  • I think in general customization of that level will require quite come code anyway. Completely overriding draw may indeed not work because some private implementation details are not exposed to the public. One option is of course to duplicate those implementation details into your custom renderer.

    Another idea is to plug into the PdfCanvas which does low-level drawing. You can create your own wrapper like the following one and delegate all operations to the PdfCanvas instance you wrap around except a couple of "interesting" operations where you will customize the logic and apply some styling:

    private static class PdfCanvasWrapper extends PdfCanvas {
        private PdfCanvas delegate;
        public PdfCanvasWrapper(PdfCanvas wrapped) {
            super(wrapped.getContentStream(), wrapped.getResources(), wrapped.getDocument());
            this.delegate = wrapped;
        }
    
        // "Interesting" methods
        @Override
        public PdfCanvas endText() {
            delegate.endText();
            delegate.setFillColor(ColorConstants.BLACK);
            delegate.rectangle(10, 10, 300, 300);
            delegate.fill();
            return this;
        }
    
        // "Boring" methods - just delegate the implementation to the wrapped instance
        @Override
        public PdfCanvas beginVariableText() {
            delegate.beginVariableText();
            return this;
        }
    
        @Override
        public PdfCanvas endVariableText() {
            delegate.endVariableText();
            return this;
        }
    
        // Override all other members like above
    }
    

    In this case your custom text renderer will only plug in the right DrawContext but use the default draw operation:

    private static class CustomTextRenderer extends TextRenderer {
        public CustomTextRenderer(Text textElement) {
            super(textElement);
        }
    
        @Override
        public void draw(DrawContext drawContext) {
            DrawContext newContext = new DrawContext(drawContext.getDocument(), new PdfCanvasWrapper(drawContext.getCanvas()));
            super.draw(newContext);
        }
    
        @Override
        public CustomTextRenderer getNextRenderer() {
            return new CustomTextRenderer((Text) modelElement);
        }
    }
    

    Main could could look like this:

    Paragraph p = new Paragraph();
    Text text = new Text("Hello");
    text.setTextRenderingMode(TextRenderingMode.CLIP);
    text.setNextRenderer(new CustomTextRenderer(text));
    p.add(text);
    

    In general this approach is also hacky and of course depends on the implementation details as much as the initial approach you suggested. The approach you suggested is a more stable one but requires more code and probably more tuning when you update to the new version of the library. The approach I described above is more hacky but it results in less business logic copy-pasting and maybe easier to maintain.