/*
* Copyright 2006-2017 ICEsoft Technologies Canada Corp.
*
* Licensed 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.icepdf.core.pobjects.annotations;
import org.icepdf.core.pobjects.Dictionary;
import org.icepdf.core.pobjects.*;
import org.icepdf.core.pobjects.acroform.FieldDictionary;
import org.icepdf.core.pobjects.acroform.FieldDictionaryFactory;
import org.icepdf.core.pobjects.actions.Action;
import org.icepdf.core.pobjects.graphics.Shapes;
import org.icepdf.core.pobjects.security.SecurityManager;
import org.icepdf.core.util.GraphicsRenderingHints;
import org.icepdf.core.util.Library;
import org.icepdf.core.util.Utils;
import java.awt.*;
import java.awt.geom.*;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.*;
import java.util.List;
import java.util.logging.Logger;
/**
* <p>An <code>Annotation</code> class associates an object such as a note, sound, or movie with
* a location on a page of a PDF document, or provides a way to interact with
* the user by means of the mouse and keyboard.</p>
* <br>
* <p>This class allows direct access to the a Annotations dictionary.
* Developers can take advantage of this information as they see fit. It is
* important to note that an annotations' rectangle coordinates are defined
* in the PDF document space. In order to map the rectangle coordinates to
* a view, they must be converted from the Cartesian plain to the the Java2D
* plain. The PageView method getPageBounds() can be used to locate the position
* of a page within its parent component.</p>
* <br>
* Base class of all the specific Annotation types
* <br>
* Taken from the PDF 1.6 spec, here is some relevant documentation,
* along with some additional commentary
* <br>
* <h2>8.4.1 Annotation Dictionaries</h2>
* <table border="1" summary="">
* <tr>
* <td>Key</td>
* <td>Type</td>
* <td>Value</td>
* </tr>
* <tr>
* <td><b>Type</b></td>
* <td>name</td>
* <td>(<i>Optional</i>) The type of PDF object that this dictionary describes;
* if present, must be <b>Annot</b> for an annotation dictionary.</td>
* </tr>
* <tr>
* <td><b>Subtype</b></td>
* <td>name</td>
* <td>(<i>Required</i>) The type of annotation that this dictionary describes.</td>
* </tr>
* <tr>
* <td><b>Rect</b></td>
* <td>rectangle</td>
* <td>(<i>Required</i>) The <i>annotation rectangle</i>, defining the location of the
* annotation on the page in default user space units.</td>
* <td>getUserspaceLocation()</td>
* </tr>
* <tr>
* <td><b>Contents</b></td>
* <td>text string</td>
* <td>(<i>Optional</i>) Text to be displayed for the annotation or, if this type of annotation
* does not display text, an alternate description of the annotation's contents
* in human-readable form.'s contents in support of accessibility to users with
* disabilities or for other purposes (see Section 10.8.2, "Alternate Descriptions").
* See Section 8.4.5, "Annotation Types" for more details on the meaning
* of this entry for each annotation type.</td>
* </tr>
* <tr>
* <td><b>P</b></td>
* <td>dictionary</td>
* <td>(<i>Optional; PDF 1.3; not used in FDF files</i>) An indirect reference to the page
* object with which this annotation is associated.</td>
* </tr>
* <tr>
* <td><b>NM</b></td>
* <td>text string</td>
* <td>(<i>Optional; PDF 1.4</i>) The <i>annotation name</i>, a text string uniquely identifying it
* among all the annotations on its page.</td>
* </tr>
* <tr>
* <td><b>M</b></td>
* <td>date or string</td>
* <td>(<i>Optional; PDF 1.1</i>) The date and time when the annotation was most
* recently modified. The preferred format is a date string as described in Section
* 3.8.3, "Dates," but viewer applications should be prepared to accept and
* display a string in any format. (See implementation note 78 in Appendix H.)</td>
* </tr>
* <tr>
* <td><b>F</b></td>
* <td>integer</td>
* <td>(<i>Optional; PDF 1.1</i>) A set of flags specifying various characteristics of the annotation
* (see Section 8.4.2, "Annotation Flags"). Default value: 0.</td>
* </tr>
* <tr>
* <td><b>BS</b></td>
* <td>dictionary</td>
* <td>(<i>Optional; PDF 1.2</i>) A border style dictionary specifying the characteristics of
* the annotation's border (see Section 8.4.3, "Border Styles"; see also implementation
* notes 79 and 86 in Appendix H).<br>
* <br>
* <i><b>Note:</b> This entry also specifies the width and dash pattern for the lines drawn by
* line, square, circle, and ink annotations. See the note under <b>Border</b> (below) for
* additional information.</i><br>
* <br>
* Table 8.13 summarizes the contents of the border style dictionary. If neither
* the <b>Border</b> nor the <b>BS</b> entry is present, the border is drawn as a solid line with a
* width of 1 point.</td>
* </tr>
* <tr>
* <td><b>AP</b></td>
* <td>dictionary</td>
* <td>(<i>Optional; PDF 1.2</i>) An <i>appearance dictionary</i> specifying how the annotation
* is presented visually on the page (see Section 8.4.4, "Appearance Streams" and
* also implementation notes 79 and 80 in Appendix H). Individual annotation
* handlers may ignore this entry and provide their own appearances.<br>
* <br>
* For convenience in managing appearance streams that are used repeatedly, the AP
* entry in a PDF document's name dictionary (see Section 3.6.3, "Name Dictionary")
* can contain a name tree mapping name strings to appearance streams. The
* name strings have no standard meanings; no PDF objects refer to appearance
* streams by name.</td>
* </tr>
* <tr>
* <td><b>AS</b></td>
* <td>name</td>
* <td>(<i>Required if the appearance dictionary <b>AP</b> contains one or more subdictionaries;
* PDF 1.2</i>) The annotation's <i>appearance state</i>, which selects the applicable
* appearance stream from an appearance subdictionary (see Section 8.4.4, "Appearance
* Streams" and also implementation note 79 in Appendix H).</td>
* </tr>
* <tr>
* <td><b>Border</b></td>
* <td>array</td>
* <td>(<i>Optional</i>) An array specifying the characteristics of the annotation's border.
* The border is specified as a rounded rectangle.<br>
* <br>
* In PDF 1.0, the array consists of three numbers defining the horizontal corner
* radius, vertical corner radius, and border width, all in default user space
* units. If the corner radii are 0, the border has square (not rounded) corners; if
* the border width is 0, no border is drawn. (See implementation note 81 in
* Appendix H.) <br>
* <br>
* In PDF 1.1, the array may have a fourth element, an optional <i>dash array</i>
* defining a pattern of dashes and gaps to be used in drawing the border. The
* dash array is specified in the same format as in the line dash pattern parameter
* of the graphics state (see "Line Dash Pattern" on page 187). For example, a
* <b>Border</b> value of [0 0 1 [3 2]] specifies a border 1 unit wide, with square corners,
* drawn with 3-unit dashes alternating with 2-unit gaps. Note that no
* dash phase is specified; the phase is assumed to be 0. (See implementation
* note 82 in Appendix H.)<br>
* <br>
* <i><b>Note:</b> In PDF 1.2 or later, this entry may be ignored in favor of the <b>BS</b>
* entry (see above); see implementation note 86 in Appendix H.</i><br>
* <br>
* Default value: [0 0 1].</td>
* </tr>
* <tr>
* <td><b>BE</b></td>
* <td>dictionary</td>
* <td>(<i>Optional; PDF 1.5</i>) Some annotations (square, circle, and polygon) may
* have a <b>BE</b> entry, which is a <i>border effect</i> dictionary that specifies an effect
* to be applied to the border of the annotations. Its entries are listed in Table 8.14.</td>
* </tr>
* <tr>
* <td><b>C</b></td>
* <td>array</td>
* <td>(<i>Optional; PDF 1.1</i>) An array of three numbers in the range 0.0 to 1.0, representing
* the components of a color in the <b>DeviceRGB</b> color space. This color is
* used for the following purposes:
* <ul>
* <li>The background of the annotation's icon when closed
* <li>The title bar of the annotation's pop-up window
* <li>The border of a link annotation
* </ul></td>
* </tr>
* <tr>
* <td><b>A</b></td>
* <td>dictionary</td>
* <td>(<i>Optional; PDF 1.1</i>) An action to be performed when the annotation is activated
* (see Section 8.5, "Actions").<br>
* <br>
* <i><b>Note:</b> This entry is not permitted in link annotations if a Dest entry is present
* (see "Link Annotations" on page 587). Also note that the A entry in movie annotations
* has a different meaning (see "Movie Annotations" on page 601).</i></td>
* </tr>
* <tr>
* <td><b>AA</b></td>
* <td>dictionary</td>
* <td>(<i>Optional; PDF 1.2</i>) An additional-actions dictionary defining the annotation's
* behavior in response to various trigger events (see Section 8.5.2,
* "Trigger Events"). At the time of publication, this entry is used only by widget
* annotations.</td>
* </tr>
* <tr>
* <td><b>StructParent</b></td>
* <td>integer</td>
* <td>(<i>(Required if the annotation is a structural content item; PDF 1.3</i>) The integer
* key of the annotation's entry in the structural parent tree (see "Finding Structure
* Elements from Content Items" on page 797).</td>
* </tr>
* <tr>
* <td><b>OC</b></td>
* <td>dictionary</td>
* <td>(<i>Optional; PDF 1.5</i>) An optional content group or optional content membership
* dictionary (see Section 4.10, "Optional Content") specifying the optional
* content properties for the annotation. Before the annotation is drawn, its visibility
* is determined based on this entry as well as the annotation flags specified
* in the <b>F</b> entry (see Section 8.4.2, "Annotation Flags"). If it is determined
* to be invisible, the annotation is skipped, as if it were not in the document.</td>
* </tr>
* </table>
* <br>
* <br>
* <h2>8.4.2 Annotation Flags</h2>
* The value of the annotation dictionary's <b>F</b> entry is an unsigned 32-bit integer containing
* flags specifying various characteristics of the annotation. Bit positions
* within the flag word are numbered from 1 (low-order) to 32 (high-order). Table
* 8.12 shows the meanings of the flags; all undefined flag bits are reserved and must
* be set to 0.
* <table border=1 summary="">
* <tr>
* <td>Bit position</td>
* <td>Name</td>
* <td>Meaning</td>
* </tr>
* <tr>
* <td>1</td>
* <td>Invisible</td>
* <td>If set, do not display the annotation if it does not belong to one of the standard
* annotation types and no annotation handler is available. If clear, display such an
* unknown annotation using an appearance stream specified by its appearance
* dictionary, if any (see Section 8.4.4, "Appearance Streams").</td>
* </tr>
* <tr>
* <td>2</td>
* <td>Hidden</td>
* <td>If set, do not display or print the annotation or allow it to interact
* with the user, regardless of its annotation type or whether an annotation
* handler is available. In cases where screen space is limited, the ability to hide
* and show annotations selectively can be used in combination with appearance
* streams (see Section 8.4.4, "Appearance Streams") to display auxiliary pop-up
* information similar in function to online help systems. (See implementation
* note 83 in Appendix H.)</td>
* </tr>
* <tr>
* <td>3</td>
* <td>Print</td>
* <td>If set, print the annotation when the page is printed. If clear, never
* print the annotation, regardless of whether it is displayed on the screen. This
* can be useful, for example, for annotations representing interactive pushbuttons,
* which would serve no meaningful purpose on the printed page. (See
* implementation note 83 in Appendix H.)</td>
* </tr>
* <tr>
* <td>4</td>
* <td>NoZoom</td>
* <td>If set, do not scale the annotation's appearance to match the magnification
* of the page. The location of the annotation on the page (defined by the
* upper-left corner of its annotation rectangle) remains fixed, regardless of the
* page magnification. See below for further discussion.</td>
* </tr>
* <tr>
* <td>5</td>
* <td>NoRotate</td>
* <td>If set, do not rotate the annotation's appearance to match the rotation
* of the page. The upper-left corner of the annotation rectangle remains in a fixed
* location on the page, regardless of the page rotation. See below for further discussion.</td>
* </tr>
* <tr>
* <td>6</td>
* <td>NoView</td>
* <td>If set, do not display the annotation on the screen or allow it to
* interact with the user. The annotation may be printed (depending on the setting
* of the Print flag) but should be considered hidden for purposes of on-screen
* display and user interaction.</td>
* </tr>
* <tr>
* <td>7</td>
* <td>ReadOnly</td>
* <td>If set, do not allow the annotation to interact with the user. The
* annotation may be displayed or printed (depending on the settings of the
* NoView and Print flags) but should not respond to mouse clicks or change its
* appearance in response to mouse motions.<br>
* <br>
* <i><b>Note:</b> This flag is ignored for widget annotations; its function is subsumed by the
* ReadOnly flag of the associated form field (see Table 8.66 on page 638).</i></td>
* </tr>
* <tr>
* <td>8</td>
* <td>Locked</td>
* <td>If set, do not allow the annotation to be deleted or its properties (including
* position and size) to be modified by the user. However, this flag does
* not restrict changes to the annotation's contents, such as the value of a form
* field. (See implementation note 84 in Appendix H.)</td>
* </tr>
* <tr>
* <td>9</td>
* <td>ToggleNoView</td>
* <td>If set, invert the interpretation of the NoView flag for certain events. A
* typical use is to have an annotation that appears only when a mouse cursor is
* held over it; see implementation note 85 in Appendix H.</td>
* </tr>
* </table>
*
* @author Mark Collette
* @since 2.5
*/
public abstract class Annotation extends Dictionary {
private static final Logger logger =
Logger.getLogger(Annotation.class.toString());
public static final Name TYPE = new Name("Annot");
public static final Name RESOURCES_VALUE = new Name("Resources");
public static final Name BBOX_VALUE = new Name("BBox");
public static final Name PARENT_KEY = new Name("Parent");
/**
* Dictionary constants for Annotations.
*/
public static final Name TYPE_VALUE = new Name("Annot");
/**
* Annotation subtype and types.
*/
public static final Name SUBTYPE_LINK = new Name("Link");
public static final Name SUBTYPE_LINE = new Name("Line");
public static final Name SUBTYPE_SQUARE = new Name("Square");
public static final Name SUBTYPE_CIRCLE = new Name("Circle");
public static final Name SUBTYPE_POLYGON = new Name("Polygon");
public static final Name SUBTYPE_POLYLINE = new Name("PolyLine");
public static final Name SUBTYPE_HIGHLIGHT = new Name("Highlight");
public static final Name SUBTYPE_POPUP = new Name("Popup");
public static final Name SUBTYPE_WIDGET = new Name("Widget");
public static final Name SUBTYPE_INK = new Name("Ink");
public static final Name SUBTYPE_FREE_TEXT = new Name("FreeText");
public static final Name SUBTYPE_TEXT = new Name("Text");
/**
* If set, do not display the annotation if it does not belong to one of the
* standard annotation types and no annotation handler is available. If clear,
* display such an unknown annotation using an appearance stream specified
* by its appearance dictionary, if any
*/
public static final int FLAG_INVISIBLE = 0x0001;
/**
* If set, do not display or print the annotation or allow it to interact
* with the user, regardless of its annotation type or whether an annotation
* handler is available.
*/
public static final int FLAG_HIDDEN = 0x0002;
/**
* If set, print the annotation when the page is printed. If clear, never
* print the annotation, regardless of whether it is displayed on the screen.
*/
public static final int FLAG_PRINT = 0x0004;
/**
* If set, do not scale the annotation’s appearance to match the magnification
* of the page. The location of the annotation on the page (defined by the
* upper-left corner of its annotation rectangle) shall remain fixed,
* regardless of the page magnification. See further discussion following
* this Table.
*/
public static final int FLAG_NO_ZOOM = 0x0008;
/**
* If set, do not rotate the annotation’s appearance to match the rotation
* of the page. The upper-left corner of the annotation rectangle shall
* remain in a fixed location on the page, regardless of the page rotation.
* See further discussion following this Table.
*/
public static final int FLAG_NO_ROTATE = 0x0010;
/**
* If set, do not display the annotation on the screen or allow it to interact
* with the user. The annotation may be printed (depending on the setting of
* the Print flag) but should be considered hidden for purposes of on-screen
* display and user interaction.
*/
public static final int FLAG_NO_VIEW = 0x0020;
/**
* If set, do not allow the annotation to interact with the user. The
* annotation may be displayed or printed (depending on the settings of the
* NoView and Print flags) but should not respond to mouse clicks or change
* its appearance in response to mouse motions.
* <br>
* This flag shall be ignored for widget annotations; its function is
* subsumed by the ReadOnly flag of the associated form field.
*/
public static final int FLAG_READ_ONLY = 0x0040;
/**
* If set, do not allow the annotation to be deleted or its properties
* (including position and size) to be modified by the user. However, this
* flag does not restrict changes to the annotation’s contents, such as the
* value of a form field.
*/
public static final int FLAG_LOCKED = 0x0080;
/**
* If set, invert the interpretation of the NoView flag for certain events.
*/
public static final int FLAG_TOGGLE_NO_VIEW = 0x0100;
/**
* If set, do not allow the contents of the annotation to be modified by the
* user. This flag does not restrict deletion of the annotation or changes
* to other annotation properties, such as position and size.
*/
public static final int FLAG_LOCKED_CONTENTS = 0x0200;
/**
* Border style
*/
public static final Name BORDER_STYLE_KEY = new Name("BS");
/**
* The annotation location on the page in user space units.
*/
public static final Name RECTANGLE_KEY = new Name("Rect");
/**
* The action to be performed whenteh annotation is activated.
*/
public static final Name ACTION_KEY = new Name("A");
/**
* Page that this annotation is associated with.
*/
public static final Name PARENT_PAGE_KEY = new Name("P");
/**
* Annotation border characteristics.
*/
public static final Name BORDER_KEY = new Name("Border");
/**
* Annotation border characteristics.
*/
public static final Name FLAG_KEY = new Name("F");
/**
* RGB colour value for background, titlebars and link annotation borders
*/
public static final Name COLOR_KEY = new Name("C");
/**
* Appearance dictionary specifying how the annotation is presented
* visually on the page.
*/
public static final Name APPEARANCE_STREAM_KEY = new Name("AP");
/**
* Appearance state selecting default from multiple AP's.
*/
public static final Name APPEARANCE_STATE_KEY = new Name("AS");
/**
* Appearance dictionary specifying how the annotation is presented
* visually on the page for normal display.
*/
public static final Name APPEARANCE_STREAM_NORMAL_KEY = new Name("N");
/**
* Appearance dictionary specifying how the annotation is presented
* visually on the page for rollover display.
*/
public static final Name APPEARANCE_STREAM_ROLLOVER_KEY = new Name("R");
/**
* Appearance dictionary specifying how the annotation is presented
* visually on the page for down display.
*/
public static final Name APPEARANCE_STREAM_DOWN_KEY = new Name("D");
/**
* (Optional) Text that shall be displayed for the annotation or, if this
* type of annotation does not display text, an alternate description of the
* annotation’s contents in human-readable form. In either case, this text
* is useful when extracting the document’s contents in support of accessibility
* to users with disabilities or for other purposes (see 14.9.3,
* "Alternate Descriptions"). See 12.5.6, "Annotation Types" for more details
* on the meaning of this entry for each annotation type.
*/
public static final Name CONTENTS_KEY = new Name("Contents");
/**
* The date and time when the annotation was most recently modified. The
* format should be a date string as described in 7.9.4, "Dates," but
* conforming readers shall accept and display a string in any format.
*/
public static final Name M_KEY = new Name("M");
/**
* (Optional; PDF 1.4) The annotation name, a text string uniquely
* identifying it among all the annotations on its page.
*/
public static final Name NM_KEY = new Name("NM");
/**
* Border property indexes for the border vector, only applicable
* if the border style has not been set.
*/
public static final int BORDER_HORIZONTAL_CORNER_RADIUS = 0;
public static final int BORDER_VERTICAL_CORNER_RADIUS = 1;
public static final int BORDER_WIDTH = 2;
public static final int BORDER_DASH = 3;
/**
* Annotion may or may not have a visible rectangle border
*/
public static final int VISIBLE_RECTANGLE = 1;
public static final int INVISIBLE_RECTANGLE = 0;
protected PropertyChangeSupport changeSupport;
/**
* Debug flag to turn off appearance stream compression for easier
* human file reading.
*/
protected static boolean compressAppearanceStream = true;
protected HashMap<Name, Appearance> appearances = new HashMap<Name, Appearance>(3);
protected Name currentAppearance;
// modified date.
protected PDate modifiedDate;
protected boolean hasBlendingMode;
// security manager need for decrypting strings.
protected SecurityManager securityManager;
// type of annotation
protected Name subtype;
// content flag
protected String content;
// borders style of the annotation, can be null
protected BorderStyle borderStyle;
// border defined by vector
protected List border;
// border color of annotation.
protected Color color;
// annotation bounding rectangle in user space.
protected Rectangle2D.Float userSpaceRectangle;
// test for borderless annotation types
protected boolean canDrawBorder;
/**
* Creates a new instance of an Annotation.
*
* @param l document library.
* @param h dictionary entries.
*/
public Annotation(Library l, HashMap h) {
super(l, h);
}
/**
* Should only be called from Parser, Use AnnotationFactory if you
* creating a new annotation.
*
* @param library document library
* @param hashMap annotation properties.
* @return annotation instance.
*/
@SuppressWarnings("unchecked")
public static Annotation buildAnnotation(Library library, HashMap hashMap) {
Annotation annot = null;
Name subType = (Name) hashMap.get(SUBTYPE_KEY);
if (subType != null) {
if (subType.equals(Annotation.SUBTYPE_LINK)) {
annot = new LinkAnnotation(library, hashMap);
}
// highlight version of a TextMarkup annotation.
else if (subType.equals(TextMarkupAnnotation.SUBTYPE_HIGHLIGHT) ||
subType.equals(TextMarkupAnnotation.SUBTYPE_STRIKE_OUT) ||
subType.equals(TextMarkupAnnotation.SUBTYPE_UNDERLINE)) {
annot = new TextMarkupAnnotation(library, hashMap);
} else if (subType.equals(Annotation.SUBTYPE_LINE)) {
annot = new LineAnnotation(library, hashMap);
} else if (subType.equals(Annotation.SUBTYPE_SQUARE)) {
annot = new SquareAnnotation(library, hashMap);
} else if (subType.equals(Annotation.SUBTYPE_CIRCLE)) {
annot = new CircleAnnotation(library, hashMap);
} else if (subType.equals(Annotation.SUBTYPE_INK)) {
annot = new InkAnnotation(library, hashMap);
} else if (subType.equals(Annotation.SUBTYPE_FREE_TEXT)) {
annot = new FreeTextAnnotation(library, hashMap);
} else if (subType.equals(Annotation.SUBTYPE_TEXT)) {
annot = new TextAnnotation(library, hashMap);
} else if (subType.equals(Annotation.SUBTYPE_POPUP)) {
annot = new PopupAnnotation(library, hashMap);
} else if (subType.equals(Annotation.SUBTYPE_WIDGET)) {
Name fieldType = library.getName(hashMap, FieldDictionary.FT_KEY);
if (fieldType == null) {
// get type from parent object if we the widget and field dictionary aren't combined.
Object tmp = library.getObject(hashMap, FieldDictionary.PARENT_KEY);
if (tmp instanceof HashMap) {
fieldType = library.getName((HashMap) tmp, FieldDictionary.FT_KEY);
}
}
if (FieldDictionaryFactory.TYPE_BUTTON.equals(fieldType)) {
annot = new ButtonWidgetAnnotation(library, hashMap);
} else if (FieldDictionaryFactory.TYPE_CHOICE.equals(fieldType)) {
annot = new ChoiceWidgetAnnotation(library, hashMap);
} else if (FieldDictionaryFactory.TYPE_TEXT.equals(fieldType)) {
annot = new TextWidgetAnnotation(library, hashMap);
} else if (FieldDictionaryFactory.TYPE_SIGNATURE.equals(fieldType)) {
annot = new SignatureWidgetAnnotation(library, hashMap);
} else {
annot = new WidgetAnnotation(library, hashMap);
}
}
}
if (annot == null) {
annot = new GenericAnnotation(library, hashMap);
}
// annot.init();
return annot;
}
public static void setCompressAppearanceStream(boolean compressAppearanceStream) {
Annotation.compressAppearanceStream = compressAppearanceStream;
}
@SuppressWarnings("unchecked")
public void init() throws InterruptedException {
super.init();
// type of Annotation
subtype = (Name) getObject(SUBTYPE_KEY);
securityManager = library.getSecurityManager();
content = getContents();
// no borders for the following types, not really in the
// spec for some reason, Acrobat doesn't render them.
// todo add other annotations types.
canDrawBorder = !(
SUBTYPE_LINE.equals(subtype) ||
SUBTYPE_CIRCLE.equals(subtype) ||
SUBTYPE_SQUARE.equals(subtype) ||
SUBTYPE_POLYGON.equals(subtype) ||
SUBTYPE_POLYLINE.equals(subtype));
// parse out border style if available
Object BS = getObject(BORDER_STYLE_KEY);
if (BS != null) {
if (BS instanceof HashMap) {
borderStyle = new BorderStyle(library, (HashMap) BS);
} else if (BS instanceof BorderStyle) {
borderStyle = (BorderStyle) BS;
}
}
// else build out a border style from the old B entry or create
// a default invisible border.
else {
HashMap<Name, Object> borderMap = new HashMap<Name, Object>();
// get old school border
Object borderObject = getObject(BORDER_KEY);
if (borderObject != null && borderObject instanceof List) {
border = (List) borderObject;
// copy over the properties to border style.
if (border.size() == 3) {
borderMap.put(BorderStyle.BORDER_STYLE_KEY, BorderStyle.BORDER_STYLE_SOLID);
borderMap.put(BorderStyle.BORDER_WIDTH_KEY, border.get(2));
} else if (border.size() == 4) {
borderMap.put(BorderStyle.BORDER_STYLE_KEY, BorderStyle.BORDER_STYLE_DASHED);
borderMap.put(BorderStyle.BORDER_WIDTH_KEY, border.get(2));
borderMap.put(BorderStyle.BORDER_DASH_KEY, Arrays.asList(3f));
}
} else {
// default to invisible border
borderMap.put(BorderStyle.BORDER_STYLE_KEY, BorderStyle.BORDER_STYLE_SOLID);
borderMap.put(BorderStyle.BORDER_WIDTH_KEY, 0f);
}
borderStyle = new BorderStyle(library, borderMap);
entries.put(BORDER_STYLE_KEY, borderStyle);
}
// parse out border colour, specific to link annotations.
color = null; // null as some borders are set as transparent via no colour
List C = (List) getObject(COLOR_KEY);
// parse thought rgb colour.
if (C != null && C.size() >= 3) {
float red = ((Number) C.get(0)).floatValue();
float green = ((Number) C.get(1)).floatValue();
float blue = ((Number) C.get(2)).floatValue();
red = Math.max(0.0f, Math.min(1.0f, red));
green = Math.max(0.0f, Math.min(1.0f, green));
blue = Math.max(0.0f, Math.min(1.0f, blue));
color = new Color(red, green, blue);
}
// if no creation date check for M or modified.
Object value = library.getObject(entries, M_KEY);
if (value != null && value instanceof StringObject) {
StringObject text = (StringObject) value;
modifiedDate = new PDate(securityManager,
text.getDecryptedLiteralString(securityManager));
}
// process the streams if available.
Object AP = getObject(APPEARANCE_STREAM_KEY);
if (AP instanceof HashMap) {
// assign the default AS key as the default appearance
currentAppearance = APPEARANCE_STREAM_NORMAL_KEY;
Name appearanceState = (Name) getObject(APPEARANCE_STATE_KEY);
if (appearanceState == null) {
appearanceState = APPEARANCE_STREAM_NORMAL_KEY;
}
// The annotations normal appearance.
Object appearance = library.getObject(
(HashMap) AP, APPEARANCE_STREAM_NORMAL_KEY);
if (appearance != null) {
appearances.put(APPEARANCE_STREAM_NORMAL_KEY,
parseAppearanceDictionary(APPEARANCE_STREAM_NORMAL_KEY,
appearance));
appearances.get(APPEARANCE_STREAM_NORMAL_KEY).setSelectedName(appearanceState);
}
// (Optional) The annotation’s rollover appearance.
// Default value: the value of the N entry.
appearance = library.getObject(
(HashMap) AP, APPEARANCE_STREAM_ROLLOVER_KEY);
if (appearance != null) {
appearances.put(APPEARANCE_STREAM_ROLLOVER_KEY,
parseAppearanceDictionary(APPEARANCE_STREAM_ROLLOVER_KEY,
appearance));
}
// (Optional) The annotation’s down appearance.
// Default value: the value of the N entry.
appearance = library.getObject(
(HashMap) AP, APPEARANCE_STREAM_DOWN_KEY);
if (appearance != null) {
appearances.put(APPEARANCE_STREAM_DOWN_KEY,
parseAppearanceDictionary(APPEARANCE_STREAM_DOWN_KEY,
appearance));
}
} else {
// new annotation, so setup the default appearance states.
Appearance newAppearance = new Appearance();
HashMap appearanceDictionary = new HashMap();
Rectangle2D rect = getUserSpaceRectangle();
if (rect == null) {
// we need a rect in order to render correctly, bail if not found.
throw new IllegalStateException("Annotation is missing required /rect value");
}
if (rect.getWidth() <= 1) {
rect.setRect(rect.getX(), rect.getY(), 15, rect.getHeight());
}
if (rect.getHeight() <= 1) {
rect.setRect(rect.getX(), rect.getY(), rect.getWidth(), 15);
}
appearanceDictionary.put(BBOX_VALUE, new Rectangle2D.Float(
0, 0, (float) rect.getWidth(), (float) rect.getHeight()));
newAppearance.addAppearance(APPEARANCE_STREAM_NORMAL_KEY,
new AppearanceState(library, appearanceDictionary));
appearances.put(APPEARANCE_STREAM_NORMAL_KEY, newAppearance);
currentAppearance = APPEARANCE_STREAM_NORMAL_KEY;
}
}
private Appearance parseAppearanceDictionary(Name appearanceDictionary,
Object streamOrDictionary) {
Appearance appearance = new Appearance();
// iterate over all of the keys so we can index the various annotation
// state names.
if (streamOrDictionary instanceof HashMap) {
HashMap dictionary = (HashMap) streamOrDictionary;
Set keys = dictionary.keySet();
Object value;
for (Object key : keys) {
value = dictionary.get(key);
if (value instanceof Reference) {
appearance.addAppearance((Name) key,
new AppearanceState(library, dictionary,
library.getObject((Reference) value)));
}
}
}
// single entry so assign is using the default key name
else {
appearance.addAppearance(appearanceDictionary,
new AppearanceState(library, entries, streamOrDictionary));
}
return appearance;
}
/**
* Gets the type of annotation that this dictionary describes.
* For compatibility with the old org.icepdf.core.pobjects.Annotation.getSubType()
*
* @return subtype of annotation
*/
public Name getSubType() {
return library.getName(entries, SUBTYPE_KEY);
}
public void setSubtype(Name subtype) {
entries.put(SUBTYPE_KEY, subtype);
this.subtype = subtype;
}
/**
* Gets the annotation rectangle, and defines the location of the annotation on
* the page in default user space units.
* For compatibility with the old org.icepdf.core.pobjects.Annotation.getRectangle()
*
* @return rectangle of annotation
*/
public Rectangle2D.Float getUserSpaceRectangle() {
if (userSpaceRectangle == null) {
Object tmp = getObject(RECTANGLE_KEY);
if (tmp instanceof List) {
userSpaceRectangle = library.getRectangle(entries, RECTANGLE_KEY);
}
}
return userSpaceRectangle;
}
/**
* Sets the users page rectangle for this annotation action instance
*/
public void setUserSpaceRectangle(Rectangle2D.Float rect) {
if (userSpaceRectangle != null && rect != null) {
userSpaceRectangle = new Rectangle2D.Float(rect.x, rect.y,
rect.width, rect.height);
entries.put(Annotation.RECTANGLE_KEY,
PRectangle.getPRectangleVector(userSpaceRectangle));
}
}
public void setBBox(Rectangle bbox) {
Appearance appearance = appearances.get(currentAppearance);
AppearanceState appearanceState = appearance.getSelectedAppearanceState();
appearanceState.setBbox(bbox);
}
public Rectangle2D getBbox() {
Appearance appearance = appearances.get(currentAppearance);
if (appearance != null) {
AppearanceState appearanceState = appearance.getSelectedAppearanceState();
if (appearanceState != null) {
return appearanceState.getBbox();
}
}
return null;
}
protected void resetNullAppearanceStream() {
// try and generate an appearance stream.
if (!hasAppearanceStream()) {
Object tmp = getObject(RECTANGLE_KEY);
Rectangle2D.Float rectangle = null;
if (tmp instanceof java.util.List) {
rectangle = library.getRectangle(entries, RECTANGLE_KEY);
}
if (rectangle != null) {
setBBox(rectangle.getBounds());
}
resetAppearanceStream(new AffineTransform());
}
}
public Name getCurrentAppearance() {
return currentAppearance;
}
public void setCurrentAppearance(Name currentAppearance) {
this.currentAppearance = currentAppearance;
}
/**
* Creates a Java2D strok from the propties tht make up the BorderStyle object.
*
* @return dashed or solid stoke.
*/
public BasicStroke getBorderStyleStroke() {
if (borderStyle.isStyleDashed()) {
return new BasicStroke(
borderStyle.getStrokeWidth(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
borderStyle.getStrokeWidth() * 2.0f, borderStyle.getDashArray(), 0.0f);
} else {
return new BasicStroke(borderStyle.getStrokeWidth());
}
}
/**
* Gets the action to be performed when the annotation is activated.
* For compatibility with the old org.icepdf.core.pobjects.Annotation.getAction()
*
* @return action to be activated, if no action, null is returned.
*/
public org.icepdf.core.pobjects.actions.Action getAction() {
Object tmp = library.getDictionary(entries, ACTION_KEY);
// initial parse will likely have the action as a dictionary, so we
// create the new action object on the fly. However it is also possible
// that we are parsing an action that has no type specification and
// thus we can't use the parser to create the new action.
if (tmp != null) {
Action action = Action.buildAction(library, (HashMap) tmp);
// assign reference if applicable
if (action != null &&
library.isReference(entries, ACTION_KEY)) {
action.setPObjectReference(
library.getReference(entries, ACTION_KEY));
}
return action;
}
// subsequent new or edit actions will put in a reference and property
// dictionary entry.
tmp = getObject(ACTION_KEY);
if (tmp != null && tmp instanceof Action) {
return (Action) tmp;
}
return null;
}
/**
* Adds the specified action to this annotation instance. If the annotation
* instance already has an action then this action replaces it.
* <br>
* todo: future enhancement add support of next/muliple action chains.
*
* @param action action to add to this annotation. This action must
* be created using the the ActionFactory in order to correctly setup
* the Pobject reference.
* @return action that was added to Annotation, null if it was not success
* fully added.
*/
public Action addAction(Action action) {
// if no object ref we bail early.
if (action.getPObjectReference() == null) {
logger.severe("Addition of action was rejected null Object reference "
+ action);
return null;
}
// gen instance of state manager
StateManager stateManager = library.getStateManager();
// check if there is a 'dest' entry, if so we need to add this as a new
// action, flag it for later processing.
boolean isDestKey = getObject(LinkAnnotation.DESTINATION_KEY) != null;
// check the annotation dictionary for an instance of an existing action
if (getObject(ACTION_KEY) != null) {
// if found we will add the new action at the beginning of the
// next chain.
boolean isReference = library.isReference(getEntries(),
ACTION_KEY);
// we have a next action that is an object, mark it for delete.
// Because its a reference no need to flag the annotation as changed.
if (isReference) {
// mark this action for delete.
Action oldAction = (Action) action.getObject(ACTION_KEY);
oldAction.setDeleted(true);
stateManager.addChange(new PObject(oldAction,
oldAction.getPObjectReference()));
}
// not a reference, we have an inline dictionary and we'll be
// clearing it later, so we only need to add this annotation
// to the state manager.
else {
getEntries().remove(ACTION_KEY);
stateManager.addChange(new PObject(this, getPObjectReference()));
}
}
// add the new action as per usual
getEntries().put(ACTION_KEY, action.getPObjectReference());
stateManager.addChange(new PObject(this, getPObjectReference()));
// if this is a link annotation and there is a dest, we need to remove
// as it is not allowed once an action has bee added.
if (isDestKey && this instanceof LinkAnnotation) {
// remove the dest key from the dictionary
this.getEntries().remove(LinkAnnotation.DESTINATION_KEY);
// mark the annotation as changed.
stateManager.addChange(new PObject(this, getPObjectReference()));
}
// add the new action to the state manager.
action.setNew(true);
stateManager.addChange(new PObject(action, action.getPObjectReference()));
// add it to the library so we can get it again.
library.addObject(action, action.getPObjectReference());
return action;
}
/**
* Deletes the annotation action specified as a paramater. If an instance
* of the specified action can not be found, no delete is make.
*
* @param action action to remove
* @return true if the delete was successful, false otherwise.
*/
public boolean deleteAction(Action action) {
// gen instance of state manager
StateManager stateManager = library.getStateManager();
if (getObject(ACTION_KEY) != null) {
// mark this action for delete.
Action currentAction = getAction();
if (currentAction.similar(action)) {
// clear the action key for the annotation and add it as changed.
// add the new action to the annotation
getEntries().remove(ACTION_KEY);
currentAction.setDeleted(true);
// mark the action as changed.
stateManager.addChange(new PObject(currentAction,
currentAction.getPObjectReference()));
// mark the action as change.d
stateManager.addChange(new PObject(this, getPObjectReference()));
return true;
}
}
return false;
}
/**
* Update the current annotation action with this entry. This is very similar
* to add but this method will return false if there was no previous annotation.
* In such a case a call to addAction should be made.
*
* @param action action to update
* @return true if the update was successful, othere; false.
*/
public boolean updateAction(Action action) {
// get instance of state manager
StateManager stateManager = library.getStateManager();
if (getObject(ACTION_KEY) != null) {
Action currentAction = getAction();
// check if we are updating an existing instance
if (!currentAction.similar(action)) {
stateManager.addChange(new PObject(action,
action.getPObjectReference()));
currentAction.setDeleted(true);
stateManager.addChange(new PObject(currentAction,
currentAction.getPObjectReference()));
}
// add the action to the annotation
getEntries().put(ACTION_KEY, action.getPObjectReference());
stateManager.addChange(new PObject(action,
action.getPObjectReference()));
return true;
}
return false;
}
public boolean allowScreenNormalMode() {
return allowScreenOrPrintRenderingOrInteraction() && !getFlagNoView();
}
public boolean allowScreenRolloverMode() {
return allowScreenOrPrintRenderingOrInteraction() && !(getFlagNoView()
&& !getFlagToggleNoView()) && !getFlagReadOnly();
}
public boolean allowScreenDownMode() {
return allowScreenOrPrintRenderingOrInteraction() && !(getFlagNoView() &&
!getFlagToggleNoView()) && !getFlagReadOnly();
}
public boolean allowPrintNormalMode() {
return allowScreenOrPrintRenderingOrInteraction() && getFlagPrint();
}
public boolean allowAlterProperties() {
return !getFlagLocked();
}
public BorderStyle getBorderStyle() {
return borderStyle;
}
public void setBorderStyle(BorderStyle borderStyle) {
this.borderStyle = borderStyle;
entries.put(Annotation.BORDER_STYLE_KEY, this.borderStyle);
}
@SuppressWarnings("unchecked")
public List<Number> getBorder() {
return border;
}
public Object getParentAnnotation() {
Annotation parent = null;
Object ob = getObject(PARENT_KEY);
if (ob instanceof Reference)
ob = library.getObject((Reference) ob);
if (ob instanceof Annotation)
parent = (Annotation) ob;
else if (ob instanceof HashMap)
return FieldDictionaryFactory.buildField(library, (HashMap) ob);
return parent;
}
public Page getPage() {
Page page = (Page) getObject(PARENT_PAGE_KEY);
if (page == null) {
Object annot = getParentAnnotation();
if (annot instanceof Annotation)
page = ((Annotation) annot).getPage();
}
return page;
}
/**
* Gets the Link type, can be either VISIBLE_RECTANGLE or
* INVISIBLE_RECTANGLE, it all depends on the if the border or BS has
* border width > 0.
*
* @return VISIBLE_RECTANGLE if the annotation has a visible borde, otherwise
* INVISIBLE_RECTANGLE
*/
public int getBorderType() {
// border style has W value for border with
if (borderStyle != null) {
if (borderStyle.getStrokeWidth() > 0) {
return VISIBLE_RECTANGLE;
}
}
// look for a border, 0,0,1 has one, 0,0,0 doesn't
else if (border != null) {
if (border.size() >= 3 && ((Number) border.get(2)).floatValue() > 0) {
return VISIBLE_RECTANGLE;
}
}
// should never happen
return INVISIBLE_RECTANGLE;
}
/**
* Gets the Annotation border style for the given annotation. If no
* annotation line style can be found the default value of BORDER_STYLE_SOLID
* is returned. Otherwise the bordStyle and border dictionaries are used
* to deduse a line style.
*
* @return BorderSTyle line constants.
*/
public Name getLineStyle() {
// check for border style
if (borderStyle != null) {
return borderStyle.getBorderStyle();
}
// check the border entry, will be solid or dashed
else if (border != null) {
if (border.size() > 3) {
return BorderStyle.BORDER_STYLE_DASHED;
} else if (((Number) border.get(2)).floatValue() > 1) {
return BorderStyle.BORDER_STYLE_SOLID;
}
}
// default value
return BorderStyle.BORDER_STYLE_SOLID;
}
/**
* Gets the line thickness assoicated with this annotation.
*
* @return point value used when drawing line thickness.
*/
public float getLineThickness() {
// check for border style
if (borderStyle != null) {
return borderStyle.getStrokeWidth();
}
// check the border entry, will be solid or dashed
else if (border != null) {
if (border.size() >= 3) {
return ((Number) border.get(2)).floatValue();
}
}
return 0;
}
/**
* Checks to see if the annotation has defined a drawable border width.
*
* @return true if a border will be drawn; otherwise, false.
*/
public boolean isBorder() {
boolean borderWidth = false;
Object border = getObject(BORDER_KEY);
if (border != null && border instanceof List) {
List borderProps = (List) border;
if (borderProps.size() == 3) {
borderWidth = ((Number) borderProps.get(2)).floatValue() > 0;
}
}
return (getBorderStyle() != null &&
getBorderStyle().getStrokeWidth() > 0) || borderWidth;
}
public void render(Graphics2D origG, int renderHintType,
float totalRotation, float userZoom,
boolean tabSelected) {
if (!allowScreenOrPrintRenderingOrInteraction())
return;
if (renderHintType == GraphicsRenderingHints.SCREEN && !allowScreenNormalMode())
return;
if (renderHintType == GraphicsRenderingHints.PRINT && !allowPrintNormalMode())
return;
//System.out.println("render(-) " + this);
Rectangle2D.Float rect = getUserSpaceRectangle();
// Show original ractangle, without taking into consideration NoZoom and NoRotate
//System.out.println("Original rectangle: " + rect);
//origG.setColor( Color.blue );
//origG.draw( rect );
//origG.setColor( Color.red );
//Line2D.Double topLine = new Line2D.Double( rect.getMinX(), rect.getMaxY(), rect.getMaxX(), rect.getMaxY() );
//origG.draw( topLine );
//origG.setColor( Color.yellow );
//Line2D.Double bottomLine = new Line2D.Double( rect.getMinX(), rect.getMinY(), rect.getMaxX(), rect.getMinY() );
//origG.draw( bottomLine );
AffineTransform oldAT = origG.getTransform();
Shape oldClip = origG.getClip();
Composite oldComp = origG.getComposite();
// Simply uncomment the //// lines to use a different Graphics object
Graphics2D g = origG;
////Graphics2D g = (Graphics2D) origG.create();
AffineTransform at = new AffineTransform(oldAT);
// translate to annotation location, as all coordinates "should" be relative to this point.
at.translate(rect.getMinX(), rect.getMinY());
boolean noRotate = getFlagNoRotate();
if (noRotate) {
float unRotation = -totalRotation;
while (unRotation < 0.0f)
unRotation += 360.0f;
while (unRotation > 360.0f)
unRotation -= 360.0f;
if (unRotation == -0.0f)
unRotation = 0.0f;
if (unRotation != 0.0) {
double radians = Math.toRadians(unRotation); // unRotation * Math.PI / 180.0
AffineTransform rotationTransform =
AffineTransform.getRotateInstance(radians);
Point2D.Double origTopLeftCorner = new Point2D.Double(0.0, Math.abs(rect.getHeight()));
Point2D rotatedTopLeftCorner = rotationTransform.transform(origTopLeftCorner, null);
at.translate(origTopLeftCorner.getX() - rotatedTopLeftCorner.getX(),
origTopLeftCorner.getY() - rotatedTopLeftCorner.getY());
at.rotate(radians);
}
}
boolean noZoom = getFlagNoZoom();
if (noZoom) {
double scaleY = Math.abs(at.getScaleY());
if (scaleY != 1.0) {
double scaleX = Math.abs(at.getScaleX());
double rectHeight = Math.abs(rect.getHeight());
double resizedY = rectHeight * ((scaleY - 1.0) / scaleY);
at.translate(0.0, resizedY);
at.scale(1.0 / scaleX, 1.0 / scaleY);
}
}
GraphicsRenderingHints grh = GraphicsRenderingHints.getDefault();
g.setRenderingHints(grh.getRenderingHints(renderHintType));
g.setTransform(at);
Shape preAppearanceStreamClip = g.getClip();
g.clip(deriveDrawingRectangle());
renderAppearanceStream(g);
g.setTransform(at);
g.setClip(preAppearanceStreamClip);
if (tabSelected) {
renderBorderTabSelected(g);
} else {
renderBorder(g);
}
g.setTransform(oldAT);
g.setClip(oldClip);
g.setComposite(oldComp);
////g.dispose();
// Show the top left corner, that NoZoom and NoRotate annotations cling to
//origG.setColor( Color.blue );
//Rectangle2D.Double topLeft = new Rectangle2D.Double(
// rect.getMinX(), rect.getMaxY()-3, 3, 3 );
//origG.fill( topLeft );
}
protected void renderAppearanceStream(Graphics2D g) {
Appearance appearance = appearances.get(currentAppearance);
if (appearance == null) return;
AppearanceState appearanceState = appearance.getSelectedAppearanceState();
if (appearanceState.getShapes() != null) {
AffineTransform matrix = appearanceState.getMatrix();
Rectangle2D bbox = appearanceState.getBbox();
// g.setColor( Color.blue );
// Rectangle2D.Float newRect = deriveDrawingRectangle();
// g.draw( newRect );
// step 1. appearance bounding box (BBox) is transformed, using
// Matrix, to produce a quadrilateral with arbitrary orientation.
Rectangle2D tBbox = matrix.createTransformedShape(bbox).getBounds2D();
// Step 2. matrix a is computed that scales and translates the
// transformed appearance box (tBbox) to align with the edges of
// the annotation's rectangle (Ret).
Rectangle2D rect = getUserSpaceRectangle();
AffineTransform tAs = AffineTransform.getScaleInstance(
(rect.getWidth() / tBbox.getWidth()),
(rect.getHeight() / tBbox.getHeight()));
// check for identity transformation
// we have to be careful in such as case as the coordinates of the annotation may actually
// be in page space. If the rectangle in page pace is more or less the same location
// as the tbbox then we know the annotation coordinate space must also be in page space.
// Thus we shift back to page space.
if (rect.getMinX() == tBbox.getMinX() && rect.getMinY() == tBbox.getMinY()) {
tAs.setTransform(tAs.getScaleX(), tAs.getShearX(), tAs.getShearY(),
tAs.getScaleY(), -rect.getX(), -rect.getY());
} else {
tAs.setTransform(tAs.getScaleX(), tAs.getShearX(), tAs.getShearY(),
tAs.getScaleY(), -tBbox.getX(), -tBbox.getY());
}
// Step 3. matrix is concatenated with A to form a matrix AA
// that maps from the appearance's coordinate system to the
// annotation's rectangle in default user space.
tAs.concatenate(matrix);
g.transform(tAs);
AffineTransform preAf = g.getTransform();
// regular paint
try {
appearanceState.getShapes().paint(g);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.fine("Page Annotation Painting interrupted.");
}
g.setTransform(preAf);
}
}
protected void renderBorder(Graphics2D g) {
// if( false ) {
// float width = 1.0f;
// Rectangle2D.Float jrect = deriveBorderDrawingRectangle( width );
// g.setColor( Color.red );
// g.setStroke( new BasicStroke(width) );
// g.draw( jrect );
// return;
// }
// we don't paint some borders as the BS has a different meanings.
if (this instanceof SquareAnnotation ||
this instanceof CircleAnnotation ||
this instanceof LineAnnotation ||
this instanceof FreeTextAnnotation ||
this instanceof InkAnnotation) {
return;
}
Color borderColor = getColor();
if (borderColor != null) {
g.setColor(borderColor);
}
BorderStyle bs = getBorderStyle();
if (bs != null) {
float width = bs.getStrokeWidth();
if (width > 0.0f && borderColor != null && canDrawBorder) {
Rectangle2D.Float jrect = deriveBorderDrawingRectangle(width);
if (bs.isStyleSolid()) {
g.setStroke(new BasicStroke(width));
g.draw(jrect);
} else if (bs.isStyleDashed()) {
BasicStroke stroke = new BasicStroke(
width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
width * 2.0f, bs.getDashArray(), 0.0f);
g.setStroke(stroke);
g.draw(jrect);
} else if (bs.isStyleBeveled()) {
jrect = deriveDrawingRectangle();
g.setStroke(new BasicStroke(1.0f));
Line2D.Double line;
// Upper top
g.setColor(BorderStyle.LIGHT);
line = new Line2D.Double( // Top line
jrect.getMinX() + 1.0, jrect.getMaxY() - 1.0, jrect.getMaxX() - 2.0, jrect.getMaxY() - 1.0);
g.draw(line);
line = new Line2D.Double( // Left line
jrect.getMinX() + 1.0f, jrect.getMinY() + 2.0, jrect.getMinX() + 1.0f, jrect.getMaxY() - 1.0);
g.draw(line);
// Inner top
g.setColor(BorderStyle.LIGHTEST);
line = new Line2D.Double( // Top line
jrect.getMinX() + 2.0, jrect.getMaxY() - 2.0, jrect.getMaxX() - 3.0, jrect.getMaxY() - 2.0);
g.draw(line);
line = new Line2D.Double( // Left line
jrect.getMinX() + 2.0f, jrect.getMinY() + 3.0, jrect.getMinX() + 2.0f, jrect.getMaxY() - 2.0);
g.draw(line);
// Inner bottom
g.setColor(BorderStyle.DARK);
line = new Line2D.Double( // Bottom line
jrect.getMinX() + 2.0, jrect.getMinY() + 2.0, jrect.getMaxX() - 2.0, jrect.getMinY() + 2.0);
g.draw(line);
line = new Line2D.Double( // Right line
jrect.getMaxX() - 2.0f, jrect.getMinY() + 2.0, jrect.getMaxX() - 2.0f, jrect.getMaxY() - 2.0);
g.draw(line);
// Lower bottom
g.setColor(BorderStyle.DARKEST);
line = new Line2D.Double( // Bottom line
jrect.getMinX() + 1.0, jrect.getMinY() + 1.0, jrect.getMaxX() - 1.0, jrect.getMinY() + 1.0);
g.draw(line);
line = new Line2D.Double( // Right line
jrect.getMaxX() - 1.0f, jrect.getMinY() + 1.0, jrect.getMaxX() - 1.0f, jrect.getMaxY() - 1.0);
g.draw(line);
} else if (bs.isStyleInset()) {
jrect = deriveDrawingRectangle();
g.setStroke(new BasicStroke(1.0f));
Line2D.Double line;
// Upper top
g.setColor(BorderStyle.DARK);
line = new Line2D.Double( // Top line
jrect.getMinX() + 1.0, jrect.getMaxY() - 1.0, jrect.getMaxX() - 1.0, jrect.getMaxY() - 1.0);
g.draw(line);
line = new Line2D.Double( // Left line
jrect.getMinX() + 1.0f, jrect.getMinY() + 1.0, jrect.getMinX() + 1.0f, jrect.getMaxY() - 1.0);
g.draw(line);
// Inner top
g.setColor(BorderStyle.DARKEST);
line = new Line2D.Double( // Top line
jrect.getMinX() + 2.0, jrect.getMaxY() - 2.0, jrect.getMaxX() - 2.0, jrect.getMaxY() - 2.0);
g.draw(line);
line = new Line2D.Double( // Left line
jrect.getMinX() + 2.0f, jrect.getMinY() + 2.0, jrect.getMinX() + 2.0f, jrect.getMaxY() - 2.0);
g.draw(line);
// Inner bottom
g.setColor(BorderStyle.LIGHTEST);
line = new Line2D.Double( // Bottom line
jrect.getMinX() + 3.0, jrect.getMinY() + 2.0, jrect.getMaxX() - 2.0, jrect.getMinY() + 2.0);
g.draw(line);
line = new Line2D.Double( // Right line
jrect.getMaxX() - 2.0f, jrect.getMinY() + 2.0, jrect.getMaxX() - 2.0f, jrect.getMaxY() - 3.0);
g.draw(line);
// Lower bottom
g.setColor(BorderStyle.LIGHT);
line = new Line2D.Double( // Bottom line
jrect.getMinX() + 2.0, jrect.getMinY() + 1.0, jrect.getMaxX() - 1.0, jrect.getMinY() + 1.0);
g.draw(line);
line = new Line2D.Double( // Right line
jrect.getMaxX() - 1.0f, jrect.getMinY() + 1.0, jrect.getMaxX() - 1.0f, jrect.getMaxY() - 2.0);
g.draw(line);
} else if (bs.isStyleUnderline()) {
g.setStroke(new BasicStroke(width));
Line2D.Double line = new Line2D.Double(
jrect.getMinX(), jrect.getMinY(), jrect.getMaxX(), jrect.getMinY());
g.draw(line);
}
}
} else {
List borderVector = (List) getObject(BORDER_KEY);
if (borderVector != null) {
if (borderColor != null) {
float horizRadius = 0.0f;
float vertRadius = 0.0f;
float width = 1.0f;
float[] dashArray = null;
if (borderVector.size() >= 1)
horizRadius = ((Number) borderVector.get(0)).floatValue();
if (borderVector.size() >= 2)
vertRadius = ((Number) borderVector.get(1)).floatValue();
if (borderVector.size() >= 3)
width = ((Number) borderVector.get(2)).floatValue();
if (borderVector.size() >= 4) {
Object dashObj = borderVector.get(3);
// I guess some encoders like having fun with us,
// and feed a number when a number-array is appropriate. The problem
// is that for the specific PDF given, apparently no border is to be
// drawn, especially not the hugely thinck one described. So,
// instead of interpretting the 4th element (Number) into a Vector,
// I'm just not going to do the border if it's the Number. I know, hack.
// The only theory I have is that LinkAnnotation defaults the border
// color to black, when maybe it should be to null, but that could
// change a _lot_ of stuff, so I won't touch it now.
if (dashObj instanceof Number) {
// Disable border drawing
width = 0.0f;
} else if (dashObj instanceof List) {
List dashVector = (List) borderVector.get(3);
int sz = dashVector.size();
dashArray = new float[sz];
for (int i = 0; i < sz; i++) {
Number num = (Number) dashVector.get(i);
dashArray[i] = num.floatValue();
}
}
}
if (width > 0.0f) {
Rectangle2D.Float jrect = deriveBorderDrawingRectangle(width);
RoundRectangle2D.Double roundRect = new RoundRectangle2D.Double(
jrect.getX(), jrect.getY(), jrect.getWidth(), jrect.getHeight(),
horizRadius, vertRadius);
BasicStroke stroke = new BasicStroke(
width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
10.0f, dashArray, 0.0f);
g.setStroke(stroke);
g.draw(roundRect);
}
}
} else {
// Draw a solid rectangle, 1 pixel wide
if (borderColor != null && SUBTYPE_LINK.equals(subtype)) {
float width = 1.0f;
Rectangle2D.Float jrect = deriveBorderDrawingRectangle(width);
g.setStroke(new BasicStroke(width));
g.draw(jrect);
}
}
}
}
protected void renderBorderTabSelected(Graphics2D g) {
float width = 1.0f;
Rectangle2D.Float jrect = deriveBorderDrawingRectangle(width);
g.setColor(Color.black);
float[] dashArray = new float[]{2.0f};
BasicStroke stroke = new BasicStroke(
width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER,
10.0f, dashArray, 0.0f);
g.setStroke(stroke);
g.draw(jrect);
}
/**
* Gest the RGB colour of the annotation used for the following purposes:
* <ul>
* <li>the background of the annotaiton's icon when closed</li>
* <li>the title bar of the anntoation's pop-up window</li>
* <li>the border of a link annotation</li>
* </ul>
*
* @return A Color for the border, or null if none is to be used
*/
public Color getColor() {
return color;
}
/**
* Sets the Annotation colour and underlying
*
* @param color new colour value
*/
public void setColor(Color color) {
this.color = new Color(color.getRGB());
// put colour back in to the dictionary
float[] compArray = new float[3];
this.color.getColorComponents(compArray);
List<Float> colorValues = new ArrayList<Float>(compArray.length);
for (float comp : compArray) {
colorValues.add(comp);
}
entries.put(Annotation.COLOR_KEY, colorValues);
}
protected Rectangle2D.Float deriveDrawingRectangle() {
Rectangle2D.Float origRect = getUserSpaceRectangle();
Rectangle2D.Float jrect = new Rectangle2D.Float(origRect.x, origRect.y,
origRect.width, origRect.height);
jrect.x = 0.0f;
jrect.y = 0.0f;
return jrect;
}
private Rectangle2D.Float deriveBorderDrawingRectangle(float borderWidth) {
Rectangle2D.Float jrect = deriveDrawingRectangle();
float halfBorderWidth = borderWidth / 2.0f;
double minX = jrect.getMinX() + halfBorderWidth;
double minY = jrect.getMinY() + halfBorderWidth;
double maxX = jrect.getMaxX() - halfBorderWidth;
double maxY = jrect.getMaxY() - halfBorderWidth;
jrect.setFrameFromDiagonal(minX, minY, maxX, maxY);
return jrect;
}
/**
* @return Whether this annotation may be shown in any way to the user
*/
public boolean allowScreenOrPrintRenderingOrInteraction() {
// Based off of the annotation flags' Invisible and Hidden values
if (getFlagHidden())
return false;
return !(getFlagInvisible() && isSupportedAnnotationType());
}
/**
* The PDF spec defines rules for displaying annotation subtypes that the viewer
* does not recognise. But, from a product point of view, we may or may not
* wish to make a best attempt at showing an unsupported annotation subtype,
* as that may make users think we're quality deficient, instead of
* feature deficient.
* Subclasses should override this, and return true, indicating that that
* particular annotation is supported.
*
* @return true, if this annotation is supported; else, false.
*/
protected boolean isSupportedAnnotationType() {
return true;
}
public boolean getFlagInvisible() {
return ((getInt(FLAG_KEY) & FLAG_INVISIBLE) != 0);
}
public boolean getFlagHidden() {
return ((getInt(FLAG_KEY) & FLAG_HIDDEN) != 0);
}
public boolean getFlagPrint() {
return ((getInt(FLAG_KEY) & FLAG_PRINT) != 0);
}
public boolean getFlagNoZoom() {
return ((getInt(FLAG_KEY) & FLAG_NO_ZOOM) != 0);
}
public boolean getFlagNoRotate() {
return ((getInt(FLAG_KEY) & FLAG_NO_ROTATE) != 0);
}
public boolean getFlagNoView() {
return ((getInt(FLAG_KEY) & FLAG_NO_VIEW) != 0);
}
public boolean getFlagReadOnly() {
return ((getInt(FLAG_KEY) & FLAG_READ_ONLY) != 0);
}
public boolean getFlagToggleNoView() {
return ((getInt(FLAG_KEY) & FLAG_TOGGLE_NO_VIEW) != 0);
}
public boolean getFlagLockedContents() {
return ((getInt(FLAG_KEY) & FLAG_LOCKED_CONTENTS) != 0);
}
public boolean getFlagLocked() {
return ((getInt(FLAG_KEY) & FLAG_LOCKED) != 0);
}
/**
* Set the specified flag key to either enabled or disabled.
*
* @param flagKey flag key to set.
* @param enable true or false key value.
*/
public void setFlag(final int flagKey, boolean enable) {
int flag = getInt(FLAG_KEY);
boolean isEnabled = (flag & flagKey) != 0;
if (!enable && isEnabled) {
flag = flag ^ flagKey;
entries.put(FLAG_KEY, flag);
} else if (enable && !isEnabled) {
flag = flag | flagKey;
entries.put(FLAG_KEY, flag);
}
}
public void setModifiedDate(String modifiedDate) {
setString(M_KEY, modifiedDate);
this.modifiedDate = new PDate(securityManager, modifiedDate);
}
public boolean hasAppearanceStream() {
return library.getObject(entries, APPEARANCE_STREAM_KEY) != null;
}
/**
* Returns the appearance stream as defined by the AP dictionary key N
*
* @return N appearance stream or null.
*/
public Stream getAppearanceStream() {
Object AP = getObject(APPEARANCE_STREAM_KEY);
if (AP instanceof HashMap) {
Object N = library.getObject(
(HashMap) AP, APPEARANCE_STREAM_NORMAL_KEY);
if (N instanceof HashMap) {
Object AS = getObject(APPEARANCE_STATE_KEY);
if (AS != null && AS instanceof Name)
N = library.getObject((HashMap) N, (Name) AS);
}
// n should be a Form but we have a few cases of Stream
if (N instanceof Stream) {
return (Stream) N;
}
}
return null;
}
/**
* Gets the Appearance Form object associated with the annotation's appearances. Many encoders do no create
* the stream if there is no data in the widget. This method insure that an appearance XObject/Form is
* created. The object new object is not added to the state manager.
*
* @return appearance for annotation.
*/
public Form getOrGenerateAppearanceForm() {
StateManager stateManager = library.getStateManager();
Form form = null;
if (hasAppearanceStream()) {
Stream stream = getAppearanceStream();
if (stream instanceof Form) {
form = (Form) stream;
} else if (stream != null) {
// build out an appearance stream, corner case iText 2.1
// didn't correctly set type = form on the appearance stream obj.
form = new Form(library, stream.getEntries(), null);
form.setPObjectReference(stream.getPObjectReference());
form.setRawBytes(stream.getDecodedStreamBytes());
form.init();
}
}// else a stream, we won't support this for annotations.
else {
// create a new xobject/form object
HashMap<Name, Object> formEntries = new HashMap<Name, Object>();
formEntries.put(Form.TYPE_KEY, Form.TYPE_VALUE);
formEntries.put(Form.SUBTYPE_KEY, Form.SUB_TYPE_VALUE);
form = new Form(library, formEntries, null);
form.setPObjectReference(stateManager.getNewReferencNumber());
library.addObject(form, form.getPObjectReference());
}
return form;
}
/**
* Create or update a Form's content stream.
*
* @param shapes shapes to associate with the appearance stream.
* @param bbox bound box.
* @param matrix form space.
* @param rawBytes raw bytes of string data making up the content stream.
* @return
*/
public Form updateAppearanceStream(Shapes shapes, Rectangle2D bbox, AffineTransform matrix, byte[] rawBytes) {
// update the appearance stream
// create/update the appearance stream of the xObject.
StateManager stateManager = library.getStateManager();
Form form;
if (hasAppearanceStream() && getAppearanceStream() instanceof Form) {
form = (Form) getAppearanceStream();
// else a stream, we won't support this for annotations.
} else {
// create a new xobject/form object
HashMap<Object, Object> formEntries = new HashMap<Object, Object>();
formEntries.put(Form.TYPE_KEY, Form.TYPE_VALUE);
formEntries.put(Form.SUBTYPE_KEY, Form.SUB_TYPE_VALUE);
form = new Form(library, formEntries, null);
form.setPObjectReference(stateManager.getNewReferencNumber());
library.addObject(form, form.getPObjectReference());
}
if (form != null && shapes != null && rawBytes != null) {
Rectangle2D formBbox = new Rectangle2D.Float((float) bbox.getX(), (float) bbox.getY(),
(float) bbox.getWidth(), (float) bbox.getHeight());
form.setAppearance(shapes, matrix, formBbox);
stateManager.addChange(new PObject(form, form.getPObjectReference()));
// update the AP's stream bytes so contents can be written out
form.setRawBytes(rawBytes);
HashMap<Object, Object> appearanceRefs = new HashMap<Object, Object>();
appearanceRefs.put(APPEARANCE_STREAM_NORMAL_KEY, form.getPObjectReference());
entries.put(APPEARANCE_STREAM_KEY, appearanceRefs);
// compress the form object stream.
if (compressAppearanceStream) {
form.getEntries().put(Stream.FILTER_KEY, new Name("FlateDecode"));
} else {
form.getEntries().remove(Stream.FILTER_KEY);
}
}
return form;
}
public String getContents() {
content = getString(CONTENTS_KEY);
return content;
}
public void setContents(String content) {
this.content = setString(CONTENTS_KEY, content);
}
/**
* Gets a known string value from the annotation dictionary, decryption will be applied as needed.
*
* @param key dictionary key value to fine.
* @return value of key if any, empty string if null;
*/
protected String getString(final Name key) {
Object value = library.getObject(entries, key);
if (value instanceof StringObject) {
StringObject text = (StringObject) value;
return Utils.convertStringObject(library, text);
} else if (value instanceof String) {
return (String) value;
} else {
return "";
}
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("ANNOTATION= {");
Set keys = entries.keySet();
for (Object key : keys) {
Object value = entries.get(key);
sb.append(key.toString());
sb.append('=');
if (value == null)
sb.append("null");
else if (value instanceof StringObject)
sb.append(((StringObject) value).getDecryptedLiteralString(library.getSecurityManager()));
else
sb.append(value.toString());
sb.append(',');
}
sb.append('}');
if (getPObjectReference() != null) {
sb.append(" ");
sb.append(getPObjectReference());
}
for (int i = sb.length() - 1; i >= 0; i--) {
if (sb.charAt(i) < 32 || sb.charAt(i) >= 127)
sb.deleteCharAt(i);
}
return sb.toString();
}
public void syncBBoxToUserSpaceRectangle(Rectangle2D bbox) {
Appearance appearance = appearances.get(currentAppearance);
AppearanceState appearanceState = appearance.getSelectedAppearanceState();
Rectangle2D tBbox = appearanceState.getMatrix().createTransformedShape(bbox).getBounds2D();
setUserSpaceRectangle(new Rectangle2D.Float(
(float) tBbox.getX(), (float) tBbox.getY(),
(float) tBbox.getWidth(), (float) tBbox.getHeight()));
}
public abstract void resetAppearanceStream(double dx, double dy, AffineTransform pageSpace);
public void resetAppearanceStream(AffineTransform pageSpace) {
resetAppearanceStream(0, 0, pageSpace);
}
public Shapes getShapes() {
Appearance appearance = appearances.get(currentAppearance);
if (appearance != null) {
AppearanceState appearanceState = appearance.getSelectedAppearanceState();
if (appearanceState != null) {
return appearanceState.getShapes();
}
}
return null;
}
public HashMap<Name, Appearance> getAppearances() {
return appearances;
}
public void addPropertyChangeListener(
PropertyChangeListener listener) {
synchronized (this) {
if (listener == null) {
return;
}
if (changeSupport == null) {
changeSupport = new PropertyChangeSupport(this);
}
changeSupport.addPropertyChangeListener(listener);
}
}
}