@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 +74 -1
- package/dist/index.js +385 -58
- package/dist/index.js.map +1 -1
- package/package.json +8 -6
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
|
|
971
|
-
|
|
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
|
-
|
|
992
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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
|
|
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(
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
|
1231
|
-
|
|
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
|
|
1250
|
-
|
|
1251
|
-
|
|
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
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
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
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
+
"&": "&",
|
|
1671
|
+
"<": "<",
|
|
1672
|
+
">": ">",
|
|
1673
|
+
""": '"',
|
|
1674
|
+
"'": "'",
|
|
1675
|
+
""": '"',
|
|
1676
|
+
"'": "'"
|
|
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
|
};
|