Rectangle.java

/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2025 Apryse Group NV
    Authors: Apryse Software.

    This program is offered under a commercial and under the AGPL license.
    For commercial licensing, contact us at https://itextpdf.com/sales.  For AGPL licensing, see below.

    AGPL licensing:
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    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 <https://www.gnu.org/licenses/>.
 */
package com.itextpdf.kernel.geom;

import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfPage;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * Class that represent rectangle object.
 */
public class Rectangle implements Cloneable {

    static float EPS = 1e-4f;


    protected float x;
    protected float y;
    protected float width;
    protected float height;

    /**
     * Creates new instance.
     *
     * @param x      the x coordinate of lower left point
     * @param y      the y coordinate of lower left point
     * @param width  the width value
     * @param height the height value
     */
    public Rectangle(float x, float y, float width, float height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }

    /**
     * Creates new instance of rectangle with (0, 0) as the lower left point.
     *
     * @param width  the width value
     * @param height the height value
     */
    public Rectangle(float width, float height) {
        this(0, 0, width, height);
    }

    /**
     * Creates the copy of given {@link Rectangle}
     *
     * @param rect the copied {@link Rectangle}
     */
    public Rectangle(Rectangle rect) {
        this(rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight());
    }

    /**
     * Calculates the common rectangle which includes all the input rectangles.
     *
     * @param rectangles list of input rectangles.
     * @return common rectangle.
     */
    public static Rectangle getCommonRectangle(Rectangle... rectangles) {
        float ury = -Float.MAX_VALUE;
        float llx = Float.MAX_VALUE;
        float lly = Float.MAX_VALUE;
        float urx = -Float.MAX_VALUE;
        for (Rectangle rectangle : rectangles) {
            if (rectangle == null)
                continue;
            Rectangle rec = rectangle.clone();
            if (rec.getY() < lly)
                lly = rec.getY();
            if (rec.getX() < llx)
                llx = rec.getX();
            if (rec.getY() + rec.getHeight() > ury)
                ury = rec.getY() + rec.getHeight();
            if (rec.getX() + rec.getWidth() > urx)
                urx = rec.getX() + rec.getWidth();
        }

        return new Rectangle(llx, lly, urx - llx, ury - lly);
    }

    /**
     * Transforms a rectangle defined in the space of unrotated origin (bottom-left)
     * into coordinates as it would appear on a passed as parameter rotated page.
     *
     * <p>
     * This method is useful when adding annotations, form fields, or other elements
     * to a PDF page that has a rotation. The iText coordinate system always
     * uses the bottom-left corner as origin, regardless of page rotation.
     * This method compensates for that rotation, returning a rectangle positioned
     * correctly in the true page coordinate space.
     *
     * @param rect the rectangle defined in page-space coordinates relative to the unrotated origin.
     * @param page the {@link PdfPage} to which the rectangle will be added.
     *             The rotation of this page is used to transform the coordinates.
     *
     * @return a new {@link Rectangle} with corrected coordinates suitable for placement
     *         in the rotated coordinate space of the page.
     */
    public static Rectangle getRectangleOnRotatedPage(Rectangle rect, PdfPage page) {
        Rectangle rectangleOnRotatedPage = rect;
        int rotation = page.getRotation();

        if (rotation != 0) {
            Rectangle pageSize = page.getPageSize();
            float x = rect.getLeft();
            float y = rect.getBottom();
            float width = rect.getWidth();
            float height = rect.getHeight();

            switch ((rotation / 90) % 4) {
                case 1: // 90 degrees
                    rectangleOnRotatedPage = new Rectangle(pageSize.getWidth() - y - height, x, height, width);
                    break;
                case 2: // 180 degrees
                    rectangleOnRotatedPage = new Rectangle(pageSize.getWidth() - x - width, pageSize.getHeight() - y - height, width, height);
                    break;
                case 3: // 270 degrees
                    rectangleOnRotatedPage = new Rectangle(y, pageSize.getHeight() - x - width, height, width);
                    break;
                default:
                    break;
            }
        }
        return rectangleOnRotatedPage;
    }

    /**
     * Calculates the bounding box of passed points.
     *
     * @param points the points which appear inside the area
     *
     * @return the bounding box of passed points.
     */
    public static Rectangle calculateBBox(List<Point> points) {
        List<Double> xs = new ArrayList<>();
        List<Double> ys = new ArrayList<>();
        for (Point point : points) {
            xs.add(point.getX());
            ys.add(point.getY());
        }

        double left = Collections.min(xs);
        double bottom = Collections.min(ys);
        double right = Collections.max(xs);
        double top = Collections.max(ys);

        return new Rectangle((float) left, (float) bottom, (float) (right - left), (float) (top - bottom));
    }

