mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Conversion engines wrote their own names into PDF metadata: LibreOffice, Ghostscript, pdfcpu, WeasyPrint, and PDFKit all stamped Producer/Creator on generated files. A new doc_scrub_meta docs-profile script (PyMuPDF) rewrites both fields to SnapOtter and drops the stale XMP copy; the worker applies it to the 25 PDF-generating tools before outputs reach object storage. Best effort by design: any failure keeps the original bytes and only logs a warning. Deliberately untouched: tools that edit the user's own PDF and preserve its metadata (qpdf edits, sign, flatten), encrypted outputs (copied through), and pdfa-convert, where a metadata rewrite risks PDF/A conformance. Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7
76 lines
2.4 KiB
TypeScript
76 lines
2.4 KiB
TypeScript
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { pdfScrubProducerPy } from "@snapotter/doc-engine";
|
|
|
|
/**
|
|
* Tools whose PDF output is GENERATED by a conversion engine that stamps its
|
|
* own name into the Producer/Creator metadata (LibreOffice, Ghostscript,
|
|
* pdfcpu, WeasyPrint, PDFKit, the OCR pipeline). Their outputs get SnapOtter
|
|
* stamped instead; see scrubPdfProducer.
|
|
*
|
|
* Deliberately excluded: tools that edit the user's own PDF and preserve its
|
|
* metadata (merge/split/rotate/protect/unlock/repair/linearize via qpdf,
|
|
* sign-pdf and flatten-pdf via PyMuPDF incremental edits) and pdfa-convert,
|
|
* where rewriting metadata after conversion risks PDF/A conformance.
|
|
*/
|
|
export const SCRUB_PDF_PRODUCER_TOOLS: ReadonlySet<string> = new Set([
|
|
// LibreOffice conversions
|
|
"convert-document",
|
|
"convert-presentation",
|
|
"convert-spreadsheet",
|
|
"word-to-pdf",
|
|
"excel-to-pdf",
|
|
"powerpoint-to-pdf",
|
|
// WeasyPrint
|
|
"html-to-pdf",
|
|
"markdown-to-pdf",
|
|
// pandoc/WeasyPrint chains
|
|
"epub-convert",
|
|
// Ghostscript rewrites (these clobber the user's Producer anyway)
|
|
"compress-pdf",
|
|
"grayscale-pdf",
|
|
// pdfcpu layout tools (pdfcpu stamps itself on every write)
|
|
"crop-pdf",
|
|
"nup-pdf",
|
|
"booklet-pdf",
|
|
"watermark-pdf",
|
|
"pdf-page-numbers",
|
|
// OCR produces a new searchable PDF
|
|
"ocr-pdf",
|
|
// PDFKit image-to-PDF family
|
|
"image-to-pdf",
|
|
"jpg-to-pdf",
|
|
"png-to-pdf",
|
|
"heic-to-pdf",
|
|
"tiff-to-pdf",
|
|
"webp-to-pdf",
|
|
"gif-to-pdf",
|
|
"eps-to-pdf",
|
|
]);
|
|
|
|
/**
|
|
* Replace the conversion engine's Producer/Creator metadata with SnapOtter.
|
|
* Best effort by design: any failure (missing PyMuPDF, malformed PDF,
|
|
* dispatcher down) returns the original buffer unchanged so a cosmetic
|
|
* metadata pass can never fail a job.
|
|
*/
|
|
export async function scrubPdfProducer(buffer: Buffer): Promise<Buffer> {
|
|
let dir: string | null = null;
|
|
try {
|
|
dir = await mkdtemp(join(tmpdir(), "pdf-scrub-"));
|
|
const inPath = join(dir, "in.pdf");
|
|
const outPath = join(dir, "out.pdf");
|
|
await writeFile(inPath, buffer);
|
|
await pdfScrubProducerPy(inPath, outPath);
|
|
return await readFile(outPath);
|
|
} catch (err) {
|
|
console.warn(
|
|
`[pdf-scrub] keeping original metadata: ${err instanceof Error ? err.message : String(err)}`,
|
|
);
|
|
return buffer;
|
|
} finally {
|
|
if (dir) await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
}
|
|
}
|