fix: correctly insert fields on rotated pages (#1183)

## Description

PDFs can have pages that are rotated, which are correctly rendered in
the frontend. However, when we load the PDF in the backend, the rotation
is applied which causes issues when we want to insert fields.

To account for this, we swap the width and height for pages that are
rotated by 90/270 degrees. This is so we can calculate the virtual
position the field was placed if it was correctly oriented in the
frontend.

Then when we insert the fields, we apply a transformation to the
position of the field so it is rotated correctly.

**Test document with 0/90/180/270 rotated pages**


[rotated_test.pdf](https://github.com/user-attachments/files/15798138/rotated_test.pdf)
This commit is contained in:
Lucas Smith
2024-06-12 21:54:45 +10:00
committed by GitHub

View File

@ -1,6 +1,7 @@
// https://github.com/Hopding/pdf-lib/issues/20#issuecomment-412852821
import fontkit from '@pdf-lib/fontkit';
import { PDFDocument } from 'pdf-lib';
import { PDFDocument, RotationTypes, degrees, radiansToDegrees } from 'pdf-lib';
import { match } from 'ts-pattern';
import {
DEFAULT_HANDWRITING_FONT_SIZE,
@ -37,7 +38,32 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
throw new Error(`Page ${field.page} does not exist`);
}
const { width: pageWidth, height: pageHeight } = page.getSize();
const pageRotation = page.getRotation();
let pageRotationInDegrees = match(pageRotation.type)
.with(RotationTypes.Degrees, () => pageRotation.angle)
.with(RotationTypes.Radians, () => radiansToDegrees(pageRotation.angle))
.exhaustive();
// Round to the closest multiple of 90 degrees.
pageRotationInDegrees = Math.round(pageRotationInDegrees / 90) * 90;
const isPageRotatedToLandscape = pageRotationInDegrees === 90 || pageRotationInDegrees === 270;
let { width: pageWidth, height: pageHeight } = page.getSize();
// PDFs can have pages that are rotated, which are correctly rendered in the frontend.
// However when we load the PDF in the backend, the rotation is applied.
//
// To account for this, we swap the width and height for pages that are rotated by 90/270
// degrees. This is so we can calculate the virtual position the field was placed if it
// was correctly oriented in the frontend.
//
// Then when we insert the fields, we apply a transformation to the position of the field
// so it is rotated correctly.
if (isPageRotatedToLandscape) {
[pageWidth, pageHeight] = [pageHeight, pageWidth];
}
const fieldWidth = pageWidth * (Number(field.width) / 100);
const fieldHeight = pageHeight * (Number(field.height) / 100);
@ -65,17 +91,31 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
imageWidth = imageWidth * scalingFactor;
imageHeight = imageHeight * scalingFactor;
const imageX = fieldX + (fieldWidth - imageWidth) / 2;
let imageX = fieldX + (fieldWidth - imageWidth) / 2;
let imageY = fieldY + (fieldHeight - imageHeight) / 2;
// Invert the Y axis since PDFs use a bottom-left coordinate system
imageY = pageHeight - imageY - imageHeight;
if (pageRotationInDegrees !== 0) {
const adjustedPosition = adjustPositionForRotation(
pageWidth,
pageHeight,
imageX,
imageY,
pageRotationInDegrees,
);
imageX = adjustedPosition.xPos;
imageY = adjustedPosition.yPos;
}
page.drawImage(image, {
x: imageX,
y: imageY,
width: imageWidth,
height: imageHeight,
rotate: degrees(pageRotationInDegrees),
});
} else {
const longestLineInTextForWidth = field.customText
@ -90,17 +130,31 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
fontSize = Math.max(Math.min(fontSize * scalingFactor, maxFontSize), minFontSize);
textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize);
const textX = fieldX + (fieldWidth - textWidth) / 2;
let textX = fieldX + (fieldWidth - textWidth) / 2;
let textY = fieldY + (fieldHeight - textHeight) / 2;
// Invert the Y axis since PDFs use a bottom-left coordinate system
textY = pageHeight - textY - textHeight;
if (pageRotationInDegrees !== 0) {
const adjustedPosition = adjustPositionForRotation(
pageWidth,
pageHeight,
textX,
textY,
pageRotationInDegrees,
);
textX = adjustedPosition.xPos;
textY = adjustedPosition.yPos;
}
page.drawText(field.customText, {
x: textX,
y: textY,
size: fontSize,
font,
rotate: degrees(pageRotationInDegrees),
});
}
@ -117,3 +171,32 @@ export const insertFieldInPDFBytes = async (
return await pdfDoc.save();
};
const adjustPositionForRotation = (
pageWidth: number,
pageHeight: number,
xPos: number,
yPos: number,
pageRotationInDegrees: number,
) => {
if (pageRotationInDegrees === 270) {
xPos = pageWidth - xPos;
[xPos, yPos] = [yPos, xPos];
}
if (pageRotationInDegrees === 90) {
yPos = pageHeight - yPos;
[xPos, yPos] = [yPos, xPos];
}
// Invert all the positions since it's rotated by 180 degrees.
if (pageRotationInDegrees === 180) {
xPos = pageWidth - xPos;
yPos = pageHeight - yPos;
}
return {
xPos,
yPos,
};
};