mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 01:15:49 +10:00
fix: match cert and audit log page dimensions to source document (#2473)
This commit is contained in:
@@ -15,11 +15,11 @@ import { groupBy } from 'remeda';
|
||||
import { addRejectionStampToPdf } from '@documenso/lib/server-only/pdf/add-rejection-stamp-to-pdf';
|
||||
import { generateAuditLogPdf } from '@documenso/lib/server-only/pdf/generate-audit-log-pdf';
|
||||
import { generateCertificatePdf } from '@documenso/lib/server-only/pdf/generate-certificate-pdf';
|
||||
import { getLastPageDimensions } from '@documenso/lib/server-only/pdf/get-page-size';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { signPdf } from '@documenso/signing';
|
||||
|
||||
import { NEXT_PRIVATE_USE_PLAYWRIGHT_PDF } from '../../../constants/app';
|
||||
import { PDF_SIZE_A4_72PPI } from '../../../constants/pdf';
|
||||
import { AppError, AppErrorCode } from '../../../errors/app-error';
|
||||
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
||||
import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf';
|
||||
@@ -29,8 +29,10 @@ import { insertFieldInPDFV2 } from '../../../server-only/pdf/insert-field-in-pdf
|
||||
import { legacy_insertFieldInPDF } from '../../../server-only/pdf/legacy-insert-field-in-pdf';
|
||||
import { getTeamSettings } from '../../../server-only/team/get-team-settings';
|
||||
import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook';
|
||||
import type { TDocumentAuditLog } from '../../../types/document-audit-logs';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
|
||||
import {
|
||||
DOCUMENT_AUDIT_LOG_TYPE,
|
||||
type TDocumentAuditLog,
|
||||
} from '../../../types/document-audit-logs';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
@@ -184,69 +186,24 @@ export const run = async ({
|
||||
|
||||
const finalEnvelopeStatus = isRejected ? DocumentStatus.REJECTED : DocumentStatus.COMPLETED;
|
||||
|
||||
let certificateDoc: PDF | null = null;
|
||||
let auditLogDoc: PDF | null = null;
|
||||
// Pre-fetch all PDF data so we can read dimensions and pass it
|
||||
// to decorateAndSignPdf without fetching again.
|
||||
const prefetchedItems = await Promise.all(
|
||||
envelopeItems.map(async (envelopeItem) => {
|
||||
const pdfData = await getFileServerSide(envelopeItem.documentData);
|
||||
|
||||
if (settings.includeSigningCertificate || settings.includeAuditLog) {
|
||||
const additionalAuditLogs = [
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
{
|
||||
...envelopeCompletedAuditLog,
|
||||
id: '',
|
||||
createdAt: new Date(),
|
||||
} as TDocumentAuditLog,
|
||||
];
|
||||
return { envelopeItem, pdfData };
|
||||
}),
|
||||
);
|
||||
|
||||
const certificatePayload = {
|
||||
envelope: {
|
||||
...envelope,
|
||||
status: finalEnvelopeStatus,
|
||||
},
|
||||
recipients: envelope.recipients, // Need to use the recipients from envelope which contains ALL recipients.
|
||||
fields,
|
||||
language: envelope.documentMeta.language,
|
||||
envelopeOwner: {
|
||||
email: envelope.user.email,
|
||||
name: envelope.user.name || '',
|
||||
},
|
||||
envelopeItems: envelopeItems.map((item) => item.title),
|
||||
pageWidth: PDF_SIZE_A4_72PPI.width,
|
||||
pageHeight: PDF_SIZE_A4_72PPI.height,
|
||||
additionalAuditLogs,
|
||||
};
|
||||
const usePlaywrightPdf = NEXT_PRIVATE_USE_PLAYWRIGHT_PDF();
|
||||
|
||||
// Use Playwright-based PDF generation if enabled, otherwise use Konva-based generation.
|
||||
// This is a temporary toggle while we validate the Konva-based approach.
|
||||
const usePlaywrightPdf = NEXT_PRIVATE_USE_PLAYWRIGHT_PDF();
|
||||
|
||||
const makeCertificatePdf = async () =>
|
||||
usePlaywrightPdf
|
||||
? getCertificatePdf({
|
||||
documentId,
|
||||
language: envelope.documentMeta.language,
|
||||
}).then(async (buffer) => PDF.load(buffer))
|
||||
: generateCertificatePdf(certificatePayload);
|
||||
|
||||
const makeAuditLogPdf = async () =>
|
||||
usePlaywrightPdf
|
||||
? getAuditLogsPdf({
|
||||
documentId,
|
||||
language: envelope.documentMeta.language,
|
||||
}).then(async (buffer) => PDF.load(buffer))
|
||||
: generateAuditLogPdf(certificatePayload);
|
||||
|
||||
const [createdCertificatePdf, createdAuditLogPdf] = await Promise.all([
|
||||
settings.includeSigningCertificate ? makeCertificatePdf() : null,
|
||||
settings.includeAuditLog ? makeAuditLogPdf() : null,
|
||||
]);
|
||||
|
||||
certificateDoc = createdCertificatePdf;
|
||||
auditLogDoc = createdAuditLogPdf;
|
||||
}
|
||||
const needsCertificate = settings.includeSigningCertificate;
|
||||
const needsAuditLog = settings.includeAuditLog;
|
||||
|
||||
const newDocumentData: Array<{ oldDocumentDataId: string; newDocumentDataId: string }> = [];
|
||||
|
||||
for (const envelopeItem of envelopeItems) {
|
||||
for (const { envelopeItem, pdfData } of prefetchedItems) {
|
||||
const envelopeItemFields = envelope.envelopeItems.find(
|
||||
(item) => item.id === envelopeItem.id,
|
||||
)?.field;
|
||||
@@ -255,12 +212,70 @@ export const run = async ({
|
||||
throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
|
||||
}
|
||||
|
||||
let certificateDoc: PDF | null = null;
|
||||
let auditLogDoc: PDF | null = null;
|
||||
|
||||
if (needsCertificate || needsAuditLog) {
|
||||
const pdfDoc = await PDF.load(pdfData);
|
||||
|
||||
const { width: pageWidth, height: pageHeight } = getLastPageDimensions(pdfDoc);
|
||||
|
||||
const additionalAuditLogs = [
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
{
|
||||
...envelopeCompletedAuditLog,
|
||||
id: '',
|
||||
createdAt: new Date(),
|
||||
} as TDocumentAuditLog,
|
||||
];
|
||||
|
||||
const certificatePayload = {
|
||||
envelope: {
|
||||
...envelope,
|
||||
status: finalEnvelopeStatus,
|
||||
},
|
||||
recipients: envelope.recipients,
|
||||
fields,
|
||||
language: envelope.documentMeta.language,
|
||||
envelopeOwner: {
|
||||
email: envelope.user.email,
|
||||
name: envelope.user.name || '',
|
||||
},
|
||||
envelopeItems: envelopeItems.map((item) => item.title),
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
additionalAuditLogs,
|
||||
};
|
||||
|
||||
const makeCertificatePdf = async () =>
|
||||
usePlaywrightPdf
|
||||
? getCertificatePdf({
|
||||
documentId,
|
||||
language: envelope.documentMeta.language,
|
||||
}).then(async (buffer) => PDF.load(buffer))
|
||||
: generateCertificatePdf(certificatePayload);
|
||||
|
||||
const makeAuditLogPdf = async () =>
|
||||
usePlaywrightPdf
|
||||
? getAuditLogsPdf({
|
||||
documentId,
|
||||
language: envelope.documentMeta.language,
|
||||
}).then(async (buffer) => PDF.load(buffer))
|
||||
: generateAuditLogPdf(certificatePayload);
|
||||
|
||||
[certificateDoc, auditLogDoc] = await Promise.all([
|
||||
needsCertificate ? makeCertificatePdf() : null,
|
||||
needsAuditLog ? makeAuditLogPdf() : null,
|
||||
]);
|
||||
}
|
||||
|
||||
const result = await decorateAndSignPdf({
|
||||
envelope,
|
||||
envelopeItem,
|
||||
envelopeItemFields,
|
||||
isRejected,
|
||||
rejectionReason,
|
||||
pdfData,
|
||||
certificateDoc,
|
||||
auditLogDoc,
|
||||
});
|
||||
@@ -349,12 +364,13 @@ type DecorateAndSignPdfOptions = {
|
||||
envelopeItemFields: Field[];
|
||||
isRejected: boolean;
|
||||
rejectionReason: string;
|
||||
pdfData: Uint8Array;
|
||||
certificateDoc: PDF | null;
|
||||
auditLogDoc: PDF | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch, normalize, flatten and insert fields into a PDF document.
|
||||
* Normalize, flatten and insert fields into a PDF document.
|
||||
*/
|
||||
const decorateAndSignPdf = async ({
|
||||
envelope,
|
||||
@@ -362,11 +378,10 @@ const decorateAndSignPdf = async ({
|
||||
envelopeItemFields,
|
||||
isRejected,
|
||||
rejectionReason,
|
||||
pdfData,
|
||||
certificateDoc,
|
||||
auditLogDoc,
|
||||
}: DecorateAndSignPdfOptions) => {
|
||||
const pdfData = await getFileServerSide(envelopeItem.documentData);
|
||||
|
||||
let pdfDoc = await PDF.load(pdfData);
|
||||
|
||||
// Normalize and flatten layers that could cause issues with the signature
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { PDFPage } from '@cantoo/pdf-lib';
|
||||
import type { PDF } from '@libpdf/core';
|
||||
|
||||
import { PDF_SIZE_A4_72PPI } from '../../constants/pdf';
|
||||
|
||||
const MIN_CERT_PAGE_WIDTH = 300;
|
||||
const MIN_CERT_PAGE_HEIGHT = 300;
|
||||
|
||||
/**
|
||||
* Gets the effective page size for PDF operations.
|
||||
@@ -16,3 +22,20 @@ export const getPageSize = (page: PDFPage) => {
|
||||
|
||||
return cropBox;
|
||||
};
|
||||
|
||||
export const getLastPageDimensions = (pdfDoc: PDF): { width: number; height: number } => {
|
||||
const lastPage = pdfDoc.getPage(pdfDoc.getPageCount() - 1);
|
||||
|
||||
if (!lastPage) {
|
||||
return PDF_SIZE_A4_72PPI;
|
||||
}
|
||||
|
||||
const width = Math.round(lastPage.width);
|
||||
const height = Math.round(lastPage.height);
|
||||
|
||||
if (width < MIN_CERT_PAGE_WIDTH || height < MIN_CERT_PAGE_HEIGHT) {
|
||||
return PDF_SIZE_A4_72PPI;
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user