ClipperBridge.java
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2025 Apryse Group NV
Authors: Apryse Software.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itextpdf.kernel.pdf.canvas.parser.clipper;
import com.itextpdf.kernel.geom.Subpath;
import com.itextpdf.kernel.pdf.canvas.PdfCanvasConstants;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* This class contains a variety of methods allowing the conversion of iText
* abstractions into abstractions of the Clipper library, and vice versa.
*
* <p>
* For example:
* <ul>
* <li>{@link PolyTree} to {@link com.itextpdf.kernel.geom.Path}
* <li>{@link com.itextpdf.kernel.geom.Point} to {@link Point.LongPoint}
* <li>{@link Point.LongPoint} to {@link com.itextpdf.kernel.geom.Point}
* </ul>
*/
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.
*
* <p>
* Note that if this value is specified, it will be used for all ClipperBridge instances and
* dynamic float multiplier calculation will be disabled.
*
*/
public static Double floatMultiplier;
private double approximatedFloatMultiplier = Math.pow(10, 14);
/**
* Creates new {@link ClipperBridge} instance with default float multiplier value which is 10^14.
*
* <p>
* 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.
*
* <p>
* 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<com.itextpdf.kernel.geom.Point> 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.
*
* <p>
* 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);
}
}
/**
* Converts Clipper library {@link PolyTree} abstraction into iText
* {@link com.itextpdf.kernel.geom.Path} object.
*
* @param result {@link PolyTree} object to convert
* @return resultant {@link com.itextpdf.kernel.geom.Path} object
*/
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();
while (node != null) {
addContour(path, node.getContour(), !node.isOpen());
node = node.getNext();
}
return path;
}
/**
* Adds iText {@link com.itextpdf.kernel.geom.Path} to the given {@link IClipper} object.
* @param clipper The {@link IClipper} object.
* @param path The {@link com.itextpdf.kernel.geom.Path} object to be added to the {@link IClipper}.
* @param polyType See {@link IClipper.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<com.itextpdf.kernel.geom.Point> linearApproxPoints = subpath.getPiecewiseLinearApproximation();
clipper.addPath(new Path(convertToLongPoints(linearApproxPoints)), polyType, subpath.isClosed());
}
}
}
/**
* Adds all iText {@link Subpath}s of the iText {@link com.itextpdf.kernel.geom.Path} to the {@link ClipperOffset} object with one
* note: it doesn't add degenerate subpaths.
*
* @param offset the {@link ClipperOffset} object to add all iText {@link Subpath}s that are not degenerated.
* @param path {@link com.itextpdf.kernel.geom.Path} object, containing the required {@link Subpath}s
* @param joinType {@link IClipper} join type. The value could be {@link IClipper.JoinType#BEVEL}, {@link IClipper.JoinType#ROUND},
* {@link IClipper.JoinType#MITER}
* @param endType {@link IClipper} end type. The value could be {@link IClipper.EndType#CLOSED_POLYGON},
* {@link IClipper.EndType#CLOSED_LINE}, {@link IClipper.EndType#OPEN_BUTT}, {@link IClipper.EndType#OPEN_SQUARE},
* {@link IClipper.EndType#OPEN_ROUND}
* @return {@link java.util.List} consisting of all degenerate iText {@link Subpath}s of the path.
*/
public List<Subpath> addPath(ClipperOffset offset, com.itextpdf.kernel.geom.Path path, IClipper.JoinType joinType,
IClipper.EndType endType) {
List<Subpath> degenerateSubpaths = new ArrayList<>();
for (Subpath subpath : path.getSubpaths()) {
if (subpath.isDegenerate()) {
degenerateSubpaths.add(subpath);
continue;
}
if (!subpath.isSinglePointClosed() && !subpath.isSinglePointOpen()) {
IClipper.EndType et;
if (subpath.isClosed()) {
// Offsetting is never used for path being filled
et = IClipper.EndType.CLOSED_LINE;
} else {
et = endType;
}
List<com.itextpdf.kernel.geom.Point> linearApproxPoints = subpath.getPiecewiseLinearApproximation();
offset.addPath(new Path(convertToLongPoints(linearApproxPoints)), joinType, et);
}
}
return degenerateSubpaths;
}
/**
* Converts list of {@link Point.LongPoint} objects into list of
* {@link com.itextpdf.kernel.geom.Point} objects.
*
* @param points the list of {@link Point.LongPoint} objects to convert
* @return the resultant list of {@link com.itextpdf.kernel.geom.Point} objects.
*/
public List<com.itextpdf.kernel.geom.Point> convertToFloatPoints(List<Point.LongPoint> points) {
List<com.itextpdf.kernel.geom.Point> convertedPoints = new ArrayList<>(points.size());
for (Point.LongPoint point : points) {
convertedPoints.add(new com.itextpdf.kernel.geom.Point(
point.getX() / getFloatMultiplier(),
point.getY() / getFloatMultiplier()
));
}
return convertedPoints;
}
/**
* Converts list of {@link com.itextpdf.kernel.geom.Point} objects into list of
* {@link Point.LongPoint} objects.
*
* @param points the list of {@link com.itextpdf.kernel.geom.Point} objects to convert
* @return the resultant list of {@link Point.LongPoint} objects.
*/
public List<Point.LongPoint> convertToLongPoints(List<com.itextpdf.kernel.geom.Point> points) {
List<Point.LongPoint> convertedPoints = new ArrayList<>(points.size());
for (com.itextpdf.kernel.geom.Point point : points) {
convertedPoints.add(new Point.LongPoint(
getFloatMultiplier() * point.getX(),
getFloatMultiplier() * point.getY()
));
}
return convertedPoints;
}
/**
* Converts iText line join style constant into the corresponding constant
* of the Clipper library.
* @param lineJoinStyle iText line join style constant. See {@link PdfCanvasConstants}
* @return Clipper line join style constant.
*/
public static IClipper.JoinType getJoinType(int lineJoinStyle) {
switch (lineJoinStyle) {
case PdfCanvasConstants.LineJoinStyle.BEVEL:
return IClipper.JoinType.BEVEL;
case PdfCanvasConstants.LineJoinStyle.MITER:
return IClipper.JoinType.MITER;
}
return IClipper.JoinType.ROUND;
}
/**
* Converts iText line cap style constant into the corresponding constant
* of the Clipper library.
* @param lineCapStyle iText line cap style constant. See {@link PdfCanvasConstants}
* @return Clipper line cap (end type) style constant.
*/
public static IClipper.EndType getEndType(int lineCapStyle) {
switch (lineCapStyle) {
case PdfCanvasConstants.LineCapStyle.BUTT:
return IClipper.EndType.OPEN_BUTT;
case PdfCanvasConstants.LineCapStyle.PROJECTING_SQUARE:
return IClipper.EndType.OPEN_SQUARE;
}
return IClipper.EndType.OPEN_ROUND;
}
/**
* Converts iText filling rule constant into the corresponding constant
* of the Clipper library.
* @param fillingRule Either {@link com.itextpdf.kernel.pdf.canvas.PdfCanvasConstants.FillingRule#NONZERO_WINDING} or
* {@link com.itextpdf.kernel.pdf.canvas.PdfCanvasConstants.FillingRule#EVEN_ODD}.
* @return Clipper fill type constant.
*/
public static IClipper.PolyFillType getFillType(int fillingRule) {
IClipper.PolyFillType fillType = IClipper.PolyFillType.NON_ZERO;
if (fillingRule == PdfCanvasConstants.FillingRule.EVEN_ODD) {
fillType = IClipper.PolyFillType.EVEN_ODD;
}
return fillType;
}
/**
* Adds polygon path based on array of {@link com.itextpdf.kernel.geom.Point} (internally converting
* them by {@link #convertToLongPoints}) and adds this path to {@link IClipper} instance, treating the path as
* a closed polygon.
* <p>
* The return value will be false if the path is invalid for clipping. A path is invalid for clipping when:
* <ul>
* <li>it has less than 3 vertices;
* <li>the vertices are all co-linear.
* </ul>
* @param clipper {@link IClipper} instance to which the created polygon path will be added.
* @param polyVertices an array of {@link com.itextpdf.kernel.geom.Point} which will be internally converted
* to clipper path and added to the clipper instance.
* @param polyType either {@link IClipper.PolyType#SUBJECT} or {@link IClipper.PolyType#CLIP} denoting whether added
* path is a subject of clipping or a part of the clipping polygon.
* @return true if polygon path was successfully added, false otherwise.
*/
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);
}
/**
* Adds polyline path based on array of {@link com.itextpdf.kernel.geom.Point} (internally converting
* them by {@link #convertToLongPoints}) and adds this path to {@link IClipper} instance, treating the path as
* a polyline (an open path in terms of clipper library). This path is added to the subject of future clipping.
* Polylines cannot be part of clipping polygon.
* <p>
* The return value will be false if the path is invalid for clipping. A path is invalid for clipping when:
* <ul>
* <li>it has less than 2 vertices;
* </ul>
* @param clipper {@link IClipper} instance to which the created polyline path will be added.
* @param lineVertices an array of {@link com.itextpdf.kernel.geom.Point} which will be internally converted
* to clipper path and added to the clipper instance.
* @return true if polyline path was successfully added, false otherwise.
*/
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);
}
/**
* Calculates the width of the rectangle represented by the {@link LongRect} object.
* @param rect the {@link LongRect} object representing the rectangle.
*
* @return the width of the rectangle.
*/
public float longRectCalculateWidth(LongRect rect) {
return (float) (Math.abs(rect.left - rect.right) / getFloatMultiplier());
}
/**
* Calculates the height of the rectangle represented by the {@link LongRect} object.
* @param rect the {@link LongRect} object representing the rectangle.
*
* @return the height of the rectangle.
*/
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;
}
void addContour(com.itextpdf.kernel.geom.Path path, List<Point.LongPoint> contour, boolean close) {
List<com.itextpdf.kernel.geom.Point> floatContour = convertToFloatPoints(contour);
com.itextpdf.kernel.geom.Point point = floatContour.get(0);
path.moveTo((float) point.getX(), (float) point.getY());
for (int i = 1; i < floatContour.size(); i++) {
point = floatContour.get(i);
path.lineTo((float) point.getX(), (float) point.getY());
}
if (close) {
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);
}
}
}