Advertisement
Guest User

iText Box Cloud

a guest
May 30th, 2019
415
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Java 32.44 KB | None | 0 0
  1. public class CBorder {
  2.  
  3.     private static final double ANGLE_180_DEG = Math.PI;
  4.     private static final double ANGLE_90_DEG = Math.PI / 2;
  5.     private static final double ANGLE_34_DEG = Math.toRadians(34);
  6.     private static final double ANGLE_12_DEG = Math.toRadians(12);
  7.  
  8.     private final PdfAppearance cs;
  9.     private final Rectangle annotRect;
  10.     private final double intensity;
  11.     private final double lineWidth;
  12.     private Rectangle rectWithDiff;
  13.     private boolean outputStarted = false;
  14.     private double bboxMinX;
  15.     private double bboxMinY;
  16.     private double bboxMaxX;
  17.     private double bboxMaxY;
  18.  
  19.     public CBorder(PdfAppearance stream, double intensity, double lineWidth, Rectangle rect) {
  20.         this.cs = stream;
  21.         this.intensity = intensity;
  22.         this.lineWidth = lineWidth;
  23.         this.annotRect = rect;
  24.     }
  25.  
  26.     /**
  27.      * Creates a cloudy border for a rectangular annotation. The rectangle is
  28.      * specified by the <code>RD</code> entry and the <code>Rect</code> entry
  29.      * that was passed in to the constructor.
  30.      * <p>
  31.      * This can be used for Square and FreeText annotations. However, this does
  32.      * not produce the text and the callout line for FreeTexts.
  33.      *
  34.      * @param rd entry <code>RD</code>, or null if the entry does not exist
  35.      * @throws IOException If there is an error writing to the stream.
  36.      */
  37.     public void createCloudyRectangle(Rectangle rd) throws IOException {
  38.         rectWithDiff = applyRectDiff(rd, lineWidth / 2);
  39.         double left = rectWithDiff.getLeft();
  40.         double bottom = rectWithDiff.getBottom();
  41.         double right = rectWithDiff.getRight();
  42.         double top = rectWithDiff.getTop();
  43.  
  44.         cloudyRectangleImpl(left, bottom, right, top, false);
  45.         finish();
  46.     }
  47.  
  48.     /**
  49.      * Creates a cloudy border for a Polygon annotation.
  50.      *
  51.      * @param path polygon path
  52.      * @throws IOException If there is an error writing to the stream.
  53.      */
  54.     public void createCloudyPolygon(float[][] path) throws IOException {
  55.         int n = path.length;
  56.         Point2D.Double[] polygon = new Point2D.Double[n];
  57.  
  58.         for (int i=0; i<n; i++) {
  59.             float[] array = path[i];
  60.             if (array.length == 2) {
  61.                 polygon[i] = new Point2D.Double(array[0], array[1]);
  62.             }
  63.             else if (array.length == 6) {
  64.                 // TODO Curve segments are not yet supported in cloudy border.
  65.                 polygon[i] = new Point2D.Double(array[4], array[5]);
  66.             }
  67.         }
  68.  
  69.         cloudyPolygonImpl(polygon, false);
  70.         finish();
  71.     }
  72.  
  73.     /**
  74.      * Creates a cloudy border for a Circle annotation. The ellipse is specified
  75.      * by the <code>RD</code> entry and the <code>Rect</code> entry that was
  76.      * passed in to the constructor.
  77.      *
  78.      * @param rd entry <code>RD</code>, or null if the entry does not exist
  79.      * @throws IOException If there is an error writing to the stream.
  80.      */
  81.     public void createCloudyEllipse(Rectangle rd) throws IOException {
  82.         rectWithDiff = applyRectDiff(rd, 0);
  83.         double left = rectWithDiff.getLeft();
  84.         double bottom = rectWithDiff.getBottom();
  85.         double right = rectWithDiff.getRight();
  86.         double top = rectWithDiff.getTop();
  87.  
  88.         cloudyEllipseImpl(left, bottom, right, top);
  89.         finish();
  90.     }
  91.  
  92.     /**
  93.      * Returns the <code>BBox</code> entry (bounding box) for the appearance
  94.      * stream form XObject.
  95.      *
  96.      * @return Bounding box for appearance stream form XObject.
  97.      */
  98.     public Rectangle getBBox() {
  99.         return getRectangle();
  100.     }
  101.  
  102.     /**
  103.      * Returns the updated <code>Rect</code> entry for the annotation. The
  104.      * rectangle completely contains the cloudy border.
  105.      *
  106.      * @return Annotation <code>Rect</code>.
  107.      */
  108.     public Rectangle getRectangle() {
  109.         return new Rectangle((float) bboxMinX, (float) bboxMinY, (float) bboxMaxX, (float) bboxMaxY);
  110.     }
  111.  
  112.     /**
  113.      * Returns the <code>Matrix</code> entry for the appearance stream form
  114.      * XObject.
  115.      *
  116.      * @return Matrix for appearance stream form XObject.
  117.      */
  118.     public AffineTransform getMatrix() {
  119.         return AffineTransform.getTranslateInstance(-bboxMinX, -bboxMinY);
  120.     }
  121.  
  122.     /**
  123.      * Returns the updated <code>RD</code> entry for Square and Circle
  124.      * annotations.
  125.      *
  126.      * @return Annotation <code>RD</code> value.
  127.      */
  128.     public Rectangle getRectDifference() {
  129.         if (annotRect == null) {
  130.             float d = (float) lineWidth / 2;
  131.             return new Rectangle(d, d, (float) lineWidth, (float) lineWidth);
  132.         }
  133.  
  134.         Rectangle re = (rectWithDiff != null) ? rectWithDiff : annotRect;
  135.  
  136.         float left = re.getLeft() - (float) bboxMinX;
  137.         float bottom = re.getBottom() - (float) bboxMinY;
  138.         float right = (float) bboxMaxX - re.getRight();
  139.         float top = (float) bboxMaxY - re.getTop();
  140.  
  141.         return new Rectangle(left, bottom, right, top);
  142.     }
  143.  
  144.     private static double cosine(double dx, double hypot) {
  145.         if (Double.compare(hypot, 0.0) == 0) {
  146.             return 0;
  147.         }
  148.         return dx / hypot;
  149.     }
  150.  
  151.     private static double sine(double dy, double hypot) {
  152.         if (Double.compare(hypot, 0.0) == 0) {
  153.             return 0;
  154.         }
  155.         return dy / hypot;
  156.     }
  157.  
  158.     /**
  159.      * Cloudy rectangle implementation is based on converting the rectangle to a
  160.      * polygon.
  161.      */
  162.     private void cloudyRectangleImpl(double left, double bottom, double right, double top, boolean isEllipse) throws IOException {
  163.         double w = right - left, h = top - bottom;
  164.  
  165.         if (intensity <= 0.0) {
  166.             cs.rectangle(left, bottom, w, h);
  167.             bboxMinX = left;
  168.             bboxMinY = bottom;
  169.             bboxMaxX = right;
  170.             bboxMaxY = top;
  171.             return;
  172.         }
  173.  
  174.         // Make a polygon with direction equal to the positive angle direction.
  175.         Point2D.Double[] polygon;
  176.  
  177.         if (w < 1.0) {
  178.             polygon = new Point2D.Double[] {
  179.                 new Point2D.Double(left, bottom), new Point2D.Double(left, top),
  180.                 new Point2D.Double(left, bottom)
  181.             };
  182.         }
  183.         else if (h < 1.0) {
  184.             polygon = new Point2D.Double[] {
  185.                 new Point2D.Double(left, bottom), new Point2D.Double(right, bottom),
  186.                 new Point2D.Double(left, bottom)
  187.             };
  188.         }
  189.         else {
  190.             polygon = new Point2D.Double[] {
  191.                 new Point2D.Double(left, bottom), new Point2D.Double(right, bottom),
  192.                 new Point2D.Double(right, top), new Point2D.Double(left, top),
  193.                 new Point2D.Double(left, bottom)
  194.             };
  195.         }
  196.  
  197.         cloudyPolygonImpl(polygon, isEllipse);
  198.     }
  199.  
  200.     /**
  201.      * Cloudy polygon implementation.
  202.      *
  203.      * @param vertices polygon vertices; first and last point must be equal
  204.      * @param isEllipse specifies if the polygon represents an ellipse
  205.      */
  206.     private void cloudyPolygonImpl(Point2D.Double[] vertices, boolean isEllipse) throws IOException {
  207.         Point2D.Double[] polygon = removeZeroLengthSegments(vertices);
  208.         getPositivePolygon(polygon);
  209.         int numPoints = polygon.length;
  210.  
  211.         if (numPoints < 2) {
  212.             return;
  213.         }
  214.  
  215.         if (intensity <= 0.0) {
  216.             moveTo(polygon[0]);
  217.             for (int i=1; i<numPoints; i++) {
  218.                 lineTo(polygon[i]);
  219.             }
  220.             return;
  221.         }
  222.  
  223.         double cloudRadius = isEllipse ? getEllipseCloudRadius() : getPolygonCloudRadius();
  224.  
  225.         if (cloudRadius < 0.5) {
  226.             cloudRadius = 0.5;
  227.         }
  228.  
  229.         final double k = Math.cos(ANGLE_34_DEG);
  230.         final double advIntermDefault = 2 * k * cloudRadius;
  231.         final double advCornerDefault = k * cloudRadius;
  232.         double[] array = new double[2];
  233.         double anglePrev = 0;
  234.  
  235.         // The number of curls per polygon segment is hardly ever an integer,
  236.         // so the length of some curls must be adjustable. We adjust the angle
  237.         // of the trailing arc of corner curls and the leading arc of the first
  238.         // intermediate curl.
  239.         // In each polygon segment, we have n intermediate curls plus one half of a
  240.         // corner curl at each end. One of the n intermediate curls is adjustable.
  241.         // Thus the number of fixed (or unadjusted) intermediate curls is n - 1.
  242.         // Find the adjusted angle `alpha` for the first corner curl.
  243.         int n0 = computeParamsPolygon(advIntermDefault, advCornerDefault, k, cloudRadius, polygon[numPoints - 2].distance(polygon[0]), array);
  244.         double alphaPrev = (n0 == 0) ? array[0] : ANGLE_34_DEG;
  245.  
  246.         for (int j=0; j+1<numPoints; j++) {
  247.             Point2D.Double pt = polygon[j];
  248.             Point2D.Double ptNext = polygon[j + 1];
  249.             double length = pt.distance(ptNext);
  250.             if (Double.compare(length, 0.0) == 0) {
  251.                 alphaPrev = ANGLE_34_DEG;
  252.                 continue;
  253.             }
  254.  
  255.             // n is the number of intermediate curls in the current polygon segment.
  256.             int n = computeParamsPolygon(advIntermDefault, advCornerDefault, k, cloudRadius, length, array);
  257.             if (n < 0) {
  258.                 if (!outputStarted) {
  259.                     moveTo(pt);
  260.                 }
  261.                 continue;
  262.             }
  263.  
  264.             double alpha = array[0], dx = array[1];
  265.             double angleCur = Math.atan2(ptNext.y - pt.y, ptNext.x - pt.x);
  266.             if (j == 0) {
  267.                 Point2D.Double ptPrev = polygon[numPoints - 2];
  268.                 anglePrev = Math.atan2(pt.y - ptPrev.y, pt.x - ptPrev.x);
  269.             }
  270.  
  271.             double cos = cosine(ptNext.x - pt.x, length);
  272.             double sin = sine(ptNext.y - pt.y, length);
  273.             double x = pt.x;
  274.             double y = pt.y;
  275.  
  276.             addCornerCurl(anglePrev, angleCur, cloudRadius, pt.x, pt.y, alpha, alphaPrev, !outputStarted);
  277.             // Proceed to the center point of the first intermediate curl.
  278.             double adv = 2 * k * cloudRadius + 2 * dx;
  279.             x += adv * cos;
  280.             y += adv * sin;
  281.  
  282.             // Create the first intermediate curl.
  283.             int numInterm = n;
  284.             if (n >= 1) {
  285.                 addFirstIntermediateCurl(angleCur, cloudRadius, alpha, x, y);
  286.                 x += advIntermDefault * cos;
  287.                 y += advIntermDefault * sin;
  288.                 numInterm = n - 1;
  289.             }
  290.  
  291.             // Create one intermediate curl and replicate it along the polygon segment.
  292.             Point2D.Double[] template = getIntermediateCurlTemplate(angleCur, cloudRadius);
  293.             for (int i=0; i<numInterm; i++) {
  294.                 outputCurlTemplate(template, x, y);
  295.                 x += advIntermDefault * cos;
  296.                 y += advIntermDefault * sin;
  297.             }
  298.  
  299.             anglePrev = angleCur;
  300.             alphaPrev = (n == 0) ? alpha : ANGLE_34_DEG;
  301.         }
  302.     }
  303.  
  304.     /**
  305.      * Computes parameters for a cloudy polygon: n, alpha, and dx.
  306.      */
  307.     private int computeParamsPolygon(double advInterm, double advCorner, double k, double r, double length, double[] array) {
  308.         if (Double.compare(length, 0.0) == 0) {
  309.             array[0] = ANGLE_34_DEG;
  310.             array[1] = 0;
  311.             return -1;
  312.         }
  313.  
  314.         // n is the number of intermediate curls in the current polygon segment
  315.         int n = (int) Math.ceil((length - 2 * advCorner) / advInterm);
  316.  
  317.         // Fitting error along polygon segment
  318.         double e = length - (2 * advCorner + n * advInterm);
  319.         // Fitting error per each adjustable half curl
  320.         double dx = e / 2;
  321.  
  322.         // Convert fitting error to an angle that can be used to control arcs.
  323.         double arg = (k * r + dx) / r;
  324.         double alpha = (arg < -1.0 || arg > 1.0) ? 0.0 : Math.acos(arg);
  325.  
  326.         array[0] = alpha;
  327.         array[1] = dx;
  328.         return n;
  329.     }
  330.  
  331.     /**
  332.      * Creates a corner curl for polygons and ellipses.
  333.      */
  334.     private void addCornerCurl(double anglePrev, double angleCur, double radius, double cx, double cy, double alpha, double alphaPrev, boolean addMoveTo) throws IOException {
  335.         double a = anglePrev + ANGLE_180_DEG + alphaPrev;
  336.         double b = anglePrev + ANGLE_180_DEG + alphaPrev - Math.toRadians(22);
  337.         getArcSegment(a, b, cx, cy, radius, radius, null, addMoveTo);
  338.  
  339.         a = b;
  340.         b = angleCur - alpha;
  341.         getArc(a, b, radius, radius, cx, cy, null, false);
  342.     }
  343.  
  344.     /**
  345.      * Generates the first intermediate curl for a cloudy polygon.
  346.      */
  347.     private void addFirstIntermediateCurl(double angleCur, double r, double alpha, double cx, double cy) throws IOException {
  348.         final double D = Math.toRadians(30);
  349.         double a = angleCur + ANGLE_180_DEG;
  350.  
  351.         getArcSegment(a + alpha, a + alpha - D, cx, cy, r, r, null, false);
  352.         getArcSegment(a + alpha - D, a + ANGLE_90_DEG, cx, cy, r, r, null, false);
  353.         getArcSegment(a + ANGLE_90_DEG, a + ANGLE_180_DEG - ANGLE_34_DEG, cx, cy, r, r, null, false);
  354.     }
  355.  
  356.     /**
  357.      * Returns a template for intermediate curls in a cloudy polygon.
  358.      */
  359.     private Point2D.Double[] getIntermediateCurlTemplate(double angleCur, double r) throws IOException {
  360.         ArrayList<Point2D.Double> points = new ArrayList<>();
  361.         double a = angleCur + ANGLE_180_DEG;
  362.         getArcSegment(a + ANGLE_34_DEG, a + ANGLE_12_DEG, 0, 0, r, r, points, false);
  363.         getArcSegment(a + ANGLE_12_DEG, a + ANGLE_90_DEG, 0, 0, r, r, points, false);
  364.         getArcSegment(a + ANGLE_90_DEG, a + ANGLE_180_DEG - ANGLE_34_DEG, 0, 0, r, r, points, false);
  365.         return points.toArray(new Point2D.Double[points.size()]);
  366.     }
  367.  
  368.     /**
  369.      * Writes the curl template points to the output and applies translation (x, y).
  370.      */
  371.     private void outputCurlTemplate(Point2D.Double[] template, double x, double y) throws IOException {
  372.         int n = template.length, i = 0;
  373.         if ((n % 3) == 1) {
  374.             Point2D.Double a = template[0];
  375.             moveTo(a.x + x, a.y + y);
  376.             i++;
  377.         }
  378.  
  379.         for (; i+2<n; i+=3) {
  380.             Point2D.Double a = template[i];
  381.             Point2D.Double b = template[i + 1];
  382.             Point2D.Double c = template[i + 2];
  383.             curveTo(a.x + x, a.y + y, b.x + x, b.y + y, c.x + x, c.y + y);
  384.         }
  385.     }
  386.  
  387.     private Rectangle applyRectDiff(Rectangle rd, double min) {
  388.         float rectLeft = annotRect.getLeft();
  389.         float rectBottom = annotRect.getBottom();
  390.         float rectRight = annotRect.getRight();
  391.         float rectTop = annotRect.getTop();
  392.  
  393.         // Normalize
  394.         rectLeft = Math.min(rectLeft, rectRight);
  395.         rectBottom = Math.min(rectBottom, rectTop);
  396.         rectRight = Math.max(rectLeft, rectRight);
  397.         rectTop = Math.max(rectBottom, rectTop);
  398.  
  399.         double rdLeft, rdBottom, rdRight, rdTop;
  400.  
  401.         if (rd != null) {
  402.             rdLeft = Math.max(rd.getLeft(), min);
  403.             rdBottom = Math.max(rd.getBottom(), min);
  404.             rdRight = Math.max(rd.getRight(), min);
  405.             rdTop = Math.max(rd.getTop(), min);
  406.         }
  407.         else {
  408.             rdLeft = min;
  409.             rdBottom = min;
  410.             rdRight = min;
  411.             rdTop = min;
  412.         }
  413.  
  414.         rectLeft += rdLeft;
  415.         rectBottom += rdBottom;
  416.         rectRight -= rdRight;
  417.         rectTop -= rdTop;
  418.  
  419.         // here try to check
  420.         return new Rectangle(rectLeft, rectBottom, rectRight, rectTop);
  421.     }
  422.  
  423.     private void reversePolygon(Point2D.Double[] points) {
  424.         int len = points.length;
  425.         int n = len / 2;
  426.         for (int i=0; i<n; i++) {
  427.             int j = len - i - 1;
  428.             Point2D.Double pi = points[i];
  429.             Point2D.Double pj = points[j];
  430.             points[i] = pj;
  431.             points[j] = pi;
  432.         }
  433.     }
  434.  
  435.     /**
  436.      * Makes a polygon whose direction is the same as the positive angle
  437.      * direction in the coordinate system. The polygon must not intersect
  438.      * itself.
  439.      */
  440.     private void getPositivePolygon(Point2D.Double[] points) {
  441.         if (getPolygonDirection(points) < 0) {
  442.             reversePolygon(points);
  443.         }
  444.     }
  445.  
  446.     /**
  447.      * Returns the direction of the specified polygon. A positive value
  448.      * indicates that the polygon's direction is the same as the direction of
  449.      * positive angles in the coordinate system. A negative value indicates the
  450.      * opposite direction.
  451.      *
  452.      * The polygon must not intersect itself. A 2-point polygon is not
  453.      * acceptable. This is based on the "shoelace formula".
  454.      */
  455.     private double getPolygonDirection(Point2D.Double[] points) {
  456.         double a = 0;
  457.         int len = points.length;
  458.         for (int i=0; i<len; i++) {
  459.             int j = (i + 1) % len;
  460.             a += points[i].x * points[j].y - points[i].y * points[j].x;
  461.         }
  462.         return a;
  463.     }
  464.  
  465.     /**
  466.      * Creates one or more Bezier curves that represent an elliptical arc.
  467.      * Angles are in radians. The arc will always proceed in the positive angle
  468.      * direction. If the argument `out` is null, this writes the results to the
  469.      * instance variable `output`.
  470.      */
  471.     private void getArc(double startAng, double endAng, double rx, double ry, double cx, double cy, ArrayList<Point2D.Double> out, boolean addMoveTo) throws IOException {
  472.         final double angleIncr = Math.PI / 2;
  473.         double startx = rx * Math.cos(startAng) + cx;
  474.         double starty = ry * Math.sin(startAng) + cy;
  475.  
  476.         double angleTodo = endAng - startAng;
  477.         while (angleTodo < 0) {
  478.             angleTodo += 2 * Math.PI;
  479.         }
  480.         double sweep = angleTodo, angleDone = 0;
  481.  
  482.         if (addMoveTo) {
  483.             if (out != null) {
  484.                 out.add(new Point2D.Double(startx, starty));
  485.             }
  486.             else {
  487.                 moveTo(startx, starty);
  488.             }
  489.         }
  490.  
  491.         while (angleTodo > angleIncr) {
  492.             getArcSegment(startAng + angleDone,
  493.                     startAng + angleDone + angleIncr, cx, cy, rx, ry, out, false);
  494.             angleDone += angleIncr;
  495.             angleTodo -= angleIncr;
  496.         }
  497.  
  498.         if (angleTodo > 0) {
  499.             getArcSegment(startAng + angleDone, startAng + sweep, cx, cy, rx, ry, out, false);
  500.         }
  501.     }
  502.  
  503.     /**
  504.      * Creates a single Bezier curve that represents a section of an elliptical
  505.      * arc. The sweep angle of the section must not be larger than 90 degrees.
  506.      * If argument `out` is null, this writes the results to the instance
  507.      * variable `output`.
  508.      */
  509.     private void getArcSegment(double startAng, double endAng, double cx, double cy, double rx, double ry, ArrayList<Point2D.Double> out, boolean addMoveTo) throws IOException {
  510.         // Algorithm is from the FAQ of the news group comp.text.pdf
  511.  
  512.         double cos_a = Math.cos(startAng);
  513.         double sin_a = Math.sin(startAng);
  514.         double cos_b = Math.cos(endAng);
  515.         double sin_b = Math.sin(endAng);
  516.         double denom = Math.sin((endAng - startAng) / 2.0);
  517.         if (Double.compare(denom, 0.0) == 0) {
  518.             // This can happen only if endAng == startAng.
  519.             // The arc sweep angle is zero, so we create no arc at all.
  520.             if (addMoveTo) {
  521.                 double xs = cx + rx * cos_a;
  522.                 double ys = cy + ry * sin_a;
  523.                 if (out != null) {
  524.                     out.add(new Point2D.Double(xs, ys));
  525.                 }
  526.                 else {
  527.                     moveTo(xs, ys);
  528.                 }
  529.             }
  530.             return;
  531.         }
  532.         double bcp = 1.333333333 * (1 - Math.cos((endAng - startAng) / 2.0)) / denom;
  533.         double p1x = cx + rx * (cos_a - bcp * sin_a);
  534.         double p1y = cy + ry * (sin_a + bcp * cos_a);
  535.         double p2x = cx + rx * (cos_b + bcp * sin_b);
  536.         double p2y = cy + ry * (sin_b - bcp * cos_b);
  537.         double p3x = cx + rx * cos_b;
  538.         double p3y = cy + ry * sin_b;
  539.  
  540.         if (addMoveTo) {
  541.             double xs = cx + rx * cos_a;
  542.             double ys = cy + ry * sin_a;
  543.             if (out != null) {
  544.                 out.add(new Point2D.Double(xs, ys));
  545.             }
  546.             else {
  547.                 moveTo(xs, ys);
  548.             }
  549.         }
  550.  
  551.         if (out != null) {
  552.             out.add(new Point2D.Double(p1x, p1y));
  553.             out.add(new Point2D.Double(p2x, p2y));
  554.             out.add(new Point2D.Double(p3x, p3y));
  555.         }
  556.         else {
  557.             curveTo(p1x, p1y, p2x, p2y, p3x, p3y);
  558.         }
  559.     }
  560.  
  561.     /**
  562.      * Flattens an ellipse into a polygon.
  563.      */
  564.     private static Point2D.Double[] flattenEllipse(double left, double bottom, double right, double top) {
  565.         Ellipse2D.Double ellipse = new Ellipse2D.Double(left, bottom, right - left, top - bottom);
  566.         final double flatness = 0.50;
  567.         PathIterator iterator = ellipse.getPathIterator(null, flatness);
  568.         double[] coords = new double[6];
  569.         ArrayList<Point2D.Double> points = new ArrayList<>();
  570.  
  571.         while (!iterator.isDone()) {
  572.             switch (iterator.currentSegment(coords)) {
  573.                 case PathIterator.SEG_MOVETO:
  574.                 case PathIterator.SEG_LINETO:
  575.                     points.add(new Point2D.Double(coords[0], coords[1]));
  576.                     break;
  577.                 // Curve segments are not expected because the path iterator is
  578.                 // flattened. SEG_CLOSE can be ignored.
  579.             }
  580.             iterator.next();
  581.         }
  582.  
  583.         int size = points.size();
  584.         final double closeTestLimit = 0.05;
  585.  
  586.         if (size >= 2 && points.get(size - 1).distance(points.get(0)) > closeTestLimit) {
  587.             points.add(points.get(points.size() - 1));
  588.         }
  589.         return points.toArray(new Point2D.Double[points.size()]);
  590.     }
  591.  
  592.     /**
  593.      * Cloudy ellipse implementation.
  594.      */
  595.     private void cloudyEllipseImpl(final double leftOrig, final double bottomOrig, final double rightOrig, final double topOrig) throws IOException {
  596.         if (intensity <= 0.0) {
  597.             drawBasicEllipse(leftOrig, bottomOrig, rightOrig, topOrig);
  598.             return;
  599.         }
  600.  
  601.         double left = leftOrig, bottom = bottomOrig, right = rightOrig, top = topOrig;
  602.         double width = right - left, height = top - bottom;
  603.         double cloudRadius = getEllipseCloudRadius();
  604.  
  605.         // Omit cloudy border if the ellipse is very small.
  606.         final double threshold1 = 0.50 * cloudRadius;
  607.         if (width < threshold1 && height < threshold1) {
  608.             drawBasicEllipse(left, bottom, right, top);
  609.             return;
  610.         }
  611.  
  612.         // Draw a cloudy rectangle instead of an ellipse when the
  613.         // width or height is very small.
  614.         final double threshold2 = 5;
  615.         if ((width < threshold2 && height > 20) || (width > 20 && height < threshold2)) {
  616.             cloudyRectangleImpl(left, bottom, right, top, true);
  617.             return;
  618.         }
  619.  
  620.         // Decrease radii (while center point does not move). This makes the
  621.         // "tails" of the curls almost touch the ellipse outline.
  622.         double radiusAdj = Math.sin(ANGLE_12_DEG) * cloudRadius - 1.50;
  623.         if (width > 2 * radiusAdj) {
  624.             left += radiusAdj;
  625.             right -= radiusAdj;
  626.         }
  627.         else {
  628.             double mid = (left + right) / 2;
  629.             left = mid - 0.10;
  630.             right = mid + 0.10;
  631.         }
  632.  
  633.         if (height > 2 * radiusAdj) {
  634.             top -= radiusAdj;
  635.             bottom += radiusAdj;
  636.         }
  637.         else {
  638.             double mid = (top + bottom) / 2;
  639.             top = mid + 0.10;
  640.             bottom = mid - 0.10;
  641.         }
  642.  
  643.         // Flatten the ellipse into a polygon. The segment lengths of the flattened
  644.         // result don't need to be extremely short because the loop below is able to
  645.         // interpolate between polygon points when it computes the center points
  646.         // at which each curl is placed.
  647.         Point2D.Double[] flatPolygon = flattenEllipse(left, bottom, right, top);
  648.         int numPoints = flatPolygon.length;
  649.         if (numPoints < 2) {
  650.             return;
  651.         }
  652.  
  653.         double totLen = 0;
  654.         for (int i=1; i<numPoints; i++) {
  655.             totLen += flatPolygon[i - 1].distance(flatPolygon[i]);
  656.         }
  657.  
  658.         final double k = Math.cos(ANGLE_34_DEG);
  659.         double curlAdvance = 2 * k * cloudRadius;
  660.         int n = (int) Math.ceil(totLen / curlAdvance);
  661.         if (n < 2) {
  662.             drawBasicEllipse(leftOrig, bottomOrig, rightOrig, topOrig);
  663.             return;
  664.         }
  665.  
  666.         curlAdvance = totLen / n;
  667.         cloudRadius = curlAdvance / (2 * k);
  668.  
  669.         if (cloudRadius < 0.5) {
  670.             cloudRadius = 0.5;
  671.             curlAdvance = 2 * k * cloudRadius;
  672.         }
  673.         else if (cloudRadius < 3.0) {
  674.             // Draw a small circle when the scaled radius becomes very small.
  675.             // This happens also if intensity is much smaller than 1.
  676.             drawBasicEllipse(leftOrig, bottomOrig, rightOrig, topOrig);
  677.             return;
  678.         }
  679.  
  680.         // Construct centerPoints array, in which each point is the center point of a curl.
  681.         // The length of each centerPoints segment ideally equals curlAdv but that
  682.         // is not true in regions where the ellipse curvature is high.
  683.         int centerPointsLength = n;
  684.         Point2D.Double[] centerPoints = new Point2D.Double[centerPointsLength];
  685.         int centerPointsIndex = 0;
  686.         double lengthRemain = 0;
  687.         final double comparisonToler = lineWidth * 0.10;
  688.  
  689.         for (int i=0; i+1<numPoints; i++) {
  690.             Point2D.Double p1 = flatPolygon[i];
  691.             Point2D.Double p2 = flatPolygon[i + 1];
  692.             double dx = p2.x - p1.x, dy = p2.y - p1.y;
  693.             double length = p1.distance(p2);
  694.             if (Double.compare(length, 0.0) == 0) {
  695.                 continue;
  696.             }
  697.             double lengthTodo = length + lengthRemain;
  698.             if (lengthTodo >= curlAdvance - comparisonToler || i == numPoints - 2) {
  699.                 double cos = cosine(dx, length), sin = sine(dy, length);
  700.                 double d = curlAdvance - lengthRemain;
  701.                 do {
  702.                     double x = p1.x + d * cos;
  703.                     double y = p1.y + d * sin;
  704.                     if (centerPointsIndex < centerPointsLength) {
  705.                         centerPoints[centerPointsIndex++] = new Point2D.Double(x, y);
  706.                     }
  707.                     lengthTodo -= curlAdvance;
  708.                     d += curlAdvance;
  709.                 } while (lengthTodo >= curlAdvance - comparisonToler);
  710.  
  711.                 lengthRemain = lengthTodo;
  712.                 if (lengthRemain < 0) {
  713.                     lengthRemain = 0;
  714.                 }
  715.             }
  716.             else {
  717.                 lengthRemain += length;
  718.             }
  719.         }
  720.  
  721.         // Note: centerPoints does not repeat the first point as the last point
  722.         // to create a "closing" segment.
  723.         // Place a curl at each point of the centerPoints array.
  724.         // In regions where the ellipse curvature is high, the centerPoints segments
  725.         // are shorter than the actual distance along the ellipse. Thus we must
  726.         // again compute arc adjustments like in cloudy polygons.
  727.         numPoints = centerPointsIndex;
  728.         double anglePrev = 0, alphaPrev = 0;
  729.  
  730.         for (int i=0; i<numPoints; i++) {
  731.             int idxNext = i + 1;
  732.             if (i + 1 >= numPoints) {
  733.                 idxNext = 0;
  734.             }
  735.             Point2D.Double pt = centerPoints[i];
  736.             Point2D.Double ptNext = centerPoints[idxNext];
  737.  
  738.             if (i == 0) {
  739.                 Point2D.Double ptPrev = centerPoints[numPoints - 1];
  740.                 anglePrev = Math.atan2(pt.y - ptPrev.y, pt.x - ptPrev.x);
  741.                 alphaPrev = computeParamsEllipse(ptPrev, pt, cloudRadius, curlAdvance);
  742.             }
  743.  
  744.             double angleCur = Math.atan2(ptNext.y - pt.y, ptNext.x - pt.x);
  745.             double alpha = computeParamsEllipse(pt, ptNext, cloudRadius, curlAdvance);
  746.  
  747.             addCornerCurl(anglePrev, angleCur, cloudRadius, pt.x, pt.y, alpha, alphaPrev, !outputStarted);
  748.  
  749.             anglePrev = angleCur;
  750.             alphaPrev = alpha;
  751.         }
  752.     }
  753.  
  754.     /**
  755.      * Computes the alpha parameter for an ellipse curl.
  756.      */
  757.     private double computeParamsEllipse(Point2D.Double pt, Point2D.Double ptNext, double r, double curlAdv) {
  758.         double length = pt.distance(ptNext);
  759.         if (Double.compare(length, 0.0) == 0) {
  760.             return ANGLE_34_DEG;
  761.         }
  762.  
  763.         double e = length - curlAdv;
  764.         double arg = (curlAdv / 2 + e / 2) / r;
  765.         return (arg < -1.0 || arg > 1.0) ? 0.0 : Math.acos(arg);
  766.     }
  767.  
  768.     private Point2D.Double[] removeZeroLengthSegments(Point2D.Double[] polygon) {
  769.         int np = polygon.length;
  770.         if (np <= 2) {
  771.             return polygon;
  772.         }
  773.  
  774.         final double toler = 0.50;
  775.         int npNew = np;
  776.         Point2D.Double ptPrev = polygon[0];
  777.  
  778.         // Don't remove the last point if it equals the first point.
  779.         for (int i=1; i<np; i++) {
  780.             Point2D.Double pt = polygon[i];
  781.             if (Math.abs(pt.x - ptPrev.x) < toler && Math.abs(pt.y - ptPrev.y) < toler) {
  782.                 polygon[i] = null;
  783.                 npNew--;
  784.             }
  785.             ptPrev = pt;
  786.         }
  787.  
  788.         if (npNew == np) {
  789.             return polygon;
  790.         }
  791.  
  792.         Point2D.Double[] polygonNew = new Point2D.Double[npNew];
  793.         int j = 0;
  794.         for (int i=0; i<np; i++) {
  795.             Point2D.Double pt = polygon[i];
  796.             if (pt != null) {
  797.                 polygonNew[j++] = pt;
  798.             }
  799.         }
  800.  
  801.         return polygonNew;
  802.     }
  803.  
  804.     /**
  805.      * Draws an ellipse without a cloudy border effect.
  806.      */
  807.     private void drawBasicEllipse(double left, double bottom, double right, double top) throws IOException {
  808.         double rx = Math.abs(right - left) / 2;
  809.         double ry = Math.abs(top - bottom) / 2;
  810.         double cx = (left + right) / 2;
  811.         double cy = (bottom + top) / 2;
  812.         getArc(0, 2 * Math.PI, rx, ry, cx, cy, null, true);
  813.     }
  814.  
  815.     private void beginOutput(double x, double y) throws IOException {
  816.         bboxMinX = x;
  817.         bboxMinY = y;
  818.         bboxMaxX = x;
  819.         bboxMaxY = y;
  820.         outputStarted = true;
  821.         // Set line join to bevel to avoid spikes
  822.         cs.setLineJoin(PdfAppearance.LINE_JOIN_BEVEL);
  823.     }
  824.  
  825.     private void updateBBox(double x, double y) {
  826.         bboxMinX = Math.min(bboxMinX, x);
  827.         bboxMinY = Math.min(bboxMinY, y);
  828.         bboxMaxX = Math.max(bboxMaxX, x);
  829.         bboxMaxY = Math.max(bboxMaxY, y);
  830.     }
  831.  
  832.     private void moveTo(Point2D.Double p) throws IOException {
  833.         moveTo(p.x, p.y);
  834.     }
  835.  
  836.     private void moveTo(double x, double y) throws IOException {
  837.         if (outputStarted) {
  838.             updateBBox(x, y);
  839.         }
  840.         else {
  841.             beginOutput(x, y);
  842.         }
  843.  
  844.         cs.moveTo(x, y);
  845.     }
  846.  
  847.     private void lineTo(Point2D.Double p) throws IOException {
  848.         lineTo(p.x, p.y);
  849.     }
  850.  
  851.     private void lineTo(double x, double y) throws IOException {
  852.         if (outputStarted) {
  853.             updateBBox(x, y);
  854.         }
  855.         else {
  856.             beginOutput(x, y);
  857.         }
  858.  
  859.         cs.lineTo(x, y);
  860.     }
  861.  
  862.     private void curveTo(Point2D.Double a, Point2D.Double b, Point2D.Double c) throws IOException {
  863.         curveTo(a.x, a.y, b.x, b.y, c.x, c.y);
  864.     }
  865.  
  866.     private void curveTo(double ax, double ay, double bx, double by, double cx, double cy) throws IOException {
  867.         updateBBox(ax, ay);
  868.         updateBBox(bx, by);
  869.         updateBBox(cx, cy);
  870.         cs.curveTo(ax, ay, bx, by, cx, cy);
  871.     }
  872.  
  873.     private void finish() throws IOException {
  874.         if (outputStarted) {
  875.             cs.closePath();
  876.         }
  877.  
  878.         if (lineWidth > 0) {
  879.             double d = lineWidth / 2;
  880.             bboxMinX -= d;
  881.             bboxMinY -= d;
  882.             bboxMaxX += d;
  883.             bboxMaxY += d;
  884.         }
  885.     }
  886.  
  887.     private double getEllipseCloudRadius() {
  888.         // Equation deduced from Acrobat Reader's appearance streams. Circle
  889.         // annotations have a slightly larger radius than Polygons and Squares.
  890.         return 4.75 * intensity + 0.5 * lineWidth;
  891.     }
  892.  
  893.     private double getPolygonCloudRadius() {
  894.         // Equation deduced from Acrobat Reader's appearance streams.
  895.         return 4 * intensity + 0.5 * lineWidth;
  896.     }
  897. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement