first commit
This commit is contained in:
140
src/pdf/pdfService.ts
Normal file
140
src/pdf/pdfService.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { PDFDocument, degrees } from 'pdf-lib';
|
||||
import type { PdfFile, SplitResult, Range } from './pdfTypes';
|
||||
|
||||
function createId() {
|
||||
return Math.random().toString(36).slice(2);
|
||||
}
|
||||
|
||||
export async function loadPdfFromFile(file: File): Promise<PdfFile> {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const doc = await PDFDocument.load(arrayBuffer);
|
||||
|
||||
return {
|
||||
id: createId(),
|
||||
name: file.name,
|
||||
doc,
|
||||
pageCount: doc.getPageCount(),
|
||||
arrayBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
export async function splitIntoSinglePages(
|
||||
pdf: PdfFile
|
||||
): Promise<SplitResult[]> {
|
||||
const { doc, name } = pdf;
|
||||
|
||||
const title = doc.getTitle();
|
||||
const author = doc.getAuthor();
|
||||
const subject = doc.getSubject();
|
||||
const keywords = doc.getKeywords();
|
||||
const producer = doc.getProducer();
|
||||
const creator = doc.getCreator();
|
||||
const creationDate = doc.getCreationDate();
|
||||
const modificationDate = doc.getModificationDate();
|
||||
|
||||
const results: SplitResult[] = [];
|
||||
|
||||
for (let i = 0; i < doc.getPageCount(); i++) {
|
||||
const newDoc = await PDFDocument.create();
|
||||
const [copiedPage] = await newDoc.copyPages(doc, [i]);
|
||||
newDoc.addPage(copiedPage);
|
||||
|
||||
if (title) newDoc.setTitle(title);
|
||||
if (author) newDoc.setAuthor(author);
|
||||
if (subject) newDoc.setSubject(subject);
|
||||
if (keywords) newDoc.setKeywords(keywords);
|
||||
if (producer) newDoc.setProducer(producer);
|
||||
if (creator) newDoc.setCreator(creator);
|
||||
if (creationDate) newDoc.setCreationDate(creationDate);
|
||||
if (modificationDate) newDoc.setModificationDate(modificationDate);
|
||||
|
||||
const bytes = await newDoc.save();
|
||||
const blob = new Blob([bytes], { type: 'application/pdf' });
|
||||
|
||||
const base = name.replace(/\.pdf$/i, '');
|
||||
const filename = `${base}_page_${String(i + 1).padStart(3, '0')}.pdf`;
|
||||
|
||||
results.push({
|
||||
pageIndex: i,
|
||||
blob,
|
||||
filename,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function extractRange(
|
||||
pdf: PdfFile,
|
||||
range: Range
|
||||
): Promise<Blob> {
|
||||
const { doc } = pdf;
|
||||
const pageCount = doc.getPageCount();
|
||||
|
||||
const fromIndex = Math.max(0, range.from - 1);
|
||||
const toIndex = Math.min(pageCount - 1, range.to - 1);
|
||||
|
||||
if (fromIndex > toIndex) {
|
||||
throw new Error('Invalid range: from > to');
|
||||
}
|
||||
|
||||
const newDoc = await PDFDocument.create();
|
||||
const indices: number[] = [];
|
||||
for (let i = fromIndex; i <= toIndex; i++) indices.push(i);
|
||||
|
||||
const copiedPages = await newDoc.copyPages(doc, indices);
|
||||
copiedPages.forEach((p) => newDoc.addPage(p));
|
||||
|
||||
const bytes = await newDoc.save();
|
||||
return new Blob([bytes], { type: 'application/pdf' });
|
||||
}
|
||||
|
||||
export async function mergePdfs(pdfs: PdfFile[]): Promise<Blob> {
|
||||
const newDoc = await PDFDocument.create();
|
||||
|
||||
for (const pdf of pdfs) {
|
||||
const pageCount = pdf.doc.getPageCount();
|
||||
const indices = Array.from({ length: pageCount }, (_, i) => i);
|
||||
const copiedPages = await newDoc.copyPages(pdf.doc, indices);
|
||||
copiedPages.forEach((p) => newDoc.addPage(p));
|
||||
}
|
||||
|
||||
const bytes = await newDoc.save();
|
||||
return new Blob([bytes], { type: 'application/pdf' });
|
||||
}
|
||||
|
||||
export async function exportReordered(
|
||||
pdf: PdfFile,
|
||||
order: number[],
|
||||
rotations?: Record<number, number>
|
||||
): Promise<Blob> {
|
||||
const { doc } = pdf;
|
||||
const pageCount = doc.getPageCount();
|
||||
|
||||
if (order.length === 0) {
|
||||
throw new Error('Order must contain at least one page');
|
||||
}
|
||||
|
||||
if (order.some((i) => i < 0 || i >= pageCount)) {
|
||||
throw new Error('Order contains invalid page indices');
|
||||
}
|
||||
|
||||
const newDoc = await PDFDocument.create();
|
||||
const indices = [...order];
|
||||
|
||||
const copiedPages = await newDoc.copyPages(doc, indices);
|
||||
|
||||
copiedPages.forEach((page, idx) => {
|
||||
const originalIndex = indices[idx];
|
||||
const angle = rotations?.[originalIndex];
|
||||
|
||||
if (typeof angle === 'number' && angle % 360 !== 0) {
|
||||
page.setRotation(degrees(angle));
|
||||
}
|
||||
|
||||
newDoc.addPage(page);
|
||||
});
|
||||
|
||||
const bytes = await newDoc.save();
|
||||
return new Blob([bytes], { type: 'application/pdf' });
|
||||
}
|
||||
108
src/pdf/pdfThumbnailService.ts
Normal file
108
src/pdf/pdfThumbnailService.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?worker&url';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(pdfjsLib as any).GlobalWorkerOptions.workerSrc = pdfjsWorker;
|
||||
|
||||
type RotationsMap = Record<number, number>;
|
||||
|
||||
export async function generateThumbnails(
|
||||
arrayBuffer: ArrayBuffer,
|
||||
maxHeight = 150
|
||||
): Promise<string[]> {
|
||||
return generateThumbnailsInternal(arrayBuffer, {}, maxHeight);
|
||||
}
|
||||
|
||||
export async function generateThumbnailsWithRotations(
|
||||
arrayBuffer: ArrayBuffer,
|
||||
rotations: RotationsMap,
|
||||
maxHeight = 150
|
||||
): Promise<string[]> {
|
||||
return generateThumbnailsInternal(arrayBuffer, rotations, maxHeight);
|
||||
}
|
||||
|
||||
async function generateThumbnailsInternal(
|
||||
arrayBuffer: ArrayBuffer,
|
||||
rotations: RotationsMap,
|
||||
maxHeight: number
|
||||
): Promise<string[]> {
|
||||
const loadingTask = pdfjsLib.getDocument({ data: arrayBuffer });
|
||||
const pdf = await loadingTask.promise;
|
||||
|
||||
const thumbs: string[] = [];
|
||||
|
||||
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
|
||||
const page = await pdf.getPage(pageNum);
|
||||
const viewport = page.getViewport({ scale: 1 });
|
||||
const scale = maxHeight / viewport.height;
|
||||
const scaledViewport = page.getViewport({ scale });
|
||||
|
||||
const baseCanvas = document.createElement('canvas');
|
||||
const baseCtx = baseCanvas.getContext('2d');
|
||||
if (!baseCtx) {
|
||||
thumbs.push('');
|
||||
continue;
|
||||
}
|
||||
|
||||
baseCanvas.width = scaledViewport.width;
|
||||
baseCanvas.height = scaledViewport.height;
|
||||
|
||||
const renderTask = page.render({
|
||||
canvasContext: baseCtx,
|
||||
viewport: scaledViewport,
|
||||
});
|
||||
await renderTask.promise;
|
||||
|
||||
const originalIndex = pageNum - 1;
|
||||
const rotationDegRaw = rotations[originalIndex] ?? 0;
|
||||
const rotationDeg = ((rotationDegRaw % 360) + 360) % 360;
|
||||
|
||||
if (rotationDeg === 0) {
|
||||
thumbs.push(baseCanvas.toDataURL('image/png'));
|
||||
continue;
|
||||
}
|
||||
|
||||
const rotatedCanvas = document.createElement('canvas');
|
||||
const rotatedCtx = rotatedCanvas.getContext('2d');
|
||||
if (!rotatedCtx) {
|
||||
thumbs.push(baseCanvas.toDataURL('image/png'));
|
||||
continue;
|
||||
}
|
||||
|
||||
const rad = (rotationDeg * Math.PI) / 180;
|
||||
|
||||
if (rotationDeg === 90 || rotationDeg === 270) {
|
||||
rotatedCanvas.width = baseCanvas.height;
|
||||
rotatedCanvas.height = baseCanvas.width;
|
||||
} else {
|
||||
rotatedCanvas.width = baseCanvas.width;
|
||||
rotatedCanvas.height = baseCanvas.height;
|
||||
}
|
||||
|
||||
rotatedCtx.save();
|
||||
|
||||
switch (rotationDeg) {
|
||||
case 90:
|
||||
rotatedCtx.translate(rotatedCanvas.width, 0);
|
||||
rotatedCtx.rotate(rad);
|
||||
break;
|
||||
case 180:
|
||||
rotatedCtx.translate(rotatedCanvas.width, rotatedCanvas.height);
|
||||
rotatedCtx.rotate(rad);
|
||||
break;
|
||||
case 270:
|
||||
rotatedCtx.translate(0, rotatedCanvas.height);
|
||||
rotatedCtx.rotate(rad);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
rotatedCtx.drawImage(baseCanvas, 0, 0);
|
||||
rotatedCtx.restore();
|
||||
|
||||
thumbs.push(rotatedCanvas.toDataURL('image/png'));
|
||||
}
|
||||
|
||||
return thumbs;
|
||||
}
|
||||
20
src/pdf/pdfTypes.ts
Normal file
20
src/pdf/pdfTypes.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { PDFDocument } from 'pdf-lib';
|
||||
|
||||
export interface PdfFile {
|
||||
id: string;
|
||||
name: string;
|
||||
doc: PDFDocument;
|
||||
pageCount: number;
|
||||
arrayBuffer: ArrayBuffer;
|
||||
}
|
||||
|
||||
export interface SplitResult {
|
||||
pageIndex: number;
|
||||
blob: Blob;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export interface Range {
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
Reference in New Issue
Block a user