@json-to-office/jto-ops 1.2.0 → 1.5.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.js CHANGED
@@ -892,6 +892,36 @@ function toGenerationWarnings(raw) {
892
892
  }
893
893
  }));
894
894
  }
895
+ function preparedThemeLabel(prepared) {
896
+ const label = prepared.metadata?.themeLabel;
897
+ return typeof label === "string" ? label : void 0;
898
+ }
899
+ var PREPARED_SOURCE = Symbol("jto.prepared.source");
900
+ var PREPARED_WARNINGS = Symbol("jto.prepared.warnings");
901
+ function stampPrepared(prepared, source, warnings) {
902
+ return Object.assign(prepared, {
903
+ [PREPARED_SOURCE]: source,
904
+ [PREPARED_WARNINGS]: warnings
905
+ });
906
+ }
907
+ function preparedFor(prepared, document) {
908
+ if (!prepared) return void 0;
909
+ const source = prepared[PREPARED_SOURCE];
910
+ return source !== void 0 && source === document ? prepared : void 0;
911
+ }
912
+ function warningKey(warning) {
913
+ return `${warning.component}|${warning.context?.code ?? ""}|${warning.message}`;
914
+ }
915
+ function withoutPreparedWarnings(warnings, prepared) {
916
+ const emitted = prepared?.[PREPARED_WARNINGS];
917
+ if (!emitted || emitted.length === 0) return warnings;
918
+ const seen = new Set(emitted.map(warningKey));
919
+ return warnings.filter((warning) => !seen.has(warningKey(warning)));
920
+ }
921
+ function documentRenderer(document) {
922
+ const renderer = document?.renderer;
923
+ return typeof renderer === "string" ? renderer : void 0;
924
+ }
895
925
  var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
