feat: add pdf image renderer

This commit is contained in:
David Nguyen
2026-01-27 14:39:16 +11:00
parent 7d38e18f93
commit 4fb3c2cb0f
73 changed files with 2045 additions and 1105 deletions
@@ -1,6 +1,7 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { prisma } from '@documenso/prisma';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
@@ -46,7 +47,7 @@ test.describe('AutoSave Fields Step', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -54,7 +55,7 @@ test.describe('AutoSave Fields Step', () => {
});
await page.getByRole('button', { name: 'Text' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -74,7 +75,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 500,
@@ -100,7 +101,7 @@ test.describe('AutoSave Fields Step', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -108,7 +109,7 @@ test.describe('AutoSave Fields Step', () => {
});
await page.getByRole('button', { name: 'Text' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -128,7 +129,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 500,
@@ -162,7 +163,7 @@ test.describe('AutoSave Fields Step', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -170,7 +171,7 @@ test.describe('AutoSave Fields Step', () => {
});
await page.getByRole('button', { name: 'Text' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -190,7 +191,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 500,
@@ -224,7 +225,7 @@ test.describe('AutoSave Fields Step', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -232,7 +233,7 @@ test.describe('AutoSave Fields Step', () => {
});
await page.getByRole('button', { name: 'Text' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -2,6 +2,7 @@ import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { EnvelopeType } from '@prisma/client';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
@@ -28,7 +29,7 @@ export const setupDocumentAndNavigateToSubjectStep = async (page: Page) => {
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -1,5 +1,6 @@
import { expect, test } from '@playwright/test';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { seedBlankDocument } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
@@ -33,14 +34,14 @@ test('[DOCUMENT_FLOW]: Simple duplicate recipients test', async ({ page }) => {
// Step 3: Add fields
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 100, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 100, y: 100 } });
await page.getByRole('combobox').first().click();
// Switch to second duplicate and add field
await page.getByText('Duplicate 2 (duplicate@example.com)').first().click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 200, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 200, y: 100 } });
// Continue to send
await page.getByRole('button', { name: 'Continue' }).click();
@@ -44,21 +44,21 @@ const completeDocumentFlowWithDuplicateRecipients = async (options: {
// Step 3: Add fields for each recipient
// Add signature field for first duplicate recipient
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 100, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 100, y: 100 } });
await page.getByText('Duplicate Recipient 1 (duplicate@example.com)').click();
// Switch to second duplicate recipient and add their field
await page.getByText('Duplicate Recipient 2 (duplicate@example.com)').click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 200, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 200, y: 100 } });
await page.getByText('Duplicate Recipient 2 (duplicate@example.com)').click();
// Switch to unique recipient and add their field
await page.getByText('Unique Recipient (unique@example.com)').click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 300, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 300, y: 100 } });
// Continue to subject
await page.getByRole('button', { name: 'Continue' }).click();
@@ -122,7 +122,7 @@ test.describe('[DOCUMENT_FLOW]: Duplicate Recipients', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 100, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 100, y: 100 } });
// Save the document by going to subject
await page.getByRole('button', { name: 'Continue' }).click();
@@ -149,7 +149,7 @@ test.describe('[DOCUMENT_FLOW]: Duplicate Recipients', () => {
await page.getByText('Test Recipient Duplicate (test@example.com)').first().click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 200, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 200, y: 100 } });
// Complete the flow
await page.getByRole('button', { name: 'Continue' }).click();
@@ -270,24 +270,24 @@ test.describe('[DOCUMENT_FLOW]: Duplicate Recipients', () => {
// Add signature for first recipient
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 100, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 100, y: 100 } });
// Add name field for second recipient
await page.getByRole('combobox').first().click();
await page.getByText('Approver Role (signer@example.com)').first().click();
await page.getByRole('button', { name: 'Name' }).click();
await page.locator('canvas').click({ position: { x: 200, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 200, y: 100 } });
// Add date field for second recipient
await page.getByRole('button', { name: 'Date' }).click();
await page.locator('canvas').click({ position: { x: 200, y: 150 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 200, y: 150 } });
// If second recipient is still a SIGNER (role change wasn't available),
// add a signature field for them to pass validation
if (!secondRecipientIsApprover) {
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 200, y: 200 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 200, y: 200 } });
}
// Complete the document
@@ -349,7 +349,7 @@ test.describe('[DOCUMENT_FLOW]: Duplicate Recipients', () => {
// Add another field to the second duplicate
await page.getByRole('button', { name: 'Name' }).click();
await page.locator('canvas').click({ position: { x: 250, y: 150 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 250, y: 150 } });
// Save changes
await page.getByRole('button', { name: 'Continue' }).click();
@@ -9,6 +9,7 @@ import {
import { DateTime } from 'luxon';
import path from 'node:path';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { prisma } from '@documenso/prisma';
import {
seedBlankDocument,
@@ -92,7 +93,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document', async ({ page }) =>
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -100,7 +101,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document', async ({ page }) =>
});
await page.getByRole('button', { name: 'Email' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -158,7 +159,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -166,7 +167,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
});
await page.getByRole('button', { name: 'Email' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -177,7 +178,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
await page.getByText('User 2 (user2@example.com)').click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 500,
y: 100,
@@ -185,7 +186,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
});
await page.getByRole('button', { name: 'Email' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 500,
y: 200,
@@ -256,7 +257,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
await page.getByRole('option', { name: 'User 1 (user1@example.com)' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -264,7 +265,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
});
await page.getByRole('button', { name: 'Email' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -275,7 +276,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
await page.getByRole('option', { name: 'User 3 (user3@example.com)' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 500,
y: 100,
@@ -283,7 +284,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie
});
await page.getByRole('button', { name: 'Email' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 500,
y: 200,
@@ -576,7 +577,7 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
}
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100 * i,
@@ -1,13 +1,12 @@
import { createCanvas } from '@napi-rs/canvas';
import type { TestInfo } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { DocumentStatus, EnvelopeType } from '@prisma/client';
import fs from 'node:fs';
import path from 'node:path';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import pixelMatch from 'pixelmatch';
import { PNG } from 'pngjs';
import { pdfToImages } from '@documenso/lib/server-only/ai/pdf-to-images';
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
import { prisma } from '@documenso/prisma';
import { seedAlignmentTestDocument } from '@documenso/prisma/seed/initial-seed';
@@ -378,39 +377,14 @@ test.skip('download envelope images', async ({ page }) => {
});
async function renderPdfToImage(pdfBytes: Uint8Array) {
const loadingTask = pdfjsLib.getDocument({ data: pdfBytes });
const pdf = await loadingTask.promise;
// Increase for higher resolution
const scale = 4;
return await Promise.all(
Array.from({ length: pdf.numPages }, async (_, index) => {
const page = await pdf.getPage(index + 1);
const viewport = page.getViewport({ scale });
const canvas = createCanvas(viewport.width, viewport.height);
const canvasContext = canvas.getContext('2d');
canvasContext.imageSmoothingEnabled = false;
await page.render({
// @ts-expect-error @napi-rs/canvas satisfies runtime requirements for pdfjs
canvas,
// @ts-expect-error @napi-rs/canvas satisfies runtime requirements for pdfjs
canvasContext,
viewport,
}).promise;
return {
image: await canvas.encode('png'),
// Rounded down because the certificate page somehow gives dimensions with decimals
width: Math.floor(viewport.width),
height: Math.floor(viewport.height),
};
}),
);
return (await pdfToImages(pdfBytes, { scale, imageFormat: 'png' })).map((image) => ({
image: image.image,
width: Math.floor(image.scaledWidth),
height: Math.floor(image.scaledHeight),
}));
}
type CompareSignedPdfWithImagesOptions = {
@@ -42,21 +42,21 @@ const completeTemplateFlowWithDuplicateRecipients = async (options: {
// Step 3: Add fields for each recipient instance
// Add signature field for first instance
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 100, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 100, y: 100 } });
// Switch to second instance and add their field
await page.getByRole('combobox').first().click();
await page.getByText('Second Instance').first().click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 200, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 200, y: 100 } });
// Switch to different recipient and add their fields
await page.getByRole('combobox').first().click();
await page.getByText('Different Recipient').first().click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 300, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 300, y: 100 } });
await page.getByRole('button', { name: 'Name' }).click();
await page.locator('canvas').click({ position: { x: 300, y: 150 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 300, y: 150 } });
// Save template
await page.getByRole('button', { name: 'Save Template' }).click();
@@ -209,17 +209,17 @@ test.describe('[TEMPLATE_FLOW]: Duplicate Recipients', () => {
// Add fields for each recipient
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({ position: { x: 100, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 100, y: 100 } });
await page.getByRole('combobox').first().click();
await page.getByText('Duplicate Recipient 2').first().click();
await page.getByRole('button', { name: 'Date' }).click();
await page.locator('canvas').click({ position: { x: 200, y: 100 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 200, y: 100 } });
await page.getByRole('combobox').first().click();
await page.getByText('Different Recipient').first().click();
await page.getByRole('button', { name: 'Name' }).click();
await page.locator('canvas').click({ position: { x: 100, y: 200 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 100, y: 200 } });
// Save template
await page.getByRole('button', { name: 'Save Template' }).click();
@@ -272,7 +272,7 @@ test.describe('[TEMPLATE_FLOW]: Duplicate Recipients', () => {
await page.getByRole('combobox').first().click();
await page.getByRole('option', { name: 'First Instance' }).first().click();
await page.getByRole('button', { name: 'Name' }).click();
await page.locator('canvas').click({ position: { x: 100, y: 300 } });
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({ position: { x: 100, y: 300 } });
await page.waitForTimeout(2500);
@@ -1,6 +1,7 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
@@ -47,7 +48,7 @@ test.describe('AutoSave Fields Step', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -55,7 +56,7 @@ test.describe('AutoSave Fields Step', () => {
});
await page.getByRole('button', { name: 'Text' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -75,7 +76,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 500,
@@ -110,7 +111,7 @@ test.describe('AutoSave Fields Step', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -118,7 +119,7 @@ test.describe('AutoSave Fields Step', () => {
});
await page.getByRole('button', { name: 'Text' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -138,7 +139,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 500,
@@ -179,7 +180,7 @@ test.describe('AutoSave Fields Step', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -187,7 +188,7 @@ test.describe('AutoSave Fields Step', () => {
});
await page.getByRole('button', { name: 'Text' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,
@@ -207,7 +208,7 @@ test.describe('AutoSave Fields Step', () => {
await page.getByRole('option', { name: 'Recipient 2 (recipient2@documenso.com)' }).click();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 500,
@@ -250,7 +251,7 @@ test.describe('AutoSave Fields Step', () => {
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
await page.getByRole('button', { name: 'Signature' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 100,
@@ -258,7 +259,7 @@ test.describe('AutoSave Fields Step', () => {
});
await page.getByRole('button', { name: 'Text' }).click();
await page.locator('canvas').click({
await page.locator(PDF_VIEWER_PAGE_SELECTOR).click({
position: {
x: 100,
y: 200,