fix: add client side pdf render

This commit is contained in:
David Nguyen
2026-01-27 15:12:59 +11:00
parent 4fb3c2cb0f
commit 8186d2817f
4 changed files with 79 additions and 10 deletions
@@ -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
@@ -50,7 +50,7 @@ export const ZConfigureEmbedFormSchema = z.object({
.object({
width: z.number(),
height: z.number(),
image: z.instanceof(Buffer),
image: z.string(),
})
.array(),
})
@@ -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<PdfImageResult[]> => {
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;
};
@@ -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: '',