/* * * This file is part of the iText (R) project. Copyright (c) 1998-2017 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; } //------------------------------------------------------------------------------ }