@json-to-office/jto-ops 1.4.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.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { FontRuntimeOpts, GenerationWarning, RendererStatus, PptxBatchRasterizer, PptxRasterizer, ResolvedFont } from '@json-to-office/shared';
2
+ import { QualityProfile, QualityPolicy, PreparedDocument, QualityAnalysis } from '@json-to-office/quality';
2
3
 
3
4
  type FormatName = 'docx' | 'pptx';
4
5
  interface GeneratorOptions {
@@ -37,6 +38,13 @@ interface GeneratorOptions {
37
38
  * `GeneratorResult` — allocate one array per logical request.
38
39
  */
39
40
  warnings?: GenerationWarning[];
41
+ /** Design profile and invocation-specific enforcement. */
42
+ quality?: {
43
+ profile?: QualityProfile;
44
+ policy?: QualityPolicy;
45
+ };
46
+ /** Opaque canonical prologue output shared by analysis and rendering. */
47
+ prepared?: PreparedDocument;
40
48
  }
41
49
  interface GeneratorResult {
42
50
  generateBuffer: (document: any) => Promise<Buffer>;
@@ -67,8 +75,14 @@ interface FormatAdapter {
67
75
  valid: boolean;
68
76
  errors?: any[];
69
77
  };
78
+ /** Analyze format-specific design quality with profiles, policy, and gate. */
79
+ analyzeQuality?(doc: unknown, options?: GeneratorOptions): Promise<QualityAnalysis>;
80
+ /** Prepare effective values and provenance once for official pipelines. */
81
+ prepareDocument?(doc: unknown, options?: GeneratorOptions): Promise<PreparedDocument>;
70
82
  generateSchema(options?: any): any;
71
83
  getBuiltinThemes(): Record<string, any>;
84
+ /** Full values for ESM hosts; falls back to `getBuiltinThemes` for plugins. */
85
+ getBuiltinThemeValues?(): Promise<Record<string, any>>;
72
86
  resolveTheme(options: GeneratorOptions): Promise<any>;
73
87
  loadCustomThemes(options: GeneratorOptions): Promise<Record<string, any> | undefined>;
74
88
  /**
@@ -107,8 +121,13 @@ declare class DocxFormatAdapter implements FormatAdapter {
107
121
  valid: boolean;
108
122
  errors?: any[];
109
123
  };
124
+ analyzeQuality(doc: unknown, options?: GeneratorOptions): Promise<QualityAnalysis>;
125
+ prepareDocument(doc: unknown, options?: GeneratorOptions): Promise<PreparedDocument>;
126
+ /** Prepare into a caller-owned sink; `prepareDocument` owns the reporting. */
127
+ private prepareModel;
110
128
  generateSchema(_options?: any): any;
111
129
  getBuiltinThemes(): Record<string, any>;
130
+ getBuiltinThemeValues(): Promise<Record<string, any>>;
112
131
  resolveTheme(options: GeneratorOptions): Promise<any>;
113
132
  /**
114
133
  * Resolve `theme`/`themePath` once for a whole run: `themePath` is read a
@@ -134,8 +153,13 @@ declare class PptxFormatAdapter implements FormatAdapter {
134
153
  valid: boolean;
135
154
  errors?: any[];
136
155
  };
156
+ analyzeQuality(doc: unknown, options?: GeneratorOptions): Promise<QualityAnalysis>;
157
+ prepareDocument(doc: unknown, options?: GeneratorOptions): Promise<PreparedDocument>;
158
+ /** Prepare into a caller-owned sink; `prepareDocument` owns the reporting. */
159
+ private prepareModel;
137
160
  generateSchema(_options?: any): any;
138
161
  getBuiltinThemes(): Record<string, any>;
162
+ getBuiltinThemeValues(): Promise<Record<string, any>>;
139
163
  resolveTheme(options: GeneratorOptions): Promise<any>;
140
164
  /**
141
165
  * Resolve `theme`/`themePath` once for a whole run: `themePath` is read a
@@ -230,6 +254,55 @@ declare function createLibreOfficePptxBatchRasterizer(options?: {
230
254
  cacheDir?: string | null;
231
255
  }): PptxBatchRasterizer;
232
256
 
257
+ /**
258
+ * Text geometry from a rendered PDF — the ground truth the quality
259
+ * estimators are guessing at (#216 follow-up).
260
+ *
261
+ * The pptx preview pipeline already produces a PDF (soffice → pdftoppm) and
262
+ * uses it purely as a bitmap source. That PDF records the exact position of
263
+ * every glyph as laid out by LibreOffice — the same engine the quality rules
264
+ * try to predict. `pdftotext -bbox` (poppler, already a rasterizer
265
+ * dependency alongside pdftoppm) dumps per-word bounding boxes; this module
266
+ * parses them into slide-space points so callers can compare a rule's
267
+ * estimate against what the renderer actually did.
268
+ *
269
+ * Coordinates: PDF points (1/72 in), origin at the page's top-left corner,
270
+ * y increasing downward — the same frame as authored inches × 72. A PDF page
271
+ * rendered from a slide has the slide's dimensions, so word boxes compare
272
+ * directly against authored shape geometry with no transform.
273
+ *
274
+ * Consumers: the quality ground-truth harness (estimator calibration) today;
275
+ * a `rendered`-certainty analysis pass tomorrow.
276
+ */
277
+ /** One word as laid out on the page, in PDF points, top-left origin. */
278
+ interface PdfTextWord {
279
+ text: string;
280
+ xMin: number;
281
+ yMin: number;
282
+ xMax: number;
283
+ yMax: number;
284
+ }
285
+ /** One PDF page: its size in points plus every word poppler segmented. */
286
+ interface PdfTextPage {
287
+ widthPt: number;
288
+ heightPt: number;
289
+ words: PdfTextWord[];
290
+ }
291
+ /**
292
+ * Parse `pdftotext -bbox` output (XHTML with `<page>`/`<word>` elements).
293
+ * Pure — feed it a captured document for tests, or the runner's stdout.
294
+ */
295
+ declare function parsePdfTextBbox(bboxXml: string): PdfTextPage[];
296
+ /** True when a `pdftotext` binary is reachable — lets harnesses skip early. */
297
+ declare function pdftotextAvailable(): Promise<boolean>;
298
+ /**
299
+ * Extract per-word text geometry from a PDF on disk. One pdftotext spawn,
300
+ * output streamed through stdout — nothing else touches the filesystem.
301
+ */
302
+ declare function extractPdfTextGeometry(pdfPath: string, options?: {
303
+ timeoutMs?: number;
304
+ }): Promise<PdfTextPage[]>;
305
+
233
306
  /**
234
307
  * Make resolved fonts visible to the LibreOffice child process for the
235
308
  * duration of one PDF conversion, then clean up.
@@ -421,4 +494,4 @@ declare function emitDiagnostic(text: string, tone?: DiagnosticTone): void;
421
494
  */
422
495
  declare const stderrDiagnosticSink: DiagnosticSink;
423
496
 
424
- export { type DiagnosticSink, type DiagnosticTone, DocxFormatAdapter, type FontStageHandle, type FontStageOptions, type FontStager, FontconfigStager, type FormatAdapter, type FormatName, type GeneratorOptions, type GeneratorResult, MacOSCoreTextStager, NoopFontStager, PptxFormatAdapter, type RasterizerCacheStats, WindowsFontStager, clearRasterizerCache, createAdapter, createLibreOfficePptxBatchRasterizer, createLibreOfficePptxRasterizer, emitDiagnostic, getFontStager, getRasterizerCacheStats, runWithDiagnosticSink, stderrDiagnosticSink };
497
+ export { type DiagnosticSink, type DiagnosticTone, DocxFormatAdapter, type FontStageHandle, type FontStageOptions, type FontStager, FontconfigStager, type FormatAdapter, type FormatName, type GeneratorOptions, type GeneratorResult, MacOSCoreTextStager, NoopFontStager, type PdfTextPage, type PdfTextWord, PptxFormatAdapter, type RasterizerCacheStats, WindowsFontStager, clearRasterizerCache, createAdapter, createLibreOfficePptxBatchRasterizer, createLibreOfficePptxRasterizer, emitDiagnostic, extractPdfTextGeometry, getFontStager, getRasterizerCacheStats, parsePdfTextBbox, pdftotextAvailable, runWithDiagnosticSink, stderrDiagnosticSink };
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";
@@ -967,12 +997,24 @@ var DocxFormatAdapter = class {
967
997
  async generateBuffer(json, options) {
968
998
  const core = await import("@json-to-office/core-docx");
969
999
  const parsed = typeof json === "string" ? JSON.parse(json) : json;
970
- const resolved = await this.resolveThemes(options);
971
- const { document: docDefinition, customThemes } = withRequestedTheme(
972
- parsed,
973
- resolved.requested,
974
- resolved.customThemes
1000
+ const prepared = preparedFor(
1001
+ options.prepared?.format === "docx" ? options.prepared : void 0,
1002
+ parsed
975
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
+ }
976
1018
  const services = buildDocxServices();
977
1019
  const warnings = [];
978
1020
  const buffer = await core.generateBufferFromJson(docDefinition, {
@@ -986,10 +1028,12 @@ var DocxFormatAdapter = class {
986
1028
  generatedAt: options.generatedAt,
987
1029
  baseDir: options.baseDir,
988
1030
  renderer: options.renderer,
1031
+ prepared,
989
1032
  warnings
990
1033
  });
991
- emitGenerationWarnings(warnings);
992
- options.warnings?.push(...toGenerationWarnings(warnings));
1034
+ const emitted = withoutPreparedWarnings(warnings, prepared);
1035
+ emitGenerationWarnings(emitted);
1036
+ options.warnings?.push(...toGenerationWarnings(emitted));
993
1037
  return buffer;
994
1038
  }
995
1039
  async createGenerator(plugins, options) {
@@ -997,32 +1041,45 @@ var DocxFormatAdapter = class {
997
1041
  const hasPlugins = plugins.length > 0;
998
1042
  const pluginNames = plugins.map((p) => p.name);
999
1043
  const services = buildDocxServices();
1000
- const {
1001
- requested: requestedTheme,
1002
- customThemes,
1003
- label: themeLabel
1004
- } = 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;
1005
1049
  if (!hasPlugins) {
1050
+ let fallbackThemes;
1051
+ const themesFor = () => fallbackThemes ??= resolved ? Promise.resolve(resolved) : this.resolveThemes(options);
1006
1052
  return {
1007
1053
  generateBuffer: async (document) => {
1008
1054
  const parsed = typeof document === "string" ? JSON.parse(document) : document;
1009
- 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
+ );
1010
1062
  const warnings = [];
1011
- const buffer = await core.generateBufferFromJson(docDefinition, {
1012
- customThemes: themes,
1013
- services,
1014
- fonts: options.fonts,
1015
- validation: {
1016
- allowUnknownFields: options.validation?.allowUnknownFields
1017
- },
1018
- deterministic: options.deterministic,
1019
- generatedAt: options.generatedAt,
1020
- baseDir: options.baseDir,
1021
- renderer: options.renderer,
1022
- warnings
1023
- });
1024
- emitGenerationWarnings(warnings);
1025
- 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));
1026
1083
  return buffer;
1027
1084
  },
1028
1085
  hasPlugins: false,
@@ -1104,6 +1161,64 @@ var DocxFormatAdapter = class {
1104
1161
  ...result.errors.length > 0 && { errors: result.errors }
1105
1162
  };
1106
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
+ }
1107
1222
  generateSchema(_options) {
1108
1223
  return null;
1109
1224
  }
@@ -1115,6 +1230,10 @@ var DocxFormatAdapter = class {
1115
1230
  return {};
1116
1231
  }
1117
1232
  }
1233
+ async getBuiltinThemeValues() {
1234
+ const core = await import("@json-to-office/core-docx");
1235
+ return core.themes || {};
1236
+ }
1118
1237
  async resolveTheme(options) {
1119
1238
  const core = await import("@json-to-office/core-docx");
1120
1239
  const { requested } = await this.resolveThemes(options);
@@ -1227,12 +1346,24 @@ var PptxFormatAdapter = class {
1227
1346
  async generateBuffer(json, options) {
1228
1347
  const core = await import("@json-to-office/core-pptx");
1229
1348
  const parsed = typeof json === "string" ? JSON.parse(json) : json;
1230
- const resolved = await this.resolveThemes(options);
1231
- const { document: docDefinition, customThemes } = withRequestedTheme(
1232
- parsed,
1233
- resolved.requested,
1234
- resolved.customThemes
1349
+ const prepared = preparedFor(
1350
+ options.prepared?.format === "pptx" ? options.prepared : void 0,
1351
+ parsed
1235
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
+ }
1236
1367
  const services = buildServicesFromEnv();
1237
1368
  const result = await core.generateBufferWithWarnings(docDefinition, {
1238
1369
  customThemes,
@@ -1244,11 +1375,15 @@ var PptxFormatAdapter = class {
1244
1375
  deterministic: options.deterministic,
1245
1376
  generatedAt: options.generatedAt,
1246
1377
  baseDir: options.baseDir,
1247
- renderer: options.renderer
1378
+ renderer: options.renderer,
1379
+ prepared
1248
1380
  });
1249
- const normalized = toGenerationWarnings(result.warnings);
1250
- emitGenerationWarnings(normalized);
1251
- options.warnings?.push(...normalized);
1381
+ const emitted = withoutPreparedWarnings(
1382
+ toGenerationWarnings(result.warnings),
1383
+ prepared
1384
+ );
1385
+ emitGenerationWarnings(emitted);
1386
+ options.warnings?.push(...emitted);
1252
1387
  return result.buffer;
1253
1388
  }
1254
1389
  async createGenerator(plugins, options) {
@@ -1256,31 +1391,46 @@ var PptxFormatAdapter = class {
1256
1391
  const hasPlugins = plugins.length > 0;
1257
1392
  const pluginNames = plugins.map((p) => p.name);
1258
1393
  const services = buildServicesFromEnv();
1259
- const {
1260
- requested: requestedTheme,
1261
- customThemes,
1262
- label: themeLabel
1263
- } = 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;
1264
1399
  if (!hasPlugins) {
1400
+ let fallbackThemes;
1401
+ const themesFor = () => fallbackThemes ??= resolved ? Promise.resolve(resolved) : this.resolveThemes(options);
1265
1402
  return {
1266
1403
  generateBuffer: async (document) => {
1267
1404
  const parsed = typeof document === "string" ? JSON.parse(document) : document;
1268
- const { document: docDefinition, customThemes: themes } = withRequestedTheme(parsed, requestedTheme, customThemes);
1269
- const result = await core.generateBufferWithWarnings(docDefinition, {
1270
- customThemes: themes,
1271
- services,
1272
- fonts: options.fonts,
1273
- validation: {
1274
- allowUnknownFields: options.validation?.allowUnknownFields
1275
- },
1276
- deterministic: options.deterministic,
1277
- generatedAt: options.generatedAt,
1278
- baseDir: options.baseDir,
1279
- renderer: options.renderer
1280
- });
1281
- const normalized = toGenerationWarnings(result.warnings);
1282
- emitGenerationWarnings(normalized);
1283
- 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);
1284
1434
  return result.buffer;
1285
1435
  },
1286
1436
  hasPlugins: false,
@@ -1345,6 +1495,66 @@ var PptxFormatAdapter = class {
1345
1495
  ...result.errors.length > 0 && { errors: result.errors }
1346
1496
  };
1347
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
+ }
1348
1558
  generateSchema(_options) {
1349
1559
  return null;
1350
1560
  }
@@ -1356,6 +1566,10 @@ var PptxFormatAdapter = class {
1356
1566
  return {};
1357
1567
  }
1358
1568
  }
1569
+ async getBuiltinThemeValues() {
1570
+ const core = await import("@json-to-office/core-pptx");
1571
+ return core.pptxThemes || {};
1572
+ }
1359
1573
  async resolveTheme(options) {
1360
1574
  const core = await import("@json-to-office/core-pptx");
1361
1575
  const themes = core.pptxThemes || {};
@@ -1448,6 +1662,116 @@ function createAdapter(format) {
1448
1662
  throw new Error(`Unknown format: ${format}`);
1449
1663
  }
1450
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
+ "&amp;": "&",
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
+ }
1451
1775
  export {
1452
1776
  DocxFormatAdapter,
1453
1777
  FontconfigStager,
@@ -1460,8 +1784,11 @@ export {
1460
1784
  createLibreOfficePptxBatchRasterizer,
1461
1785
  createLibreOfficePptxRasterizer,
1462
1786
  emitDiagnostic,
1787
+ extractPdfTextGeometry,
1463
1788
  getFontStager,
1464
1789
  getRasterizerCacheStats,
1790
+ parsePdfTextBbox,
1791
+ pdftotextAvailable,
1465
1792
  runWithDiagnosticSink,
1466
1793
  stderrDiagnosticSink
1467
1794
  };