896
926
  function safeThemeKey(name) {
897
927
  return name && !UNSAFE_KEYS.has(name) ? name : "custom";
@@ -960,15 +990,31 @@ var DocxFormatAdapter = class {
960
990
  const core = await import("@json-to-office/core-docx");
961
991
  return core.docxRendererIds();
962
992
  }
993
+ async rendererStatuses() {
994
+ const core = await import("@json-to-office/core-docx");
995
+ return core.docxRendererStatuses();
996
+ }
963
997
  async generateBuffer(json, options) {
964
998
  const core = await import("@json-to-office/core-docx");
965
999
  const parsed = typeof json === "string" ? JSON.parse(json) : json;
966
- const resolved = await this.resolveThemes(options);
967
- const { document: docDefinition, customThemes } = withRequestedTheme(
968
- parsed,
969
- resolved.requested,
970
- resolved.customThemes
1000
+ const prepared = preparedFor(
1001
+ options.prepared?.format === "docx" ? options.prepared : void 0,
1002
+ parsed
971
1003
  );
1004
+ let docDefinition;
1005
+ let customThemes;
1006
+ if (prepared) {
1007
+ docDefinition = prepared.model.authored;
1008
+ } else {
1009
+ const resolved = await this.resolveThemes(options);
1010
+ const normalized = withRequestedTheme(
1011
+ parsed,
1012
+ resolved.requested,
1013
+ resolved.customThemes
1014
+ );
1015
+ docDefinition = normalized.document;
1016
+ customThemes = normalized.customThemes;
1017
+ }
972
1018
  const services = buildDocxServices();
973
1019
  const warnings = [];
974
1020
  const buffer = await core.generateBufferFromJson(docDefinition, {
@@ -982,10 +1028,12 @@ var DocxFormatAdapter = class {
982
1028
  generatedAt: options.generatedAt,
983
1029
  baseDir: options.baseDir,
984
1030
  renderer: options.renderer,
1031
+ prepared,
985
1032
  warnings
986
1033
  });
987
- emitGenerationWarnings(warnings);
988
- options.warnings?.push(...toGenerationWarnings(warnings));
1034
+ const emitted = withoutPreparedWarnings(warnings, prepared);
1035
+ emitGenerationWarnings(emitted);
1036
+ options.warnings?.push(...toGenerationWarnings(emitted));
989
1037
  return buffer;
990
1038
  }
991
1039
  async createGenerator(plugins, options) {
@@ -993,32 +1041,45 @@ var DocxFormatAdapter = class {
993
1041
  const hasPlugins = plugins.length > 0;
994
1042
  const pluginNames = plugins.map((p) => p.name);
995
1043
  const services = buildDocxServices();
996
- const {
997
- requested: requestedTheme,
998
- customThemes,
999
- label: themeLabel
1000
- } = await this.resolveThemes(options);
1044
+ const prepared = !hasPlugins && options.prepared?.format === "docx" ? options.prepared : void 0;
1045
+ const resolved = prepared ? void 0 : await this.resolveThemes(options);
1046
+ const requestedTheme = resolved?.requested;
1047
+ const customThemes = resolved?.customThemes;
1048
+ const themeLabel = prepared ? preparedThemeLabel(prepared) : resolved?.label;
1001
1049
  if (!hasPlugins) {
1050
+ let fallbackThemes;
1051
+ const themesFor = () => fallbackThemes ??= resolved ? Promise.resolve(resolved) : this.resolveThemes(options);
1002
1052
  return {
1003
1053
  generateBuffer: async (document) => {
1004
1054
  const parsed = typeof document === "string" ? JSON.parse(document) : document;
1005
- const { document: docDefinition, customThemes: themes } = withRequestedTheme(parsed, requestedTheme, customThemes);
1055
+ const usable = preparedFor(prepared, parsed);
1056
+ const themes = usable ? void 0 : await themesFor();
1057
+ const normalized = usable ? { document: usable.model.authored, customThemes: void 0 } : withRequestedTheme(
1058
+ parsed,
1059
+ themes?.requested,
1060
+ themes?.customThemes
1061
+ );
1006
1062
  const warnings = [];
1007
- const buffer = await core.generateBufferFromJson(docDefinition, {
1008
- customThemes: themes,
1009
- services,
1010
- fonts: options.fonts,
1011
- validation: {
1012
- allowUnknownFields: options.validation?.allowUnknownFields
1013
- },
1014
- deterministic: options.deterministic,
1015
- generatedAt: options.generatedAt,
1016
- baseDir: options.baseDir,
1017
- renderer: options.renderer,
1018
- warnings
1019
- });
1020
- emitGenerationWarnings(warnings);
1021
- options.warnings?.push(...toGenerationWarnings(warnings));
1063
+ const buffer = await core.generateBufferFromJson(
1064
+ normalized.document,
1065
+ {
1066
+ customThemes: normalized.customThemes,
1067
+ services,
1068
+ fonts: options.fonts,
1069
+ validation: {
1070
+ allowUnknownFields: options.validation?.allowUnknownFields
1071
+ },
1072
+ deterministic: options.deterministic,
1073
+ generatedAt: options.generatedAt,
1074
+ baseDir: options.baseDir,
1075
+ renderer: options.renderer,
1076
+ prepared: usable,
1077
+ warnings
1078
+ }
1079
+ );
1080
+ const emitted = withoutPreparedWarnings(warnings, usable);
1081
+ emitGenerationWarnings(emitted);
1082
+ options.warnings?.push(...toGenerationWarnings(emitted));
1022
1083
  return buffer;
1023
1084
  },
1024
1085
  hasPlugins: false,
@@ -1100,6 +1161,64 @@ var DocxFormatAdapter = class {
1100
1161
  ...result.errors.length > 0 && { errors: result.errors }
1101
1162
  };
1102
1163
  }
1164
+ async analyzeQuality(doc, options = {}) {
1165
+ const core = await import("@json-to-office/core-docx");
1166
+ const parsed = typeof doc === "string" ? JSON.parse(doc) : doc;
1167
+ let prepared = preparedFor(
1168
+ options.prepared?.format === "docx" ? options.prepared : void 0,
1169
+ parsed
1170
+ );
1171
+ if (!prepared) {
1172
+ try {
1173
+ prepared = await this.prepareModel(parsed, options, []);
1174
+ } catch {
1175
+ }
1176
+ }
1177
+ return core.analyzeDocxQuality(prepared?.model.authored ?? parsed, {
1178
+ ...prepared && { prepared },
1179
+ renderer: options.renderer ?? documentRenderer(parsed),
1180
+ profile: options.quality?.profile,
1181
+ policy: options.quality?.policy
1182
+ });
1183
+ }
1184
+ async prepareDocument(doc, options = {}) {
1185
+ const warnings = [];
1186
+ const prepared = await this.prepareModel(doc, options, warnings);
1187
+ emitGenerationWarnings(warnings);
1188
+ options.warnings?.push(...toGenerationWarnings(warnings));
1189
+ return prepared;
1190
+ }
1191
+ /** Prepare into a caller-owned sink; `prepareDocument` owns the reporting. */
1192
+ async prepareModel(doc, options, warnings) {
1193
+ const core = await import("@json-to-office/core-docx");
1194
+ const parsed = typeof doc === "string" ? JSON.parse(doc) : doc;
1195
+ const resolved = await this.resolveThemes(options);
1196
+ const normalized = withRequestedTheme(
1197
+ parsed,
1198
+ resolved.requested,
1199
+ resolved.customThemes
1200
+ );
1201
+ const prepared = core.prepareDocxQualityDocument(
1202
+ normalized.document,
1203
+ {
1204
+ customThemes: normalized.customThemes,
1205
+ fonts: options.fonts,
1206
+ renderer: options.renderer ?? documentRenderer(parsed),
1207
+ warnings
1208
+ }
1209
+ );
1210
+ return stampPrepared(
1211
+ {
1212
+ ...prepared,
1213
+ metadata: {
1214
+ ...prepared.metadata,
1215
+ ...resolved.label && { themeLabel: resolved.label }
1216
+ }
1217
+ },
1218
+ parsed,
1219
+ toGenerationWarnings(warnings)
1220
+ );
1221
+ }
1103
1222
  generateSchema(_options) {
1104
1223
  return null;
1105
1224
  }
@@ -1111,6 +1230,10 @@ var DocxFormatAdapter = class {
1111
1230
  return {};
1112
1231
  }
1113
1232
  }
1233
+ async getBuiltinThemeValues() {
1234
+ const core = await import("@json-to-office/core-docx");
1235
+ return core.themes || {};
1236
+ }
1114
1237
  async resolveTheme(options) {
1115
1238
  const core = await import("@json-to-office/core-docx");
1116
1239
  const { requested } = await this.resolveThemes(options);
@@ -1216,15 +1339,31 @@ var PptxFormatAdapter = class {
1216
1339
  const core = await import("@json-to-office/core-pptx");
1217
1340
  return core.pptxRendererIds();
1218
1341
  }
1342
+ async rendererStatuses() {
1343
+ const core = await import("@json-to-office/core-pptx");
1344
+ return core.pptxRendererStatuses();
1345
+ }
1219
1346
  async generateBuffer(json, options) {
1220
1347
  const core = await import("@json-to-office/core-pptx");
1221
1348
  const parsed = typeof json === "string" ? JSON.parse(json) : json;
1222
- const resolved = await this.resolveThemes(options);
1223
- const { document: docDefinition, customThemes } = withRequestedTheme(
1224
- parsed,
1225
- resolved.requested,
1226
- resolved.customThemes
1349
+ const prepared = preparedFor(
1350
+ options.prepared?.format === "pptx" ? options.prepared : void 0,
1351
+ parsed
1227
1352
  );
1353
+ let docDefinition;
1354
+ let customThemes;
1355
+ if (prepared) {
1356
+ docDefinition = prepared.model.authored;
1357
+ } else {
1358
+ const resolved = await this.resolveThemes(options);
1359
+ const normalized = withRequestedTheme(
1360
+ parsed,
1361
+ resolved.requested,
1362
+ resolved.customThemes
1363
+ );
1364
+ docDefinition = normalized.document;
1365
+ customThemes = normalized.customThemes;
1366
+ }
1228
1367
  const services = buildServicesFromEnv();
1229
1368
  const result = await core.generateBufferWithWarnings(docDefinition, {
1230
1369
  customThemes,
@@ -1236,11 +1375,15 @@ var PptxFormatAdapter = class {
1236
1375
  deterministic: options.deterministic,
1237
1376
  generatedAt: options.generatedAt,
1238
1377
  baseDir: options.baseDir,
1239
- renderer: options.renderer
1378
+ renderer: options.renderer,
1379
+ prepared
1240
1380
  });
1241
- const normalized = toGenerationWarnings(result.warnings);
1242
- emitGenerationWarnings(normalized);
1243
- options.warnings?.push(...normalized);
1381
+ const emitted = withoutPreparedWarnings(
1382
+ toGenerationWarnings(result.warnings),
1383
+ prepared
1384
+ );
1385
+ emitGenerationWarnings(emitted);
1386
+ options.warnings?.push(...emitted);
1244
1387
  return result.buffer;
1245
1388
  }
1246
1389
  async createGenerator(plugins, options) {
@@ -1248,31 +1391,46 @@ var PptxFormatAdapter = class {
1248
1391
  const hasPlugins = plugins.length > 0;
1249
1392
  const pluginNames = plugins.map((p) => p.name);
1250
1393
  const services = buildServicesFromEnv();
1251
- const {
1252
- requested: requestedTheme,
1253
- customThemes,
1254
- label: themeLabel
1255
- } = await this.resolveThemes(options);
1394
+ const prepared = !hasPlugins && options.prepared?.format === "pptx" ? options.prepared : void 0;
1395
+ const resolved = prepared ? void 0 : await this.resolveThemes(options);
1396
+ const requestedTheme = resolved?.requested;
1397
+ const customThemes = resolved?.customThemes;
1398
+ const themeLabel = prepared ? preparedThemeLabel(prepared) : resolved?.label;
1256
1399
  if (!hasPlugins) {
1400
+ let fallbackThemes;
1401
+ const themesFor = () => fallbackThemes ??= resolved ? Promise.resolve(resolved) : this.resolveThemes(options);
1257
1402
  return {
1258
1403
  generateBuffer: async (document) => {
1259
1404
  const parsed = typeof document === "string" ? JSON.parse(document) : document;
1260
- const { document: docDefinition, customThemes: themes } = withRequestedTheme(parsed, requestedTheme, customThemes);
1261
- const result = await core.generateBufferWithWarnings(docDefinition, {
1262
- customThemes: themes,
1263
- services,
1264
- fonts: options.fonts,
1265
- validation: {
1266
- allowUnknownFields: options.validation?.allowUnknownFields
1267
- },
1268
- deterministic: options.deterministic,
1269
- generatedAt: options.generatedAt,
1270
- baseDir: options.baseDir,
1271
- renderer: options.renderer
1272
- });
1273
- const normalized = toGenerationWarnings(result.warnings);
1274
- emitGenerationWarnings(normalized);
1275
- options.warnings?.push(...normalized);
1405
+ const usable = preparedFor(prepared, parsed);
1406
+ const themes = usable ? void 0 : await themesFor();
1407
+ const normalized = usable ? { document: usable.model.authored, customThemes: void 0 } : withRequestedTheme(
1408
+ parsed,
1409
+ themes?.requested,
1410
+ themes?.customThemes
1411
+ );
1412
+ const result = await core.generateBufferWithWarnings(
1413
+ normalized.document,
1414
+ {
1415
+ customThemes: normalized.customThemes,
1416
+ services,
1417
+ fonts: options.fonts,
1418
+ validation: {
1419
+ allowUnknownFields: options.validation?.allowUnknownFields
1420
+ },
1421
+ deterministic: options.deterministic,
1422
+ generatedAt: options.generatedAt,
1423
+ baseDir: options.baseDir,
1424
+ renderer: options.renderer,
1425
+ prepared: usable
1426
+ }
1427
+ );
1428
+ const warnings = withoutPreparedWarnings(
1429
+ toGenerationWarnings(result.warnings),
1430
+ usable
1431
+ );
1432
+ emitGenerationWarnings(warnings);
1433
+ options.warnings?.push(...warnings);
1276
1434
  return result.buffer;
1277
1435
  },
1278
1436
  hasPlugins: false,
@@ -1337,6 +1495,66 @@ var PptxFormatAdapter = class {
1337
1495
  ...result.errors.length > 0 && { errors: result.errors }
1338
1496
  };
1339
1497
  }
1498
+ async analyzeQuality(doc, options = {}) {
1499
+ const core = await import("@json-to-office/core-pptx");
1500
+ const parsed = typeof doc === "string" ? JSON.parse(doc) : doc;
1501
+ let prepared = preparedFor(
1502
+ options.prepared?.format === "pptx" ? options.prepared : void 0,
1503
+ parsed
1504
+ );
1505
+ if (!prepared) {
1506
+ try {
1507
+ prepared = await this.prepareModel(parsed, options, []);
1508
+ } catch {
1509
+ }
1510
+ }
1511
+ return core.analyzePptxQuality(prepared?.model.authored ?? parsed, {
1512
+ ...prepared && { prepared },
1513
+ renderer: options.renderer ?? documentRenderer(parsed),
1514
+ profile: options.quality?.profile,
1515
+ policy: options.quality?.policy
1516
+ });
1517
+ }
1518
+ async prepareDocument(doc, options = {}) {
1519
+ const warnings = [];
1520
+ const prepared = await this.prepareModel(doc, options, warnings);
1521
+ const normalized = toGenerationWarnings(warnings);
1522
+ emitGenerationWarnings(normalized);
1523
+ options.warnings?.push(...normalized);
1524
+ return prepared;
1525
+ }
1526
+ /** Prepare into a caller-owned sink; `prepareDocument` owns the reporting. */
1527
+ async prepareModel(doc, options, warnings) {
1528
+ const core = await import("@json-to-office/core-pptx");
1529
+ const parsed = typeof doc === "string" ? JSON.parse(doc) : doc;
1530
+ const resolved = await this.resolveThemes(options);
1531
+ const normalized = withRequestedTheme(
1532
+ parsed,
1533
+ resolved.requested,
1534
+ resolved.customThemes
1535
+ );
1536
+ const prepared = core.preparePptxQualityDocument(
1537
+ normalized.document,
1538
+ {
1539
+ customThemes: normalized.customThemes,
1540
+ fonts: options.fonts,
1541
+ services: buildServicesFromEnv(),
1542
+ renderer: options.renderer ?? documentRenderer(parsed),
1543
+ warnings
1544
+ }
1545
+ );
1546
+ return stampPrepared(
1547
+ {
1548
+ ...prepared,
1549
+ metadata: {
1550
+ ...prepared.metadata,
1551
+ ...resolved.label && { themeLabel: resolved.label }
1552
+ }
1553
+ },
1554
+ parsed,
1555
+ toGenerationWarnings(warnings)
1556
+ );
1557
+ }
1340
1558
  generateSchema(_options) {
1341
1559
  return null;
1342
1560
  }
@@ -1348,6 +1566,10 @@ var PptxFormatAdapter = class {
1348
1566
  return {};
1349
1567
  }
1350
1568
  }
1569
+ async getBuiltinThemeValues() {
1570
+ const core = await import("@json-to-office/core-pptx");
1571
+ return core.pptxThemes || {};
1572
+ }
1351
1573
  async resolveTheme(options) {
1352
1574
  const core = await import("@json-to-office/core-pptx");
1353
1575
  const themes = core.pptxThemes || {};
@@ -1440,6 +1662,116 @@ function createAdapter(format) {
1440
1662
  throw new Error(`Unknown format: ${format}`);
1441
1663
  }
1442
1664
  }
1665
+
1666
+ // src/pdf-text-geometry.ts
1667
+ import { execFile as execFile2 } from "child_process";
1668
+ import * as path6 from "path";
1669
+ var ENTITIES = {
1670
+ "&": "&",
1671
+ "&lt;": "<",
1672
+ "&gt;": ">",
1673
+ "&quot;": '"',
1674
+ "&apos;": "'",
1675
+ "&#34;": '"',
1676
+ "&#39;": "'"
1677
+ };
1678
+ function decodeEntities(value) {
1679
+ return value.replace(
1680
+ /&(?:amp|lt|gt|quot|apos|#34|#39);/g,
1681
+ (entity) => ENTITIES[entity] ?? entity
1682
+ );
1683
+ }
1684
+ var PAGE_PATTERN = /<page\s+width="([\d.]+)"\s+height="([\d.]+)">([\s\S]*?)<\/page>/g;
1685
+ var WORD_PATTERN = /<word\s+xMin="(-?[\d.]+)"\s+yMin="(-?[\d.]+)"\s+xMax="(-?[\d.]+)"\s+yMax="(-?[\d.]+)">([\s\S]*?)<\/word>/g;
1686
+ function parsePdfTextBbox(bboxXml) {
1687
+ const pages = [];
1688
+ for (const pageMatch of bboxXml.matchAll(PAGE_PATTERN)) {
1689
+ const words = [];
1690
+ for (const wordMatch of pageMatch[3].matchAll(WORD_PATTERN)) {
1691
+ words.push({
1692
+ xMin: Number(wordMatch[1]),
1693
+ yMin: Number(wordMatch[2]),
1694
+ xMax: Number(wordMatch[3]),
1695
+ yMax: Number(wordMatch[4]),
1696
+ text: decodeEntities(wordMatch[5])
1697
+ });
1698
+ }
1699
+ pages.push({
1700
+ widthPt: Number(pageMatch[1]),
1701
+ heightPt: Number(pageMatch[2]),
1702
+ words
1703
+ });
1704
+ }
1705
+ return pages;
1706
+ }
1707
+ function pdftotextCandidates() {
1708
+ const candidates = [];
1709
+ const configured = process.env.PDFTOTEXT_PATH?.trim();
1710
+ if (configured) candidates.push(configured);
1711
+ candidates.push("pdftotext");
1712
+ return [...new Set(candidates)];
1713
+ }
1714
+ async function run(binary, args, timeoutMs) {
1715
+ return new Promise((resolve2, reject) => {
1716
+ execFile2(
1717
+ binary,
1718
+ args,
1719
+ { timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 },
1720
+ (error, stdout) => {
1721
+ if (error) reject(error);
1722
+ else resolve2(stdout);
1723
+ }
1724
+ );
1725
+ });
1726
+ }
1727
+ var pdftotextPromise;
1728
+ async function resolvePdftotext() {
1729
+ if (!pdftotextPromise) {
1730
+ pdftotextPromise = (async () => {
1731
+ for (const candidate of pdftotextCandidates()) {
1732
+ if (candidate.includes(path6.sep)) {
1733
+ try {
1734
+ await run(candidate, ["-v"], 1e4);
1735
+ return candidate;
1736
+ } catch {
1737
+ continue;
1738
+ }
1739
+ }
1740
+ try {
1741
+ await run(candidate, ["-v"], 1e4);
1742
+ return candidate;
1743
+ } catch (error) {
1744
+ const code = error.code;
1745
+ if (code !== "ENOENT" && code !== "EACCES") return candidate;
1746
+ }
1747
+ }
1748
+ throw new Error(
1749
+ `Text geometry extraction needs pdftotext (poppler), which was not found. Install poppler-utils or set PDFTOTEXT_PATH (searched: ${pdftotextCandidates().join(", ")}).`
1750
+ );
1751
+ })().catch((error) => {
1752
+ pdftotextPromise = void 0;
1753
+ throw error;
1754
+ });
1755
+ }
1756
+ return pdftotextPromise;
1757
+ }
1758
+ async function pdftotextAvailable() {
1759
+ try {
1760
+ await resolvePdftotext();
1761
+ return true;
1762
+ } catch {
1763
+ return false;
1764
+ }
1765
+ }
1766
+ async function extractPdfTextGeometry(pdfPath, options = {}) {
1767
+ const binary = await resolvePdftotext();
1768
+ const stdout = await run(
1769
+ binary,
1770
+ ["-bbox", pdfPath, "-"],
1771
+ options.timeoutMs ?? 6e4
1772
+ );
1773
+ return parsePdfTextBbox(stdout);
1774
+ }
1443
1775
  export {
1444
1776
  DocxFormatAdapter,
1445
1777
  FontconfigStager,
@@ -1452,8 +1784,11 @@ export {
1452
1784
  createLibreOfficePptxBatchRasterizer,
1453
1785
  createLibreOfficePptxRasterizer,
1454
1786
  emitDiagnostic,
1787
+ extractPdfTextGeometry,
1455
1788
  getFontStager,
1456
1789
  getRasterizerCacheStats,
1790
+ parsePdfTextBbox,
1791
+ pdftotextAvailable,
1457
1792
  runWithDiagnosticSink,
1458
1793
  stderrDiagnosticSink
1459
1794
  };