PageResizer.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.pdf;

import com.itextpdf.kernel.geom.AffineTransform;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.pdf.colorspace.PdfPattern;
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;

/**
 * The PageResizer class provides functionality to resize PDF pages to a specified
 * target page size using various resizing methods. It adjusts page dimensions,
 * content, annotations, and resources accordingly, also supports configuration
 * options for maintaining the aspect ratio during the resize operation.
 */
class PageResizer {
    /**
     * Represents the target page size for the {@code PageResizer} functionality.
     * This variable dictates the dimensions to which a page needs to be resized.
     * The value is immutable and initialised during the construction of the {@code PageResizer}.
     */
    private final PageSize size;
    /**
     * Represents the type of resize operation to be applied to a page.
     * This variable specifies the method by which the page resize is performed.
     */
    private final ResizeType type;

    /**
     * Constructs a new PageResizer instance with the specified page size and resize type.
     *
     * @param size the target page size to which the content should be resized
     * @param type the resizing method to be applied, such as maintaining the aspect ratio
     */
    PageResizer(PageSize size, ResizeType type) {
        this.size = size;
        this.type = type;
    }

    /**
     * Resizes a given PDF page based on the specified dimensions and resize type.
     * Depending on the resize type, the aspect ratio may be maintained during scaling.
     * Updates the page's content, annotations, and resources to reflect the new size.
     *
     * @param page the PDF page to be resized
     */
    public void resize(PdfPage page) {
        Rectangle originalPageSize = page.getMediaBox();
        double horizontalScale = size.getWidth() / originalPageSize.getWidth();
        double verticalScale = size.getHeight() / originalPageSize.getHeight();

        if (ResizeType.MAINTAIN_ASPECT_RATIO == type) {
            double scale = Math.min(horizontalScale, verticalScale);
            horizontalScale = scale;
            verticalScale = scale;
        }

        updateBoxes(page, originalPageSize);

        AffineTransform scalingMatrix = new AffineTransform();
        scalingMatrix.scale(horizontalScale, verticalScale);
        PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamBefore(), page.getResources(), page.getDocument());
        pdfCanvas.concatMatrix(scalingMatrix);
        pdfCanvas.saveState();

        for (PdfName resName : page.getResources().getResourceNames())
        {
            PdfPattern pattern = page.getResources().getPattern(resName);
            if (pattern != null)
            {
                resizePattern(pattern, scalingMatrix);
            }
            PdfFormXObject form = page.getResources().getForm(resName);
            if (form != null) {
                resizeForm(form, scalingMatrix);
            }
        }

