From commits-return-14815-archive-asf-public=cust-asf.ponee.io@pdfbox.apache.org Sun Jun 16 15:00:20 2019 Return-Path: X-Original-To: archive-asf-public@cust-asf.ponee.io Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [207.244.88.153]) by mx-eu-01.ponee.io (Postfix) with SMTP id 03BFE180662 for ; Sun, 16 Jun 2019 17:00:19 +0200 (CEST) Received: (qmail 90900 invoked by uid 500); 16 Jun 2019 15:00:19 -0000 Mailing-List: contact commits-help@pdfbox.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@pdfbox.apache.org Delivered-To: mailing list commits@pdfbox.apache.org Received: (qmail 90891 invoked by uid 99); 16 Jun 2019 15:00:19 -0000 Received: from Unknown (HELO svn01-us-west.apache.org) (209.188.14.144) by apache.org (qpsmtpd/0.29) with ESMTP; Sun, 16 Jun 2019 15:00:19 +0000 Received: from svn01-us-west.apache.org (localhost [127.0.0.1]) by svn01-us-west.apache.org (ASF Mail Server at svn01-us-west.apache.org) with ESMTP id 770743A31B6 for ; Sun, 16 Jun 2019 15:00:18 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Subject: svn commit: r1861460 - in /pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation: handlers/ layout/ Date: Sun, 16 Jun 2019 15:00:17 -0000 To: commits@pdfbox.apache.org From: msahyoun@apache.org X-Mailer: svnmailer-1.0.9 Message-Id: <20190616150018.770743A31B6@svn01-us-west.apache.org> Author: msahyoun Date: Sun Jun 16 15:00:17 2019 New Revision: 1861460 URL: http://svn.apache.org/viewvc?rev=1861460&view=rev Log: PDFBOX-4574: port utility classes for individual appearance handlers Added: pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/AnnotationBorder.java pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/CloudyBorder.java pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/layout/ pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/layout/AppearanceStyle.java pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/layout/PlainText.java pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/layout/PlainTextFormatter.java Added: pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/AnnotationBorder.java URL: http://svn.apache.org/viewvc/pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/AnnotationBorder.java?rev=1861460&view=auto ============================================================================== --- pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/AnnotationBorder.java (added) +++ pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/AnnotationBorder.java Sun Jun 16 15:00:17 2019 @@ -0,0 +1,88 @@ +/* + * Copyright 2018 The Apache Software Foundation. + * + * Licensed 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 org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSBase; +import org.apache.pdfbox.cos.COSNumber; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary; + +/** + * Class to collect all sort of border info about annotations. + * + * @author Tilman Hausherr + */ +class AnnotationBorder +{ + float[] dashArray = null; + boolean underline = false; + float width = 0; + + // return border info. BorderStyle must be provided as parameter because + // method is not available in the base class + static AnnotationBorder getAnnotationBorder(PDAnnotation annotation, + PDBorderStyleDictionary borderStyle) + { + AnnotationBorder ab = new AnnotationBorder(); + if (borderStyle == null) + { + COSArray border = annotation.getBorder(); + if (border.size() >= 3 && border.getObject(2) instanceof COSNumber) + { + ab.width = ((COSNumber) border.getObject(2)).floatValue(); + } + if (border.size() > 3) + { + COSBase base3 = border.getObject(3); + if (base3 instanceof COSArray) + { + ab.dashArray = ((COSArray) base3).toFloatArray(); + } + } + } + else + { + ab.width = borderStyle.getWidth(); + if (borderStyle.getStyle().equals(PDBorderStyleDictionary.STYLE_DASHED)) + { + ab.dashArray = borderStyle.getDashStyle().getDashArray(); + } + if (borderStyle.getStyle().equals(PDBorderStyleDictionary.STYLE_UNDERLINE)) + { + ab.underline = true; + } + } + if (ab.dashArray != null) + { + boolean allZero = true; + for (float f : ab.dashArray) + { + if (Float.compare(f, 0) != 0) + { + allZero = false; + break; + } + } + if (allZero) + { + ab.dashArray = null; + } + } + return ab; + } +} Added: pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/CloudyBorder.java URL: http://svn.apache.org/viewvc/pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/CloudyBorder.java?rev=1861460&view=auto ============================================================================== --- pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/CloudyBorder.java (added) +++ pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/handlers/CloudyBorder.java Sun Jun 16 15:00:17 2019 @@ -0,0 +1,1105 @@ +/* + * 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; + } +} Added: pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/layout/AppearanceStyle.java URL: http://svn.apache.org/viewvc/pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/layout/AppearanceStyle.java?rev=1861460&view=auto ============================================================================== --- pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/layout/AppearanceStyle.java (added) +++ pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/layout/AppearanceStyle.java Sun Jun 16 15:00:17 2019 @@ -0,0 +1,102 @@ +/* + * 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.layout; + +import org.apache.pdfbox.pdmodel.font.PDFont; + +/** + * Define styling attributes to be used for text formatting. + * + */ +public class AppearanceStyle +{ + private PDFont font; + /** + * The font size to be used for text formatting. + * + * Defaulting to 12 to match Acrobats default. + */ + private float fontSize = 12.0f; + + /** + * The leading (distance between lines) to be used for text formatting. + * + * Defaulting to 1.2*fontSize to match Acrobats default. + */ + private float leading = 14.4f; + + /** + * Get the font used for text formatting. + * + * @return the font used for text formatting. + */ + PDFont getFont() + { + return font; + } + + /** + * Set the font to be used for text formatting. + * + * @param font the font to be used. + */ + public void setFont(PDFont font) + { + this.font = font; + } + + /** + * Get the fontSize used for text formatting. + * + * @return the fontSize used for text formatting. + */ + float getFontSize() + { + return fontSize; + } + + /** + * Set the font size to be used for formatting. + * + * @param fontSize the font size. + */ + public void setFontSize(float fontSize) + { + this.fontSize = fontSize; + leading = fontSize * 1.2f; + } + + /** + * Get the leading used for text formatting. + * + * @return the leading used for text formatting. + */ + float getLeading() + { + return leading; + } + + /** + * Set the leading used for text formatting. + * + * @param leading the leading to be used. + */ + void setLeading(float leading) + { + this.leading = leading; + } +} \ No newline at end of file Added: pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/layout/PlainText.java URL: http://svn.apache.org/viewvc/pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/layout/PlainText.java?rev=1861460&view=auto ============================================================================== --- pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/layout/PlainText.java (added) +++ pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/layout/PlainText.java Sun Jun 16 15:00:17 2019 @@ -0,0 +1,290 @@ +/* + * 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.layout; + +import java.io.IOException; +import java.text.AttributedString; +import java.text.BreakIterator; +import java.text.AttributedCharacterIterator.Attribute; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.pdfbox.pdmodel.font.PDFont; + +/** + * A block of text. + *

