/* * 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.Name; import org.icepdf.core.pobjects.Resources; import org.icepdf.core.pobjects.acroform.FieldDictionary; import org.icepdf.core.pobjects.acroform.InteractiveForm; 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.Rectangle2D; import java.util.HashMap; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Abstract base class for Widget annotations types, button, choice and text. * * @since 5.1 */ public abstract class AbstractWidgetAnnotation<T extends FieldDictionary> extends Annotation { /** * Indicates that the annotation has no highlight effect. */ public static final Name HIGHLIGHT_NONE = new Name("N"); protected static final Logger logger = Logger.getLogger(AbstractWidgetAnnotation.class.toString()); /** * Transparency value used to simulate text highlighting. */ protected static float highlightAlpha = 0.1f; // text selection colour protected static Color highlightColor; private boolean enableHighlightedWidget; static { // sets the background colour of the annotation highlight try { String color = Defs.sysProperty( "org.icepdf.core.views.page.annotation.widget.highlight.color", "#CC00FF"); int colorValue = ColorUtil.convertColor(color); highlightColor = new Color(colorValue >= 0 ? colorValue : Integer.parseInt("0077FF", 16)); } catch (NumberFormatException e) { if (logger.isLoggable(Level.WARNING)) { logger.warning("Error reading widget highlight colour."); } } try { highlightAlpha = (float) Defs.doubleProperty( "org.icepdf.core.views.page.annotation.widget.highlight.alpha", 0.1f); } catch (NumberFormatException e) { if (logger.isLoggable(Level.WARNING)) { logger.warning("Error reading widget highlight alpha."); } } } protected Name highlightMode; public AbstractWidgetAnnotation(Library l, HashMap h) { super(l, h); Object possibleName = getObject(LinkAnnotation.HIGHLIGHT_MODE_KEY); if (possibleName instanceof Name) { Name name = (Name) possibleName; if (HIGHLIGHT_NONE.equals(name.getName())) { highlightMode = HIGHLIGHT_NONE; } else if (LinkAnnotation.HIGHLIGHT_OUTLINE.equals(name.getName())) { highlightMode = LinkAnnotation.HIGHLIGHT_OUTLINE; } else if (LinkAnnotation.HIGHLIGHT_PUSH.equals(name.getName())) { highlightMode = LinkAnnotation.HIGHLIGHT_PUSH; } } else { highlightMode = LinkAnnotation.HIGHLIGHT_INVERT; } } @Override public void init() throws InterruptedException { super.init(); // check to make sure the field value matches the content stream. InteractiveForm interactiveForm = library.getCatalog().getInteractiveForm(); if (interactiveForm != null && interactiveForm.needAppearances()) { resetAppearanceStream(new AffineTransform()); } // todo check if we have content value but no appearance stream. } public abstract void reset(); @Override public abstract void resetAppearanceStream(double dx, double dy, AffineTransform pageSpace); @Override protected void renderAppearanceStream(Graphics2D g) { Appearance appearance = appearances.get(currentAppearance); if (appearance != null) { AppearanceState appearanceState = appearance.getSelectedAppearanceState(); if (appearanceState != null && appearanceState.getShapes() != null) { // render the main annotation content super.renderAppearanceStream(g); } } // check the highlight widgetAnnotation field and if true we draw a light background colour to mark // the widgets on a page. if (enableHighlightedWidget) { AffineTransform preHighLightTransform = g.getTransform(); g.setColor(highlightColor); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, highlightAlpha)); g.fill(getBbox() != null ? getBbox() : getRectangle()); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)); g.setTransform(preHighLightTransform); } } private Rectangle2D getRectangle() { Rectangle2D origRect = getBbox() != null ? getBbox() : getUserSpaceRectangle(); Rectangle2D.Float jrect = new Rectangle2D.Float(0, 0, (float) origRect.getWidth(), (float) origRect.getHeight()); return jrect; } public abstract T getFieldDictionary(); /** * Generally immediately after the BMC there is a rectangle that defines the actual size of the annotation. If * found we can use this to make many assumptions and regenerate the content stream. * * @param markedContent content stream of the marked content. * @return a rectangle either way, if the q # # # # re isn't found then we use the bbox as a potential bound. */ protected Rectangle2D.Float findBoundRectangle(String markedContent) { int selectionStart = markedContent.indexOf("q") + 1; int selectionEnd = markedContent.indexOf("re"); if (selectionStart < selectionEnd && selectionEnd > 0) { String potentialNumbers = markedContent.substring(selectionStart, selectionEnd); float[] points = parseRectanglePoints(potentialNumbers); if (points != null) { return new Rectangle2D.Float(points[0], points[1], points[2], points[3]); } } // default to the bounding box. Rectangle2D bbox = getBbox(); return new Rectangle2D.Float(1, 1, (float) bbox.getWidth(), (float) bbox.getHeight()); } /** * Finds a rectangle in the marked content. * * @param markedContent content to search for a rectangle. * @return rectangle if found, otherwise bbox is used. */ protected Rectangle2D.Float findRectangle(String markedContent) { int selectionEnd = markedContent.indexOf("re"); if (selectionEnd >= 0) { String potentialNumbers = markedContent.substring(0, selectionEnd); float[] points = parseRectanglePoints(potentialNumbers); if (points != null) { return new Rectangle2D.Float(points[0], points[1], points[2], points[3]); } // default to the bounding box. Rectangle2D bbox = getBbox(); return new Rectangle2D.Float(1, 1, (float) bbox.getWidth(), (float) bbox.getHeight()); } else { return null; } } /** * Get the line height as specified by Th or the font size. * * @param defaultAppearance searchable stream * @return line height, or 13.87 if no reasonable approximation can be found. */ protected double getLineHeight(String defaultAppearance) { if (defaultAppearance != null && checkAppearance(defaultAppearance)) { String sub = defaultAppearance.substring(0, defaultAppearance.indexOf("Tf")); StringTokenizer toker = new StringTokenizer(sub); while (toker.hasMoreTokens()) { Object obj = toker.nextElement(); if (obj instanceof String) { try { double tmp = Double.parseDouble((String) obj); tmp *= 1.15; if (tmp > 0) { return tmp; } } catch (NumberFormatException e) { // intentionally blank. } } } } return 13.87; } protected double getFontSize(String content) { // try and find text size double size = 12; if (content != null) { Pattern pattern = Pattern.compile("\\d+(\\.\\d+)?\\s+Tf"); Matcher matcher = pattern.matcher(content); if (matcher.find()) { String fontDef = content.substring(matcher.start(), matcher.end()); fontDef = fontDef.split(" ")[0]; try { size = Double.parseDouble(fontDef); } catch (NumberFormatException e) { // ignore and move on } if (size < 2) { size = 12; } } } return size; } /** * Encodes the given cotents string into a valid postscript string that is literal encoded. * * @param content current content stream to append literal string to. * @param contents string to be encoded into '(...)' literal format. * @return original content stream with contents encoded in the literal string format. */ protected StringBuilder encodeLiteralString(StringBuilder content, String contents) { String[] lines = contents.split("\n|\r|\f"); for (String line : lines) { content.append('(').append(line.replaceAll("(?=[()\\\\])", "\\\\") .replaceAll("ÿ", "")).append(")' "); } return content; } /** * Encodes the given contents string into a valid postscript hex string. * * @param content current content stream to append literal string to. * @param contents string to be encoded into '<...></...>' hex format. * @return original content stream with contents encoded in the hex string format. */ protected StringBuilder encodeHexString(StringBuilder content, String contents) { String[] lines = contents.split("\n|\r|\f"); for (String line : lines) { char[] chars = line.toCharArray(); StringBuffer hex = new StringBuffer(); for (int i = 0; i < chars.length; i++) { hex.append(Integer.toHexString((int) chars[i])); } content.append('<').append(hex).append(">' "); } return content; } /** * Utility to try and determine if the appearance is valid. * * @param appearance appearance ot test. * @return true if valid, false otherwise. */ protected boolean checkAppearance(String appearance) { // example of a bad appearance, /TiBo 0 Tf 0 g // size is zero and the font can't be found. StringTokenizer toker = new StringTokenizer(appearance); if (toker.hasMoreTokens()) { String fontName = toker.nextToken().substring(1); String fontSize = toker.nextToken(); Appearance appearance1 = appearances.get(currentAppearance); AppearanceState appearanceState = appearance1.getSelectedAppearanceState(); org.icepdf.core.pobjects.fonts.Font font = null; Resources resources = appearanceState.getResources(); if (resources != null) { font = resources.getFont(new Name(fontName)); } return !(font == null || library.getInteractiveFormFont(fontName) == null || fontSize.equals("0")); } return false; } /** * The selection rectangle if present will help define the line height of the text. If not present we can use * the default value 13.87 later which seems to be very common in the samples. * * @param markedContent content to look for "rg # # # # re". * @return selection rectangle, null if not found. */ protected Rectangle2D.Float findSelectionRectangle(String markedContent) { int selectionStart = markedContent.indexOf("rg") + 2; int selectionEnd = markedContent.lastIndexOf("re"); if (selectionStart < selectionEnd && selectionEnd > 0) { String potentialNumbers = markedContent.substring(selectionStart, selectionEnd); float[] points = parseRectanglePoints(potentialNumbers); if (points != null) { return new Rectangle2D.Float(points[0], points[1], points[2], points[3]); } } return null; } /** * Simple utility to write Rectangle2D.Float in postscript. * * @param rect Rectangle2D.Float to convert to postscript. Null value with throw null pointer exception. * @return postscript representation of the rect. */ protected String generateRectangle(Rectangle2D.Float rect) { return rect.x + " " + rect.y + " " + rect.width + " " + rect.height + " re "; } /** * Converts a given string of four numbers into an array of floats. If a conversion error is encountered * null value is returned. * * @param potentialNumbers space separated string of four numbers. * @return list of four numbers, null if string can not be converted. */ protected float[] parseRectanglePoints(String potentialNumbers) { StringTokenizer toker = new StringTokenizer(potentialNumbers); float[] points = new float[4]; int max = toker.countTokens(); Object[] tokens = new Object[max]; for (int i = 0; i < max; i++) { tokens[i] = toker.nextElement(); } boolean notFound = false; for (int i = 3, j = 0; j < 4; j++, i--) { try { points[j] = Float.parseFloat((String) tokens[max - i - 1]); } catch (NumberFormatException e) { notFound = true; } } if (!notFound) { return points; } else { return null; } } /** * Set the static highlight color used to highlight widget annotations. * * @param highlightColor colour of */ public static void setHighlightColor(Color highlightColor) { AbstractWidgetAnnotation.highlightColor = highlightColor; } /** * Set enable highlight on an individual widget. * * @param enableHighlightedWidget true to enable highlight mode, otherwise false. */ public void setEnableHighlightedWidget(boolean enableHighlightedWidget) { this.enableHighlightedWidget = enableHighlightedWidget; } /** * Set the static alpha value uses to paint a color over a widget annotation. * * @param highlightAlpha */ public static void setHighlightAlpha(float highlightAlpha) { AbstractWidgetAnnotation.highlightAlpha = highlightAlpha; } /** * Is enable highlight enabled. * * @return return true if highlight is enabled, false otherwise. */ public boolean isEnableHighlightedWidget() { return enableHighlightedWidget; } }