emf-converter 4.23.0 → 4.24.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/README.md +248 -8
- package/THIRD_PARTY_NOTICES +88 -0
- package/dist/browser.js +14437 -3413
- package/dist/browser.mjs +14432 -3414
- package/dist/cli.mjs +357 -0
- package/dist/index.d.mts +359 -15
- package/dist/index.d.ts +359 -15
- package/dist/index.js +14407 -3383
- package/dist/index.mjs +14402 -3384
- package/package.json +10 -3
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import { resolve, dirname, extname, basename, join, sep } from 'path';
|
|
5
|
+
import { parseArgs } from 'util';
|
|
6
|
+
import { convertMetafileToPdf, convertMetafile, loadSystemFonts } from './index.mjs';
|
|
7
|
+
|
|
8
|
+
var CLI_FORMATS = [
|
|
9
|
+
{ name: "png", extensions: [".png"] },
|
|
10
|
+
{ name: "jpeg", extensions: [".jpg", ".jpeg"] },
|
|
11
|
+
{ name: "webp", extensions: [".webp"] },
|
|
12
|
+
{ name: "svg", extensions: [".svg"] },
|
|
13
|
+
{ name: "pdf", extensions: [".pdf"] }
|
|
14
|
+
];
|
|
15
|
+
var INPUT_EXTENSIONS = [".emf", ".wmf", ".emz", ".wmz"];
|
|
16
|
+
var OPTIONS = {
|
|
17
|
+
format: { type: "string", short: "f" },
|
|
18
|
+
out: { type: "string", short: "o" },
|
|
19
|
+
width: { type: "string" },
|
|
20
|
+
height: { type: "string" },
|
|
21
|
+
scale: { type: "string" },
|
|
22
|
+
quality: { type: "string" },
|
|
23
|
+
background: { type: "string" },
|
|
24
|
+
antialias: { type: "boolean" },
|
|
25
|
+
lossy: { type: "boolean" },
|
|
26
|
+
fonts: { type: "string" },
|
|
27
|
+
quiet: { type: "boolean", short: "q" },
|
|
28
|
+
help: { type: "boolean", short: "h" },
|
|
29
|
+
version: { type: "boolean", short: "v" }
|
|
30
|
+
};
|
|
31
|
+
var HELP_TEXT = `Usage: emf-converter <files...> [options]
|
|
32
|
+
|
|
33
|
+
Convert EMF, EMF+, WMF and gzip-compressed (.emz, .wmz) metafiles to PNG,
|
|
34
|
+
JPEG, WebP, SVG or PDF.
|
|
35
|
+
|
|
36
|
+
Inputs:
|
|
37
|
+
<files...> Files to convert. "-" reads one file from stdin.
|
|
38
|
+
Wildcards (* and ?) are expanded by the CLI itself.
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
-f, --format <fmt> Output format: png, jpeg, webp, svg or pdf. Default:
|
|
42
|
+
from the --out extension, else png.
|
|
43
|
+
-o, --out <path> Output file for one input, or a directory (created if
|
|
44
|
+
missing) for several. Default: next to each input,
|
|
45
|
+
with the new extension. "-" writes to stdout.
|
|
46
|
+
Stdin input without --out also writes to stdout.
|
|
47
|
+
--width <px> Maximum output width in pixels.
|
|
48
|
+
--height <px> Maximum output height in pixels.
|
|
49
|
+
--scale <n> DPI scale factor (default 1; the library clamps to 4).
|
|
50
|
+
--quality <0-1> JPEG quality (default 0.92), or lossy WebP quality
|
|
51
|
+
(default 0.8).
|
|
52
|
+
--lossy Lossy WebP instead of the default lossless WebP.
|
|
53
|
+
--background <c> JPEG colour under transparent areas, #rgb or #rrggbb
|
|
54
|
+
(default #ffffff). JPEG only.
|
|
55
|
+
--antialias Smooth shape edges instead of reproducing Windows'
|
|
56
|
+
own rasterisation.
|
|
57
|
+
--fonts <value> "system" (default): the installed fonts, so text
|
|
58
|
+
matches Windows; "none": no font files; or a
|
|
59
|
+
directory of .ttf, .ttc, .fon or .fnt files.
|
|
60
|
+
-q, --quiet Print only errors.
|
|
61
|
+
-h, --help Show this help.
|
|
62
|
+
-v, --version Print the version.
|
|
63
|
+
|
|
64
|
+
Exit codes: 0 all files converted, 1 one or more files failed (the rest are
|
|
65
|
+
still converted), 2 usage error.
|
|
66
|
+
|
|
67
|
+
Examples:
|
|
68
|
+
emf-converter drawing.emf
|
|
69
|
+
emf-converter drawing.emf --width 1200 -o drawing.jpg
|
|
70
|
+
emf-converter "charts/*.wmf" -f svg -o out/
|
|
71
|
+
cat drawing.emz | emf-converter - -f png > drawing.png
|
|
72
|
+
`;
|
|
73
|
+
var UsageError = class extends Error {
|
|
74
|
+
};
|
|
75
|
+
async function main(argv, io) {
|
|
76
|
+
try {
|
|
77
|
+
return await run(argv, io);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (err instanceof UsageError) {
|
|
80
|
+
io.stderr.write(`emf-converter: ${err.message}
|
|
81
|
+
Try 'emf-converter --help' for more information.
|
|
82
|
+
`);
|
|
83
|
+
return 2;
|
|
84
|
+
}
|
|
85
|
+
io.stderr.write(`emf-converter: ${describeError(err)}
|
|
86
|
+
`);
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function run(argv, io) {
|
|
91
|
+
const { values, positionals } = parseCommandLine(argv);
|
|
92
|
+
if (values.help) {
|
|
93
|
+
io.stdout.write(HELP_TEXT);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
if (values.version) {
|
|
97
|
+
io.stdout.write(`${readVersion()}
|
|
98
|
+
`);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
if (positionals.length === 0) {
|
|
102
|
+
throw new UsageError("no input files given");
|
|
103
|
+
}
|
|
104
|
+
const quiet = values.quiet === true;
|
|
105
|
+
const toStdout = values.out === "-";
|
|
106
|
+
const outPath = values.out !== void 0 && !toStdout ? resolve(io.cwd, values.out) : void 0;
|
|
107
|
+
const format = chooseFormat(values.format, values.out);
|
|
108
|
+
const settings = convertSettings(values);
|
|
109
|
+
const fonts = chooseFonts(values.fonts ?? "system");
|
|
110
|
+
if (fonts.kind === "dir" && !await isDirectory(resolve(io.cwd, fonts.dir))) {
|
|
111
|
+
throw new UsageError(`--fonts: "${fonts.dir}" is not a directory`);
|
|
112
|
+
}
|
|
113
|
+
const jobs = await expandInputs(positionals, io.cwd);
|
|
114
|
+
const several = positionals.length > 1 || positionals.some((p) => /[*?]/.test(p));
|
|
115
|
+
const plans = await planOutputs(jobs, { format, outPath, toStdout, several});
|
|
116
|
+
let fontData;
|
|
117
|
+
const loadFonts = () => {
|
|
118
|
+
fontData ?? (fontData = fonts.kind === "none" ? Promise.resolve(void 0) : loadSystemFonts(fonts.kind === "dir" ? { dirs: [resolve(io.cwd, fonts.dir)] } : void 0));
|
|
119
|
+
return fontData;
|
|
120
|
+
};
|
|
121
|
+
let stdinData;
|
|
122
|
+
const readStdin = () => {
|
|
123
|
+
stdinData ?? (stdinData = collect(io.stdin));
|
|
124
|
+
return stdinData;
|
|
125
|
+
};
|
|
126
|
+
let failures = 0;
|
|
127
|
+
const fail = (label, message) => {
|
|
128
|
+
failures++;
|
|
129
|
+
io.stderr.write(`emf-converter: ${label}: ${message}
|
|
130
|
+
`);
|
|
131
|
+
};
|
|
132
|
+
for (const plan of plans) {
|
|
133
|
+
const { job } = plan;
|
|
134
|
+
if (job.kind === "error") {
|
|
135
|
+
fail(job.label, job.message);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const bytes = job.kind === "stdin" ? await readStdin() : await fs.readFile(job.path);
|
|
140
|
+
const fontList = await loadFonts();
|
|
141
|
+
const options = { ...settings, format, ...fontList ? { fonts: fontList } : {} };
|
|
142
|
+
const image = format === "pdf" ? await convertMetafileToPdf(bytes, options).then((data) => data && { data }) : await convertMetafile(bytes, options);
|
|
143
|
+
if (!image) {
|
|
144
|
+
fail(job.label, "not a readable EMF or WMF file, or the conversion failed");
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (plan.target === void 0) {
|
|
148
|
+
io.stdout.write(image.data);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
await fs.mkdir(dirname(plan.target), { recursive: true });
|
|
152
|
+
await fs.writeFile(plan.target, image.data);
|
|
153
|
+
if (!quiet) {
|
|
154
|
+
io.stderr.write(`${job.label} -> ${plan.target}
|
|
155
|
+
`);
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
fail(job.label, describeError(err));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return failures > 0 ? 1 : 0;
|
|
162
|
+
}
|
|
163
|
+
function parseCommandLine(argv) {
|
|
164
|
+
try {
|
|
165
|
+
const parsed = parseArgs({ args: [...argv], options: OPTIONS, allowPositionals: true, strict: true });
|
|
166
|
+
return { values: parsed.values, positionals: parsed.positionals };
|
|
167
|
+
} catch (err) {
|
|
168
|
+
throw new UsageError(describeError(err));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function chooseFormat(value, out) {
|
|
172
|
+
if (value !== void 0) {
|
|
173
|
+
const found = CLI_FORMATS.find((f) => f.name === value.toLowerCase());
|
|
174
|
+
if (!found) {
|
|
175
|
+
throw new UsageError(`unknown format "${value}" (use ${CLI_FORMATS.map((f) => f.name).join(", ")})`);
|
|
176
|
+
}
|
|
177
|
+
return found.name;
|
|
178
|
+
}
|
|
179
|
+
if (out !== void 0 && out !== "-") {
|
|
180
|
+
const ext = extname(out).toLowerCase();
|
|
181
|
+
const found = CLI_FORMATS.find((f) => f.extensions.includes(ext));
|
|
182
|
+
if (found) {
|
|
183
|
+
return found.name;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return "png";
|
|
187
|
+
}
|
|
188
|
+
function convertSettings(values) {
|
|
189
|
+
const settings = {};
|
|
190
|
+
if (values.width !== void 0) {
|
|
191
|
+
settings.maxWidth = parseNumber("width", values.width, (n) => n > 0, "a positive number");
|
|
192
|
+
}
|
|
193
|
+
if (values.height !== void 0) {
|
|
194
|
+
settings.maxHeight = parseNumber("height", values.height, (n) => n > 0, "a positive number");
|
|
195
|
+
}
|
|
196
|
+
if (values.scale !== void 0) {
|
|
197
|
+
settings.dpiScale = parseNumber("scale", values.scale, (n) => n > 0, "a positive number");
|
|
198
|
+
}
|
|
199
|
+
if (values.quality !== void 0) {
|
|
200
|
+
settings.quality = parseNumber("quality", values.quality, (n) => n >= 0 && n <= 1, "a number from 0 to 1");
|
|
201
|
+
}
|
|
202
|
+
if (values.background !== void 0) {
|
|
203
|
+
if (!/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(values.background)) {
|
|
204
|
+
throw new UsageError(`--background needs #rgb or #rrggbb, not "${values.background}"`);
|
|
205
|
+
}
|
|
206
|
+
settings.background = values.background;
|
|
207
|
+
}
|
|
208
|
+
if (values.antialias) {
|
|
209
|
+
settings.gdiAntialias = true;
|
|
210
|
+
}
|
|
211
|
+
if (values.lossy) {
|
|
212
|
+
settings.lossless = false;
|
|
213
|
+
}
|
|
214
|
+
return settings;
|
|
215
|
+
}
|
|
216
|
+
function parseNumber(flag, raw, valid, expected) {
|
|
217
|
+
const n = raw.trim() === "" ? Number.NaN : Number(raw);
|
|
218
|
+
if (!Number.isFinite(n) || !valid(n)) {
|
|
219
|
+
throw new UsageError(`--${flag} needs ${expected}, not "${raw}"`);
|
|
220
|
+
}
|
|
221
|
+
return n;
|
|
222
|
+
}
|
|
223
|
+
function chooseFonts(value) {
|
|
224
|
+
if (value === "system" || value === "none") {
|
|
225
|
+
return { kind: value };
|
|
226
|
+
}
|
|
227
|
+
return { kind: "dir", dir: value };
|
|
228
|
+
}
|
|
229
|
+
async function expandInputs(args, cwd) {
|
|
230
|
+
const jobs = [];
|
|
231
|
+
for (const arg of args) {
|
|
232
|
+
if (arg === "-") {
|
|
233
|
+
jobs.push({ kind: "stdin", label: "stdin" });
|
|
234
|
+
} else if (/[*?]/.test(arg)) {
|
|
235
|
+
jobs.push(...await expandPattern(arg, cwd));
|
|
236
|
+
} else {
|
|
237
|
+
jobs.push(fileJob(arg, resolve(cwd, arg)));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return jobs;
|
|
241
|
+
}
|
|
242
|
+
async function expandPattern(pattern, cwd) {
|
|
243
|
+
if (typeof fs.glob !== "function") {
|
|
244
|
+
return [{ kind: "error", label: pattern, message: "wildcards need Node.js 22 or later, so the pattern was not expanded" }];
|
|
245
|
+
}
|
|
246
|
+
const globPattern = sep === "\\" ? pattern.replace(/\\/g, "/") : pattern;
|
|
247
|
+
const matches = [];
|
|
248
|
+
for await (const match of fs.glob(globPattern, { cwd })) {
|
|
249
|
+
if (INPUT_EXTENSIONS.includes(extname(match).toLowerCase())) {
|
|
250
|
+
matches.push(match);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (matches.length === 0) {
|
|
254
|
+
return [{ kind: "error", label: pattern, message: "no .emf, .wmf, .emz or .wmz files match" }];
|
|
255
|
+
}
|
|
256
|
+
return matches.sort().map((match) => fileJob(match, resolve(cwd, match)));
|
|
257
|
+
}
|
|
258
|
+
function fileJob(label, path) {
|
|
259
|
+
return { kind: "file", label, path };
|
|
260
|
+
}
|
|
261
|
+
async function planOutputs(jobs, ctx) {
|
|
262
|
+
if (ctx.toStdout && jobs.length > 1) {
|
|
263
|
+
throw new UsageError('"-o -" writes one file to stdout, so give a single input');
|
|
264
|
+
}
|
|
265
|
+
const newExt = CLI_FORMATS.find((f) => f.name === ctx.format).extensions[0];
|
|
266
|
+
let outIsDirectory = false;
|
|
267
|
+
if (ctx.outPath !== void 0) {
|
|
268
|
+
const existing = await statOrNull(ctx.outPath);
|
|
269
|
+
if (ctx.several) {
|
|
270
|
+
if (existing && !existing.isDirectory()) {
|
|
271
|
+
throw new UsageError(`"${ctx.outPath}" is not a directory; several inputs need an output directory`);
|
|
272
|
+
}
|
|
273
|
+
outIsDirectory = true;
|
|
274
|
+
} else {
|
|
275
|
+
outIsDirectory = existing?.isDirectory() ?? false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const plans = [];
|
|
279
|
+
const claimed = /* @__PURE__ */ new Map();
|
|
280
|
+
const inputPaths = new Set(jobs.flatMap((job) => job.kind === "file" ? [resolve(job.path)] : []));
|
|
281
|
+
for (const job of jobs) {
|
|
282
|
+
if (job.kind === "error" || ctx.toStdout || job.kind === "stdin" && ctx.outPath === void 0) {
|
|
283
|
+
plans.push({ job, target: void 0 });
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
let target;
|
|
287
|
+
if (ctx.outPath !== void 0) {
|
|
288
|
+
const stem = job.kind === "stdin" ? "stdin" : basename(job.path, extname(job.path));
|
|
289
|
+
target = outIsDirectory ? join(ctx.outPath, stem + newExt) : ctx.outPath;
|
|
290
|
+
} else if (job.kind === "file") {
|
|
291
|
+
target = join(dirname(job.path), basename(job.path, extname(job.path)) + newExt);
|
|
292
|
+
} else {
|
|
293
|
+
throw new Error("unreachable: stdin without --out goes to stdout");
|
|
294
|
+
}
|
|
295
|
+
const key = resolve(target);
|
|
296
|
+
if (inputPaths.has(key)) {
|
|
297
|
+
throw new UsageError(`the output "${target}" would overwrite an input file`);
|
|
298
|
+
}
|
|
299
|
+
const previous = claimed.get(key);
|
|
300
|
+
if (previous !== void 0) {
|
|
301
|
+
throw new UsageError(`"${previous}" and "${job.label}" would both write "${target}"`);
|
|
302
|
+
}
|
|
303
|
+
claimed.set(key, job.label);
|
|
304
|
+
plans.push({ job, target });
|
|
305
|
+
}
|
|
306
|
+
return plans;
|
|
307
|
+
}
|
|
308
|
+
async function statOrNull(path) {
|
|
309
|
+
return fs.stat(path).catch(() => null);
|
|
310
|
+
}
|
|
311
|
+
async function isDirectory(path) {
|
|
312
|
+
return (await statOrNull(path))?.isDirectory() ?? false;
|
|
313
|
+
}
|
|
314
|
+
async function collect(stream) {
|
|
315
|
+
const chunks = [];
|
|
316
|
+
let total = 0;
|
|
317
|
+
for await (const chunk of stream) {
|
|
318
|
+
const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk;
|
|
319
|
+
chunks.push(bytes);
|
|
320
|
+
total += bytes.byteLength;
|
|
321
|
+
}
|
|
322
|
+
const out = new Uint8Array(total);
|
|
323
|
+
let offset = 0;
|
|
324
|
+
for (const chunk of chunks) {
|
|
325
|
+
out.set(chunk, offset);
|
|
326
|
+
offset += chunk.byteLength;
|
|
327
|
+
}
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
function describeError(err) {
|
|
331
|
+
const code = err?.code;
|
|
332
|
+
if (code === "ENOENT") {
|
|
333
|
+
return "no such file or directory";
|
|
334
|
+
}
|
|
335
|
+
if (code === "EISDIR") {
|
|
336
|
+
return "is a directory";
|
|
337
|
+
}
|
|
338
|
+
return err instanceof Error ? err.message : String(err);
|
|
339
|
+
}
|
|
340
|
+
function readVersion() {
|
|
341
|
+
try {
|
|
342
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
343
|
+
return pkg.version ?? "unknown";
|
|
344
|
+
} catch {
|
|
345
|
+
return "unknown";
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/cli-bin.ts
|
|
350
|
+
main(process.argv.slice(2), {
|
|
351
|
+
stdout: process.stdout,
|
|
352
|
+
stderr: process.stderr,
|
|
353
|
+
stdin: process.stdin,
|
|
354
|
+
cwd: process.cwd()
|
|
355
|
+
}).then((code) => {
|
|
356
|
+
process.exitCode = code;
|
|
357
|
+
});
|