Java tutorial
/* * * This file is part of the iText (R) project. Copyright (c) 1998-2019 iText Group NV * Authors: Bruno Lowagie, Paulo Soares, et al. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License version 3 * as published by the Free Software Foundation with the addition of the * following permission added to Section 15 as permitted in Section 7(a): * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY * ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT * OF THIRD PARTY RIGHTS * * 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 http://www.gnu.org/licenses or write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA, 02110-1301 USA, or download the license from the following URL: * http://itextpdf.com/terms-of-use/ * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License. * * In accordance with Section 7(b) of the GNU Affero General Public License, * a covered work must retain the producer line in every PDF that is created * or manipulated using iText. * * You can be released from the requirements of the license by purchasing * a commercial license. Buying such a license is mandatory as soon as you * develop commercial activities involving the iText software without * disclosing the source code of your own applications. * These activities include: offering paid services to customers as an ASP, * serving PDFs on the fly in a web application, shipping iText with a closed * source product. * * For more information, please contact iText Software Corp. at this * address: sales@itextpdf.com */ package com.itextpdf.text.pdf.parser.clipper; import java.util.ArrayList; import java.util.Collections; import java.util.List; import com.itextpdf.text.pdf.parser.clipper.Clipper.ClipType; import com.itextpdf.text.pdf.parser.clipper.Clipper.EndType; import com.itextpdf.text.pdf.parser.clipper.Clipper.JoinType; import com.itextpdf.text.pdf.parser.clipper.Clipper.PolyFillType; import com.itextpdf.text.pdf.parser.clipper.Clipper.PolyType; import com.itextpdf.text.pdf.parser.clipper.Point.DoublePoint; import com.itextpdf.text.pdf.parser.clipper.Point.LongPoint; public class ClipperOffset { private static boolean nearZero(double val) { return val > -TOLERANCE && val < TOLERANCE; } private Paths destPolys; private Path srcPoly; private Path destPoly; private final List<DoublePoint> normals; private double delta, inA, sin, cos; private double miterLim, stepsPerRad; private LongPoint lowest; private final PolyNode polyNodes; private final double arcTolerance; private final double miterLimit; private final static double TWO_PI = Math.PI * 2; private final static double DEFAULT_ARC_TOLERANCE = 0.25; private final static double TOLERANCE = 1.0E-20; public ClipperOffset() { this(2, DEFAULT_ARC_TOLERANCE); } public ClipperOffset(double miterLimit) { this(miterLimit, DEFAULT_ARC_TOLERANCE); } public ClipperOffset(double miterLimit, double arcTolerance) { this.miterLimit = miterLimit; this.arcTolerance = arcTolerance; lowest = new LongPoint(); lowest.setX(-1l); polyNodes = new PolyNode(); normals = new ArrayList<DoublePoint>(); } public void addPath(Path path, JoinType joinType, EndType endType) { int highI = path.size() - 1; if (highI < 0) { return; } final PolyNode newNode = new PolyNode(); newNode.setJoinType(joinType); newNode.setEndType(endType); //strip duplicate points from path and also get index to the lowest point ... if (endType == EndType.CLOSED_LINE || endType == EndType.CLOSED_POLYGON) { while (highI > 0 && path.get(0) == path.get(highI)) { highI--; } } newNode.getPolygon().add(path.get(0)); int j = 0, k = 0; for (int i = 1; i <= highI; i++) { if (newNode.getPolygon().get(j) != path.get(i)) { j++; newNode.getPolygon().add(path.get(i)); if (path.get(i).getY() > newNode.getPolygon().get(k).getY() || path.get(i).getY() == newNode.getPolygon().get(k).getY() && path.get(i).getX() < newNode.getPolygon().get(k).getX()) { k = j; } } } if (endType == EndType.CLOSED_POLYGON && j < 2) { return; } polyNodes.addChild(newNode); //if this path's lowest pt is lower than all the others then update m_lowest if (endType != EndType.CLOSED_POLYGON) { return; } if (lowest.getX() < 0) { lowest = new LongPoint(polyNodes.getChildCount() - 1, k); } else { final LongPoint ip = polyNodes.getChilds().get((int) lowest.getX()).getPolygon() .get((int) lowest.getY()); if (newNode.getPolygon().get(k).getY() > ip.getY() || newNode.getPolygon().get(k).getY() == ip.getY() && newNode.getPolygon().get(k).getX() < ip.getX()) { lowest = new LongPoint(polyNodes.getChildCount() - 1, k); } } } public void addPaths(Paths paths, JoinType joinType, EndType endType) { for (final Path p : paths) { addPath(p, joinType, endType); } } public void clear() { polyNodes.getChilds().clear(); lowest.setX(-1l); } private void doMiter(int j, int k, double r) { final double q = delta / r; destPoly.add(new LongPoint( Math.round(srcPoly.get(j).getX() + (normals.get(k).getX() + normals.get(j).getX()) * q), Math.round(srcPoly.get(j).getY() + (normals.get(k).getY() + normals.get(j).getY()) * q))); } private void doOffset(double delta) { destPolys = new Paths(); this.delta = delta; //if Zero offset, just copy any CLOSED polygons to m_p and return ... if (nearZero(delta)) { for (int i = 0; i < polyNodes.getChildCount(); i++) { final PolyNode node = polyNodes.getChilds().get(i); if (node.getEndType() == EndType.CLOSED_POLYGON) { destPolys.add(node.getPolygon()); } } return; } //see offset_triginometry3.svg in the documentation folder ... if (miterLimit > 2) { miterLim = 2 / (miterLimit * miterLimit); } else { miterLim = 0.5; } double y; if (arcTolerance <= 0.0) { y = DEFAULT_ARC_TOLERANCE; } else if (arcTolerance > Math.abs(delta) * DEFAULT_ARC_TOLERANCE) { y = Math.abs(delta) * DEFAULT_ARC_TOLERANCE; } else { y = arcTolerance; } //see offset_triginometry2.svg in the documentation folder ... final double steps = Math.PI / Math.acos(1 - y / Math.abs(delta)); sin = Math.sin(TWO_PI / steps); cos = Math.cos(TWO_PI / steps); stepsPerRad = steps / TWO_PI; if (delta < 0.0) { sin = -sin; } for (int i = 0; i < polyNodes.getChildCount(); i++) { final PolyNode node = polyNodes.getChilds().get(i); srcPoly = node.getPolygon(); final int len = srcPoly.size(); if (len == 0 || (delta <= 0 && (len < 3 || node.getEndType() != EndType.CLOSED_POLYGON))) { continue; } destPoly = new Path(); if (len == 1) { if (node.getJoinType() == JoinType.ROUND) { double X = 1.0, Y = 0.0; for (int j = 1; j <= steps; j++) { destPoly.add(new LongPoint(Math.round(srcPoly.get(0).getX() + X * delta), Math.round(srcPoly.get(0).getY() + Y * delta))); final double X2 = X; X = X * cos - sin * Y; Y = X2 * sin + Y * cos; } } else { double X = -1.0, Y = -1.0; for (int j = 0; j < 4; ++j) { destPoly.add(new LongPoint(Math.round(srcPoly.get(0).getX() + X * delta), Math.round(srcPoly.get(0).getY() + Y * delta))); if (X < 0) { X = 1; } else if (Y < 0) { Y = 1; } else { X = -1; } } } destPolys.add(destPoly); continue; } //build m_normals ... normals.clear(); for (int j = 0; j < len - 1; j++) { normals.add(Point.getUnitNormal(srcPoly.get(j), srcPoly.get(j + 1))); } if (node.getEndType() == EndType.CLOSED_LINE || node.getEndType() == EndType.CLOSED_POLYGON) { normals.add(Point.getUnitNormal(srcPoly.get(len - 1), srcPoly.get(0))); } else { normals.add(new DoublePoint(normals.get(len - 2))); } if (node.getEndType() == EndType.CLOSED_POLYGON) { final int[] k = new int[] { len - 1 }; for (int j = 0; j < len; j++) { offsetPoint(j, k, node.getJoinType()); } destPolys.add(destPoly); } else if (node.getEndType() == EndType.CLOSED_LINE) { final int[] k = new int[] { len - 1 }; for (int j = 0; j < len; j++) { offsetPoint(j, k, node.getJoinType()); } destPolys.add(destPoly); destPoly = new Path(); //re-build m_normals ... final DoublePoint n = normals.get(len - 1); for (int j = len - 1; j > 0; j--) { normals.set(j, new DoublePoint(-normals.get(j - 1).getX(), -normals.get(j - 1).getY())); } normals.set(0, new DoublePoint(-n.getX(), -n.getY(), 0)); k[0] = 0; for (int j = len - 1; j >= 0; j--) { offsetPoint(j, k, node.getJoinType()); } destPolys.add(destPoly); } else { final int[] k = new int[1]; for (int j = 1; j < len - 1; ++j) { offsetPoint(j, k, node.getJoinType()); } LongPoint pt1; if (node.getEndType() == EndType.OPEN_BUTT) { final int j = len - 1; pt1 = new LongPoint(Math.round(srcPoly.get(j).getX() + normals.get(j).getX() * delta), Math.round(srcPoly.get(j).getY() + normals.get(j).getY() * delta), 0); destPoly.add(pt1); pt1 = new LongPoint(Math.round(srcPoly.get(j).getX() - normals.get(j).getX() * delta), Math.round(srcPoly.get(j).getY() - normals.get(j).getY() * delta), 0); destPoly.add(pt1); } else { final int j = len - 1; k[0] = len - 2; inA = 0; normals.set(j, new DoublePoint(-normals.get(j).getX(), -normals.get(j).getY())); if (node.getEndType() == EndType.OPEN_SQUARE) { doSquare(j, k[0], true); } else { doRound(j, k[0]); } } //re-build m_normals ... for (int j = len - 1; j > 0; j--) { normals.set(j, new DoublePoint(-normals.get(j - 1).getX(), -normals.get(j - 1).getY())); } normals.set(0, new DoublePoint(-normals.get(1).getX(), -normals.get(1).getY())); k[0] = len - 1; for (int j = k[0] - 1; j > 0; --j) { offsetPoint(j, k, node.getJoinType()); } if (node.getEndType() == EndType.OPEN_BUTT) { pt1 = new LongPoint(Math.round(srcPoly.get(0).getX() - normals.get(0).getX() * delta), Math.round(srcPoly.get(0).getY() - normals.get(0).getY() * delta)); destPoly.add(pt1); pt1 = new LongPoint(Math.round(srcPoly.get(0).getX() + normals.get(0).getX() * delta), Math.round(srcPoly.get(0).getY() + normals.get(0).getY() * delta)); destPoly.add(pt1); } else { k[0] = 1; inA = 0; if (node.getEndType() == EndType.OPEN_SQUARE) { doSquare(0, 1, true); } else { doRound(0, 1); } } destPolys.add(destPoly); } } } private void doRound(int j, int k) { final double a = Math.atan2(inA, normals.get(k).getX() * normals.get(j).getX() + normals.get(k).getY() * normals.get(j).getY()); final int steps = Math.max((int) Math.round(stepsPerRad * Math.abs(a)), 1); double X = normals.get(k).getX(), Y = normals.get(k).getY(), X2; for (int i = 0; i < steps; ++i) { destPoly.add(new LongPoint(Math.round(srcPoly.get(j).getX() + X * delta), Math.round(srcPoly.get(j).getY() + Y * delta))); X2 = X; X = X * cos - sin * Y; Y = X2 * sin + Y * cos; } destPoly.add(new LongPoint(Math.round(srcPoly.get(j).getX() + normals.get(j).getX() * delta), Math.round(srcPoly.get(j).getY() + normals.get(j).getY() * delta))); } private void doSquare(int j, int k, boolean addExtra) { final double nkx = normals.get(k).getX(); final double nky = normals.get(k).getY(); final double njx = normals.get(j).getX(); final double njy = normals.get(j).getY(); final double sjx = srcPoly.get(j).getX(); final double sjy = srcPoly.get(j).getY(); final double dx = Math.tan(Math.atan2(inA, nkx * njx + nky * njy) / 4); destPoly.add(new LongPoint(Math.round(sjx + delta * (nkx - (addExtra ? nky * dx : 0))), Math.round(sjy + delta * (nky + (addExtra ? nkx * dx : 0))), 0)); destPoly.add(new LongPoint(Math.round(sjx + delta * (njx + (addExtra ? njy * dx : 0))), Math.round(sjy + delta * (njy - (addExtra ? njx * dx : 0))), 0)); } //------------------------------------------------------------------------------ public void execute(Paths solution, double delta) { solution.clear(); fixOrientations(); doOffset(delta); //now clean up 'corners' ... final DefaultClipper clpr = new DefaultClipper(Clipper.REVERSE_SOLUTION); clpr.addPaths(destPolys, PolyType.SUBJECT, true); if (delta > 0) { clpr.execute(ClipType.UNION, solution, PolyFillType.POSITIVE, PolyFillType.POSITIVE); } else { final LongRect r = destPolys.getBounds(); final Path outer = new Path(4); outer.add(new LongPoint(r.left - 10, r.bottom + 10, 0)); outer.add(new LongPoint(r.right + 10, r.bottom + 10, 0)); outer.add(new LongPoint(r.right + 10, r.top - 10, 0)); outer.add(new LongPoint(r.left - 10, r.top - 10, 0)); clpr.addPath(outer, PolyType.SUBJECT, true); clpr.execute(ClipType.UNION, solution, PolyFillType.NEGATIVE, PolyFillType.NEGATIVE); if (solution.size() > 0) { solution.remove(0); } } } //------------------------------------------------------------------------------ public void execute(PolyTree solution, double delta) { solution.Clear(); fixOrientations(); doOffset(delta); //now clean up 'corners' ... final DefaultClipper clpr = new DefaultClipper(Clipper.REVERSE_SOLUTION); clpr.addPaths(destPolys, PolyType.SUBJECT, true); if (delta > 0) { clpr.execute(ClipType.UNION, solution, PolyFillType.POSITIVE, PolyFillType.POSITIVE); } else { final LongRect r = destPolys.getBounds(); final Path outer = new Path(4); outer.add(new LongPoint(r.left - 10, r.bottom + 10, 0)); outer.add(new LongPoint(r.right + 10, r.bottom + 10, 0)); outer.add(new LongPoint(r.right + 10, r.top - 10, 0)); outer.add(new LongPoint(r.left - 10, r.top - 10, 0)); clpr.addPath(outer, PolyType.SUBJECT, true); clpr.execute(ClipType.UNION, solution, PolyFillType.NEGATIVE, PolyFillType.NEGATIVE); //remove the outer PolyNode rectangle ... if (solution.getChildCount() == 1 && solution.getChilds().get(0).getChildCount() > 0) { final PolyNode outerNode = solution.getChilds().get(0); solution.getChilds().set(0, outerNode.getChilds().get(0)); solution.getChilds().get(0).setParent(solution); for (int i = 1; i < outerNode.getChildCount(); i++) { solution.addChild(outerNode.getChilds().get(i)); } } else { solution.Clear(); } } } //------------------------------------------------------------------------------ private void fixOrientations() { //fixup orientations of all closed paths if the orientation of the //closed path with the lowermost vertex is wrong ... if (lowest.getX() >= 0 && !polyNodes.childs.get((int) lowest.getX()).getPolygon().orientation()) { for (int i = 0; i < polyNodes.getChildCount(); i++) { final PolyNode node = polyNodes.childs.get(i); if (node.getEndType() == EndType.CLOSED_POLYGON || node.getEndType() == EndType.CLOSED_LINE && node.getPolygon().orientation()) { Collections.reverse(node.getPolygon()); } } } else { for (int i = 0; i < polyNodes.getChildCount(); i++) { final PolyNode node = polyNodes.childs.get(i); if (node.getEndType() == EndType.CLOSED_LINE && !node.getPolygon().orientation()) { Collections.reverse(node.getPolygon()); } } } } private void offsetPoint(int j, int[] kV, JoinType jointype) { //cross product ... final int k = kV[0]; final double nkx = normals.get(k).getX(); final double nky = normals.get(k).getY(); final double njy = normals.get(j).getY(); final double njx = normals.get(j).getX(); final long sjx = srcPoly.get(j).getX(); final long sjy = srcPoly.get(j).getY(); inA = nkx * njy - njx * nky; if (Math.abs(inA * delta) < 1.0) { //dot product ... final double cosA = nkx * njx + njy * nky; if (cosA > 0) // angle ==> 0 degrees { destPoly.add(new LongPoint(Math.round(sjx + nkx * delta), Math.round(sjy + nky * delta), 0)); return; } //else angle ==> 180 degrees } else if (inA > 1.0) { inA = 1.0; } else if (inA < -1.0) { inA = -1.0; } if (inA * delta < 0) { destPoly.add(new LongPoint(Math.round(sjx + nkx * delta), Math.round(sjy + nky * delta))); destPoly.add(srcPoly.get(j)); destPoly.add(new LongPoint(Math.round(sjx + njx * delta), Math.round(sjy + njy * delta))); } else { switch (jointype) { case MITER: { final double r = 1 + njx * nkx + njy * nky; if (r >= miterLim) { doMiter(j, k, r); } else { doSquare(j, k, false); } break; } case BEVEL: doSquare(j, k, false); break; case ROUND: doRound(j, k); break; } } kV[0] = j; } //------------------------------------------------------------------------------ }