FDFAnnotation.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pdfbox.pdmodel.fdf;
import java.awt.Color;
import java.io.IOException;
import java.util.Calendar;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSNumber;
import org.apache.pdfbox.cos.COSStream;
import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.pdmodel.common.COSObjectable;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderEffectDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary;
import org.apache.pdfbox.util.DateConverter;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
/**
* This represents an FDF annotation that is part of the FDF document.
*
* @author Ben Litchfield
* @author Johanneke Lamberink
*
* */
public abstract class FDFAnnotation implements COSObjectable
{
private static final Log LOG = LogFactory.getLog(FDFAnnotation.class);
/**
* An annotation flag.
*/
private static final int FLAG_INVISIBLE = 1;
/**
* An annotation flag.
*/
private static final int FLAG_HIDDEN = 1 << 1;
/**
* An annotation flag.
*/
private static final int FLAG_PRINTED = 1 << 2;
/**
* An annotation flag.
*/
private static final int FLAG_NO_ZOOM = 1 << 3;
/**
* An annotation flag.
*/
private static final int FLAG_NO_ROTATE = 1 << 4;
/**
* An annotation flag.
*/
private static final int FLAG_NO_VIEW = 1 << 5;
/**
* An annotation flag.
*/
private static final int FLAG_READ_ONLY = 1 << 6;
/**
* An annotation flag.
*/
private static final int FLAG_LOCKED = 1 << 7;
/**
* An annotation flag.
*/
private static final int FLAG_TOGGLE_NO_VIEW = 1 << 8;
/**
* An annotation flag.
*/
private static final int FLAG_LOCKED_CONTENTS = 1 << 9;
/**
* Annotation dictionary.
*/
protected final COSDictionary annot;
/**
* Default constructor.
*/
public FDFAnnotation()
{
annot = new COSDictionary();
annot.setItem(COSName.TYPE, COSName.ANNOT);
}
/**
* Constructor.
*
* @param a The FDF annotation.
*/
public FDFAnnotation(COSDictionary a)
{
annot = a;
}
/**
* Constructor.
*
* @param element An XFDF element.
*
* @throws IOException If there is an error extracting data from the element.
*/
public FDFAnnotation(Element element) throws IOException
{
this();
String page = element.getAttribute("page");
if (page == null || page.isEmpty())
{
throw new IOException("Error: missing required attribute 'page'");
}
setPage(Integer.parseInt(page));
String color = element.getAttribute("color");
if (color != null && color.length() == 7 && color.charAt(0) == '#')
{
int colorValue = Integer.parseInt(color.substring(1, 7), 16);
setColor(new Color(colorValue));
}
setDate(element.getAttribute("date"));
String flags = element.getAttribute("flags");
if (flags != null)
{
String[] flagTokens = flags.split(",");
for (String flagToken : flagTokens)
{
switch (flagToken)
{
case "invisible":
setInvisible(true);
break;
case "hidden":
setHidden(true);
break;
case "print":
setPrinted(true);
break;
case "nozoom":
setNoZoom(true);
break;
case "norotate":
setNoRotate(true);
break;
case "noview":
setNoView(true);
break;
case "readonly":
setReadOnly(true);
break;
case "locked":
setLocked(true);
break;
case "togglenoview":
setToggleNoView(true);
break;
default:
break;
}
}
}
setName(element.getAttribute("name"));
String rect = element.getAttribute("rect");
if (rect == null)
{
throw new IOException("Error: missing attribute 'rect'");
}
String[] rectValues = rect.split(",");
if (rectValues.length != 4)
{
throw new IOException("Error: wrong amount of numbers in attribute 'rect'");
}
float[] values = new float[4];
for (int i = 0; i < 4; i++)
{
values[i] = Float.parseFloat(rectValues[i]);
}
COSArray array = new COSArray();
array.setFloatArray(values);
setRectangle(new PDRectangle(array));
setTitle(element.getAttribute("title"));
/*
* Set the markup annotation attributes
*/
setCreationDate(DateConverter.toCalendar(element.getAttribute("creationdate")));
String opac = element.getAttribute("opacity");
if (opac != null && !opac.isEmpty())
{
setOpacity(Float.parseFloat(opac));
}
setSubject(element.getAttribute("subject"));
String intent = element.getAttribute("intent");
if (intent.isEmpty())
{
// not conforming to spec, but qoppa produces it and Adobe accepts it
intent = element.getAttribute("IT");
}
setIntent(intent);
XPath xpath = XPathFactory.newInstance().newXPath();
try
{
setContents(xpath.evaluate("contents[1]", element));
}
catch (XPathExpressionException e)
{
LOG.debug("Error while evaluating XPath expression for richtext contents", e);
}
try
{
Node richContents = (Node) xpath.evaluate("contents-richtext[1]", element,
XPathConstants.NODE);
if (richContents != null)
{
setRichContents(richContentsToString(richContents, true));
setContents(richContents.getTextContent().trim());
}
}
catch (XPathExpressionException e)
{
LOG.debug("Error while evaluating XPath expression for richtext contents", e);
}
PDBorderStyleDictionary borderStyle = new PDBorderStyleDictionary();
String width = element.getAttribute("width");
if (width != null && !width.isEmpty())
{
borderStyle.setWidth(Float.parseFloat(width));
}
if (borderStyle.getWidth() > 0)
{
String style = element.getAttribute("style");
if (style != null && !style.isEmpty())
{
switch (style)
{
case "dash":
borderStyle.setStyle("D");
break;
case "bevelled":
borderStyle.setStyle("B");
break;
case "inset":
borderStyle.setStyle("I");
break;
case "underline":
borderStyle.setStyle("U");
break;
case "cloudy":
borderStyle.setStyle("S");
PDBorderEffectDictionary borderEffect = new PDBorderEffectDictionary();
borderEffect.setStyle("C");
String intensity = element.getAttribute("intensity");
if (intensity != null && !intensity.isEmpty())
{
borderEffect.setIntensity(Float.parseFloat(element
.getAttribute("intensity")));
}
setBorderEffect(borderEffect);
break;
default:
borderStyle.setStyle("S");
break;
}
}
String dashes = element.getAttribute("dashes");
if (dashes != null && !dashes.isEmpty())
{
String[] dashesValues = dashes.split(",");
COSArray dashPattern = new COSArray();
for (String dashesValue : dashesValues)
{
dashPattern.add(COSNumber.get(dashesValue));
}
borderStyle.setDashStyle(dashPattern);
}
setBorderStyle(borderStyle);
}
}
/**
* Create the correct FDFAnnotation.
*
* @param fdfDic The FDF dictionary.
*
* @return A newly created FDFAnnotation
*/
public static FDFAnnotation create(COSDictionary fdfDic)
{
FDFAnnotation retval = null;
if (fdfDic != null)
{
String fdfDicName = fdfDic.getNameAsString(COSName.SUBTYPE);
if (FDFAnnotationText.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationText(fdfDic);
}
else if (FDFAnnotationCaret.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationCaret(fdfDic);
}
else if (FDFAnnotationFreeText.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationFreeText(fdfDic);
}
else if (FDFAnnotationFileAttachment.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationFileAttachment(fdfDic);
}
else if (FDFAnnotationHighlight.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationHighlight(fdfDic);
}
else if (FDFAnnotationInk.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationInk(fdfDic);
}
else if (FDFAnnotationLine.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationLine(fdfDic);
}
else if (FDFAnnotationLink.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationLink(fdfDic);
}
else if (FDFAnnotationCircle.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationCircle(fdfDic);
}
else if (FDFAnnotationSquare.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationSquare(fdfDic);
}
else if (FDFAnnotationPolygon.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationPolygon(fdfDic);
}
else if (FDFAnnotationPolyline.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationPolyline(fdfDic);
}
else if (FDFAnnotationSound.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationSound(fdfDic);
}
else if (FDFAnnotationSquiggly.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationSquiggly(fdfDic);
}
else if (FDFAnnotationStamp.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationStamp(fdfDic);
}
else if (FDFAnnotationStrikeOut.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationStrikeOut(fdfDic);
}
else if (FDFAnnotationUnderline.SUBTYPE.equals(fdfDicName))
{
retval = new FDFAnnotationUnderline(fdfDic);
}
else
{
LOG.warn("Unknown or unsupported annotation type '" + fdfDicName + "'");
}
}
return retval;
}
/**
* Convert this standard java object to a COS object.
*
* @return The cos object that matches this Java object.
*/
@Override
public COSDictionary getCOSObject()
{
return annot;
}
/**
* This will get the page number or null if it does not exist.
*
* @return The page number.
*/
public Integer getPage()
{
Integer retval = null;
COSNumber page = (COSNumber) annot.getDictionaryObject(COSName.PAGE);
if (page != null)
{
retval = page.intValue();
}
return retval;
}
/**
* This will set the page.
*
* @param page The page number.
*/
public final void setPage(int page)
{
annot.setInt(COSName.PAGE, page);
}
/**
* Get the annotation color.
*
* @return The annotation color, or null if there is none.
*/
public Color getColor()
{
Color retval = null;
COSArray array = annot.getCOSArray(COSName.C);
if (array != null)
{
float[] rgb = array.toFloatArray();
if (rgb.length >= 3)
{
retval = new Color(rgb[0], rgb[1], rgb[2]);
}
}
return retval;
}
/**
* Set the annotation color.
*
* @param c The annotation color.
*/
public final void setColor(Color c)
{
COSArray color = null;
if (c != null)
{
float[] colors = c.getRGBColorComponents(null);
color = new COSArray();
color.setFloatArray(colors);
}
annot.setItem(COSName.C, color);
}
/**
* Modification date.
*
* @return The date as a string.
*/
public String getDate()
{
return annot.getString(COSName.M);
}
/**
* The annotation modification date.
*
* @param date The date to store in the FDF annotation.
*/
public final void setDate(String date)
{
annot.setString(COSName.M, date);
}
/**
* Get the invisible flag.
*
* @return The invisible flag.
*/
public boolean isInvisible()
{
return annot.getFlag(COSName.F, FLAG_INVISIBLE);
}
/**
* Set the invisible flag.
*
* @param invisible The new invisible flag.
*/
public final void setInvisible(boolean invisible)
{
annot.setFlag(COSName.F, FLAG_INVISIBLE, invisible);
}
/**
* Get the hidden flag.
*
* @return The hidden flag.
*/
public boolean isHidden()
{
return annot.getFlag(COSName.F, FLAG_HIDDEN);
}
/**
* Set the hidden flag.
*
* @param hidden The new hidden flag.
*/
public final void setHidden(boolean hidden)
{
annot.setFlag(COSName.F, FLAG_HIDDEN, hidden);
}
/**
* Get the printed flag.
*
* @return The printed flag.
*/
public boolean isPrinted()
{
return annot.getFlag(COSName.F, FLAG_PRINTED);
}
/**
* Set the printed flag.
*
* @param printed The new printed flag.
*/
public final void setPrinted(boolean printed)
{
annot.setFlag(COSName.F, FLAG_PRINTED, printed);
}
/**
* Get the noZoom flag.
*
* @return The noZoom flag.
*/
public boolean isNoZoom()
{
return annot.getFlag(COSName.F, FLAG_NO_ZOOM);
}
/**
* Set the noZoom flag.
*
* @param noZoom The new noZoom flag.
*/
public final void setNoZoom(boolean noZoom)
{
annot.setFlag(COSName.F, FLAG_NO_ZOOM, noZoom);
}
/**
* Get the noRotate flag.
*
* @return The noRotate flag.
*/
public boolean isNoRotate()
{
return annot.getFlag(COSName.F, FLAG_NO_ROTATE);
}
/**
* Set the noRotate flag.
*
* @param noRotate The new noRotate flag.
*/
public final void setNoRotate(boolean noRotate)
{
annot.setFlag(COSName.F, FLAG_NO_ROTATE, noRotate);
}
/**
* Get the noView flag.
*
* @return The noView flag.
*/
public boolean isNoView()
{
return annot.getFlag(COSName.F, FLAG_NO_VIEW);
}
/**
* Set the noView flag.
*
* @param noView The new noView flag.
*/
public final void setNoView(boolean noView)
{
annot.setFlag(COSName.F, FLAG_NO_VIEW, noView);
}
/**
* Get the readOnly flag.
*
* @return The readOnly flag.
*/
public boolean isReadOnly()
{
return annot.getFlag(COSName.F, FLAG_READ_ONLY);
}
/**
* Set the readOnly flag.
*
* @param readOnly The new readOnly flag.
*/
public final void setReadOnly(boolean readOnly)
{
annot.setFlag(COSName.F, FLAG_READ_ONLY, readOnly);
}
/**
* Get the locked flag.
*
* @return The locked flag.
*/
public boolean isLocked()
{
return annot.getFlag(COSName.F, FLAG_LOCKED);
}
/**
* Set the locked flag.
*
* @param locked The new locked flag.
*/
public final void setLocked(boolean locked)
{
annot.setFlag(COSName.F, FLAG_LOCKED, locked);
}
/**
* Get the toggleNoView flag.
*
* @return The toggleNoView flag.
*/
public boolean isToggleNoView()
{
return annot.getFlag(COSName.F, FLAG_TOGGLE_NO_VIEW);
}
/**
* Set the toggleNoView flag.
*
* @param toggleNoView The new toggleNoView flag.
*/
public final void setToggleNoView(boolean toggleNoView)
{
annot.setFlag(COSName.F, FLAG_TOGGLE_NO_VIEW, toggleNoView);
}
/**
* Get the LockedContents flag.
*
* @return The LockedContents flag.
*/
public boolean isLockedContents()
{
return annot.getFlag(COSName.F, FLAG_LOCKED_CONTENTS);
}
/**
* Set the LockedContents flag.
*
* @param lockedContents The new LockedContents flag.
*/
public void setLockedContents(boolean lockedContents)
{
annot.setFlag(COSName.F, FLAG_LOCKED_CONTENTS, lockedContents);
}
/**
* Set a unique name for an annotation.
*
* @param name The unique annotation name.
*/
public final void setName(String name)
{
annot.setString(COSName.NM, name);
}
/**
* Get the annotation name.
*
* @return The unique name of the annotation.
*/
public String getName()
{
return annot.getString(COSName.NM);
}
/**
* Set the rectangle associated with this annotation.
*
* @param rectangle The annotation rectangle.
*/
public final void setRectangle(PDRectangle rectangle)
{
annot.setItem(COSName.RECT, rectangle);
}
/**
* The rectangle associated with this annotation.
*
* @return The annotation rectangle.
*/
public PDRectangle getRectangle()
{
COSArray rectArray = annot.getCOSArray(COSName.RECT);
return rectArray != null ? new PDRectangle(rectArray) : null;
}
/**
* Set the contents, or a description, for an annotation.
*
* @param contents The annotation contents, or a description.
*/
public final void setContents(String contents)
{
annot.setString(COSName.CONTENTS, contents);
}
/**
* Get the text, or a description, of the annotation.
*
* @return The text, or a description, of the annotation.
*/
public String getContents()
{
return annot.getString(COSName.CONTENTS);
}
/**
* Set a unique title for an annotation.
*
* @param title The annotation title.
*/
public final void setTitle(String title)
{
annot.setString(COSName.T, title);
}
/**
* Get the annotation title.
*
* @return The title of the annotation.
*/
public String getTitle()
{
return annot.getString(COSName.T);
}
/**
* The annotation create date.
*
* @return The date of the creation of the annotation date
*
* @throws IOException If there is an error converting the string to a Calendar object.
*/
public Calendar getCreationDate() throws IOException
{
return annot.getDate(COSName.CREATION_DATE);
}
/**
* Set the creation date.
*
* @param date The date the annotation was created.
*/
public final void setCreationDate(Calendar date)
{
annot.setDate(COSName.CREATION_DATE, date);
}
/**
* Set the annotation opacity.
*
* @param opacity The new opacity value.
*/
public final void setOpacity(float opacity)
{
annot.setFloat(COSName.CA, opacity);
}
/**
* Get the opacity value.
*
* @return The opacity of the annotation.
*/
public float getOpacity()
{
return annot.getFloat(COSName.CA, 1f);
}
/**
* A short description of the annotation.
*
* @param subject The annotation subject.
*/
public final void setSubject(String subject)
{
annot.setString(COSName.SUBJ, subject);
}
/**
* Get the description of the annotation.
*
* @return The subject of the annotation.
*/
public String getSubject()
{
return annot.getString(COSName.SUBJ);
}
/**
* The intent of the annotation.
*
* @param intent The annotation's intent.
*/
public final void setIntent(String intent)
{
annot.setName(COSName.IT, intent);
}
/**
* Get the intent of the annotation.
*
* @return The intent of the annotation.
*/
public String getIntent()
{
return annot.getNameAsString(COSName.IT);
}
/**
* This will retrieve the rich text stream which is displayed in the popup window.
*
* @return the rich text stream.
*/
public String getRichContents()
{
return getStringOrStream(annot.getDictionaryObject(COSName.RC));
}
/**
* This will set the rich text stream which is displayed in the popup window.
*
* @param rc the rich text stream.
*/
public final void setRichContents(String rc)
{
annot.setItem(COSName.RC, new COSString(rc));
}
/**
* This will set the border style dictionary, specifying the width and dash pattern used in drawing the annotation.
*
* @param bs the border style dictionary to set.
*
*/
public final void setBorderStyle(PDBorderStyleDictionary bs)
{
annot.setItem(COSName.BS, bs);
}
/**
* This will retrieve the border style dictionary, specifying the width and dash pattern used in drawing the
* annotation.
*
* @return the border style dictionary.
*/
public PDBorderStyleDictionary getBorderStyle()
{
COSDictionary bs = annot.getCOSDictionary(COSName.BS);
return bs != null ? new PDBorderStyleDictionary(bs) : null;
}
/**
* This will set the border effect dictionary, describing the effect applied to the border described by the BS
* entry.
*
* @param be the border effect dictionary to set.
*
*/
public final void setBorderEffect(PDBorderEffectDictionary be)
{
annot.setItem(COSName.BE, be);
}
/**
* This will retrieve the border style dictionary, describing the effect applied to the border described by the BS
* entry.
*
* @return the border effect dictionary.
*/
public PDBorderEffectDictionary getBorderEffect()
{
COSDictionary be = annot.getCOSDictionary(COSName.BE);
return be != null ? new PDBorderEffectDictionary(be) : null;
}
/**
* Get a text or text stream.
*
* Some dictionary entries allow either a text or a text stream.
*
* @param base the potential text or text stream
* @return the text stream
*/
protected final String getStringOrStream(COSBase base)
{
if (base == null)
{
return "";
}
else if (base instanceof COSString)
{
return ((COSString) base).getString();
}
else if (base instanceof COSStream)
{
return ((COSStream) base).toTextString();
}
else
{
return "";
}
}
private String richContentsToString(Node node, boolean root)
{
StringBuilder sb = new StringBuilder();
NodeList nodelist = node.getChildNodes();
for (int i = 0; i < nodelist.getLength(); i++)
{
Node child = nodelist.item(i);
if (child instanceof Element)
{
sb.append(richContentsToString(child, false));
}
else if (child instanceof CDATASection)
{
sb.append("<![CDATA[").append(((CDATASection) child).getData()).append("]]>");
}
else if (child instanceof Text)
{
String cdata = ((Text) child).getData();
if (cdata!=null)
{
cdata = cdata.replace("&", "&").replace("<", "<");
}
sb.append(cdata);
}
}
if (root)
{
return sb.toString();
}
NamedNodeMap attributes = node.getAttributes();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < attributes.getLength(); i++)
{
Node attribute = attributes.item(i);
String attributeNodeValue = attribute.getNodeValue();
if (attributeNodeValue!=null)
{
attributeNodeValue = attributeNodeValue.replace("\"", """);
}
builder.append(String.format(" %s=\"%s\"", attribute.getNodeName(),
attributeNodeValue));
}
return String.format("<%s%s>%s</%s>", node.getNodeName(), builder.toString(),
sb.toString(), node.getNodeName());
}
}