document-cli 1.0.1 → 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/dist/index.cjs CHANGED
@@ -15,6 +15,8 @@ const EXTENSION_TO_FORMAT = {
15
15
  ods: "ods",
16
16
  odg: "odg",
17
17
  odf: "odf",
18
+ markdown: "markdown",
19
+ md: "markdown",
18
20
  pdf: "pdf"
19
21
  };
20
22
  const FORMAT_TO_EXTENSION = {
@@ -26,6 +28,7 @@ const FORMAT_TO_EXTENSION = {
26
28
  ods: "ods",
27
29
  odg: "odg",
28
30
  odf: "odf",
31
+ markdown: "md",
29
32
  pdf: "pdf"
30
33
  };
31
34
  function isDocumentFormat(value) {
@@ -109,6 +112,22 @@ function createDiagnosticReporter(options) {
109
112
  }
110
113
  };
111
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
+ }
112
131
  function substitutionToDiagnostic(substitution, pageIndex) {
113
132
  return {
114
133
  severity: "warning",
@@ -117,6 +136,13 @@ function substitutionToDiagnostic(substitution, pageIndex) {
117
136
  pageIndex
118
137
  };
119
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
+ }
120
146
  function pdfDiagnosticToDiagnostic(diagnostic) {
121
147
  return {
122
148
  severity: diagnostic.severity,
@@ -141,6 +167,172 @@ function mapErrorToExit(error, abortReason) {
141
167
  return 1;
142
168
  }
143
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
144
336
  //#region src/runtime/io.ts
145
337
  function isUint8Array(value) {
146
338
  return value instanceof Uint8Array;
@@ -181,6 +373,18 @@ function resolveDefaultOutputPath(inputPath, targetFormat) {
181
373
  }
182
374
  //#endregion
183
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
+ }
184
388
  function formatError(error, verbose) {
185
389
  if (!(error instanceof Error)) return `error: ${String(error)}`;
186
390
  const stackClause = verbose && error.stack !== void 0 ? `\n${error.stack}` : "";
@@ -197,13 +401,22 @@ function buildConversionAction(source, target) {
197
401
  const { signal, getAbortReason } = createRuntimeSignal({ timeoutMs: options.timeoutMs });
198
402
  try {
199
403
  const inputBytes = await readInput(input, { signal });
404
+ const fonts = await loadProvidedFonts(options.fontFiles ?? [], { signal });
200
405
  const result = await (0, documents_js.createLocalDocumentConverter)().convert({
201
406
  source: {
202
407
  format: source,
203
408
  bytes: new Uint8Array(inputBytes)
204
409
  },
205
410
  targetFormat: target
206
- }, { signal });
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
+ });
207
420
  await writeOutput(resolvedOutput, result.document.bytes);
208
421
  const reporter = createDiagnosticReporter({
209
422
  json: options.json,
@@ -212,7 +425,7 @@ function buildConversionAction(source, target) {
212
425
  });
213
426
  for (const diagnostic of result.diagnostics) reporter.report(diagnostic);
214
427
  if (options.dumpPackage !== void 0) if (result.package === void 0) process.stderr.write(`[${command}] this conversion does not produce an intermediate DocumentPackage\n`);
215
- 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));
216
429
  reporter.summarize({
217
430
  output: resolvedOutput,
218
431
  bytes: result.document.bytes.byteLength,
@@ -245,6 +458,14 @@ function addVerboseOption(command) {
245
458
  function addDumpPackageOption(command) {
246
459
  return command.option("--dump-package <file>", "write the intermediate DocumentPackage (content + layout) this conversion built to a JSON file");
247
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
+ }
248
469
  function addConversionFlags(command) {
249
470
  addOutOption(command);
250
471
  addTimeoutOption(command);
@@ -262,21 +483,11 @@ function toConversionCommandOptions(options) {
262
483
  json: options.json,
263
484
  quiet: options.quiet,
264
485
  verbose: options.verbose,
265
- dumpPackage: options.dumpPackage
486
+ dumpPackage: options.dumpPackage,
487
+ fontFiles: options.fontFile,
488
+ reportFontSubstitutions: options.reportFontSubstitutions
266
489
  };
267
490
  }
268
- const KNOWN_FORMATS = "docx, pptx, xlsx, odt, odp, ods, odg, odf, pdf";
269
- function resolveGenericTarget(output, options) {
270
- if (options.to !== void 0) {
271
- if (!isDocumentFormat(options.to)) return { errorMessage: `unknown --to format '${options.to}'; expected one of ${KNOWN_FORMATS}` };
272
- return { format: options.to };
273
- }
274
- const destination = output ?? options.out;
275
- 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>" };
276
- const inferred = inferFormatFromExtension(destination);
277
- if (inferred === void 0) return { errorMessage: `cannot infer a target format from '${destination}'; pass --to <format> instead` };
278
- return { format: inferred };
279
- }
280
491
  async function runGenericConvert(input, output, options) {
281
492
  const extension = (0, node_path.extname)(input).toLowerCase();
282
493
  if (extension === ".odm") {
@@ -289,10 +500,10 @@ async function runGenericConvert(input, output, options) {
289
500
  }
290
501
  const source = inferFormatFromExtension(input);
291
502
  if (source === void 0) {
292
- process.stderr.write(`convert: cannot infer a source format from '${input}'; rename the file with a recognised extension (${KNOWN_FORMATS}) or use one of the explicit '<source>-to-<target>' commands\n`);
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`);
293
504
  return 2;
294
505
  }
295
- const target = resolveGenericTarget(output, options);
506
+ const target = resolveTargetFormat(output, options.out, options.to);
296
507
  if ("errorMessage" in target) {
297
508
  process.stderr.write(`convert: ${target.errorMessage}\n`);
298
509
  return 2;
@@ -306,6 +517,7 @@ function registerConversionCommands(program) {
306
517
  const command = program.command(`${commandName} <input> [output]`).description(`convert a ${source} document to ${target}`);
307
518
  addConversionFlags(command);
308
519
  addDumpPackageOption(command);
520
+ if (target === "pdf") addFontOptions(command);
309
521
  command.action(async (input, output, options) => {
310
522
  process.exitCode = await buildConversionAction(source, target)(input, output, toConversionCommandOptions(options));
311
523
  });
@@ -313,14 +525,15 @@ function registerConversionCommands(program) {
313
525
  const generic = program.command("convert <input> [output]").description("convert between any two supported document formats, inferring source/target from file extensions where possible");
314
526
  addConversionFlags(generic);
315
527
  addDumpPackageOption(generic);
316
- generic.option("--to <format>", `target format when it cannot be inferred from the output path (${KNOWN_FORMATS})`);
528
+ addFontOptions(generic);
529
+ generic.option("--to <format>", `target format when it cannot be inferred from the output path (${KNOWN_DOCUMENT_FORMATS})`);
317
530
  generic.action(async (input, output, options) => {
318
531
  process.exitCode = await runGenericConvert(input, output, options);
319
532
  });
320
533
  }
321
534
  //#endregion
322
535
  //#region src/commands/formats.ts
323
- 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";
324
537
  function registerFormatsCommand(program) {
325
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) => {
326
539
  const { conversions } = (0, documents_js.createLocalDocumentConverter)();
@@ -333,6 +546,218 @@ function registerFormatsCommand(program) {
333
546
  });
334
547
  }
335
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
336
761
  //#region src/commands/odb.ts
337
762
  function reportOdbError(command, error, verbose, abortReason) {
338
763
  if (error instanceof documents_js.OdbNoEmbeddedDataSourceError || error instanceof documents_js.OdbUnsupportedFormatError) {
@@ -434,6 +859,52 @@ async function runOdbTables(input, options) {
434
859
  return reportOdbError(command, error, false, getAbortReason());
435
860
  }
436
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
+ }
437
908
  function registerOdbToXlsxCommand(program) {
438
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");
439
910
  addOutOption(command);
@@ -462,10 +933,22 @@ function registerOdbTablesCommand(program) {
462
933
  process.exitCode = await runOdbTables(input, options);
463
934
  });
464
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
+ }
465
946
  function registerOdbCommands(program) {
466
947
  registerOdbToXlsxCommand(program);
467
948
  registerOdbToCsvCommand(program);
468
949
  registerOdbTablesCommand(program);
950
+ registerOdbFormsCommand(program);
951
+ registerOdbReportsCommand(program);
469
952
  }
470
953
  //#endregion
471
954
  //#region src/commands/odm.ts
@@ -501,11 +984,26 @@ async function runOdmToPdf(input, output, options) {
501
984
  command
502
985
  });
503
986
  let diagnosticCount = 0;
987
+ const reportFontSubstitution = options.reportFontSubstitutions === true ? createFontSubstitutionReporter({
988
+ json: options.json,
989
+ quiet: options.quiet,
990
+ command
991
+ }) : void 0;
504
992
  try {
505
993
  const inputBytes = await readInput(input, { signal });
994
+ const fonts = await loadProvidedFonts(options.fontFile ?? [], { signal });
506
995
  const bytes = (0, documents_js.odmToPdf)(new Uint8Array(inputBytes), {
507
996
  signal,
508
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
+ },
509
1007
  onSubstitution: (substitution, context) => {
510
1008
  diagnosticCount += 1;
511
1009
  reporter.report(substitutionToDiagnostic(substitution, context.pageIndex));
@@ -534,6 +1032,7 @@ function registerOdmCommand(program) {
534
1032
  addJsonOption(command);
535
1033
  addQuietOption(command);
536
1034
  addVerboseOption(command);
1035
+ addFontOptions(command);
537
1036
  command.option("--chapters-dir <dir>", "directory to search for each unresolved chapter href, matched by the href's own basename");
538
1037
  command.option("--chapter <href>=<file>", "resolve one chapter href to a local file explicitly; repeatable", collectChapterOverride, /* @__PURE__ */ new Map());
539
1038
  command.action(async (input, output, options) => {
@@ -630,7 +1129,7 @@ function registerPdfInspectCommand(program) {
630
1129
  }
631
1130
  //#endregion
632
1131
  //#region package.json
633
- var version = "1.0.1";
1132
+ var version = "1.2.0";
634
1133
  //#endregion
635
1134
  //#region src/program.ts
636
1135
  function createProgram() {
@@ -643,6 +1142,7 @@ function createProgram() {
643
1142
  });
644
1143
  registerConversionCommands(program);
645
1144
  registerFormatsCommand(program);
1145
+ registerFromPackageCommand(program);
646
1146
  registerOdmCommand(program);
647
1147
  registerOdbCommands(program);
648
1148
  registerPdfInspectCommand(program);