+ * A block of text can contain multiple paragraphs which will + * be treated individually within the block placement. + *

+ * + */ +public class PlainText +{ + private static final float FONTSCALE = 1000f; + + private final List paragraphs; + + /** + * Construct the text block from a single value. + * + * Constructs the text block from a single value splitting + * into individual {@link Paragraph} when a new line character is + * encountered. + * + * @param textValue the text block string. + */ + public PlainText(String textValue) + { + List parts = Arrays.asList(textValue.replaceAll("\t", " ").split("\\r\\n|\\n|\\r|\\u2028|\\u2029")); + paragraphs = new ArrayList(); + for (String part : parts) + { + // Acrobat prints a space for an empty paragraph + if (part.length() == 0) + { + part = " "; + } + paragraphs.add(new Paragraph(part)); + } + } + + /** + * Construct the text block from a list of values. + * + * Constructs the text block from a list of values treating each + * entry as an individual {@link Paragraph}. + * + * @param listValue the text block string. + */ + public PlainText(List listValue) + { + paragraphs = new ArrayList(); + for (String part : listValue) + { + paragraphs.add(new Paragraph(part)); + } + } + + /** + * Get the list of paragraphs. + * + * @return the paragraphs. + */ + List getParagraphs() + { + return paragraphs; + } + + /** + * Attribute keys and attribute values used for text handling. + * + * This is similar to {@link java.awt.font.TextAttribute} but + * handled individually as to avoid a dependency on awt. + * + */ + static class TextAttribute extends Attribute + { + /** + * UID for serializing. + */ + private static final long serialVersionUID = -3138885145941283005L; + + /** + * Attribute width of the text. + */ + public static final Attribute WIDTH = new TextAttribute("width"); + + protected TextAttribute(String name) + { + super(name); + } + + + } + + /** + * A block of text to be formatted as a whole. + *

+ * A block of text can contain multiple paragraphs which will + * be treated individually within the block placement. + *

+ * + */ + static class Paragraph + { + private final String textContent; + + Paragraph(String text) + { + textContent = text; + } + + /** + * Get the paragraph text. + * + * @return the text. + */ + String getText() + { + return textContent; + } + + /** + * Break the paragraph into individual lines. + * + * @param font the font used for rendering the text. + * @param fontSize the fontSize used for rendering the text. + * @param width the width of the box holding the content. + * @return the individual lines. + * @throws IOException + */ + List getLines(PDFont font, float fontSize, float width) throws IOException + { + BreakIterator iterator = BreakIterator.getLineInstance(); + iterator.setText(textContent); + + final float scale = fontSize/FONTSCALE; + + int start = iterator.first(); + int end = iterator.next(); + float lineWidth = 0; + + List textLines = new ArrayList(); + Line textLine = new Line(); + + while (end != BreakIterator.DONE) + { + String word = textContent.substring(start,end); + float wordWidth = font.getStringWidth(word) * scale; + + lineWidth = lineWidth + wordWidth; + + // check if the last word would fit without the whitespace ending it + if (lineWidth >= width && Character.isWhitespace(word.charAt(word.length()-1))) + { + float whitespaceWidth = font.getStringWidth(word.substring(word.length()-1)) * scale; + lineWidth = lineWidth - whitespaceWidth; + } + + if (lineWidth >= width) + { + textLine.setWidth(textLine.calculateWidth(font, fontSize)); + textLines.add(textLine); + textLine = new Line(); + lineWidth = font.getStringWidth(word) * scale; + } + + AttributedString as = new AttributedString(word); + as.addAttribute(TextAttribute.WIDTH, wordWidth); + Word wordInstance = new Word(word); + wordInstance.setAttributes(as); + textLine.addWord(wordInstance); + start = end; + end = iterator.next(); + } + textLine.setWidth(textLine.calculateWidth(font, fontSize)); + textLines.add(textLine); + return textLines; + } + } + + /** + * An individual line of text. + */ + static class Line + { + private final List words = new ArrayList(); + private float lineWidth; + + float getWidth() + { + return lineWidth; + } + + void setWidth(float width) + { + lineWidth = width; + } + + float calculateWidth(PDFont font, float fontSize) throws IOException + { + final float scale = fontSize/FONTSCALE; + float calculatedWidth = 0f; + for (Word word : words) + { + calculatedWidth = calculatedWidth + + (Float) word.getAttributes().getIterator().getAttribute(TextAttribute.WIDTH); + String text = word.getText(); + if (words.indexOf(word) == words.size() -1 && Character.isWhitespace(text.charAt(text.length()-1))) + { + float whitespaceWidth = font.getStringWidth(text.substring(text.length()-1)) * scale; + calculatedWidth = calculatedWidth - whitespaceWidth; + } + } + return calculatedWidth; + } + + List getWords() + { + return words; + } + + float getInterWordSpacing(float width) + { + return (width - lineWidth)/(words.size()-1); + } + + void addWord(Word word) + { + words.add(word); + } + } + + /** + * An individual word. + * + * A word is defined as a string which must be kept + * on the same line. + */ + static class Word + { + private AttributedString attributedString; + private final String textContent; + + Word(String text) + { + textContent = text; + } + + String getText() + { + return textContent; + } + + AttributedString getAttributes() + { + return attributedString; + } + + void setAttributes(AttributedString as) + { + this.attributedString = as; + } + } +} Added: pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/layout/PlainTextFormatter.java URL: http://svn.apache.org/viewvc/pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/layout/PlainTextFormatter.java?rev=1861460&view=auto ============================================================================== --- pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/layout/PlainTextFormatter.java (added) +++ pdfbox/branches/2.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/annotation/layout/PlainTextFormatter.java Sun Jun 16 15:00:17 2019 @@ -0,0 +1,287 @@ +/* + * 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.layout; + +import java.io.IOException; +import java.util.List; + +import org.apache.pdfbox.pdmodel.PDAppearanceContentStream; +import org.apache.pdfbox.pdmodel.interactive.annotation.layout.PlainText.Line; +import org.apache.pdfbox.pdmodel.interactive.annotation.layout.PlainText.Paragraph; +import org.apache.pdfbox.pdmodel.interactive.annotation.layout.PlainText.TextAttribute; +import org.apache.pdfbox.pdmodel.interactive.annotation.layout.PlainText.Word; + +/** + * TextFormatter to handle plain text formatting for annotation rectangles. + * + * The text formatter will take a single value or an array of values which + * are treated as paragraphs. + */ + +public class PlainTextFormatter +{ + + enum TextAlign + { + LEFT(0), CENTER(1), RIGHT(2), JUSTIFY(4); + + private final int alignment; + + private TextAlign(int alignment) + { + this.alignment = alignment; + } + + int getTextAlign() + { + return alignment; + } + + public static TextAlign valueOf(int alignment) + { + for (TextAlign textAlignment : TextAlign.values()) + { + if (textAlignment.getTextAlign() == alignment) + { + return textAlignment; + } + } + return TextAlign.LEFT; + } + } + + /** + * The scaling factor for font units to PDF units + */ + private static final int FONTSCALE = 1000; + + private final AppearanceStyle appearanceStyle; + private final boolean wrapLines; + private final float width; + + private final PDAppearanceContentStream contents; + private final PlainText textContent; + private final TextAlign textAlignment; + + private float horizontalOffset; + private float verticalOffset; + + public static class Builder + { + + // required parameters + private PDAppearanceContentStream contents; + + // optional parameters + private AppearanceStyle appearanceStyle; + private boolean wrapLines = false; + private float width = 0f; + private PlainText textContent; + private TextAlign textAlignment = TextAlign.LEFT; + + + // initial offset from where to start the position of the first line + private float horizontalOffset = 0f; + private float verticalOffset = 0f; + + public Builder(PDAppearanceContentStream contents) + { + this.contents = contents; + } + + public Builder style(AppearanceStyle appearanceStyle) + { + this.appearanceStyle = appearanceStyle; + return this; + } + + public Builder wrapLines(boolean wrapLines) + { + this.wrapLines = wrapLines; + return this; + } + + public Builder width(float width) + { + this.width = width; + return this; + } + + public Builder textAlign(int alignment) + { + this.textAlignment = TextAlign.valueOf(alignment); + return this; + } + + public Builder textAlign(TextAlign alignment) + { + this.textAlignment = alignment; + return this; + } + + + public Builder text(PlainText textContent) + { + this.textContent = textContent; + return this; + } + + public Builder initialOffset(float horizontalOffset, float verticalOffset) + { + this.horizontalOffset = horizontalOffset; + this.verticalOffset = verticalOffset; + return this; + } + + public PlainTextFormatter build() + { + return new PlainTextFormatter(this); + } + } + + private PlainTextFormatter(Builder builder) + { + appearanceStyle = builder.appearanceStyle; + wrapLines = builder.wrapLines; + width = builder.width; + contents = builder.contents; + textContent = builder.textContent; + textAlignment = builder.textAlignment; + horizontalOffset = builder.horizontalOffset; + verticalOffset = builder.verticalOffset; + } + + /** + * Format the text block. + * + * @throws IOException if there is an error writing to the stream. + */ + public void format() throws IOException + { + if (textContent != null && !textContent.getParagraphs().isEmpty()) + { + boolean isFirstParagraph = true; + for (Paragraph paragraph : textContent.getParagraphs()) + { + if (wrapLines) + { + List lines = paragraph.getLines( + appearanceStyle.getFont(), + appearanceStyle.getFontSize(), + width + ); + processLines(lines, isFirstParagraph); + isFirstParagraph = false; + } + else + { + float startOffset = 0f; + + + float lineWidth = appearanceStyle.getFont().getStringWidth(paragraph.getText()) * + appearanceStyle.getFontSize() / FONTSCALE; + + if (lineWidth < width) + { + switch (textAlignment) + { + case CENTER: + startOffset = (width - lineWidth)/2; + break; + case RIGHT: + startOffset = width - lineWidth; + break; + case JUSTIFY: + default: + startOffset = 0f; + } + } + + contents.newLineAtOffset(horizontalOffset + startOffset, verticalOffset); + contents.showText(paragraph.getText()); + } + } + } + } + + /** + * Process lines for output. + * + * Process lines for an individual paragraph and generate the + * commands for the content stream to show the text. + * + * @param lines the lines to process. + * @throws IOException if there is an error writing to the stream. + */ + private void processLines(List lines, boolean isFirstParagraph) throws IOException + { + float wordWidth; + + float lastPos = 0f; + float startOffset = 0f; + float interWordSpacing = 0f; + + for (Line line : lines) + { + switch (textAlignment) + { + case CENTER: + startOffset = (width - line.getWidth())/2; + break; + case RIGHT: + startOffset = width - line.getWidth(); + break; + case JUSTIFY: + if (lines.indexOf(line) != lines.size() -1) + { + interWordSpacing = line.getInterWordSpacing(width); + } + break; + default: + startOffset = 0f; + } + + float offset = -lastPos + startOffset + horizontalOffset; + + if (lines.indexOf(line) == 0 && isFirstParagraph) + { + contents.newLineAtOffset(offset, verticalOffset); + } + else + { + // keep the last position + verticalOffset = verticalOffset - appearanceStyle.getLeading(); + contents.newLineAtOffset(offset, - appearanceStyle.getLeading()); + } + + lastPos += offset; + + List words = line.getWords(); + for (Word word : words) + { + contents.showText(word.getText()); + wordWidth = (Float) word.getAttributes().getIterator().getAttribute(TextAttribute.WIDTH); + if (words.indexOf(word) != words.size() -1) + { + contents.newLineAtOffset(wordWidth + interWordSpacing, 0f); + lastPos = lastPos + wordWidth + interWordSpacing; + } + } + } + horizontalOffset = horizontalOffset - lastPos; + } +}