Skip to content

CustomAnnotations

Ralf Stuckert edited this page Feb 25, 2017 · 3 revisions

Custom Annotations and Markup

As already said, this library is drop dead simple, and is all but complete. Sooner or later you will miss something, so with version 0.9.0 comes a way to extend this library: custom annotations. When implementing hyperlinks the rendering mechanism has been extended with some hooks in order to add some surplus content during and even after rendering. So the StyledText has been pimped up to allow adding annotations. These annotations are to be handled by annotation processors. So in case of the hyperlinks, there is a HyperlinkAnnotation which carries information about the link, and a HyperlinkAnnotationProcessor that renders the hyperlink to the PDF. Sounds weird? Don't worry, let's do this example.

Building a Highlight Annotation

We will implement textmarker-like highlighting using annotations. You will find all code in the exammple CustomAnnotations.java, and the resulting PDF is in customannotations.pdf. What we want to achieve is something like this:

custom annotations example

So let's start with the annotation itself. It is just a simple class that implements the marker interface Annotation. It just contains the information additional to the rendered text, namely the color used for highlighting:

public static class HighlightAnnotation implements Annotation {

  private Color color;

  public HighlightAnnotation(Color color) {
    this.color = color;
  }

  public Color getColor() {
    return color;
  }
}

When an annotation is detected, all registered annotation processors get the chance to process it. Currently, there are some built in processors for e.g. hyperlinks. In order to get our highlight annotation handled, we have to write our own processor. We start by implementing the interface AnnotationProcessor:

public static class HighlightAnnotationProcessor implements
                      AnnotationProcessor {

  @Override
  public void annotatedObjectDrawn(Annotated drawnObject,
    DrawContext drawContext, Position upperLeft, float width,
    float height) throws IOException {
    ...

We get passed an annotated object, which has - as you might have already guessed - annotations, a drawing context and some positioning data. So we have to inspect the annotated object for our HighlightAnnotation and process it:

    Iterable<HighlightAnnotation> HighlightAnnotations = drawnObject
        .getAnnotationsOfType(HighlightAnnotation.class);

    for (HighlightAnnotation HighlightAnnotation : HighlightAnnotations) {
      ...
    }

To implement the highlighting, we will use the PDF text markup feature:

      // use PDF text markup to implement the highlight
      PDAnnotationTextMarkup markup = new PDAnnotationTextMarkup(
        PDAnnotationTextMarkup.SUB_TYPE_HIGHLIGHT);

This markup is added to the PDF independent of the content (text) itself, it is just positioned on the page. So we have to calculate the position, and set it on the markup. The CompatibilityHelper provides some helper methods for you:

      // use the bounding box of the drawn object to position the
      // highlight
      PDRectangle bounds = new PDRectangle();
      bounds.setLowerLeftX(upperLeft.getX());
      bounds.setLowerLeftY(upperLeft.getY() - height);
      bounds.setUpperRightX(upperLeft.getX() + width);
      bounds.setUpperRightY(upperLeft.getY() + 1);
      markup.setRectangle(bounds);
      float[] quadPoints = CompatibilityHelper.toQuadPoints(bounds);
      quadPoints = CompatibilityHelper.transformToPageRotation(
                 quadPoints, drawContext.getCurrentPage());
      markup.setQuadPoints(quadPoints);

Our HighlightAnnotation provides the color, so we have to set it on the markup (we use the CompatibilityHelper in this example in order to be runnable both with PDFBox 1.8.x and 2.x, but you don't have to do this):

      // set the highlight color if given
      if (highlightAnnotation.getColor() != null) {
        CompatibilityHelper.setAnnotationColor(markup, highlightAnnotation.getColor());
      }

What's left is to add the markup to the current page. The DrawContext provides access to the current page (and also to the current content stream and the document):

      // finally add the markup to the PDF
      drawContext.getCurrentPage().getAnnotations().add(markup);
    }
  }

That's it. Ok, but what if I have to do some processing before the page is rendered...or after it...or after the complete document has been rendered? No problem, the interface AnnotationProcessor provides some more hooks exactly for that:

  public void beforePage(DrawContext drawContext) throws IOException;

  public void afterPage(DrawContext drawContext) throws IOException;

  public void afterRender(PDDocument document) throws IOException;

Hmmm, how do I use this highlight annotation after all? Let's have a look at this. We will build a simple example using a AnnotatedStyledText, and annotate this with our highlight annotation. Since we want the library to use our annotation processor, we have to register it using AnnotationProcessorFactory.register().

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

  // register our custom highlight annotation processor
  AnnotationProcessorFactory.register(HighlightAnnotationProcessor.class);

  Document document = new Document(PageFormat.with().A4()
      .margins(40, 60, 40, 60).portrait().build());

  Paragraph paragraph = new Paragraph();
  paragraph.addText("Hello there, here is ", 10, PDType1Font.HELVETICA);
  
  // now add some annotated text using our custom highlight annotation
  HighlightAnnotation annotation = new HighlightAnnotation(Color.green);
  AnnotatedStyledText highlightedText = new AnnotatedStyledText(
      "highlighted text", 10, PDType1Font.HELVETICA, Color.black,
      Collections.singleton(annotation));
  paragraph.add(highlightedText);

  paragraph.addText(
          ". Do whatever you want here...strike, squiggle, whatsoever\n\n",
          10, PDType1Font.HELVETICA);
  paragraph.setMaxWidth(150);
  document.add(paragraph);

  final OutputStream outputStream = new FileOutputStream(
      "customannotation.pdf");
  document.save(outputStream);
}

