66aeab282c
Build and Push Multi-Platform Images / build-and-push (push) Successful in 19s
This reverts commit 07dfd7e840.
131 lines
3.5 KiB
TypeScript
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(() => {});
|
|
}
|
|
}
|
|
}
|