From c8e254aff197cb396793be548418884f278dcaab Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Tue, 18 Nov 2025 21:05:37 +0000 Subject: [PATCH] chore: remove duplicates --- .../envelope-editor-fields-page.tsx | 2 +- .../server/api/document-analysis/index.ts | 116 ++---------------- .../server/api/document-analysis/types.ts | 2 +- apps/remix/server/router.ts | 2 +- .../image/resize-and-compress-image.ts | 15 +++ .../server-only/pdf/render-pdf-to-image.ts | 61 +++++++++ packages/lib/utils/recipients.ts | 13 ++ 7 files changed, 104 insertions(+), 107 deletions(-) create mode 100644 packages/lib/server-only/image/resize-and-compress-image.ts create mode 100644 packages/lib/server-only/pdf/render-pdf-to-image.ts diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx index d2f7d09ae..d44dd6eeb 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx @@ -12,7 +12,7 @@ import { match } from 'ts-pattern'; import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider'; import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider'; import { getPageCanvasRefs } from '@documenso/lib/client-only/utils/page-canvas-registry'; -import type { TDetectedFormField } from '@documenso/lib/types/ai'; +import type { TDetectedFormField } from '@documenso/lib/types/document-analysis'; import type { TCheckboxFieldMeta, TDateFieldMeta, diff --git a/apps/remix/server/api/document-analysis/index.ts b/apps/remix/server/api/document-analysis/index.ts index 06921e245..7f63c3235 100644 --- a/apps/remix/server/api/document-analysis/index.ts +++ b/apps/remix/server/api/document-analysis/index.ts @@ -1,108 +1,30 @@ -// sort-imports-ignore - -// ---- PATCH pdfjs-dist's canvas require BEFORE importing it ---- -import { createRequire } from 'node:module'; -import { fileURLToPath } from 'node:url'; -import { Canvas, Image } from 'skia-canvas'; - -const require = createRequire(import.meta.url || fileURLToPath(new URL('.', import.meta.url))); -const Module = require('node:module'); - -const originalRequire = Module.prototype.require; -Module.prototype.require = function (path: string) { - if (path === 'canvas') { - return { - createCanvas: (width: number, height: number) => new Canvas(width, height), - Image, // needed by pdfjs-dist - }; - } - // eslint-disable-next-line prefer-rest-params, @typescript-eslint/consistent-type-assertions - return originalRequire.apply(this, arguments as unknown as [string]); -}; - -// Use dynamic require to bypass Vite SSR transformation -// eslint-disable-next-line @typescript-eslint/no-var-requires -const pdfjsLib = require('pdfjs-dist/legacy/build/pdf.js'); - -import { mkdir, writeFile } from 'node:fs/promises'; -import { join } from 'node:path'; - import { generateObject } from 'ai'; import { Hono } from 'hono'; -import sharp from 'sharp'; -import { z } from 'zod'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { Canvas, Image } from 'skia-canvas'; import { getSession } from '@documenso/auth/server/lib/utils/get-session'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { resizeAndCompressImage } from '@documenso/lib/server-only/image/resize-and-compress-image'; +import { renderPdfToImage } from '@documenso/lib/server-only/pdf/render-pdf-to-image'; import { getTeamById } from '@documenso/lib/server-only/team/get-team'; import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server'; import { env } from '@documenso/lib/utils/env'; +import { resolveRecipientEmail } from '@documenso/lib/utils/recipients'; import { prisma } from '@documenso/prisma'; -import { ANALYZE_RECIPIENTS_PROMPT, DETECT_OBJECTS_PROMPT } from './ai.prompts'; -import type { HonoEnv } from '../router'; +import type { HonoEnv } from '../../router'; +import { ANALYZE_RECIPIENTS_PROMPT, DETECT_OBJECTS_PROMPT } from './prompts'; import { type TAnalyzeRecipientsResponse, - type TDetectedRecipient, type TDetectFormFieldsResponse, + type TDetectedRecipient, ZAnalyzeRecipientsRequestSchema, - ZDetectedRecipientLLMSchema, - ZDetectedFormFieldSchema, ZDetectFormFieldsRequestSchema, -} from './ai.types'; - -const renderPdfToImage = async (pdfBytes: Uint8Array) => { - const loadingTask = pdfjsLib.getDocument({ data: pdfBytes }); - const pdf = await loadingTask.promise; - - try { - const scale = 4; - - const pages = await Promise.all( - Array.from({ length: pdf.numPages }, async (_, index) => { - const pageNumber = index + 1; - const page = await pdf.getPage(pageNumber); - - try { - const viewport = page.getViewport({ scale }); - - const virtualCanvas = new Canvas(viewport.width, viewport.height); - const context = virtualCanvas.getContext('2d'); - context.imageSmoothingEnabled = false; - - await page.render({ canvasContext: context, viewport }).promise; - - return { - image: await virtualCanvas.toBuffer('png'), - pageNumber, - width: Math.floor(viewport.width), - height: Math.floor(viewport.height), - }; - } finally { - page.cleanup(); - } - }), - ); - - return pages; - } finally { - await pdf.destroy(); - } -}; - -const resizeAndCompressImage = async (imageBuffer: Buffer): Promise => { - const metadata = await sharp(imageBuffer).metadata(); - const originalWidth = metadata.width || 0; - - if (originalWidth > 1000) { - return await sharp(imageBuffer) - .resize({ width: 1000, withoutEnlargement: true }) - .jpeg({ quality: 70 }) - .toBuffer(); - } - - return await sharp(imageBuffer).jpeg({ quality: 70 }).toBuffer(); -}; + ZDetectedFormFieldSchema, + ZDetectedRecipientLLMSchema, +} from './types'; type FieldDetectionRecipient = { id: number; @@ -203,20 +125,6 @@ const runFormFieldDetection = async ( // Limit recipient detection to first 3 pages for performance and cost efficiency const MAX_PAGES_FOR_RECIPIENT_ANALYSIS = 3; -const recipientEmailSchema = z.string().email(); - -const resolveRecipientEmail = (candidateEmail: string | undefined) => { - if (candidateEmail) { - const trimmedEmail = candidateEmail.trim(); - - if (recipientEmailSchema.safeParse(trimmedEmail).success) { - return trimmedEmail; - } - } - - return undefined; -}; - const authorizeDocumentAccess = async (envelopeId: string, userId: number) => { const envelope = await prisma.envelope.findUnique({ where: { id: envelopeId }, diff --git a/apps/remix/server/api/document-analysis/types.ts b/apps/remix/server/api/document-analysis/types.ts index d8741dffa..67bba42b0 100644 --- a/apps/remix/server/api/document-analysis/types.ts +++ b/apps/remix/server/api/document-analysis/types.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import type { TDetectedFormField } from '@documenso/lib/types/ai'; +import type { TDetectedFormField } from '@documenso/lib/types/document-analysis'; export const ZGenerateTextRequestSchema = z.object({ prompt: z.string().min(1, 'Prompt is required').max(5000, 'Prompt is too long'), diff --git a/apps/remix/server/router.ts b/apps/remix/server/router.ts index b82b508cf..0ed2100d6 100644 --- a/apps/remix/server/router.ts +++ b/apps/remix/server/router.ts @@ -14,7 +14,7 @@ import { getIpAddress } from '@documenso/lib/universal/get-ip-address'; import { logger } from '@documenso/lib/utils/logger'; import { openApiDocument } from '@documenso/trpc/server/open-api'; -import { aiRoute } from './api/ai'; +import { aiRoute } from './api/document-analysis/index'; import { downloadRoute } from './api/download/download'; import { filesRoute } from './api/files/files'; import { type AppContext, appContext } from './context'; diff --git a/packages/lib/server-only/image/resize-and-compress-image.ts b/packages/lib/server-only/image/resize-and-compress-image.ts new file mode 100644 index 000000000..008b8a5a7 --- /dev/null +++ b/packages/lib/server-only/image/resize-and-compress-image.ts @@ -0,0 +1,15 @@ +import sharp from 'sharp'; + +export const resizeAndCompressImage = async (imageBuffer: Buffer): Promise => { + const metadata = await sharp(imageBuffer).metadata(); + const originalWidth = metadata.width || 0; + + if (originalWidth > 1000) { + return await sharp(imageBuffer) + .resize({ width: 1000, withoutEnlargement: true }) + .jpeg({ quality: 70 }) + .toBuffer(); + } + + return await sharp(imageBuffer).jpeg({ quality: 70 }).toBuffer(); +}; diff --git a/packages/lib/server-only/pdf/render-pdf-to-image.ts b/packages/lib/server-only/pdf/render-pdf-to-image.ts new file mode 100644 index 000000000..841222367 --- /dev/null +++ b/packages/lib/server-only/pdf/render-pdf-to-image.ts @@ -0,0 +1,61 @@ +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +import { Canvas, Image } from 'skia-canvas'; + +const require = createRequire(import.meta.url || fileURLToPath(new URL('.', import.meta.url))); +const Module = require('node:module'); + +const originalRequire = Module.prototype.require; +Module.prototype.require = function (path: string) { + if (path === 'canvas') { + return { + createCanvas: (width: number, height: number) => new Canvas(width, height), + Image, // needed by pdfjs-dist + }; + } + // eslint-disable-next-line prefer-rest-params, @typescript-eslint/consistent-type-assertions + return originalRequire.apply(this, arguments as unknown as [string]); +}; + +// Use dynamic require to bypass Vite SSR transformation +// eslint-disable-next-line @typescript-eslint/no-var-requires +const pdfjsLib = require('pdfjs-dist/legacy/build/pdf.js'); + +export const renderPdfToImage = async (pdfBytes: Uint8Array) => { + const loadingTask = pdfjsLib.getDocument({ data: pdfBytes }); + const pdf = await loadingTask.promise; + + try { + const scale = 4; + + const pages = await Promise.all( + Array.from({ length: pdf.numPages }, async (_, index) => { + const pageNumber = index + 1; + const page = await pdf.getPage(pageNumber); + + try { + const viewport = page.getViewport({ scale }); + + const virtualCanvas = new Canvas(viewport.width, viewport.height); + const context = virtualCanvas.getContext('2d'); + context.imageSmoothingEnabled = false; + + await page.render({ canvasContext: context, viewport }).promise; + + return { + image: await virtualCanvas.toBuffer('png'), + pageNumber, + width: Math.floor(viewport.width), + height: Math.floor(viewport.height), + }; + } finally { + page.cleanup(); + } + }), + ); + + return pages; + } finally { + await pdf.destroy(); + } +}; diff --git a/packages/lib/utils/recipients.ts b/packages/lib/utils/recipients.ts index 4d14f5ac7..e7eb788d1 100644 --- a/packages/lib/utils/recipients.ts +++ b/packages/lib/utils/recipients.ts @@ -1,5 +1,6 @@ import type { Envelope } from '@prisma/client'; import { type Field, type Recipient, RecipientRole, SigningStatus } from '@prisma/client'; +import { z } from 'zod'; import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app'; import { extractLegacyIds } from '../universal/id'; @@ -8,6 +9,18 @@ const UNKNOWN_RECIPIENT_NAME_PLACEHOLDER = ''; export const formatSigningLink = (token: string) => `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${token}`; +export const resolveRecipientEmail = (candidateEmail: string | undefined | null) => { + if (candidateEmail) { + const trimmedEmail = candidateEmail.trim(); + + if (z.string().email().safeParse(trimmedEmail).success) { + return trimmedEmail; + } + } + + return undefined; +}; + /** * Whether a recipient can be modified by the document owner. */