diff --git a/apps/remix/app/components/embed/authoring/configure-document-upload.tsx b/apps/remix/app/components/embed/authoring/configure-document-upload.tsx index d1c12b989..aa1680615 100644 --- a/apps/remix/app/components/embed/authoring/configure-document-upload.tsx +++ b/apps/remix/app/components/embed/authoring/configure-document-upload.tsx @@ -9,7 +9,7 @@ import { useFormContext } from 'react-hook-form'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { PDF_IMAGE_RENDER_SCALE } from '@documenso/lib/constants/pdf-viewer'; -import { pdfToImages } from '@documenso/lib/server-only/ai/pdf-to-images'; +import { pdfToImagesClientSide } from '@documenso/lib/server-only/ai/pdf-to-images.client'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { @@ -54,7 +54,7 @@ export const ConfigureDocumentUpload = ({ isSubmitting = false }: ConfigureDocum const arrayBuffer = await file.arrayBuffer(); const uint8Array = new Uint8Array(arrayBuffer); - const pdfImages = await pdfToImages(uint8Array, { + const pdfImages = await pdfToImagesClientSide(uint8Array, { scale: PDF_IMAGE_RENDER_SCALE, }); @@ -64,11 +64,7 @@ export const ConfigureDocumentUpload = ({ isSubmitting = false }: ConfigureDocum type: file.type, size: file.size, data: uint8Array, // Store as UInt8Array - images: pdfImages.map((image) => ({ - width: image.originalWidth, - height: image.originalHeight, - image: image.image, - })), + images: pdfImages, }); // Auto-populate title if it's empty diff --git a/apps/remix/app/components/embed/authoring/configure-document-view.types.ts b/apps/remix/app/components/embed/authoring/configure-document-view.types.ts index f661887e2..5e322d66c 100644 --- a/apps/remix/app/components/embed/authoring/configure-document-view.types.ts +++ b/apps/remix/app/components/embed/authoring/configure-document-view.types.ts @@ -50,7 +50,7 @@ export const ZConfigureEmbedFormSchema = z.object({ .object({ width: z.number(), height: z.number(), - image: z.instanceof(Buffer), + image: z.string(), }) .array(), }) diff --git a/packages/lib/server-only/ai/pdf-to-images.client.ts b/packages/lib/server-only/ai/pdf-to-images.client.ts new file mode 100644 index 000000000..19b3dc9bf --- /dev/null +++ b/packages/lib/server-only/ai/pdf-to-images.client.ts @@ -0,0 +1,73 @@ +import pMap from 'p-map'; +import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs'; + +pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( + 'pdfjs-dist/legacy/build/pdf.worker.min.mjs', + import.meta.url, +).toString(); + +export type PdfToImagesOptions = { + scale?: number; +}; + +export type PdfImageResult = { + pageIndex: number; + image: string; + width: number; + height: number; +}; + +export const pdfToImagesClientSide = async ( + pdfBytes: Uint8Array, + options: PdfToImagesOptions = {}, +): Promise => { + const { scale = 2 } = options; + + const task = pdfjsLib.getDocument({ + data: pdfBytes, + }); + + const pdf = await task.promise; + + const images = await pMap( + Array.from({ length: pdf.numPages }), + async (_, pageIndex) => { + const pageNumber = pageIndex + 1; + const page = await pdf.getPage(pageNumber); + + const viewport = page.getViewport({ scale }); + + const canvas = document.createElement('canvas'); + canvas.width = Math.floor(viewport.width); + canvas.height = Math.floor(viewport.height); + + const context = canvas.getContext('2d'); + if (!context) { + throw new Error('Failed to get 2D canvas context'); + } + + await page.render({ + canvasContext: context, + viewport, + canvas, + }).promise; + + const imageBase64 = canvas.toDataURL('image/jpeg'); + + page.cleanup(); + + return { + pageIndex, + image: imageBase64, + width: canvas.width, + height: canvas.height, + }; + }, + { concurrency: 50 }, + ); + + await pdf.destroy(); + await task.destroy(); + + return images; +}; diff --git a/packages/ui/components/pdf-viewer/pdf-viewer.tsx b/packages/ui/components/pdf-viewer/pdf-viewer.tsx index 3fcfba3d9..dcd05260e 100644 --- a/packages/ui/components/pdf-viewer/pdf-viewer.tsx +++ b/packages/ui/components/pdf-viewer/pdf-viewer.tsx @@ -19,7 +19,7 @@ import { useVirtualList } from '../virtual-list/use-virtual-list'; import { PdfViewerErrorState, PdfViewerLoadingState } from './pdf-viewer-states'; export type OverrideImage = { - image: Buffer; + image: string; width: number; height: number; }; @@ -78,7 +78,7 @@ export const PDFViewer = ({ const derivedPages = useMemo((): PageMeta[] => { if (overrideImages) { return overrideImages.map((image) => ({ - imageUrl: `data:image/jpeg;base64,${image.image.toString('base64')}`, + imageUrl: image.image, width: image.width, height: image.height, documentDataId: '',