diff --git a/package-lock.json b/package-lock.json index 8da4be38f..a3aab94d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "dependencies": { "@ai-sdk/google-vertex": "3.0.81", "@documenso/prisma": "*", - "@libpdf/core": "^0.2.12", + "@libpdf/core": "^0.3.3", "@lingui/conf": "^5.6.0", "@lingui/core": "^5.6.0", "@prisma/extension-read-replicas": "^0.4.1", @@ -31,6 +31,7 @@ "devDependencies": { "@commitlint/cli": "^20.1.0", "@commitlint/config-conventional": "^20.0.0", + "@datadog/pprof": "^5.13.5", "@lingui/cli": "^5.6.0", "@prisma/client": "^6.19.0", "@trpc/client": "11.8.1", @@ -2852,6 +2853,32 @@ "node": ">=v18" } }, + "node_modules/@datadog/pprof": { + "version": "5.13.5", + "resolved": "https://registry.npmjs.org/@datadog/pprof/-/pprof-5.13.5.tgz", + "integrity": "sha512-W0dvo91ff2EMQI9Vhv8PNM+w1ZWuClm1pBVdLB6y0bMB3+E+wEGg0VD1iNJxsuPbwDt5+yV0u3e4WkqK12Lzlg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "node-gyp-build": "<4.0", + "pprof-format": "^2.2.1", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@datadog/pprof/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, "node_modules/@documenso/api": { "resolved": "packages/api", "link": true @@ -4645,9 +4672,9 @@ "license": "MIT" }, "node_modules/@libpdf/core": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@libpdf/core/-/core-0.2.12.tgz", - "integrity": "sha512-z22SyNEXa8YsCJarJBkgQv4SvvDn0Opw21cNQOQ0Xax9Ys1qjpAyVTSjlGExYVI8bT9b02VNy+nsOcJ79SzsQg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@libpdf/core/-/core-0.3.3.tgz", + "integrity": "sha512-MoyjZ00RPJ1sDgFooerCw3WqXzaaufHFkBYZv6v8qKUaIljdS2MYm1OYvcyV+V1qplo+o8qc0X+0p/JipzJ/Jw==", "license": "MIT", "dependencies": { "@noble/ciphers": "^2.1.1", @@ -29555,6 +29582,18 @@ "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", "license": "MIT" }, + "node_modules/node-gyp-build": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.9.0.tgz", + "integrity": "sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-html-better-parser": { "version": "1.5.8", "resolved": "https://registry.npmjs.org/node-html-better-parser/-/node-html-better-parser-1.5.8.tgz", @@ -31215,6 +31254,13 @@ "node": ">=15.0.0" } }, + "node_modules/pprof-format": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/pprof-format/-/pprof-format-2.2.1.tgz", + "integrity": "sha512-p4tVN7iK19ccDqQv8heyobzUmbHyds4N2FI6aBMcXz6y99MglTWDxIyhFkNaLeEXs6IFUEzT0zya0icbSLLY0g==", + "dev": true, + "license": "MIT" + }, "node_modules/preact": { "version": "10.28.2", "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", diff --git a/package.json b/package.json index 59e650605..bd2ee1d8c 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "devDependencies": { "@commitlint/cli": "^20.1.0", "@commitlint/config-conventional": "^20.0.0", + "@datadog/pprof": "^5.13.5", "@lingui/cli": "^5.6.0", "@prisma/client": "^6.19.0", "@trpc/client": "11.8.1", @@ -86,7 +87,7 @@ "dependencies": { "@ai-sdk/google-vertex": "3.0.81", "@documenso/prisma": "*", - "@libpdf/core": "^0.2.12", + "@libpdf/core": "^0.3.3", "@lingui/conf": "^5.6.0", "@lingui/core": "^5.6.0", "@prisma/extension-read-replicas": "^0.4.1", diff --git a/packages/lib/server-only/pdf/helpers.ts b/packages/lib/server-only/pdf/helpers.ts index 2e9a97369..1a2677fef 100644 --- a/packages/lib/server-only/pdf/helpers.ts +++ b/packages/lib/server-only/pdf/helpers.ts @@ -1,9 +1,45 @@ import { FieldType } from '@prisma/client'; import type { Recipient } from '@prisma/client'; +import path from 'node:path'; +import { FontLibrary } from 'skia-canvas'; import { match } from 'ts-pattern'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +/** + * Ensure all required fonts are registered in the skia-canvas FontLibrary. + * + * Fonts are registered once per process and retained — calling this multiple + * times is a no-op after the first invocation. + */ +export const ensureFontLibrary = () => { + const fontPath = path.join(process.cwd(), 'public/fonts'); + + if (!FontLibrary.has('Caveat')) { + // eslint-disable-next-line react-hooks/rules-of-hooks + FontLibrary.use({ + ['Caveat']: [path.join(fontPath, 'caveat.ttf')], + }); + } + + if (!FontLibrary.has('Inter')) { + // eslint-disable-next-line react-hooks/rules-of-hooks + FontLibrary.use({ + ['Inter']: [path.join(fontPath, 'inter-variablefont_opsz,wght.ttf')], + }); + } + + if (!FontLibrary.has('Noto Sans')) { + // eslint-disable-next-line react-hooks/rules-of-hooks + FontLibrary.use({ + ['Noto Sans']: [path.join(fontPath, 'noto-sans.ttf')], + ['Noto Sans Japanese']: [path.join(fontPath, 'noto-sans-japanese.ttf')], + ['Noto Sans Chinese']: [path.join(fontPath, 'noto-sans-chinese.ttf')], + ['Noto Sans Korean']: [path.join(fontPath, 'noto-sans-korean.ttf')], + }); + } +}; + type RecipientPlaceholderInfo = { email: string; name: string; diff --git a/packages/lib/server-only/pdf/insert-field-in-pdf-v2.ts b/packages/lib/server-only/pdf/insert-field-in-pdf-v2.ts index ccea99714..5794ba87b 100644 --- a/packages/lib/server-only/pdf/insert-field-in-pdf-v2.ts +++ b/packages/lib/server-only/pdf/insert-field-in-pdf-v2.ts @@ -2,13 +2,12 @@ import '../konva/skia-backend'; import Konva from 'konva'; -import path from 'node:path'; import type { Canvas } from 'skia-canvas'; -import { FontLibrary } from 'skia-canvas'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { renderField } from '../../universal/field-renderer/render-field'; +import { ensureFontLibrary } from './helpers'; type InsertFieldInPDFV2Options = { pageWidth: number; @@ -21,16 +20,7 @@ export const insertFieldInPDFV2 = async ({ pageHeight, fields, }: InsertFieldInPDFV2Options) => { - const fontPath = path.join(process.cwd(), 'public/fonts'); - - // eslint-disable-next-line react-hooks/rules-of-hooks - FontLibrary.use({ - ['Caveat']: [path.join(fontPath, 'caveat.ttf')], - ['Noto Sans']: [path.join(fontPath, 'noto-sans.ttf')], - ['Noto Sans Japanese']: [path.join(fontPath, 'noto-sans-japanese.ttf')], - ['Noto Sans Chinese']: [path.join(fontPath, 'noto-sans-chinese.ttf')], - ['Noto Sans Korean']: [path.join(fontPath, 'noto-sans-korean.ttf')], - }); + ensureFontLibrary(); let stage: Konva.Stage | null = new Konva.Stage({ width: pageWidth, height: pageHeight }); let layer: Konva.Layer | null = new Konva.Layer(); diff --git a/packages/lib/server-only/pdf/render-audit-logs.ts b/packages/lib/server-only/pdf/render-audit-logs.ts index 53874e96e..8ff5ffde5 100644 --- a/packages/lib/server-only/pdf/render-audit-logs.ts +++ b/packages/lib/server-only/pdf/render-audit-logs.ts @@ -9,7 +9,6 @@ import { DateTime } from 'luxon'; import fs from 'node:fs'; import path from 'node:path'; import type { Canvas } from 'skia-canvas'; -import { FontLibrary } from 'skia-canvas'; import { Image as SkiaImage } from 'skia-canvas'; import { match } from 'ts-pattern'; import { P } from 'ts-pattern'; @@ -21,6 +20,7 @@ import { RECIPIENT_ROLES_DESCRIPTION } from '../../constants/recipient-roles'; import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs'; import type { TDocumentAuditLog } from '../../types/document-audit-logs'; import { formatDocumentAuditLogAction } from '../../utils/document-audit-logs'; +import { ensureFontLibrary } from './helpers'; export type AuditLogRecipient = { id: number; @@ -575,13 +575,7 @@ export async function renderAuditLogs({ i18n, hidePoweredBy, }: GenerateAuditLogsOptions) { - const fontPath = path.join(process.cwd(), 'public/fonts'); - - // eslint-disable-next-line react-hooks/rules-of-hooks - FontLibrary.use({ - ['Caveat']: [path.join(fontPath, 'caveat.ttf')], - ['Inter']: [path.join(fontPath, 'inter-variablefont_opsz,wght.ttf')], - }); + ensureFontLibrary(); const minimumMargin = 10; diff --git a/packages/lib/server-only/pdf/render-certificate.ts b/packages/lib/server-only/pdf/render-certificate.ts index 9991e447a..32c7556c0 100644 --- a/packages/lib/server-only/pdf/render-certificate.ts +++ b/packages/lib/server-only/pdf/render-certificate.ts @@ -9,7 +9,6 @@ import { DateTime } from 'luxon'; import fs from 'node:fs'; import path from 'node:path'; import type { Canvas } from 'skia-canvas'; -import { FontLibrary } from 'skia-canvas'; import { Image as SkiaImage } from 'skia-canvas'; import { UAParser } from 'ua-parser-js'; import { renderSVG } from 'uqr'; @@ -22,6 +21,7 @@ import { } from '../../constants/recipient-roles'; import type { TDocumentAuditLogBaseSchema } from '../../types/document-audit-logs'; import { svgToPng } from '../../utils/images/svg-to-png'; +import { ensureFontLibrary } from './helpers'; type ColumnWidths = [number, number, number]; @@ -724,13 +724,7 @@ export async function renderCertificate({ pageWidth, pageHeight, }: GenerateCertificateOptions) { - const fontPath = path.join(process.cwd(), 'public/fonts'); - - // eslint-disable-next-line react-hooks/rules-of-hooks - FontLibrary.use({ - ['Caveat']: [path.join(fontPath, 'caveat.ttf')], - ['Inter']: [path.join(fontPath, 'inter-variablefont_opsz,wght.ttf')], - }); + ensureFontLibrary(); const minimumMargin = 10;