    /**
     * Convert rectangle to an array of points
     *
     * @return array of four extreme points of rectangle
     */
    public Point[] toPointsArray() {
        return new Point[] {new Point(x, y), new Point(x + width, y),
                new Point(x + width, y + height), new Point(x, y + height)};
    }

    /**
     * Get the rectangle representation of the intersection between this rectangle and the passed rectangle
     *
     * @param rect the rectangle to find the intersection with
     * @return the intersection rectangle if the passed rectangles intersects with this rectangle,
     * a rectangle representing a line if the intersection is along an edge or
     * a rectangle representing a point if the intersection is a single point,
     * null otherwise
     */
    public Rectangle getIntersection(Rectangle rect) {
        Rectangle result = null;

        //Calculate possible lower-left corner and upper-right corner
        float llx = Math.max(x, rect.x);
        float lly = Math.max(y, rect.y);
        float urx = Math.min(getRight(), rect.getRight());
        float ury = Math.min(getTop(), rect.getTop());

        //If width or height is non-negative, there is overlap and we can construct the intersection rectangle
        float width = urx - llx;
        if (Math.abs(width) < EPS) {
            width = 0;
        }

        float height = ury - lly;
        if (Math.abs(height) < EPS) {
            height = 0;
        }

        if (Float.compare(width, 0) >= 0
                && Float.compare(height, 0) >= 0) {
            if (Float.compare(width, 0) < 0) width = 0;
            if (Float.compare(height, 0) < 0) height = 0;
            result = new Rectangle(llx, lly, width, height);
        }

        return result;
    }

    /**
     * Check if this rectangle contains the passed rectangle.
     * A rectangle will envelop itself, meaning that for any rectangle {@code rect}
     * the expression {@code rect.contains(rect)} always returns true.
     *
     * @param rect a rectangle which is to be checked if it is fully contained inside this rectangle.
     * @return true if this rectangle contains the passed rectangle, false otherwise.
     */
    public boolean contains(Rectangle rect) {
        float llx = this.getX();
        float lly = this.getY();
        float urx = llx + this.getWidth();
        float ury = lly + this.getHeight();

        float rllx = rect.getX();
        float rlly = rect.getY();
        float rurx = rllx + rect.getWidth();
        float rury = rlly + rect.getHeight();

        return llx - EPS <= rllx && lly - EPS <= rlly
                && rurx <= urx + EPS && rury <= ury + EPS;
    }

    /**
     * Check if this rectangle and the passed rectangle overlap
     *
     * @param rect a rectangle which is to be checked if it overlaps the passed rectangle
     * @return true if there is overlap of some kind
     */
    public boolean overlaps(final Rectangle rect) {
        return overlaps(rect, -EPS);
    }

    /**
     * Check if this rectangle and the passed rectangle overlap
     *
     * @param rect a rectangle which is to be checked if it overlaps the passed rectangle
     * @param epsilon if greater than zero, then this is the maximum distance that one rectangle can go to another, but
     *               they will not overlap, if less than zero, then this is the minimum required distance between the
     *                rectangles so that they do not overlap
     * @return true if there is overlap of some kind
     */
    public boolean overlaps(final Rectangle rect, final float epsilon) {
        // Two rectangles do not overlap if any of the following holds

        // The first rectangle lies to the left of the second rectangle or touches very slightly
        if ((this.getX() + this.getWidth()) < (rect.getX() + epsilon)) {
            return false;
        }
        // The first rectangle lies to the right of the second rectangle or touches very slightly
        if ((this.getX() + epsilon) > (rect.getX() + rect.getWidth())) {
            return false;
        }

        // The first rectangle lies to the bottom of the second rectangle or touches very slightly
        if ((this.getY() + this.getHeight()) < (rect.getY() + epsilon)) {
            return false;
        }
        // The first rectangle lies to the top of the second rectangle or touches very slightly
        if ((this.getY() + epsilon) > (rect.getY() + rect.getHeight())) {
            return false;
        }

        return true;
    }

