101 lines
3.4 KiB
TypeScript
101 lines
3.4 KiB
TypeScript
import * as fs from 'fs/promises';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { PDFDocument, degrees } from 'pdf-lib';
|
|
import type { InboxDocument } from '../database/entities/inbox-document.entity';
|
|
|
|
/**
|
|
* Wendet die virtuellen Edits (DeletedPages, Rotations) auf das Original-PDF an
|
|
* und schreibt das Ergebnis in eine temporäre Datei. Gibt den Pfad zurück.
|
|
* Aufrufer ist verantwortlich für das Aufräumen.
|
|
*/
|
|
export async function applyEditsToTemp(
|
|
doc: InboxDocument,
|
|
pdfPath: string,
|
|
): Promise<string> {
|
|
const bytes = await fs.readFile(pdfPath);
|
|
const pdf = await PDFDocument.load(bytes, { ignoreEncryption: true });
|
|
|
|
const rotations = doc.Rotations ?? {};
|
|
for (const [pageStr, rot] of Object.entries(rotations)) {
|
|
const pageNum = Number(pageStr);
|
|
if (!Number.isInteger(pageNum)) continue;
|
|
const idx = pageNum - 1;
|
|
if (idx < 0 || idx >= pdf.getPageCount()) continue;
|
|
const normalized = ((Math.round(rot / 90) * 90) % 360 + 360) % 360;
|
|
if (normalized === 0) continue;
|
|
pdf.getPage(idx).setRotation(degrees(normalized));
|
|
}
|
|
|
|
// Seiten in absteigender Reihenfolge entfernen, damit Indizes stabil bleiben
|
|
const deleted = [...(doc.DeletedPages ?? [])].sort((a, b) => b - a);
|
|
for (const pageNum of deleted) {
|
|
const idx = pageNum - 1;
|
|
if (idx < 0 || idx >= pdf.getPageCount()) continue;
|
|
pdf.removePage(idx);
|
|
}
|
|
|
|
const out = await pdf.save();
|
|
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'inbox-pp-'));
|
|
const tmpPath = path.join(tmpDir, 'document.pdf');
|
|
await fs.writeFile(tmpPath, out);
|
|
return tmpPath;
|
|
}
|
|
|
|
export async function cleanupTemp(filePath: string | null): Promise<void> {
|
|
if (!filePath) return;
|
|
try {
|
|
await fs.unlink(filePath);
|
|
await fs.rmdir(path.dirname(filePath)).catch(() => undefined);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Baut ein PDF aus einer Teilmenge von Originalseiten auf (rotiert, keine gelöschten Seiten).
|
|
* segmentPages enthält 1-basierte Originalseitenzahlen; gelöschte Seiten müssen vom Aufrufer
|
|
* bereits herausgefiltert worden sein.
|
|
*/
|
|
export async function buildSegmentBuffer(
|
|
doc: InboxDocument,
|
|
pdfPath: string,
|
|
segmentPages: number[],
|
|
): Promise<Buffer> {
|
|
const bytes = await fs.readFile(pdfPath);
|
|
const srcPdf = await PDFDocument.load(bytes, { ignoreEncryption: true });
|
|
const outPdf = await PDFDocument.create();
|
|
|
|
const rotations = doc.Rotations ?? {};
|
|
const indices = segmentPages.map((p) => p - 1);
|
|
const copied = await outPdf.copyPages(srcPdf, indices);
|
|
|
|
copied.forEach((page, i) => {
|
|
const rot = rotations[String(segmentPages[i])];
|
|
if (rot !== undefined) {
|
|
const normalized = ((Math.round(rot / 90) * 90) % 360 + 360) % 360;
|
|
if (normalized !== 0) page.setRotation(degrees(normalized));
|
|
}
|
|
outPdf.addPage(page);
|
|
});
|
|
|
|
return Buffer.from(await outPdf.save());
|
|
}
|
|
|
|
export async function extractSectionToTemp(
|
|
pdfPath: string,
|
|
pageIndices: number[],
|
|
): Promise<string> {
|
|
const bytes = await fs.readFile(pdfPath);
|
|
const srcPdf = await PDFDocument.load(bytes, { ignoreEncryption: true });
|
|
const outPdf = await PDFDocument.create();
|
|
const copied = await outPdf.copyPages(srcPdf, pageIndices);
|
|
copied.forEach(p => outPdf.addPage(p));
|
|
const out = await outPdf.save();
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'inbox-section-'));
|
|
const tmpPath = path.join(tmpDir, 'section.pdf');
|
|
await fs.writeFile(tmpPath, out);
|
|
return tmpPath;
|
|
}
|