/*
* 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.*;
import org.icepdf.core.pobjects.graphics.Shapes;
import org.icepdf.core.pobjects.graphics.commands.*;
import org.icepdf.core.util.ColorUtil;
import org.icepdf.core.util.Defs;
import org.icepdf.core.util.Library;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Text markup annotations shall appear as highlights, underlines, strikeouts
* (all PDF 1.3), or jagged ("squiggly") underlines (PDF 1.4) in the text of a
* document. When opened, they shall display a pop-up window containing the text
* of the associated note. Table 179 shows the annotation dictionary entries
* specific to these types of annotations.
*
* @since 5.0
*/
public class TextMarkupAnnotation extends MarkupAnnotation {
private static final Logger logger =
Logger.getLogger(TextMarkupAnnotation.class.toString());
public static final Name SUBTYPE_HIGHLIGHT = new Name("Highlight");
public static final Name SUBTYPE_UNDERLINE = new Name("Underline");
public static final Name SUBTYPE_SQUIGGLY = new Name("Squiggly");
public static final Name SUBTYPE_STRIKE_OUT = new Name("StrikeOut");
private static Color highlightColor;
private static Color strikeOutColor;
private static Color underlineColor;
static {
// sets annotation selected highlight colour
try {
String color = Defs.sysProperty(
"org.icepdf.core.views.page.annotation.textmarkup.highlight.color", "#ffff00");
int colorValue = ColorUtil.convertColor(color);
highlightColor =
new Color(colorValue >= 0 ? colorValue :
Integer.parseInt("ffff00", 16));
} catch (NumberFormatException e) {
if (logger.isLoggable(Level.WARNING)) {
logger.warning("Error reading Text Markup Annotation highlight colour");
}
}
// sets annotation selected highlight colour
try {
String color = Defs.sysProperty(
"org.icepdf.core.views.page.annotation.textmarkup.strikeOut.color", "#ff0000");
int colorValue = ColorUtil.convertColor(color);
strikeOutColor =
new Color(colorValue >= 0 ? colorValue :
Integer.parseInt("ff0000", 16));
} catch (NumberFormatException e) {
if (logger.isLoggable(Level.WARNING)) {
logger.warning("Error reading Text Markup Annotation strike out colour");
}
}
// sets annotation selected highlight colour
try {
String color = Defs.sysProperty(
"org.icepdf.core.views.page.annotation.textmarkup.underline.color", "#00ff00");
int colorValue = ColorUtil.convertColor(color);
underlineColor =
new Color(colorValue >= 0 ? colorValue :
Integer.parseInt("00ff00", 16));
} catch (NumberFormatException e) {
if (logger.isLoggable(Level.WARNING)) {
logger.warning("Error reading Text Markup Annotation underline colour");
}
}
}
/**
* (Required) An array of 8 × n numbers specifying the coordinates of
* n quadrilaterals in default user space. Each quadrilateral shall encompasses
* a word or group of contiguous words in the text underlying the annotation.
* The coordinates for each quadrilateral shall be given in the order
* x1 y1 x2 y2 x3 y3 x4 y4
* specifying the quadrilateral’s four vertices in counterclockwise order
* (see Figure 64). The text shall be oriented with respect to the edge
* connecting points (x1, y1) and (x2, y2).
* <br>
* The annotation dictionary’s AP entry, if present, shall take precedence
* over QuadPoints; see Table 168 and 12.5.5, "Appearance Streams."
*/
public static final Name KEY_QUAD_POINTS = new Name("QuadPoints");
/**
* Highlight transparency default
*/
public static final int HIGHLIGHT_ALPHA = 80;
/**
* Converted Quad points.
*/
private Shape[] quadrilaterals;
private Color textMarkupColor;
private GeneralPath markupPath;
private ArrayList<Shape> markupBounds;
/**
* Creates a new instance of an TextMarkupAnnotation.
*
* @param l document library.
* @param h dictionary entries.
*/
public TextMarkupAnnotation(Library l, HashMap h) {
super(l, h);
}
@SuppressWarnings("unchecked")
public void init() throws InterruptedException {
super.init();
// collect the quad points.
List<Number> quadPoints = library.getArray(entries, KEY_QUAD_POINTS);
if (quadPoints != null) {
int size = quadPoints.size() / 8;
quadrilaterals = new Shape[size];
GeneralPath shape;
for (int i = 0, count = 0; i < size; i++, count += 8) {
shape = new GeneralPath(GeneralPath.WIND_EVEN_ODD, 4);
shape.moveTo(quadPoints.get(count + 6).floatValue(), quadPoints.get(count + 7).floatValue());
shape.lineTo(quadPoints.get(count + 4).floatValue(), quadPoints.get(count + 5).floatValue());
shape.lineTo(quadPoints.get(count).floatValue(), quadPoints.get(count + 1).floatValue());
shape.lineTo(quadPoints.get(count + 2).floatValue(), quadPoints.get(count + 3).floatValue());
shape.closePath();
quadrilaterals[i] = shape;
}
}
if (SUBTYPE_HIGHLIGHT.equals(subtype)) {
textMarkupColor = highlightColor;
} else if (SUBTYPE_STRIKE_OUT.equals(subtype)) {
textMarkupColor = strikeOutColor;
} else if (SUBTYPE_UNDERLINE.equals(subtype)) {
textMarkupColor = underlineColor;
} else if (SUBTYPE_SQUIGGLY.equals(subtype)) {
// not implemented
}
// for editing purposes grab anny shapes from the AP Stream and
// store them as markupBounds and markupPath. This works ok but
// perhaps a better way would be to reapply the bound box
Appearance appearance = appearances.get(currentAppearance);
AppearanceState appearanceState = appearance.getSelectedAppearanceState();
Shapes shapes = appearanceState.getShapes();
if (shapes != null) {
markupBounds = new ArrayList<Shape>();
markupPath = new GeneralPath();
ShapeDrawCmd shapeDrawCmd;
for (DrawCmd cmd : shapes.getShapes()) {
if (cmd instanceof ShapeDrawCmd) {
shapeDrawCmd = (ShapeDrawCmd) cmd;
markupBounds.add(shapeDrawCmd.getShape());
markupPath.append(shapeDrawCmd.getShape(), false);
}
}
}
// try and generate an appearance stream.
resetNullAppearanceStream();
}
/**
* Gets an instance of a TextMarkupAnnotation that has valid Object Reference.
*
* @param library document library
* @param rect bounding rectangle in user space
* @return new TextMarkupAnnotation Instance.
*/
public static TextMarkupAnnotation getInstance(Library library,
Rectangle rect,
final Name subType) {
// state manager
StateManager stateManager = library.getStateManager();
// create a new entries to hold the annotation properties
HashMap<Name, Object> entries = new HashMap<Name, Object>();
// set default link annotation values.
entries.put(Dictionary.TYPE_KEY, Annotation.TYPE_VALUE);
entries.put(Dictionary.SUBTYPE_KEY, subType);
entries.put(Annotation.FLAG_KEY, 4);
// coordinates
if (rect != null) {
entries.put(Annotation.RECTANGLE_KEY,
PRectangle.getPRectangleVector(rect));
} else {
entries.put(Annotation.RECTANGLE_KEY, new Rectangle(10, 10, 50, 100));
}
TextMarkupAnnotation textMarkupAnnotation =
null;
try {
textMarkupAnnotation = new TextMarkupAnnotation(library, entries);
textMarkupAnnotation.init();
entries.put(NM_KEY,
new LiteralStringObject(String.valueOf(textMarkupAnnotation.hashCode())));
textMarkupAnnotation.setPObjectReference(stateManager.getNewReferencNumber());
textMarkupAnnotation.setNew(true);
textMarkupAnnotation.setModifiedDate(PDate.formatDateTime(new Date()));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.fine("Text markup annotation instance creation was interrupted");
}
return textMarkupAnnotation;
}
public static boolean isTextMarkupAnnotation(Name subType) {
return SUBTYPE_HIGHLIGHT.equals(subType) ||
SUBTYPE_UNDERLINE.equals(subType) ||
SUBTYPE_SQUIGGLY.equals(subType) ||
SUBTYPE_STRIKE_OUT.equals(subType);
}
/**
* Resets the annotations appearance stream.
*/
public void resetAppearanceStream(double dx, double dy, AffineTransform pageTransform) {
// check if we have anything to reset.
if (markupBounds == null) {
return;
}
Appearance appearance = appearances.get(currentAppearance);
AppearanceState appearanceState = appearance.getSelectedAppearanceState();
appearanceState.setShapes(new Shapes());
Rectangle2D bbox = appearanceState.getBbox();
AffineTransform matrix = appearanceState.getMatrix();
Shapes shapes = appearanceState.getShapes();
// setup the stroke from the border settings.
BasicStroke stroke = new BasicStroke(1f);
shapes.add(new StrokeDrawCmd(stroke));
shapes.add(new GraphicsStateCmd(EXT_GSTATE_NAME));
shapes.add(new AlphaDrawCmd(
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity)));
if (SUBTYPE_HIGHLIGHT.equals(subtype)) {
shapes.add(new ShapeDrawCmd(markupPath));
shapes.add(new ColorDrawCmd(textMarkupColor));
shapes.add(new FillDrawCmd());
shapes.add(new AlphaDrawCmd(
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)));
} else if (SUBTYPE_STRIKE_OUT.equals(subtype)) {
for (Shape shape : markupBounds) {
// calculate the line that will stroke the bounds
GeneralPath strikeOutPath = new GeneralPath();
Rectangle2D bound = shape.getBounds2D();
float y = (float) (bound.getMinY() + (bound.getHeight() / 2));
strikeOutPath.moveTo((float) bound.getMinX(), y);
strikeOutPath.lineTo((float) bound.getMaxX(), y);
strikeOutPath.closePath();
shapes.add(new ShapeDrawCmd(strikeOutPath));
shapes.add(new ColorDrawCmd(textMarkupColor));
shapes.add(new DrawDrawCmd());
}
} else if (SUBTYPE_UNDERLINE.equals(subtype)) {
for (Shape shape : markupBounds) {
// calculate the line that will stroke the bounds
GeneralPath underlinePath = new GeneralPath();
Rectangle2D bound = shape.getBounds2D();
underlinePath.moveTo((float) bound.getMinX(), (float) bound.getMinY());
underlinePath.lineTo((float) bound.getMaxX(), (float) bound.getMinY());
underlinePath.closePath();
shapes.add(new ShapeDrawCmd(underlinePath));
shapes.add(new ColorDrawCmd(textMarkupColor));
shapes.add(new DrawDrawCmd());
}
} else if (SUBTYPE_SQUIGGLY.equals(subtype)) {
// not implemented, need to create a custom stroke or
// build out a custom line move.
}
shapes.add(new AlphaDrawCmd(
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)));
// create the quad points
List<Float> quadPoints = new ArrayList<Float>();
if (markupBounds != null) {
Rectangle2D bounds;
// build out the square in quadrant 1.
for (Shape shape : markupBounds) {
bounds = shape.getBounds2D();
quadPoints.add((float) bounds.getX());
quadPoints.add((float) (bounds.getY() + bounds.getHeight()));
quadPoints.add((float) (bounds.getX() + bounds.getWidth()));
quadPoints.add((float) (bounds.getY() + bounds.getHeight()));
quadPoints.add((float) (bounds.getX()));
quadPoints.add((float) (bounds.getY()));
quadPoints.add((float) (bounds.getX() + bounds.getWidth()));
quadPoints.add((float) (bounds.getY()));
}
}
entries.put(KEY_QUAD_POINTS, quadPoints);
setModifiedDate(PDate.formatDateTime(new Date()));
// update the appearance stream
// create/update the appearance stream of the xObject.
Form form = updateAppearanceStream(shapes, bbox, matrix,
PostScriptEncoder.generatePostScript(shapes.getShapes()));
generateExternalGraphicsState(form, opacity);
}
@Override
protected void renderAppearanceStream(Graphics2D g) {
Appearance appearance = appearances.get(currentAppearance);
AppearanceState appearanceState = appearance.getSelectedAppearanceState();
Shapes shapes = appearanceState.getShapes();
// Appearance stream takes precedence over the quad points.
if (shapes != null) {
super.renderAppearanceStream(g);
}
// draw the quad points.
else if (quadrilaterals != null) {
// check to see if we are painting highlight annotations.
// if so we add some transparency to the context.
if (subtype != null && SUBTYPE_HIGHLIGHT.equals(subtype)) {
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .30f));
// remove other alpha defs from painting
if (shapes != null) {
shapes.setPaintAlpha(false);
}
}
Object tmp = getObject(RECTANGLE_KEY);
Rectangle2D.Float rectangle = null;
if (tmp instanceof List) {
rectangle = library.getRectangle(entries, RECTANGLE_KEY);
}
// get the current position of the userSpaceRectangle
Rectangle2D.Float origRect = getUserSpaceRectangle();
// build the transform to go back to users space
AffineTransform af = g.getTransform();
double x = rectangle.getX() - origRect.getX();
double y = rectangle.getY() - origRect.getY();
af.translate(-origRect.getX(), -origRect.getY());
g.setTransform(af);
g.setColor(highlightColor);
AffineTransform af2 = new AffineTransform();
af2.translate(-x, -y);
for (Shape shape : quadrilaterals) {
g.fill(af2.createTransformedShape(shape));
}
// revert the alpha value.
if (subtype != null && SUBTYPE_HIGHLIGHT.equals(subtype)) {
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f));
// remove other alpha defs from painting
if (shapes != null) {
shapes.setPaintAlpha(true);
}
}
}
}
public void setMarkupPath(GeneralPath markupPath) {
this.markupPath = markupPath;
}
public GeneralPath getMarkupPath() {
return markupPath;
}
public void setMarkupBounds(ArrayList<Shape> markupBounds) {
this.markupBounds = markupBounds;
}
public Color getTextMarkupColor() {
return textMarkupColor;
}
public void setTextMarkupColor(Color textMarkupColor) {
this.textMarkupColor = textMarkupColor;
}
}