    /**
     * Sets the rectangle by the coordinates, specifying its lower left and upper right points. May be used in chain.
     * <br>
     * <br>
     * Note: this method will normalize coordinates, so the rectangle will have non negative width and height,
     * and its x and y coordinates specified lower left point.
     *
     * @param llx the X coordinate of lower left point
     * @param lly the Y coordinate of lower left point
     * @param urx the X coordinate of upper right point
     * @param ury the Y coordinate of upper right point
     * @return this {@link Rectangle} instance.
     */
    public Rectangle setBbox(float llx, float lly, float urx, float ury) {
        // If llx is greater than urx, swap them (normalize)
        if (llx > urx) {
            float temp = llx;
            llx = urx;
            urx = temp;
        }
        // If lly is greater than ury, swap them (normalize)
        if (lly > ury) {
            float temp = lly;
            lly = ury;
            ury = temp;
        }
        x = llx;
        y = lly;
        width = urx - llx;
        height = ury - lly;
        return this;
    }

    /**
     * Gets the X coordinate of lower left point.
     *
     * @return the X coordinate of lower left point.
     */
    public float getX() {
        return x;
    }

    /**
     * Sets the X coordinate of lower left point. May be used in chain.
     *
     * @param x the X coordinate of lower left point to be set.
     * @return this {@link Rectangle} instance.
     */
    public Rectangle setX(float x) {
        this.x = x;
        return this;
    }

    /**
     * Gets the Y coordinate of lower left point.
     *
     * @return the Y coordinate of lower left point.
     */
    public float getY() {
        return y;
    }

    /**
     * Sets the Y coordinate of lower left point. May be used in chain.
     *
     * @param y the Y coordinate of lower left point to be set.
     * @return this {@link Rectangle} instance.
     */
    public Rectangle setY(float y) {
        this.y = y;
        return this;
    }

    /**
     * Gets the width of rectangle.
     *
     * @return the width of rectangle.
     */
    public float getWidth() {
        return width;
    }

    /**
     * Sets the width of rectangle. May be used in chain.
     *
     * @param width the the width of rectangle to be set.
     * @return this {@link Rectangle} instance.
     */
    public Rectangle setWidth(float width) {
        this.width = width;
        return this;
    }

    /**
     * Gets the height of rectangle.
     *
     * @return the height of rectangle.
     */
    public float getHeight() {
        return height;
    }

    /**
     * Sets the height of rectangle. May be used in chain.
     *
     * @param height the the width of rectangle to be set.
     * @return this {@link Rectangle} instance.
     */
    public Rectangle setHeight(float height) {
        this.height = height;
        return this;
    }

    /**
     * Increases the height of rectangle by the given value. May be used in chain.
     *
     * @param extra the value of the extra height to be added.
     * @return this {@link Rectangle} instance.
     */
    public Rectangle increaseHeight(float extra) {
        this.height += extra;
        return this;
    }

    /**
     * Decreases the height of rectangle by the given value. May be used in chain.
     *
     * @param extra the value of the extra height to be subtracted.
     * @return this {@link Rectangle} instance.
     */
    public Rectangle decreaseHeight(float extra) {
        this.height -= extra;
        return this;
    }

    /**
     * Increases the width of rectangle by the given value. May be used in chain.
     *
     * @param extra the value of the extra wudth to be added.
     * @return this {@link Rectangle} instance.
     */
    public Rectangle increaseWidth(float extra) {
        this.width += extra;
        return this;
    }

    /**
     * Decreases the width of rectangle by the given value. May be used in chain.
     *
     * @param extra the value of the extra width to be subtracted.
     * @return this {@link Rectangle} instance.
     */
    public Rectangle decreaseWidth(float extra) {
        this.width -= extra;
        return this;
    }

    /**
     * Gets the X coordinate of the left edge of the rectangle. Same as: {@code getX()}.
     *
     * @return the X coordinate of the left edge of the rectangle.
     */
    public float getLeft() {
        return x;
    }

    /**
     * Gets the X coordinate of the right edge of the rectangle. Same as: {@code getX() + getWidth()}.
     *
     * @return the X coordinate of the right edge of the rectangle.
     */
    public float getRight() {
        return x + width;
    }

    /**
     * Gets the Y coordinate of the upper edge of the rectangle. Same as: {@code getY() + getHeight()}.
     *
     * @return the Y coordinate of the upper edge of the rectangle.
     */
    public float getTop() {
        return y + height;
    }

    /**
     * Gets the Y coordinate of the lower edge of the rectangle. Same as: {@code getY()}.
     *
     * @return the Y coordinate of the lower edge of the rectangle.
     */
    public float getBottom() {
        return y;
    }

