Files
paperlessmanager/paperless-backend/src/preprocessing/pdf.service.ts
T
bjoernpoettker 66aeab282c
Build and Push Multi-Platform Images / build-and-push (push) Successful in 19s
Revert "fix: resolve all ESLint errors in backend and frontend"
This reverts commit 07dfd7e840.
2026-06-16 16:19:11 +02:00

131 lines
3.5 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import { execFile } from 'child_process';
import { promisify } from 'util';
import * as path from 'path';
import * as fs from 'fs/promises';
import * as os from 'os';
const execFileAsync = promisify(execFile);
@Injectable()
export class PdfService {
private readonly logger = new Logger(PdfService.name);
/**
* Konvertiert eine PDF-Seite in ein PNG-Bild via Ghostscript.
* Gibt den Pfad zum temporären Bild zurück.
*/
async pdfPageToImage(pdfPath: string, page = 1, dpi = 300): Promise<string> {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'pdf-'));
const outputPath = path.join(tmpDir, `page-${page}.png`);
await execFileAsync('gs', [
'-dNOPAUSE',
'-dBATCH',
'-dNOSAFER',
'-sDEVICE=png16m',
`-dFirstPage=${page}`,
`-dLastPage=${page}`,
`-r${dpi}`,
`-sOutputFile=${outputPath}`,
pdfPath,
]);
this.logger.debug(`PDF Seite ${page} konvertiert: ${outputPath}`);
return outputPath;
}
/**
* Konvertiert alle Seiten einer PDF in Bilder.
* Verwendet einen einzigen Ghostscript-Aufruf mit %d-Platzhalter für alle Seiten.
*/
async pdfToImages(pdfPath: string, dpi = 200): Promise<string[]> {
const stat = await fs.stat(pdfPath);
if (stat.size === 0) {
this.logger.warn(`PDF ist leer (0 Bytes), wird übersprungen: ${pdfPath}`);
return [];
}
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'pdf-'));
await execFileAsync('gs', [
'-dNOPAUSE',
'-dBATCH',
'-dNOSAFER',
'-sDEVICE=png16m',
`-r${dpi}`,
`-sOutputFile=${path.join(tmpDir, 'page-%d.png')}`,
pdfPath,
]);
const entries = await fs.readdir(tmpDir);
const images = entries
.filter((f) => f.endsWith('.png'))
.sort((a, b) => {
const numA = parseInt(a.match(/\d+/)?.[0] ?? '0', 10);
const numB = parseInt(b.match(/\d+/)?.[0] ?? '0', 10);
return numA - numB;
})
.map((f) => path.join(tmpDir, f));
if (images.length === 0) {
this.logger.warn(
`Ghostscript hat keine Seiten erstellt: ${pdfPath} — Verzeichnisinhalt: [${entries.join(', ')}]`,
);
} else {
this.logger.debug(
`PDF konvertiert: ${images.length} Seite(n) in ${tmpDir}`,
);
}
return images;
}
/**
* Ermittelt die Seitenanzahl einer PDF via Ghostscript.
*/
async getPageCount(pdfPath: string): Promise<number> {
const { stdout } = await execFileAsync('gs', [
'-q',
'-dNODISPLAY',
'-dNOSAFER',
`-c`,
`(${pdfPath.replace(/\\/g, '/')}) (r) file runpdfbegin pdfpagecount = quit`,
]);
return parseInt(stdout.trim(), 10) || 1;
}
/**
* Bereinigt eine PDF (entschlüsselt sie ggf. wenn nur Owner-Passwort gesetzt ist)
* via Ghostscript pdfwrite.
*/
async sanitizePdf(inputPath: string): Promise<string> {
const outputPath = path.join(os.tmpdir(), `sanitized-${Date.now()}.pdf`);
await execFileAsync('gs', [
'-dNOPAUSE',
'-dBATCH',
'-dNOSAFER',
'-sDEVICE=pdfwrite',
`-sOutputFile=${outputPath}`,
inputPath,
]);
return outputPath;
}
/**
* Räumt temporäre Bilder und ihre Verzeichnisse auf.
*/
async cleanup(imagePaths: string[]): Promise<void> {
const dirs = new Set<string>();
for (const imgPath of imagePaths) {
try {
await fs.unlink(imgPath);
} catch {}
dirs.add(path.dirname(imgPath));
}
for (const dir of dirs) {
await fs.rmdir(dir).catch(() => {});
}
}
}