All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.pdfbox.pdmodel.interactive.annotation.handlers.CloudyBorder Maven / Gradle / Ivy

Go to download

The Apache PDFBox library is an open source Java tool for working with PDF documents.

There is a newer version: 3.0.2
Show newest version
/*
 * 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy