mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
This PR is handles the changes required to support envelopes. The new envelope editor/signing page will be hidden during release. The core changes here is to migrate the documents and templates model to a centralized envelopes model. Even though Documents and Templates are removed, from the user perspective they will still exist as we remap envelopes to documents and templates.
134 lines
4.3 KiB
TypeScript
134 lines
4.3 KiB
TypeScript
import type { PDFDocument } from '@cantoo/pdf-lib';
|
|
import { RotationTypes, radiansToDegrees } from '@cantoo/pdf-lib';
|
|
import fontkit from '@pdf-lib/fontkit';
|
|
import Konva from 'konva';
|
|
import 'konva/skia-backend';
|
|
import fs from 'node:fs';
|
|
import type { Canvas } from 'skia-canvas';
|
|
import { match } from 'ts-pattern';
|
|
|
|
import { isSignatureFieldType } from '@documenso/prisma/guards/is-signature-field';
|
|
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
|
|
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|
import { renderField } from '../../universal/field-renderer/render-field';
|
|
import { getPageSize } from './get-page-size';
|
|
|
|
// const font = await pdf.embedFont(
|
|
// isSignatureField ? fontCaveat : fontNoto,
|
|
// isSignatureField ? { features: { calt: false } } : undefined,
|
|
// );
|
|
// const minFontSize = isSignatureField ? MIN_HANDWRITING_FONT_SIZE : MIN_STANDARD_FONT_SIZE;
|
|
// const maxFontSize = isSignatureField ? DEFAULT_HANDWRITING_FONT_SIZE : DEFAULT_STANDARD_FONT_SIZE;
|
|
|
|
export const insertFieldInPDFV2 = async (pdf: PDFDocument, field: FieldWithSignature) => {
|
|
const [fontCaveat, fontNoto] = await Promise.all([
|
|
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/caveat.ttf`).then(async (res) => res.arrayBuffer()),
|
|
fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(async (res) => res.arrayBuffer()),
|
|
]);
|
|
|
|
const isSignatureField = isSignatureFieldType(field.type);
|
|
|
|
pdf.registerFontkit(fontkit);
|
|
|
|
const pages = pdf.getPages();
|
|
|
|
const page = pages.at(field.page - 1);
|
|
|
|
if (!page) {
|
|
throw new Error(`Page ${field.page} does not exist`);
|
|
}
|
|
|
|
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;
|
|
|
|
// Todo: Evenloeps - getPageSize this had extra logic? Ask lucas
|
|
|
|
console.log({
|
|
cropBox: page.getCropBox(),
|
|
mediaBox: page.getMediaBox(),
|
|
mediaBox2: page.getSize(),
|
|
});
|
|
|
|
const { width: pageWidth, height: pageHeight } = getPageSize(page);
|
|
|
|
// 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];
|
|
}
|
|
|
|
console.log({
|
|
pageWidth,
|
|
pageHeight,
|
|
fieldWidth: field.width,
|
|
fieldHeight: field.height,
|
|
});
|
|
|
|
const stage = new Konva.Stage({ width: pageWidth, height: pageHeight });
|
|
const layer = new Konva.Layer();
|
|
|
|
// Will render onto the layer.
|
|
renderField({
|
|
field: {
|
|
renderId: field.id.toString(),
|
|
...field,
|
|
width: Number(field.width),
|
|
height: Number(field.height),
|
|
positionX: Number(field.positionX),
|
|
positionY: Number(field.positionY),
|
|
},
|
|
pageLayer: layer,
|
|
pageWidth,
|
|
pageHeight,
|
|
mode: 'export',
|
|
});
|
|
|
|
stage.add(layer);
|
|
const canvas = layer.canvas._canvas as unknown as Canvas;
|
|
|
|
const renderedField = await canvas.toBuffer('svg');
|
|
|
|
fs.writeFileSync(
|
|
`rendered-field-${field.envelopeId}--${field.id}.svg`,
|
|
renderedField.toString('utf-8'),
|
|
);
|
|
|
|
// Embed the SVG into the PDF
|
|
const svgElement = await pdf.embedSvg(renderedField.toString('utf-8'));
|
|
|
|
// Calculate position to cover the whole page
|
|
// pdf-lib coordinates: (0,0) is bottom-left, y increases upward
|
|
const svgWidth = pageWidth; // Use full page width
|
|
const svgHeight = pageHeight; // Use full page height
|
|
|
|
const x = 0; // Start from left edge
|
|
const y = pageHeight; // Start from bottom edge
|
|
|
|
// Draw the SVG on the page
|
|
page.drawSvg(svgElement, {
|
|
x: x,
|
|
y: y,
|
|
width: svgWidth,
|
|
height: svgHeight,
|
|
});
|
|
|
|
return pdf;
|
|
};
|