mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 16:23:06 +10:00
Merge branch 'main' into feat/auto-placing-fields
This commit is contained in:
@ -1,133 +1,64 @@
|
||||
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 path from 'node:path';
|
||||
import type { Canvas } from 'skia-canvas';
|
||||
import { match } from 'ts-pattern';
|
||||
import { FontLibrary } from 'skia-canvas';
|
||||
|
||||
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;
|
||||
type InsertFieldInPDFV2Options = {
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
fields: FieldWithSignature[];
|
||||
};
|
||||
|
||||
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()),
|
||||
export const insertFieldInPDFV2 = async ({
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
fields,
|
||||
}: InsertFieldInPDFV2Options) => {
|
||||
const fontPath = path.join(process.cwd(), 'public/fonts');
|
||||
|
||||
FontLibrary.use([
|
||||
path.join(fontPath, 'caveat.ttf'),
|
||||
path.join(fontPath, 'noto-sans.ttf'),
|
||||
path.join(fontPath, 'noto-sans-japanese.ttf'),
|
||||
path.join(fontPath, 'noto-sans-chinese.ttf'),
|
||||
path.join(fontPath, 'noto-sans-korean.ttf'),
|
||||
]);
|
||||
|
||||
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',
|
||||
});
|
||||
const insertedFields = fields.filter((field) => field.inserted);
|
||||
|
||||
// Render the fields onto the layer.
|
||||
for (const field of insertedFields) {
|
||||
renderField({
|
||||
scale: 1,
|
||||
field: {
|
||||
renderId: field.id.toString(),
|
||||
...field,
|
||||
width: Number(field.width),
|
||||
height: Number(field.height),
|
||||
positionX: Number(field.positionX),
|
||||
positionY: Number(field.positionY),
|
||||
},
|
||||
translations: null,
|
||||
pageLayer: layer,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
mode: 'export',
|
||||
});
|
||||
}
|
||||
|
||||
stage.add(layer);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
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;
|
||||
return await canvas.toBuffer('pdf');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user