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
@@ -162,8 +162,8 @@ export const detectFieldsFromPdf = async ({
// Mask existing fields on the image
const maskedImage = await maskFieldsOnImage({
image: page.image,
width: page.width,
height: page.height,
width: page.scaledWidth,
height: page.scaledHeight,
fields: fieldsOnPage,
});
+95 -32
View File
@@ -1,7 +1,11 @@
import pMap from 'p-map';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import type { PDFDocumentProxy } from 'pdfjs-dist/types/src/display/api';
import type { ExportFormat } from 'skia-canvas';
import { Canvas, Image, Path2D } from 'skia-canvas';
import { PDF_IMAGE_RENDER_SCALE } from '../../constants/pdf-viewer';
// @ts-expect-error napi-rs/canvas satisfies the requirements
globalThis.Path2D = Path2D;
// @ts-expect-error napi-rs/canvas satisfies the requirements
@@ -42,10 +46,17 @@ class SkiaCanvasFactory {
export type PdfToImagesOptions = {
scale?: number;
/**
* The format of the images to return.
*
* Defaults to 'jpeg'.
*/
imageFormat?: ExportFormat;
};
export const pdfToImages = async (pdfBytes: Uint8Array, options: PdfToImagesOptions = {}) => {
const { scale = 2 } = options;
const { scale = PDF_IMAGE_RENDER_SCALE, imageFormat = 'jpeg' } = options;
const task = await pdfjsLib.getDocument({
data: pdfBytes,
@@ -56,37 +67,7 @@ export const pdfToImages = async (pdfBytes: Uint8Array, options: PdfToImagesOpti
const images = await pMap(
Array.from({ length: pdf.numPages }),
async (_, index) => {
const pageNumber = index + 1;
const page = await pdf.getPage(pageNumber);
const viewport = page.getViewport({ scale });
const canvas = new Canvas(viewport.width, viewport.height);
canvas.gpu = false;
const canvasContext = canvas.getContext('2d');
await page.render({
// @ts-expect-error napi-rs/canvas satifies the requirements
canvas,
// @ts-expect-error napi-rs/canvas satifies the requirements
canvasContext,
viewport,
}).promise;
const result = {
pageNumber,
image: await canvas.toBuffer('jpeg'),
width: Math.floor(viewport.width),
height: Math.floor(viewport.height),
mimeType: 'image/jpeg',
};
void page.cleanup();
return result;
},
async (_, pageIndex) => getPdfPageAsImage({ pdf, pageIndex, scale, imageFormat }),
{ concurrency: 10 },
);
@@ -95,3 +76,85 @@ export const pdfToImages = async (pdfBytes: Uint8Array, options: PdfToImagesOpti
return images;
};
export type PdfToImageOptions = {
scale?: number;
pageIndex: number;
/**
* The format of the image to return.
* Defaults to 'jpeg'.
*/
imageFormat?: ExportFormat;
};
export const pdfToImage = async (pdfBytes: Uint8Array, options: PdfToImageOptions) => {
const { scale = PDF_IMAGE_RENDER_SCALE, pageIndex, imageFormat = 'jpeg' } = options;
if (pageIndex !== undefined && pageIndex < 0) {
throw new Error('Page index must be greater than 0');
}
const task = await pdfjsLib.getDocument({
data: pdfBytes,
CanvasFactory: SkiaCanvasFactory,
});
const pdf = await task.promise;
const image = await getPdfPageAsImage({ pdf, pageIndex, scale, imageFormat });
void pdf.destroy().catch((e) => console.error(e));
void task.destroy().catch((e) => console.error(e));
return image;
};
type GetPdfPageAsImageOptions = {
pdf: PDFDocumentProxy;
pageIndex: number;
scale: number;
imageFormat: ExportFormat;
};
const getPdfPageAsImage = async ({
pdf,
pageIndex,
scale,
imageFormat,
}: GetPdfPageAsImageOptions) => {
const page = await pdf.getPage(pageIndex + 1);
const viewport = page.getViewport({ scale });
const canvas = new Canvas(viewport.width, viewport.height);
canvas.gpu = false;
const canvasContext = canvas.getContext('2d');
await page.render({
// @ts-expect-error napi-rs/canvas satifies the requirements
canvas,
// @ts-expect-error napi-rs/canvas satifies the requirements
canvasContext,
viewport,
}).promise;
const originalViewport = page.getViewport({ scale: 1 });
const result = {
pageIndex,
pageNumber: pageIndex + 1,
image: await canvas.toBuffer(imageFormat),
originalWidth: originalViewport.width,
originalHeight: originalViewport.height,
scale,
scaledWidth: Math.floor(viewport.width),
scaledHeight: Math.floor(viewport.height),
mimeType: 'image/jpeg',
};
void page.cleanup();
return result;
};
@@ -310,7 +310,7 @@ export const sendDocument = async ({
const injectFormValuesIntoDocument = async (
envelope: Envelope,
envelopeItem: Pick<EnvelopeItem, 'id'> & { documentData: DocumentData },
envelopeItem: Pick<EnvelopeItem, 'id'> & { documentData: Omit<DocumentData, 'metadata'> },
) => {
const file = await getFileServerSide(envelopeItem.documentData);