You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pdfbox.apache.org by ti...@apache.org on 2018/05/19 19:15:17 UTC

svn commit: r1831914 - /pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDPolylineAppearanceHandler.java

Author: tilman
Date: Sat May 19 19:15:17 2018
New Revision: 1831914

URL: http://svn.apache.org/viewvc?rev=1831914&view=rev
Log:
PDFBOX-3353: draw line endings

Modified:
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDPolylineAppearanceHandler.java

Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDPolylineAppearanceHandler.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDPolylineAppearanceHandler.java?rev=1831914&r1=1831913&r2=1831914&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDPolylineAppearanceHandler.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/PDPolylineAppearanceHandler.java Sat May 19 19:15:17 2018
@@ -28,7 +28,12 @@ import org.apache.pdfbox.pdmodel.graphic
 import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
 import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationPolyline;
 import org.apache.pdfbox.pdmodel.PDAppearanceContentStream;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLine;
+import static org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLine.LE_NONE;
 import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary;
+import static org.apache.pdfbox.pdmodel.interactive.annotation.handlers.PDAbstractAppearanceHandler.INTERIOR_COLOR_STYLES;
+import static org.apache.pdfbox.pdmodel.interactive.annotation.handlers.PDLineAppearanceHandler.ARROW_ANGLE;
+import org.apache.pdfbox.util.Matrix;
 
 /**
  * Handler to generate the polyline annotations appearance.
@@ -84,17 +89,20 @@ public class PDPolylineAppearanceHandler
             maxX = Math.max(maxX, x);
             maxY = Math.max(maxY, y);
         }
-        rect.setLowerLeftX(Math.min(minX - ab.width / 2, rect.getLowerLeftX()));
-        rect.setLowerLeftY(Math.min(minY - ab.width / 2, rect.getLowerLeftY()));
-        rect.setUpperRightX(Math.max(maxX + ab.width, rect.getUpperRightX()));
-        rect.setUpperRightY(Math.max(maxY + ab.width, rect.getUpperRightY()));
+        // arrow length is 9 * width at about 30° => 10 * width seems to be enough
+        rect.setLowerLeftX(Math.min(minX - ab.width * 10, rect.getLowerLeftX()));
+        rect.setLowerLeftY(Math.min(minY - ab.width * 10, rect.getLowerLeftY()));
+        rect.setUpperRightX(Math.max(maxX + ab.width * 10, rect.getUpperRightX()));
+        rect.setUpperRightY(Math.max(maxY + ab.width * 10, rect.getUpperRightY()));
         annotation.setRectangle(rect);
 
         try
         {
             try (PDAppearanceContentStream cs = getNormalAppearanceAsContentStream())
             {
+                boolean hasBackground = cs.setNonStrokingColorOnDemand(annotation.getInteriorColor());
                 setOpacity(cs, annotation.getConstantOpacity());
+                boolean hasStroke = cs.setStrokingColorOnDemand(color);
 
                 cs.setStrokingColor(color);
                 if (ab.dashArray != null)
@@ -112,10 +120,9 @@ public class PDPolylineAppearanceHandler
                         if (SHORT_STYLES.contains(annotation.getStartPointEndingStyle()))
                         {
                             // modify coordinate to shorten the segment
+                            // https://stackoverflow.com/questions/7740507/extend-a-line-segment-a-specific-distance
                             float x1 = pathsArray[2];
                             float y1 = pathsArray[3];
-
-                            // https://stackoverflow.com/questions/7740507/extend-a-line-segment-a-specific-distance
                             float len = (float) (Math.sqrt(Math.pow(x - x1, 2) + Math.pow(y - y1, 2)));
                             if (Float.compare(len, 0) != 0)
                             {
@@ -125,15 +132,15 @@ public class PDPolylineAppearanceHandler
                         }
                         cs.moveTo(x, y);
                     }
-                    else if (i == pathsArray.length / 2 - 1)
+                    else
                     {
-                        if (SHORT_STYLES.contains(annotation.getEndPointEndingStyle()))
+                        if (i == pathsArray.length / 2 - 1 &&
+                            SHORT_STYLES.contains(annotation.getEndPointEndingStyle()))
                         {
                             // modify coordinate to shorten the segment
+                            // https://stackoverflow.com/questions/7740507/extend-a-line-segment-a-specific-distance
                             float x0 = pathsArray[pathsArray.length - 4];
                             float y0 = pathsArray[pathsArray.length - 3];
-
-                            // https://stackoverflow.com/questions/7740507/extend-a-line-segment-a-specific-distance
                             float len = (float) (Math.sqrt(Math.pow(x0 - x, 2) + Math.pow(y0 - y, 2)));
                             if (Float.compare(len, 0) != 0)
                             {
@@ -143,19 +150,41 @@ public class PDPolylineAppearanceHandler
                         }
                         cs.lineTo(x, y);
                     }
-                    else
-                    {
-                        cs.lineTo(x, y);
-                    }
                 }
-                //TODO line endings (LE) are missing
-                // How it could be done by reusing some of the code from the line handler
-                // 1) do a transform so that first and last "arms" are imagined flat
-                // (like in line handler)
-                // 2) refactor + reuse the line handler code that draws the ending shapes
+                cs.stroke();
+
+                // do a transform so that first and last "arms" are imagined flat, like in line handler
                 // the alternative would be to apply the transform to the LE shapes directly,
                 // which would be more work and produce code difficult to understand
-                cs.stroke();
+
+                // this must be done after polyline draw, to avoid line crossing a filled shape
+                if (!LE_NONE.equals(annotation.getStartPointEndingStyle()))
+                {
+                    // check only needed to avoid q cm Q if LE_NONE
+                    float x2 = pathsArray[2];
+                    float y2 = pathsArray[3];
+                    float x1 = pathsArray[0];
+                    float y1 = pathsArray[1];
+                    cs.saveGraphicsState();
+                    double angle = Math.atan2(y2 - y1, x2 - x1);
+                    cs.transform(Matrix.getRotateInstance(angle, x1, y1));
+                    drawStyle(annotation.getStartPointEndingStyle(), cs, 0, 0, ab.width, hasStroke, hasBackground);
+                    cs.restoreGraphicsState();
+                }
+
+                if (!LE_NONE.equals(annotation.getEndPointEndingStyle()))
+                {
+                    // check only needed to avoid q cm Q if LE_NONE
+                    float x1 = pathsArray[pathsArray.length - 4];
+                    float y1 = pathsArray[pathsArray.length - 3];
+                    float x2 = pathsArray[pathsArray.length - 2];
+                    float y2 = pathsArray[pathsArray.length - 1];
+                    // save / restore not needed because it's the last one
+                    double angle = Math.atan2(y2 - y1, x2 - x1);
+                    cs.transform(Matrix.getRotateInstance(angle, x1, y1));
+                    float lineLength = (float) Math.sqrt(((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1)));
+                    drawStyle(annotation.getEndPointEndingStyle(), cs, lineLength, 0, ab.width, hasStroke, hasBackground);
+                }
             }
         }
         catch (IOException ex)
@@ -218,4 +247,142 @@ public class PDPolylineAppearanceHandler
 
         return 1;
     }
+
+    //TODO refactor to remove double code drawStyle and related
+    // (100% copied from line handler)
+
+    private void drawStyle(String style, final PDAppearanceContentStream cs, float x, float y,
+                           float width, boolean hasStroke, boolean hasBackground) throws IOException
+    {
+        switch (style)
+        {
+            case PDAnnotationLine.LE_OPEN_ARROW:
+            case PDAnnotationLine.LE_CLOSED_ARROW:
+                if (Float.compare(x, 0) != 0)
+                {
+                    // ending
+                    drawArrow(cs, x - width, y, -width * 9);
+                }
+                else
+                {
+                    // start
+                    drawArrow(cs, width, y, width * 9);
+                }
+                if (PDAnnotationLine.LE_CLOSED_ARROW.equals(style))
+                {
+                    cs.closePath();
+                }
+                break;
+            case PDAnnotationLine.LE_BUTT:
+                cs.moveTo(x, y - width * 3);
+                cs.lineTo(x, y + width * 3);
+                break;
+            case PDAnnotationLine.LE_DIAMOND:
+                drawDiamond(cs, x, y, width * 3);
+                break;
+            case PDAnnotationLine.LE_SQUARE:
+                cs.addRect(x - width * 3, y - width * 3, width * 6, width * 6);
+                break;
+            case PDAnnotationLine.LE_CIRCLE:
+                addCircle(cs, x, y, width * 3);
+                break;
+            case PDAnnotationLine.LE_R_OPEN_ARROW:
+            case PDAnnotationLine.LE_R_CLOSED_ARROW:
+                if (Float.compare(x, 0) != 0)
+                {
+                    // ending
+                    drawArrow(cs, x + width, y, width * 9);
+                }
+                else
+                {
+                    // start
+                    drawArrow(cs, -width, y, -width * 9);
+                }
+                if (PDAnnotationLine.LE_R_CLOSED_ARROW.equals(style))
+                {
+                    cs.closePath();
+                }
+                break;
+            case PDAnnotationLine.LE_SLASH:
+                // the line is 18 x linewidth at an angle of 60°
+                cs.moveTo(x + (float) (Math.cos(Math.toRadians(60)) * width * 9),
+                          y + (float) (Math.sin(Math.toRadians(60)) * width * 9));
+                cs.lineTo(x + (float) (Math.cos(Math.toRadians(240)) * width * 9),
+                          y + (float) (Math.sin(Math.toRadians(240)) * width * 9));
+                break;
+            default:
+                break;
+        }
+        if (INTERIOR_COLOR_STYLES.contains(style))
+        {
+            cs.drawShape(width, hasStroke, hasBackground);
+        }
+        else if (!PDAnnotationLine.LE_NONE.equals(style))
+        {
+            // need to do this separately, because sometimes /IC is set anyway
+            cs.drawShape(width, hasStroke, false);
+        }
+    }
+
+    /**
+     * 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
+     */
+    private 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
+        cs.moveTo(x + (float) (Math.cos(ARROW_ANGLE) * len), y + (float) (Math.sin(ARROW_ANGLE) * len));
+        cs.lineTo(x, y);
+        cs.lineTo(x + (float) (Math.cos(ARROW_ANGLE) * len), y - (float) (Math.sin(ARROW_ANGLE) * len));
+    }
+
+    /**
+     * 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
+     */
+    private 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.
+     *
+     * @param cs Content stream
+     * @param x
+     * @param y
+     * @param r Radius
+     * 
+     * @throws IOException If the content stream could not be written
+     */
+    private void addCircle(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();
+    }
 }