com.itextpdf.text.pdf.parser.clipper.ClipperOffset.java Source code

Java tutorial

Introduction

Here is the source code for com.itextpdf.text.pdf.parser.clipper.ClipperOffset.java

Source

/*
 *
 * 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;
    }
    //------------------------------------------------------------------------------
}