feat: add envelopes (#2025)

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.
This commit is contained in:
David Nguyen
2025-10-14 21:56:36 +11:00
committed by GitHub
parent 7b17156e56
commit 7f09ba72f4
447 changed files with 33467 additions and 9622 deletions

View File

@ -1,6 +1,6 @@
import type { PDFDocument } from '@cantoo/pdf-lib';
import { TextAlignment, rgb, setFontAndSize } from '@cantoo/pdf-lib';
import fontkit from '@pdf-lib/fontkit';
import type { PDFDocument } from 'pdf-lib';
import { TextAlignment, rgb, setFontAndSize } from 'pdf-lib';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { getPageSize } from './get-page-size';

View File

@ -1,4 +1,4 @@
import { PDFAnnotation, PDFRef } from 'pdf-lib';
import { PDFAnnotation, PDFRef } from '@cantoo/pdf-lib';
import {
PDFDict,
type PDFDocument,
@ -8,7 +8,7 @@ import {
pushGraphicsState,
rotateInPlace,
translate,
} from 'pdf-lib';
} from '@cantoo/pdf-lib';
export const flattenAnnotations = (document: PDFDocument) => {
const pages = document.getPages();

View File

@ -1,5 +1,4 @@
import fontkit from '@pdf-lib/fontkit';
import type { PDFField, PDFWidgetAnnotation } from 'pdf-lib';
import type { PDFField, PDFWidgetAnnotation } from '@cantoo/pdf-lib';
import {
PDFCheckBox,
PDFDict,
@ -12,7 +11,8 @@ import {
pushGraphicsState,
rotateInPlace,
translate,
} from 'pdf-lib';
} from '@cantoo/pdf-lib';
import fontkit from '@pdf-lib/fontkit';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';

View File

@ -1,4 +1,4 @@
import type { PDFPage } from 'pdf-lib';
import type { PDFPage } from '@cantoo/pdf-lib';
/**
* Gets the effective page size for PDF operations.

View File

@ -1,7 +1,5 @@
// https://github.com/Hopding/pdf-lib/issues/20#issuecomment-412852821
import fontkit from '@pdf-lib/fontkit';
import { FieldType } from '@prisma/client';
import type { PDFDocument, PDFFont, PDFTextField } from 'pdf-lib';
import type { PDFDocument, PDFFont, PDFTextField } from '@cantoo/pdf-lib';
import {
RotationTypes,
TextAlignment,
@ -9,7 +7,9 @@ import {
radiansToDegrees,
rgb,
setFontAndSize,
} from 'pdf-lib';
} from '@cantoo/pdf-lib';
import fontkit from '@pdf-lib/fontkit';
import { FieldType } from '@prisma/client';
import { P, match } from 'ts-pattern';
import {
@ -35,7 +35,7 @@ import {
} from '../../types/field-meta';
import { getPageSize } from './get-page-size';
export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignature) => {
export const insertFieldInPDFV1 = 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()),

View File

@ -0,0 +1,133 @@
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;
};

View File

@ -1,4 +1,10 @@
import { PDFCheckBox, PDFDocument, PDFDropdown, PDFRadioGroup, PDFTextField } from 'pdf-lib';
import {
PDFCheckBox,
PDFDocument,
PDFDropdown,
PDFRadioGroup,
PDFTextField,
} from '@cantoo/pdf-lib';
export type InsertFormValuesInPdfOptions = {
pdf: Buffer;

View File

@ -1,4 +1,4 @@
import { PDFDocument } from 'pdf-lib';
import { PDFDocument } from '@cantoo/pdf-lib';
export async function insertImageInPDF(
pdfAsBase64: string,

View File

@ -1,5 +1,5 @@
import { PDFDocument, StandardFonts, rgb } from '@cantoo/pdf-lib';
import fontkit from '@pdf-lib/fontkit';
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
import { CAVEAT_FONT_PATH } from '../../constants/pdf';

View File

@ -1,8 +1,8 @@
// https://github.com/Hopding/pdf-lib/issues/20#issuecomment-412852821
import type { PDFDocument } from '@cantoo/pdf-lib';
import { RotationTypes, degrees, radiansToDegrees, rgb } from '@cantoo/pdf-lib';
import fontkit from '@pdf-lib/fontkit';
import { FieldType } from '@prisma/client';
import type { PDFDocument } from 'pdf-lib';
import { RotationTypes, degrees, radiansToDegrees, rgb } from 'pdf-lib';
import { P, match } from 'ts-pattern';
import {

View File

@ -1,4 +1,4 @@
import { PDFDocument } from 'pdf-lib';
import { PDFDocument } from '@cantoo/pdf-lib';
import { flattenAnnotations } from './flatten-annotations';
import { flattenForm, removeOptionalContentGroups } from './flatten-form';

View File

@ -1,5 +1,5 @@
import type { PDFDocument } from 'pdf-lib';
import { PDFSignature, rectangle } from 'pdf-lib';
import type { PDFDocument } from '@cantoo/pdf-lib';
import { PDFSignature, rectangle } from '@cantoo/pdf-lib';
export const normalizeSignatureAppearances = (document: PDFDocument) => {
const form = document.getForm();