-
Notifications
You must be signed in to change notification settings - Fork 71
CustomAnnotations
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.
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:
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:
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);