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.commons.utils.MessageFormatUtil;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
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 java.util.ArrayList;
import java.util.Arrays;
/**
* 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 {
private static final float EPSILON = 1e-6f;
private final PageSize size;
private VerticalAnchorPoint verticalAnchorPoint = VerticalAnchorPoint.CENTER;
private HorizontalAnchorPoint horizontalAnchorPoint = HorizontalAnchorPoint.CENTER;
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;
}
/**
* Retrieves the horizontal anchor point of the PageResizer.
*
* @return the horizontal anchor point, which determines the horizontal alignment (e.g., LEFT, CENTER, RIGHT).
*/
public HorizontalAnchorPoint getHorizontalAnchorPoint() {
return horizontalAnchorPoint;
}
/**
* Sets the horizontal anchor point, which determines how the horizontal alignment is handled
* (e.g., LEFT, CENTER, RIGHT).
*
* @param anchorPoint the horizontal anchor point to set; it specifies the horizontal alignment type
* for resizing operations
*/
public void setHorizontalAnchorPoint(HorizontalAnchorPoint anchorPoint) {
this.horizontalAnchorPoint = anchorPoint;
}
/**
* Retrieves the vertical anchor point of the PageResizer.
*
* @return the vertical anchor point, which determines the vertical alignment (e.g., TOP, CENTER, BOTTOM).
*/
public VerticalAnchorPoint getVerticalAnchorPoint() {
return verticalAnchorPoint;
}
/**
* Sets the vertical anchor point, which determines how the vertical alignment is handled
* (e.g., TOP, CENTER, BOTTOM).
*
* @param anchorPoint the vertical anchor point to set; it specifies the vertical alignment type
* for resizing operations
*/
public void setVerticalAnchorPoint(VerticalAnchorPoint anchorPoint) {
this.verticalAnchorPoint = anchorPoint;
}
/**
* 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) {
if (size == null || size.getWidth() < EPSILON || size.getHeight() < EPSILON) {
throw new IllegalArgumentException(MessageFormatUtil.format(KernelExceptionMessageConstant
.CANNOT_RESIZE_PAGE_WITH_NEGATIVE_OR_INFINITE_SCALE, size));
}
if (page == null) {
return;
}
Rectangle originalPageSize = page.getMediaBox();
double horizontalScale = size.getWidth() / originalPageSize.getWidth();
double verticalScale = size.getHeight() / originalPageSize.getHeight();
double horizontalFreeSpace = 0;
double verticalFreeSpace = 0;
if (ResizeType.MAINTAIN_ASPECT_RATIO == type) {
double scale = Math.min(horizontalScale, verticalScale);
horizontalScale = scale;
verticalScale = scale;
horizontalFreeSpace = size.getWidth() - originalPageSize.getWidth()*scale;
verticalFreeSpace = size.getHeight() - originalPageSize.getHeight()*scale;
}
updateBoxes(page, originalPageSize);
AffineTransform scalingMatrix = calculateAffineTransform(horizontalScale, verticalScale,
horizontalFreeSpace, verticalFreeSpace);
// Ensure resources exist to avoid NPEs when creating PdfCanvas or iterating resources
PdfResources resources = page.getResources();
if (resources == null) {
resources = new PdfResources();
page.getPdfObject().put(PdfName.Resources, resources.getPdfObject());
}
PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamBefore(), page.getResources(), page.getDocument());
pdfCanvas.concatMatrix(scalingMatrix);
for (PdfName resName : page.getResources().getResourceNames()) {
PdfPattern pattern = page.getResources().getPattern(resName);
if (pattern != null) {
resizePattern(page.getResources(), resName, scalingMatrix);
}
}
for (PdfAnnotation annot : page.getAnnotations()) {
resizeAnnotation(annot, scalingMatrix);
}
}
static String scaleDaString(String daString, double scale) {
if (daString == null || daString.trim().isEmpty()) {
return daString;
}
// Optimization for identity scaling. Use an epsilon for robust float comparison.
if (Math.abs(scale - 1.0) < 1e-9) {
return daString;
}
java.util.List<String> tokens = new ArrayList<>(Arrays.asList(daString.trim().split("\\s+")));
// Operators we care about in DA:
// Tf => operands: /FontName <fontSize> (scale the <fontSize> only)
// TL => operand: <leading> (scale)
// Tc => operand: <charSpacing> (scale)
// Tw => operand: <wordSpacing> (scale)
// Ts => operand: <textRise> (scale)
//
// Operators we intentionally do NOT scale:
// Tz (horizontal scaling, percentage), Tr (rendering mode),
// color ops (g/rg/k/G/RG/K), etc.
final java.util.Set<String> scalableOperators = new java.util.HashSet<>(java.util.Arrays.asList(
"Tf", "TL", "Tc", "Tw", "Ts"));
for (int i = 0; i < tokens.size(); i++) {
if (scalableOperators.contains(tokens.get(i))) {
int operandIdx = i - 1;
while (operandIdx >= 0) {
String token = tokens.get(operandIdx);
if (token.startsWith("/")) {
// Skip resource names, e.g., /Helv
operandIdx--;
continue;
}
try {
double value = Double.parseDouble(token);
tokens.set(operandIdx, formatNumber(value * scale));
break;
} catch (NumberFormatException ignore) {
// Not a number, continue searching backwards.
operandIdx--;
}
}
}
}
return String.join(" ", tokens);
}
/**
* 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
*/
static Rectangle scalePageBox(Rectangle originalPageSize, PageSize newPageSize, Rectangle box) {
if (originalPageSize == null || newPageSize == null || box == null) {
return box;
}
float origW = originalPageSize.getWidth();
float origH = originalPageSize.getHeight();
float newW = newPageSize.getWidth();
float newH = newPageSize.getHeight();
if (origW < EPSILON || origH < EPSILON) {
return box;
}
float left = box.getLeft() * newW / origW;
float bottom = box.getBottom() * newH / origH;
float width = box.getWidth() * newW / origW;
float height = box.getHeight() * newH / origH;
return new Rectangle(left, bottom, width, height);
}
/**
* Resizes the appearance streams of a given PDF annotation by applying the specified affine transformation matrix.
* This involves scaling the content of the appearance streams in the annotation's
* appearance dictionary and adjusting their transformation matrices to reflect the scaling.
*
* @param annot the PDF annotation whose appearance streams are to be resized
* @param scalingMatrix the affine transformation matrix used to scale the appearance streams
*/
static void resizeAppearanceStreams(PdfAnnotation annot, AffineTransform scalingMatrix) {
PdfDictionary ap = annot.getAppearanceDictionary();
if (ap == null) {
return;
}
for (PdfName key : ap.keySet()) {
PdfObject apState = ap.get(key);
if (apState.isStream()) {
resizeAppearanceStream((PdfStream) apState, scalingMatrix);
} else if (apState.isDictionary()) {
PdfDictionary apStateDict = (PdfDictionary) apState;
for (PdfName subKeyState : apStateDict.keySet()) {
PdfObject subApState = apStateDict.get(subKeyState);
if (subApState.isStream()) {
resizeAppearanceStream((PdfStream) subApState, scalingMatrix);
}
}
}
}
}
/**
* Scales the transformation matrix of the provided pattern using the given scaling matrix,
* without mutating the original pattern.
*
* The method:
* - Locates the pattern object in the provided resources by name.
* - Deep-copies the pattern object into the same document.
* - Updates the /Matrix of the copied pattern.
* - Replaces the entry in the page's /Pattern resources with the copied (resized) pattern,
* so other pages that reference the original pattern remain unaffected.
*
* @param resources the resource dictionary that holds the pattern
* @param resName the name of the pattern resource to resize
* @param scalingMatrix the affine transformation matrix to be applied for scaling
*/
private static void resizePattern(PdfResources resources, PdfName resName, AffineTransform scalingMatrix) {
if (resources == null || resName == null || scalingMatrix == null) {
return;
}
PdfDictionary patternDictContainer = resources.getResource(PdfName.Pattern);
if (patternDictContainer == null) {
return;
}
PdfObject patternObj = resources.getResourceObject(PdfName.Pattern, resName);
if (patternObj == null) {
return;
}
PdfObject clonedObj = (PdfObject) patternObj.clone();
PdfDictionary clonedPatternDict = (PdfDictionary) clonedObj;
PdfArray existingMatrix = clonedPatternDict.getAsArray(PdfName.Matrix);
AffineTransform newTransform;
if (existingMatrix == null) {
newTransform = new AffineTransform(scalingMatrix);
} else {
newTransform = new AffineTransform(existingMatrix.toDoubleArray());
newTransform.preConcatenate(scalingMatrix);
}
double[] newMatrixArray = new double[6];
newTransform.getMatrix(newMatrixArray);
clonedPatternDict.put(PdfName.Matrix, new PdfArray(newMatrixArray));
patternDictContainer.put(resName, clonedPatternDict);
}
/**
* 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) {
// Transform all geometric coordinate-based properties of the annotation.
PdfArray rectArray = annot.getRectangle();
if (rectArray != null) {
double[] rectPoints = {rectArray.getAsNumber(0).doubleValue(), rectArray.getAsNumber(1).doubleValue(),
rectArray.getAsNumber(2).doubleValue(), rectArray.getAsNumber(3).doubleValue()};
// Transform ll
scalingMatrix.transform(rectPoints, 0, rectPoints, 0, 1);
// Transform ur
scalingMatrix.transform(rectPoints, 2, rectPoints, 2, 1);
annot.setRectangle(new PdfArray(rectPoints));
}
PdfDictionary annotDict = annot.getPdfObject();
transformCoordinateArray(annotDict.getAsArray(PdfName.L), scalingMatrix);
transformCoordinateArray(annotDict.getAsArray(PdfName.Vertices), scalingMatrix);
transformCoordinateArray(annotDict.getAsArray(PdfName.QuadPoints), scalingMatrix);
transformCoordinateArray(annotDict.getAsArray(PdfName.CL), scalingMatrix);
PdfArray inkList = annotDict.getAsArray(PdfName.InkList);
if (inkList != null) {
for (int i = 0; i < inkList.size(); i++) {
transformCoordinateArray(inkList.getAsArray(i), scalingMatrix);
}
}
// Scale all scalar properties of the annotation, such as border widths and font sizes.
scaleAnnotationScalarProperties(annot, scalingMatrix.getScaleX(), scalingMatrix.getScaleY());
// Resize the appearance streams, which define the annotation's visual representation.
resizeAppearanceStreams(annot, scalingMatrix);
}
/**
* Scales an array of coordinates (x1, y1, x2, y2, ...) by applying horizontal and vertical scale factors.
*
* @param coordinateArray the array of coordinates to scale
* @param transform the transformation to be applied
*/
private static void transformCoordinateArray(PdfArray coordinateArray, AffineTransform transform) {
if (coordinateArray == null) {
return;
}
// Only transform complete pairs.
if (coordinateArray.size() % 2 != 0) {
return;
}
double[] points = new double[coordinateArray.size()];
for (int i = 0; i < coordinateArray.size(); i++) {
points[i] = coordinateArray.getAsNumber(i).doubleValue();
}
transform.transform(points, 0, points, 0, points.length / 2);
for (int i = 0; i < coordinateArray.size(); i++) {
coordinateArray.set(i, new PdfNumber(points[i]));
}
}
/**
* Scales the scalar properties of an annotation based on the provided horizontal and vertical scaling factors.
*
* @param annot the annotation whose scalar properties are to be scaled
* @param horizontalScale the factor by which the horizontal dimensions should be scaled
* @param verticalScale the factor by which the vertical dimensions should be scaled
*/
private static void scaleAnnotationScalarProperties(PdfAnnotation annot,
double horizontalScale, double verticalScale) {
PdfDictionary annotDict = annot.getPdfObject();
// Scale border width in a Border array [horizontal_radius vertical_radius width ...]
PdfArray border = annotDict.getAsArray(PdfName.Border);
if (border != null && border.size() >= 3) {
border.set(0, new PdfNumber(border.getAsNumber(0).doubleValue() * horizontalScale));
border.set(1, new PdfNumber(border.getAsNumber(1).doubleValue() * verticalScale));
border.set(2, new PdfNumber(border.getAsNumber(2).doubleValue()
* Math.min(horizontalScale, verticalScale)));
}
// Scale border width in a BS (Border Style) dictionary
PdfDictionary bs = annotDict.getAsDictionary(PdfName.BS);
if (bs != null) {
PdfNumber width = bs.getAsNumber(PdfName.W);
if (width != null) {
bs.put(PdfName.W, new PdfNumber(width.doubleValue() * Math.min(horizontalScale, verticalScale)));
}
}
// Scale RD (Rectangle Differences) - defines differences between Rect and actual drawing area
// These are lengths/insets, so they should be scaled, not transformed.
PdfArray rd = annotDict.getAsArray(PdfName.RD);
if (rd != null && rd.size() == 4) {
rd.set(0, new PdfNumber(rd.getAsNumber(0).doubleValue() * horizontalScale));
rd.set(1, new PdfNumber(rd.getAsNumber(1).doubleValue() * verticalScale));
rd.set(2, new PdfNumber(rd.getAsNumber(2).doubleValue() * horizontalScale));
rd.set(3, new PdfNumber(rd.getAsNumber(3).doubleValue() * verticalScale));
}
// Scale LeaderLine-related lengths for Line annotations
double lengthScale = Math.min(horizontalScale, verticalScale);
if (annotDict.getAsNumber(PdfName.LL) != null) {
annotDict.put(PdfName.LL, new PdfNumber(annotDict.getAsNumber(PdfName.LL).doubleValue() * lengthScale));
}
if (annotDict.getAsNumber(PdfName.LLE) != null) {
annotDict.put(PdfName.LLE, new PdfNumber(annotDict.getAsNumber(PdfName.LLE).doubleValue() * lengthScale));
}
if (annotDict.getAsNumber(PdfName.LLO) != null) {
annotDict.put(PdfName.LLO, new PdfNumber(annotDict.getAsNumber(PdfName.LLO).doubleValue() * lengthScale));
}
// Scale font size in the Default Appearance string
String da = null;
if (annotDict.getAsString(PdfName.DA) != null) {
da = annotDict.getAsString(PdfName.DA).toUnicodeString();
} else {
if (PdfName.Widget.equals(annotDict.getAsName(PdfName.Subtype))) {
// For widget annotation we should also check parents
da = getDaFromParent(annotDict);
if (da == null) {
// Nothing in parents - check Acroform
PdfDictionary acroFormDictionary = annot.getPage().getDocument()
.getCatalog().getPdfObject().getAsDictionary(PdfName.AcroForm);
if (acroFormDictionary != null && acroFormDictionary.getAsString(PdfName.DA) != null) {
da = acroFormDictionary.getAsString(PdfName.DA).toUnicodeString();
}
}
}
}
if (da != null) {
annotDict.put(PdfName.DA, new PdfString(scaleDaString(da, lengthScale)));
}
}
/**
* Formats a given double value to a string representation with reasonable precision
*
* @param v the double value to be formatted
* @return string representation of the formatted number
*/
private static String formatNumber(double v) {
if (Double.isNaN(v)) {
return Double.toString(v);
}
// Round to 4 decimal places.
long scaled = (long) Math.round(v * 10000.0);
if (scaled == 0) {
return "0";
}
StringBuilder sb = new StringBuilder();
if (scaled < 0) {
sb.append('-');
scaled = -scaled;
}
long wholePart = scaled / 10000;
long fractionalPart = scaled % 10000;
sb.append(wholePart);
if (fractionalPart > 0) {
sb.append('.');
String fractionalStr = String.valueOf(10000 + fractionalPart).substring(1);
sb.append(fractionalStr);
while (sb.length() > 0 && sb.charAt(sb.length() - 1) == '0') {
sb.setLength(sb.length() - 1);
}
}
return sb.toString();
}
/**
* Resizes a single appearance stream by scaling its bounding box and adjusting its transformation matrix.
* The method takes into consideration the existing matrix if present, and applies the scaling transformation.
*
* @param appearanceStream the appearance stream to be resized
* @param scalingMatrix the affine transformation matrix representing the scaling to be applied
*/
private static void resizeAppearanceStream(PdfStream appearanceStream, AffineTransform scalingMatrix) {
// The appearance stream's transformation matrix should only handle scaling.
// Page-level translations are handled by the annotation's /Rect entry.
// We create a new AffineTransform containing only the scaling components.
AffineTransform scaleOnlyMatrix = new AffineTransform();
scaleOnlyMatrix.scale(scalingMatrix.getScaleX(), scalingMatrix.getScaleY());
PdfArray existingMatrix = appearanceStream.getAsArray(PdfName.Matrix);
AffineTransform newMatrix;
if (existingMatrix == null) {
newMatrix = scaleOnlyMatrix;
} else {
newMatrix = new AffineTransform(existingMatrix.toDoubleArray());
newMatrix.preConcatenate(scaleOnlyMatrix);
}
double[] newMatrixArray = new double[6];
newMatrix.getMatrix(newMatrixArray);
appearanceStream.put(PdfName.Matrix, new PdfArray(newMatrixArray));
}
private static String getDaFromParent(PdfDictionary dict) {
PdfDictionary parentDict = dict.getAsDictionary(PdfName.Parent);
if (parentDict == null) {
return null;
} else {
PdfString da = parentDict.getAsString(PdfName.DA);
if (da != null) {
return da.toUnicodeString();
} else {
return getDaFromParent(parentDict);
}
}
}
/**
* 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);
}
}
private AffineTransform calculateAffineTransform(double horizontalScale, double verticalScale,
double horizontalFreeSpace, double verticalFreeSpace) {
AffineTransform scalingMatrix = new AffineTransform();
scalingMatrix.scale(horizontalScale, verticalScale);
AffineTransform transformMatrix = new AffineTransform();
switch (horizontalAnchorPoint) {
case CENTER:
transformMatrix.translate(horizontalFreeSpace / 2, 0);
break;
case RIGHT:
transformMatrix.translate(horizontalFreeSpace, 0);
break;
case LEFT:
default:
// PDF default nothing to do here
break;
}
switch (verticalAnchorPoint) {
case CENTER:
transformMatrix.translate(0, verticalFreeSpace / 2);
break;
case TOP:
transformMatrix.translate(0, verticalFreeSpace);
break;
case BOTTOM:
default:
// PDF default nothing to do here
break;
}
transformMatrix.concatenate(scalingMatrix);
scalingMatrix = transformMatrix;
return scalingMatrix;
}
/**
* 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
}
enum VerticalAnchorPoint {
TOP,
CENTER,
BOTTOM
}
enum HorizontalAnchorPoint {
LEFT,
CENTER,
RIGHT
}
}