Ok, let's run it, and...tada:

custom annotations example

Markup for Custom Annotations

Being able to extend the library is quite nice, but programmatically assembling paragraphs of annotated text fragments is not my favorite: I like using markup. I would prefer using my own markup for my custom annotations... and you can do that. Let's say we want use some markup like "Hello there, here is {hl:#ffff00}highlighted text{hl}.", where {hl starts the highlighting and the #ffff00 after the colon is the color to use. To implement that, you have to provide two building blocks: an AnnotationControlCharacter that represents the parsed markup in an intermediate step, and an AnnotationControlCharacterFactory that parses the markup and creates the corresponding control character. Let's start with the control character:

public static class HighlightControlCharacter extends
    AnnotationControlCharacter<HighlightAnnotation> {

  private HighlightAnnotation annotation;

  protected HighlightControlCharacter(final Color color) {
    super("HIGHLIGHT", HighlightControlCharacterFactory.TO_ESCAPE);
    annotation = new HighlightAnnotation(color);
  }

  @Override
  public HighlightAnnotation getAnnotation() {
    return annotation;
  }

  @Override
  public Class<HighlightAnnotation> getAnnotationType() {
    return HighlightAnnotation.class;
  }
}

And now the control character factory. The main part is the regex to match the markup which is then used to extract the color to use:

private static class HighlightControlCharacterFactory implements
      AnnotationControlCharacterFactory<HighlightControlCharacter> {

  private final static Pattern PATTERN = Pattern
      .compile("(?<!\\\\)(\\\\\\\\)*\\{hl(:#(\\p{XDigit}{6}))?\\}");

  private final static String TO_ESCAPE = "{";

  @Override
  public HighlightControlCharacter createControlCharacter(String text,
        Matcher matcher, final List<CharSequence> charactersSoFar) {
    Color color = null;
    String hex = matcher.group(3);
    if (hex != null) {
      int r = Integer.parseUnsignedInt(hex.substring(0, 2), 16);
      int g = Integer.parseUnsignedInt(hex.substring(2, 4), 16);
      int b = Integer.parseUnsignedInt(hex.substring(4, 6), 16);
      color = new Color(r, g, b);
    }
    return new HighlightControlCharacter(color);
  }

  @Override
  public Pattern getPattern() {
    return PATTERN;
  }

  @Override
  public String unescape(String text) {
    return text.replaceAll("\\\\" + Pattern.quote(TO_ESCAPE), TO_ESCAPE);
  }

  @Override
  public boolean patternMatchesBeginOfLine() {
    return false;
  }

}

Like the annotation processor, you must register the control character factory in order to integrate into the library. That's it, so let's give it a try

  // register markup processing for the highlight annotation
  AnnotationCharacters.register(new HighlightControlCharacterFactory());

  paragraph = new Paragraph();
  paragraph.addMarkup(
        "Hello there, here is {hl:#ffff00}highlighted text{hl}. "
            + "Do whatever you want here...strike, squiggle, whatsoever\n\n",
        10, BaseFont.Helvetica);
  paragraph.setMaxWidth(150);
  document.add(paragraph);

custom annotations 2 example

Clone this wiki locally