mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-08-17 21:25:54 +02:00
515 lines
14 KiB
JavaScript
515 lines
14 KiB
JavaScript
import { PDFDocument, rgb } from 'pdf-lib';
|
|
import fs from 'node:fs';
|
|
import fontkit from '@pdf-lib/fontkit';
|
|
import { formatDateTime } from '../../../Utils.js';
|
|
|
|
const formatDateStr = (dateStr, DateFormat, timezone, Is12Hr) => {
|
|
if (!dateStr) return '';
|
|
const date = new Date(dateStr);
|
|
if (isNaN(date.getTime())) return dateStr;
|
|
return formatDateTime(date, DateFormat, timezone, Is12Hr);
|
|
};
|
|
|
|
export default async function GenerateCertificate(docDetails) {
|
|
const timezone = docDetails?.ExtUserPtr?.Timezone || '';
|
|
const Is12Hr = docDetails?.ExtUserPtr?.Is12HourTime || false;
|
|
const DateFormat = docDetails?.ExtUserPtr?.DateFormat || 'MM/DD/YYYY';
|
|
const pdfDoc = await PDFDocument.create();
|
|
// `fontBytes` is used to embed custom font in pdf
|
|
const fontBytes = fs.readFileSync('./font/times.ttf'); //
|
|
pdfDoc.registerFontkit(fontkit);
|
|
const timesRomanFont = await pdfDoc.embedFont(fontBytes, { subset: true });
|
|
const pngUrl = fs.readFileSync('./images/logo.png').buffer;
|
|
const naSignUrl = fs.readFileSync('./images/na_sign.png').buffer;
|
|
const nasign = await pdfDoc.embedPng(naSignUrl);
|
|
const pngImage = await pdfDoc.embedPng(pngUrl);
|
|
const page = pdfDoc.addPage();
|
|
const { width, height } = page.getSize();
|
|
const startX = 15;
|
|
const startY = 15;
|
|
const borderColor = rgb(0.12, 0.12, 0.12);
|
|
const titleColor = rgb(0, 0.2, 0.4); //rgb(0, 0.53, 0.71);
|
|
const titleUnderline = rgb(0, 0.2, 0.4); // rgb(0.12, 0.12, 0.12);
|
|
const title = 25;
|
|
const subtitle = 16;
|
|
const text = 13;
|
|
const signertext = 13;
|
|
const timeText = 11;
|
|
const textKeyColor = rgb(0.12, 0.12, 0.12);
|
|
const textValueColor = rgb(0.3, 0.3, 0.3);
|
|
const completedAt = docDetails?.completedAt ? new Date(docDetails?.completedAt) : new Date();
|
|
const completedAtperTimezone = formatDateStr(completedAt, DateFormat, timezone, Is12Hr);
|
|
const completedUTCtime = completedAtperTimezone;
|
|
const signersCount = docDetails?.Signers?.length || 1;
|
|
const generateAt = docDetails?.completedAt ? new Date(docDetails?.completedAt) : new Date();
|
|
const generatedAtperTimezone = formatDateTime(generateAt, DateFormat, timezone, Is12Hr);
|
|
const generatedUTCTime = generatedAtperTimezone;
|
|
const generatedOn = 'Generated On ' + generatedUTCTime;
|
|
const textWidth = timesRomanFont.widthOfTextAtSize(generatedOn, 12);
|
|
const margin = 30;
|
|
const maxX = width - margin - textWidth; // Ensures text stays inside the border with 30px margin
|
|
const OriginIp = docDetails?.OriginIp || '';
|
|
const company = docDetails?.ExtUserPtr?.Company || '';
|
|
const documentHash = docDetails?.DocumentHash || '';
|
|
const createdAt = docDetails?.DocSentAt?.iso || docDetails.createdAt;
|
|
const createdAtperTimezone = formatDateStr(createdAt, DateFormat, timezone, Is12Hr);
|
|
const IsEnableOTP = docDetails?.IsEnableOTP || false;
|
|
const placeholders = Array.isArray(docDetails?.Placeholders) ? docDetails.Placeholders : [];
|
|
const filteredaudit = docDetails?.AuditTrail?.filter(x => {
|
|
if (!x?.UserPtr?.objectId) return false;
|
|
return true;
|
|
});
|
|
const toTs = v => {
|
|
if (!v) return 0;
|
|
if (typeof v === 'object' && v?.iso) return new Date(v.iso).getTime() || 0;
|
|
const t = new Date(v).getTime();
|
|
return Number.isFinite(t) ? t : 0;
|
|
};
|
|
const auditTrail =
|
|
docDetails?.Signers?.length > 0
|
|
? filteredaudit?.map(x => {
|
|
const data = docDetails.Signers.find(y => y.objectId === x.UserPtr.objectId);
|
|
return {
|
|
...data,
|
|
ipAddress: x.ipAddress,
|
|
SignedOn: x?.SignedOn || generatedUTCTime,
|
|
ViewedOn: x?.ViewedOn || x?.SignedOn || generatedUTCTime,
|
|
Signature: x?.Signature || '',
|
|
_signedOnTs: toTs(x?.SignedOn),
|
|
};
|
|
})
|
|
: [
|
|
{
|
|
...docDetails.ExtUserPtr,
|
|
ipAddress: filteredaudit[0].ipAddress,
|
|
SignedOn: filteredaudit[0]?.SignedOn || generatedUTCTime,
|
|
ViewedOn: filteredaudit[0]?.ViewedOn || filteredaudit[0]?.SignedOn || generatedUTCTime,
|
|
Signature: filteredaudit[0]?.Signature || '',
|
|
_signedOnTs: toTs(filteredaudit[0]?.SignedOn),
|
|
},
|
|
];
|
|
if (Array.isArray(auditTrail)) {
|
|
auditTrail.sort((a, b) => (a?._signedOnTs || 0) - (b?._signedOnTs || 0));
|
|
}
|
|
|
|
const ownerName = docDetails?.SenderName || docDetails.ExtUserPtr?.Name || 'n/a';
|
|
const ownerEmail = docDetails?.SenderMail || docDetails.ExtUserPtr?.Email || 'n/a';
|
|
const half = width / 2;
|
|
// Draw a border
|
|
page.drawRectangle({
|
|
x: startX,
|
|
y: startY,
|
|
width: width - 2 * startX,
|
|
height: height - 2 * startY,
|
|
borderColor: borderColor,
|
|
borderWidth: 1,
|
|
});
|
|
page.drawImage(pngImage, {
|
|
x: 30,
|
|
y: 790,
|
|
width: 100,
|
|
height: 25,
|
|
});
|
|
|
|
page.drawText(generatedOn, {
|
|
x: Math.max(startX, maxX), // Adjusts dynamically 320
|
|
y: 810,
|
|
size: 12,
|
|
font: timesRomanFont,
|
|
color: rgb(0.12, 0.12, 0.12),
|
|
});
|
|
|
|
page.drawText('Certificate of Completion', {
|
|
x: 160,
|
|
y: 755,
|
|
size: title,
|
|
font: timesRomanFont,
|
|
color: titleColor,
|
|
});
|
|
|
|
const underlineY = 745;
|
|
page.drawLine({
|
|
start: { x: 30, y: underlineY },
|
|
end: { x: width - 30, y: underlineY },
|
|
color: titleUnderline,
|
|
thickness: 1,
|
|
});
|
|
|
|
page.drawText('Summary', {
|
|
x: 30,
|
|
y: 727,
|
|
size: subtitle,
|
|
font: timesRomanFont,
|
|
color: titleColor,
|
|
});
|
|
|
|
page.drawText('Document Id :', {
|
|
x: 30,
|
|
y: 710,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
|
|
page.drawText(docDetails.objectId, {
|
|
x: 110,
|
|
y: 710,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textValueColor,
|
|
});
|
|
|
|
page.drawText('Document Name :', {
|
|
x: 30,
|
|
y: 690,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
|
|
page.drawText(docDetails?.Name, {
|
|
x: 130,
|
|
y: 690,
|
|
size: docDetails?.Name?.length >= 78 ? 12 : text,
|
|
font: timesRomanFont,
|
|
color: textValueColor,
|
|
});
|
|
|
|
if (documentHash) {
|
|
page.drawText('Document hash (sha256) :', {
|
|
x: 30,
|
|
y: 670,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
|
|
page.drawText(documentHash, {
|
|
x: 170,
|
|
y: 670,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textValueColor,
|
|
});
|
|
}
|
|
|
|
const organizationY = documentHash ? 650 : 670;
|
|
const createdOnY = organizationY - 20;
|
|
const completedOnY = createdOnY - 20;
|
|
const signersY = completedOnY - 20;
|
|
const originatorHeaderY = signersY - 20;
|
|
const nameY = originatorHeaderY - 17;
|
|
const emailY = nameY - 20;
|
|
const ipY = emailY - 20;
|
|
|
|
page.drawText('Organization :', {
|
|
x: 30,
|
|
y: organizationY,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
|
|
page.drawText(company, {
|
|
x: 110,
|
|
y: organizationY,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textValueColor,
|
|
});
|
|
page.drawText('Created on :', {
|
|
x: 30,
|
|
y: createdOnY,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
|
|
page.drawText(`${createdAtperTimezone}`, {
|
|
x: 97,
|
|
y: createdOnY,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textValueColor,
|
|
});
|
|
page.drawText('Completed on :', {
|
|
x: 30,
|
|
y: completedOnY,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
|
|
page.drawText(`${completedUTCtime}`, {
|
|
x: 115,
|
|
y: completedOnY,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textValueColor,
|
|
});
|
|
page.drawText('Signers :', {
|
|
x: 30,
|
|
y: signersY,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
|
|
page.drawText(`${signersCount}`, {
|
|
x: 80,
|
|
y: signersY,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textValueColor,
|
|
});
|
|
page.drawText('Document originator', {
|
|
x: 30,
|
|
y: originatorHeaderY,
|
|
size: 17,
|
|
font: timesRomanFont,
|
|
color: titleColor,
|
|
});
|
|
page.drawText('Name :', {
|
|
x: 60,
|
|
y: nameY,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
page.drawText(ownerName, {
|
|
x: 105,
|
|
y: nameY,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textValueColor,
|
|
});
|
|
page.drawText('Email :', {
|
|
x: 60,
|
|
y: emailY,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
page.drawText(ownerEmail, {
|
|
x: 105,
|
|
y: emailY,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textValueColor,
|
|
});
|
|
page.drawText('IP address :', {
|
|
x: 60,
|
|
y: ipY,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
page.drawText(`${OriginIp}`, {
|
|
x: 125,
|
|
y: ipY,
|
|
size: text,
|
|
font: timesRomanFont,
|
|
color: textValueColor,
|
|
});
|
|
|
|
page.drawLine({
|
|
start: { x: 30, y: ipY - 6 },
|
|
end: { x: width - 30, y: ipY - 6 },
|
|
color: rgb(0.12, 0.12, 0.12),
|
|
thickness: 0.5,
|
|
});
|
|
let yPosition1 = ipY - 21;
|
|
let yPosition2 = yPosition1 - 14;
|
|
let yPosition3 = yPosition2 - 20;
|
|
let yPosition4 = yPosition3 - 20;
|
|
let yPosition5 = yPosition4 - 20;
|
|
let yPosition6 = yPosition5 - 20;
|
|
let yPosition7 = yPosition6 - 20;
|
|
let yPosition8 = yPosition7 - 35;
|
|
|
|
// A signer/approver block spans from yPosition1 down to yPosition8 (the
|
|
// separator line at the bottom of the block). The signature image's
|
|
// bottom edge sits at yPosition7 - 30 and must remain inside the page
|
|
// border (whose bottom edge is at startY). Use the lowest of those two
|
|
// values when deciding whether the next block fits on the current page.
|
|
const minY = startY + 5;
|
|
const blockBottom = () => Math.min(yPosition7 - 30, yPosition8);
|
|
|
|
// Helper that resets the y-positions to the top of a freshly added page so
|
|
// the next block starts cleanly under the border.
|
|
const startNewPage = () => {
|
|
const newPage = pdfDoc.addPage();
|
|
newPage.drawRectangle({
|
|
x: startX,
|
|
y: startY,
|
|
width: width - 2 * startX,
|
|
height: height - 2 * startY,
|
|
borderColor: borderColor,
|
|
borderWidth: 1,
|
|
});
|
|
yPosition1 = newPage.getHeight() - 40;
|
|
yPosition2 = yPosition1 - 20;
|
|
yPosition3 = yPosition2 - 20;
|
|
yPosition4 = yPosition3 - 20;
|
|
yPosition5 = yPosition4 - 20;
|
|
yPosition6 = yPosition5 - 20;
|
|
yPosition7 = yPosition6 - 20;
|
|
yPosition8 = yPosition7 - 35;
|
|
return newPage;
|
|
};
|
|
|
|
let currentPage = page;
|
|
for (let i = 0; i < auditTrail.length; i++) {
|
|
const x = auditTrail[i];
|
|
// If the next block would overflow the bottom border, move to a new page.
|
|
if (blockBottom() < minY) {
|
|
currentPage = startNewPage();
|
|
}
|
|
const embedPng = x.Signature ? await pdfDoc.embedPng(x.Signature) : nasign;
|
|
const headerLabel = `${i + 1}. ${'Signer'}`;
|
|
const signedOnLabel = 'Signed on :';
|
|
|
|
currentPage.drawText(headerLabel, {
|
|
x: 30,
|
|
y: yPosition1,
|
|
size: subtitle,
|
|
font: timesRomanFont,
|
|
color: titleColor,
|
|
});
|
|
currentPage.drawText('Name :', {
|
|
x: 30,
|
|
y: yPosition2,
|
|
size: signertext,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
currentPage.drawText(x?.Name || '', {
|
|
x: 75,
|
|
y: yPosition2,
|
|
size: signertext,
|
|
font: timesRomanFont,
|
|
color: textValueColor,
|
|
});
|
|
|
|
if (IsEnableOTP) {
|
|
currentPage.drawText('Security level :', {
|
|
x: half + 120,
|
|
y: yPosition2,
|
|
size: timeText,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
currentPage.drawText('Email, OTP Auth', {
|
|
x: half + 190,
|
|
y: yPosition2,
|
|
size: timeText,
|
|
font: timesRomanFont,
|
|
color: textValueColor,
|
|
});
|
|
}
|
|
|
|
currentPage.drawText('Email :', {
|
|
x: 30,
|
|
y: yPosition3,
|
|
size: signertext,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
currentPage.drawText(x?.Email || '', {
|
|
x: 75,
|
|
y: yPosition3,
|
|
size: signertext,
|
|
font: timesRomanFont,
|
|
color: textValueColor,
|
|
});
|
|
|
|
currentPage.drawText('Viewed on :', {
|
|
x: 30,
|
|
y: yPosition4,
|
|
size: signertext,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
currentPage.drawText(`${formatDateStr(x?.ViewedOn, DateFormat, timezone, Is12Hr)}`, {
|
|
x: 97,
|
|
y: yPosition4,
|
|
size: signertext,
|
|
font: timesRomanFont,
|
|
color: textValueColor,
|
|
});
|
|
|
|
currentPage.drawText(signedOnLabel, {
|
|
x: 30,
|
|
y: yPosition5,
|
|
size: signertext,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
const signedOnValueX = 30 + timesRomanFont.widthOfTextAtSize(signedOnLabel, signertext) + 5;
|
|
currentPage.drawText(`${formatDateStr(x?.SignedOn, DateFormat, timezone, Is12Hr)}`, {
|
|
x: signedOnValueX,
|
|
y: yPosition5,
|
|
size: signertext,
|
|
font: timesRomanFont,
|
|
color: textValueColor,
|
|
});
|
|
|
|
currentPage.drawText('IP address :', {
|
|
x: 30,
|
|
y: yPosition6,
|
|
size: signertext,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
currentPage.drawText(x?.ipAddress || '', {
|
|
x: 95,
|
|
y: yPosition6,
|
|
size: signertext,
|
|
font: timesRomanFont,
|
|
color: textValueColor,
|
|
});
|
|
|
|
currentPage.drawText('Signature :', {
|
|
x: 30,
|
|
y: yPosition7,
|
|
size: signertext,
|
|
font: timesRomanFont,
|
|
color: textKeyColor,
|
|
});
|
|
currentPage.drawRectangle({
|
|
x: 98,
|
|
y: yPosition7 - 30,
|
|
width: 104,
|
|
height: 44,
|
|
borderColor: rgb(0.22, 0.18, 0.47),
|
|
borderWidth: 1,
|
|
});
|
|
if (embedPng) {
|
|
currentPage.drawImage(embedPng, {
|
|
x: 100,
|
|
y: yPosition7 - 27,
|
|
width: 100,
|
|
height: 40,
|
|
});
|
|
}
|
|
currentPage.drawLine({
|
|
start: { x: 30, y: yPosition8 },
|
|
end: { x: width - 30, y: yPosition8 },
|
|
color: rgb(0.12, 0.12, 0.12),
|
|
thickness: 0.5,
|
|
});
|
|
|
|
yPosition1 = yPosition8 - 20;
|
|
yPosition2 = yPosition1 - 20;
|
|
yPosition3 = yPosition2 - 20;
|
|
yPosition4 = yPosition3 - 20;
|
|
yPosition5 = yPosition4 - 20;
|
|
yPosition6 = yPosition5 - 20;
|
|
yPosition7 = yPosition6 - 20;
|
|
yPosition8 = yPosition8 - 174;
|
|
}
|
|
|
|
const pdfBytes = await pdfDoc.save();
|
|
return pdfBytes;
|
|
}
|