    /**
     * Decreases the y coordinate.
     *
     * @param move the value on which the position will be changed.
     * @return this {@link Rectangle} instance.
     */
    public Rectangle moveDown(float move) {
        y -= move;
        return this;
    }


    /**
     * Increases the y coordinate.
     *
     * @param move the value on which the position will be changed.
     * @return this {@link Rectangle} instance.
     */
    public Rectangle moveUp(float move) {
        y += move;
        return this;
    }

    /**
     * Increases the x coordinate.
     *
     * @param move the value on which the position will be changed.
     * @return this {@link Rectangle} instance.
     */
    public Rectangle moveRight(float move) {
        x += move;
        return this;
    }

    /**
     * Decreases the x coordinate.
     *
     * @param move the value on which the position will be changed.
     * @return this {@link Rectangle} instance.
     */
    public Rectangle moveLeft(float move) {
        x -= move;
        return this;
    }

    /**
     * Change the rectangle according the specified margins.
     *
     * @param topIndent    the value on which the top y coordinate will change.
     * @param rightIndent  the value on which the right x coordinate will change.
     * @param bottomIndent the value on which the bottom y coordinate will change.
     * @param leftIndent the value on which the left x coordinate will change.
     * @param reverse if {@code true} the rectangle will expand, otherwise it will shrink
     * @return the rectangle with applied margins
     */
    public Rectangle applyMargins(float topIndent, float rightIndent, float bottomIndent, float leftIndent, boolean reverse) {
        x += leftIndent * (reverse ? -1 : 1);
        width -= (leftIndent + rightIndent) * (reverse ? -1 : 1);
        y += bottomIndent * (reverse ? -1 : 1);
        height -= (topIndent + bottomIndent) * (reverse ? -1 : 1);
        return this;
    }

    /**
     * Checks if rectangle have common points with line, specified by two points.
     *
     * @param x1 the x coordinate of first line's point.
     * @param y1 the y coordinate of first line's point.
     * @param x2 the x coordinate of second line's point.
     * @param y2 the y coordinate of second line's point.
     * @return {@code true} if rectangle have common points with line and {@code false} otherwise.
     */
    public boolean intersectsLine(float x1, float y1, float x2, float y2) {
        double rx1 = getX();
        double ry1 = getY();
        double rx2 = rx1 + getWidth();
        double ry2 = ry1 + getHeight();
        return
                (rx1 <= x1 && x1 <= rx2 && ry1 <= y1 && y1 <= ry2) ||
                        (rx1 <= x2 && x2 <= rx2 && ry1 <= y2 && y2 <= ry2) ||
                        linesIntersect(rx1, ry1, rx2, ry2, x1, y1, x2, y2) ||
                        linesIntersect(rx2, ry1, rx1, ry2, x1, y1, x2, y2);
    }

    /**
     * Gets the string representation of rectangle.
     *
     * @return the string representation of rectangle.
     */
    @Override
    public String toString() {
        return "Rectangle: " + getWidth() +
                'x' +
                getHeight();
    }

    /**
     * Creates a "deep copy" of this rectangle, meaning the object returned by this method will be independent
     * of the object being cloned.
     *
     * @return the copied rectangle.
     */
    @Override
    public Rectangle clone() {
        try {
            // super.clone is safe to return since all of the Rectangle's fields are primitive.
            return (Rectangle) super.clone();
        } catch (CloneNotSupportedException e) {
            // should never happen since Cloneable is implemented
            return null;
        }
    }

    /**
     * Compares instance of this rectangle with given deviation equals to 0.0001
     *
     * @param that the {@link Rectangle} to compare with.
     * @return {@code true} if the difference between corresponding rectangle values is less than deviation and {@code false} otherwise.
     */
    public boolean equalsWithEpsilon(Rectangle that) {
        return equalsWithEpsilon(that, EPS);
    }

    /**
     * Compares instance of this rectangle with given deviation.
     *
     * @param that the {@link Rectangle} to compare with.
     * @param eps  the deviation value.
     * @return {@code true} if the difference between corresponding rectangle values is less than deviation and {@code false} otherwise.
     */
    public boolean equalsWithEpsilon(Rectangle that, float eps) {
        float dx = Math.abs(x - that.x);
        float dy = Math.abs(y - that.y);
        float dw = Math.abs(width - that.width);
        float dh = Math.abs(height - that.height);
        return dx < eps && dy < eps && dw < eps && dh < eps;
    }

