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.kernel.pdf.canvas.parser.clipper.ClipperOffset Maven / Gradle / Ivy
/*
* This class is based on the C# open source freeware library Clipper:
* http://www.angusj.com/delphi/clipper.php
* The original classes were distributed under the Boost Software License:
*
* Freeware for both open source and commercial applications
* Copyright 2010-2014 Angus Johnson
* Boost Software License - Version 1.0 - August 17th, 2003
*
* Permission is hereby granted, free of charge, to any person or organization
* obtaining a copy of the software and accompanying documentation covered by
* this license (the "Software") to use, reproduce, display, distribute,
* execute, and transmit the Software, and to prepare derivative works of the
* Software, and to permit third-parties to whom the Software is furnished to
* do so, all subject to the following:
*
* The copyright notices in the Software and this entire statement, including
* the above license grant, this restriction and the following disclaimer,
* must be included in all copies of the Software, in whole or in part, and
* all derivative works of the Software, unless such copies or derivative
* works are solely in the form of machine-executable object code generated by
* a source language processor.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
* SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
* FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package com.itextpdf.kernel.pdf.canvas.parser.clipper;
import com.itextpdf.kernel.pdf.canvas.parser.clipper.IClipper.*;
import com.itextpdf.kernel.pdf.canvas.parser.clipper.Point.DoublePoint;
import com.itextpdf.kernel.pdf.canvas.parser.clipper.Point.LongPoint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
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 static final double TWO_PI = Math.PI * 2;
private static final double DEFAULT_ARC_TOLERANCE = 0.25;
private static final 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 ).equals(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 ).equals(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( IClipper.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( IClipper.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;
}
//------------------------------------------------------------------------------
}