document-cli 1.1.0 → 1.2.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 +46 -8
- package/dist/cli.js +223 -24
- package/dist/index.cjs +517 -20
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +519 -22
- package/dist/odb-structure-CX_0zL8_.js +382 -0
- package/dist/{tui-Dywpifqx.js → tui-DD4bQp39.js} +464 -68
- package/package.json +3 -3
- package/dist/io-CKOE9Xda.js +0 -84
package/dist/index.cjs
CHANGED
|
@@ -112,6 +112,22 @@ function createDiagnosticReporter(options) {
|
|
|
112
112
|
}
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
|
+
function createFontSubstitutionReporter(options) {
|
|
116
|
+
const { json, quiet, command } = options;
|
|
117
|
+
return (substitution) => {
|
|
118
|
+
if (quiet) return;
|
|
119
|
+
if (json) {
|
|
120
|
+
process.stderr.write(`${JSON.stringify({
|
|
121
|
+
type: "font-substitution",
|
|
122
|
+
command,
|
|
123
|
+
...substitution
|
|
124
|
+
})}\n`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const styleClause = `${substitution.requestedBold ? " bold" : ""}${substitution.requestedItalic ? " italic" : ""}`;
|
|
128
|
+
process.stderr.write(`[${command}] font substitution: "${substitution.requestedFamily}"${styleClause} -> "${substitution.resolvedFamily}" (${substitution.reason})\n`);
|
|
129
|
+
};
|
|
130
|
+
}
|
|
115
131
|
function substitutionToDiagnostic(substitution, pageIndex) {
|
|
116
132
|
return {
|
|
117
133
|
severity: "warning",
|
|
@@ -120,6 +136,13 @@ function substitutionToDiagnostic(substitution, pageIndex) {
|
|
|
120
136
|
pageIndex
|
|
121
137
|
};
|
|
122
138
|
}
|
|
139
|
+
function fontSubstitutionToDiagnostic(substitution) {
|
|
140
|
+
return {
|
|
141
|
+
severity: "info",
|
|
142
|
+
code: "font/substituted",
|
|
143
|
+
message: `"${`${substitution.requestedFamily}${substitution.requestedBold ? " bold" : ""}${substitution.requestedItalic ? " italic" : ""}`}" is not available; ${substitution.reason === "vendored-substitute" ? `substituted the metric-compatible "${substitution.resolvedFamily}"` : `substituted another face of "${substitution.resolvedFamily}"`}`
|
|
144
|
+
};
|
|
145
|
+
}
|
|
123
146
|
function pdfDiagnosticToDiagnostic(diagnostic) {
|
|
124
147
|
return {
|
|
125
148
|
severity: diagnostic.severity,
|
|
@@ -144,6 +167,172 @@ function mapErrorToExit(error, abortReason) {
|
|
|
144
167
|
return 1;
|
|
145
168
|
}
|
|
146
169
|
//#endregion
|
|
170
|
+
//#region src/runtime/font-face.ts
|
|
171
|
+
var FontFaceError = class extends Error {
|
|
172
|
+
constructor(message) {
|
|
173
|
+
super(message);
|
|
174
|
+
this.name = "FontFaceError";
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const SFNT_VERSION_TRUETYPE = 65536;
|
|
178
|
+
const SFNT_VERSION_CFF = 1330926671;
|
|
179
|
+
const SFNT_VERSION_APPLE_TRUE = 1953658213;
|
|
180
|
+
const SFNT_VERSION_APPLE_TYP1 = 1954115633;
|
|
181
|
+
const SFNT_VERSION_COLLECTION = 1953784678;
|
|
182
|
+
const SFNT_VERSIONS = /* @__PURE__ */ new Set([
|
|
183
|
+
SFNT_VERSION_TRUETYPE,
|
|
184
|
+
SFNT_VERSION_CFF,
|
|
185
|
+
SFNT_VERSION_APPLE_TRUE,
|
|
186
|
+
SFNT_VERSION_APPLE_TYP1
|
|
187
|
+
]);
|
|
188
|
+
const TABLE_DIRECTORY_HEADER_SIZE = 12;
|
|
189
|
+
const TABLE_RECORD_SIZE = 16;
|
|
190
|
+
const TABLE_TAG_SIZE = 4;
|
|
191
|
+
const NAME_HEADER_SIZE = 6;
|
|
192
|
+
const NAME_RECORD_SIZE = 12;
|
|
193
|
+
const NAME_ID_FAMILY = 1;
|
|
194
|
+
const NAME_ID_TYPOGRAPHIC_FAMILY = 16;
|
|
195
|
+
const PLATFORM_UNICODE = 0;
|
|
196
|
+
const PLATFORM_MACINTOSH = 1;
|
|
197
|
+
const PLATFORM_WINDOWS = 3;
|
|
198
|
+
const MACINTOSH_ENCODING_ROMAN = 0;
|
|
199
|
+
const OS2_FS_SELECTION_OFFSET = 62;
|
|
200
|
+
const OS2_MINIMUM_SIZE = 64;
|
|
201
|
+
const OS2_FS_SELECTION_ITALIC = 1;
|
|
202
|
+
const OS2_FS_SELECTION_BOLD = 32;
|
|
203
|
+
const HEAD_MAC_STYLE_OFFSET = 44;
|
|
204
|
+
const HEAD_MINIMUM_SIZE = 46;
|
|
205
|
+
const HEAD_MAC_STYLE_BOLD = 1;
|
|
206
|
+
const HEAD_MAC_STYLE_ITALIC = 2;
|
|
207
|
+
function viewOf(bytes) {
|
|
208
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
209
|
+
}
|
|
210
|
+
function decodeTag(view, offset) {
|
|
211
|
+
let tag = "";
|
|
212
|
+
for (let i = 0; i < TABLE_TAG_SIZE; i++) tag += String.fromCharCode(view.getUint8(offset + i));
|
|
213
|
+
return tag;
|
|
214
|
+
}
|
|
215
|
+
function decodeUtf16Be(view, offset, length) {
|
|
216
|
+
let text = "";
|
|
217
|
+
for (let i = 0; i + 1 < length; i += 2) text += String.fromCharCode(view.getUint16(offset + i));
|
|
218
|
+
return text;
|
|
219
|
+
}
|
|
220
|
+
function decodeMacintoshAscii(view, offset, length) {
|
|
221
|
+
let text = "";
|
|
222
|
+
for (let i = 0; i < length; i++) {
|
|
223
|
+
const byte = view.getUint8(offset + i);
|
|
224
|
+
if (byte > 126) return;
|
|
225
|
+
text += String.fromCharCode(byte);
|
|
226
|
+
}
|
|
227
|
+
return text;
|
|
228
|
+
}
|
|
229
|
+
function readTableDirectory(view, source) {
|
|
230
|
+
if (view.byteLength < TABLE_DIRECTORY_HEADER_SIZE) throw new FontFaceError(`${source} is too short to be a font file (${String(view.byteLength)} bytes)`);
|
|
231
|
+
const sfntVersion = view.getUint32(0);
|
|
232
|
+
if (sfntVersion === SFNT_VERSION_COLLECTION) throw new FontFaceError(`${source} is a TrueType Collection (.ttc), which packs several faces into one file; extract the single face you want and pass that instead`);
|
|
233
|
+
if (!SFNT_VERSIONS.has(sfntVersion)) throw new FontFaceError(`${source} is not a TrueType/OpenType font file (no recognised sfnt version); a .woff/.woff2 file must be converted to .ttf/.otf first`);
|
|
234
|
+
const numTables = view.getUint16(4);
|
|
235
|
+
if (view.byteLength < TABLE_DIRECTORY_HEADER_SIZE + numTables * TABLE_RECORD_SIZE) throw new FontFaceError(`${source} declares ${String(numTables)} tables but is too short to hold that many table records`);
|
|
236
|
+
const tables = /* @__PURE__ */ new Map();
|
|
237
|
+
for (let i = 0; i < numTables; i++) {
|
|
238
|
+
const recordOffset = TABLE_DIRECTORY_HEADER_SIZE + i * TABLE_RECORD_SIZE;
|
|
239
|
+
const tag = decodeTag(view, recordOffset);
|
|
240
|
+
const offset = view.getUint32(recordOffset + 8);
|
|
241
|
+
const length = view.getUint32(recordOffset + 12);
|
|
242
|
+
if (offset + length > view.byteLength || tables.has(tag)) continue;
|
|
243
|
+
tables.set(tag, {
|
|
244
|
+
offset,
|
|
245
|
+
length
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return tables;
|
|
249
|
+
}
|
|
250
|
+
function tableView(bytes, table) {
|
|
251
|
+
return new DataView(bytes.buffer, bytes.byteOffset + table.offset, table.length);
|
|
252
|
+
}
|
|
253
|
+
function readNameRecords(name) {
|
|
254
|
+
if (name.byteLength < NAME_HEADER_SIZE) return [];
|
|
255
|
+
const count = name.getUint16(2);
|
|
256
|
+
const storageOffset = name.getUint16(4);
|
|
257
|
+
if (name.byteLength < NAME_HEADER_SIZE + count * NAME_RECORD_SIZE) return [];
|
|
258
|
+
const records = [];
|
|
259
|
+
for (let i = 0; i < count; i++) {
|
|
260
|
+
const recordOffset = NAME_HEADER_SIZE + i * NAME_RECORD_SIZE;
|
|
261
|
+
records.push({
|
|
262
|
+
platformId: name.getUint16(recordOffset),
|
|
263
|
+
encodingId: name.getUint16(recordOffset + 2),
|
|
264
|
+
nameId: name.getUint16(recordOffset + 6),
|
|
265
|
+
length: name.getUint16(recordOffset + 8),
|
|
266
|
+
stringOffset: storageOffset + name.getUint16(recordOffset + 10)
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return records;
|
|
270
|
+
}
|
|
271
|
+
function platformPreference(record) {
|
|
272
|
+
if (record.platformId === PLATFORM_WINDOWS) return 0;
|
|
273
|
+
if (record.platformId === PLATFORM_UNICODE) return 1;
|
|
274
|
+
if (record.platformId === PLATFORM_MACINTOSH && record.encodingId === MACINTOSH_ENCODING_ROMAN) return 2;
|
|
275
|
+
return Number.POSITIVE_INFINITY;
|
|
276
|
+
}
|
|
277
|
+
function readNameString(name, records, nameId) {
|
|
278
|
+
const candidates = records.filter((record) => record.nameId === nameId && Number.isFinite(platformPreference(record))).sort((left, right) => platformPreference(left) - platformPreference(right));
|
|
279
|
+
for (const record of candidates) {
|
|
280
|
+
if (record.stringOffset + record.length > name.byteLength) continue;
|
|
281
|
+
const text = record.platformId === PLATFORM_MACINTOSH ? decodeMacintoshAscii(name, record.stringOffset, record.length) : decodeUtf16Be(name, record.stringOffset, record.length);
|
|
282
|
+
if (text !== void 0 && text.length > 0) return text;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function readStyleBits(bytes, tables, source) {
|
|
286
|
+
const os2 = tables.get("OS/2");
|
|
287
|
+
if (os2 !== void 0 && os2.length >= OS2_MINIMUM_SIZE) {
|
|
288
|
+
const fsSelection = tableView(bytes, os2).getUint16(OS2_FS_SELECTION_OFFSET);
|
|
289
|
+
return {
|
|
290
|
+
bold: (fsSelection & OS2_FS_SELECTION_BOLD) !== 0,
|
|
291
|
+
italic: (fsSelection & OS2_FS_SELECTION_ITALIC) !== 0
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const head = tables.get("head");
|
|
295
|
+
if (head !== void 0 && head.length >= HEAD_MINIMUM_SIZE) {
|
|
296
|
+
const macStyle = tableView(bytes, head).getUint16(HEAD_MAC_STYLE_OFFSET);
|
|
297
|
+
return {
|
|
298
|
+
bold: (macStyle & HEAD_MAC_STYLE_BOLD) !== 0,
|
|
299
|
+
italic: (macStyle & HEAD_MAC_STYLE_ITALIC) !== 0
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
throw new FontFaceError(`${source} declares neither a readable 'OS/2' nor a readable 'head' table, so its weight and slope cannot be determined`);
|
|
303
|
+
}
|
|
304
|
+
function describeFontFace(bytes, source) {
|
|
305
|
+
const tables = readTableDirectory(viewOf(bytes), source);
|
|
306
|
+
const nameTable = tables.get("name");
|
|
307
|
+
if (nameTable === void 0) throw new FontFaceError(`${source} declares no 'name' table, so the font family it provides cannot be determined`);
|
|
308
|
+
const name = tableView(bytes, nameTable);
|
|
309
|
+
const records = readNameRecords(name);
|
|
310
|
+
const family = readNameString(name, records, NAME_ID_TYPOGRAPHIC_FAMILY) ?? readNameString(name, records, NAME_ID_FAMILY);
|
|
311
|
+
if (family === void 0) throw new FontFaceError(`${source} declares no family name in its 'name' table, so the font family it provides cannot be determined`);
|
|
312
|
+
const { bold, italic } = readStyleBits(bytes, tables, source);
|
|
313
|
+
return {
|
|
314
|
+
family,
|
|
315
|
+
bold,
|
|
316
|
+
italic
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/runtime/fonts.ts
|
|
321
|
+
async function loadProvidedFonts(paths, options) {
|
|
322
|
+
const fonts = [];
|
|
323
|
+
for (const path of paths) {
|
|
324
|
+
const bytes = new Uint8Array(await (0, node_fs_promises.readFile)(path, { signal: options?.signal }));
|
|
325
|
+
const face = describeFontFace(bytes, path);
|
|
326
|
+
fonts.push({
|
|
327
|
+
family: face.family,
|
|
328
|
+
bold: face.bold,
|
|
329
|
+
italic: face.italic,
|
|
330
|
+
bytes
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return fonts;
|
|
334
|
+
}
|
|
335
|
+
//#endregion
|
|
147
336
|
//#region src/runtime/io.ts
|
|
148
337
|
function isUint8Array(value) {
|
|
149
338
|
return value instanceof Uint8Array;
|
|
@@ -184,6 +373,18 @@ function resolveDefaultOutputPath(inputPath, targetFormat) {
|
|
|
184
373
|
}
|
|
185
374
|
//#endregion
|
|
186
375
|
//#region src/commands/shared.ts
|
|
376
|
+
const KNOWN_DOCUMENT_FORMATS = "docx, pptx, xlsx, odt, odp, ods, odg, odf, markdown, pdf";
|
|
377
|
+
function resolveTargetFormat(output, out, to) {
|
|
378
|
+
if (to !== void 0) {
|
|
379
|
+
if (!isDocumentFormat(to)) return { errorMessage: `unknown --to format '${to}'; expected one of ${KNOWN_DOCUMENT_FORMATS}` };
|
|
380
|
+
return { format: to };
|
|
381
|
+
}
|
|
382
|
+
const destination = output ?? out;
|
|
383
|
+
if (destination === void 0) return { errorMessage: "cannot infer a target format -- pass an output path with a recognised extension, --out with one, or --to <format>" };
|
|
384
|
+
const inferred = inferFormatFromExtension(destination);
|
|
385
|
+
if (inferred === void 0) return { errorMessage: `cannot infer a target format from '${destination}'; pass --to <format> instead` };
|
|
386
|
+
return { format: inferred };
|
|
387
|
+
}
|
|
187
388
|
function formatError(error, verbose) {
|
|
188
389
|
if (!(error instanceof Error)) return `error: ${String(error)}`;
|
|
189
390
|
const stackClause = verbose && error.stack !== void 0 ? `\n${error.stack}` : "";
|
|
@@ -200,13 +401,22 @@ function buildConversionAction(source, target) {
|
|
|
200
401
|
const { signal, getAbortReason } = createRuntimeSignal({ timeoutMs: options.timeoutMs });
|
|
201
402
|
try {
|
|
202
403
|
const inputBytes = await readInput(input, { signal });
|
|
404
|
+
const fonts = await loadProvidedFonts(options.fontFiles ?? [], { signal });
|
|
203
405
|
const result = await (0, documents_js.createLocalDocumentConverter)().convert({
|
|
204
406
|
source: {
|
|
205
407
|
format: source,
|
|
206
408
|
bytes: new Uint8Array(inputBytes)
|
|
207
409
|
},
|
|
208
410
|
targetFormat: target
|
|
209
|
-
}, {
|
|
411
|
+
}, {
|
|
412
|
+
signal,
|
|
413
|
+
fonts,
|
|
414
|
+
onFontSubstitution: options.reportFontSubstitutions === true ? createFontSubstitutionReporter({
|
|
415
|
+
json: options.json,
|
|
416
|
+
quiet: options.quiet,
|
|
417
|
+
command
|
|
418
|
+
}) : void 0
|
|
419
|
+
});
|
|
210
420
|
await writeOutput(resolvedOutput, result.document.bytes);
|
|
211
421
|
const reporter = createDiagnosticReporter({
|
|
212
422
|
json: options.json,
|
|
@@ -215,7 +425,7 @@ function buildConversionAction(source, target) {
|
|
|
215
425
|
});
|
|
216
426
|
for (const diagnostic of result.diagnostics) reporter.report(diagnostic);
|
|
217
427
|
if (options.dumpPackage !== void 0) if (result.package === void 0) process.stderr.write(`[${command}] this conversion does not produce an intermediate DocumentPackage\n`);
|
|
218
|
-
else await (0, node_fs_promises.writeFile)(options.dumpPackage, JSON.stringify(result.package, void 0, 2));
|
|
428
|
+
else await (0, node_fs_promises.writeFile)(options.dumpPackage, JSON.stringify((0, documents_js.documentPackageWithSchema)(result.package), void 0, 2));
|
|
219
429
|
reporter.summarize({
|
|
220
430
|
output: resolvedOutput,
|
|
221
431
|
bytes: result.document.bytes.byteLength,
|
|
@@ -248,6 +458,14 @@ function addVerboseOption(command) {
|
|
|
248
458
|
function addDumpPackageOption(command) {
|
|
249
459
|
return command.option("--dump-package <file>", "write the intermediate DocumentPackage (content + layout) this conversion built to a JSON file");
|
|
250
460
|
}
|
|
461
|
+
function collectFontFile(value, previous) {
|
|
462
|
+
return [...previous, value];
|
|
463
|
+
}
|
|
464
|
+
function addFontOptions(command) {
|
|
465
|
+
command.option("--font-file <path>", "embed this font file (.ttf/.otf) when the document asks for the family it declares; repeatable. The family, weight, and slope are read from the font's own 'name'/'OS/2' tables, so no accompanying family flag is needed", collectFontFile, []);
|
|
466
|
+
command.option("--report-font-substitutions", "print each font face that resolved to something other than what the document asked for to stderr, as it happens", false);
|
|
467
|
+
return command;
|
|
468
|
+
}
|
|
251
469
|
function addConversionFlags(command) {
|
|
252
470
|
addOutOption(command);
|
|
253
471
|
addTimeoutOption(command);
|
|
@@ -265,21 +483,11 @@ function toConversionCommandOptions(options) {
|
|
|
265
483
|
json: options.json,
|
|
266
484
|
quiet: options.quiet,
|
|
267
485
|
verbose: options.verbose,
|
|
268
|
-
dumpPackage: options.dumpPackage
|
|
486
|
+
dumpPackage: options.dumpPackage,
|
|
487
|
+
fontFiles: options.fontFile,
|
|
488
|
+
reportFontSubstitutions: options.reportFontSubstitutions
|
|
269
489
|
};
|
|
270
490
|
}
|
|
271
|
-
const KNOWN_FORMATS = "docx, pptx, xlsx, odt, odp, ods, odg, odf, markdown, pdf";
|
|
272
|
-
function resolveGenericTarget(output, options) {
|
|
273
|
-
if (options.to !== void 0) {
|
|
274
|
-
if (!isDocumentFormat(options.to)) return { errorMessage: `unknown --to format '${options.to}'; expected one of ${KNOWN_FORMATS}` };
|
|
275
|
-
return { format: options.to };
|
|
276
|
-
}
|
|
277
|
-
const destination = output ?? options.out;
|
|
278
|
-
if (destination === void 0) return { errorMessage: "cannot infer a target format -- pass an output path with a recognised extension, --out with one, or --to <format>" };
|
|
279
|
-
const inferred = inferFormatFromExtension(destination);
|
|
280
|
-
if (inferred === void 0) return { errorMessage: `cannot infer a target format from '${destination}'; pass --to <format> instead` };
|
|
281
|
-
return { format: inferred };
|
|
282
|
-
}
|
|
283
491
|
async function runGenericConvert(input, output, options) {
|
|
284
492
|
const extension = (0, node_path.extname)(input).toLowerCase();
|
|
285
493
|
if (extension === ".odm") {
|
|
@@ -292,10 +500,10 @@ async function runGenericConvert(input, output, options) {
|
|
|
292
500
|
}
|
|
293
501
|
const source = inferFormatFromExtension(input);
|
|
294
502
|
if (source === void 0) {
|
|
295
|
-
process.stderr.write(`convert: cannot infer a source format from '${input}'; rename the file with a recognised extension (${
|
|
503
|
+
process.stderr.write(`convert: cannot infer a source format from '${input}'; rename the file with a recognised extension (${KNOWN_DOCUMENT_FORMATS}) or use one of the explicit '<source>-to-<target>' commands\n`);
|
|
296
504
|
return 2;
|
|
297
505
|
}
|
|
298
|
-
const target =
|
|
506
|
+
const target = resolveTargetFormat(output, options.out, options.to);
|
|
299
507
|
if ("errorMessage" in target) {
|
|
300
508
|
process.stderr.write(`convert: ${target.errorMessage}\n`);
|
|
301
509
|
return 2;
|
|
@@ -309,6 +517,7 @@ function registerConversionCommands(program) {
|
|
|
309
517
|
const command = program.command(`${commandName} <input> [output]`).description(`convert a ${source} document to ${target}`);
|
|
310
518
|
addConversionFlags(command);
|
|
311
519
|
addDumpPackageOption(command);
|
|
520
|
+
if (target === "pdf") addFontOptions(command);
|
|
312
521
|
command.action(async (input, output, options) => {
|
|
313
522
|
process.exitCode = await buildConversionAction(source, target)(input, output, toConversionCommandOptions(options));
|
|
314
523
|
});
|
|
@@ -316,14 +525,15 @@ function registerConversionCommands(program) {
|
|
|
316
525
|
const generic = program.command("convert <input> [output]").description("convert between any two supported document formats, inferring source/target from file extensions where possible");
|
|
317
526
|
addConversionFlags(generic);
|
|
318
527
|
addDumpPackageOption(generic);
|
|
319
|
-
generic
|
|
528
|
+
addFontOptions(generic);
|
|
529
|
+
generic.option("--to <format>", `target format when it cannot be inferred from the output path (${KNOWN_DOCUMENT_FORMATS})`);
|
|
320
530
|
generic.action(async (input, output, options) => {
|
|
321
531
|
process.exitCode = await runGenericConvert(input, output, options);
|
|
322
532
|
});
|
|
323
533
|
}
|
|
324
534
|
//#endregion
|
|
325
535
|
//#region src/commands/formats.ts
|
|
326
|
-
const COMMANDS_NOT_LISTED = "odm-to-pdf, odb-to-csv, odb-to-xlsx, odb-tables, pdf-inspect";
|
|
536
|
+
const COMMANDS_NOT_LISTED = "odm-to-pdf, odb-to-csv, odb-to-xlsx, odb-tables, odb-forms, odb-reports, pdf-inspect, from-package";
|
|
327
537
|
function registerFormatsCommand(program) {
|
|
328
538
|
program.command("formats").description("list every source -> target conversion this CLI supports via a <source>-to-<target> command").option("--json", "emit the conversion list as a JSON array instead of a human-readable table", false).action((options) => {
|
|
329
539
|
const { conversions } = (0, documents_js.createLocalDocumentConverter)();
|
|
@@ -336,6 +546,218 @@ function registerFormatsCommand(program) {
|
|
|
336
546
|
});
|
|
337
547
|
}
|
|
338
548
|
//#endregion
|
|
549
|
+
//#region src/commands/from-package.ts
|
|
550
|
+
function buildBytesForTarget(pkg, target) {
|
|
551
|
+
if (target === "pdf") {
|
|
552
|
+
if (pkg.layout === void 0) throw new Error("this DocumentPackage has no layout -- only a package dumped from a <format>-to-pdf or pdf-to-<format> conversion carries one; a bridge conversion's own dump (e.g. odt-to-docx) never does, so 'pdf' is not a reachable target from it");
|
|
553
|
+
return (0, documents_js.writePdf)(pkg.layout);
|
|
554
|
+
}
|
|
555
|
+
switch (target) {
|
|
556
|
+
case "docx": return (0, documents_js.encodePackage)((0, documents_js.buildDocxPackage)(pkg.content));
|
|
557
|
+
case "pptx": return (0, documents_js.encodePackage)((0, documents_js.buildPptxPackage)(pkg.content));
|
|
558
|
+
case "odt": return (0, odf_js.encodePackage)((0, documents_js.buildOdtPackage)(pkg.content));
|
|
559
|
+
case "odp": return (0, odf_js.encodePackage)((0, documents_js.buildOdpPackage)(pkg.content));
|
|
560
|
+
case "ods": return (0, odf_js.encodePackage)((0, documents_js.buildOdsPackage)(pkg.content));
|
|
561
|
+
case "odg": return (0, odf_js.encodePackage)((0, documents_js.buildOdgPackage)(pkg.content));
|
|
562
|
+
case "markdown": return (0, documents_js.encodeMarkdownText)((0, documents_js.buildMarkdownText)(pkg.content));
|
|
563
|
+
case "xlsx": throw new Error("'xlsx' cannot be built from a DocumentPackage directly -- documents.js does not re-export a ContentDocument-to-xlsx builder; convert to 'ods' here, then run 'ods-to-xlsx' on the result instead");
|
|
564
|
+
case "odf": throw new Error("'odf' (a standalone formula document) cannot be built from a DocumentPackage -- there is no ContentDocument-to-odf builder");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async function runFromPackage(input, output, options) {
|
|
568
|
+
const command = "from-package";
|
|
569
|
+
if (output !== void 0 && options.out !== void 0 && output !== options.out) {
|
|
570
|
+
process.stderr.write(`[${command}] conflicting output destinations: positional '${output}' and --out '${options.out}'\n`);
|
|
571
|
+
return 2;
|
|
572
|
+
}
|
|
573
|
+
const target = resolveTargetFormat(output, options.out, options.to);
|
|
574
|
+
if ("errorMessage" in target) {
|
|
575
|
+
process.stderr.write(`[${command}] ${target.errorMessage}\n`);
|
|
576
|
+
return 2;
|
|
577
|
+
}
|
|
578
|
+
const resolvedOutput = output ?? options.out ?? (input === "-" ? "-" : resolveDefaultOutputPath(input, target.format));
|
|
579
|
+
const { signal, getAbortReason } = createRuntimeSignal({ timeoutMs: options.timeout });
|
|
580
|
+
try {
|
|
581
|
+
const inputBytes = await readInput(input, { signal });
|
|
582
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(inputBytes);
|
|
583
|
+
let parsed;
|
|
584
|
+
try {
|
|
585
|
+
parsed = JSON.parse(text);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
process.stderr.write(`[${command}] '${input}' is not valid JSON: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
588
|
+
return 1;
|
|
589
|
+
}
|
|
590
|
+
const result = (0, documents_js.documentFromJson)(parsed);
|
|
591
|
+
if (result.kind !== "DocumentPackage") {
|
|
592
|
+
process.stderr.write(`[${command}] '${input}' is a ${result.kind}, not a DocumentPackage -- only a file written by --dump-package can be read back by this command\n`);
|
|
593
|
+
return 2;
|
|
594
|
+
}
|
|
595
|
+
const bytes = buildBytesForTarget(result.value, target.format);
|
|
596
|
+
await writeOutput(resolvedOutput, bytes);
|
|
597
|
+
createDiagnosticReporter({
|
|
598
|
+
json: options.json,
|
|
599
|
+
quiet: options.quiet,
|
|
600
|
+
command
|
|
601
|
+
}).summarize({
|
|
602
|
+
output: resolvedOutput,
|
|
603
|
+
bytes: bytes.byteLength,
|
|
604
|
+
diagnosticCount: 0
|
|
605
|
+
});
|
|
606
|
+
return 0;
|
|
607
|
+
} catch (error) {
|
|
608
|
+
if (error instanceof documents_js.UnrecognizedDocumentSchemaError) {
|
|
609
|
+
process.stderr.write(`[${command}] '${input}' has no recognised $schema -- only a file written by --dump-package can be read back by this command\n`);
|
|
610
|
+
return 1;
|
|
611
|
+
}
|
|
612
|
+
process.stderr.write(`${formatError(error, options.verbose)}\n`);
|
|
613
|
+
return mapErrorToExit(error, getAbortReason());
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function registerFromPackageCommand(program) {
|
|
617
|
+
const command = program.command("from-package <input> [output]").description("read a DocumentPackage previously written by --dump-package and export it to a real target format");
|
|
618
|
+
addOutOption(command);
|
|
619
|
+
addTimeoutOption(command);
|
|
620
|
+
addJsonOption(command);
|
|
621
|
+
addQuietOption(command);
|
|
622
|
+
addVerboseOption(command);
|
|
623
|
+
command.option("--to <format>", `target format when it cannot be inferred from the output path (${KNOWN_DOCUMENT_FORMATS})`);
|
|
624
|
+
command.action(async (input, output, options) => {
|
|
625
|
+
process.exitCode = await runFromPackage(input, output, options);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
//#endregion
|
|
629
|
+
//#region src/odb-structure.ts
|
|
630
|
+
const INDENT = " ";
|
|
631
|
+
function indent(depth) {
|
|
632
|
+
return INDENT.repeat(depth);
|
|
633
|
+
}
|
|
634
|
+
function quoted(value) {
|
|
635
|
+
return `"${value}"`;
|
|
636
|
+
}
|
|
637
|
+
function describeDataSource(command, commandType) {
|
|
638
|
+
if (command === void 0) return commandType;
|
|
639
|
+
return commandType === void 0 ? quoted(command) : `${commandType} ${quoted(command)}`;
|
|
640
|
+
}
|
|
641
|
+
function countOdbFormControls(controls) {
|
|
642
|
+
return controls.reduce((total, control) => total + 1 + countOdbFormControls(control.controls), 0);
|
|
643
|
+
}
|
|
644
|
+
function countOdbFormDefinitionControls(definition) {
|
|
645
|
+
return countOdbFormControls(definition.controls) + definition.subForms.reduce((total, subForm) => total + countOdbFormDefinitionControls(subForm), 0);
|
|
646
|
+
}
|
|
647
|
+
function countOdbFormBoundControls(controls) {
|
|
648
|
+
return controls.reduce((total, control) => total + (control.dataField === void 0 ? 0 : 1) + countOdbFormBoundControls(control.controls), 0);
|
|
649
|
+
}
|
|
650
|
+
function countOdbFormDefinitionBoundControls(definition) {
|
|
651
|
+
return countOdbFormBoundControls(definition.controls) + definition.subForms.reduce((total, subForm) => total + countOdbFormDefinitionBoundControls(subForm), 0);
|
|
652
|
+
}
|
|
653
|
+
function describeOdbForm(form) {
|
|
654
|
+
const controlCount = form.forms.reduce((total, definition) => total + countOdbFormDefinitionControls(definition), 0);
|
|
655
|
+
const boundCount = form.forms.reduce((total, definition) => total + countOdbFormDefinitionBoundControls(definition), 0);
|
|
656
|
+
return `${form.name} [${form.href}] -- ${form.forms.length} form${form.forms.length === 1 ? "" : "s"}, ${controlCount} control${controlCount === 1 ? "" : "s"} (${boundCount} bound)`;
|
|
657
|
+
}
|
|
658
|
+
function formControlLines(control, depth) {
|
|
659
|
+
const parts = [control.tag];
|
|
660
|
+
if (control.name !== void 0) parts.push(control.name);
|
|
661
|
+
if (control.dataField !== void 0) parts.push(`-> ${control.dataField}`);
|
|
662
|
+
if (control.label !== void 0) parts.push(`label ${quoted(control.label)}`);
|
|
663
|
+
if (control.controlImplementation !== void 0) parts.push(`(${control.controlImplementation})`);
|
|
664
|
+
const nested = control.controls.flatMap((child) => formControlLines(child, depth + 1));
|
|
665
|
+
return [`${indent(depth)}${parts.join(" ")}`, ...nested];
|
|
666
|
+
}
|
|
667
|
+
function formDefinitionLines(definition, depth, kindLabel) {
|
|
668
|
+
const headerParts = [kindLabel];
|
|
669
|
+
if (definition.name !== void 0) headerParts.push(definition.name);
|
|
670
|
+
const dataSource = describeDataSource(definition.command, definition.commandType);
|
|
671
|
+
if (dataSource !== void 0) headerParts.push(`on ${dataSource}`);
|
|
672
|
+
const lines = [`${indent(depth)}${headerParts.join(" ")}`];
|
|
673
|
+
if (definition.datasource !== void 0) lines.push(`${indent(depth + 1)}datasource: ${definition.datasource}`);
|
|
674
|
+
if (definition.filter !== void 0) lines.push(`${indent(depth + 1)}filter: ${definition.filter}`);
|
|
675
|
+
if (definition.order !== void 0) lines.push(`${indent(depth + 1)}order: ${definition.order}`);
|
|
676
|
+
if (definition.controls.length === 0) lines.push(`${indent(depth + 1)}(no controls)`);
|
|
677
|
+
for (const control of definition.controls) lines.push(...formControlLines(control, depth + 1));
|
|
678
|
+
for (const subForm of definition.subForms) lines.push(...formDefinitionLines(subForm, depth + 1, "subform"));
|
|
679
|
+
return lines;
|
|
680
|
+
}
|
|
681
|
+
function formatOdbFormLines(form) {
|
|
682
|
+
if (form.forms.length === 0) return ["(this form document declares no form:form definitions)"];
|
|
683
|
+
return form.forms.flatMap((definition) => formDefinitionLines(definition, 0, "form"));
|
|
684
|
+
}
|
|
685
|
+
function odbFormSummary(form) {
|
|
686
|
+
return {
|
|
687
|
+
name: form.name,
|
|
688
|
+
href: form.href,
|
|
689
|
+
forms: form.forms
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
function countReportBandElements(band) {
|
|
693
|
+
return band?.elements.length ?? 0;
|
|
694
|
+
}
|
|
695
|
+
function countReportGroupElements(group) {
|
|
696
|
+
return countReportBandElements(group.header) + countReportBandElements(group.footer) + group.groups.reduce((total, nested) => total + countReportGroupElements(nested), 0);
|
|
697
|
+
}
|
|
698
|
+
function countReportGroups(groups) {
|
|
699
|
+
return groups.reduce((total, group) => total + 1 + countReportGroups(group.groups), 0);
|
|
700
|
+
}
|
|
701
|
+
function describeOdbReport(report) {
|
|
702
|
+
const dataSource = describeDataSource(report.command, report.commandType);
|
|
703
|
+
const groupCount = countReportGroups(report.groups);
|
|
704
|
+
const elementCount = countReportBandElements(report.reportHeader) + countReportBandElements(report.pageHeader) + countReportBandElements(report.detail) + countReportBandElements(report.pageFooter) + countReportBandElements(report.reportFooter) + report.groups.reduce((total, group) => total + countReportGroupElements(group), 0);
|
|
705
|
+
const source = dataSource === void 0 ? "no data source" : `on ${dataSource}`;
|
|
706
|
+
return `${report.name} [${report.href}] -- ${source}, ${groupCount} group${groupCount === 1 ? "" : "s"}, ${elementCount} element${elementCount === 1 ? "" : "s"}`;
|
|
707
|
+
}
|
|
708
|
+
function reportElementLine(element, depth) {
|
|
709
|
+
const parts = [element.tag];
|
|
710
|
+
if (element.name !== void 0) parts.push(quoted(element.name));
|
|
711
|
+
if (element.formula !== void 0) parts.push(`= ${element.formula}`);
|
|
712
|
+
if (element.dataField !== void 0) parts.push(`-> ${element.dataField}`);
|
|
713
|
+
const head = `${indent(depth)}${parts.join(" ")}`;
|
|
714
|
+
return element.text === void 0 ? head : `${head}: ${quoted(element.text)}`;
|
|
715
|
+
}
|
|
716
|
+
function reportBandLines(band, depth) {
|
|
717
|
+
if (band === void 0) return [];
|
|
718
|
+
const header = band.name === void 0 ? band.kind : `${band.kind} ${quoted(band.name)}`;
|
|
719
|
+
const lines = [`${indent(depth)}${header}`];
|
|
720
|
+
if (band.elements.length === 0) lines.push(`${indent(depth + 1)}(no elements)`);
|
|
721
|
+
for (const element of band.elements) lines.push(reportElementLine(element, depth + 1));
|
|
722
|
+
return lines;
|
|
723
|
+
}
|
|
724
|
+
function reportFunctionLines(functions, depth) {
|
|
725
|
+
if (functions.length === 0) return [];
|
|
726
|
+
return [`${indent(depth)}functions`, ...functions.map((fn) => `${indent(depth + 1)}${fn.name} = ${fn.formula}`)];
|
|
727
|
+
}
|
|
728
|
+
function reportGroupLines(group, depth) {
|
|
729
|
+
const headerParts = ["group"];
|
|
730
|
+
if (group.groupExpression !== void 0) headerParts.push(group.groupExpression);
|
|
731
|
+
const attributes = [];
|
|
732
|
+
if (group.sortExpression !== void 0) attributes.push(`sort ${group.sortExpression} ${group.sortAscending === false ? "descending" : "ascending"}`);
|
|
733
|
+
if (group.startNewColumn === true) attributes.push("new column");
|
|
734
|
+
if (group.resetPageNumber === true) attributes.push("reset page number");
|
|
735
|
+
if (group.keepTogether !== void 0) attributes.push(`keep together ${group.keepTogether}`);
|
|
736
|
+
if (attributes.length > 0) headerParts.push(`(${attributes.join(", ")})`);
|
|
737
|
+
return [
|
|
738
|
+
`${indent(depth)}${headerParts.join(" ")}`,
|
|
739
|
+
...reportBandLines(group.header, depth + 1),
|
|
740
|
+
...group.groups.flatMap((nested) => reportGroupLines(nested, depth + 1)),
|
|
741
|
+
...reportFunctionLines(group.functions, depth + 1),
|
|
742
|
+
...reportBandLines(group.footer, depth + 1)
|
|
743
|
+
];
|
|
744
|
+
}
|
|
745
|
+
function formatOdbReportLines(report) {
|
|
746
|
+
const lines = [];
|
|
747
|
+
const dataSource = describeDataSource(report.command, report.commandType);
|
|
748
|
+
if (dataSource !== void 0) lines.push(`data source: ${dataSource}`);
|
|
749
|
+
if (report.caption !== void 0) lines.push(`caption: ${report.caption}`);
|
|
750
|
+
if (report.mimeType !== void 0) lines.push(`mime type: ${report.mimeType}`);
|
|
751
|
+
lines.push(...reportBandLines(report.reportHeader, 0));
|
|
752
|
+
lines.push(...reportBandLines(report.pageHeader, 0));
|
|
753
|
+
lines.push(...report.groups.flatMap((group) => reportGroupLines(group, 0)));
|
|
754
|
+
lines.push(...reportBandLines(report.detail, 0));
|
|
755
|
+
lines.push(...reportBandLines(report.pageFooter, 0));
|
|
756
|
+
lines.push(...reportBandLines(report.reportFooter, 0));
|
|
757
|
+
lines.push(...reportFunctionLines(report.functions, 0));
|
|
758
|
+
return lines;
|
|
759
|
+
}
|
|
760
|
+
//#endregion
|
|
339
761
|
//#region src/commands/odb.ts
|
|
340
762
|
function reportOdbError(command, error, verbose, abortReason) {
|
|
341
763
|
if (error instanceof documents_js.OdbNoEmbeddedDataSourceError || error instanceof documents_js.OdbUnsupportedFormatError) {
|
|
@@ -437,6 +859,52 @@ async function runOdbTables(input, options) {
|
|
|
437
859
|
return reportOdbError(command, error, false, getAbortReason());
|
|
438
860
|
}
|
|
439
861
|
}
|
|
862
|
+
async function runOdbForms(input, options) {
|
|
863
|
+
const command = "odb-forms";
|
|
864
|
+
const { signal, getAbortReason } = createRuntimeSignal({});
|
|
865
|
+
try {
|
|
866
|
+
const inputBytes = await readInput(input, { signal });
|
|
867
|
+
const forms = (0, documents_js.readOdbForms)((0, odf_js.decodePackage)(new Uint8Array(inputBytes)));
|
|
868
|
+
if (options.json) {
|
|
869
|
+
process.stdout.write(`${JSON.stringify(forms.map((form) => odbFormSummary(form)))}\n`);
|
|
870
|
+
return 0;
|
|
871
|
+
}
|
|
872
|
+
if (forms.length === 0) {
|
|
873
|
+
process.stdout.write("This database declares no forms.\n");
|
|
874
|
+
return 0;
|
|
875
|
+
}
|
|
876
|
+
for (const form of forms) {
|
|
877
|
+
process.stdout.write(`${describeOdbForm(form)}\n`);
|
|
878
|
+
for (const line of formatOdbFormLines(form)) process.stdout.write(` ${line}\n`);
|
|
879
|
+
}
|
|
880
|
+
return 0;
|
|
881
|
+
} catch (error) {
|
|
882
|
+
return reportOdbError(command, error, false, getAbortReason());
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
async function runOdbReports(input, options) {
|
|
886
|
+
const command = "odb-reports";
|
|
887
|
+
const { signal, getAbortReason } = createRuntimeSignal({});
|
|
888
|
+
try {
|
|
889
|
+
const inputBytes = await readInput(input, { signal });
|
|
890
|
+
const reports = (0, documents_js.readOdbReports)((0, odf_js.decodePackage)(new Uint8Array(inputBytes)));
|
|
891
|
+
if (options.json) {
|
|
892
|
+
process.stdout.write(`${JSON.stringify(reports)}\n`);
|
|
893
|
+
return 0;
|
|
894
|
+
}
|
|
895
|
+
if (reports.length === 0) {
|
|
896
|
+
process.stdout.write("This database declares no reports.\n");
|
|
897
|
+
return 0;
|
|
898
|
+
}
|
|
899
|
+
for (const report of reports) {
|
|
900
|
+
process.stdout.write(`${describeOdbReport(report)}\n`);
|
|
901
|
+
for (const line of formatOdbReportLines(report)) process.stdout.write(` ${line}\n`);
|
|
902
|
+
}
|
|
903
|
+
return 0;
|
|
904
|
+
} catch (error) {
|
|
905
|
+
return reportOdbError(command, error, false, getAbortReason());
|
|
906
|
+
}
|
|
907
|
+
}
|
|
440
908
|
function registerOdbToXlsxCommand(program) {
|
|
441
909
|
const command = program.command("odb-to-xlsx <input> [output]").description("extract every table an embedded .odb database declares into one xlsx workbook, one sheet per table");
|
|
442
910
|
addOutOption(command);
|
|
@@ -465,10 +933,22 @@ function registerOdbTablesCommand(program) {
|
|
|
465
933
|
process.exitCode = await runOdbTables(input, options);
|
|
466
934
|
});
|
|
467
935
|
}
|
|
936
|
+
function registerOdbFormsCommand(program) {
|
|
937
|
+
program.command("odb-forms <input>").description("list every form an .odb declares, with each form's own data source and its field-bound controls").option("--json", "emit the form structure as a JSON array instead of a human-readable report", false).action(async (input, options) => {
|
|
938
|
+
process.exitCode = await runOdbForms(input, options);
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
function registerOdbReportsCommand(program) {
|
|
942
|
+
program.command("odb-reports <input>").description("list every report an .odb declares, with each report's own data-source command, band/group structure, and rpt: formula expressions").option("--json", "emit the report structure as a JSON array instead of a human-readable report", false).action(async (input, options) => {
|
|
943
|
+
process.exitCode = await runOdbReports(input, options);
|
|
944
|
+
});
|
|
945
|
+
}
|
|
468
946
|
function registerOdbCommands(program) {
|
|
469
947
|
registerOdbToXlsxCommand(program);
|
|
470
948
|
registerOdbToCsvCommand(program);
|
|
471
949
|
registerOdbTablesCommand(program);
|
|
950
|
+
registerOdbFormsCommand(program);
|
|
951
|
+
registerOdbReportsCommand(program);
|
|
472
952
|
}
|
|
473
953
|
//#endregion
|
|
474
954
|
//#region src/commands/odm.ts
|
|
@@ -504,11 +984,26 @@ async function runOdmToPdf(input, output, options) {
|
|
|
504
984
|
command
|
|
505
985
|
});
|
|
506
986
|
let diagnosticCount = 0;
|
|
987
|
+
const reportFontSubstitution = options.reportFontSubstitutions === true ? createFontSubstitutionReporter({
|
|
988
|
+
json: options.json,
|
|
989
|
+
quiet: options.quiet,
|
|
990
|
+
command
|
|
991
|
+
}) : void 0;
|
|
507
992
|
try {
|
|
508
993
|
const inputBytes = await readInput(input, { signal });
|
|
994
|
+
const fonts = await loadProvidedFonts(options.fontFile ?? [], { signal });
|
|
509
995
|
const bytes = (0, documents_js.odmToPdf)(new Uint8Array(inputBytes), {
|
|
510
996
|
signal,
|
|
511
997
|
resolveSubDocument,
|
|
998
|
+
fonts,
|
|
999
|
+
onFontSubstitution: (substitution) => {
|
|
1000
|
+
if (reportFontSubstitution !== void 0) {
|
|
1001
|
+
reportFontSubstitution(substitution);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
diagnosticCount += 1;
|
|
1005
|
+
reporter.report(fontSubstitutionToDiagnostic(substitution));
|
|
1006
|
+
},
|
|
512
1007
|
onSubstitution: (substitution, context) => {
|
|
513
1008
|
diagnosticCount += 1;
|
|
514
1009
|
reporter.report(substitutionToDiagnostic(substitution, context.pageIndex));
|
|
@@ -537,6 +1032,7 @@ function registerOdmCommand(program) {
|
|
|
537
1032
|
addJsonOption(command);
|
|
538
1033
|
addQuietOption(command);
|
|
539
1034
|
addVerboseOption(command);
|
|
1035
|
+
addFontOptions(command);
|
|
540
1036
|
command.option("--chapters-dir <dir>", "directory to search for each unresolved chapter href, matched by the href's own basename");
|
|
541
1037
|
command.option("--chapter <href>=<file>", "resolve one chapter href to a local file explicitly; repeatable", collectChapterOverride, /* @__PURE__ */ new Map());
|
|
542
1038
|
command.action(async (input, output, options) => {
|
|
@@ -633,7 +1129,7 @@ function registerPdfInspectCommand(program) {
|
|
|
633
1129
|
}
|
|
634
1130
|
//#endregion
|
|
635
1131
|
//#region package.json
|
|
636
|
-
var version = "1.
|
|
1132
|
+
var version = "1.2.0";
|
|
637
1133
|
//#endregion
|
|
638
1134
|
//#region src/program.ts
|
|
639
1135
|
function createProgram() {
|
|
@@ -646,6 +1142,7 @@ function createProgram() {
|
|
|
646
1142
|
});
|
|
647
1143
|
registerConversionCommands(program);
|
|
648
1144
|
registerFormatsCommand(program);
|
|
1145
|
+
registerFromPackageCommand(program);
|
|
649
1146
|
registerOdmCommand(program);
|
|
650
1147
|
registerOdbCommands(program);
|
|
651
1148
|
registerPdfInspectCommand(program);
|