dsh-file-convert 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.md +225 -0
- package/README.zh-CN.md +203 -0
- package/cordis.patch.yml +3 -0
- package/lib/config.d.ts +36 -0
- package/lib/config.js +17 -0
- package/lib/core/binaries/cache.d.ts +13 -0
- package/lib/core/binaries/cache.js +137 -0
- package/lib/core/binaries/download.d.ts +21 -0
- package/lib/core/binaries/download.js +140 -0
- package/lib/core/binary.d.ts +7 -0
- package/lib/core/binary.js +29 -0
- package/lib/core/converters/data.d.ts +14 -0
- package/lib/core/converters/data.js +145 -0
- package/lib/core/converters/image.d.ts +12 -0
- package/lib/core/converters/image.js +64 -0
- package/lib/core/converters/media.d.ts +21 -0
- package/lib/core/converters/media.js +135 -0
- package/lib/core/converters/office.d.ts +33 -0
- package/lib/core/converters/office.js +206 -0
- package/lib/core/converters/pdf-env.d.ts +1 -0
- package/lib/core/converters/pdf-env.js +8 -0
- package/lib/core/converters/pdf.d.ts +22 -0
- package/lib/core/converters/pdf.js +316 -0
- package/lib/core/detect.d.ts +16 -0
- package/lib/core/detect.js +121 -0
- package/lib/core/errors.d.ts +5 -0
- package/lib/core/errors.js +16 -0
- package/lib/core/formats.d.ts +11 -0
- package/lib/core/formats.js +60 -0
- package/lib/core/index.d.ts +25 -0
- package/lib/core/index.js +52 -0
- package/lib/core/inspect.d.ts +13 -0
- package/lib/core/inspect.js +134 -0
- package/lib/core/ocr.d.ts +38 -0
- package/lib/core/ocr.js +152 -0
- package/lib/core/optimizers.d.ts +29 -0
- package/lib/core/optimizers.js +275 -0
- package/lib/core/paths.d.ts +11 -0
- package/lib/core/paths.js +19 -0
- package/lib/core/router.d.ts +64 -0
- package/lib/core/router.js +288 -0
- package/lib/core/types.d.ts +199 -0
- package/lib/core/types.js +8 -0
- package/lib/core/utils/exec.d.ts +38 -0
- package/lib/core/utils/exec.js +89 -0
- package/lib/core/utils/pages.d.ts +12 -0
- package/lib/core/utils/pages.js +46 -0
- package/lib/format.d.ts +23 -0
- package/lib/format.js +84 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +51 -0
- package/lib/tools/batch-convert.d.ts +5 -0
- package/lib/tools/batch-convert.js +155 -0
- package/lib/tools/convert-file.d.ts +3 -0
- package/lib/tools/convert-file.js +57 -0
- package/lib/tools/inspect-file.d.ts +3 -0
- package/lib/tools/inspect-file.js +29 -0
- package/lib/tools/install-media.d.ts +9 -0
- package/lib/tools/install-media.js +55 -0
- package/lib/tools/install-ocr.d.ts +7 -0
- package/lib/tools/install-ocr.js +43 -0
- package/lib/tools/list-conversions.d.ts +2 -0
- package/lib/tools/list-conversions.js +18 -0
- package/lib/tools/optimize-file.d.ts +3 -0
- package/lib/tools/optimize-file.js +91 -0
- package/package.json +66 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { ExecError, execTool } from '../utils/exec.js';
|
|
3
|
+
import { convertError } from '../errors.js';
|
|
4
|
+
export const FFMPEG = {
|
|
5
|
+
name: 'ffmpeg',
|
|
6
|
+
commands: ['ffmpeg'],
|
|
7
|
+
configKey: 'ffmpegPath',
|
|
8
|
+
installHint: {
|
|
9
|
+
win32: 'winget install Gyan.FFmpeg (or scoop install ffmpeg)',
|
|
10
|
+
darwin: 'brew install ffmpeg',
|
|
11
|
+
linux: 'sudo apt install ffmpeg',
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
export const FFPROBE = {
|
|
15
|
+
name: 'ffprobe',
|
|
16
|
+
commands: ['ffprobe'],
|
|
17
|
+
configKey: 'ffprobePath',
|
|
18
|
+
installHint: FFMPEG.installHint, // always ships together
|
|
19
|
+
};
|
|
20
|
+
/** Options that ffmpeg must never prompt or decorate about. */
|
|
21
|
+
const GLOBAL = ['-hide_banner', '-nostdin', '-y'];
|
|
22
|
+
/**
|
|
23
|
+
* Audio/video conversions, delegated to a locally installed FFmpeg.
|
|
24
|
+
* The converter declares its dependencies; the router refuses to run (and
|
|
25
|
+
* list_conversions reports what is missing) until the binaries resolve.
|
|
26
|
+
*/
|
|
27
|
+
export class MediaConverter {
|
|
28
|
+
resolve;
|
|
29
|
+
id = 'media';
|
|
30
|
+
concurrency = 2;
|
|
31
|
+
binaryDeps = [FFMPEG, FFPROBE];
|
|
32
|
+
capabilities = [
|
|
33
|
+
{ from: 'mp4', to: 'gif' },
|
|
34
|
+
{ from: 'mp4', to: 'mp3' },
|
|
35
|
+
{ from: 'mov', to: 'mp4' },
|
|
36
|
+
{ from: 'wav', to: 'mp3' },
|
|
37
|
+
];
|
|
38
|
+
constructor(resolve) {
|
|
39
|
+
this.resolve = resolve;
|
|
40
|
+
}
|
|
41
|
+
async convert(req, ctx) {
|
|
42
|
+
const started = Date.now();
|
|
43
|
+
try {
|
|
44
|
+
const bytesIn = (await fs.stat(req.input)).size;
|
|
45
|
+
const ffmpeg = await this.required(FFMPEG);
|
|
46
|
+
switch (`${req.from}->${req.to}`) {
|
|
47
|
+
case 'mp4->gif':
|
|
48
|
+
await execTool(ffmpeg, gifArgs(req), this.execOpts(ctx));
|
|
49
|
+
break;
|
|
50
|
+
case 'mp4->mp3':
|
|
51
|
+
case 'wav->mp3':
|
|
52
|
+
await execTool(ffmpeg, audioArgs(req), this.execOpts(ctx));
|
|
53
|
+
break;
|
|
54
|
+
case 'mov->mp4':
|
|
55
|
+
await this.movToMp4(req, ctx, ffmpeg);
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
return fail(req, convertError('unsupported_conversion', `MediaConverter cannot handle ${req.from} -> ${req.to}.`));
|
|
59
|
+
}
|
|
60
|
+
const bytesOut = (await fs.stat(req.output)).size;
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
input: req.input,
|
|
64
|
+
output: req.output,
|
|
65
|
+
from: req.from,
|
|
66
|
+
to: req.to,
|
|
67
|
+
bytesIn,
|
|
68
|
+
bytesOut,
|
|
69
|
+
durationMs: Date.now() - started,
|
|
70
|
+
warnings: [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (err instanceof BinaryMissingError) {
|
|
75
|
+
return fail(req, convertError('missing_dependency', `Missing dependency: ${err.dep.name}.`, {
|
|
76
|
+
missing: [err.dep],
|
|
77
|
+
hint: `Install hint (${process.platform}): ${err.dep.installHint[platformKey()]}`,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
if (err instanceof ExecError) {
|
|
81
|
+
const code = err.code === 'timeout' ? 'timeout' : err.code === 'cancelled' ? 'cancelled' : 'conversion_failed';
|
|
82
|
+
return fail(req, convertError(code, `Failed to convert ${req.from} → ${req.to} (ffmpeg).`, { detail: err.stderr }));
|
|
83
|
+
}
|
|
84
|
+
return fail(req, convertError('conversion_failed', `Failed to convert ${req.from} → ${req.to}`, {
|
|
85
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** MOV→MP4: try a lossless container swap first, fall back to re-encoding. */
|
|
90
|
+
async movToMp4(req, ctx, ffmpeg) {
|
|
91
|
+
try {
|
|
92
|
+
await execTool(ffmpeg, [...GLOBAL, '-i', req.input, '-c', 'copy', '-movflags', '+faststart', req.output], this.execOpts(ctx));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
await execTool(ffmpeg, [...GLOBAL, '-i', req.input, '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '23', '-c:a', 'aac', '-b:a', '128k', '-movflags', '+faststart', req.output], this.execOpts(ctx));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async required(dep) {
|
|
99
|
+
const resolved = await this.resolve(dep);
|
|
100
|
+
if (!resolved) {
|
|
101
|
+
throw new BinaryMissingError(dep);
|
|
102
|
+
}
|
|
103
|
+
return resolved;
|
|
104
|
+
}
|
|
105
|
+
execOpts(ctx) {
|
|
106
|
+
return { timeoutMs: ctx.timeoutMs, signal: ctx.signal };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
class BinaryMissingError extends Error {
|
|
110
|
+
dep;
|
|
111
|
+
constructor(dep) {
|
|
112
|
+
super(`Missing dependency: ${dep.name}`);
|
|
113
|
+
this.dep = dep;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function platformKey() {
|
|
117
|
+
return process.platform === 'win32' ? 'win32' : process.platform === 'darwin' ? 'darwin' : 'linux';
|
|
118
|
+
}
|
|
119
|
+
function gifArgs(req) {
|
|
120
|
+
// Two-pass palette in one filter graph: split -> palettegen + paletteuse.
|
|
121
|
+
return [
|
|
122
|
+
...GLOBAL,
|
|
123
|
+
'-i', req.input,
|
|
124
|
+
'-filter_complex', '[0:v] fps=12,scale=480:-2:flags=lanczos,split [a][b];[a] palettegen [p];[b][p] paletteuse',
|
|
125
|
+
req.output,
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
function audioArgs(req) {
|
|
129
|
+
const quality = req.options.quality ?? 85;
|
|
130
|
+
const kbps = Math.round(64 + (Math.min(100, Math.max(1, quality)) / 100) * (320 - 64));
|
|
131
|
+
return [...GLOBAL, '-i', req.input, '-vn', '-codec:a', 'libmp3lame', '-b:a', `${kbps}k`, req.output];
|
|
132
|
+
}
|
|
133
|
+
function fail(req, error) {
|
|
134
|
+
return { ok: false, input: req.input, from: req.from, to: req.to, error };
|
|
135
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { BinaryDependency, ConvertContext, ConvertRequest, ConvertResult, Converter, ConversionCapability } from '../types.js';
|
|
2
|
+
export declare const SOFFICE: BinaryDependency;
|
|
3
|
+
export declare const PYTHON_PDF2DOCX: BinaryDependency;
|
|
4
|
+
/**
|
|
5
|
+
* Office documents to PDF via LibreOffice headless. LibreOffice is a
|
|
6
|
+
* single-instance application, so this converter serializes (concurrency 1)
|
|
7
|
+
* and always runs against its own UserInstallation profile — a document the
|
|
8
|
+
* user has open in LibreOffice GUI must never block or break a conversion.
|
|
9
|
+
*/
|
|
10
|
+
export declare class OfficeConverter implements Converter {
|
|
11
|
+
private readonly resolve;
|
|
12
|
+
readonly id = "office";
|
|
13
|
+
readonly concurrency = 1;
|
|
14
|
+
readonly binaryDeps: BinaryDependency[];
|
|
15
|
+
readonly capabilities: ConversionCapability[];
|
|
16
|
+
constructor(resolve: (dep: BinaryDependency) => Promise<string | null>);
|
|
17
|
+
convert(req: ConvertRequest, ctx: ConvertContext): Promise<ConvertResult>;
|
|
18
|
+
private required;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* PDF → DOCX (EXPERIMENTAL): layout reconstruction via the Python pdf2docx
|
|
22
|
+
* package. Works well on text PDFs; scanned PDFs need OCR (not implemented).
|
|
23
|
+
* The dependency probe checks that python can actually import pdf2docx.
|
|
24
|
+
*/
|
|
25
|
+
export declare class PdfToDocxConverter implements Converter {
|
|
26
|
+
private readonly resolve;
|
|
27
|
+
readonly id = "pdf-docx";
|
|
28
|
+
readonly concurrency = 1;
|
|
29
|
+
readonly binaryDeps: BinaryDependency[];
|
|
30
|
+
readonly capabilities: ConversionCapability[];
|
|
31
|
+
constructor(resolve: (dep: BinaryDependency) => Promise<string | null>);
|
|
32
|
+
convert(req: ConvertRequest, ctx: ConvertContext): Promise<ConvertResult>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { ExecError, execTool } from '../utils/exec.js';
|
|
6
|
+
import { convertError } from '../errors.js';
|
|
7
|
+
export const SOFFICE = {
|
|
8
|
+
name: 'soffice',
|
|
9
|
+
displayName: 'LibreOffice',
|
|
10
|
+
commands: ['soffice'],
|
|
11
|
+
configKey: 'sofficePath',
|
|
12
|
+
extraPaths: {
|
|
13
|
+
win32: [
|
|
14
|
+
'C:\\Program Files\\LibreOffice\\program\\soffice.exe',
|
|
15
|
+
'C:\\Program Files (x86)\\LibreOffice\\program\\soffice.exe',
|
|
16
|
+
],
|
|
17
|
+
darwin: ['/Applications/LibreOffice.app/Contents/MacOS/soffice'],
|
|
18
|
+
linux: ['/usr/lib/libreoffice/program/soffice', '/opt/libreoffice/program/soffice'],
|
|
19
|
+
},
|
|
20
|
+
installHint: {
|
|
21
|
+
win32: 'winget install TheDocumentFoundation.LibreOffice',
|
|
22
|
+
darwin: 'brew install --cask libreoffice',
|
|
23
|
+
linux: 'sudo apt install libreoffice-writer libreoffice-calc libreoffice-impress',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
export const PYTHON_PDF2DOCX = {
|
|
27
|
+
name: 'python',
|
|
28
|
+
displayName: 'python with the pdf2docx package',
|
|
29
|
+
commands: ['python', 'python3', 'py'],
|
|
30
|
+
configKey: 'pythonPath',
|
|
31
|
+
probe: async (pythonPath) => {
|
|
32
|
+
try {
|
|
33
|
+
await execTool(pythonPath, ['-c', 'import pdf2docx'], { timeoutMs: 30_000 });
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
installHint: {
|
|
41
|
+
win32: 'install Python from python.org, then: pip install pdf2docx',
|
|
42
|
+
darwin: 'brew install python && pip3 install pdf2docx',
|
|
43
|
+
linux: 'sudo apt install python3-pip && pip3 install pdf2docx',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
async function exists(p) {
|
|
47
|
+
try {
|
|
48
|
+
await fs.access(p);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function fail(req, error) {
|
|
56
|
+
return { ok: false, input: req.input, from: req.from, to: req.to, error };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Office documents to PDF via LibreOffice headless. LibreOffice is a
|
|
60
|
+
* single-instance application, so this converter serializes (concurrency 1)
|
|
61
|
+
* and always runs against its own UserInstallation profile — a document the
|
|
62
|
+
* user has open in LibreOffice GUI must never block or break a conversion.
|
|
63
|
+
*/
|
|
64
|
+
export class OfficeConverter {
|
|
65
|
+
resolve;
|
|
66
|
+
id = 'office';
|
|
67
|
+
concurrency = 1;
|
|
68
|
+
binaryDeps = [SOFFICE];
|
|
69
|
+
capabilities = [
|
|
70
|
+
{ from: 'docx', to: 'pdf' },
|
|
71
|
+
{ from: 'pptx', to: 'pdf' },
|
|
72
|
+
{ from: 'xlsx', to: 'pdf' },
|
|
73
|
+
];
|
|
74
|
+
constructor(resolve) {
|
|
75
|
+
this.resolve = resolve;
|
|
76
|
+
}
|
|
77
|
+
async convert(req, ctx) {
|
|
78
|
+
const started = Date.now();
|
|
79
|
+
try {
|
|
80
|
+
const soffice = await this.required(SOFFICE, req, ctx);
|
|
81
|
+
const bytesIn = (await fs.stat(req.input)).size;
|
|
82
|
+
// A stable private profile: created once, reused across conversions.
|
|
83
|
+
const profile = path.join(os.homedir(), '.dsh-file-convert', 'lo-profile');
|
|
84
|
+
await fs.mkdir(profile, { recursive: true });
|
|
85
|
+
const outDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-office-'));
|
|
86
|
+
try {
|
|
87
|
+
await execTool(soffice, [
|
|
88
|
+
'--headless', '--norestore', '--nolockcheck',
|
|
89
|
+
`-env:UserInstallation=${pathToFileURL(profile).href}`,
|
|
90
|
+
'--convert-to', 'pdf', '--outdir', outDir,
|
|
91
|
+
req.input,
|
|
92
|
+
], { timeoutMs: ctx.timeoutMs, signal: ctx.signal });
|
|
93
|
+
const produced = path.join(outDir, path.basename(req.input, path.extname(req.input)) + '.pdf');
|
|
94
|
+
if (!(await exists(produced))) {
|
|
95
|
+
return fail(req, convertError('conversion_failed', `LibreOffice did not produce a PDF for ${req.input}.`, {
|
|
96
|
+
hint: 'The document may be corrupt or password-protected; open it once in LibreOffice to check.',
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
await fs.copyFile(produced, req.output);
|
|
100
|
+
return {
|
|
101
|
+
ok: true,
|
|
102
|
+
input: req.input,
|
|
103
|
+
output: req.output,
|
|
104
|
+
from: req.from,
|
|
105
|
+
to: req.to,
|
|
106
|
+
bytesIn,
|
|
107
|
+
bytesOut: (await fs.stat(req.output)).size,
|
|
108
|
+
durationMs: Date.now() - started,
|
|
109
|
+
warnings: [],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
await fs.rm(outDir, { recursive: true, force: true }).catch(() => undefined);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (err instanceof OfficeDependencyError) {
|
|
118
|
+
return fail(req, convertError('missing_dependency', `Missing external dependency: ${err.dep.displayName ?? err.dep.name}.`, {
|
|
119
|
+
missing: [err.dep],
|
|
120
|
+
hint: `Install hint (${process.platform}): ${err.dep.installHint[platformKey()]}`,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
if (err instanceof ExecError) {
|
|
124
|
+
const code = err.code === 'timeout' ? 'timeout' : err.code === 'cancelled' ? 'cancelled' : 'conversion_failed';
|
|
125
|
+
return fail(req, convertError(code, `Failed to convert ${req.from} → ${req.to} (LibreOffice).`, { detail: err.stderr }));
|
|
126
|
+
}
|
|
127
|
+
return fail(req, convertError('conversion_failed', `Failed to convert ${req.from} → ${req.to}`, {
|
|
128
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async required(dep, req, ctx) {
|
|
133
|
+
const resolved = await this.resolve(dep);
|
|
134
|
+
if (!resolved)
|
|
135
|
+
throw new OfficeDependencyError(dep);
|
|
136
|
+
void ctx;
|
|
137
|
+
return resolved;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
class OfficeDependencyError extends Error {
|
|
141
|
+
dep;
|
|
142
|
+
constructor(dep) {
|
|
143
|
+
super(`Missing dependency: ${dep.name}`);
|
|
144
|
+
this.dep = dep;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function platformKey() {
|
|
148
|
+
return process.platform === 'win32' ? 'win32' : process.platform === 'darwin' ? 'darwin' : 'linux';
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* PDF → DOCX (EXPERIMENTAL): layout reconstruction via the Python pdf2docx
|
|
152
|
+
* package. Works well on text PDFs; scanned PDFs need OCR (not implemented).
|
|
153
|
+
* The dependency probe checks that python can actually import pdf2docx.
|
|
154
|
+
*/
|
|
155
|
+
export class PdfToDocxConverter {
|
|
156
|
+
resolve;
|
|
157
|
+
id = 'pdf-docx';
|
|
158
|
+
concurrency = 1;
|
|
159
|
+
binaryDeps = [PYTHON_PDF2DOCX];
|
|
160
|
+
capabilities = [{ from: 'pdf', to: 'docx', experimental: true }];
|
|
161
|
+
constructor(resolve) {
|
|
162
|
+
this.resolve = resolve;
|
|
163
|
+
}
|
|
164
|
+
async convert(req, ctx) {
|
|
165
|
+
const started = Date.now();
|
|
166
|
+
try {
|
|
167
|
+
const python = await this.resolve(PYTHON_PDF2DOCX);
|
|
168
|
+
if (!python) {
|
|
169
|
+
return fail(req, convertError('missing_dependency', 'Missing external dependency: python with the pdf2docx package.', {
|
|
170
|
+
missing: [PYTHON_PDF2DOCX],
|
|
171
|
+
hint: `Install hint (${process.platform}): ${PYTHON_PDF2DOCX.installHint[platformKey()]}`,
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
const bytesIn = (await fs.stat(req.input)).size;
|
|
175
|
+
// pdf2docx has no __main__ module and its console script may not be on
|
|
176
|
+
// PATH - drive the library API directly through the resolved interpreter.
|
|
177
|
+
await execTool(python, [
|
|
178
|
+
'-c', 'import sys; from pdf2docx import parse; parse(sys.argv[1], sys.argv[2])',
|
|
179
|
+
req.input, req.output,
|
|
180
|
+
], { timeoutMs: ctx.timeoutMs, signal: ctx.signal });
|
|
181
|
+
if (!(await exists(req.output))) {
|
|
182
|
+
return fail(req, convertError('conversion_failed', 'pdf2docx reported success but produced no file.'));
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
input: req.input,
|
|
187
|
+
output: req.output,
|
|
188
|
+
from: req.from,
|
|
189
|
+
to: req.to,
|
|
190
|
+
bytesIn,
|
|
191
|
+
bytesOut: (await fs.stat(req.output)).size,
|
|
192
|
+
durationMs: Date.now() - started,
|
|
193
|
+
warnings: ['PDF → DOCX is experimental: complex layouts may shift; check the result.'],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
if (err instanceof ExecError) {
|
|
198
|
+
const code = err.code === 'timeout' ? 'timeout' : err.code === 'cancelled' ? 'cancelled' : 'conversion_failed';
|
|
199
|
+
return fail(req, convertError(code, `Failed to convert ${req.from} → ${req.to} (pdf2docx).`, { detail: err.stderr }));
|
|
200
|
+
}
|
|
201
|
+
return fail(req, convertError('conversion_failed', `Failed to convert ${req.from} → ${req.to}`, {
|
|
202
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// pdfjs-dist expects browser globals (DOMMatrix, Path2D, ImageData) that Node
|
|
2
|
+
// does not provide. @napi-rs/canvas ships compatible implementations.
|
|
3
|
+
// IMPORTANT: import this module before any pdfjs-dist import.
|
|
4
|
+
import { DOMMatrix, ImageData, Path2D } from '@napi-rs/canvas';
|
|
5
|
+
const globals = globalThis;
|
|
6
|
+
globals.DOMMatrix ??= DOMMatrix;
|
|
7
|
+
globals.Path2D ??= Path2D;
|
|
8
|
+
globals.ImageData ??= ImageData;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import './pdf-env.js';
|
|
2
|
+
import type { BinaryDependency, ConvertContext, ConvertRequest, ConvertResult, Converter, ConversionCapability } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* PDF conversions, fully local: rasterization via pdfjs-dist + @napi-rs/canvas
|
|
5
|
+
* (prebuilt npm binaries), text extraction via pdfjs-dist. No Poppler needed.
|
|
6
|
+
*/
|
|
7
|
+
export declare class PdfConverter implements Converter {
|
|
8
|
+
private readonly resolve?;
|
|
9
|
+
readonly id = "pdf";
|
|
10
|
+
readonly concurrency = 2;
|
|
11
|
+
readonly binaryDeps: never[];
|
|
12
|
+
readonly capabilities: ConversionCapability[];
|
|
13
|
+
/** Optional binary resolver, needed only for OCR (Tesseract). */
|
|
14
|
+
constructor(resolve?: ((dep: BinaryDependency) => Promise<string | null>) | undefined);
|
|
15
|
+
convert(req: ConvertRequest, ctx: ConvertContext): Promise<ConvertResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Multi-page PDFs produce `<base>-<n>.<ext>` files named after the REAL page
|
|
18
|
+
* number; converting a single page (or a one-page PDF) writes exactly `output`.
|
|
19
|
+
*/
|
|
20
|
+
private toImages;
|
|
21
|
+
private toText;
|
|
22
|
+
}
|