        for (PdfAnnotation annot : page.getAnnotations())
        {
            resizeAnnotation(annot, scalingMatrix);
        }
    }

    /**
     * Scales the transformation matrix of the provided PDF pattern using the given scaling matrix.
     *
     * @param pattern the PDF pattern whose transformation matrix is to be scaled
     * @param scalingMatrix the affine transformation matrix to be applied for scaling
     */
    private static void resizePattern(PdfPattern pattern, AffineTransform scalingMatrix) {
        AffineTransform origTrans;
        if (pattern.getMatrix() == null) {
            origTrans = new AffineTransform(scalingMatrix);
        } else {
            origTrans = new AffineTransform(pattern.getMatrix().toDoubleArray());
            AffineTransform newMatrix = new AffineTransform(scalingMatrix);
            newMatrix.concatenate(origTrans);
            origTrans = newMatrix;
        }
        double[] newMatrixArray = new double[6];
        origTrans.getMatrix(newMatrixArray);
        pattern.setMatrix(new PdfArray(newMatrixArray));
    }

    /**
     * Resizes the given PDF form XObject by applying the specified affine transformation matrix.
     * This method adjusts the content of the form XObject based on the scaling matrix to fit
     * the desired dimensions or proportions.
     *
     * @param form the PDF form XObject to be resized
     * @param scalingMatrix the affine transformation matrix representing the scaling to be applied
     */
    private static void resizeForm(PdfFormXObject form, AffineTransform scalingMatrix) {
        //TODO DEVSIX-9439 implement this method
    }

    /**
     * Resizes the given PDF annotation by applying the specified affine transformation.
     * This method adjusts the annotation's properties, such as its bounding box,
     * to reflect the transformation based on the scaling and translation defined
     * in the affine transformation matrix.
     *
     * @param annot the PDF annotation to be resized
     * @param scalingMatrix the affine transformation matrix representing the scaling
     *                      and translation to be applied
     */
    private static void resizeAnnotation(PdfAnnotation annot, AffineTransform scalingMatrix) {
        annot.setRectangle(scalePdfRect(annot.getRectangle(), scalingMatrix.getScaleY(), scalingMatrix.getScaleX()));
    }

    /**
     * Scales the given page box dimensions from the original page size to the new page size.
     *
     * @param originalPageSize the size of the original page
     * @param newPageSize the size of the new page to scale to
     * @param box the rectangular box representing the dimensions to be scaled
     *
     * @return a new Rectangle representing the scaled dimensions of the page box
     */
    private static Rectangle scalePageBox(Rectangle originalPageSize, PageSize newPageSize, Rectangle box) {
        float lfr = originalPageSize.getWidth() / box.getLeft();
        float wfr = originalPageSize.getWidth() / box.getWidth();
        float tfr = originalPageSize.getHeight() / box.getBottom();
        float hfr = originalPageSize.getHeight() / box.getHeight();
        return new Rectangle(newPageSize.getWidth() / lfr , newPageSize.getHeight() / tfr,
                newPageSize.getWidth() / wfr, newPageSize.getHeight() / hfr);
    }

    /**
     * Scales the dimensions of the given PDF rectangle by applying the specified vertical and horizontal scale factors.
     *
     * @param rect the PDF array representing the rectangular dimensions to be scaled
     * @param verticalScale the factor by which the vertical dimensions should be scaled
     * @param horizontalScale the factor by which the horizontal dimensions should be scaled
     *
     * @return the updated PDF array with the scaled dimensions
     */
    private static PdfArray scalePdfRect(PdfArray rect, double verticalScale, double horizontalScale) {
        rect.set(0, new PdfNumber(((PdfNumber)rect.get(0)).doubleValue() * horizontalScale));
        rect.set(1, new PdfNumber(((PdfNumber)rect.get(1)).doubleValue() * verticalScale));
        rect.set(2, new PdfNumber(((PdfNumber)rect.get(2)).doubleValue() * horizontalScale));
        rect.set(3, new PdfNumber(((PdfNumber)rect.get(3)).doubleValue() * verticalScale));
        return rect;
    }

    /**
     * Updates the page boxes (MediaBox, CropBox, TrimBox, BleedBox, ArtBox) of the given PDF page
     * based on the specified original page size and the size associated with the current instance.
     * The page boxes are scaled proportionally to fit the new page size.
     *
     * @param page the PDF page whose boxes are to be updated
     * @param originalPageSize the dimensions of the original page size
     */
    private void updateBoxes(PdfPage page, Rectangle originalPageSize) {
        Rectangle newCP = scalePageBox(originalPageSize, size, page.getCropBox());
        Rectangle newTB = scalePageBox(originalPageSize, size, page.getTrimBox());
        Rectangle newBB =  scalePageBox(originalPageSize, size, page.getBleedBox());
        Rectangle newAB = scalePageBox(originalPageSize, size, page.getArtBox());

        page.setMediaBox(size);

        if (page.getPdfObject().getAsArray(PdfName.CropBox) != null) {
            page.setCropBox(newCP);
        }
        if (page.getPdfObject().getAsArray(PdfName.TrimBox) != null) {
            page.setTrimBox(newTB);
        }
        if (page.getPdfObject().getAsArray(PdfName.BleedBox) != null) {
            page.setBleedBox(newBB);
        }
        if (page.getPdfObject().getAsArray(PdfName.ArtBox) != null) {
            page.setArtBox(newAB);
        }
    }


    /**
     * Enum representing the available types of resizing strategies when modifying the dimensions
     * of a PDF page. These strategies determine how the content is scaled relative to the new size.
     */
    enum ResizeType {
        MAINTAIN_ASPECT_RATIO,
        DEFAULT
    }
}