document-cli 1.1.0 → 1.2.1

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