mirror of
https://github.com/documenso/documenso.git
synced 2026-07-24 00:43:40 +10:00
feat: add pdf image renderer
This commit is contained in:
@@ -1,13 +1,19 @@
|
||||
import { PDF } from '@libpdf/core';
|
||||
import { DocumentDataType } from '@prisma/client';
|
||||
import { base64 } from '@scure/base';
|
||||
import pMap from 'p-map';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import type { TDocumentDataMeta } from '@documenso/lib/types/document-data';
|
||||
import { ZDocumentDataMetaSchema } from '@documenso/lib/types/document-data';
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError } from '../../errors/app-error';
|
||||
import { pdfToImages } from '../../server-only/ai/pdf-to-images';
|
||||
import { createDocumentData } from '../../server-only/document-data/create-document-data';
|
||||
import { normalizePdf } from '../../server-only/pdf/normalize-pdf';
|
||||
import { getEnvelopeItemPageImageS3Key } from '../../utils/envelope-images';
|
||||
import { uploadS3File } from './server-actions';
|
||||
|
||||
type File = {
|
||||
@@ -41,7 +47,67 @@ export const putPdfFileServerSide = async (file: File) => {
|
||||
|
||||
const { type, data } = await putFileServerSide(file);
|
||||
|
||||
return await createDocumentData({ type, data });
|
||||
const newDocumentData = await createDocumentData({ type, data });
|
||||
|
||||
void extractAndStorePdfImages(arrayBuffer, newDocumentData.id);
|
||||
|
||||
return newDocumentData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract and stores page images and metadata to S3.
|
||||
*/
|
||||
export const extractAndStorePdfImages = async (
|
||||
arrayBuffer: ArrayBuffer,
|
||||
documentDataId: string,
|
||||
) => {
|
||||
const images = await pdfToImages(new Uint8Array(arrayBuffer));
|
||||
|
||||
const pageMetadata = images.map((image) => ({
|
||||
originalWidth: image.originalWidth,
|
||||
originalHeight: image.originalHeight,
|
||||
scale: image.scale,
|
||||
scaledWidth: image.scaledWidth,
|
||||
scaledHeight: image.scaledHeight,
|
||||
}));
|
||||
|
||||
const documentDataMetadata = ZDocumentDataMetaSchema.parse({
|
||||
pages: pageMetadata,
|
||||
} satisfies TDocumentDataMeta);
|
||||
|
||||
const updatedDocumentData = await prisma.documentData.update({
|
||||
where: { id: documentDataId },
|
||||
data: {
|
||||
metadata: documentDataMetadata,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
env('NEXT_PUBLIC_UPLOAD_TRANSPORT') === 's3' &&
|
||||
updatedDocumentData.type === DocumentDataType.S3_PATH
|
||||
) {
|
||||
await pMap(
|
||||
images,
|
||||
async (image) => {
|
||||
const imageBlob = new Blob([new Uint8Array(image.image)], { type: 'image/jpeg' });
|
||||
|
||||
const pageIndex = image.pageIndex;
|
||||
|
||||
const s3Key = getEnvelopeItemPageImageS3Key(updatedDocumentData.data, pageIndex);
|
||||
|
||||
const imageFile = new File([imageBlob], `${pageIndex}.jpeg`, {
|
||||
type: 'image/jpeg',
|
||||
});
|
||||
|
||||
const { key } = await uploadS3File(imageFile, s3Key);
|
||||
|
||||
return key;
|
||||
},
|
||||
{ concurrency: 100 },
|
||||
);
|
||||
}
|
||||
|
||||
return pageMetadata;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -63,10 +129,14 @@ export const putNormalizedPdfFileServerSide = async (
|
||||
arrayBuffer: async () => Promise.resolve(normalized),
|
||||
});
|
||||
|
||||
return await createDocumentData({
|
||||
const newDocumentData = await createDocumentData({
|
||||
type: documentData.type,
|
||||
data: documentData.data,
|
||||
});
|
||||
|
||||
void extractAndStorePdfImages(normalized, newDocumentData.id);
|
||||
|
||||
return newDocumentData;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -91,13 +91,13 @@ export const getPresignGetUrl = async (key: string) => {
|
||||
/**
|
||||
* Uploads a file to S3.
|
||||
*/
|
||||
export const uploadS3File = async (file: File) => {
|
||||
export const uploadS3File = async (file: File, keyOverride?: string) => {
|
||||
const client = getS3Client();
|
||||
|
||||
// Get the basename and extension for the file
|
||||
const { name, ext } = path.parse(file.name);
|
||||
|
||||
const key = `${alphaid(12)}/${slugify(name)}${ext}`;
|
||||
const key = keyOverride ?? `${alphaid(12)}/${slugify(name)}${ext}`;
|
||||
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
|
||||
@@ -124,6 +124,28 @@ export const deleteS3File = async (key: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Be careful about using this function as we don't allow the
|
||||
* frontend to ever pull a file from S3 directly.
|
||||
*/
|
||||
export const UNSAFE_getS3File = async (key: string) => {
|
||||
// Additional safeguard to prevent path traversal.
|
||||
if (key.includes('..') || key.startsWith('/')) {
|
||||
throw new Error('Invalid S3 key');
|
||||
}
|
||||
|
||||
const client = getS3Client();
|
||||
|
||||
const response = await client.send(
|
||||
new GetObjectCommand({
|
||||
Bucket: env('NEXT_PRIVATE_UPLOAD_BUCKET'),
|
||||
Key: key,
|
||||
}),
|
||||
);
|
||||
|
||||
return response.Body;
|
||||
};
|
||||
|
||||
const getS3Client = () => {
|
||||
const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user