Search code examples
itextitext7

iText 7 Add Annotation at a specific position


I convert HTML to PDF using iText7 and need to add Text Markup Annotations for specific text in the HTML. I am using CustomTagWorkers as explained in this link and then I am using the annotation examples given here.

I am able to successfully add Link Annotation by replacing the qr tag with a link annotation. However my requirement is to add a Text Markup Annotation. The Text Markup annotation can only be drawn by giving the specific coordinates of the page (rectangle object) which I do not know in the code. I tried to give Rectangle(0, 0) hoping that iText will render this in place of tag. However I am unable to add the Text Markup Annotation to paragraph object which is the return object of public IPropertyContainer getElementResult() {.

Here is my entire code:

/**
 * Converts an HTML file to a PDF document, introducing a custom tag to create a
 * QR Code involving a custom TagWorker and a custom CssApplier.
 */
public class C05E04_QRCode2 {

    /**
     * The path to the resulting PDF file.
     */
    public static final String DEST = "C:\\Samples\\itext-annotation\\qrcode.pdf";

    /**
     * The path to the source HTML file.
     */
    public static final String SRC = "C:\\Samples\\itext-annotation\\qrcode.html";

    /**
     * The main method of this example.
     *
     * @param args no arguments are needed to run this example.
     * @throws IOException signals that an I/O exception has occurred.
     */
    public static void main(String[] args) throws IOException {
        File file = new File(DEST);
        file.getParentFile().mkdirs();

        C05E04_QRCode2 app = new C05E04_QRCode2();
        System.out.println("pdf");
        app.createPdf(SRC, DEST);
    }

    /**
     * Creates the PDF file.
     *
     * @param src  the path to the source HTML file
     * @param dest the path to the resulting PDF
     * @throws IOException signals that an I/O exception has occurred.
     */
    public void createPdf(String src, String dest) throws IOException {
        ConverterProperties properties = new ConverterProperties();
        properties.setCssApplierFactory(new QRCodeTagCssApplierFactory())
                .setTagWorkerFactory(new QRCodeTagWorkerFactory());
        HtmlConverter.convertToPdf(new File(src), new File(dest), properties);
    }

    /**
     * A factory for creating QRCodeTagCssApplier objects.
     */
    class QRCodeTagCssApplierFactory extends DefaultCssApplierFactory {

        /**
         * Gets the custom css applier.
         *
         * @param tag the tag
         * @return the custom css applier
         */
        /*
         * (non-Javadoc)
         * 
         * @see com.itextpdf.html2pdf.css.apply.impl.DefaultCssApplierFactory#
         * getCustomCssApplier(com.itextpdf.html2pdf.html.node.IElementNode)
         */
        @Override
        public ICssApplier getCustomCssApplier(IElementNode tag) {
            if (tag.name().equals("qr")) {
                return new BlockCssApplier();
            }
            return null;
        }
    }

    /**
     * A factory for creating QRCodeTagWorker objects.
     */
    class QRCodeTagWorkerFactory extends DefaultTagWorkerFactory {
        
        /**
         * Gets the custom tag worker.
         *
         * @param tag the tag
         * @param context the context
         * @return the custom tag worker
         */
        @Override
        public ITagWorker getCustomTagWorker(IElementNode tag, ProcessorContext context) {
            if (tag.name().equals("qr")) {
                return new QRCodeTagWorker(tag, context);
            }
            return null;
        }
    }

    /**
     * The custom ITagWorker implementation for the qr-tag.
     */
    static class QRCodeTagWorker implements ITagWorker {
        
        /** The p. */
        private Paragraph p;
        
        /** The annotation. */
        private PdfLinkAnnotation linkAnnotation;

        /**
         * Instantiates a new QR code tag worker.
         *
         * @param element the element
         * @param context the context
         */
        public QRCodeTagWorker(IElementNode element, ProcessorContext context) {
            
        }

        /**
         * Process content.
         *
         * @param content the content
         * @param context the context
         * @return true, if successful
         */
        @Override
        public boolean processContent(String content, ProcessorContext context) {
            return true;
        }

        /**
         * Process tag child.
         *
         * @param childTagWorker the child tag worker
         * @param context the context
         * @return true, if successful
         */
        @Override
        public boolean processTagChild(ITagWorker childTagWorker, ProcessorContext context) {
            return false;
        }

        /**
         * Process end.
         *
         * @param element the element
         * @param context the context
         */
        @Override
        public void processEnd(IElementNode element, ProcessorContext context) { 
            
            //Link Annotation
            linkAnnotation = new PdfLinkAnnotation(new Rectangle(0, 0)).setAction(PdfAction.createURI(
                    "https://kb.itextpdf.com/"));
            Link link = new Link("here", linkAnnotation);
            p = new Paragraph("The example of link annotation. Click ").add(link.setUnderline())
                    .add(" to learn more...");
            
            
            //Line Annotation

            float[] floatArray = new float[] { 169, 790, 105, 790, 169, 800, 105, 800 };

            PdfAnnotation lineAnnotation = PdfTextMarkupAnnotation.createHighLight(new Rectangle(0, 0), floatArray);
            lineAnnotation.setTitle(new PdfString("You are here:"));
            lineAnnotation.setContents("Cambridge Innovation Center");
            lineAnnotation.setColor(ColorConstants.YELLOW); 
            
            //Text Markup Annotation
            PdfAnnotation ann = PdfTextMarkupAnnotation.createHighLight(
                    new Rectangle(105, 790, 64, 10),
                    new float[]{169, 790, 105, 790, 169, 800, 105, 800})
                .setColor(Color.YELLOW)
                .setTitle(new PdfString("Hello!"))
                .setContents(new PdfString("I'm a popup."))
                .setTitle(new PdfString("iText"))
                .setOpen(true)
                .setRectangle(new PdfArray(new float[]{100, 600, 200, 100}));           

        }

        /**
         * Gets the element result.
         *
         * @return the element result
         */
        @Override
        public IPropertyContainer getElementResult() {
            return p;
        }
    }

}

HTML File:

 <html>    
    <head>
        <meta charset="UTF-8">
        <title>QRCode Example</title>
        <link rel="stylesheet" type="text/css" href="css/qrcode.css"/>
    </head>
    <body>
    <span> The example of <qr> text markup </qr> annotation. </span>
    </body>
    </html>

CSS File:

qr {
    border: solid 1px red;
    height: 200px;
    width: 200px;
}

Illustration of the final output

enter image description here


Solution

  • The easiest pragmatic way to proceed is make your <qr> tag behave as inline-block. This will make sure that we will not have the text from the tag wrap to the next line (in this case annotation is tricky to define), and also we will have a natural grouping element that we will be able to fetch the coordinates from.

    I have modified the input HTML slightly, to get rid of <qr> tag in favor of <annot> and add the above mentioned display: inline-block behavior:

    <html>
    <head>
      <style>
        annot {
          display: inline-block;
        }
      </style>
    </head>
    <body>
    <span> The example of <annot> text markup </annot> annotation. </span>
    </body>
    </html>
    

    We proceed with the definition of the custom tag worker factory and custom tag worker in the similar fashion as in the original post:

    private static class CustomTagWorkerFactory extends DefaultTagWorkerFactory {
        @Override
        public ITagWorker getCustomTagWorker(IElementNode tag, ProcessorContext context) {
            if ("annot".equals(tag.name())) {
                return new AnnotTagWorker(tag, context);
            }
            return super.getCustomTagWorker(tag, context);
        }
    }
    
    private static class AnnotTagWorker extends PTagWorker {
        public AnnotTagWorker(IElementNode element, ProcessorContext context) {
            super(element, context);
        }
    
        @Override
        public IPropertyContainer getElementResult() {
            IPropertyContainer baseResult = super.getElementResult();
            if (baseResult instanceof Paragraph) {
                ((Paragraph) baseResult).setNextRenderer(new AnnotTagRenderer((Paragraph) baseResult));
            }
            return baseResult;
        }
    }
    

    Now the meaty part will be implemented in AnnotTagRenderer. Notice how we set the renderer to the resultant element in AnnotTagWorker. The renedrer implementation is aware of the physical coordinates of the text block corresponding to our <annot> tag on the PDF page. We can now fetch the coordinates from this.getOccupiedArea() in draw() method and this does the main trick for us - all we need to do as a remainder is create the right annotation object and add it to the page. Here is the implementation:

    private static class AnnotTagRenderer extends ParagraphRenderer {
        public AnnotTagRenderer(Paragraph modelElement) {
            super(modelElement);
        }
    
        @Override
        public IRenderer getNextRenderer() {
            return new AnnotTagRenderer((Paragraph) modelElement);
        }
    
        @Override
        public void draw(DrawContext drawContext) {
            super.draw(drawContext);
    
            Rectangle occupiedArea = this.getOccupiedAreaBBox();
            float[] quadPoints = new float[] {occupiedArea.getLeft(), occupiedArea.getTop(), occupiedArea.getRight(), occupiedArea.getTop(),
                    occupiedArea.getLeft(), occupiedArea.getBottom(), occupiedArea.getRight(), occupiedArea.getBottom()};
            PdfAnnotation ann = PdfTextMarkupAnnotation.createHighLight(
                            new Rectangle(occupiedArea), quadPoints)
                    .setColor(ColorConstants.YELLOW)
                    .setTitle(new PdfString("Hello!"))
                    .setContents(new PdfString("I'm a popup."))
                    .setTitle(new PdfString("iText"));
    
            drawContext.getDocument().getPage(this.getOccupiedArea().getPageNumber()).addAnnotation(ann);
        }
    }
    

    Visual result looks as follows:

    result

    Finally, don't forget to plug the custom tag worker factory into the converter properties:

    ConverterProperties properties = new ConverterProperties().setTagWorkerFactory(new CustomTagWorkerFactory());
    HtmlConverter.convertToPdf(new File(sourceHTML), new File(targetPDF), properties);