Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.itextpdf.text.pdf.parser.clipper.ClipperOffset Maven / Gradle / Ivy
/*
* $Id: 7b36d61246e86841ef634042728f5d854aad3b3a $
*
* This file is part of the iText (R) project.
* Copyright (c) 1998-2016 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: [email protected]
*/
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 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();
}
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;
}
//------------------------------------------------------------------------------
}