diff --git a/kernel/src/main/java/com/itextpdf/kernel/pdf/canvas/parser/ParserGraphicsState.java b/kernel/src/main/java/com/itextpdf/kernel/pdf/canvas/parser/ParserGraphicsState.java
index 63dca56416..c77a591d9e 100644
--- a/kernel/src/main/java/com/itextpdf/kernel/pdf/canvas/parser/ParserGraphicsState.java
+++ b/kernel/src/main/java/com/itextpdf/kernel/pdf/canvas/parser/ParserGraphicsState.java
@@ -72,6 +72,7 @@ public void updateCtm(Matrix newCtm) {
/**
* Intersects the current clipping path with the given path.
*
+ *
* Note: Coordinates of the given path should be in
* the transformed user space.
*
@@ -89,18 +90,20 @@ public void clip(Path path, int fillingRule) {
pathCopy.closeAllSubpaths();
IClipper clipper = new DefaultClipper();
- ClipperBridge.addPath(clipper, clippingPath, IClipper.PolyType.SUBJECT);
- ClipperBridge.addPath(clipper, pathCopy, IClipper.PolyType.CLIP);
+ ClipperBridge clipperBridge = new ClipperBridge(clippingPath, pathCopy);
+ clipperBridge.addPath(clipper, clippingPath, IClipper.PolyType.SUBJECT);
+ clipperBridge.addPath(clipper, pathCopy, IClipper.PolyType.CLIP);
PolyTree resultTree = new PolyTree();
clipper.execute(IClipper.ClipType.INTERSECTION, resultTree, IClipper.PolyFillType.NON_ZERO, ClipperBridge.getFillType(fillingRule));
- clippingPath = ClipperBridge.convertToPath(resultTree);
+ clippingPath = clipperBridge.convertToPath(resultTree);
}
/**
* Getter for the current clipping path.
*
+ *
* Note: The returned clipping path is in the transformed user space, so
* if you want to get it in default user space, apply transformation matrix ({@link CanvasGraphicsState#getCtm()}).
*
@@ -113,6 +116,7 @@ public Path getClippingPath() {
/**
* Sets the current clipping path to the specified path.
*
+ *
* Note:This method doesn't modify existing clipping path,
* it simply replaces it with the new one instead.
*
diff --git a/kernel/src/main/java/com/itextpdf/kernel/pdf/canvas/parser/clipper/ClipperBridge.java b/kernel/src/main/java/com/itextpdf/kernel/pdf/canvas/parser/clipper/ClipperBridge.java
index 61dd4bd5a2..db8deb6d16 100644
--- a/kernel/src/main/java/com/itextpdf/kernel/pdf/canvas/parser/clipper/ClipperBridge.java
+++ b/kernel/src/main/java/com/itextpdf/kernel/pdf/canvas/parser/clipper/ClipperBridge.java
@@ -30,8 +30,9 @@ This file is part of the iText (R) project.
import java.util.List;
/**
- * This class contains variety of methods allowing to convert iText
- * abstractions into the abstractions of the Clipper library and vise versa.
+ * This class contains a variety of methods allowing the conversion of iText
+ * abstractions into abstractions of the Clipper library, and vice versa.
+ *
*
* For example:
*
@@ -41,17 +42,78 @@ This file is part of the iText (R) project.
*
*/
public final class ClipperBridge {
+ private static final long MAX_ALLOWED_VALUE = 0x3FFFFFFFFFFFFFL;
/**
* Since the clipper library uses integer coordinates, we should convert
* our floating point numbers into fixed point numbers by multiplying by
* this coefficient. Vary it to adjust the preciseness of the calculations.
+ *
+ *
+ * Note that if this value is specified, it will be used for all ClipperBridge instances and
+ * dynamic float multiplier calculation will be disabled.
+ *
*/
- //TODO DEVSIX-5770 make this constant a single non-static configuration
- public static double floatMultiplier = Math.pow(10, 14);
+ public static Double floatMultiplier;
- private ClipperBridge() {
- //empty constructor
+ private double approximatedFloatMultiplier = Math.pow(10, 14);
+
+ /**
+ * Creates new {@link ClipperBridge} instance with default float multiplier value which is 10^14.
+ *
+ *
+ * Since the clipper library uses integer coordinates, we should convert our floating point numbers into fixed
+ * point numbers by multiplying by float multiplier coefficient. It is possible to vary it to adjust the preciseness
+ * of the calculations: if static {@link #floatMultiplier} is specified, it will be used for all ClipperBridge
+ * instances and default value will be ignored.
+ */
+ public ClipperBridge() {
+ // Empty constructor.
+ }
+
+ /**
+ * Creates new {@link ClipperBridge} instance with adjusted float multiplier value. This instance will work
+ * correctly with the provided paths only.
+ *
+ *
+ * Since the clipper library uses integer coordinates, we should convert our floating point numbers into fixed
+ * point numbers by multiplying by float multiplier coefficient. It is calculated automatically, however
+ * it is possible to vary it to adjust the preciseness of the calculations: if static {@link #floatMultiplier} is
+ * specified, it will be used for all ClipperBridge instances and automatic calculation won't work.
+ *
+ * @param paths paths to calculate multiplier coefficient to convert floating point numbers into fixed point numbers
+ */
+ public ClipperBridge(com.itextpdf.kernel.geom.Path... paths) {
+ if (floatMultiplier == null) {
+ List pointsList = new ArrayList<>();
+ for (com.itextpdf.kernel.geom.Path path : paths) {
+ for (Subpath subpath : path.getSubpaths()) {
+ if (!subpath.isSinglePointClosed() && !subpath.isSinglePointOpen()) {
+ pointsList.addAll(subpath.getPiecewiseLinearApproximation());
+ }
+ }
+ }
+ calculateFloatMultiplier(pointsList.toArray(new com.itextpdf.kernel.geom.Point[0]));
+ }
+ }
+
+ /**
+ * Creates new {@link ClipperBridge} instance with adjusted float multiplier value. This instance will work
+ * correctly with the provided point only.
+ *
+ *
+ * Since the clipper library uses integer coordinates, we should convert our floating point numbers into fixed
+ * point numbers by multiplying by float multiplier coefficient. It is calculated automatically, however
+ * it is possible to vary it to adjust the preciseness of the calculations: if static {@link #floatMultiplier} is
+ * specified, it will be used for all ClipperBridge instances and automatic calculation won't work.
+ *
+ * @param points points to calculate multiplier coefficient to convert floating point numbers
+ * into fixed point numbers
+ */
+ public ClipperBridge(com.itextpdf.kernel.geom.Point[]... points) {
+ if (floatMultiplier == null) {
+ calculateFloatMultiplier(points);
+ }
}
/**
@@ -61,7 +123,7 @@ private ClipperBridge() {
* @param result {@link PolyTree} object to convert
* @return resultant {@link com.itextpdf.kernel.geom.Path} object
*/
- public static com.itextpdf.kernel.geom.Path convertToPath(PolyTree result) {
+ public com.itextpdf.kernel.geom.Path convertToPath(PolyTree result) {
com.itextpdf.kernel.geom.Path path = new com.itextpdf.kernel.geom.Path();
PolyNode node = result.getFirst();
@@ -79,7 +141,7 @@ public static com.itextpdf.kernel.geom.Path convertToPath(PolyTree result) {
* @param path The {@link com.itextpdf.kernel.geom.Path} object to be added to the {@link IClipper}.
* @param polyType See {@link IClipper.PolyType}.
*/
- public static void addPath(IClipper clipper, com.itextpdf.kernel.geom.Path path, IClipper.PolyType polyType) {
+ public void addPath(IClipper clipper, com.itextpdf.kernel.geom.Path path, IClipper.PolyType polyType) {
for (Subpath subpath : path.getSubpaths()) {
if (!subpath.isSinglePointClosed() && !subpath.isSinglePointOpen()) {
List linearApproxPoints = subpath.getPiecewiseLinearApproximation();
@@ -101,7 +163,8 @@ public static void addPath(IClipper clipper, com.itextpdf.kernel.geom.Path path,
* {@link IClipper.EndType#OPEN_ROUND}
* @return {@link java.util.List} consisting of all degenerate iText {@link Subpath}s of the path.
*/
- public static List addPath(ClipperOffset offset, com.itextpdf.kernel.geom.Path path, IClipper.JoinType joinType, IClipper.EndType endType) {
+ public List addPath(ClipperOffset offset, com.itextpdf.kernel.geom.Path path, IClipper.JoinType joinType,
+ IClipper.EndType endType) {
List degenerateSubpaths = new ArrayList<>();
for (Subpath subpath : path.getSubpaths()) {
@@ -135,13 +198,13 @@ public static List addPath(ClipperOffset offset, com.itextpdf.kernel.ge
* @param points the list of {@link Point.LongPoint} objects to convert
* @return the resultant list of {@link com.itextpdf.kernel.geom.Point} objects.
*/
- public static List convertToFloatPoints(List points) {
+ public List convertToFloatPoints(List points) {
List convertedPoints = new ArrayList<>(points.size());
for (Point.LongPoint point : points) {
convertedPoints.add(new com.itextpdf.kernel.geom.Point(
- point.getX() / floatMultiplier,
- point.getY() / floatMultiplier
+ point.getX() / getFloatMultiplier(),
+ point.getY() / getFloatMultiplier()
));
}
@@ -155,13 +218,13 @@ public static List convertToFloatPoints(List convertToLongPoints(List points) {
+ public List convertToLongPoints(List points) {
List convertedPoints = new ArrayList<>(points.size());
for (com.itextpdf.kernel.geom.Point point : points) {
convertedPoints.add(new Point.LongPoint(
- floatMultiplier * point.getX(),
- floatMultiplier * point.getY()
+ getFloatMultiplier() * point.getX(),
+ getFloatMultiplier() * point.getY()
));
}
@@ -238,7 +301,8 @@ public static IClipper.PolyFillType getFillType(int fillingRule) {
* path is a subject of clipping or a part of the clipping polygon.
* @return true if polygon path was successfully added, false otherwise.
*/
- public static boolean addPolygonToClipper(IClipper clipper, com.itextpdf.kernel.geom.Point[] polyVertices, IClipper.PolyType polyType) {
+ public boolean addPolygonToClipper(IClipper clipper, com.itextpdf.kernel.geom.Point[] polyVertices,
+ IClipper.PolyType polyType) {
return clipper.addPath(new Path(convertToLongPoints(new ArrayList<>(Arrays.asList(polyVertices)))), polyType, true);
}
@@ -257,7 +321,7 @@ public static boolean addPolygonToClipper(IClipper clipper, com.itextpdf.kernel.
* to clipper path and added to the clipper instance.
* @return true if polyline path was successfully added, false otherwise.
*/
- public static boolean addPolylineSubjectToClipper(IClipper clipper, com.itextpdf.kernel.geom.Point[] lineVertices) {
+ public boolean addPolylineSubjectToClipper(IClipper clipper, com.itextpdf.kernel.geom.Point[] lineVertices) {
return clipper.addPath(new Path(convertToLongPoints(new ArrayList<>(Arrays.asList(lineVertices)))), IClipper.PolyType.SUBJECT, false);
}
@@ -267,8 +331,8 @@ public static boolean addPolylineSubjectToClipper(IClipper clipper, com.itextpdf
*
* @return the width of the rectangle.
*/
- public static float longRectCalculateWidth(LongRect rect) {
- return (float) (Math.abs(rect.left - rect.right) / ClipperBridge.floatMultiplier);
+ public float longRectCalculateWidth(LongRect rect) {
+ return (float) (Math.abs(rect.left - rect.right) / getFloatMultiplier());
}
/**
@@ -277,11 +341,23 @@ public static float longRectCalculateWidth(LongRect rect) {
*
* @return the height of the rectangle.
*/
- public static float longRectCalculateHeight(LongRect rect) {
- return (float) (Math.abs(rect.top - rect.bottom) / ClipperBridge.floatMultiplier);
+ public float longRectCalculateHeight(LongRect rect) {
+ return (float) (Math.abs(rect.top - rect.bottom) / getFloatMultiplier());
+ }
+
+ /**
+ * Gets multiplier coefficient for converting our floating point numbers into fixed point numbers.
+ *
+ * @return multiplier coefficient for converting our floating point numbers into fixed point numbers
+ */
+ public double getFloatMultiplier() {
+ if (floatMultiplier == null) {
+ return approximatedFloatMultiplier;
+ }
+ return (double) floatMultiplier;
}
- static void addContour(com.itextpdf.kernel.geom.Path path, List contour, boolean close) {
+ void addContour(com.itextpdf.kernel.geom.Path path, List contour, boolean close) {
List floatContour = convertToFloatPoints(contour);
com.itextpdf.kernel.geom.Point point = floatContour.get(0);
path.moveTo((float) point.getX(), (float) point.getY());
@@ -295,4 +371,19 @@ static void addContour(com.itextpdf.kernel.geom.Path path, List
path.closeSubpath();
}
}
+
+ private void calculateFloatMultiplier(com.itextpdf.kernel.geom.Point[]... points) {
+ double maxPoint = 0;
+ for (com.itextpdf.kernel.geom.Point[] pointsArray : points) {
+ for (com.itextpdf.kernel.geom.Point point : pointsArray) {
+ maxPoint = Math.max(maxPoint, Math.abs(point.getX()));
+ maxPoint = Math.max(maxPoint, Math.abs(point.getY()));
+ }
+ }
+ // The significand of the double type is approximately 15 to 17 decimal digits for most platforms.
+ double epsilon = 1E-16;
+ if (maxPoint > epsilon) {
+ this.approximatedFloatMultiplier = Math.floor(MAX_ALLOWED_VALUE / maxPoint);
+ }
+ }
}
diff --git a/kernel/src/test/java/com/itextpdf/kernel/pdf/canvas/parser/PdfContentExtractionTest.java b/kernel/src/test/java/com/itextpdf/kernel/pdf/canvas/parser/PdfContentExtractionTest.java
index 40fdac3077..1b3ee9796b 100644
--- a/kernel/src/test/java/com/itextpdf/kernel/pdf/canvas/parser/PdfContentExtractionTest.java
+++ b/kernel/src/test/java/com/itextpdf/kernel/pdf/canvas/parser/PdfContentExtractionTest.java
@@ -24,36 +24,52 @@ This file is part of the iText (R) project.
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
+import com.itextpdf.kernel.pdf.canvas.parser.clipper.ClipperBridge;
import com.itextpdf.kernel.pdf.canvas.parser.clipper.ClipperException;
import com.itextpdf.kernel.pdf.canvas.parser.clipper.ClipperExceptionConstant;
import com.itextpdf.kernel.pdf.canvas.parser.listener.LocationTextExtractionStrategy;
+import com.itextpdf.test.AssertUtil;
import com.itextpdf.test.ExtendedITextTest;
-
-import java.io.IOException;
import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
@Tag("IntegrationTest")
public class PdfContentExtractionTest extends ExtendedITextTest {
-
- private static final String sourceFolder = "./src/test/resources/com/itextpdf/kernel/parser/PdfContentExtractionTest/";
+
+ private static final String SOURCE_FOLDER =
+ "./src/test/resources/com/itextpdf/kernel/parser/PdfContentExtractionTest/";
@Test
- //TODO: remove the expected exception construct once the issue is fixed (DEVSIX-1279)
public void contentExtractionInDocWithBigCoordinatesTest() throws IOException {
- String inputFileName = sourceFolder + "docWithBigCoordinates.pdf";
- //In this document the CTM shrinks coordinates and this coordinates are large numbers.
+ String inputFileName = SOURCE_FOLDER + "docWithBigCoordinates.pdf";
+ // In this document the CTM shrinks coordinates and these coordinates are large numbers.
// At the moment creation of this test clipper has a problem with handling large numbers
// since internally it deals with integers and has to multiply large numbers even more
// for internal purposes
+ try (PdfDocument pdfDocument = new PdfDocument(new PdfReader(inputFileName))) {
+ PdfDocumentContentParser contentParser = new PdfDocumentContentParser(pdfDocument);
+ AssertUtil.doesNotThrow(() -> contentParser.processContent(1, new LocationTextExtractionStrategy()));
+ }
+ }
- PdfDocument pdfDocument = new PdfDocument(new PdfReader(inputFileName));
- PdfDocumentContentParser contentParser = new PdfDocumentContentParser(pdfDocument);
-
- Exception e = Assertions.assertThrows(ClipperException.class,
- () -> contentParser.processContent(1, new LocationTextExtractionStrategy())
- );
- Assertions.assertEquals(ClipperExceptionConstant.COORDINATE_OUTSIDE_ALLOWED_RANGE, e.getMessage());
+ @Test
+ public void contentExtractionInDocWithStaticFloatMultiplierTest() throws IOException {
+ String inputFileName = SOURCE_FOLDER + "docWithBigCoordinates.pdf";
+ // In this document the CTM shrinks coordinates and these coordinates are large numbers.
+ // At the moment creation of this test clipper has a problem with handling large numbers
+ // since internally it deals with integers and has to multiply large numbers even more
+ // for internal purposes
+ try (PdfDocument pdfDocument = new PdfDocument(new PdfReader(inputFileName))) {
+ PdfDocumentContentParser contentParser = new PdfDocumentContentParser(pdfDocument);
+ ClipperBridge.floatMultiplier = Math.pow(10, 14);
+ Exception e = Assertions.assertThrows(ClipperException.class,
+ () -> contentParser.processContent(1, new LocationTextExtractionStrategy())
+ );
+ Assertions.assertEquals(ClipperExceptionConstant.COORDINATE_OUTSIDE_ALLOWED_RANGE, e.getMessage());
+ ClipperBridge.floatMultiplier = null;
+ }
}
}
diff --git a/kernel/src/test/java/com/itextpdf/kernel/pdf/canvas/parser/clipper/ClipperBridgeTest.java b/kernel/src/test/java/com/itextpdf/kernel/pdf/canvas/parser/clipper/ClipperBridgeTest.java
index 2d36a35c8e..14938726bb 100644
--- a/kernel/src/test/java/com/itextpdf/kernel/pdf/canvas/parser/clipper/ClipperBridgeTest.java
+++ b/kernel/src/test/java/com/itextpdf/kernel/pdf/canvas/parser/clipper/ClipperBridgeTest.java
@@ -25,6 +25,7 @@ This file is part of the iText (R) project.
import com.itextpdf.kernel.geom.IShape;
import com.itextpdf.kernel.geom.Line;
import com.itextpdf.kernel.geom.Path;
+import com.itextpdf.kernel.geom.Point;
import com.itextpdf.kernel.geom.Subpath;
import com.itextpdf.kernel.pdf.canvas.PdfCanvasConstants.LineCapStyle;
import com.itextpdf.kernel.pdf.canvas.PdfCanvasConstants.LineJoinStyle;
@@ -34,6 +35,7 @@ This file is part of the iText (R) project.
import com.itextpdf.kernel.pdf.canvas.parser.clipper.IClipper.PolyType;
import com.itextpdf.test.ExtendedITextTest;
+import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Assertions;
@@ -64,12 +66,13 @@ public void squareClippingTest() {
rectanglePath.addSubpath(rectangleSubpath);
DefaultClipper clipper = new DefaultClipper();
- ClipperBridge.addPath(clipper, squarePath, PolyType.SUBJECT);
- ClipperBridge.addPath(clipper, rectanglePath, PolyType.CLIP);
+ ClipperBridge clipperBridge = new ClipperBridge(squarePath, rectanglePath);
+ clipperBridge.addPath(clipper, squarePath, PolyType.SUBJECT);
+ clipperBridge.addPath(clipper, rectanglePath, PolyType.CLIP);
PolyTree polyTree = new PolyTree();
clipper.execute(ClipType.UNION, polyTree);
- Path result = ClipperBridge.convertToPath(polyTree);
+ Path result = clipperBridge.convertToPath(polyTree);
Assertions.assertEquals(new com.itextpdf.kernel.geom.Point(20, 40), result.getCurrentPoint());
Assertions.assertEquals(2, result.getSubpaths().size());
@@ -108,14 +111,40 @@ public void getEndTypeTest() {
@Test
public void longRectWidthTest() {
LongRect longRect = new LongRect(14900000000000000L, 21275000000000000L, 71065802001953128L, 71075000000000000L);
- Assertions.assertEquals(561.658, ClipperBridge.longRectCalculateWidth(longRect), 0.001f);
+ Assertions.assertEquals(561.658, new ClipperBridge().longRectCalculateWidth(longRect), 0.001f);
}
@Test
public void longRectHeightTest() {
LongRect longRect = new LongRect(14900000000000000L, 21275000000000000L, 71065802001953128L, 71075000000000000L);
- Assertions.assertEquals(498, ClipperBridge.longRectCalculateHeight(longRect), 0.001f);
+ Assertions.assertEquals(498, new ClipperBridge().longRectCalculateHeight(longRect), 0.001f);
+ }
+
+ @Test
+ public void dynamicFloatMultiplierCalculationsSmallValuesTest() {
+ Point[] points = new Point[]{
+ new Point(1e-10, 0),
+ new Point(0, 1e-13)
+ };
+ Assertions.assertEquals(1.8014398509481984e26, new ClipperBridge(points).getFloatMultiplier(), 0e+10);
+ }
+
+ @Test
+ public void dynamicFloatMultiplierCalculationsBigValuesTest() {
+ Point[] points = new Point[]{
+ new Point(1e+11, 10),
+ new Point(10, 1e+10)
+ };
+ Assertions.assertEquals(180143, new ClipperBridge(points).getFloatMultiplier(), 0.001f);
+ }
+
+ @Test
+ public void smallFloatMultiplierCoefficientTest() {
+ Point[] points = new Point[]{new Point(1e-10, 1e+10)};
+ Assertions.assertEquals(
+ new com.itextpdf.kernel.pdf.canvas.parser.clipper.Point.LongPoint(0, 18014390000000000L),
+ new ClipperBridge(points).convertToLongPoints(Arrays.asList(points)).get(0));
}
private boolean areShapesEqual(IShape expected, IShape actual) {