Author: tilman
Date: Wed Sep 24 14:49:38 2025
New Revision: 1928735

Log:
PDFBOX-5660: optimize, as suggested by Valery Bokov; closes #262

Modified:
   
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDAbstractAppearanceHandler.java

Modified: 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDAbstractAppearanceHandler.java
==============================================================================
--- 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDAbstractAppearanceHandler.java
 Wed Sep 24 14:49:34 2025        (r1928734)
+++ 
pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDAbstractAppearanceHandler.java
 Wed Sep 24 14:49:38 2025        (r1928735)
@@ -1,549 +1,549 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.pdfbox.pdmodel.interactive.annotation.handlers;
-
-import java.awt.geom.AffineTransform;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
-import org.apache.pdfbox.cos.COSStream;
-import org.apache.pdfbox.pdmodel.PDResources;
-import org.apache.pdfbox.pdmodel.common.PDRectangle;
-import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
-import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
-import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
-import org.apache.pdfbox.pdmodel.PDAppearanceContentStream;
-import org.apache.pdfbox.pdmodel.PDDocument;
-import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLine;
-import 
org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationSquareCircle;
-import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
-import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry;
-import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
-
-/**
- * Generic handler to generate the fields appearance.
- * 
- * Individual handler will provide specific implementations for different field
- * types.
- * 
- */
-public abstract class PDAbstractAppearanceHandler implements 
PDAppearanceHandler
-{
-    private final PDAnnotation annotation;
-    protected PDDocument document;
-
-    /**
-     * Line ending styles where the line has to be drawn shorter (minus line 
width).
-     */
-    protected static final Set<String> SHORT_STYLES = createShortStyles();
-
-    static final double ARROW_ANGLE = Math.toRadians(30);
-
-    /**
-     * Line ending styles where there is an interior color.
-     */
-    protected static final Set<String> INTERIOR_COLOR_STYLES = 
createInteriorColorStyles();
-    
-    /**
-     * Line ending styles where the shape changes its angle, e.g. arrows.
-     */
-    protected static final Set<String> ANGLED_STYLES = createAngledStyles();
-
-    public PDAbstractAppearanceHandler(PDAnnotation annotation)
-    {
-        this(annotation, null);
-    }
-
-    public PDAbstractAppearanceHandler(PDAnnotation annotation, PDDocument 
document)
-    {
-        this.annotation = annotation;
-        this.document = document;
-    }
-
-    @Override
-    public void generateAppearanceStreams()
-    {
-        generateNormalAppearance();
-        generateRolloverAppearance();
-        generateDownAppearance();
-    }
-
-    PDAnnotation getAnnotation()
-    {
-        return annotation;
-    }
-
-    PDColor getColor()
-    {
-        return annotation.getColor();
-    }
-
-    PDRectangle getRectangle()
-    {
-        return annotation.getRectangle();
-    }
-    
-    protected COSStream createCOSStream()
-    {
-        return document == null ? new COSStream() : 
document.getDocument().createCOSStream();
-    }
-
-    /**
-     * Get the annotations appearance dictionary.
-     * 
-     * <p>
-     * This will get the annotations appearance dictionary. If this is not
-     * existent an empty appearance dictionary will be created.
-     * 
-     * @return the annotations appearance dictionary
-     */
-    PDAppearanceDictionary getAppearance()
-    {
-        PDAppearanceDictionary appearanceDictionary = 
annotation.getAppearance();
-        if (appearanceDictionary == null)
-        {
-            appearanceDictionary = new PDAppearanceDictionary();
-            annotation.setAppearance(appearanceDictionary);
-        }
-        return appearanceDictionary;
-    }
-
-    /**
-     * Get the annotations normal appearance content stream.
-     * 
-     * <p>
-     * This will get the annotations normal appearance content stream, to 
'draw' to. It will be
-     * uncompressed.
-     *
-     * @return the appearance entry representing the normal appearance.
-     * @throws IOException 
-     */
-    PDAppearanceContentStream getNormalAppearanceAsContentStream() throws 
IOException
-    {
-        return getNormalAppearanceAsContentStream(false);
-    }
-    
-    /**
-     * Get the annotations normal appearance content stream.
-     * 
-     * <p>
-     * This will get the annotations normal appearance content stream, to 
'draw' to.
-     * 
-     * @param compress whether the content stream is to be compressed. Set 
this to true when
-     * creating long content streams.
-     * @return the appearance entry representing the normal appearance.
-     * @throws IOException
-     */
-    PDAppearanceContentStream getNormalAppearanceAsContentStream(boolean 
compress) throws IOException
-    {
-        PDAppearanceEntry appearanceEntry = getNormalAppearance();
-        return getAppearanceEntryAsContentStream(appearanceEntry, compress);
-    }
-    
-    /**
-     * Get the annotations down appearance.
-     * 
-     * <p>
-     * This will get the annotations down appearance. If this is not existent 
an
-     * empty appearance entry will be created.
-     * 
-     * @return the appearance entry representing the down appearance.
-     */
-    PDAppearanceEntry getDownAppearance()
-    {
-        PDAppearanceDictionary appearanceDictionary = getAppearance();
-        PDAppearanceEntry downAppearanceEntry = 
appearanceDictionary.getDownAppearance();
-
-        if (downAppearanceEntry.isSubDictionary())
-        {
-            downAppearanceEntry = new PDAppearanceEntry(createCOSStream());
-            appearanceDictionary.setDownAppearance(downAppearanceEntry);
-        }
-
-        return downAppearanceEntry;
-    }
-
-    /**
-     * Get the annotations rollover appearance.
-     * 
-     * <p>
-     * This will get the annotations rollover appearance. If this is not
-     * existent an empty appearance entry will be created.
-     * 
-     * @return the appearance entry representing the rollover appearance.
-     */
-    PDAppearanceEntry getRolloverAppearance()
-    {
-        PDAppearanceDictionary appearanceDictionary = getAppearance();
-        PDAppearanceEntry rolloverAppearanceEntry = 
appearanceDictionary.getRolloverAppearance();
-
-        if (rolloverAppearanceEntry.isSubDictionary())
-        {
-            rolloverAppearanceEntry = new PDAppearanceEntry(createCOSStream());
-            
appearanceDictionary.setRolloverAppearance(rolloverAppearanceEntry);
-        }
-
-        return rolloverAppearanceEntry;
-    }
-
-    /**
-     * Get a padded rectangle.
-     * 
-     * <p>Creates a new rectangle with padding applied to each side.
-     * .
-     * @param rectangle the rectangle.
-     * @param padding the padding to apply.
-     * @return the padded rectangle.
-     */
-    PDRectangle getPaddedRectangle(PDRectangle rectangle, float padding)
-    {
-        return new PDRectangle(rectangle.getLowerLeftX() + padding, 
rectangle.getLowerLeftY() + padding,
-                rectangle.getWidth() - 2 * padding, rectangle.getHeight() - 2 
* padding);
-    }
-    
-    /**
-     * Get a rectangle enlarged by the differences.
-     *
-     * <p>
-     * Creates a new rectangle with differences added to each side. If there 
are no valid
-     * differences, then the original rectangle is returned.
-     *
-     * @param rectangle the rectangle.
-     * @param differences the differences to apply.
-     * @return the padded rectangle.
-     */
-    PDRectangle addRectDifferences(PDRectangle rectangle, float[] differences)
-    {
-        if (differences == null || differences.length != 4)
-        {
-            return rectangle;
-        }
-        
-        return new PDRectangle(rectangle.getLowerLeftX() - differences[0],
-                rectangle.getLowerLeftY() - differences[1],
-                rectangle.getWidth() + differences[0] + differences[2],
-                rectangle.getHeight() + differences[1] + differences[3]);
-    }
-    
-    /**
-     * Get a rectangle with the differences applied to each side.
-     *
-     * <p>
-     * Creates a new rectangle with differences added to each side. If there 
are no valid
-     * differences, then the original rectangle is returned.
-     *
-     * @param rectangle the rectangle.
-     * @param differences the differences to apply.
-     * @return the padded rectangle.
-     */
-    PDRectangle applyRectDifferences(PDRectangle rectangle, float[] 
differences)
-    {
-        if (differences == null || differences.length != 4)
-        {
-            return rectangle;
-        }
-        return new PDRectangle(rectangle.getLowerLeftX() + differences[0],
-                rectangle.getLowerLeftY() + differences[1],
-                rectangle.getWidth() - differences[0] - differences[2],
-                rectangle.getHeight() - differences[1] - differences[3]);
-    }
-
-    void setOpacity(PDAppearanceContentStream contentStream, float opacity) 
throws IOException
-    {
-        if (opacity < 1)
-        {
-            PDExtendedGraphicsState gs = new PDExtendedGraphicsState();
-            gs.setStrokingAlphaConstant(opacity);
-            gs.setNonStrokingAlphaConstant(opacity);
-
-            contentStream.setGraphicsStateParameters(gs);
-        }
-    }
-
-    /**
-     * Draw a line ending style.
-     * 
-     * @param style
-     * @param cs
-     * @param x
-     * @param y
-     * @param width
-     * @param hasStroke
-     * @param hasBackground
-     * @param ending false if left, true if right of an imagined horizontal 
line (important for
-     * arrows).
-     *
-     * @throws IOException
-     */
-    void drawStyle(String style, final PDAppearanceContentStream cs, float x, 
float y,
-                   float width, boolean hasStroke, boolean hasBackground, 
boolean ending) throws IOException
-    {
-        int sign = ending ? -1 : 1;
-
-        if (PDAnnotationLine.LE_OPEN_ARROW.equals(style) || 
PDAnnotationLine.LE_CLOSED_ARROW.equals(style))
-        {
-            drawArrow(cs, x + sign * width, y, sign * width * 9);
-        }
-        else if (PDAnnotationLine.LE_BUTT.equals(style))
-        {
-            cs.moveTo(x, y - width * 3);
-            cs.lineTo(x, y + width * 3);
-        }
-        else if (PDAnnotationLine.LE_DIAMOND.equals(style))
-        {
-            drawDiamond(cs, x, y, width * 3);
-        }
-        else if (PDAnnotationLine.LE_SQUARE.equals(style))
-        {
-            cs.addRect(x - width * 3, y - width * 3, width * 6, width * 6);
-        }
-        else if (PDAnnotationLine.LE_CIRCLE.equals(style))
-        {
-            drawCircle(cs, x, y, width * 3);
-        }
-        else if (PDAnnotationLine.LE_R_OPEN_ARROW.equals(style) || 
PDAnnotationLine.LE_R_CLOSED_ARROW.equals(style))
-        {
-            drawArrow(cs, x + (-sign) * width, y, (-sign) * width * 9);
-        }
-        else if (PDAnnotationLine.LE_SLASH.equals(style))
-        {
-            // the line is 18 x linewidth at an angle of 60°
-            float width9 = width * 9;
-            cs.moveTo(x + (float) (Math.cos(Math.toRadians(60)) * width9),
-                      y + (float) (Math.sin(Math.toRadians(60)) * width9));
-            cs.lineTo(x + (float) (Math.cos(Math.toRadians(240)) * width9),
-                      y + (float) (Math.sin(Math.toRadians(240)) * width9));
-        }
-
-        if (PDAnnotationLine.LE_R_CLOSED_ARROW.equals(style) || 
-            PDAnnotationLine.LE_CLOSED_ARROW.equals(style))
-        {
-            cs.closePath();
-        }
-        cs.drawShape(width, hasStroke, 
-                     // make sure to only paint a background color (/IC value) 
-                     // for interior color styles, even if an /IC value is set.
-                     INTERIOR_COLOR_STYLES.contains(style) && hasBackground);
-    }
-
-    /**
-     * Add the two arms of a horizontal arrow.
-     * 
-     * @param cs Content stream
-     * @param x
-     * @param y
-     * @param len The arm length. Positive goes to the right, negative goes to 
the left.
-     * 
-     * @throws IOException If the content stream could not be written
-     */
-    void drawArrow(PDAppearanceContentStream cs, float x, float y, float len) 
throws IOException
-    {
-        // strategy for arrows: angle 30°, arrow arm length = 9 * line width
-        // cos(angle) = x position
-        // sin(angle) = y position
-        // this comes very close to what Adobe is doing
-        float armX = x + (float) (Math.cos(ARROW_ANGLE) * len);
-        float armYdelta = (float) (Math.sin(ARROW_ANGLE) * len);
-        cs.moveTo(armX, y + armYdelta);
-        cs.lineTo(x, y);
-        cs.lineTo(armX, y - armYdelta);
-    }
-
-    /**
-     * Add a square diamond shape (corner on top) to the path.
-     *
-     * @param cs Content stream
-     * @param x
-     * @param y
-     * @param r Radius (to a corner)
-     * 
-     * @throws IOException If the content stream could not be written
-     */
-    void drawDiamond(PDAppearanceContentStream cs, float x, float y, float r) 
throws IOException
-    {
-        cs.moveTo(x - r, y);
-        cs.lineTo(x, y + r);
-        cs.lineTo(x + r, y);
-        cs.lineTo(x, y - r);
-        cs.closePath();
-    }
-
-    /**
-     * Add a circle shape to the path in clockwise direction.
-     *
-     * @param cs Content stream
-     * @param x
-     * @param y
-     * @param r Radius
-     * 
-     * @throws IOException If the content stream could not be written.
-     */
-    void drawCircle(PDAppearanceContentStream cs, float x, float y, float r) 
throws IOException
-    {
-        // http://stackoverflow.com/a/2007782/535646
-        float magic = r * 0.551784f;
-        cs.moveTo(x, y + r);
-        cs.curveTo(x + magic, y + r, x + r, y + magic, x + r, y);
-        cs.curveTo(x + r, y - magic, x + magic, y - r, x, y - r);
-        cs.curveTo(x - magic, y - r, x - r, y - magic, x - r, y);
-        cs.curveTo(x - r, y + magic, x - magic, y + r, x, y + r);
-        cs.closePath();
-    }
-
-    /**
-     * Add a circle shape to the path in counterclockwise direction. You'll 
need this e.g. when
-     * drawing a doughnut shape. See "Nonzero Winding Number Rule" for more 
information.
-     *
-     * @param cs Content stream
-     * @param x
-     * @param y
-     * @param r Radius
-     *
-     * @throws IOException If the content stream could not be written.
-     */
-    void drawCircle2(PDAppearanceContentStream cs, float x, float y, float r) 
throws IOException
-    {
-        // http://stackoverflow.com/a/2007782/535646
-        float magic = r * 0.551784f;
-        cs.moveTo(x, y + r);
-        cs.curveTo(x - magic, y + r, x - r, y + magic, x - r, y);
-        cs.curveTo(x - r, y - magic, x - magic, y - r, x, y - r);
-        cs.curveTo(x + magic, y - r, x + r, y - magic, x + r, y);
-        cs.curveTo(x + r, y + magic, x + magic, y + r, x, y + r);
-        cs.closePath();
-    }
-
-    private static Set<String> createShortStyles()
-    {
-        Set<String> shortStyles = new HashSet<String>();
-        shortStyles.add(PDAnnotationLine.LE_OPEN_ARROW);
-        shortStyles.add(PDAnnotationLine.LE_CLOSED_ARROW);
-        shortStyles.add(PDAnnotationLine.LE_SQUARE);
-        shortStyles.add(PDAnnotationLine.LE_CIRCLE);
-        shortStyles.add(PDAnnotationLine.LE_DIAMOND);
-        return Collections.unmodifiableSet(shortStyles);
-    }
-
-    private static Set<String> createInteriorColorStyles()
-    {
-        Set<String> interiorColorStyles = new HashSet<String>();
-        interiorColorStyles.add(PDAnnotationLine.LE_CLOSED_ARROW);
-        interiorColorStyles.add(PDAnnotationLine.LE_CIRCLE);
-        interiorColorStyles.add(PDAnnotationLine.LE_DIAMOND);
-        interiorColorStyles.add(PDAnnotationLine.LE_R_CLOSED_ARROW);
-        interiorColorStyles.add(PDAnnotationLine.LE_SQUARE);
-        return Collections.unmodifiableSet(interiorColorStyles);
-    }
-
-    private static Set<String> createAngledStyles()
-    {
-        Set<String> angledStyles = new HashSet<String>();
-        angledStyles.add(PDAnnotationLine.LE_CLOSED_ARROW);
-        angledStyles.add(PDAnnotationLine.LE_OPEN_ARROW);
-        angledStyles.add(PDAnnotationLine.LE_R_CLOSED_ARROW);
-        angledStyles.add(PDAnnotationLine.LE_R_OPEN_ARROW);
-        angledStyles.add(PDAnnotationLine.LE_BUTT);
-        angledStyles.add(PDAnnotationLine.LE_SLASH);
-        return Collections.unmodifiableSet(angledStyles);
-    }
-
-    /**
-     * Get the annotations normal appearance.
-     * 
-     * <p>
-     * This will get the annotations normal appearance. If this is not existent
-     * an empty appearance entry will be created.
-     * 
-     * @return the appearance entry representing the normal appearance.
-     */
-    private PDAppearanceEntry getNormalAppearance()
-    {
-        PDAppearanceDictionary appearanceDictionary = getAppearance();
-        PDAppearanceEntry normalAppearanceEntry = 
appearanceDictionary.getNormalAppearance();
-
-        if (normalAppearanceEntry == null || 
normalAppearanceEntry.isSubDictionary())
-        {
-            normalAppearanceEntry = new PDAppearanceEntry(createCOSStream());
-            appearanceDictionary.setNormalAppearance(normalAppearanceEntry);
-        }
-
-        return normalAppearanceEntry;
-    }
-    
-    
-    private PDAppearanceContentStream getAppearanceEntryAsContentStream(
-              PDAppearanceEntry appearanceEntry, boolean compress) throws 
IOException
-    {
-        PDAppearanceStream appearanceStream = 
appearanceEntry.getAppearanceStream();
-        setTransformationMatrix(appearanceStream);
-
-        // ensure there are resources
-        PDResources resources = appearanceStream.getResources();
-        if (resources == null)
-        {
-            resources = new PDResources();
-            appearanceStream.setResources(resources);
-        }
-
-        return new PDAppearanceContentStream(appearanceStream, compress);
-    }
-    
-    private void setTransformationMatrix(PDAppearanceStream appearanceStream)
-    {
-        PDRectangle bbox = getRectangle();
-        appearanceStream.setBBox(bbox);
-        AffineTransform transform = 
AffineTransform.getTranslateInstance(-bbox.getLowerLeftX(),
-                -bbox.getLowerLeftY());
-        appearanceStream.setMatrix(transform);
-    }
-
-    PDRectangle handleBorderBox(PDAnnotationSquareCircle annotation, float 
lineWidth)
-    {
-        // There are two options. The handling is not part of the PDF 
specification but
-        // implementation specific to Adobe Reader
-        // - if /RD is set the border box is the /Rect entry inset by the 
respective
-        //   border difference.
-        // - if /RD is not set the border box is defined by the /Rect entry. 
The /RD entry will
-        //   be set to be the line width and the /Rect is enlarged by the /RD 
amount
-        PDRectangle borderBox;
-        float[] rectDifferences = annotation.getRectDifferences();
-        if (rectDifferences.length == 0)
-        {
-            borderBox = getPaddedRectangle(getRectangle(), lineWidth / 2);
-            // the differences rectangle
-            annotation.setRectDifferences(lineWidth / 2);
-            annotation.setRectangle(addRectDifferences(getRectangle(), 
annotation.getRectDifferences()));
-            // when the normal appearance stream was generated BBox and Matrix 
have been set to the
-            // values of the original /Rect. As the /Rect was changed that 
needs to be adjusted too.
-            PDRectangle rect = getRectangle();
-            PDAppearanceStream appearanceStream = 
annotation.getNormalAppearanceStream();
-            AffineTransform transform =
-                    
AffineTransform.getTranslateInstance(-rect.getLowerLeftX(), 
-rect.getLowerLeftY());
-            appearanceStream.setBBox(rect);
-            appearanceStream.setMatrix(transform);
-        }
-        else
-        {
-            borderBox = applyRectDifferences(getRectangle(), rectDifferences);
-            borderBox = getPaddedRectangle(borderBox, lineWidth / 2);
-        }
-        return borderBox;
-    }
-}
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pdfbox.pdmodel.interactive.annotation.handlers;
+
+import java.awt.geom.AffineTransform;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.pdfbox.cos.COSStream;
+import org.apache.pdfbox.pdmodel.PDResources;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
+import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
+import org.apache.pdfbox.pdmodel.PDAppearanceContentStream;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLine;
+import 
org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationSquareCircle;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceEntry;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
+
+/**
+ * Generic handler to generate the fields appearance.
+ * 
+ * Individual handler will provide specific implementations for different field
+ * types.
+ * 
+ */
+public abstract class PDAbstractAppearanceHandler implements 
PDAppearanceHandler
+{
+    private final PDAnnotation annotation;
+    protected PDDocument document;
+
+    /**
+     * Line ending styles where the line has to be drawn shorter (minus line 
width).
+     */
+    protected static final Set<String> SHORT_STYLES = createShortStyles();
+
+    static final double ARROW_ANGLE = Math.toRadians(30);
+
+    /**
+     * Line ending styles where there is an interior color.
+     */
+    protected static final Set<String> INTERIOR_COLOR_STYLES = 
createInteriorColorStyles();
+    
+    /**
+     * Line ending styles where the shape changes its angle, e.g. arrows.
+     */
+    protected static final Set<String> ANGLED_STYLES = createAngledStyles();
+
+    public PDAbstractAppearanceHandler(PDAnnotation annotation)
+    {
+        this(annotation, null);
+    }
+
+    public PDAbstractAppearanceHandler(PDAnnotation annotation, PDDocument 
document)
+    {
+        this.annotation = annotation;
+        this.document = document;
+    }
+
+    @Override
+    public void generateAppearanceStreams()
+    {
+        generateNormalAppearance();
+        generateRolloverAppearance();
+        generateDownAppearance();
+    }
+
+    PDAnnotation getAnnotation()
+    {
+        return annotation;
+    }
+
+    PDColor getColor()
+    {
+        return annotation.getColor();
+    }
+
+    PDRectangle getRectangle()
+    {
+        return annotation.getRectangle();
+    }
+    
+    protected COSStream createCOSStream()
+    {
+        return document == null ? new COSStream() : 
document.getDocument().createCOSStream();
+    }
+
+    /**
+     * Get the annotations appearance dictionary.
+     * 
+     * <p>
+     * This will get the annotations appearance dictionary. If this is not
+     * existent an empty appearance dictionary will be created.
+     * 
+     * @return the annotations appearance dictionary
+     */
+    PDAppearanceDictionary getAppearance()
+    {
+        PDAppearanceDictionary appearanceDictionary = 
annotation.getAppearance();
+        if (appearanceDictionary == null)
+        {
+            appearanceDictionary = new PDAppearanceDictionary();
+            annotation.setAppearance(appearanceDictionary);
+        }
+        return appearanceDictionary;
+    }
+
+    /**
+     * Get the annotations normal appearance content stream.
+     * 
+     * <p>
+     * This will get the annotations normal appearance content stream, to 
'draw' to. It will be
+     * uncompressed.
+     *
+     * @return the appearance entry representing the normal appearance.
+     * @throws IOException 
+     */
+    PDAppearanceContentStream getNormalAppearanceAsContentStream() throws 
IOException
+    {
+        return getNormalAppearanceAsContentStream(false);
+    }
+    
+    /**
+     * Get the annotations normal appearance content stream.
+     * 
+     * <p>
+     * This will get the annotations normal appearance content stream, to 
'draw' to.
+     * 
+     * @param compress whether the content stream is to be compressed. Set 
this to true when
+     * creating long content streams.
+     * @return the appearance entry representing the normal appearance.
+     * @throws IOException
+     */
+    PDAppearanceContentStream getNormalAppearanceAsContentStream(boolean 
compress) throws IOException
+    {
+        PDAppearanceEntry appearanceEntry = getNormalAppearance();
+        return getAppearanceEntryAsContentStream(appearanceEntry, compress);
+    }
+    
+    /**
+     * Get the annotations down appearance.
+     * 
+     * <p>
+     * This will get the annotations down appearance. If this is not existent 
an
+     * empty appearance entry will be created.
+     * 
+     * @return the appearance entry representing the down appearance.
+     */
+    PDAppearanceEntry getDownAppearance()
+    {
+        PDAppearanceDictionary appearanceDictionary = getAppearance();
+        PDAppearanceEntry downAppearanceEntry = 
appearanceDictionary.getDownAppearance();
+
+        if (downAppearanceEntry.isSubDictionary())
+        {
+            downAppearanceEntry = new PDAppearanceEntry(createCOSStream());
+            appearanceDictionary.setDownAppearance(downAppearanceEntry);
+        }
+
+        return downAppearanceEntry;
+    }
+
+    /**
+     * Get the annotations rollover appearance.
+     * 
+     * <p>
+     * This will get the annotations rollover appearance. If this is not
+     * existent an empty appearance entry will be created.
+     * 
+     * @return the appearance entry representing the rollover appearance.
+     */
+    PDAppearanceEntry getRolloverAppearance()
+    {
+        PDAppearanceDictionary appearanceDictionary = getAppearance();
+        PDAppearanceEntry rolloverAppearanceEntry = 
appearanceDictionary.getRolloverAppearance();
+
+        if (rolloverAppearanceEntry.isSubDictionary())
+        {
+            rolloverAppearanceEntry = new PDAppearanceEntry(createCOSStream());
+            
appearanceDictionary.setRolloverAppearance(rolloverAppearanceEntry);
+        }
+
+        return rolloverAppearanceEntry;
+    }
+
+    /**
+     * Get a padded rectangle.
+     * 
+     * <p>Creates a new rectangle with padding applied to each side.
+     * .
+     * @param rectangle the rectangle.
+     * @param padding the padding to apply.
+     * @return the padded rectangle.
+     */
+    PDRectangle getPaddedRectangle(PDRectangle rectangle, float padding)
+    {
+        return new PDRectangle(rectangle.getLowerLeftX() + padding, 
rectangle.getLowerLeftY() + padding,
+                rectangle.getWidth() - 2 * padding, rectangle.getHeight() - 2 
* padding);
+    }
+    
+    /**
+     * Get a rectangle enlarged by the differences.
+     *
+     * <p>
+     * Creates a new rectangle with differences added to each side. If there 
are no valid
+     * differences, then the original rectangle is returned.
+     *
+     * @param rectangle the rectangle.
+     * @param differences the differences to apply.
+     * @return the padded rectangle.
+     */
+    PDRectangle addRectDifferences(PDRectangle rectangle, float[] differences)
+    {
+        if (differences == null || differences.length != 4)
+        {
+            return rectangle;
+        }
+        
+        return new PDRectangle(rectangle.getLowerLeftX() - differences[0],
+                rectangle.getLowerLeftY() - differences[1],
+                rectangle.getWidth() + differences[0] + differences[2],
+                rectangle.getHeight() + differences[1] + differences[3]);
+    }
+    
+    /**
+     * Get a rectangle with the differences applied to each side.
+     *
+     * <p>
+     * Creates a new rectangle with differences added to each side. If there 
are no valid
+     * differences, then the original rectangle is returned.
+     *
+     * @param rectangle the rectangle.
+     * @param differences the differences to apply.
+     * @return the padded rectangle.
+     */
+    PDRectangle applyRectDifferences(PDRectangle rectangle, float[] 
differences)
+    {
+        if (differences == null || differences.length != 4)
+        {
+            return rectangle;
+        }
+        return new PDRectangle(rectangle.getLowerLeftX() + differences[0],
+                rectangle.getLowerLeftY() + differences[1],
+                rectangle.getWidth() - differences[0] - differences[2],
+                rectangle.getHeight() - differences[1] - differences[3]);
+    }
+
+    void setOpacity(PDAppearanceContentStream contentStream, float opacity) 
throws IOException
+    {
+        if (opacity < 1)
+        {
+            PDExtendedGraphicsState gs = new PDExtendedGraphicsState();
+            gs.setStrokingAlphaConstant(opacity);
+            gs.setNonStrokingAlphaConstant(opacity);
+
+            contentStream.setGraphicsStateParameters(gs);
+        }
+    }
+
+    /**
+     * Draw a line ending style.
+     * 
+     * @param style
+     * @param cs
+     * @param x
+     * @param y
+     * @param width
+     * @param hasStroke
+     * @param hasBackground
+     * @param ending false if left, true if right of an imagined horizontal 
line (important for
+     * arrows).
+     *
+     * @throws IOException
+     */
+    void drawStyle(String style, final PDAppearanceContentStream cs, float x, 
float y,
+                   float width, boolean hasStroke, boolean hasBackground, 
boolean ending) throws IOException
+    {
+        int sign = ending ? -1 : 1;
+
+        if (PDAnnotationLine.LE_OPEN_ARROW.equals(style) || 
PDAnnotationLine.LE_CLOSED_ARROW.equals(style))
+        {
+            drawArrow(cs, x + sign * width, y, sign * width * 9);
+        }
+        else if (PDAnnotationLine.LE_BUTT.equals(style))
+        {
+            cs.moveTo(x, y - width * 3);
+            cs.lineTo(x, y + width * 3);
+        }
+        else if (PDAnnotationLine.LE_DIAMOND.equals(style))
+        {
+            drawDiamond(cs, x, y, width * 3);
+        }
+        else if (PDAnnotationLine.LE_SQUARE.equals(style))
+        {
+            cs.addRect(x - width * 3, y - width * 3, width * 6, width * 6);
+        }
+        else if (PDAnnotationLine.LE_CIRCLE.equals(style))
+        {
+            drawCircle(cs, x, y, width * 3);
+        }
+        else if (PDAnnotationLine.LE_R_OPEN_ARROW.equals(style) || 
PDAnnotationLine.LE_R_CLOSED_ARROW.equals(style))
+        {
+            drawArrow(cs, x + (-sign) * width, y, (-sign) * width * 9);
+        }
+        else if (PDAnnotationLine.LE_SLASH.equals(style))
+        {
+            // the line is 18 x linewidth at an angle of 60°
+            float width9 = width * 9;
+            cs.moveTo(x + (float) (Math.cos(Math.toRadians(60)) * width9),
+                      y + (float) (Math.sin(Math.toRadians(60)) * width9));
+            cs.lineTo(x + (float) (Math.cos(Math.toRadians(240)) * width9),
+                      y + (float) (Math.sin(Math.toRadians(240)) * width9));
+        }
+
+        if (PDAnnotationLine.LE_R_CLOSED_ARROW.equals(style) || 
+            PDAnnotationLine.LE_CLOSED_ARROW.equals(style))
+        {
+            cs.closePath();
+        }
+        cs.drawShape(width, hasStroke, 
+                     // make sure to only paint a background color (/IC value) 
+                     // for interior color styles, even if an /IC value is set.
+                     INTERIOR_COLOR_STYLES.contains(style) && hasBackground);
+    }
+
+    /**
+     * Add the two arms of a horizontal arrow.
+     * 
+     * @param cs Content stream
+     * @param x
+     * @param y
+     * @param len The arm length. Positive goes to the right, negative goes to 
the left.
+     * 
+     * @throws IOException If the content stream could not be written
+     */
+    void drawArrow(PDAppearanceContentStream cs, float x, float y, float len) 
throws IOException
+    {
+        // strategy for arrows: angle 30°, arrow arm length = 9 * line width
+        // cos(angle) = x position
+        // sin(angle) = y position
+        // this comes very close to what Adobe is doing
+        float armX = x + (float) (Math.cos(ARROW_ANGLE) * len);
+        float armYdelta = (float) (Math.sin(ARROW_ANGLE) * len);
+        cs.moveTo(armX, y + armYdelta);
+        cs.lineTo(x, y);
+        cs.lineTo(armX, y - armYdelta);
+    }
+
+    /**
+     * Add a square diamond shape (corner on top) to the path.
+     *
+     * @param cs Content stream
+     * @param x
+     * @param y
+     * @param r Radius (to a corner)
+     * 
+     * @throws IOException If the content stream could not be written
+     */
+    void drawDiamond(PDAppearanceContentStream cs, float x, float y, float r) 
throws IOException
+    {
+        cs.moveTo(x - r, y);
+        cs.lineTo(x, y + r);
+        cs.lineTo(x + r, y);
+        cs.lineTo(x, y - r);
+        cs.closePath();
+    }
+
+    /**
+     * Add a circle shape to the path in clockwise direction.
+     *
+     * @param cs Content stream
+     * @param x
+     * @param y
+     * @param r Radius
+     * 
+     * @throws IOException If the content stream could not be written.
+     */
+    void drawCircle(PDAppearanceContentStream cs, float x, float y, float r) 
throws IOException
+    {
+        // http://stackoverflow.com/a/2007782/535646
+        float magic = r * 0.551784f;
+        cs.moveTo(x, y + r);
+        cs.curveTo(x + magic, y + r, x + r, y + magic, x + r, y);
+        cs.curveTo(x + r, y - magic, x + magic, y - r, x, y - r);
+        cs.curveTo(x - magic, y - r, x - r, y - magic, x - r, y);
+        cs.curveTo(x - r, y + magic, x - magic, y + r, x, y + r);
+        cs.closePath();
+    }
+
+    /**
+     * Add a circle shape to the path in counterclockwise direction. You'll 
need this e.g. when
+     * drawing a doughnut shape. See "Nonzero Winding Number Rule" for more 
information.
+     *
+     * @param cs Content stream
+     * @param x
+     * @param y
+     * @param r Radius
+     *
+     * @throws IOException If the content stream could not be written.
+     */
+    void drawCircle2(PDAppearanceContentStream cs, float x, float y, float r) 
throws IOException
+    {
+        // http://stackoverflow.com/a/2007782/535646
+        float magic = r * 0.551784f;
+        cs.moveTo(x, y + r);
+        cs.curveTo(x - magic, y + r, x - r, y + magic, x - r, y);
+        cs.curveTo(x - r, y - magic, x - magic, y - r, x, y - r);
+        cs.curveTo(x + magic, y - r, x + r, y - magic, x + r, y);
+        cs.curveTo(x + r, y + magic, x + magic, y + r, x, y + r);
+        cs.closePath();
+    }
+
+    private static Set<String> createShortStyles()
+    {
+        Set<String> shortStyles = new HashSet<String>();
+        shortStyles.add(PDAnnotationLine.LE_OPEN_ARROW);
+        shortStyles.add(PDAnnotationLine.LE_CLOSED_ARROW);
+        shortStyles.add(PDAnnotationLine.LE_SQUARE);
+        shortStyles.add(PDAnnotationLine.LE_CIRCLE);
+        shortStyles.add(PDAnnotationLine.LE_DIAMOND);
+        return Collections.unmodifiableSet(shortStyles);
+    }
+
+    private static Set<String> createInteriorColorStyles()
+    {
+        Set<String> interiorColorStyles = new HashSet<String>();
+        interiorColorStyles.add(PDAnnotationLine.LE_CLOSED_ARROW);
+        interiorColorStyles.add(PDAnnotationLine.LE_CIRCLE);
+        interiorColorStyles.add(PDAnnotationLine.LE_DIAMOND);
+        interiorColorStyles.add(PDAnnotationLine.LE_R_CLOSED_ARROW);
+        interiorColorStyles.add(PDAnnotationLine.LE_SQUARE);
+        return Collections.unmodifiableSet(interiorColorStyles);
+    }
+
+    private static Set<String> createAngledStyles()
+    {
+        Set<String> angledStyles = new HashSet<String>();
+        angledStyles.add(PDAnnotationLine.LE_CLOSED_ARROW);
+        angledStyles.add(PDAnnotationLine.LE_OPEN_ARROW);
+        angledStyles.add(PDAnnotationLine.LE_R_CLOSED_ARROW);
+        angledStyles.add(PDAnnotationLine.LE_R_OPEN_ARROW);
+        angledStyles.add(PDAnnotationLine.LE_BUTT);
+        angledStyles.add(PDAnnotationLine.LE_SLASH);
+        return Collections.unmodifiableSet(angledStyles);
+    }
+
+    /**
+     * Get the annotations normal appearance.
+     * 
+     * <p>
+     * This will get the annotations normal appearance. If this is not existent
+     * an empty appearance entry will be created.
+     * 
+     * @return the appearance entry representing the normal appearance.
+     */
+    private PDAppearanceEntry getNormalAppearance()
+    {
+        PDAppearanceDictionary appearanceDictionary = getAppearance();
+        PDAppearanceEntry normalAppearanceEntry = 
appearanceDictionary.getNormalAppearance();
+
+        if (normalAppearanceEntry == null || 
normalAppearanceEntry.isSubDictionary())
+        {
+            normalAppearanceEntry = new PDAppearanceEntry(createCOSStream());
+            appearanceDictionary.setNormalAppearance(normalAppearanceEntry);
+        }
+
+        return normalAppearanceEntry;
+    }
+    
+    
+    private PDAppearanceContentStream getAppearanceEntryAsContentStream(
+              PDAppearanceEntry appearanceEntry, boolean compress) throws 
IOException
+    {
+        PDAppearanceStream appearanceStream = 
appearanceEntry.getAppearanceStream();
+        setTransformationMatrix(appearanceStream);
+
+        // ensure there are resources
+        PDResources resources = appearanceStream.getResources();
+        if (resources == null)
+        {
+            resources = new PDResources();
+            appearanceStream.setResources(resources);
+        }
+
+        return new PDAppearanceContentStream(appearanceStream, compress);
+    }
+    
+    private void setTransformationMatrix(PDAppearanceStream appearanceStream)
+    {
+        PDRectangle bbox = getRectangle();
+        appearanceStream.setBBox(bbox);
+        AffineTransform transform = 
AffineTransform.getTranslateInstance(-bbox.getLowerLeftX(),
+                -bbox.getLowerLeftY());
+        appearanceStream.setMatrix(transform);
+    }
+
+    PDRectangle handleBorderBox(PDAnnotationSquareCircle annotation, float 
lineWidth)
+    {
+        // There are two options. The handling is not part of the PDF 
specification but
+        // implementation specific to Adobe Reader
+        // - if /RD is set the border box is the /Rect entry inset by the 
respective
+        //   border difference.
+        // - if /RD is not set the border box is defined by the /Rect entry. 
The /RD entry will
+        //   be set to be the line width and the /Rect is enlarged by the /RD 
amount
+        PDRectangle borderBox;
+        float[] rectDifferences = annotation.getRectDifferences();
+        if (rectDifferences.length == 0)
+        {
+            PDRectangle rect = getRectangle();
+            borderBox = getPaddedRectangle(rect, lineWidth / 2);
+            // the differences rectangle
+            annotation.setRectDifferences(lineWidth / 2);
+            annotation.setRectangle(addRectDifferences(rect, 
annotation.getRectDifferences()));
+            // when the normal appearance stream was generated BBox and Matrix 
have been set to the
+            // values of the original /Rect. As the /Rect was changed that 
needs to be adjusted too.
+            PDAppearanceStream appearanceStream = 
annotation.getNormalAppearanceStream();
+            AffineTransform transform =
+                    
AffineTransform.getTranslateInstance(-rect.getLowerLeftX(), 
-rect.getLowerLeftY());
+            appearanceStream.setBBox(rect);
+            appearanceStream.setMatrix(transform);
+        }
+        else
+        {
+            borderBox = applyRectDifferences(getRectangle(), rectDifferences);
+            borderBox = getPaddedRectangle(borderBox, lineWidth / 2);
+        }
+        return borderBox;
+    }
+}

Reply via email to