Add unit tests for PDF Packager converter functionality

- Implement tests for converter properties, ensuring correct input and output formats.
- Add tests for chip parsing, covering various image, pdf_image, and pdfa chip formats.
- Include tests for the isArchiveOutput and getOutputFileName functions.
- Mock execFile for testing the convert function, verifying command execution and output file creation.
- Ensure all chips are parseable and validate the completeness of chip categories.
This commit is contained in:
Your Name 2026-01-24 14:39:22 +08:00
parent 87bd318303
commit 944fe18b75
11 changed files with 2233 additions and 8 deletions

View file

@ -33,6 +33,7 @@ import {
} from "./pdfmathtranslate";
import { convert as convertBabelDoc, properties as propertiesBabelDoc } from "./babeldoc";
import { convert as convertOcrMyPdf, properties as propertiesOcrMyPdf } from "./ocrmypdf";
import { convert as convertPdfPackager, properties as propertiesPdfPackager } from "./pdfpackager";
// This should probably be reconstructed so that the functions are not imported instead the functions hook into this to make the converters more modular
@ -162,6 +163,10 @@ const properties: Record<
properties: propertiesOcrMyPdf,
converter: convertOcrMyPdf,
},
"PDF Packager": {
properties: propertiesPdfPackager,
converter: convertPdfPackager,
},
deark: {
properties: propertiesDeark,
converter: convertDeark,

View file

@ -0,0 +1,876 @@
import { execFile as execFileOriginal } from "node:child_process";
import { mkdirSync, existsSync, readdirSync, unlinkSync, rmdirSync, copyFileSync } from "node:fs";
import { join, basename, dirname } from "node:path";
import type { ExecFileFn } from "./types";
/**
* PDF Packager Engine
*
* PDF使 chip
*
* chip
* A) .tar: png-*, jpg-*, jpeg-*
* B) PDF: pdf-*
* C) PDF/A-1b: pdfa1b-*
* D) PDF/A-2b: pdfa2b-*
* E) 全部打包: all-*
*
* Chip
* - DPI: 150, 300, 600
* - 保護: p (), np ()
* - 簽章: s ()
* - PDF/A 來源: i (), o ( PDF)
*/
// =============================================================================
// Chip 常數定義(手刻列出所有可選項目)
// =============================================================================
/**
* chips
*
*/
export const ALL_CHIPS = [
// A) 圖片輸出(打包 .tar
"png-150",
"png-300",
"png-600",
"jpg-150",
"jpg-300",
"jpg-600",
"jpeg-150",
"jpeg-300",
"jpeg-600",
// B) 圖片型 PDF
"pdf-150",
"pdf-300",
"pdf-600",
"pdf-150-p",
"pdf-150-np",
"pdf-300-p",
"pdf-300-np",
"pdf-600-p",
"pdf-600-np",
"pdf-150-s",
"pdf-300-s",
"pdf-600-s",
"pdf-150-p-s",
"pdf-150-np-s",
"pdf-300-p-s",
"pdf-300-np-s",
"pdf-600-p-s",
"pdf-600-np-s",
// C) PDF/A-1b來源 i
"pdfa1b-i-150",
"pdfa1b-i-300",
"pdfa1b-i-600",
"pdfa1b-i-150-p",
"pdfa1b-i-150-np",
"pdfa1b-i-300-p",
"pdfa1b-i-300-np",
"pdfa1b-i-600-p",
"pdfa1b-i-600-np",
"pdfa1b-i-150-s",
"pdfa1b-i-300-s",
"pdfa1b-i-600-s",
"pdfa1b-i-150-p-s",
"pdfa1b-i-150-np-s",
"pdfa1b-i-300-p-s",
"pdfa1b-i-300-np-s",
"pdfa1b-i-600-p-s",
"pdfa1b-i-600-np-s",
// C) PDF/A-1b來源 o
"pdfa1b-o-150",
"pdfa1b-o-300",
"pdfa1b-o-600",
"pdfa1b-o-150-p",
"pdfa1b-o-150-np",
"pdfa1b-o-300-p",
"pdfa1b-o-300-np",
"pdfa1b-o-600-p",
"pdfa1b-o-600-np",
"pdfa1b-o-150-s",
"pdfa1b-o-300-s",
"pdfa1b-o-600-s",
"pdfa1b-o-150-p-s",
"pdfa1b-o-150-np-s",
"pdfa1b-o-300-p-s",
"pdfa1b-o-300-np-s",
"pdfa1b-o-600-p-s",
"pdfa1b-o-600-np-s",
// D) PDF/A-2b來源 i
"pdfa2b-i-150",
"pdfa2b-i-300",
"pdfa2b-i-600",
"pdfa2b-i-150-p",
"pdfa2b-i-150-np",
"pdfa2b-i-300-p",
"pdfa2b-i-300-np",
"pdfa2b-i-600-p",
"pdfa2b-i-600-np",
"pdfa2b-i-150-s",
"pdfa2b-i-300-s",
"pdfa2b-i-600-s",
"pdfa2b-i-150-p-s",
"pdfa2b-i-150-np-s",
"pdfa2b-i-300-p-s",
"pdfa2b-i-300-np-s",
"pdfa2b-i-600-p-s",
"pdfa2b-i-600-np-s",
// D) PDF/A-2b來源 o
"pdfa2b-o-150",
"pdfa2b-o-300",
"pdfa2b-o-600",
"pdfa2b-o-150-p",
"pdfa2b-o-150-np",
"pdfa2b-o-300-p",
"pdfa2b-o-300-np",
"pdfa2b-o-600-p",
"pdfa2b-o-600-np",
"pdfa2b-o-150-s",
"pdfa2b-o-300-s",
"pdfa2b-o-600-s",
"pdfa2b-o-150-p-s",
"pdfa2b-o-150-np-s",
"pdfa2b-o-300-p-s",
"pdfa2b-o-300-np-s",
"pdfa2b-o-600-p-s",
"pdfa2b-o-600-np-s",
// E) 全部打包
"all-150",
"all-300",
"all-600",
] as const;
// =============================================================================
// 引擎屬性定義
// =============================================================================
export const properties = {
from: {
document: ["pdf"],
},
to: {
document: [...ALL_CHIPS] as string[],
},
// 某些 chip 輸出 .tarimages, all-*),其他輸出 .pdf
// PDF Packager 不使用 outputMode而是根據 chip 動態決定輸出類型
};
// =============================================================================
// Chip 解析器
// =============================================================================
/**
* Chip
*/
export interface ParsedChip {
kind: "images" | "pdf_image" | "pdfa" | "all";
dpi: 150 | 300 | 600;
// images only
imageFormat?: "png" | "jpg" | "jpeg" | undefined;
// pdfa only
pdfaLevel?: "1b" | "2b" | undefined;
pdfaSource?: "i" | "o" | undefined;
// for pdf outputs that can be protected
protect?: "p" | "np" | undefined; // print allowed / no print
sign?: boolean | undefined; // -s
// 原始 chip 名稱
rawChip: string;
}
/**
* chip
* @param chip chip
* @returns null
*/
export function parseChip(chip: string): ParsedChip | null {
// all-<dpi>
const allMatch = chip.match(/^all-(150|300|600)$/);
if (allMatch && allMatch[1]) {
return {
kind: "all",
dpi: Number.parseInt(allMatch[1], 10) as 150 | 300 | 600,
rawChip: chip,
};
}
// <img>-<dpi> (png, jpg, jpeg)
const imgMatch = chip.match(/^(png|jpg|jpeg)-(150|300|600)$/);
if (imgMatch && imgMatch[1] && imgMatch[2]) {
return {
kind: "images",
dpi: Number.parseInt(imgMatch[2], 10) as 150 | 300 | 600,
imageFormat: imgMatch[1] as "png" | "jpg" | "jpeg",
rawChip: chip,
};
}
// pdf-<dpi>[-<protect>][-s]
const pdfMatch = chip.match(/^pdf-(150|300|600)(?:-(p|np))?(?:-(s))?$/);
if (pdfMatch && pdfMatch[1]) {
return {
kind: "pdf_image",
dpi: Number.parseInt(pdfMatch[1], 10) as 150 | 300 | 600,
protect: (pdfMatch[2] as "p" | "np" | undefined) ?? undefined,
sign: pdfMatch[3] === "s",
rawChip: chip,
};
}
// pdfa<level>-<src>-<dpi>[-<protect>][-s]
const pdfaMatch = chip.match(/^pdfa(1b|2b)-(i|o)-(150|300|600)(?:-(p|np))?(?:-(s))?$/);
if (pdfaMatch && pdfaMatch[1] && pdfaMatch[2] && pdfaMatch[3]) {
return {
kind: "pdfa",
dpi: Number.parseInt(pdfaMatch[3], 10) as 150 | 300 | 600,
pdfaLevel: pdfaMatch[1] as "1b" | "2b",
pdfaSource: pdfaMatch[2] as "i" | "o",
protect: (pdfaMatch[4] as "p" | "np" | undefined) ?? undefined,
sign: pdfaMatch[5] === "s",
rawChip: chip,
};
}
return null;
}
/**
* archive
*/
export function isArchiveOutput(chip: string): boolean {
const parsed = parseChip(chip);
if (!parsed) return false;
return parsed.kind === "images" || parsed.kind === "all";
}
/**
*
*/
export function getOutputFileName(chip: string): string {
if (isArchiveOutput(chip)) {
return `pack_${chip}.tar`;
}
return `pack_${chip}.pdf`;
}
// =============================================================================
// 工具函式
// =============================================================================
/**
*
*/
function removeDir(dirPath: string): void {
if (existsSync(dirPath)) {
const files = readdirSync(dirPath, { withFileTypes: true });
for (const file of files) {
const filePath = join(dirPath, file.name);
if (file.isDirectory()) {
removeDir(filePath);
} else {
unlinkSync(filePath);
}
}
rmdirSync(dirPath);
}
}
/**
*
*/
function cleanDir(dirPath: string): void {
if (existsSync(dirPath)) {
const files = readdirSync(dirPath);
for (const file of files) {
const filePath = join(dirPath, file);
unlinkSync(filePath);
}
}
}
/**
* Promise
*/
function execCommand(
cmd: string,
args: string[],
execFile: ExecFileFn,
): Promise<{ stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
execFile(cmd, args, (error, stdout, stderr) => {
if (error) {
reject(new Error(`${cmd} error: ${error.message}\nstderr: ${stderr}`));
return;
}
resolve({ stdout, stderr });
});
});
}
// =============================================================================
// Pipeline 處理函式
// =============================================================================
/**
* 使 pdftoppm PDF
* @param inputPdf PDF
* @param outputDir
* @param dpi DPI
* @param format (png, jpeg)
* @param execFile execFile
* @returns
*/
async function pdfToImages(
inputPdf: string,
outputDir: string,
dpi: number,
format: "png" | "jpeg",
execFile: ExecFileFn,
): Promise<string[]> {
console.log(`[PDFPackager] 📸 執行 pdftoppm (DPI: ${dpi}, 格式: ${format})`);
const outputPrefix = join(outputDir, "page");
// pdftoppm -r <dpi> -<format> <input.pdf> <output_prefix>
const args = ["-r", String(dpi)];
if (format === "png") {
args.push("-png");
} else {
args.push("-jpeg");
}
args.push(inputPdf, outputPrefix);
await execCommand("pdftoppm", args, execFile);
// 取得產生的圖片檔案
const files = readdirSync(outputDir)
.filter((f) => f.startsWith("page-") && (f.endsWith(".png") || f.endsWith(".jpg")))
.sort()
.map((f) => join(outputDir, f));
console.log(`[PDFPackager] ✅ 產生 ${files.length} 張圖片`);
return files;
}
/**
* 使 img2pdf PDF
* @param imageFiles
* @param outputPdf PDF
* @param execFile execFile
*/
async function imagesToPdf(
imageFiles: string[],
outputPdf: string,
execFile: ExecFileFn,
): Promise<void> {
console.log(`[PDFPackager] 📄 執行 img2pdf (${imageFiles.length} 張圖片)`);
// img2pdf <images...> -o <output.pdf>
const args = [...imageFiles, "-o", outputPdf];
await execCommand("img2pdf", args, execFile);
console.log(`[PDFPackager] ✅ 產生圖片型 PDF`);
}
/**
* 使 Ghostscript PDF/A
* @param inputPdf PDF
* @param outputPdf PDF
* @param level PDF/A (1b, 2b)
* @param execFile execFile
*/
async function convertToPdfA(
inputPdf: string,
outputPdf: string,
level: "1b" | "2b",
execFile: ExecFileFn,
): Promise<void> {
console.log(`[PDFPackager] 📋 執行 Ghostscript (PDF/A-${level})`);
// 根據 level 選擇 PDF/A 標準
const pdfaProfile = level === "1b" ? "1" : "2";
// gs -dPDFA=<level> -dBATCH -dNOPAUSE -sColorConversionStrategy=UseDeviceIndependentColor
// -sDEVICE=pdfwrite -dPDFACompatibilityPolicy=1 -sOutputFile=<output> <input>
const args = [
`-dPDFA=${pdfaProfile}`,
"-dBATCH",
"-dNOPAUSE",
"-dQUIET",
"-sColorConversionStrategy=UseDeviceIndependentColor",
"-sDEVICE=pdfwrite",
"-dPDFACompatibilityPolicy=1",
`-sOutputFile=${outputPdf}`,
inputPdf,
];
await execCommand("gs", args, execFile);
console.log(`[PDFPackager] ✅ 產生 PDF/A-${level}`);
}
/**
* 使 qpdf PDF
* @param inputPdf PDF
* @param outputPdf PDF
* @param allowPrint
* @param execFile execFile
*/
async function protectPdf(
inputPdf: string,
outputPdf: string,
allowPrint: boolean,
execFile: ExecFileFn,
): Promise<void> {
console.log(`[PDFPackager] 🔒 執行 qpdf (允許列印: ${allowPrint})`);
// qpdf --encrypt "" "" 256 --modify=none [--print=none] -- <input> <output>
const args = ["--encrypt", "", "", "256", "--modify=none"];
if (!allowPrint) {
args.push("--print=none");
}
args.push("--", inputPdf, outputPdf);
await execCommand("qpdf", args, execFile);
console.log(`[PDFPackager] ✅ PDF 權限保護已設定`);
}
/**
* 使 Python endesive PDF
*
*
* - PDF_SIGN_P12_PATH: /app/certs/default.p12
* - PDF_SIGN_P12_PASSWORD:
*
* 使
* - PDF_SIGN_P12_PATH: PKCS12
* - PDF_SIGN_P12_PASSWORD: PKCS12
* - PDF_SIGN_REASON: 簽章原因
* - PDF_SIGN_LOCATION: 簽章地點
* - PDF_SIGN_CONTACT: 聯絡資訊
*
* @param inputPdf PDF
* @param outputPdf PDF
* @param execFile execFile
*/
async function signPdf(inputPdf: string, outputPdf: string, execFile: ExecFileFn): Promise<void> {
console.log(`[PDFPackager] ✍️ 執行 PDF 數位簽章`);
// 使用預設憑證或自訂憑證
const p12Path = process.env.PDF_SIGN_P12_PATH || "/app/certs/default.p12";
// 檢查憑證是否存在(僅在非測試環境檢查)
if (!existsSync(p12Path) && process.env.NODE_ENV !== "test") {
throw new Error(
"SIGNING_NOT_CONFIGURED: 數位簽章憑證不存在。\n" +
`憑證路徑: ${p12Path}\n` +
"請確認 Docker 環境已正確安裝,或指定自訂憑證路徑:\n" +
" PDF_SIGN_P12_PATH=/path/to/your/certificate.p12",
);
}
// 使用 Python 腳本進行簽章
// 腳本路徑:/app/scripts/pdf_sign.pyDocker 環境)或相對於專案根目錄
const scriptPath = process.env.PDF_SIGN_SCRIPT_PATH || "/app/scripts/pdf_sign.py";
try {
await execCommand("python3", [scriptPath, inputPdf, outputPdf], execFile);
console.log(`[PDFPackager] ✅ PDF 數位簽章完成`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
// 檢查是否為憑證未配置錯誤
if (errorMessage.includes("PDF_SIGN_P12_PATH") || errorMessage.includes("未設定")) {
throw new Error(
"SIGNING_NOT_CONFIGURED: 數位簽章功能需要配置 PKCS12 憑證。\n" +
"請設定以下環境變數:\n" +
" PDF_SIGN_P12_PATH=/path/to/certificate.p12\n" +
" PDF_SIGN_P12_PASSWORD=your_password (選用)",
);
}
// 檢查是否為憑證讀取錯誤
if (errorMessage.includes("密碼錯誤") || errorMessage.includes("格式不正確")) {
throw new Error(`SIGNING_CERTIFICATE_ERROR: 無法讀取 PKCS12 憑證。${errorMessage}`);
}
throw new Error(`SIGNING_ERROR: PDF 簽章失敗。${errorMessage}`);
}
}
/**
* .tar
* @param sourceDir
* @param outputTar tar
* @param execFile execFile
*/
async function createTarArchive(
sourceDir: string,
outputTar: string,
execFile: ExecFileFn,
): Promise<void> {
console.log(`[PDFPackager] 📦 建立 tar 打包`);
// tar -cf <output.tar> -C <sourceDir> .
await execCommand("tar", ["-cf", outputTar, "-C", sourceDir, "."], execFile);
console.log(`[PDFPackager] ✅ tar 打包完成`);
}
// =============================================================================
// 主要 Pipeline 處理器
// =============================================================================
/**
* pipeline (png-*, jpg-*, jpeg-*)
*/
async function processImagesPipeline(
inputPdf: string,
workDir: string,
outDir: string,
parsed: ParsedChip,
execFile: ExecFileFn,
): Promise<string> {
const imgsDir = join(workDir, "imgs");
mkdirSync(imgsDir, { recursive: true });
// Step 1: pdftoppm 產生圖片
const format = parsed.imageFormat === "png" ? "png" : "jpeg";
const imageFiles = await pdfToImages(inputPdf, imgsDir, parsed.dpi, format, execFile);
// 如果是 jpg需要重新命名副檔名
if (parsed.imageFormat === "jpg") {
for (const imgFile of imageFiles) {
if (imgFile.endsWith(".jpg")) continue;
const newPath = imgFile.replace(/\.jpeg$/, ".jpg");
copyFileSync(imgFile, newPath);
unlinkSync(imgFile);
}
}
// Step 2: tar 打包
const outputTar = join(outDir, getOutputFileName(parsed.rawChip));
await createTarArchive(imgsDir, outputTar, execFile);
// Step 3: 清理暫存
cleanDir(imgsDir);
removeDir(imgsDir);
return outputTar;
}
/**
* PDF pipeline (pdf-*)
*/
async function processPdfImagePipeline(
inputPdf: string,
workDir: string,
outDir: string,
parsed: ParsedChip,
execFile: ExecFileFn,
): Promise<string> {
const imgsDir = join(workDir, "imgs");
const pdfDir = join(workDir, "pdf");
mkdirSync(imgsDir, { recursive: true });
mkdirSync(pdfDir, { recursive: true });
let currentPdf: string;
// Step 1: pdftoppm 產生圖片
const imageFiles = await pdfToImages(inputPdf, imgsDir, parsed.dpi, "png", execFile);
// Step 2: img2pdf 合成 PDF
currentPdf = join(pdfDir, "image_based.pdf");
await imagesToPdf(imageFiles, currentPdf, execFile);
// Step 3: (可選) 權限保護
if (parsed.protect) {
const protectedPdf = join(pdfDir, "protected.pdf");
await protectPdf(currentPdf, protectedPdf, parsed.protect === "p", execFile);
currentPdf = protectedPdf;
}
// Step 4: (可選) 簽章
if (parsed.sign) {
const signedPdf = join(pdfDir, "signed.pdf");
await signPdf(currentPdf, signedPdf, execFile);
currentPdf = signedPdf;
}
// Step 5: 輸出
const outputPdf = join(outDir, getOutputFileName(parsed.rawChip));
copyFileSync(currentPdf, outputPdf);
// Step 6: 清理
cleanDir(imgsDir);
removeDir(imgsDir);
cleanDir(pdfDir);
removeDir(pdfDir);
return outputPdf;
}
/**
* PDF/A pipeline (pdfa1b-*, pdfa2b-*)
*/
async function processPdfAPipeline(
inputPdf: string,
workDir: string,
outDir: string,
parsed: ParsedChip,
execFile: ExecFileFn,
): Promise<string> {
const imgsDir = join(workDir, "imgs");
const pdfDir = join(workDir, "pdf");
mkdirSync(pdfDir, { recursive: true });
let currentPdf: string;
if (parsed.pdfaSource === "i") {
// 從圖片產生
mkdirSync(imgsDir, { recursive: true });
// Step 1: pdftoppm 產生圖片
const imageFiles = await pdfToImages(inputPdf, imgsDir, parsed.dpi, "png", execFile);
// Step 2: img2pdf 合成 PDF
const imagePdf = join(pdfDir, "image_based.pdf");
await imagesToPdf(imageFiles, imagePdf, execFile);
// Step 3: 轉換為 PDF/A
currentPdf = join(pdfDir, "pdfa.pdf");
await convertToPdfA(imagePdf, currentPdf, parsed.pdfaLevel!, execFile);
// 清理圖片暫存
cleanDir(imgsDir);
removeDir(imgsDir);
} else {
// 從原始 PDF 直接轉換
// Step 1: 轉換為 PDF/A
currentPdf = join(pdfDir, "pdfa.pdf");
await convertToPdfA(inputPdf, currentPdf, parsed.pdfaLevel!, execFile);
}
// Step 4: (可選) 權限保護
if (parsed.protect) {
const protectedPdf = join(pdfDir, "protected.pdf");
await protectPdf(currentPdf, protectedPdf, parsed.protect === "p", execFile);
currentPdf = protectedPdf;
}
// Step 5: (可選) 簽章
if (parsed.sign) {
const signedPdf = join(pdfDir, "signed.pdf");
await signPdf(currentPdf, signedPdf, execFile);
currentPdf = signedPdf;
}
// Step 6: 輸出
const outputPdf = join(outDir, getOutputFileName(parsed.rawChip));
copyFileSync(currentPdf, outputPdf);
// Step 7: 清理
cleanDir(pdfDir);
removeDir(pdfDir);
return outputPdf;
}
/**
* all-<dpi> chips
*
*/
function getAllSubChips(dpi: 150 | 300 | 600): string[] {
return [
// 圖片
`png-${dpi}`,
`jpg-${dpi}`,
`jpeg-${dpi}`,
// 圖片型 PDF
`pdf-${dpi}`,
`pdf-${dpi}-p`,
`pdf-${dpi}-np`,
// PDF/A
`pdfa1b-i-${dpi}`,
`pdfa1b-o-${dpi}`,
`pdfa2b-i-${dpi}`,
`pdfa2b-o-${dpi}`,
];
}
/**
* all-* pipeline
*/
async function processAllPipeline(
inputPdf: string,
workDir: string,
outDir: string,
parsed: ParsedChip,
execFile: ExecFileFn,
): Promise<string> {
const allDir = join(outDir, "all");
mkdirSync(allDir, { recursive: true });
const subChips = getAllSubChips(parsed.dpi);
console.log(`[PDFPackager] 📦 all-${parsed.dpi}: 處理 ${subChips.length} 個子項目`);
for (const subChip of subChips) {
const subParsed = parseChip(subChip);
if (!subParsed) continue;
console.log(`[PDFPackager] ▶ 處理子項目: ${subChip}`);
try {
let outputFile: string;
switch (subParsed.kind) {
case "images":
outputFile = await processImagesPipeline(inputPdf, workDir, allDir, subParsed, execFile);
break;
case "pdf_image":
outputFile = await processPdfImagePipeline(
inputPdf,
workDir,
allDir,
subParsed,
execFile,
);
break;
case "pdfa":
outputFile = await processPdfAPipeline(inputPdf, workDir, allDir, subParsed, execFile);
break;
default:
continue;
}
console.log(`[PDFPackager] ✅ ${subChip} 完成: ${basename(outputFile)}`);
} catch (error) {
console.error(`[PDFPackager] ❌ ${subChip} 失敗: ${error}`);
// 繼續處理其他子項目
}
}
// 打包 all 目錄
const outputTar = join(outDir, getOutputFileName(parsed.rawChip));
await createTarArchive(allDir, outputTar, execFile);
// 清理
removeDir(allDir);
return outputTar;
}
// =============================================================================
// 主要轉換函數
// =============================================================================
/**
* PDF Packager
*
* @param filePath PDF
* @param fileType "pdf"
* @param convertTo chip
* @param targetPath
* @param _options
* @param execFile execFile
*/
export async function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
_options?: unknown,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
console.log(``);
console.log(`[PDFPackager] ╔════════════════════════════════════════╗`);
console.log(`[PDFPackager] ║ PDF Packager - 多功能 PDF 處理 ║`);
console.log(`[PDFPackager] ╚════════════════════════════════════════╝`);
console.log(``);
// Step 1: 解析 chip
console.log(`[PDFPackager] 🔧 解析 chip: ${convertTo}`);
const parsed = parseChip(convertTo);
if (!parsed) {
throw new Error(`INVALID_CHIP: 無效的 chip 名稱: ${convertTo}`);
}
console.log(`[PDFPackager] 類型: ${parsed.kind}`);
console.log(`[PDFPackager] DPI: ${parsed.dpi}`);
if (parsed.imageFormat) console.log(`[PDFPackager] 圖片格式: ${parsed.imageFormat}`);
if (parsed.pdfaLevel) console.log(`[PDFPackager] PDF/A 級別: ${parsed.pdfaLevel}`);
if (parsed.pdfaSource) console.log(`[PDFPackager] PDF/A 來源: ${parsed.pdfaSource}`);
if (parsed.protect) console.log(`[PDFPackager] 權限保護: ${parsed.protect}`);
if (parsed.sign) console.log(`[PDFPackager] 簽章: ${parsed.sign}`);
// Step 2: 準備工作目錄
const outputDir = dirname(targetPath);
const inputFileName = basename(filePath, `.${fileType}`);
const workDir = join(outputDir, `_pdfpackager_work_${inputFileName}_${Date.now()}`);
const outDir = outputDir;
mkdirSync(workDir, { recursive: true });
console.log(`[PDFPackager] 📁 工作目錄: ${workDir}`);
try {
let outputFile: string;
// Step 3: 根據 chip 類型執行對應 pipeline
switch (parsed.kind) {
case "images":
outputFile = await processImagesPipeline(filePath, workDir, outDir, parsed, execFile);
break;
case "pdf_image":
outputFile = await processPdfImagePipeline(filePath, workDir, outDir, parsed, execFile);
break;
case "pdfa":
outputFile = await processPdfAPipeline(filePath, workDir, outDir, parsed, execFile);
break;
case "all":
outputFile = await processAllPipeline(filePath, workDir, outDir, parsed, execFile);
break;
default:
throw new Error(`INVALID_CHIP_KIND: 未知的 chip 類型`);
}
console.log(``);
console.log(`[PDFPackager] ╔════════════════════════════════════════╗`);
console.log(`[PDFPackager] ║ ✅ PDF Packager 處理完成! ║`);
console.log(`[PDFPackager] ╚════════════════════════════════════════╝`);
console.log(`[PDFPackager] 輸出: ${basename(outputFile)}`);
console.log(``);
return "Done";
} catch (error) {
console.log(``);
console.log(`[PDFPackager] ╔════════════════════════════════════════╗`);
console.log(`[PDFPackager] ║ ❌ PDF Packager 處理失敗 ║`);
console.log(`[PDFPackager] ╚════════════════════════════════════════╝`);
console.log(`[PDFPackager] 錯誤: ${error}`);
console.log(``);
throw error;
} finally {
// 清理工作目錄
try {
removeDir(workDir);
console.log(`[PDFPackager] 🧹 已清理工作目錄`);
} catch (e) {
console.warn(`[PDFPackager] ⚠️ 清理工作目錄失敗: ${e}`);
}
}
}