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: *

*/ 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) {