org.apache.pdfbox.pdmodel.interactive.annotation.handlers.CloudyBorder Maven / Gradle / Ivy
Show all versions of pdfbox Show documentation
/*
* 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.
*
* 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 CloudyBorder
that writes to the specified
* content stream.
*
* @param stream content stream
* @param intensity intensity of cloudy effect (entry I
); typically 1.0 or 2.0
* @param lineWidth line width for annotation border (entry W
)
* @param rect annotation rectangle (entry Rect
)
*/
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 RD
entry and the
* Rect
entry that was passed in to the constructor.
*
* 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 RD
, 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, 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 RD
entry and the
* Rect
entry that was passed in to the constructor.
*
* @param rd entry RD
, 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 BBox
entry (bounding box) for the
* appearance stream form XObject.
*
* @return Bounding box for appearance stream form XObject.
*/
PDRectangle getBBox()
{
return getRectangle();
}
/**
* Returns the updated Rect
entry for the annotation.
* The rectangle completely contains the cloudy border.
*
* @return Annotation Rect
.
*/
PDRectangle getRectangle()
{
return new PDRectangle((float)bboxMinX, (float)bboxMinY,
(float)(bboxMaxX - bboxMinX), (float)(bboxMaxY - bboxMinY));
}
/**
* Returns the Matrix
entry for the appearance stream form XObject.
*
* @return Matrix for appearance stream form XObject.
*/
AffineTransform getMatrix()
{
return AffineTransform.getTranslateInstance(-bboxMinX, -bboxMinY);
}
/**
* Returns the updated RD
entry for Square and Circle annotations.
*
* @return Annotation RD
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 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, double 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);
double rdLeft;
double rdBottom;
double rdRight;
double 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 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 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 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;
}
}