org.jpedal.objects.acroforms.creation.AnnotationFactory Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of OpenViewerFX Show documentation
Show all versions of OpenViewerFX Show documentation
Open Source (LGPL) JavaFX PDF Viewer for NetBeans plugin
The newest version!
/*
* ===========================================
* Java Pdf Extraction Decoding Access Library
* ===========================================
*
* Project Info: http://www.idrsolutions.com
* Help section for developers at http://www.idrsolutions.com/support/
*
* (C) Copyright 1997-2017 IDRsolutions and Contributors.
*
* This file is part of JPedal/JPDF2HTML5
*
@LICENSE@
*
* ---------------
* AnnotationFactory.java
* ---------------
*/
package org.jpedal.objects.acroforms.creation;
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Arrays;
import java.util.Calendar;
import javax.imageio.ImageIO;
import org.jpedal.color.DeviceCMYKColorSpace;
import org.jpedal.fonts.FontMappings;
import org.jpedal.fonts.StandardFonts;
import org.jpedal.objects.GraphicsState;
import org.jpedal.objects.raw.FormObject;
import org.jpedal.objects.raw.PdfArrayIterator;
import org.jpedal.objects.raw.PdfDictionary;
import org.jpedal.objects.raw.PdfObject;
import org.jpedal.render.DynamicVectorRenderer;
import org.jpedal.utils.LogWriter;
public class AnnotationFactory {
/**
* Determine the type of annotation from the sub type value and call
* appropriate method to create an icon for the annotation
*
* @param form PdfObject containing the annotation
* @return BufferedImage of the annotation or null if the supplied FormObject contains errors
*/
public static BufferedImage getIcon(final PdfObject form) {
return getIcon(form, 1.0f);
}
/**
* Determine the type of annotation from the sub type value and call
* appropriate method to create an icon for the annotation with the given
* scaling applied
*
* @param form PdfObject containing the annotation
* @param scaling Scaling to display the annotation at
* @return BufferedImage of the annotation or null if the supplied FormObject contains errors
*/
public static BufferedImage getIcon(final PdfObject form, final float scaling) {
switch (form.getParameterConstant(PdfDictionary.Subtype)) {
case PdfDictionary.Text:
return getTextIcon(form);
case PdfDictionary.Highlight:
return getHightlightIcon(form, scaling);
case PdfDictionary.Square:
return getSquareIcon(form, scaling);
case PdfDictionary.Underline:
return getUnderLineIcon(form);
case PdfDictionary.StrickOut:
return getStrickOutIcon(form);
case PdfDictionary.Caret:
return getCaretIcon(form);
case PdfDictionary.FileAttachment:
return getFileAttachmentIcon();
case PdfDictionary.Line:
return getLineIcon(form, scaling);
case PdfDictionary.Polygon:
return getPolyIcon(form, false, scaling);
case PdfDictionary.PolyLine:
return getPolyIcon(form, true, scaling);
case PdfDictionary.Circle:
return getCircleIcon(form, scaling);
case PdfDictionary.Squiggly:
return getSquigglyIcon(form);
case PdfDictionary.Sound:
return getSoundIcon(form);
case PdfDictionary.Ink:
return getInkIcon((FormObject) form, scaling);
}
return null;
}
private static BufferedImage getInkIcon(final FormObject form, final float scaling) {
final float[] quad = form.getFloatArray(PdfDictionary.Rect);
if (quad != null) {
final Rectangle bounds = getFormBounds(form, quad, scaling);
final float[][] InkListArray = form.getFloat2DArray(PdfDictionary.InkList);
if (bounds.width > 0 && bounds.height > 0) {
final BufferedImage icon1 = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_4BYTE_ABGR);
final Graphics2D g2 = (Graphics2D) icon1.getGraphics();
setStroke(form, g2, scaling);
scanInkListFloatTree(InkListArray, form, g2, scaling);
return icon1;
}
}
return null;
}
/**
* Method to scan through an InkList array, draw the ink to the provided
* Graphics object and return the bounds of the InkList
*
* @param InkListArray Object array representing an InkList
* @param form FormObject for the Ink annotation this InkList came from
* @param g Graphics object to draw the Ink to
* @return float array representing the Inks bounds in the order
* lowest x / y, largest x / x
*/
@SuppressWarnings("UnusedDeclaration")
public static float[] scanInkListTree(final Object[] InkListArray, final PdfObject form, final Graphics g) {
return scanInkListTree(InkListArray, form, g, 1.0f);
}
/**
* Method to scan through an InkList array, draw the ink to the provided
* Graphics object at the specified scaling and return the bounds of the InkList
*
* @param InkListArray Object array representing an InkList
* @param form FormObject representing the Ink annotation this InkList came from
* @param g Graphics object to draw the Ink to
* @param scaling float value representing scaling where 1 equals 100%
* @return float array representing the Inks bounds in the order
* lowest x / y, largest x / x
*/
public static float[] scanInkListTree(final Object[] InkListArray, final PdfObject form, final Graphics g, final float scaling) {
final float[] quad = form.getFloatArray(PdfDictionary.Rect);
if (quad == null) {
return null;
}
final Rectangle bounds = getFormBounds((FormObject) form, quad, scaling);
float minX = bounds.x;
float minY = bounds.y;
float maxX = bounds.x + bounds.width;
float maxY = bounds.y + bounds.height;
float[] vals = null;
final Graphics2D g2 = (Graphics2D) g;
setStroke(form, g2, scaling);
//if specific DecodeParms for each filter, set othereise use global
if (InkListArray != null) {
final int count = InkListArray.length;
float x;
float y;
//If Graphics not set, don't draw anything.
if (g != null) {
final float[] underlineColor = form.getFloatArray(PdfDictionary.C);
Color c1 = new Color(0);
if (underlineColor != null) {
switch (underlineColor.length) {
case 0:
//Should not happen. Do nothing. Annotation is transparent
break;
case 1:
//DeviceGrey colorspace
c1 = new Color(underlineColor[0], underlineColor[0], underlineColor[0], 1.0f);
break;
case 3:
//DeviceRGB colorspace
c1 = new Color(underlineColor[0], underlineColor[1], underlineColor[2], 1.0f);
break;
case 4:
//DeviceCMYK colorspace
final DeviceCMYKColorSpace cmyk = new DeviceCMYKColorSpace();
cmyk.setColor(underlineColor, 4);
c1 = new Color(cmyk.getColor().getRGB());
break;
default:
break;
}
}
g2.setColor(new Color(0.0f, 0.0f, 0.0f, 0.0f));
g2.fillRect(0, 0, bounds.width, bounds.height);
g2.setColor(c1);
g2.setPaint(c1);
}
for (int i = 0; i < count; i++) {
if (InkListArray[i] instanceof byte[]) {
final byte[] decodeByteData = (byte[]) InkListArray[i];
if (vals == null) {
vals = new float[count];
}
if (decodeByteData != null) {
final String val = new String(decodeByteData);
final float v = Float.parseFloat(val) * scaling;
switch (i % 2) {
case 0:
if (v < minX) {
minX = v;
}
if (v > maxX) {
maxX = v;
}
x = (v - bounds.x);
vals[i] = x;
break;
case 1:
if (v < minY) {
minY = v;
}
if (v > maxY) {
maxY = v;
}
y = bounds.height - (v - bounds.y);
vals[i] = y;
break;
}
}
} else {
final float[] r = scanInkListTree((Object[]) InkListArray[i], form, g, scaling);
if (r[0] < minX) {
minX = r[0];
}
if (r[2] > maxX) {
maxX = r[2];
}
if (r[1] < minY) {
minY = r[1];
}
if (r[3] > maxY) {
maxY = r[3];
}
}
}
}
if (vals != null) {
if (vals.length < 4) {
if (g2 != null) {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
final Line2D.Float line = new Line2D.Float(vals[0], vals[1], vals[0], vals[1]);
g2.draw(line);
}
} else {
if (vals.length < 6) { //Only use lines on ink
if (g2 != null) {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
for (int i = 0; i < vals.length; i += 4) {
final Line2D.Float line = new Line2D.Float(vals[0], vals[1], vals[2], vals[3]);
g2.draw(line);
}
}
} else { //Enough armguments so curve ink
final float[] values = createInkCurve(vals);
if (g2 != null) {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
for (int i = 0; i < values.length - 7; i += 6) {
final CubicCurve2D curve = new CubicCurve2D.Double(values[i], values[i + 1], values[i + 2], values[i + 3], values[i + 4], values[i + 5], values[i + 6], values[i + 7]);
g2.draw(curve);
}
}
}
}
}
return new float[]{minX, minY, maxX, maxY};
}
/**
* Method to scan through an InkList array, draw the ink to the provided
* Graphics object at the specified scaling and return the bounds of the InkList
*
* @param InkListArray 2 Dimensional float array representing an InkList
* @param form FormObject representing the Ink annotation this InkList came from
* @param g Graphics object to draw the Ink to
* @param scaling float value representing scaling where 1 equals 100%
* @return float array representing the Inks bounds in the order
* lowest x / y, largest x / x
*/
public static float[] scanInkListFloatTree(final float[][] InkListArray, final PdfObject form, final Graphics g, final float scaling) {
final float[] quad = form.getFloatArray(PdfDictionary.Rect);
if (quad == null) {
return null;
}
Rectangle2D bounds = getFormBounds((FormObject) form, quad, scaling);
final Graphics2D g2 = (Graphics2D) g;
setStroke(form, g2, scaling);
//if specific DecodeParms for each filter, set othereise use global
if (InkListArray != null) {
//If Graphics not set, don't draw anything.
if (g != null) {
final float[] underlineColor = form.getFloatArray(PdfDictionary.C);
Color c1 = new Color(0);
if (underlineColor != null) {
switch (underlineColor.length) {
case 0:
//Should not happen. Do nothing. Annotation is transparent
break;
case 1:
//DeviceGrey colorspace
c1 = new Color(underlineColor[0], underlineColor[0], underlineColor[0], 1.0f);
break;
case 3:
//DeviceRGB colorspace
c1 = new Color(underlineColor[0], underlineColor[1], underlineColor[2], 1.0f);
break;
case 4:
//DeviceCMYK colorspace
final DeviceCMYKColorSpace cmyk = new DeviceCMYKColorSpace();
cmyk.setColor(underlineColor, 4);
c1 = new Color(cmyk.getColor().getRGB());
break;
default:
break;
}
}
g2.setColor(new Color(0.0f, 0.0f, 0.0f, 0.0f));
g2.fillRect(0, 0, (int) bounds.getWidth(), (int) bounds.getHeight());
g2.setColor(c1);
g2.setPaint(c1);
}
for (final float[] coords : InkListArray) {
if (coords.length < 4) {
if (g2 != null) {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
final Line2D.Float line = new Line2D.Float(coords[0], coords[1], coords[0], coords[1]);
g2.draw(line);
}
} else {
if (coords.length < 6) { //Only use lines on ink
if (g2 != null) {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
for (int i = 0; i < coords.length; i += 4) {
final Line2D.Float line = new Line2D.Float(coords[0], coords[1], coords[2], coords[3]);
g2.draw(line);
}
}
} else { //Enough armguments so curve ink
final float[] values = createInkCurve(coords);
if (g2 != null) {
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
for (int i = 0; i < values.length - 7; i += 6) {
final CubicCurve2D curve = new CubicCurve2D.Double(values[i], values[i + 1], values[i + 2], values[i + 3], values[i + 4], values[i + 5], values[i + 6], values[i + 7]);
g2.draw(curve);
bounds = bounds.createUnion(curve.getBounds2D());
}
}
}
}
}
}
form.setFloatArray(PdfDictionary.Rect, new float[]{(float) bounds.getMinX(), (float) bounds.getMinY(), (float) bounds.getMaxX(), (float) bounds.getMaxY()});
return form.getFloatArray(PdfDictionary.Rect);
}
private static Color convertFloatArrayToColor(final float[] values) {
Color c = new Color(0, 0, 0, 0);
if (values != null) {
switch (values.length) {
case 0:
//Should not happen. Do nothing. Annotation is transparent
break;
case 1:
//DeviceGrey colorspace
c = new Color(values[0], values[0], values[0]);
break;
case 3:
//DeviceRGB colorspace
c = new Color(values[0], values[1], values[2]);
break;
case 4:
//DeviceCMYK colorspace
final DeviceCMYKColorSpace cmyk = new DeviceCMYKColorSpace();
cmyk.setColor(values, 4);
c = new Color(cmyk.getColor().getRGB());
c = new Color(c.getRed(), c.getGreen(), c.getBlue());
break;
default:
break;
}
}
return c;
}
private static Rectangle getFormBounds(final FormObject form, final float[] rect, final float scaling) {
final Rectangle bounds = (form).getBoundingRectangle();
//Bounds is 0 so calculate based on rect areas
if (bounds.getWidth() == 0 && bounds.getHeight() == 0) {
for (int i = 0; i != rect.length; i++) {
if (i % 2 == 0) {
if (bounds.x > rect[i]) {
bounds.x = (int) rect[i];
}
if (bounds.x + bounds.width < rect[i]) {
bounds.width = (int) (rect[i] - bounds.x);
}
} else {
if (bounds.y > rect[i]) {
bounds.y = (int) rect[i];
}
if (bounds.y + bounds.height < rect[i]) {
bounds.height = (int) (rect[i] - bounds.y);
}
}
}
}
bounds.x = (int) ((bounds.x * scaling) + 0.5f);
bounds.y = (int) ((bounds.y * scaling) + 0.5f);
bounds.width = (int) ((bounds.width * scaling) + 0.5f);
bounds.height = (int) ((bounds.height * scaling) + 0.5f);
return bounds;
}
private static BufferedImage getStrickOutIcon(final PdfObject form) {
final Color color = convertFloatArrayToColor(form.getFloatArray(PdfDictionary.C));
float[] quad = form.getFloatArray(PdfDictionary.QuadPoints);
if (quad == null) {
quad = form.getFloatArray(PdfDictionary.Rect);
}
final Rectangle bounds = getFormBounds((FormObject) form, quad, 1.0f);
if (bounds.width > 0 && bounds.height > 0) {
final BufferedImage icon = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_4BYTE_ABGR);
final Graphics g = icon.getGraphics();
if (quad.length >= 8) {
for (int hi = 0; hi != quad.length; hi += 8) {
final int x = (int) quad[hi] - bounds.x;
int y = (int) quad[hi + 5] - bounds.y;
//Adjust y for display
y = (bounds.height - y) - (int) (quad[hi + 1] - quad[hi + 5]);
final int width = (int) (quad[hi + 2] - quad[hi]);
final int height = (int) (quad[hi + 1] - quad[hi + 5]);
try {
g.setColor(new Color(0.0f, 0.0f, 0.0f, 0.0f));
g.fillRect(0, 0, width, height);
g.setColor(color);
g.fillRect(x, y + (height / 2), width, 1);
} catch (final Exception e) {
LogWriter.writeLog("Exception: " + e.getMessage());
}
}
}
return icon;
}
return null;
}
private static BufferedImage getUnderLineIcon(final PdfObject form) {
final Color color = convertFloatArrayToColor(form.getFloatArray(PdfDictionary.C));
float[] quad = form.getFloatArray(PdfDictionary.QuadPoints);
if (quad == null) {
quad = form.getFloatArray(PdfDictionary.Rect);
}
final Rectangle bounds = getFormBounds((FormObject) form, quad, 1.0f);
if (bounds.width > 0 && bounds.height > 0) {
final BufferedImage icon = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_4BYTE_ABGR);
final Graphics g = icon.getGraphics();
if (quad.length >= 8) {
for (int hi = 0; hi != quad.length; hi += 8) {
final int x = (int) quad[hi] - bounds.x;
int y = (int) quad[hi + 5] - bounds.y;
//Adjust y for display
y = (bounds.height - y) - (int) (quad[hi + 1] - quad[hi + 5]);
final int width = (int) (quad[hi + 2] - quad[hi]);
final int height = (int) (quad[hi + 1] - quad[hi + 5]);
try {
g.setColor(new Color(0.0f, 0.0f, 0.0f, 0.0f));
g.fillRect(x, y, width, height);
g.setColor(color);
g.fillRect(x, y + height - 1, width, 1);
} catch (final Exception e) {
LogWriter.writeLog("Exception: " + e.getMessage());
}
}
}
return icon;
}
return null;
}
private static BufferedImage getSquigglyIcon(final PdfObject form) {
final Color color = convertFloatArrayToColor(form.getFloatArray(PdfDictionary.C));
float[] quad = form.getFloatArray(PdfDictionary.QuadPoints);
if (quad == null) {
quad = form.getFloatArray(PdfDictionary.Rect);
}
final Rectangle bounds = getFormBounds((FormObject) form, quad, 1.0f);
if (bounds.width > 0 && bounds.height > 0) {
final BufferedImage icon = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_4BYTE_ABGR);
final Graphics g = icon.getGraphics();
if (quad.length >= 8) {
for (int hi = 0; hi != quad.length; hi += 8) {
final int x = (int) quad[hi] - bounds.x;
int y = (int) quad[hi + 5] - bounds.y;
//Adjust y for display
y = (bounds.height - y) - (int) (quad[hi + 1] - quad[hi + 5]);
final int width = (int) (quad[hi + 2] - quad[hi]);
final int height = (int) (quad[hi + 1] - quad[hi + 5]);
final int step = 6;
final int bottom = y + height - 1;
final int top = bottom - (step / 2);
try {
g.setColor(color);
for (int i = 0; i < width; i += step) {
g.drawLine(x + i, bottom, x + i + (step / 2), top);
g.drawLine(x + i + (step / 2), top, x + i + step, bottom);
}
} catch (final Exception e) {
LogWriter.writeLog("Exception: " + e.getMessage());
}
}
}
return icon;
}
return null;
}
private static BufferedImage getSquareIcon(final PdfObject form, final float scaling) {
final Color c = convertFloatArrayToColor(form.getFloatArray(PdfDictionary.C));
final Color ic = convertFloatArrayToColor(form.getFloatArray(PdfDictionary.IC));
final float[] quad = form.getFloatArray(PdfDictionary.Rect);
if (quad != null) {
final Rectangle bounds = getFormBounds((FormObject) form, quad, scaling);
if (bounds.width > 0 && bounds.height > 0) {
final BufferedImage icon = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_4BYTE_ABGR);
final Graphics2D g = (Graphics2D) icon.getGraphics();
final int width = setStroke(form, g, scaling);
g.setColor(ic);
g.fillRect(0, 0, bounds.width, bounds.height);
g.setColor(c);
g.drawRect(width / 2, width / 2, bounds.width - width, bounds.height - width);
// FormRenderUtilsG2.renderBorder(g, (FormObject)form, 0,0,icon.getWidth(), icon.getHeight());
return icon;
}
}
//Return a small empty image as no highlight to make.
return null;
}
private static int setStroke(final PdfObject form, final Graphics2D g, final float scaling) {
int borderWidth = 1;
final PdfObject BS = form.getDictionary(PdfDictionary.BS);
if (BS != null && g != null) {
final String s = BS.getName(PdfDictionary.S);
borderWidth = BS.getInt(PdfDictionary.W);
if (borderWidth == -1) {
borderWidth = 1;
}
final PdfArrayIterator d = BS.getMixedArray(PdfDictionary.D);
if (s == null || s.equals("S")) {
borderWidth *= scaling;
g.setStroke(new BasicStroke(borderWidth));
} else {
if (s.equals("D")) {
float[] dash = {3};
if (d != null && d.hasMoreTokens()) {
final int count = d.getTokenCount();
if (count > 0) {
dash = d.getNextValueAsFloatArray();
}
}
borderWidth *= scaling;
g.setStroke(new BasicStroke(borderWidth, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 10.0f, dash, 0));
}
}
}
if (borderWidth < 1) {
borderWidth = 1;
}
return borderWidth;
}
private static BufferedImage getCircleIcon(final PdfObject form, final float scaling) {
final Color c = convertFloatArrayToColor(form.getFloatArray(PdfDictionary.C));
final Color ic = convertFloatArrayToColor(form.getFloatArray(PdfDictionary.IC));
final float[] quad = form.getFloatArray(PdfDictionary.Rect);
if (quad != null) {
final Rectangle bounds = getFormBounds((FormObject) form, quad, scaling);
if (bounds.width > 0 && bounds.height > 0) {
final BufferedImage icon = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_4BYTE_ABGR);
final Graphics2D g = (Graphics2D) icon.getGraphics();
final int width = setStroke(form, g, scaling);
g.setColor(ic);
g.fillOval((width / 2), (width / 2), bounds.width - width, bounds.height - width);
g.setColor(c);
g.drawOval((width / 2), (width / 2), bounds.width - width, bounds.height - width);
return icon;
}
}
//Return a small empty image as no highlight to make.
return null;
}
private static BufferedImage getLineIcon(final PdfObject form, final float scaling) {
final Color c = convertFloatArrayToColor(form.getFloatArray(PdfDictionary.C));
final float[] quad = form.getFloatArray(PdfDictionary.Rect);
final float[] line = form.getFloatArray(PdfDictionary.L);
if (quad != null && line != null) {
final Rectangle bounds = getFormBounds((FormObject) form, quad, scaling);
if (bounds.width > 0 && bounds.height > 0) {
final BufferedImage icon = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_4BYTE_ABGR);
final Graphics2D g = (Graphics2D) icon.getGraphics();
setStroke(form, g, scaling);
g.setColor(c);
g.drawLine((int) (line[0] * scaling) - bounds.x, (int) (bounds.height - ((line[1] * scaling) - bounds.y)), (int) (line[2] * scaling) - bounds.x, (int) (bounds.height - ((line[3] * scaling) - bounds.y)));
return icon;
}
}
//Return a small empty image as no highlight to make.
return null;
}
private static BufferedImage getPolyIcon(final PdfObject form, final boolean line, final float scaling) {
final Color c = convertFloatArrayToColor(form.getFloatArray(PdfDictionary.C));
final Color ic = convertFloatArrayToColor(form.getFloatArray(PdfDictionary.IC));
final float[] quad = form.getFloatArray(PdfDictionary.Rect);
final float[] vertices = form.getFloatArray(PdfDictionary.Vertices);
if (quad != null && vertices != null) {
final Rectangle bounds = getFormBounds((FormObject) form, quad, scaling);
if (bounds.width > 0 && bounds.height > 0) {
final BufferedImage icon = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_4BYTE_ABGR);
final Graphics2D g = (Graphics2D) icon.getGraphics();
setStroke(form, g, scaling);
final GeneralPath gPath = new GeneralPath(Path2D.WIND_NON_ZERO);
gPath.moveTo((int) (vertices[0] * scaling) - bounds.x, (int) (bounds.height - ((vertices[1] * scaling) - bounds.y)));
for (int i = 2; i != vertices.length; i += 2) {
gPath.lineTo((int) (vertices[i] * scaling) - bounds.x, (int) (bounds.height - ((vertices[i + 1] * scaling) - bounds.y)));
}
if (!line) {
gPath.lineTo((int) (vertices[0] * scaling) - bounds.x, (int) (bounds.height - ((vertices[1] * scaling) - bounds.y)));
g.setColor(ic);
g.fill(gPath);
}
g.setColor(c);
g.draw(gPath);
return icon;
}
}
//Return a small empty image as no highlight to make.
return null;
}
private static BufferedImage getCaretIcon(final PdfObject form) {
final Color c = convertFloatArrayToColor(form.getFloatArray(PdfDictionary.C));
final float[] rd = form.getFloatArray(PdfDictionary.RD);
final float[] quad = form.getFloatArray(PdfDictionary.Rect);
if (quad != null) {
final Rectangle bounds = getFormBounds((FormObject) form, quad, 1.0f);
if (bounds.width > 0 && bounds.height > 0) {
final BufferedImage icon = new BufferedImage(bounds.width, bounds.height, BufferedImage.TYPE_4BYTE_ABGR);
final Graphics2D g = (Graphics2D) icon.getGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g.setStroke(new BasicStroke(rd[1]));
g.setColor(c);
g.drawLine(0, bounds.height, bounds.width / 2, 0);
g.drawLine(bounds.width / 2, 0, bounds.width, bounds.height);
return icon;
}
}
//Return a small empty image as no highlight to make.
return null;
}
private static BufferedImage getHightlightIcon(final PdfObject form, final float scaling) {
final float[] f = form.getFloatArray(PdfDictionary.C);
Color c = new Color(0);
if (f != null) {
switch (f.length) {
case 0:
//Should not happen. Do nothing. Annotation is transparent
break;
case 1:
//DeviceGrey colorspace
c = new Color(f[0], f[0], f[0], 0.5f);
break;
case 3:
//DeviceRGB colorspace
c = new Color(f[0], f[1], f[2], 0.5f);
break;
case 4:
//DeviceCMYK colorspace
final DeviceCMYKColorSpace cmyk = new DeviceCMYKColorSpace();
cmyk.setColor(f, 4);
c = new Color(cmyk.getColor().getRGB());
c = new Color(c.getRed(), c.getGreen(), c.getBlue(), 0.5f);
break;
default:
break;
}
}
final float[] quad = form.getFloatArray(PdfDictionary.QuadPoints);
if (quad != null) {
final Rectangle bounds = ((FormObject) form).getBoundingRectangle();
//Bounds is 0 so calculate based on quad areas
if (bounds.getWidth() == 0 && bounds.getHeight() == 0) {
for (int i = 0; i != quad.length; i++) {
if (i % 2 == 0) {
if (bounds.x > quad[i]) {
bounds.x = (int) quad[i];
}
if (bounds.x + bounds.width < quad[i]) {
bounds.width = (int) (quad[i] - bounds.x);
}
} else {
if (bounds.y > quad[i]) {
bounds.y = (int) quad[i];
}
if (bounds.y + bounds.height < quad[i]) {
bounds.height = (int) (quad[i] - bounds.y);
}
}
}
}
final int scaledWidth = (int) (bounds.width * scaling);
final int scaledHeight = (int) (bounds.height * scaling);
if (scaledWidth > 0 && scaledHeight * scaling > 0) {
final BufferedImage icon = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_4BYTE_ABGR);
final Graphics g = icon.getGraphics();
if (quad.length >= 8) {
for (int hi = 0; hi != quad.length; hi += 8) {
float x1 = quad[hi];
float x2 = quad[hi + 2];
float y1 = quad[hi + 1];
float y2 = quad[hi + 3];
for (int j = 0; j < 8; j++) {
// If even
if (j % 2 == 0) {
if (quad[hi + j] < x1) {
x1 = quad[hi + j];
}
if (quad[hi + j] > x2) {
x2 = quad[hi + j];
}
} else {
if (quad[hi + j] < y1) {
y1 = quad[hi + j];
}
if (quad[hi + j] > y2) {
y2 = quad[hi + j];
}
}
}
final int x = (int) x1 - bounds.x;
int y = (int) y1 - bounds.y;
// Adjust y for display
y = (bounds.height - y) - (int) (y2 - y1);
final int width = (int) (x2 - x1);
final int height = (int) (y2 - y1);
final Rectangle rh = new Rectangle((int) (x * scaling), (int) (y * scaling), (int) (width * scaling), (int) (height * scaling));
g.setColor(c);
g.fillRect(rh.x, rh.y, rh.width, rh.height);
}
}
return icon;
}
}
//Return a small empty image as no highlight to make.
return null;
}
/**
* Get the Icon to be used for Text annotations
*
* @param form PdfObject representing a TextAnnotation
* @return BufferedImage representing the icon for this annotation
*/
public static BufferedImage getTextIcon(final PdfObject form) {
final String iconFile = getPngImageForAnnotation(form);
BufferedImage commentIcon = null;
try {
commentIcon = ImageIO.read(AnnotationFactory.class.getResource(iconFile));
} catch (final IOException e) {
LogWriter.writeLog("Exception: " + e.getMessage());
}
setColorForAnnotation(form, commentIcon);
return commentIcon;
}
private static final int STYLE_KEY_FONT = 1718578804;
private static final int STYLE_KEY_TEXT = 1952807028;
private static final int STYLE_KEY_COLOR = 1668246639;
/**
* Load the font style described in a DS string into a Component for use by
* some annotations (e.g FreeText annotations).
*
* @param DS byte[] representing the DS string
* @param textInput Component to be used by a given annotation
* @param scaling float value representing scaling where 1 is equal to 100%
*/
public static void loadFontValues(final byte[] DS, final Component textInput, final float scaling) {
Font font = new Font("Lucida", Font.PLAIN, 12);
int position = 0;
while (position < DS.length) {
while (DS[position] == ' ') {
position++;
}
final int key = (DS[position++] << 24) + (DS[position++] << 16) + (DS[position++] << 8) + (DS[position++]);
switch (key) {
case STYLE_KEY_FONT:
switch (DS[position]) {
case 58: //font
position++;
if (DS[position] == ' ') {
position++;
}
final StringBuilder name = new StringBuilder();
while (position < DS.length && DS[position] != ' ') {
name.append((char) DS[position]);
position++;
}
position++;
final StringBuilder size = new StringBuilder();
while (position < DS.length && (DS[position] >= '0' && DS[position] <= '9')) { //ignore decimal as we use integer in java
size.append((char) DS[position]);
position++;
}
//Progress to end
while (position < DS.length && DS[position] != ';') {
position++;
}
position++;
String fontName = StandardFonts.expandName(name.toString());
final String altName = FontMappings.fontSubstitutionAliasTable.get(fontName.toLowerCase());
if (altName != null) {
fontName = altName;
}
fontName = StandardFonts.expandName(fontName);
if (size.length() > 0) {
final int fontSize = (int) (Integer.parseInt(size.toString()) * scaling);
font = new Font(fontName, Font.PLAIN, fontSize);
}
break;
case 45: //font-stretch
//Do nothing for now
while (position < DS.length && DS[position] != ';') {
position++;
}
position++;
break;
default:
LogWriter.writeLog("Unknown style key for FreeText annotation.");
}
break;
case STYLE_KEY_TEXT: //text-align
while (position < DS.length && DS[position] != ';') {
position++;
}
position++;
break;
case STYLE_KEY_COLOR: //color as hex values
while (position < DS.length && DS[position] != ':') {
position++;
}
position++;
if (DS[position] == ' ') {
position++;
}
if (DS[position] == '#') {
final StringBuilder colString = new StringBuilder();
while (position < DS.length && DS[position] != ';') {
colString.append((char) DS[position]);
position++;
}
final Color c = new Color(
Integer.valueOf(colString.substring(1, 3), 16),
Integer.valueOf(colString.substring(3, 5), 16),
Integer.valueOf(colString.substring(5, 7), 16));
textInput.setForeground(c);
} else {
LogWriter.writeLog("Unknown color for FreeText annotation.");
}
break;
}
}
textInput.setFont(font);
}
private static void setColorForAnnotation(final PdfObject form, final BufferedImage commentIcon) {
//Set color of annotation
float[] col = form.getFloatArray(PdfDictionary.C);
if (col == null) { //If not color set we should use white
col = new float[]{1.0f, 1.0f, 1.0f};
}
final Color c = new Color(col[0], col[1], col[2]);
final int rgb = c.getRGB();
//Replace default color with specified color
for (int x = 0; x != commentIcon.getWidth(); x++) {
for (int y = 0; y != commentIcon.getHeight(); y++) {
//Checks for yellow (R255,G255,B000) and replaces with color
if (commentIcon.getRGB(x, y) == -256) {
commentIcon.setRGB(x, y, rgb);
}
}
}
}
/**
* Get the image to be used to display an annotation based on the Name
* variable it contains.
* This method is intended for internal use with Text annotations only.
*
* @param form PdfObject representing a form Object
* @return String value representing the location of the image to use
*/
public static String getPngImageForAnnotation(final PdfObject form) {
String name = form.getName(PdfDictionary.Name);
if (name == null) {
name = "Note";
}
/* Name of the icon image to use for the icon of this annotation
* - predefined icons are needed for names:-
* Comment, Key, Note, Help, NewParagraph, Paragraph, Insert
*/
if (name.equals("Comment")) {
return "/org/jpedal/objects/acroforms/res/comment.png";
} else if (name.equals("Check")) {
return "/org/jpedal/objects/acroforms/res/Check.png";
} else if (name.equals("Checkmark")) {
return "/org/jpedal/objects/acroforms/res/Checkmark.png";
} else if (name.equals("Circle")) {
return "/org/jpedal/objects/acroforms/res/Circle.png";
} else if (name.equals("Cross")) {
return "/org/jpedal/objects/acroforms/res/Cross.png";
} else if (name.equals("CrossHairs")) {
return "/org/jpedal/objects/acroforms/res/CrossHairs.png";
} else if (name.equals("Help")) {
return "/org/jpedal/objects/acroforms/res/Help.png";
} else if (name.equals("Insert")) {
return "/org/jpedal/objects/acroforms/res/InsertText.png";
} else if (name.equals("Key")) {
return "/org/jpedal/objects/acroforms/res/Key.png";
} else if (name.equals("NewParagraph")) {
return "/org/jpedal/objects/acroforms/res/NewParagraph.png";
} else if (name.equals("Paragraph")) {
return "/org/jpedal/objects/acroforms/res/Paragraph.png";
} else if (name.equals("RightArrow")) {
return "/org/jpedal/objects/acroforms/res/RightArrow.png";
} else if (name.equals("RightPointer")) {
return "/org/jpedal/objects/acroforms/res/RightPointer.png";
} else if (name.equals("Star")) {
return "/org/jpedal/objects/acroforms/res/Star.png";
} else if (name.equals("UpLeftArrow")) {
return "/org/jpedal/objects/acroforms/res/Up-LeftArrow.png";
} else if (name.equals("UpArrow")) {
return "/org/jpedal/objects/acroforms/res/UpArrow.png";
} else { //Default option. Name = Note
return "/org/jpedal/objects/acroforms/res/TextNote.png";
}
}
private static BufferedImage getFileAttachmentIcon() {
final String iconFile = "/org/jpedal/objects/acroforms/res/FileAttachment.png";
BufferedImage icon = null;
try {
icon = ImageIO.read(AnnotationFactory.class.getResource(iconFile));
} catch (final IOException e) {
LogWriter.writeLog("Exception: " + e.getMessage());
}
return icon;
}
private static BufferedImage getSoundIcon(final PdfObject form) {
String iconFile = "/org/jpedal/objects/acroforms/res/Speaker.png";
final String name = form.getName(PdfDictionary.Name);
if (name != null && name.equals("Mic")) {
iconFile = "/org/jpedal/objects/acroforms/res/Microphone.png";
}
BufferedImage icon = null;
try {
icon = ImageIO.read(AnnotationFactory.class.getResource(iconFile));
} catch (final IOException e) {
LogWriter.writeLog("Exception: " + e.getMessage());
}
return icon;
}
/**
* Method to create an icon to represent the annotation and render it.
*
* @param form :: PdfObject to hold the annotation data
* @param current :: DynamicVectorRender to draw the annotation
* @param pageNumber :: Int value of the page number
* @param rotation :: Int value of the page rotation
*/
public static void renderFlattenedAnnotation(final PdfObject form, final DynamicVectorRenderer current, final int pageNumber, final int rotation) {
final BufferedImage image = AnnotationFactory.getIcon(form);
if (image != null) {
final GraphicsState gs = new GraphicsState();
/*
* now draw the finished image of the form
*/
final int iconHeight = image.getHeight();
final int iconWidth = image.getWidth();
final float[] rect = form.getFloatArray(PdfDictionary.Rect);
//Some Text annotations can have incorrect sizes so correct to icon size
if (form.getParameterConstant(PdfDictionary.Subtype) == PdfDictionary.Text) {
rect[2] = rect[0] + iconWidth;
rect[1] = rect[3] - iconHeight;
form.setFloatArray(PdfDictionary.Rect, rect);
//4 needed as we upsample by a factor of 4
switch (rotation % 360) {
case 0:
gs.CTM = new float[][]{{iconWidth, 0, 1}, {0, iconHeight, 1}, {0, 0, 0}};
gs.x = rect[0];
gs.y = rect[3] - iconHeight;
// Draw onto image
gs.CTM[2][0] = rect[0];
gs.CTM[2][1] = rect[3] - iconHeight;
break;
case 90:
gs.CTM = new float[][]{{0, iconWidth, 1}, {-iconHeight, 0, 1}, {0, 0, 0}};
gs.x = rect[0] + iconHeight;
gs.y = rect[3];
// Draw onto image
gs.CTM[2][0] = rect[0] + iconHeight;
gs.CTM[2][1] = rect[3];
break;
case 180:
gs.CTM = new float[][]{{-iconWidth, 0, 1}, {0, -iconHeight, 1}, {0, 0, 0}};
gs.x = rect[0];
gs.y = rect[3] + iconHeight;
// Draw onto image
gs.CTM[2][0] = rect[0];
gs.CTM[2][1] = rect[3] + iconHeight;
break;
case 270:
gs.CTM = new float[][]{{0, -iconWidth, 1}, {iconHeight, 0, 1}, {0, 0, 0}};
gs.x = rect[0] - iconHeight;
gs.y = rect[3];
// Draw onto image
gs.CTM[2][0] = rect[0] - iconHeight;
gs.CTM[2][1] = rect[3];
break;
}
} else {
// Code in switch statement was causing issues with 15jan/21353.pdf, rotating highlight annotation incorrectly
// Icon contains no rotation so no need to factor out
gs.CTM = new float[][]{{iconWidth, 0, 1}, {0, iconHeight, 1}, {0, 0, 0}};
gs.x = rect[0];
gs.y = rect[3] - iconHeight;
// Draw onto image
gs.CTM[2][0] = rect[0];
gs.CTM[2][1] = rect[3] - iconHeight;
}
// Hard code blendMode for highlights to ensure correct output
if (form.getParameterConstant(PdfDictionary.Subtype) == PdfDictionary.Highlight) {
current.setGraphicsState(GraphicsState.STROKE, gs.getAlpha(GraphicsState.STROKE), PdfDictionary.Darken);
current.setGraphicsState(GraphicsState.FILL, gs.getAlpha(GraphicsState.FILL), PdfDictionary.Darken);
}
current.drawImage(pageNumber, image, gs, false, form.getObjectRefAsString(), -1);
if (form.getParameterConstant(PdfDictionary.Subtype) == PdfDictionary.Highlight) {
current.setGraphicsState(GraphicsState.STROKE, gs.getAlpha(GraphicsState.STROKE), PdfDictionary.Normal);
current.setGraphicsState(GraphicsState.FILL, gs.getAlpha(GraphicsState.FILL), PdfDictionary.Normal);
}
}
}
/**
* Create a String to represent the current date.
*
* @return String representing the current date.
* The String has the format "D:YYYYMMDDHHMMSS"
* For instance January 1, 2000, 11:58:55 become D:20000101115855
*/
public static String getCurrentDateAsString() {
final Calendar cal = Calendar.getInstance();
String date = "D:" + cal.get(Calendar.YEAR) + String.format("%02d", (cal.get(Calendar.MONTH)) + 1) + String.format("%02d", cal.get(Calendar.DAY_OF_MONTH)) + String.format("%02d", cal.get(Calendar.HOUR_OF_DAY)) + String.format("%02d", cal.get(Calendar.MINUTE)) + String.format("%02d", cal.get(Calendar.SECOND));
final int offset = cal.getTimeZone().getOffset(System.currentTimeMillis());
if (offset > 0) {
date += '+';
}
if (offset != 0) {
date += String.format("%02d", ((offset / 1000) / 3600));
date += '\'';
date += String.format("%02d", ((offset / 1000) % 3600) / 60);
date += '\'';
}
return date;
}
/**
* Method to create a curve from a series of points. This is used internally to generate the curve in ink annotations.
*
* @param points float array of values to use as point in the order x coord, y coords...
*
* @return float array of coords in alternating x / y order. The values being with a start coordinate at index 0 and 1.
* The array then continues with 2 control points (next 4 values) and an end point for this curve which is also the
* start point of the next curve (next 2 values). This repeats until the final end point is reached.
*/
public static float[] createInkCurve(final float[] points) {
float[] inkPoint = new float[points.length];
inkPoint[0] = points[0];
inkPoint[1] = points[1];
int duplicateIDX = 2;
for (int i = 2; i != points.length; i += 2) {
if (inkPoint[duplicateIDX - 2] != points[i] ||
inkPoint[duplicateIDX - 1] != points[i + 1]) {
inkPoint[duplicateIDX] = points[i];
inkPoint[duplicateIDX + 1] = points[i + 1];
duplicateIDX += 2;
}
}
inkPoint = Arrays.copyOf(inkPoint, duplicateIDX);
final float[] curvePoints = new float[(inkPoint.length * 3) - 4];
curvePoints[0] = inkPoint[0];
curvePoints[1] = inkPoint[1];
float[] controlPoints = getControlPoints(inkPoint[0], inkPoint[1], inkPoint[0], inkPoint[1], inkPoint[2], inkPoint[3]);
curvePoints[2] = controlPoints[2];
curvePoints[3] = controlPoints[3];
int idx = 4;
for (int i = 2; i != inkPoint.length - 2; i += 2) {
controlPoints = getControlPoints(inkPoint[i - 2], inkPoint[i - 1], inkPoint[i], inkPoint[i + 1], inkPoint[i + 2], inkPoint[i + 3]);
curvePoints[idx] = controlPoints[0];
idx++;
curvePoints[idx] = controlPoints[1];
idx++;
curvePoints[idx] = inkPoint[i];
idx++;
curvePoints[idx] = inkPoint[i + 1];
idx++;
curvePoints[idx] = controlPoints[2];
idx++;
curvePoints[idx] = controlPoints[3];
idx++;
}
controlPoints = getControlPoints(inkPoint[inkPoint.length - 4], inkPoint[inkPoint.length - 3], inkPoint[inkPoint.length - 2],
inkPoint[inkPoint.length - 1], inkPoint[inkPoint.length - 2], inkPoint[inkPoint.length - 1]);
curvePoints[curvePoints.length - 4] = controlPoints[0];
curvePoints[curvePoints.length - 3] = controlPoints[1];
curvePoints[curvePoints.length - 2] = inkPoint[inkPoint.length - 2];
curvePoints[curvePoints.length - 1] = inkPoint[inkPoint.length - 1];
return curvePoints;
}
/**
* Generate to control points for a given point.
* This method generates the control points for the curve connecting this point to the previous and next points.
*
* @param p1x x coord of the previous point
* @param p1y y coord of the next point
* @param p2x x coords of the point we are creating control points for
* @param p2y y coords of the point we are creating control points for
* @param p3x x coord of the previous point
* @param p3y y coord of the next point
*
* @return float array with a length of 4. The first 2 values are the x and y coords of the control point for p2
* whose curve ends at p1. The last 2 values are the x and y coords of the control point for p2 whose curve ends at p3.
*/
private static float[] getControlPoints(final float p1x, final float p1y,
final float p2x, final float p2y,
final float p3x, final float p3y) {
/*
* The control points created follow the same angled line as the line from p1 to p3.
*
* The control point for p2 curving to p1 follows the line and direction from p3 to p1.
* It is placed on this line at 1 third the distance of p2 to p1.
*
* The control point for p2 curving to p3 follows the line and direction from p1 to p3.
* It is placed on this line at 1 third the distance of p2 to p3.
*
* To find these points we just perform some basic trigonometry detailed below.
*/
final float[] controls = new float[4];
//Get the sides of a right angled triangle where p3 to p1 is the longest side (sideC).
final float sideA = p1x - p3x;
final float sideB = p1y - p3y;
final float sideC = (float) Math.sqrt((sideA * sideA) + (sideB * sideB));
//Control 1
//Get the control points distance from p2
final float length1 = (float) Math.sqrt(((p1x - p2x) * (p1x - p2x)) + ((p1y - p2y) * (p1y - p2y))) / 3f;
//Find scaling to scale sideC to the control point distance
final float scale1 = length1 / sideC;
//Scale down te other sides and add them to p2 to get the control point position, add value to follow the line
controls[0] = (p2x + (sideA * scale1));
controls[1] = (p2y + (sideB * scale1));
//Control 2
//Get the control points distance from p2
final float length2 = (float) Math.sqrt(((p2x - p3x) * (p2x - p3x)) + ((p2y - p3y) * (p2y - p3y))) / 3f;
//Find scaling to scale sideC to the control point distance
final float scale2 = length2 / sideC;
//Scale down te other sides and add them to p2 to get the control point position, instead of creating a new triangle
//we can just minus the values from p2 to get the same results
controls[2] = (p2x - (sideA * scale2));
controls[3] = (p2y - (sideB * scale2));
return controls;
}
}