# CloudyBorder.java

    /*
     * 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.awt.geom.Ellipse2D;
    import java.awt.geom.PathIterator;
    import java.awt.geom.Point2D;
    import java.io.IOException;
    import java.util.ArrayList;

    import org.apache.pdfbox.pdmodel.common.PDRectangle;
    import org.apache.pdfbox.pdmodel.PDAppearanceContentStream;

    /**
     * Generates annotation appearances with a cloudy border.
     * <p>
     * Dashed stroke styles are not recommended with cloudy borders. The result would
     * not look good because some parts of the arcs are traced twice by the stroked
     * path. Actually Acrobat Reader's line style dialog does not allow to choose a
     * dashed and a cloudy style at the same time.
     */

    class CloudyBorder
    {
        private static final double ANGLE_180_DEG = Math.PI;
        private static final double ANGLE_90_DEG = Math.PI / 2;
        private static final double ANGLE_34_DEG = Math.toRadians(34);
        private static final double ANGLE_30_DEG = Math.toRadians(30);
        private static final double ANGLE_12_DEG = Math.toRadians(12);

        private final PDAppearanceContentStream output;
        private final PDRectangle annotRect;
        private final double intensity;
        private final double lineWidth;
        private PDRectangle rectWithDiff;
        private boolean outputStarted = false;
        private double bboxMinX;
        private double bboxMinY;
        private double bboxMaxX;
        private double bboxMaxY;

        /**
         * Creates a new <code>CloudyBorder</code> that writes to the specified
         * content stream.
         *
         * @param stream content stream
         * @param intensity intensity of cloudy effect (entry <code>I</code>); typically 1.0 or 2.0
         * @param lineWidth line width for annotation border (entry <code>W</code>)
         * @param rect annotation rectangle (entry <code>Rect</code>)
         */
        CloudyBorder(PDAppearanceContentStream stream, double intensity,
        double lineWidth, PDRectangle rect)
        {
            this.output = stream;
            this.intensity = intensity;
            this.lineWidth = lineWidth;
            this.annotRect = rect;
        }

        /**
         * Creates a cloudy border for a rectangular annotation.
         * The rectangle is specified by the <code>RD</code> entry and the
         * <code>Rect</code> entry that was passed in to the constructor.
         * <p>
         * This can be used for Square and FreeText annotations. However, this does
         * not produce the text and the callout line for FreeTexts.
         *
         * @param rd entry <code>RD</code>, or null if the entry does not exist
         * @throws IOException If there is an error writing to the stream.
         */
        void createCloudyRectangle(PDRectangle rd) throws IOException
        {
            rectWithDiff = applyRectDiff(rd, (float) (lineWidth / 2));
            double left = rectWithDiff.getLowerLeftX();
            double bottom = rectWithDiff.getLowerLeftY();
            double right = rectWithDiff.getUpperRightX();
            double top = rectWithDiff.getUpperRightY();

            cloudyRectangleImpl(left, bottom, right, top, false);
            finish();
        }

        /**
         * Creates a cloudy border for a Polygon annotation.
         *
         * @param path polygon path
         * @throws IOException If there is an error writing to the stream.
         */
        void createCloudyPolygon(float[][] path) throws IOException
        {
            int n = path.length;
            Point2D.Double[] polygon = new Point2D.Double[n];

            for (int i = 0; i < n; i++)
            {
                float[] array = path[i];
                if (array.length == 2)
                {
                    polygon[i] = new Point2D.Double(array[0], array[1]);
                }
                else if (array.length == 6)
                {
                    // TODO Curve segments are not yet supported in cloudy border.
                    polygon[i] = new Point2D.Double(array[4], array[5]);
                }
            }

            cloudyPolygonImpl(polygon, false);
            finish();
        }

        /**
         * Creates a cloudy border for a Circle annotation.
         * The ellipse is specified by the <code>RD</code> entry and the
         * <code>Rect</code> entry that was passed in to the constructor.
         *
         * @param rd entry <code>RD</code>, or null if the entry does not exist
         * @throws IOException If there is an error writing to the stream.
         */
        void createCloudyEllipse(PDRectangle rd) throws IOException
        {
            rectWithDiff = applyRectDiff(rd, 0);
            double left = rectWithDiff.getLowerLeftX();
            double bottom = rectWithDiff.getLowerLeftY();
            double right = rectWithDiff.getUpperRightX();
            double top = rectWithDiff.getUpperRightY();

            cloudyEllipseImpl(left, bottom, right, top);
            finish();
        }

        /**
         * Returns the <code>BBox</code> entry (bounding box) for the
         * appearance stream form XObject.
         *
         * @return Bounding box for appearance stream form XObject.
         */
        PDRectangle getBBox()
        {
            return getRectangle();
        }

        /**
         * Returns the updated <code>Rect</code> entry for the annotation.
         * The rectangle completely contains the cloudy border.
         *
         * @return Annotation <code>Rect</code>.
         */
        PDRectangle getRectangle()
        {
            return new PDRectangle((float)bboxMinX, (float)bboxMinY,
                (float)(bboxMaxX - bboxMinX), (float)(bboxMaxY - bboxMinY));
        }

        /**
         * Returns the <code>Matrix</code> entry for the appearance stream form XObject.
         *
         * @return Matrix for appearance stream form XObject.
         */
        AffineTransform getMatrix()
        {
            return AffineTransform.getTranslateInstance(-bboxMinX, -bboxMinY);
        }

        /**
         * Returns the updated <code>RD</code> entry for Square and Circle annotations.
         *
         * @return Annotation <code>RD</code> value.
         */
        PDRectangle getRectDifference()
        {
            if (annotRect == null)
            {
                float d = (float)lineWidth / 2;
                return new PDRectangle(d, d, (float)lineWidth, (float)lineWidth);
            }

            PDRectangle re = (rectWithDiff != null) ? rectWithDiff : annotRect;

            float left = re.getLowerLeftX() - (float)bboxMinX;
            float bottom = re.getLowerLeftY() - (float)bboxMinY;
            float right = (float)bboxMaxX - re.getUpperRightX();
            float top = (float)bboxMaxY - re.getUpperRightY();

            return new PDRectangle(left, bottom, right - left, top - bottom);
        }

        private static double cosine(double dx, double hypot)
        {
            if (Double.compare(hypot, 0.0) == 0)
            {
                return 0;
            }
            return dx / hypot;
        }

        private static double sine(double dy, double hypot)
        {
            if (Double.compare(hypot, 0.0) == 0)
            {
                return 0;
            }
            return dy / hypot;
        }

        /**
         * Cloudy rectangle implementation is based on converting the rectangle
         * to a polygon.
         */
        private void cloudyRectangleImpl(double left, double bottom,
        double right, double top, boolean isEllipse) throws IOException
        {
            double w = right - left;
            double h = top - bottom;

            if (intensity <= 0.0)
            {
                output.addRect((float)left, (float)bottom, (float)w, (float)h);
                bboxMinX = left;
                bboxMinY = bottom;
                bboxMaxX = right;
                bboxMaxY = top;
                return;
            }

            // Make a polygon with direction equal to the positive angle direction.
            Point2D.Double[] polygon;

            if (w < 1.0)
            {
                polygon = new Point2D.Double[]
                {
                    new Point2D.Double(left, bottom), new Point2D.Double(left, top),
                    new Point2D.Double(left, bottom)
                };
            }
            else if (h < 1.0)
            {
                polygon = new Point2D.Double[]
                {
                    new Point2D.Double(left, bottom), new Point2D.Double(right, bottom),
                    new Point2D.Double(left, bottom)
                };
            }
            else
            {
                polygon = new Point2D.Double[]
                {
                    new Point2D.Double(left, bottom), new Point2D.Double(right, bottom),
                    new Point2D.Double(right, top), new Point2D.Double(left, top),
                    new Point2D.Double(left, bottom)
                };
            }

            cloudyPolygonImpl(polygon, isEllipse);
        }

        /**
         * Cloudy polygon implementation.
         *
         * @param vertices polygon vertices; first and last point must be equal
         * @param isEllipse specifies if the polygon represents an ellipse
         */
        private void cloudyPolygonImpl(Point2D.Double[] vertices, boolean isEllipse)
        throws IOException
        {
            Point2D.Double[] polygon = removeZeroLengthSegments(vertices);
            getPositivePolygon(polygon);
            int numPoints = polygon.length;

            if (numPoints < 2)
            {
                return;
            }
            if (intensity <= 0.0)
            {
                moveTo(polygon[0]);
                for (int i = 1; i < numPoints; i++)
                {
                    lineTo(polygon[i]);
                }
                return;
            }

            double cloudRadius = isEllipse ? getEllipseCloudRadius() : getPolygonCloudRadius();

            if (cloudRadius < 0.5)
            {
                cloudRadius = 0.5;
            }

            final double k = Math.cos(ANGLE_34_DEG);
            final double advIntermDefault = 2 * k * cloudRadius;
            final double advCornerDefault = k * cloudRadius;
            double[] array = new double[2];
            double anglePrev = 0;

            // The number of curls per polygon segment is hardly ever an integer,
            // so the length of some curls must be adjustable. We adjust the angle
            // of the trailing arc of corner curls and the leading arc of the first
            // intermediate curl.
            // In each polygon segment, we have n intermediate curls plus one half of a
            // corner curl at each end. One of the n intermediate curls is adjustable.
            // Thus the number of fixed (or unadjusted) intermediate curls is n - 1.

            // Find the adjusted angle `alpha` for the first corner curl.
            int n0 = computeParamsPolygon(advIntermDefault, advCornerDefault, k, cloudRadius,
                polygon[numPoints - 2].distance(polygon[0]), array);
            double alphaPrev = (n0 == 0) ? array[0] : ANGLE_34_DEG;

            for (int j = 0; j + 1 < numPoints; j++)
            {
                Point2D.Double pt = polygon[j];
                Point2D.Double ptNext = polygon[j + 1];
                double length = pt.distance(ptNext);
                if (Double.compare(length, 0.0) == 0)
                {
                    alphaPrev = ANGLE_34_DEG;
                    continue;
                }

                // n is the number of intermediate curls in the current polygon segment.
                int n = computeParamsPolygon(advIntermDefault, advCornerDefault, k,
                    cloudRadius, length, array);
                if (n < 0)
                {
                    if (!outputStarted)
                    {
                        moveTo(pt);
                    }
                    continue;
                }

                double alpha = array[0];
                double dx = array[1];

                double angleCur = Math.atan2(ptNext.y - pt.y, ptNext.x - pt.x);
                if (j == 0)
                {
                    Point2D.Double ptPrev = polygon[numPoints - 2];
                    anglePrev = Math.atan2(pt.y - ptPrev.y, pt.x - ptPrev.x);
                }

                double cos = cosine(ptNext.x - pt.x, length);
                double sin = sine(ptNext.y - pt.y, length);
                double x = pt.x;
                double y = pt.y;

                addCornerCurl(anglePrev, angleCur, cloudRadius, pt.x, pt.y, alpha,
                    alphaPrev, !outputStarted);
                // Proceed to the center point of the first intermediate curl.
                double adv = 2 * k * cloudRadius + 2 * dx;
                x += adv * cos;
                y += adv * sin;

                // Create the first intermediate curl.
                int numInterm = n;
                if (n >= 1)
                {
                    addFirstIntermediateCurl(angleCur, cloudRadius, alpha, x, y);
                    x += advIntermDefault * cos;
                    y += advIntermDefault * sin;
                    numInterm = n - 1;
                }

                // Create one intermediate curl and replicate it along the polygon segment.
                Point2D.Double[] template = getIntermediateCurlTemplate(angleCur, cloudRadius);
                for (int i = 0; i < numInterm; i++)
                {
                    outputCurlTemplate(template, x, y);
                    x += advIntermDefault * cos;
                    y += advIntermDefault * sin;
                }

                anglePrev = angleCur;
                alphaPrev = (n == 0) ? alpha : ANGLE_34_DEG;
            }
        }

        /**
         * Computes parameters for a cloudy polygon: n, alpha, and dx.
         */
        private int computeParamsPolygon(double advInterm, double advCorner, double k,
        double r, double length, double[] array)
        {
            if (Double.compare(length, 0.0) == 0)
            {
                array[0] = ANGLE_34_DEG;
                array[1] = 0;
                return -1;
            }

            // n is the number of intermediate curls in the current polygon segment
            int n = (int) Math.ceil((length - 2 * advCorner) / advInterm);

            // Fitting error along polygon segment
            double e = length - (2 * advCorner + n * advInterm);
            // Fitting error per each adjustable half curl
            double dx = e / 2;

            // Convert fitting error to an angle that can be used to control arcs.
            double arg = (k * r + dx) / r;
            double alpha = (arg < -1.0 || arg > 1.0) ? 0.0 : Math.acos(arg);

            array[0] = alpha;
            array[1] = dx;
            return n;
        }

        /**
         * Creates a corner curl for polygons and ellipses.
         */
        private void addCornerCurl(double anglePrev, double angleCur, double radius,
        double cx, double cy, double alpha, double alphaPrev, boolean addMoveTo)
        throws IOException
        {
            double a = anglePrev + ANGLE_180_DEG + alphaPrev;
            double b = anglePrev + ANGLE_180_DEG + alphaPrev - Math.toRadians(22);
            getArcSegment(a, b, cx, cy, radius, radius, null, addMoveTo);

            a = b;
            b = angleCur - alpha;
            getArc(a, b, radius, radius, cx, cy, null, false);
        }

        /**
         * Generates the first intermediate curl for a cloudy polygon.
         */
        private void addFirstIntermediateCurl(double angleCur, double r, double alpha,
        double cx, double cy) throws IOException
        {
            double a = angleCur + ANGLE_180_DEG;

            getArcSegment(a + alpha, a + alpha - ANGLE_30_DEG, cx, cy, r, r, null, false);
            getArcSegment(a + alpha - ANGLE_30_DEG, a + ANGLE_90_DEG, cx, cy, r, r, null, false);
            getArcSegment(a + ANGLE_90_DEG, a + ANGLE_180_DEG - ANGLE_34_DEG,
                cx, cy, r, r, null, false);
        }

        /**
         * Returns a template for intermediate curls in a cloudy polygon.
         */
        private Point2D.Double[] getIntermediateCurlTemplate(double angleCur, double r)
        throws IOException
        {
            ArrayList<Point2D.Double> points = new ArrayList<>();
            double a = angleCur + ANGLE_180_DEG;

            getArcSegment(a + ANGLE_34_DEG, a + ANGLE_12_DEG, 0, 0, r, r, points, false);
            getArcSegment(a + ANGLE_12_DEG, a + ANGLE_90_DEG,  0, 0, r, r, points, false);
            getArcSegment(a + ANGLE_90_DEG, a + ANGLE_180_DEG - ANGLE_34_DEG,
                0, 0, r, r, points, false);

            return points.toArray(new Point2D.Double[points.size()]);
        }

        /**
         * Writes the curl template points to the output and applies translation (x, y).
         */
        private void outputCurlTemplate(Point2D.Double[] template, double x, double y)
        throws IOException
        {
            int n = template.length;
            int i = 0;

            if ((n % 3) == 1)
            {
                Point2D.Double a = template[0];
                moveTo(a.x + x, a.y + y);
                i++;
            }
            for (; i + 2 < n; i += 3)
            {
                Point2D.Double a = template[i];
                Point2D.Double b = template[i + 1];
                Point2D.Double c = template[i + 2];
                curveTo(a.x + x, a.y + y, b.x + x, b.y + y, c.x + x, c.y + y);
            }
        }

        private PDRectangle applyRectDiff(PDRectangle rd, float min)
        {
            float rectLeft = annotRect.getLowerLeftX();
            float rectBottom = annotRect.getLowerLeftY();
            float rectRight = annotRect.getUpperRightX();
            float rectTop = annotRect.getUpperRightY();

            // Normalize
            rectLeft = Math.min(rectLeft, rectRight);
            rectBottom = Math.min(rectBottom, rectTop);
            rectRight = Math.max(rectLeft, rectRight);
            rectTop = Math.max(rectBottom, rectTop);

            float rdLeft;
            float rdBottom;
            float rdRight;
            float rdTop;

            if (rd != null)
            {
                rdLeft = Math.max(rd.getLowerLeftX(), min);
                rdBottom = Math.max(rd.getLowerLeftY(), min);
                rdRight = Math.max(rd.getUpperRightX(), min);
                rdTop = Math.max(rd.getUpperRightY(), min);
            }
            else
            {
                rdLeft = min;
                rdBottom = min;
                rdRight = min;
                rdTop = min;
            }

            rectLeft += rdLeft;
            rectBottom += rdBottom;
            rectRight -= rdRight;
            rectTop -= rdTop;

            return new PDRectangle(rectLeft, rectBottom, rectRight - rectLeft, rectTop - rectBottom);
        }

        private void reversePolygon(Point2D.Double[] points)
        {
            int len = points.length;
            int n = len / 2;
            for (int i = 0; i < n; i++)
            {
                int j = len - i - 1;
                Point2D.Double pi = points[i];
                Point2D.Double pj = points[j];
                points[i] = pj;
                points[j] = pi;
            }
        }

        /**
         * Makes a polygon whose direction is the same as the positive angle
         * direction in the coordinate system.
         * The polygon must not intersect itself.
         */
        private void getPositivePolygon(Point2D.Double[] points)
        {
            if (getPolygonDirection(points) < 0)
            {
                reversePolygon(points);
            }
        }

        /**
         * Returns the direction of the specified polygon.
         * A positive value indicates that the polygon's direction is the same as the
         * direction of positive angles in the coordinate system.
         * A negative value indicates the opposite direction.
         *
         * The polygon must not intersect itself. A 2-point polygon is not acceptable.
         * This is based on the "shoelace formula".
         */
        private double getPolygonDirection(Point2D.Double[] points)
        {
            double a = 0;
            int len = points.length;
            for (int i = 0; i < len; i++)
            {
                int j = (i + 1) % len;
                a += points[i].x * points[j].y - points[i].y * points[j].x;
            }
            return a;
        }

        /**
         * Creates one or more Bézier curves that represent an elliptical arc.
         * Angles are in radians.
         * The arc will always proceed in the positive angle direction.
         * If the argument `out` is null, this writes the results to the instance
         * variable `output`.
         */
        private void getArc(double startAng, double endAng, double rx, double ry,
        double cx, double cy, ArrayList<Point2D.Double> out, boolean addMoveTo) throws IOException
        {
            final double angleIncr = Math.PI / 2;
            double startx = rx * Math.cos(startAng) + cx;
            double starty = ry * Math.sin(startAng) + cy;

            double angleTodo = endAng - startAng;
            while (angleTodo < 0)
            {
                angleTodo += 2 * Math.PI;
            }
            double sweep = angleTodo;
            double angleDone = 0;

            if (addMoveTo)
            {
                if (out != null)
                {
                    out.add(new Point2D.Double(startx, starty));
                }
                else
                {
                    moveTo(startx, starty);
                }
            }

            while (angleTodo > angleIncr)
            {
                getArcSegment(startAng + angleDone,
                    startAng + angleDone + angleIncr, cx, cy, rx, ry, out, false);
                angleDone += angleIncr;
                angleTodo -= angleIncr;
            }

            if (angleTodo > 0)
            {
                getArcSegment(startAng + angleDone, startAng + sweep, cx, cy, rx, ry, out, false);
            }
        }

        /**
         * Creates a single Bézier curve that represents a section of an elliptical
         * arc. The sweep angle of the section must not be larger than 90 degrees.
         * If argument `out` is null, this writes the results to the instance
         * variable `output`.
         */
        private void getArcSegment(double startAng, double endAng, double cx, double cy,
        double rx, double ry, ArrayList<Point2D.Double> out, boolean addMoveTo) throws IOException
        {
            // Algorithm is from the FAQ of the news group comp.text.pdf

            double cosA = Math.cos(startAng);
            double sinA = Math.sin(startAng);
            double cosB = Math.cos(endAng);
            double sinB = Math.sin(endAng);
            double denom = Math.sin((endAng - startAng) / 2.0);
            if (Double.compare(denom, 0.0) == 0)
            {
                // This can happen only if endAng == startAng.
                // The arc sweep angle is zero, so we create no arc at all.
                if (addMoveTo)
                {
                    double xs = cx + rx * cosA;
                    double ys = cy + ry * sinA;
                    if (out != null)
                    {
                        out.add(new Point2D.Double(xs, ys));
                    }
                    else
                    {
                        moveTo(xs, ys);
                    }
                }
                return;
            }
            double bcp = 1.333333333 * (1 - Math.cos((endAng - startAng) / 2.0)) / denom;
            double p1x = cx + rx * (cosA - bcp * sinA);
            double p1y = cy + ry * (sinA + bcp * cosA);
            double p2x = cx + rx * (cosB + bcp * sinB);
            double p2y = cy + ry * (sinB - bcp * cosB);
            double p3x = cx + rx * cosB;
            double p3y = cy + ry * sinB;

            if (addMoveTo)
            {
                double xs = cx + rx * cosA;
                double ys = cy + ry * sinA;
                if (out != null)
                {
                    out.add(new Point2D.Double(xs, ys));
                }
                else
                {
                    moveTo(xs, ys);
                }
            }

            if (out != null)
            {
                out.add(new Point2D.Double(p1x, p1y));
                out.add(new Point2D.Double(p2x, p2y));
                out.add(new Point2D.Double(p3x, p3y));
            }
            else
            {
                curveTo(p1x, p1y, p2x, p2y, p3x, p3y);
            }
        }

        /**
         * Flattens an ellipse into a polygon.
         */
        private static Point2D.Double[] flattenEllipse(double left, double bottom,
        double right, double top)
        {
            Ellipse2D.Double ellipse = new Ellipse2D.Double(left, bottom, right - left, top - bottom);
            final double flatness = 0.50;
            PathIterator iterator = ellipse.getPathIterator(null, flatness);
            double[] coords = new double[6];
            ArrayList<Point2D.Double> points = new ArrayList<>();

            while (!iterator.isDone())
            {
                switch (iterator.currentSegment(coords))
                {
                    case PathIterator.SEG_MOVETO:
                    case PathIterator.SEG_LINETO:
                        points.add(new Point2D.Double(coords[0], coords[1]));
                        break;
                    // Curve segments are not expected because the path iterator is
                    // flattened. SEG_CLOSE can be ignored.
                    default:
                        break;
                }
                iterator.next();
            }

            int size = points.size();
            final double closeTestLimit = 0.05;

            if (size >= 2 && points.get(size - 1).distance(points.get(0)) > closeTestLimit)
            {
                points.add(points.get(points.size() - 1));
            }
            return points.toArray(new Point2D.Double[points.size()]);
        }

        /**
         * Cloudy ellipse implementation.
         */
        private void cloudyEllipseImpl(final double leftOrig, final double bottomOrig,
        final double rightOrig, final double topOrig) throws IOException
        {
            if (intensity <= 0.0)
            {
                drawBasicEllipse(leftOrig, bottomOrig, rightOrig, topOrig);
                return;
            }

            double left = leftOrig;
            double bottom = bottomOrig;
            double right = rightOrig;
            double top = topOrig;
            double width = right - left;
            double height = top - bottom;
            double cloudRadius = getEllipseCloudRadius();

            // Omit cloudy border if the ellipse is very small.
            final double threshold1 = 0.50 * cloudRadius;
            if (width < threshold1 && height < threshold1)
            {
                drawBasicEllipse(left, bottom, right, top);
                return;
            }

            // Draw a cloudy rectangle instead of an ellipse when the
            // width or height is very small.
            final double threshold2 = 5;
            if ((width < threshold2 && height > 20) || (width > 20 && height < threshold2))
            {
                cloudyRectangleImpl(left, bottom, right, top, true);
                return;
            }

            // Decrease radii (while center point does not move). This makes the
            // "tails" of the curls almost touch the ellipse outline.
            double radiusAdj = Math.sin(ANGLE_12_DEG) * cloudRadius - 1.50;
            if (width > 2 * radiusAdj)
            {
                left += radiusAdj;
                right -= radiusAdj;
            }
            else
            {
                double mid = (left + right) / 2;
                left = mid - 0.10;
                right = mid + 0.10;
            }
            if (height > 2 * radiusAdj)
            {
                top -= radiusAdj;
                bottom += radiusAdj;
            }
            else
            {
                double mid = (top + bottom) / 2;
                top = mid + 0.10;
                bottom = mid - 0.10;
            }

            // Flatten the ellipse into a polygon. The segment lengths of the flattened
            // result don't need to be extremely short because the loop below is able to
            // interpolate between polygon points when it computes the center points
            // at which each curl is placed.

            Point2D.Double[] flatPolygon = flattenEllipse(left, bottom, right, top);
            int numPoints = flatPolygon.length;
            if (numPoints < 2)
            {
                return;
            }

            double totLen = 0;
            for(int i = 1; i < numPoints; i++){
                totLen += flatPolygon[i - 1].distance(flatPolygon[i]);
            }

            final double k = Math.cos(ANGLE_34_DEG);
            double curlAdvance = 2 * k * cloudRadius;
            int n = (int) Math.ceil(totLen / curlAdvance);
            if (n < 2)
            {
                drawBasicEllipse(leftOrig, bottomOrig, rightOrig, topOrig);
                return;
            }

            curlAdvance = totLen / n;
            cloudRadius = curlAdvance / (2 * k);

            if (cloudRadius < 0.5)
            {
                cloudRadius = 0.5;
                curlAdvance = 2 * k * cloudRadius;
            }
            else if (cloudRadius < 3.0)
            {
                // Draw a small circle when the scaled radius becomes very small.
                // This happens also if intensity is much smaller than 1.
                drawBasicEllipse(leftOrig, bottomOrig, rightOrig, topOrig);
                return;
            }

            // Construct centerPoints array, in which each point is the center point of a curl.
            // The length of each centerPoints segment ideally equals curlAdv but that
            // is not true in regions where the ellipse curvature is high.

            int centerPointsLength = n;
            Point2D.Double[] centerPoints = new Point2D.Double[centerPointsLength];
            int centerPointsIndex = 0;
            double lengthRemain = 0;
            final double comparisonToler = lineWidth * 0.10;

            for (int i = 0; i + 1 < numPoints; i++)
            {
                Point2D.Double p1 = flatPolygon[i];
                Point2D.Double p2 = flatPolygon[i + 1];
                double dx = p2.x - p1.x;
                double dy = p2.y - p1.y;
                double length = p1.distance(p2);
                if (Double.compare(length, 0.0) == 0)
                {
                    continue;
                }
                double lengthTodo = length + lengthRemain;
                if (lengthTodo >= curlAdvance - comparisonToler || i == numPoints - 2)
                {
                    double cos = cosine(dx, length);
                    double sin = sine(dy, length);
                    double d = curlAdvance - lengthRemain;
                    do
                    {
                        double x = p1.x + d * cos;
                        double y = p1.y + d * sin;
                        if (centerPointsIndex < centerPointsLength)
                        {
                            centerPoints[centerPointsIndex++] = new Point2D.Double(x, y);
                        }
                        lengthTodo -= curlAdvance;
                        d += curlAdvance;
                    }
                    while (lengthTodo >= curlAdvance - comparisonToler);

                    lengthRemain = lengthTodo;
                    if (lengthRemain < 0)
                    {
                        lengthRemain = 0;
                    }
                }
                else
                {
                    lengthRemain += length;
                }
            }

            // Note: centerPoints does not repeat the first point as the last point
            // to create a "closing" segment.

            // Place a curl at each point of the centerPoints array.
            // In regions where the ellipse curvature is high, the centerPoints segments
            // are shorter than the actual distance along the ellipse. Thus we must
            // again compute arc adjustments like in cloudy polygons.

            numPoints = centerPointsIndex;
            double anglePrev = 0;
            double alphaPrev = 0;

            for (int i = 0; i < numPoints; i++)
            {
                int idxNext = i + 1;
                if (i + 1 >= numPoints)
                {
                    idxNext = 0;
                }
                Point2D.Double pt = centerPoints[i];
                Point2D.Double ptNext = centerPoints[idxNext];

                if (i == 0)
                {
                    Point2D.Double ptPrev = centerPoints[numPoints - 1];
                    anglePrev = Math.atan2(pt.y - ptPrev.y, pt.x - ptPrev.x);
                    alphaPrev = computeParamsEllipse(ptPrev, pt, cloudRadius, curlAdvance);
                }

                double angleCur = Math.atan2(ptNext.y - pt.y, ptNext.x - pt.x);
                double alpha = computeParamsEllipse(pt, ptNext, cloudRadius, curlAdvance);

                addCornerCurl(anglePrev, angleCur, cloudRadius, pt.x, pt.y, alpha,
                    alphaPrev, !outputStarted);

                anglePrev = angleCur;
                alphaPrev = alpha;
            }
        }

        /**
         * Computes the alpha parameter for an ellipse curl.
         */
        private double computeParamsEllipse(Point2D.Double pt, Point2D.Double ptNext,
        double r, double curlAdv)
        {
            double length = pt.distance(ptNext);
            if (Double.compare(length, 0.0) == 0)
            {
                return ANGLE_34_DEG;
            }

            double e = length - curlAdv;
            double arg = (curlAdv / 2 + e / 2) / r;
            return (arg < -1.0 || arg > 1.0) ? 0.0 : Math.acos(arg);
        }

        private Point2D.Double[] removeZeroLengthSegments(Point2D.Double[] polygon)
        {
            int np = polygon.length;
            if (np <= 2)
            {
                return polygon;
            }

            final double toler = 0.50;
            int npNew = np;
            Point2D.Double ptPrev = polygon[0];

            // Don't remove the last point if it equals the first point.
            for (int i = 1; i < np; i++)
            {
                Point2D.Double pt = polygon[i];
                if (Math.abs(pt.x - ptPrev.x) < toler && Math.abs(pt.y - ptPrev.y) < toler)
                {
                    polygon[i] = null;
                    npNew--;
                }
                ptPrev = pt;
            }

            if (npNew == np)
            {
                return polygon;
            }

            Point2D.Double[] polygonNew = new Point2D.Double[npNew];
            int j = 0;
            for (int i = 0; i < np; i++)
            {
                Point2D.Double pt = polygon[i];
                if (pt != null)
                {
                    polygonNew[j++] = pt;
                }
            }

            return polygonNew;
        }

        /**
         * Draws an ellipse without a cloudy border effect.
         */
        private void drawBasicEllipse(double left, double bottom, double right, double top)
        throws IOException
        {
            double rx = Math.abs(right - left) / 2;
            double ry = Math.abs(top - bottom) / 2;
            double cx = (left + right) / 2;
            double cy = (bottom + top) / 2;
            getArc(0, 2 * Math.PI, rx, ry, cx, cy, null, true);
        }

        private void beginOutput(double x, double y) throws IOException
        {
            bboxMinX = x;
            bboxMinY = y;
            bboxMaxX = x;
            bboxMaxY = y;
            outputStarted = true;
            // Set line join to bevel to avoid spikes
            output.setLineJoinStyle(2);
        }

        private void updateBBox(double x, double y)
        {
            bboxMinX = Math.min(bboxMinX, x);
            bboxMinY = Math.min(bboxMinY, y);
            bboxMaxX = Math.max(bboxMaxX, x);
            bboxMaxY = Math.max(bboxMaxY, y);
        }

        private void moveTo(Point2D.Double p) throws IOException
        {
            moveTo(p.x, p.y);
        }

        private void moveTo(double x, double y) throws IOException
        {
            if (outputStarted)
            {
                updateBBox(x, y);
            }
            else
            {
                beginOutput(x, y);
            }

            output.moveTo((float)x, (float)y);
        }

        private void lineTo(Point2D.Double p) throws IOException
        {
            lineTo(p.x, p.y);
        }

        private void lineTo(double x, double y) throws IOException
        {
            if (outputStarted)
            {
                updateBBox(x, y);
            }
            else
            {
                beginOutput(x, y);
            }

            output.lineTo((float)x, (float)y);
        }

        private void curveTo(double ax, double ay, double bx, double by, double cx, double cy)
        throws IOException
        {
            updateBBox(ax, ay);
            updateBBox(bx, by);
            updateBBox(cx, cy);
            output.curveTo((float)ax, (float)ay, (float)bx, (float)by, (float)cx, (float)cy);
        }

        private void finish() throws IOException
        {
            if (outputStarted)
            {
                output.closePath();
            }

            if (lineWidth > 0)
            {
                double d = lineWidth / 2;
                bboxMinX -= d;
                bboxMinY -= d;
                bboxMaxX += d;
                bboxMaxY += d;
            }
        }

        private double getEllipseCloudRadius()
        {
            // Equation deduced from Acrobat Reader's appearance streams. Circle
            // annotations have a slightly larger radius than Polygons and Squares.
            return 4.75 * intensity + 0.5 * lineWidth;
        }

        private double getPolygonCloudRadius()
        {
            // Equation deduced from Acrobat Reader's appearance streams.
            return 4 * intensity + 0.5 * lineWidth;
        }
    }