feat: 新增 deark 引擎支援解包/解析多種格式

- 新增 deark 轉換器 (src/converters/deark.ts)
- 支援 100+ 種輸入格式(壓縮檔、舊圖片、字型、可執行檔資源等)
- 輸出為 .tar 封裝
- Dockerfile 添加 deark 從源碼編譯安裝
- 更新 main.ts 註冊新引擎
- 添加單元測試 (tests/converters/deark.test.ts)
- 更新文件 (docs/功能說明/轉換器.md)
- CI/CD 添加 deark 版本驗證
This commit is contained in:
Your Name 2026-01-23 23:33:26 +08:00
parent 2a28e4073a
commit ef3bf66cce
6 changed files with 445 additions and 3 deletions

286
src/converters/deark.ts Normal file
View file

@ -0,0 +1,286 @@
import { execFile as execFileOriginal } from "node:child_process";
import { mkdirSync, existsSync, readdirSync, unlinkSync, rmdirSync, statSync } from "node:fs";
import { join, basename, dirname } from "node:path";
import { ExecFileFn } from "./types";
/**
* deark
*
* deark
* - ZIPLHAARC
* - BMPICOPCXGIF
* - EXE
*
* @see https://github.com/jsummers/deark
* @see https://entropymine.com/deark/
*/
export const properties = {
from: {
// 壓縮/封存格式
archive: [
"zip",
"lha",
"lzh",
"arc",
"arj",
"zoo",
"z",
"gz",
"bz2",
"xz",
"cab",
"sit",
"sitx",
"stuffit",
"hqx",
"bin",
"macbin",
"cpio",
"rpm",
"deb",
"ar",
"a",
],
// 圖片格式
images: [
"ico",
"cur",
"ani",
"icns",
"bmp",
"dib",
"pcx",
"dcx",
"pict",
"pic",
"pct",
"wmf",
"emf",
"gem",
"img",
"mac",
"msp",
"iff",
"ilbm",
"lbm",
"ham",
"xbm",
"xpm",
"ras",
"sun",
"tga",
"vst",
"icb",
"vda",
"sgi",
"rgb",
"rgba",
"bw",
"int",
"inta",
"psd",
"xcf",
"ora",
"kra",
"psp",
"jbig",
"jbg",
"fpx",
],
// 字型格式
fonts: ["fon", "fnt", "psf", "bdf", "pcf"],
// 可執行檔 / 資源檔
binary: ["exe", "dll", "com", "ne", "mz", "res", "rsrc", "icl"],
// 其他格式
other: [
"swf",
"fla",
"amiga",
"adf",
"dms",
"d64",
"t64",
"prg",
"crt",
"tap",
"tzx",
"wav",
"riff",
"avi",
"ani",
],
},
to: {
archive: ["extract"],
images: ["extract"],
fonts: ["extract"],
binary: ["extract"],
other: ["extract"],
},
// deark 輸出可能是多個檔案,使用 archive 模式
outputMode: "archive" as const,
};
/**
* Helper function to create a .tar archive from a directory
*/
function createTarArchive(
sourceDir: string,
outputTar: string,
execFile: ExecFileFn,
): Promise<void> {
return new Promise((resolve, reject) => {
execFile("tar", ["-cf", outputTar, "-C", sourceDir, "."], (error, stdout, stderr) => {
if (error) {
reject(`tar error: ${error}`);
return;
}
if (stdout) {
console.log(`tar stdout: ${stdout}`);
}
if (stderr) {
console.error(`tar stderr: ${stderr}`);
}
resolve();
});
});
}
/**
* Helper function to remove a directory recursively
*/
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);
}
}
/**
* Count files in a directory (including subdirectories)
*/
function countFiles(dirPath: string): number {
if (!existsSync(dirPath)) return 0;
let count = 0;
const files = readdirSync(dirPath, { withFileTypes: true });
for (const file of files) {
if (file.isDirectory()) {
count += countFiles(join(dirPath, file.name));
} else {
count++;
}
}
return count;
}
export async function convert(
filePath: string,
fileType: string,
convertTo: string,
targetPath: string,
options?: unknown,
execFile: ExecFileFn = execFileOriginal,
): Promise<string> {
// Create a temporary output directory for deark
const outputDir = dirname(targetPath);
const inputFileName = basename(filePath, `.${fileType}`);
const dearkOutputDir = join(outputDir, `${inputFileName}_deark_extract`);
// Ensure output directory exists
if (!existsSync(dearkOutputDir)) {
mkdirSync(dearkOutputDir, { recursive: true });
}
/**
* Execute deark with proper options
*
* deark
* -od <dir>
* -a
* -zip ZIP 使 tar
* -l
* -m <module>
*/
const runDeark = (): Promise<void> => {
return new Promise((resolve, reject) => {
// Build deark command arguments
const args = [
"-od",
dearkOutputDir, // Output directory
"-a", // Extract all (including auxiliary files)
"-nomodtime", // Don't modify file timestamps
filePath, // Input file
];
console.log(`[deark] Running: deark ${args.join(" ")}`);
execFile("deark", args, (error, stdout, stderr) => {
if (stdout) {
console.log(`deark stdout: ${stdout}`);
}
if (stderr) {
console.error(`deark stderr: ${stderr}`);
}
if (error) {
// Check for common deark errors
const errorStr = String(error) + String(stderr);
if (errorStr.includes("Unknown or unsupported")) {
reject(new Error(`deark: 不支援此檔案格式 (${fileType})`));
} else if (errorStr.includes("Failed to read")) {
reject(new Error(`deark: 無法讀取檔案 - ${filePath}`));
} else {
reject(new Error(`deark error: ${error}`));
}
return;
}
resolve();
});
});
};
// Execute deark
try {
await runDeark();
} catch (error) {
// Cleanup on error
removeDir(dearkOutputDir);
throw error;
}
// Check if any files were extracted
const fileCount = countFiles(dearkOutputDir);
if (fileCount === 0) {
removeDir(dearkOutputDir);
throw new Error(`deark: 沒有可提取的檔案 (${fileType})`);
}
// Create .tar archive from the output directory
try {
const tarOutputPath = `${targetPath}.tar`;
await createTarArchive(dearkOutputDir, tarOutputPath, execFile);
console.log(`[deark] Created tar archive: ${tarOutputPath} (${fileCount} files)`);
// Cleanup temporary directory
removeDir(dearkOutputDir);
return `Done - extracted ${fileCount} file(s)`;
} catch (error) {
removeDir(dearkOutputDir);
throw new Error(`Failed to create tar archive: ${error}`);
}
}

View file

@ -5,6 +5,7 @@ import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalize
import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
import { convert as convertCalibre, properties as propertiesCalibre } from "./calibre";
import { convert as convertDasel, properties as propertiesDasel } from "./dasel";
import { convert as convertDeark, properties as propertiesDeark } from "./deark";
import { convert as convertDvisvgm, properties as propertiesDvisvgm } from "./dvisvgm";
import { convert as convertFFmpeg, properties as propertiesFFmpeg } from "./ffmpeg";
import {
@ -161,6 +162,10 @@ const properties: Record<
properties: propertiesOcrMyPdf,
converter: convertOcrMyPdf,
},
deark: {
properties: propertiesDeark,
converter: convertDeark,
},
};
function chunks<T>(arr: T[], size: number): T[][] {