import { existsSync, rmSync } from "node:fs"; import path from "node:path"; import { OfficeError } from "../core/errors.ts"; import { cleanupStaged, createOutputDirectory, planOutput, publishStagedFile, requireInputFile, type OutputPlan } from "../core/paths.ts"; import { makePreviewDirectory } from "../core/temp.ts"; import { PdfRuntime } from "./runtime.ts"; export interface PdfArtifactResult { outputPath: string; validation: unknown; previewDirectory: string; previewPaths: string[]; } async function render(runtime: PdfRuntime, inputPath: string, outputDirectory?: string, password?: string) { const directory = outputDirectory ? createOutputDirectory(outputDirectory, runtime.cwd) : makePreviewDirectory(); try { const result = await runtime.worker<{ previewPaths: string[] }>("render", { inputPath, outputDirectory: directory, dpi: 144, }, password ? { password } : {}); if (!Array.isArray(result.previewPaths) || result.previewPaths.length === 0 || result.previewPaths.some((item) => !existsSync(item))) { throw new OfficeError("PDF 渲染未生成有效页面图片。", "PDF_RENDER_EMPTY"); } return { directory, paths: result.previewPaths }; } catch (error) { if (!outputDirectory) rmSync(directory, { recursive: true, force: true }); throw error; } } export async function finalizePdfOutput(runtime: PdfRuntime, plan: OutputPlan, previewDirectory?: string, password?: string): Promise { let managedPreview: string | undefined; try { const checked = await runtime.worker("validate", { inputPath: plan.stagedPath }, password ? { password } : {}); const validation = checked && typeof checked === "object" ? { ...checked, path: plan.finalPath } : checked; const preview = await render(runtime, plan.stagedPath, previewDirectory, password); managedPreview = preview.directory; publishStagedFile(plan); return { outputPath: plan.finalPath, validation, previewDirectory: preview.directory, previewPaths: preview.paths }; } catch (error) { cleanupStaged(plan); if (managedPreview && !previewDirectory) rmSync(managedPreview, { recursive: true, force: true }); throw error; } } function inputPdf(value: string, cwd: string): string { return requireInputFile(value, cwd, [".pdf"]); } export async function pdfValidate(runtime: PdfRuntime, inputPath: string, password?: string) { const input = inputPdf(inputPath, runtime.cwd); return await runtime.worker("validate", { inputPath: input }, password ? { password } : {}); } export async function pdfMetadataGet(runtime: PdfRuntime, inputPath: string, password?: string) { const input = inputPdf(inputPath, runtime.cwd); return await runtime.worker("metadata_get", { inputPath: input }, password ? { password } : {}); } export async function pdfMetadataSet(runtime: PdfRuntime, options: { inputPath: string; metadata: Record; outputPath?: string; overwrite?: boolean; previewDirectory?: string; }): Promise { const input = inputPdf(options.inputPath, runtime.cwd); const plan = planOutput({ inputPath: input, outputPath: options.outputPath, cwd: runtime.cwd, extension: ".pdf", overwrite: options.overwrite }); try { await runtime.worker("metadata_set", { inputPath: input, outputPath: plan.stagedPath, metadata: options.metadata }); return await finalizePdfOutput(runtime, plan, options.previewDirectory); } catch (error) { cleanupStaged(plan); throw error; } } export async function pdfExtract(runtime: PdfRuntime, inputPath: string, tables = true, password?: string) { const input = inputPdf(inputPath, runtime.cwd); return await runtime.worker("extract", { inputPath: input, tables }, password ? { password } : {}); } export async function pdfMerge(runtime: PdfRuntime, options: { inputPaths: string[]; outputPath?: string; overwrite?: boolean; previewDirectory?: string; }): Promise { if (options.inputPaths.length < 2) throw new OfficeError("合并至少需要两个 PDF。", "INPUTS_REQUIRED"); const inputs = options.inputPaths.map((item) => inputPdf(item, runtime.cwd)); const plan = planOutput({ inputPath: inputs[0], outputPath: options.outputPath, cwd: runtime.cwd, extension: ".pdf", overwrite: options.overwrite }); try { await runtime.worker("merge", { inputPaths: inputs, outputPath: plan.stagedPath }); return await finalizePdfOutput(runtime, plan, options.previewDirectory); } catch (error) { cleanupStaged(plan); throw error; } } export async function pdfSplit(runtime: PdfRuntime, options: { inputPath: string; startPage: number; endPage?: number; outputPath?: string; overwrite?: boolean; previewDirectory?: string; }): Promise { const input = inputPdf(options.inputPath, runtime.cwd); const plan = planOutput({ inputPath: input, outputPath: options.outputPath, cwd: runtime.cwd, extension: ".pdf", overwrite: options.overwrite }); try { await runtime.worker("split", { inputPath: input, outputPath: plan.stagedPath, startPage: options.startPage, ...(options.endPage ? { endPage: options.endPage } : {}) }); return await finalizePdfOutput(runtime, plan, options.previewDirectory); } catch (error) { cleanupStaged(plan); throw error; } } export async function pdfRotate(runtime: PdfRuntime, options: { inputPath: string; angle: 90 | 180 | 270; pages?: number[]; outputPath?: string; overwrite?: boolean; previewDirectory?: string; }): Promise { const input = inputPdf(options.inputPath, runtime.cwd); const plan = planOutput({ inputPath: input, outputPath: options.outputPath, cwd: runtime.cwd, extension: ".pdf", overwrite: options.overwrite }); try { await runtime.worker("rotate", { inputPath: input, outputPath: plan.stagedPath, angle: options.angle, ...(options.pages ? { pages: options.pages } : {}) }); return await finalizePdfOutput(runtime, plan, options.previewDirectory); } catch (error) { cleanupStaged(plan); throw error; } } export async function pdfWatermark(runtime: PdfRuntime, options: { inputPath: string; text: string; pages?: number[]; fontSize?: number; color?: string; opacity?: number; rotation?: number; outputPath?: string; overwrite?: boolean; previewDirectory?: string; }): Promise { const input = inputPdf(options.inputPath, runtime.cwd); const plan = planOutput({ inputPath: input, outputPath: options.outputPath, cwd: runtime.cwd, extension: ".pdf", overwrite: options.overwrite }); try { await runtime.worker("watermark", { inputPath: input, outputPath: plan.stagedPath, text: options.text, ...(options.pages ? { pages: options.pages } : {}), fontSize: options.fontSize ?? 36, color: options.color ?? "808080", opacity: options.opacity ?? 0.25, rotation: options.rotation ?? 35 }); return await finalizePdfOutput(runtime, plan, options.previewDirectory); } catch (error) { cleanupStaged(plan); throw error; } } export async function pdfOverlayText(runtime: PdfRuntime, options: { inputPath: string; page: number; text: string; x: number; y: number; fontSize?: number; color?: string; outputPath?: string; overwrite?: boolean; previewDirectory?: string; }): Promise { const input = inputPdf(options.inputPath, runtime.cwd); const plan = planOutput({ inputPath: input, outputPath: options.outputPath, cwd: runtime.cwd, extension: ".pdf", overwrite: options.overwrite }); try { await runtime.worker("overlay_text", { inputPath: input, outputPath: plan.stagedPath, page: options.page, text: options.text, x: options.x, y: options.y, fontSize: options.fontSize ?? 12, color: options.color ?? "000000", opacity: 1, rotation: 0 }); return await finalizePdfOutput(runtime, plan, options.previewDirectory); } catch (error) { cleanupStaged(plan); throw error; } } export async function pdfStampImage(runtime: PdfRuntime, options: { inputPath: string; imagePath: string; page: number; x: number; y: number; width: number; height: number; outputPath?: string; overwrite?: boolean; previewDirectory?: string; }): Promise { const input = inputPdf(options.inputPath, runtime.cwd); const image = requireInputFile(options.imagePath, runtime.cwd, [".png", ".jpg", ".jpeg"]); const plan = planOutput({ inputPath: input, outputPath: options.outputPath, cwd: runtime.cwd, extension: ".pdf", overwrite: options.overwrite }); try { await runtime.worker("stamp_image", { inputPath: input, outputPath: plan.stagedPath, imagePath: image, page: options.page, x: options.x, y: options.y, width: options.width, height: options.height }); return await finalizePdfOutput(runtime, plan, options.previewDirectory); } catch (error) { cleanupStaged(plan); throw error; } } export async function pdfFillForm(runtime: PdfRuntime, options: { inputPath: string; fields: Record; outputPath?: string; overwrite?: boolean; previewDirectory?: string; }): Promise { const input = inputPdf(options.inputPath, runtime.cwd); const plan = planOutput({ inputPath: input, outputPath: options.outputPath, cwd: runtime.cwd, extension: ".pdf", overwrite: options.overwrite }); try { await runtime.worker("fill_form", { inputPath: input, outputPath: plan.stagedPath, fields: options.fields }); return await finalizePdfOutput(runtime, plan, options.previewDirectory); } catch (error) { cleanupStaged(plan); throw error; } } export async function pdfEncrypt(runtime: PdfRuntime, options: { inputPath: string; outputPath?: string; overwrite?: boolean; previewDirectory?: string; }, password: string, ownerPassword?: string): Promise { const input = inputPdf(options.inputPath, runtime.cwd); const plan = planOutput({ inputPath: input, outputPath: options.outputPath, cwd: runtime.cwd, extension: ".pdf", overwrite: options.overwrite }); try { await runtime.worker("encrypt", { inputPath: input, outputPath: plan.stagedPath }, { password, ...(ownerPassword ? { ownerPassword } : {}) }); return await finalizePdfOutput(runtime, plan, options.previewDirectory, password); } catch (error) { cleanupStaged(plan); throw error; } } export async function pdfDecrypt(runtime: PdfRuntime, options: { inputPath: string; outputPath?: string; overwrite?: boolean; previewDirectory?: string; }, password: string): Promise { const input = inputPdf(options.inputPath, runtime.cwd); const plan = planOutput({ inputPath: input, outputPath: options.outputPath, cwd: runtime.cwd, extension: ".pdf", overwrite: options.overwrite }); try { await runtime.worker("decrypt", { inputPath: input, outputPath: plan.stagedPath }, { password }); return await finalizePdfOutput(runtime, plan, options.previewDirectory); } catch (error) { cleanupStaged(plan); throw error; } } export async function pdfRender(runtime: PdfRuntime, inputPath: string, outputDirectory?: string, pages?: number[]) { const input = inputPdf(inputPath, runtime.cwd); const directory = outputDirectory ? createOutputDirectory(outputDirectory, runtime.cwd) : makePreviewDirectory(); try { return await runtime.worker("render", { inputPath: input, outputDirectory: directory, ...(pages ? { pages } : {}), dpi: 144 }); } catch (error) { if (!outputDirectory) rmSync(directory, { recursive: true, force: true }); throw error; } }