    private static boolean linesIntersect(double x1, double y1, double x2,
                                          double y2, double x3, double y3, double x4, double y4) {
        /*
         * A = (x2-x1, y2-y1) B = (x3-x1, y3-y1) C = (x4-x1, y4-y1) D = (x4-x3,
         * y4-y3) = C-B E = (x1-x3, y1-y3) = -B F = (x2-x3, y2-y3) = A-B
         *
         * Result is ((AxB) * (AxC) <=0) and ((DxE) * (DxF) <= 0)
         *
         * DxE = (C-B)x(-B) = BxB-CxB = BxC DxF = (C-B)x(A-B) = CxA-CxB-BxA+BxB =
         * AxB+BxC-AxC
         */

        // A
        x2 -= x1;
        y2 -= y1;
        // B
        x3 -= x1;
        y3 -= y1;
        // C
        x4 -= x1;
        y4 -= y1;

        double AvB = x2 * y3 - x3 * y2;
        double AvC = x2 * y4 - x4 * y2;

        // Online
        if (AvB == 0.0 && AvC == 0.0) {
            if (x2 != 0.0) {
                return
                        (x4 * x3 <= 0.0) ||
                                ((x3 * x2 >= 0.0) &&
                                        (x2 > 0.0 ? x3 <= x2 || x4 <= x2 : x3 >= x2 || x4 >= x2));
            }
            if (y2 != 0.0) {
                return
                        (y4 * y3 <= 0.0) ||
                                ((y3 * y2 >= 0.0) &&
                                        (y2 > 0.0 ? y3 <= y2 || y4 <= y2 : y3 >= y2 || y4 >= y2));
            }
            return false;
        }

        double BvC = x3 * y4 - x4 * y3;

        return (AvB * AvC <= 0.0) && (BvC * (AvB + BvC - AvC) <= 0.0);
    }

    /**
     * Create a list of bounding rectangles from an 8 x n array of Quadpoints.
     * @param quadPoints 8xn array of numbers representing 4 points
     * @return a list of bounding rectangles for the passed quadpoints
     * @throws PdfException if the passed array's size is not a multiple of 8.
     */
    public static List<Rectangle> createBoundingRectanglesFromQuadPoint(PdfArray quadPoints) throws PdfException {
        List<Rectangle> boundingRectangles = new ArrayList<>();
        if (quadPoints.size() % 8 != 0) {
            throw new PdfException(KernelExceptionMessageConstant.QUAD_POINT_ARRAY_LENGTH_IS_NOT_A_MULTIPLE_OF_EIGHT);
        }
        for (int i = 0; i < quadPoints.size(); i += 8) {
            float[] quadPointEntry = Arrays.copyOfRange(quadPoints.toFloatArray(),i,i+8);
            PdfArray quadPointEntryFA = new PdfArray(quadPointEntry);
            boundingRectangles.add(createBoundingRectangleFromQuadPoint(quadPointEntryFA));
        }
        return boundingRectangles;
    }

    /**
     * Create the bounding rectangle for the given array of quadpoints.
     * @param quadPoints an array containing 8 numbers that correspond to 4 points.
     * @return The smallest orthogonal rectangle containing the quadpoints.
     * @throws PdfException if the passed array's size is not a multiple of 8.
     */
    public static Rectangle createBoundingRectangleFromQuadPoint(PdfArray quadPoints) throws PdfException {

        //Check if array length is a multiple of 8
        if (quadPoints.size() % 8 != 0) {
            throw new PdfException(KernelExceptionMessageConstant.QUAD_POINT_ARRAY_LENGTH_IS_NOT_A_MULTIPLE_OF_EIGHT);
        }
        float llx = Float.MAX_VALUE;
        float lly = Float.MAX_VALUE;
        float urx = -Float.MAX_VALUE;
        float ury = -Float.MAX_VALUE;

        // QuadPoints in redact annotations have "Z" order, in spec they're specified
        for (int j = 0; j < 8; j += 2) {
            float x = quadPoints.getAsNumber(j).floatValue();
            float y = quadPoints.getAsNumber(j + 1).floatValue();

            if (x < llx) llx = x;
            if (x > urx) urx = x;
            if (y < lly) lly = y;
            if (y > ury) ury = y;
        }
        return (new Rectangle(llx,
                lly,
                urx - llx,
                ury - lly));
    }


}