feat(multi-pdf): enhance PDF multi-tool functionality to allow drag n drop of PDF and images

fix(ocr): pin font CDN to rawcdn.githack.com with commit SHAs
This commit is contained in:
alam00000
2026-05-29 20:05:50 +05:30
parent 1534ce4d85
commit 1d9b7c2a07
30 changed files with 751 additions and 121 deletions
+84
View File
@@ -0,0 +1,84 @@
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { preprocessImageFile } from './image-input-utils.js';
export interface EmbeddableImage {
bytes: Uint8Array;
format: 'jpg' | 'png';
}
export async function normalizeImageToEmbeddable(
file: File
): Promise<EmbeddableImage> {
const processed = await preprocessImageFile(file);
const name = processed.name.toLowerCase();
const isJpg =
processed.type === 'image/jpeg' ||
name.endsWith('.jpg') ||
name.endsWith('.jpeg');
const isPng = processed.type === 'image/png' || name.endsWith('.png');
if (isJpg) {
return {
bytes: new Uint8Array(await processed.arrayBuffer()),
format: 'jpg',
};
}
if (isPng) {
return {
bytes: new Uint8Array(await processed.arrayBuffer()),
format: 'png',
};
}
const url = URL.createObjectURL(processed);
try {
const img = await new Promise<HTMLImageElement>((resolve, reject) => {
const i = new Image();
i.onload = () => resolve(i);
i.onerror = () =>
reject(new Error(`Failed to decode image: ${processed.name}`));
i.src = url;
});
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Canvas context unavailable');
ctx.drawImage(img, 0, 0);
const blob = await new Promise<Blob>((resolve, reject) => {
canvas.toBlob(
(b) => (b ? resolve(b) : reject(new Error('Canvas toBlob failed'))),
'image/png'
);
});
return { bytes: new Uint8Array(await blob.arrayBuffer()), format: 'png' };
} finally {
URL.revokeObjectURL(url);
}
}
export async function convertImagesToPdfFile(images: File[]): Promise<File> {
const pdfDoc = await PDFLibDocument.create();
for (const img of images) {
const { bytes, format } = await normalizeImageToEmbeddable(img);
const embedded =
format === 'jpg'
? await pdfDoc.embedJpg(bytes)
: await pdfDoc.embedPng(bytes);
const page = pdfDoc.addPage([embedded.width, embedded.height]);
page.drawImage(embedded, {
x: 0,
y: 0,
width: embedded.width,
height: embedded.height,
});
}
const pdfBytes = await pdfDoc.save();
const filename =
images.length === 1
? images[0].name.replace(/\.[^.]+$/, '.pdf')
: `images-${Date.now()}.pdf`;
return new File([new Uint8Array(pdfBytes)], filename, {
type: 'application/pdf',
});
}
+25
View File
@@ -0,0 +1,25 @@
import { isValidImageFile } from './image-input-utils.js';
export interface PartitionedFiles {
pdfFiles: File[];
imageFiles: File[];
skipped: string[];
}
export function partitionIncomingFiles(rawFiles: File[]): PartitionedFiles {
const pdfFiles: File[] = [];
const imageFiles: File[] = [];
const skipped: string[] = [];
for (const f of rawFiles) {
if (f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')) {
pdfFiles.push(f);
} else if (isValidImageFile(f)) {
imageFiles.push(f);
} else {
skipped.push(f.name);
}
}
return { pdfFiles, imageFiles, skipped };
}