@json-to-office/core-pptx 6.8.0 → 7.0.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.
Files changed (37) hide show
  1. package/dist/blocks/document.d.ts.map +1 -1
  2. package/dist/core/generationContext.d.ts +3 -2
  3. package/dist/core/generationContext.d.ts.map +1 -1
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +896 -274
  7. package/dist/index.js.map +1 -1
  8. package/dist/ir/compiler.d.ts.map +1 -1
  9. package/dist/ir/types.d.ts +7 -0
  10. package/dist/ir/types.d.ts.map +1 -1
  11. package/dist/plugin/createPresentationGenerator.d.ts.map +1 -1
  12. package/dist/plugin/index.d.ts +1 -1
  13. package/dist/plugin/index.d.ts.map +1 -1
  14. package/dist/plugin/types.d.ts +24 -0
  15. package/dist/plugin/types.d.ts.map +1 -1
  16. package/dist/quality/facts.d.ts +59 -2
  17. package/dist/quality/facts.d.ts.map +1 -1
  18. package/dist/quality/rules.d.ts +31 -6
  19. package/dist/quality/rules.d.ts.map +1 -1
  20. package/dist/renderers/office-open/chartParts.d.ts.map +1 -1
  21. package/dist/renderers/office-open/index.d.ts.map +1 -1
  22. package/dist/renderers/pptxgenjs/emit.d.ts +7 -1
  23. package/dist/renderers/pptxgenjs/emit.d.ts.map +1 -1
  24. package/dist/renderers/pptxgenjs/fills.d.ts +21 -1
  25. package/dist/renderers/pptxgenjs/fills.d.ts.map +1 -1
  26. package/dist/renderers/pptxgenjs/index.d.ts +2 -1
  27. package/dist/renderers/pptxgenjs/index.d.ts.map +1 -1
  28. package/dist/renderers/pptxgenjs/packaging.d.ts +3 -0
  29. package/dist/renderers/pptxgenjs/packaging.d.ts.map +1 -1
  30. package/dist/renderers/pptxgenjs/radialRaster.d.ts +49 -0
  31. package/dist/renderers/pptxgenjs/radialRaster.d.ts.map +1 -0
  32. package/dist/themes/defaults.d.ts +16 -1
  33. package/dist/themes/defaults.d.ts.map +1 -1
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/dist/types.d.ts +2 -0
  36. package/dist/types.d.ts.map +1 -1
  37. package/package.json +6 -4
package/dist/index.js CHANGED
@@ -121,6 +121,9 @@ var init_units = __esm({
121
121
  });
122
122
 
123
123
  // src/renderers/pptxgenjs/fills.ts
124
+ function buildImageFillXml() {
125
+ return `<a:blipFill rotWithShape="1"><a:blip r:embed="${IMAGE_FILL_RID}"/><a:stretch><a:fillRect/></a:stretch></a:blipFill>`;
126
+ }
124
127
  function colorXml(color2) {
125
128
  const alpha = color2.transparency !== void 0 ? `<a:alpha val="${Math.round((100 - color2.transparency) * PCT_UNIT)}"/>` : "";
126
129
  return `<a:srgbClr val="${color2.hex.toUpperCase()}">${alpha}</a:srgbClr>`;
@@ -142,18 +145,27 @@ function radialShadeXml(focus) {
142
145
  function buildPatternFillXml(preset, foreground, background2) {
143
146
  return `<a:pattFill prst="${preset}"><a:fgClr>${opaqueColorXml(foreground)}</a:fgClr><a:bgClr>${opaqueColorXml(background2)}</a:bgClr></a:pattFill>`;
144
147
  }
145
- function registerAdvancedFill(opts, fill2, elementPath, sink) {
148
+ function registerAdvancedFill(opts, fill2, elementPath, sink, raster) {
146
149
  const sentinel = fill2.kind === "gradient" ? fill2.gradient.stops[0].color : fill2.foreground;
147
150
  if (sink) {
148
- const xml = fill2.kind === "gradient" ? buildGradientFillXml(fill2.gradient) : buildPatternFillXml(fill2.preset, fill2.foreground, fill2.background);
149
151
  const objectName = `__jto_fill_${sink.length}__`;
150
- sink.push({ objectName, xml });
152
+ if (fill2.kind === "gradient" && fill2.gradient.type === "radial" && raster) {
153
+ const image = raster.rasterize(
154
+ fill2.gradient,
155
+ raster.widthEmu,
156
+ raster.heightEmu
157
+ );
158
+ sink.push({ objectName, xml: buildImageFillXml(), image });
159
+ } else {
160
+ const xml = fill2.kind === "gradient" ? buildGradientFillXml(fill2.gradient) : buildPatternFillXml(fill2.preset, fill2.foreground, fill2.background);
161
+ sink.push({ objectName, xml });
162
+ }
151
163
  opts.objectName = objectName;
152
164
  }
153
165
  void elementPath;
154
166
  opts.fill = { color: sentinel.hex };
155
167
  }
156
- var ANGLE_UNIT, PCT_UNIT, RADIAL_FOCUS_RECTS;
168
+ var ANGLE_UNIT, PCT_UNIT, RADIAL_FOCUS_RECTS, IMAGE_FILL_RID;
157
169
  var init_fills = __esm({
158
170
  "src/renderers/pptxgenjs/fills.ts"() {
159
171
  "use strict";
@@ -166,6 +178,7 @@ var init_fills = __esm({
166
178
  bottomLeft: { l: 0, t: 1e5, r: 1e5, b: 0 },
167
179
  bottomRight: { l: 1e5, t: 1e5, r: 0, b: 0 }
168
180
  };
181
+ IMAGE_FILL_RID = "__jto_fill_rid__";
169
182
  }
170
183
  });
171
184
 
@@ -243,7 +256,13 @@ function chartOpts(element) {
243
256
  face: "catAxisLabelFontFace",
244
257
  bold: "catAxisLabelFontBold",
245
258
  size: "catAxisLabelFontSize",
246
- color: "catAxisLabelColor"
259
+ color: "catAxisLabelColor",
260
+ titleFont: {
261
+ face: "catAxisTitleFontFace",
262
+ size: "catAxisTitleFontSize",
263
+ color: "catAxisTitleColor"
264
+ },
265
+ titleRotate: "catAxisTitleRotate"
247
266
  });
248
267
  applyValueAxis(opts, o.valueAxis);
249
268
  if (o.dataBorder) {
@@ -264,6 +283,10 @@ function applyAxis(opts, axis, keys) {
264
283
  if (axis.title !== void 0) {
265
284
  opts[keys.title] = axis.title;
266
285
  opts[keys.showTitle] = true;
286
+ applyLabelFont(opts, axis.titleFont, keys.titleFont);
287
+ if (axis.titleRotate !== void 0) {
288
+ opts[keys.titleRotate] = axis.titleRotate;
289
+ }
267
290
  }
268
291
  if (axis.hidden !== void 0) opts[keys.hidden] = axis.hidden;
269
292
  if (axis.labelRotate !== void 0) opts[keys.labelRotate] = axis.labelRotate;
@@ -282,7 +305,13 @@ function applyValueAxis(opts, axis) {
282
305
  face: "valAxisLabelFontFace",
283
306
  bold: "valAxisLabelFontBold",
284
307
  size: "valAxisLabelFontSize",
285
- color: "valAxisLabelColor"
308
+ color: "valAxisLabelColor",
309
+ titleFont: {
310
+ face: "valAxisTitleFontFace",
311
+ size: "valAxisTitleFontSize",
312
+ color: "valAxisTitleColor"
313
+ },
314
+ titleRotate: "valAxisTitleRotate"
286
315
  });
287
316
  assignDefined(opts, {
288
317
  valAxisMinVal: axis.minValue,
@@ -546,7 +575,7 @@ function shadowOpts(shadow) {
546
575
  function hyperlinkOpts(link) {
547
576
  return link.kind === "external" ? { url: link.url, tooltip: link.tooltip } : { slide: link.slideIndex, tooltip: link.tooltip };
548
577
  }
549
- function applyFill(opts, fill2, elementPath, ctx) {
578
+ function applyFill(opts, fill2, elementPath, ctx, transform) {
550
579
  switch (fill2.kind) {
551
580
  case "none":
552
581
  opts.fill = { type: "none" };
@@ -561,7 +590,17 @@ function applyFill(opts, fill2, elementPath, ctx) {
561
590
  }
562
591
  case "gradient":
563
592
  case "pattern":
564
- registerAdvancedFill(opts, fill2, elementPath, ctx.pendingFills);
593
+ registerAdvancedFill(
594
+ opts,
595
+ fill2,
596
+ elementPath,
597
+ ctx.pendingFills,
598
+ ctx.rasterizeRadial && transform ? {
599
+ rasterize: ctx.rasterizeRadial,
600
+ widthEmu: transform.widthEmu,
601
+ heightEmu: transform.heightEmu
602
+ } : void 0
603
+ );
565
604
  return;
566
605
  case "image":
567
606
  throw new Error(
@@ -668,7 +707,9 @@ function emitTextBox(slide, element, ctx) {
668
707
  ...bodyDefaultOpts(element.style),
669
708
  ...textBodyOpts(element.style)
670
709
  };
671
- if (element.fill) applyFill(opts, element.fill, element.path, ctx);
710
+ if (element.fill) {
711
+ applyFill(opts, element.fill, element.path, ctx, element.transform);
712
+ }
672
713
  if (element.shadow) opts.shadow = shadowOpts(element.shadow);
673
714
  if (element.hyperlink) opts.hyperlink = hyperlinkOpts(element.hyperlink);
674
715
  if (element.altText) opts.altText = element.altText;
@@ -703,7 +744,9 @@ function emitShape(slide, element, ctx) {
703
744
  return;
704
745
  }
705
746
  const opts = { ...transformOpts(element.transform) };
706
- if (element.fill) applyFill(opts, element.fill, element.path, ctx);
747
+ if (element.fill) {
748
+ applyFill(opts, element.fill, element.path, ctx, element.transform);
749
+ }
707
750
  if (element.line) opts.line = lineOpts(element.line);
708
751
  if (element.shadow) opts.shadow = shadowOpts(element.shadow);
709
752
  if (element.cornerRadius !== void 0)
@@ -1059,7 +1102,7 @@ var init_svgRasterFallback = __esm({
1059
1102
  });
1060
1103
 
1061
1104
  // src/renderers/pptxgenjs/packaging.ts
1062
- function applyPendingFills(xml, pendingFills) {
1105
+ function applyPendingFills(xml, pendingFills, spliced = []) {
1063
1106
  let out = xml;
1064
1107
  for (const [index, fill2] of pendingFills.entries()) {
1065
1108
  const marker = `name="${fill2.objectName}"`;
@@ -1070,15 +1113,52 @@ function applyPendingFills(xml, pendingFills) {
1070
1113
  const solidEndTag = "</a:solidFill>";
1071
1114
  const solidEnd = out.indexOf(solidEndTag, solidStart);
1072
1115
  if (solidStart !== -1 && solidEnd !== -1 && spEnd !== -1 && solidStart < spEnd) {
1073
- out = out.slice(0, solidStart) + fill2.xml + out.slice(solidEnd + solidEndTag.length);
1116
+ let fillXml = fill2.xml;
1117
+ if (fill2.image) {
1118
+ const token = `${IMAGE_FILL_RID}${index}`;
1119
+ fillXml = fillXml.replace(IMAGE_FILL_RID, token);
1120
+ spliced.push({ token, image: fill2.image });
1121
+ }
1122
+ out = out.slice(0, solidStart) + fillXml + out.slice(solidEnd + solidEndTag.length);
1074
1123
  }
1075
1124
  out = out.slice(0, markerIdx) + `name="Fill ${index + 1}"` + out.slice(markerIdx + marker.length);
1076
1125
  }
1077
1126
  return out;
1078
1127
  }
1128
+ async function linkImageFills(zip, slidePath, xml, images, media) {
1129
+ if (images.length === 0) return xml;
1130
+ const relsPath = slidePath.replace(
1131
+ /^ppt\/slides\/(slide\d+\.xml)$/,
1132
+ "ppt/slides/_rels/$1.rels"
1133
+ );
1134
+ let rels = await zip.file(relsPath)?.async("string") ?? '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>';
1135
+ let nextId = Math.max(
1136
+ 0,
1137
+ ...[...rels.matchAll(/\bId="rId(\d+)"/g)].map((match) => Number(match[1]))
1138
+ ) + 1;
1139
+ let out = xml;
1140
+ for (const { token, image } of images) {
1141
+ let part = media.get(image);
1142
+ if (!part) {
1143
+ part = `ppt/media/jto-gradient-${media.size + 1}.png`;
1144
+ media.set(image, part);
1145
+ zip.file(part, image);
1146
+ }
1147
+ const rId = `rId${nextId}`;
1148
+ nextId += 1;
1149
+ rels = rels.replace(
1150
+ "</Relationships>",
1151
+ `<Relationship Id="${rId}" Type="${IMAGE_REL_TYPE}" Target="../media/${part.slice("ppt/media/".length)}"/></Relationships>`
1152
+ );
1153
+ out = out.replace(`r:embed="${token}"`, `r:embed="${rId}"`);
1154
+ }
1155
+ zip.file(relsPath, rels);
1156
+ return out;
1157
+ }
1079
1158
  async function packagePptxGenJsBuffer(buffer, options = {}) {
1080
1159
  const zip = await readPackage(buffer);
1081
1160
  let changed = false;
1161
+ const media = /* @__PURE__ */ new Map();
1082
1162
  for (const [path4, entry] of Object.entries(zip.files)) {
1083
1163
  if (!SLIDE_PART.test(path4)) continue;
1084
1164
  let xml = await entry.async("string");
@@ -1088,7 +1168,9 @@ async function packagePptxGenJsBuffer(buffer, options = {}) {
1088
1168
  fileChanged = true;
1089
1169
  }
1090
1170
  if (options.pendingFills?.length) {
1091
- const withFills = applyPendingFills(xml, options.pendingFills);
1171
+ const spliced = [];
1172
+ let withFills = applyPendingFills(xml, options.pendingFills, spliced);
1173
+ withFills = await linkImageFills(zip, path4, withFills, spliced, media);
1092
1174
  if (withFills !== xml) {
1093
1175
  xml = withFills;
1094
1176
  fileChanged = true;
@@ -1107,15 +1189,190 @@ async function packagePptxGenJsBuffer(buffer, options = {}) {
1107
1189
  if (!changed) return buffer;
1108
1190
  return writePackage(zip);
1109
1191
  }
1110
- var MEDIUM_STYLE_2_ACCENT_1, NO_STYLE_NO_GRID, SLIDE_PART;
1192
+ var MEDIUM_STYLE_2_ACCENT_1, NO_STYLE_NO_GRID, SLIDE_PART, IMAGE_REL_TYPE;
1111
1193
  var init_packaging = __esm({
1112
1194
  "src/renderers/pptxgenjs/packaging.ts"() {
1113
1195
  "use strict";
1114
1196
  init_finalizePackage();
1115
1197
  init_svgRasterFallback();
1198
+ init_fills();
1116
1199
  MEDIUM_STYLE_2_ACCENT_1 = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}";
1117
1200
  NO_STYLE_NO_GRID = "{2D5ABB26-0587-4C30-8999-92F81FD0307C}";
1118
1201
  SLIDE_PART = /^ppt\/slides\/slide\d+\.xml$/;
1202
+ IMAGE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
1203
+ }
1204
+ });
1205
+
1206
+ // src/renderers/pptxgenjs/radialRaster.ts
1207
+ import pako from "pako";
1208
+ function radialRasterSize(widthEmu, heightEmu) {
1209
+ const w = Math.max(widthEmu, 1);
1210
+ const h = Math.max(heightEmu, 1);
1211
+ const longInches = Math.max(w, h) / EMU_PER_INCH3;
1212
+ const longPx = Math.min(
1213
+ MAX_EDGE_PX2,
1214
+ Math.max(MIN_EDGE_PX2, Math.round(longInches * PX_PER_INCH))
1215
+ );
1216
+ const scale = longPx / Math.max(w, h);
1217
+ return {
1218
+ width: Math.max(1, Math.round(w * scale)),
1219
+ height: Math.max(1, Math.round(h * scale))
1220
+ };
1221
+ }
1222
+ function parseStops(gradient) {
1223
+ return gradient.stops.map((stop, index) => ({ stop, index })).sort((a, b) => a.stop.position - b.stop.position || a.index - b.index).map(({ stop }) => {
1224
+ const hex = parseInt(stop.color.hex, 16);
1225
+ const alpha = stop.color.transparency !== void 0 ? 255 * (1 - Math.min(100, Math.max(0, stop.color.transparency)) / 100) : 255;
1226
+ return {
1227
+ at: Math.min(100, Math.max(0, stop.position)) / 100,
1228
+ rgba: [hex >> 16 & 255, hex >> 8 & 255, hex & 255, alpha]
1229
+ };
1230
+ });
1231
+ }
1232
+ function paintRadialGradient(gradient, width, height) {
1233
+ const stops = parseStops(gradient);
1234
+ const channels = stops.every((stop) => stop.rgba[3] === 255) ? 3 : 4;
1235
+ const [fx, fy] = FOCUS_POINTS[gradient.focus] ?? FOCUS_POINTS.center;
1236
+ const cx = fx * width;
1237
+ const cy = fy * height;
1238
+ const radius = Math.sqrt(width * width + height * height) / 2;
1239
+ const pixels = new Uint8Array(width * height * channels);
1240
+ for (let y = 0; y < height; y += 1) {
1241
+ const dy = y + 0.5 - cy;
1242
+ for (let x = 0; x < width; x += 1) {
1243
+ const dx = x + 0.5 - cx;
1244
+ const t = Math.min(1, Math.sqrt(dx * dx + dy * dy) / radius);
1245
+ let next = 0;
1246
+ while (next < stops.length && stops[next].at < t) next += 1;
1247
+ const offset = (y * width + x) * channels;
1248
+ if (next === 0 || next === stops.length) {
1249
+ const only = stops[next === 0 ? 0 : stops.length - 1].rgba;
1250
+ for (let c = 0; c < channels; c += 1) pixels[offset + c] = only[c];
1251
+ continue;
1252
+ }
1253
+ const from = stops[next - 1];
1254
+ const to = stops[next];
1255
+ const u = (t - from.at) / (to.at - from.at);
1256
+ for (let c = 0; c < channels; c += 1) {
1257
+ pixels[offset + c] = Math.round(
1258
+ from.rgba[c] + (to.rgba[c] - from.rgba[c]) * u
1259
+ );
1260
+ }
1261
+ }
1262
+ }
1263
+ return { pixels, channels };
1264
+ }
1265
+ function crc32(bytes) {
1266
+ let c = 4294967295;
1267
+ for (let i = 0; i < bytes.length; i += 1) {
1268
+ c = CRC_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
1269
+ }
1270
+ return (c ^ 4294967295) >>> 0;
1271
+ }
1272
+ function chunk(type, data) {
1273
+ const out = new Uint8Array(12 + data.length);
1274
+ const view = new DataView(out.buffer);
1275
+ view.setUint32(0, data.length);
1276
+ for (let i = 0; i < 4; i += 1) out[4 + i] = type.charCodeAt(i);
1277
+ out.set(data, 8);
1278
+ view.setUint32(8 + data.length, crc32(out.subarray(4, 8 + data.length)));
1279
+ return out;
1280
+ }
1281
+ function paethFiltered(pixels, width, height, channels) {
1282
+ const stride = width * channels;
1283
+ const out = new Uint8Array(height * (stride + 1));
1284
+ for (let y = 0; y < height; y += 1) {
1285
+ const row = y * stride;
1286
+ const up = row - stride;
1287
+ const at = y * (stride + 1);
1288
+ out[at] = 4;
1289
+ for (let i = 0; i < stride; i += 1) {
1290
+ const a = i >= channels ? pixels[row + i - channels] : 0;
1291
+ const b = y > 0 ? pixels[up + i] : 0;
1292
+ const c = y > 0 && i >= channels ? pixels[up + i - channels] : 0;
1293
+ const pa = Math.abs(b - c);
1294
+ const pb = Math.abs(a - c);
1295
+ const pc = Math.abs(a + b - 2 * c);
1296
+ const predictor = pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
1297
+ out[at + 1 + i] = pixels[row + i] - predictor & 255;
1298
+ }
1299
+ }
1300
+ return out;
1301
+ }
1302
+ function encodePng(pixels, width, height, channels) {
1303
+ const header = new Uint8Array(13);
1304
+ const view = new DataView(header.buffer);
1305
+ view.setUint32(0, width);
1306
+ view.setUint32(4, height);
1307
+ header[8] = 8;
1308
+ header[9] = channels === 4 ? 6 : 2;
1309
+ const parts = [
1310
+ new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
1311
+ chunk("IHDR", header),
1312
+ chunk(
1313
+ "IDAT",
1314
+ pako.deflate(paethFiltered(pixels, width, height, channels), {
1315
+ level: 6
1316
+ })
1317
+ ),
1318
+ chunk("IEND", new Uint8Array(0))
1319
+ ];
1320
+ const png = new Uint8Array(parts.reduce((sum, p) => sum + p.length, 0));
1321
+ let offset = 0;
1322
+ for (const part of parts) {
1323
+ png.set(part, offset);
1324
+ offset += part.length;
1325
+ }
1326
+ return png;
1327
+ }
1328
+ function pngDataUri(png) {
1329
+ return `data:image/png;base64,${Buffer.from(png).toString("base64")}`;
1330
+ }
1331
+ var MAX_EDGE_PX2, PX_PER_INCH, MIN_EDGE_PX2, EMU_PER_INCH3, CACHE_LIMIT, FOCUS_POINTS, CRC_TABLE, cache, rasterizeRadialGradient;
1332
+ var init_radialRaster = __esm({
1333
+ "src/renderers/pptxgenjs/radialRaster.ts"() {
1334
+ "use strict";
1335
+ MAX_EDGE_PX2 = 1600;
1336
+ PX_PER_INCH = 160;
1337
+ MIN_EDGE_PX2 = 32;
1338
+ EMU_PER_INCH3 = 914400;
1339
+ CACHE_LIMIT = 32;
1340
+ FOCUS_POINTS = {
1341
+ center: [0.5, 0.5],
1342
+ topLeft: [0, 0],
1343
+ topRight: [1, 0],
1344
+ bottomLeft: [0, 1],
1345
+ bottomRight: [1, 1]
1346
+ };
1347
+ CRC_TABLE = (() => {
1348
+ const table = new Uint32Array(256);
1349
+ for (let n = 0; n < 256; n += 1) {
1350
+ let c = n;
1351
+ for (let k = 0; k < 8; k += 1) {
1352
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
1353
+ }
1354
+ table[n] = c >>> 0;
1355
+ }
1356
+ return table;
1357
+ })();
1358
+ cache = /* @__PURE__ */ new Map();
1359
+ rasterizeRadialGradient = (gradient, widthEmu, heightEmu) => {
1360
+ const { width, height } = radialRasterSize(widthEmu, heightEmu);
1361
+ const key = JSON.stringify([gradient.focus, gradient.stops, width, height]);
1362
+ const hit = cache.get(key);
1363
+ if (hit) {
1364
+ cache.delete(key);
1365
+ cache.set(key, hit);
1366
+ return hit;
1367
+ }
1368
+ const { pixels, channels } = paintRadialGradient(gradient, width, height);
1369
+ const png = encodePng(pixels, width, height, channels);
1370
+ cache.set(key, png);
1371
+ if (cache.size > CACHE_LIMIT) {
1372
+ cache.delete(cache.keys().next().value);
1373
+ }
1374
+ return png;
1375
+ };
1119
1376
  }
1120
1377
  });
1121
1378
 
@@ -1126,6 +1383,9 @@ __export(pptxgenjs_exports, {
1126
1383
  buildPresentation: () => buildPresentation,
1127
1384
  createPptxGenJsRenderer: () => createPptxGenJsRenderer
1128
1385
  });
1386
+ import { readFile } from "fs/promises";
1387
+ import { imageIntegrityDefect as imageIntegrityDefect2 } from "@json-to-office/shared/images/node";
1388
+ import { assetUnreadableError as assetUnreadableError2 } from "@json-to-office/shared/rendering";
1129
1389
  import PptxGenJS from "pptxgenjs";
1130
1390
  function createPptxGenJsRenderer() {
1131
1391
  return {
@@ -1133,6 +1393,7 @@ function createPptxGenJsRenderer() {
1133
1393
  format: "pptx",
1134
1394
  capabilities: PPTXGENJS_CAPABILITIES,
1135
1395
  async render(ir, options) {
1396
+ await assertFileImagesDecode(ir.resources);
1136
1397
  const pendingFills = [];
1137
1398
  const pptx = buildPresentation(ir, pendingFills, options?.warnings);
1138
1399
  const raw = await pptx.write({
@@ -1148,7 +1409,21 @@ function createPptxGenJsRenderer() {
1148
1409
  }
1149
1410
  };
1150
1411
  }
1151
- function buildPresentation(ir, pendingFills, warnings) {
1412
+ async function assertFileImagesDecode(resources) {
1413
+ await Promise.all(
1414
+ resources.map(async ({ origin }) => {
1415
+ if (origin.kind !== "file") return;
1416
+ let defect;
1417
+ try {
1418
+ defect = imageIntegrityDefect2(await readFile(origin.path));
1419
+ } catch (error) {
1420
+ throw assetUnreadableError2(origin.path, error);
1421
+ }
1422
+ if (defect) throw assetUnreadableError2(origin.path, new Error(defect));
1423
+ })
1424
+ );
1425
+ }
1426
+ function buildPresentation(ir, pendingFills, warnings, rasterizeRadial = rasterizeRadialGradient) {
1152
1427
  const pptx = new PptxGenJS();
1153
1428
  if (ir.metadata.title) pptx.title = ir.metadata.title;
1154
1429
  if (ir.metadata.author) pptx.author = ir.metadata.author;
@@ -1168,9 +1443,15 @@ function buildPresentation(ir, pendingFills, warnings) {
1168
1443
  const resources = new Map(
1169
1444
  ir.resources.map((resource) => [resource.id, resource])
1170
1445
  );
1171
- const ctx = { pptx, resources, pendingFills, warnings };
1446
+ const ctx = {
1447
+ pptx,
1448
+ resources,
1449
+ pendingFills,
1450
+ warnings,
1451
+ rasterizeRadial
1452
+ };
1172
1453
  for (const slide of ir.slides) {
1173
- emitSlide(pptx, slide, ctx);
1454
+ emitSlide(pptx, slide, ir.size, ctx);
1174
1455
  }
1175
1456
  return pptx;
1176
1457
  }
@@ -1181,12 +1462,36 @@ function backgroundProps(background2, resources) {
1181
1462
  if (!resource) return void 0;
1182
1463
  return imageSourceOpts(resource);
1183
1464
  }
1184
- function emitSlide(pptx, slideIr, ctx) {
1465
+ function radialBackdrop(element, size) {
1466
+ if (element?.kind !== "shape") return void 0;
1467
+ const { transform, fill: fill2 } = element;
1468
+ if (fill2?.kind !== "gradient" || fill2.gradient.type !== "radial") {
1469
+ return void 0;
1470
+ }
1471
+ const bare = element.geometry === "rect" && !element.line && !element.shadow && !element.hyperlink && !element.altText && !(element.runs && element.runs.length > 0) && !transform.rotationDegrees && !transform.flipHorizontal && !transform.flipVertical;
1472
+ const fullBleed = transform.xEmu === 0 && transform.yEmu === 0 && transform.widthEmu === size.widthEmu && transform.heightEmu === size.heightEmu;
1473
+ return bare && fullBleed ? fill2.gradient : void 0;
1474
+ }
1475
+ function emitSlide(pptx, slideIr, size, ctx) {
1185
1476
  const slide = pptx.addSlide();
1477
+ let elements = slideIr.elements;
1186
1478
  const background2 = backgroundProps(slideIr.background, ctx.resources);
1187
- if (background2) slide.background = background2;
1479
+ if (background2) {
1480
+ slide.background = background2;
1481
+ } else if (ctx.rasterizeRadial) {
1482
+ const gradient = radialBackdrop(elements[0], size);
1483
+ if (gradient) {
1484
+ const png = ctx.rasterizeRadial(
1485
+ gradient,
1486
+ elements[0].transform.widthEmu,
1487
+ elements[0].transform.heightEmu
1488
+ );
1489
+ slide.background = { data: pngDataUri(png) };
1490
+ elements = elements.slice(1);
1491
+ }
1492
+ }
1188
1493
  if (slideIr.hidden) slide.hidden = true;
1189
- for (const element of slideIr.elements) {
1494
+ for (const element of elements) {
1190
1495
  emitElement(slide, element, ctx);
1191
1496
  }
1192
1497
  if (slideIr.notes) slide.addNotes(slideIr.notes);
@@ -1198,6 +1503,7 @@ var init_pptxgenjs = __esm({
1198
1503
  init_units();
1199
1504
  init_emit();
1200
1505
  init_packaging();
1506
+ init_radialRaster();
1201
1507
  PPTXGENJS_RENDERER_ID = "pptxgenjs";
1202
1508
  PPTXGENJS_CAPABILITIES = /* @__PURE__ */ new Set([
1203
1509
  "rich-text",
@@ -1300,6 +1606,8 @@ function axisEdits(axis) {
1300
1606
  const value = axis;
1301
1607
  return {
1302
1608
  ...axis.title !== void 0 ? { title: axis.title } : {},
1609
+ ...axis.title !== void 0 && textStyle(axis.titleFont) ? { titleFont: textStyle(axis.titleFont) } : {},
1610
+ ...axis.title !== void 0 && axis.titleRotate !== void 0 ? { titleRotation: axis.titleRotate } : {},
1303
1611
  ...axis.hidden !== void 0 ? { hidden: axis.hidden } : {},
1304
1612
  ...axis.showLine !== void 0 ? { lineVisible: axis.showLine } : {},
1305
1613
  ...axis.labelRotate !== void 0 ? { labelRotation: axis.labelRotate } : {},
@@ -1894,8 +2202,9 @@ __export(office_open_exports, {
1894
2202
  buildPresentationOptions: () => buildPresentationOptions,
1895
2203
  createOfficeOpenPptxRenderer: () => createOfficeOpenPptxRenderer
1896
2204
  });
1897
- import { readFile } from "fs/promises";
1898
- import { assetUnreadableError } from "@json-to-office/shared/rendering";
2205
+ import { readFile as readFile2 } from "fs/promises";
2206
+ import { imageIntegrityDefect as imageIntegrityDefect3 } from "@json-to-office/shared/images/node";
2207
+ import { assetUnreadableError as assetUnreadableError3 } from "@json-to-office/shared/rendering";
1899
2208
  async function createOfficeOpenPptxRenderer() {
1900
2209
  const backend = await import(
1901
2210
  /* @vite-ignore */
@@ -2004,16 +2313,21 @@ async function resourceBytes(origin) {
2004
2313
  if (origin.kind === "inline") return origin.bytes;
2005
2314
  const location = origin.kind === "file" ? origin.path : origin.url;
2006
2315
  try {
2316
+ let bytes;
2007
2317
  if (origin.kind === "file") {
2008
- return new Uint8Array(await readFile(origin.path));
2009
- }
2010
- const response = await fetch(origin.url);
2011
- if (!response.ok) {
2012
- throw new Error(`HTTP ${response.status} ${response.statusText}`);
2318
+ bytes = new Uint8Array(await readFile2(origin.path));
2319
+ } else {
2320
+ const response = await fetch(origin.url);
2321
+ if (!response.ok) {
2322
+ throw new Error(`HTTP ${response.status} ${response.statusText}`);
2323
+ }
2324
+ bytes = new Uint8Array(await response.arrayBuffer());
2013
2325
  }
2014
- return new Uint8Array(await response.arrayBuffer());
2326
+ const defect = imageIntegrityDefect3(bytes);
2327
+ if (defect) throw new Error(defect);
2328
+ return bytes;
2015
2329
  } catch (error) {
2016
- throw assetUnreadableError(location, error);
2330
+ throw assetUnreadableError3(location, error);
2017
2331
  }
2018
2332
  }
2019
2333
  var OFFICE_OPEN_PPTX_RENDERER_ID, OFFICE_OPEN_PPTX, OFFICE_OPEN_CAPABILITIES, SCHEME_SLOTS;
@@ -2101,7 +2415,9 @@ function resolveFromBaseDir(filePath) {
2101
2415
  }
2102
2416
 
2103
2417
  // src/ir/compiler.ts
2418
+ import { imageIntegrityDefect } from "@json-to-office/shared/images/node";
2104
2419
  import {
2420
+ assetUnreadableError,
2105
2421
  FeatureRequirementCollector
2106
2422
  } from "@json-to-office/shared/rendering";
2107
2423
  import { PATTERN_FILL_PRESETS } from "@json-to-office/shared-pptx";
@@ -3097,6 +3413,8 @@ function compileImage(component, scope) {
3097
3413
  function internImageSource(source, path4, ctx) {
3098
3414
  const inline = parseDataUri(source);
3099
3415
  if (inline) {
3416
+ const defect = imageIntegrityDefect(inline.bytes);
3417
+ if (defect) throw assetUnreadableError(source, new Error(defect));
3100
3418
  return ctx.resources.intern({
3101
3419
  kind: "inline",
3102
3420
  bytes: inline.bytes,
@@ -3456,6 +3774,16 @@ function compileChartLabelFont(ctx, slot, face, weight, hasBoldToggle, extra = {
3456
3774
  }
3457
3775
  return font;
3458
3776
  }
3777
+ var DEFAULT_AXIS_TITLE_POINTS = 12;
3778
+ function axisTitleFont(ctx, labelFont, resolved, authored) {
3779
+ const fontFamily = authored.face ?? labelFont.fontFamily ?? ctx.theme.fonts?.body;
3780
+ return {
3781
+ ...fontFamily !== void 0 ? { fontFamily } : {},
3782
+ fontSize: authored.size ?? labelFont.fontSize ?? DEFAULT_AXIS_TITLE_POINTS,
3783
+ bold: false,
3784
+ color: authored.color !== void 0 ? resolved(authored.color) : labelFont.color ?? resolved(void 0)
3785
+ };
3786
+ }
3459
3787
  function compileChartOptions(props, ctx) {
3460
3788
  const colorSources = props.chartColors ?? definedChartColorTokens(ctx.theme);
3461
3789
  const colors = colorSources.map(
@@ -3463,6 +3791,28 @@ function compileChartOptions(props, ctx) {
3463
3791
  );
3464
3792
  const themeTextColor = irColor(resolveColor("text", ctx.theme, ctx.warnings));
3465
3793
  const resolved = (value) => value ? irColor(resolveColor(value, ctx.theme, ctx.warnings)) : themeTextColor;
3794
+ const catLabelFont = compileChartLabelFont(
3795
+ ctx,
3796
+ "catAxisLabelFontFace",
3797
+ props.catAxisLabelFontFace,
3798
+ props.catAxisLabelFontWeight,
3799
+ true,
3800
+ {
3801
+ ...props.catAxisLabelFontSize !== void 0 ? { fontSize: props.catAxisLabelFontSize } : {},
3802
+ color: resolved(props.catAxisLabelColor)
3803
+ }
3804
+ );
3805
+ const valLabelFont = compileChartLabelFont(
3806
+ ctx,
3807
+ "valAxisLabelFontFace",
3808
+ props.valAxisLabelFontFace,
3809
+ props.valAxisLabelFontWeight,
3810
+ true,
3811
+ {
3812
+ ...props.valAxisLabelFontSize !== void 0 ? { fontSize: props.valAxisLabelFontSize } : {},
3813
+ color: resolved(props.valAxisLabelColor)
3814
+ }
3815
+ );
3466
3816
  return {
3467
3817
  colors,
3468
3818
  ...pick(props, {
@@ -3499,24 +3849,26 @@ function compileChartOptions(props, ctx) {
3499
3849
  ),
3500
3850
  categoryAxis: {
3501
3851
  ...props.catAxisTitle !== void 0 ? { title: props.catAxisTitle } : {},
3852
+ titleFont: axisTitleFont(ctx, catLabelFont, resolved, {
3853
+ face: props.catAxisTitleFontFace,
3854
+ size: props.catAxisTitleFontSize,
3855
+ color: props.catAxisTitleColor
3856
+ }),
3857
+ ...props.catAxisTitleRotate !== void 0 ? { titleRotate: props.catAxisTitleRotate } : {},
3502
3858
  ...props.catAxisHidden !== void 0 ? { hidden: props.catAxisHidden } : {},
3503
3859
  ...props.catAxisLabelRotate !== void 0 ? { labelRotate: props.catAxisLabelRotate } : {},
3504
3860
  ...props.catAxisLineShow !== void 0 ? { showLine: props.catAxisLineShow } : {},
3505
3861
  ...props.catGridLine !== void 0 ? { gridLine: compileGridLine(props.catGridLine, ctx) } : {},
3506
- labelFont: compileChartLabelFont(
3507
- ctx,
3508
- "catAxisLabelFontFace",
3509
- props.catAxisLabelFontFace,
3510
- props.catAxisLabelFontWeight,
3511
- true,
3512
- {
3513
- ...props.catAxisLabelFontSize !== void 0 ? { fontSize: props.catAxisLabelFontSize } : {},
3514
- color: resolved(props.catAxisLabelColor)
3515
- }
3516
- )
3862
+ labelFont: catLabelFont
3517
3863
  },
3518
3864
  valueAxis: {
3519
3865
  ...props.valAxisTitle !== void 0 ? { title: props.valAxisTitle } : {},
3866
+ titleFont: axisTitleFont(ctx, valLabelFont, resolved, {
3867
+ face: props.valAxisTitleFontFace,
3868
+ size: props.valAxisTitleFontSize,
3869
+ color: props.valAxisTitleColor
3870
+ }),
3871
+ ...props.valAxisTitleRotate !== void 0 ? { titleRotate: props.valAxisTitleRotate } : {},
3520
3872
  ...props.valAxisHidden !== void 0 ? { hidden: props.valAxisHidden } : {},
3521
3873
  ...props.valAxisMinVal !== void 0 ? { minValue: props.valAxisMinVal } : {},
3522
3874
  ...props.valAxisMaxVal !== void 0 ? { maxValue: props.valAxisMaxVal } : {},
@@ -3524,17 +3876,7 @@ function compileChartOptions(props, ctx) {
3524
3876
  ...props.valAxisLabelFormatCode !== void 0 ? { labelFormatCode: props.valAxisLabelFormatCode } : {},
3525
3877
  ...props.valAxisLineShow !== void 0 ? { showLine: props.valAxisLineShow } : {},
3526
3878
  ...props.valGridLine !== void 0 ? { gridLine: compileGridLine(props.valGridLine, ctx) } : {},
3527
- labelFont: compileChartLabelFont(
3528
- ctx,
3529
- "valAxisLabelFontFace",
3530
- props.valAxisLabelFontFace,
3531
- props.valAxisLabelFontWeight,
3532
- true,
3533
- {
3534
- ...props.valAxisLabelFontSize !== void 0 ? { fontSize: props.valAxisLabelFontSize } : {},
3535
- color: resolved(props.valAxisLabelColor)
3536
- }
3537
- )
3879
+ labelFont: valLabelFont
3538
3880
  },
3539
3881
  ...props.dataBorder !== void 0 ? {
3540
3882
  dataBorder: {
@@ -3949,7 +4291,7 @@ function isNodeEnvironment() {
3949
4291
  }
3950
4292
 
3951
4293
  // src/core/expandHighcharts.ts
3952
- var PX_PER_INCH = 96;
4294
+ var PX_PER_INCH2 = 96;
3953
4295
  var DEFAULT_CHART_WIDTH_PX = 960;
3954
4296
  var DEFAULT_EXPORT_SERVER_URL = "http://localhost:7801";
3955
4297
  async function expandHighchartsComponents(presentation, services, warnings, chartFonts) {
@@ -4021,8 +4363,8 @@ async function expandOne(component, path4, scope) {
4021
4363
  base64: chart.dataUri,
4022
4364
  x: props.x ?? 0,
4023
4365
  y: props.y ?? 0,
4024
- w: props.w ?? chart.widthPx / PX_PER_INCH,
4025
- h: props.h ?? chart.heightPx / PX_PER_INCH
4366
+ w: props.w ?? chart.widthPx / PX_PER_INCH2,
4367
+ h: props.h ?? chart.heightPx / PX_PER_INCH2
4026
4368
  }
4027
4369
  };
4028
4370
  }
@@ -4039,7 +4381,7 @@ function assertExportServerAllowed(serverUrl, services, warnings) {
4039
4381
  message: notice
4040
4382
  });
4041
4383
  }
4042
- async function renderChart(config, services, warnings, cache) {
4384
+ async function renderChart(config, services, warnings, cache2) {
4043
4385
  if (!isNodeEnvironment()) {
4044
4386
  throw new Error(
4045
4387
  "Highcharts export server requires a Node.js environment. Chart generation is not available in browser environments."
@@ -4057,7 +4399,7 @@ async function renderChart(config, services, warnings, cache) {
4057
4399
  ...config.resources ? { resources: config.resources } : {}
4058
4400
  };
4059
4401
  return sendChartRequest({
4060
- cache,
4402
+ cache: cache2,
4061
4403
  serverUrl,
4062
4404
  concurrency: services?.concurrency,
4063
4405
  requestBody,
@@ -4104,7 +4446,7 @@ function placedWidthPt(props, slideWidth) {
4104
4446
  return parseFloat(w) / 100 * slideWidth * 72;
4105
4447
  }
4106
4448
  if (w === void 0 && props.grid === void 0) {
4107
- return (props.options.chart?.width ?? DEFAULT_CHART_WIDTH_PX) / PX_PER_INCH * 72;
4449
+ return (props.options.chart?.width ?? DEFAULT_CHART_WIDTH_PX) / PX_PER_INCH2 * 72;
4108
4450
  }
4109
4451
  return void 0;
4110
4452
  }
@@ -5163,36 +5505,8 @@ var DEFAULT_STYLES = {
5163
5505
  body: { fontSize: 14 },
5164
5506
  caption: { fontSize: 10, italic: true, fontColor: "text2" }
5165
5507
  };
5166
- var DEFAULT_PPTX_THEME = {
5167
- name: "default",
5168
- displayName: "Office Default",
5169
- description: "The stock Office look: blue primary, orange and green accents, Arial throughout, centred titles",
5170
- whenToUse: "A deck that has to match a plain PowerPoint look; pick a designed theme for anything a client will see.",
5171
- colors: {
5172
- primary: "#4472C4",
5173
- secondary: "#ED7D31",
5174
- accent: "#70AD47",
5175
- background: "#FFFFFF",
5176
- text: "#333333",
5177
- text2: "#44546A",
5178
- background2: "#E7E6E6",
5179
- accent4: "#FFC000",
5180
- accent5: "#5B9BD5",
5181
- accent6: "#70AD47"
5182
- },
5183
- fonts: {
5184
- heading: "Arial",
5185
- body: "Arial"
5186
- },
5187
- defaults: {
5188
- fontSize: 18,
5189
- fontColor: "#333333"
5190
- },
5191
- styles: DEFAULT_STYLES,
5192
- componentDefaults: { table: DEFAULT_TABLE }
5193
- };
5508
+ var DEFAULT_PPTX_THEME = CONSULTING_PPTX_THEME;
5194
5509
  var PPTX_THEMES = {
5195
- default: DEFAULT_PPTX_THEME,
5196
5510
  consulting: CONSULTING_PPTX_THEME,
5197
5511
  vermilion: VERMILION_PPTX_THEME,
5198
5512
  devportal: DEVPORTAL_PPTX_THEME,
@@ -5332,7 +5646,7 @@ function resolveThemeContext(documentIn, options = {}) {
5332
5646
  inlineTheme = document.props.theme;
5333
5647
  }
5334
5648
  const authoredThemeName = typeof document.props.theme === "string" ? document.props.theme : void 0;
5335
- const baseThemeName = inlineTheme ? inlineTheme.name || "inline-theme" : authoredThemeName ?? defaultThemeName ?? "default";
5649
+ const baseThemeName = inlineTheme ? inlineTheme.name || "inline-theme" : authoredThemeName ?? defaultThemeName ?? "consulting";
5336
5650
  let theme = inlineTheme ?? (resolveNamedTheme ? resolveNamedTheme(baseThemeName, authoredThemeName !== void 0) : customThemes?.[baseThemeName] ?? getPptxTheme(baseThemeName));
5337
5651
  theme = resolvePptxDesignSystem(
5338
5652
  theme,
@@ -5913,10 +6227,22 @@ async function expandPptxBlocksWithPlugins(document, theme, plugins, render, pre
5913
6227
  preserve
5914
6228
  });
5915
6229
  const finished = finishPptxBlocks(composed.standard, evaluator, effects2);
5916
- return { ...finished, preserved: composed.preserved };
6230
+ return {
6231
+ ...finished,
6232
+ preserved: composed.preserved,
6233
+ pluginOutputs: evaluator.pluginOutputs
6234
+ };
5917
6235
  }
5918
6236
 
5919
6237
  // src/quality/facts.ts
6238
+ function textInsetsPt(props, componentName) {
6239
+ const margin = props.margin;
6240
+ if (typeof margin === "number" && Number.isFinite(margin))
6241
+ return [margin, margin, margin, margin];
6242
+ if (Array.isArray(margin) && margin.length === 4 && margin.every((side) => typeof side === "number" && Number.isFinite(side)))
6243
+ return margin;
6244
+ return componentName === "shape" ? [3.6, 7.2, 3.6, 7.2] : [0, 0, 0, 0];
6245
+ }
5920
6246
  function asRecord(value) {
5921
6247
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
5922
6248
  }
@@ -6054,6 +6380,7 @@ function fillColorHexes(fill2, theme) {
6054
6380
  return [...new Set(found)];
6055
6381
  }
6056
6382
  var FOCUS_CORNERS = {
6383
+ center: [0.5, 0.5],
6057
6384
  topLeft: [0, 0],
6058
6385
  topRight: [1, 0],
6059
6386
  bottomLeft: [0, 1],
@@ -6069,7 +6396,7 @@ function lerpHex(a, b, t) {
6069
6396
  }
6070
6397
  function gradientPosition(gradient, fx, fy, widthUnits, heightUnits) {
6071
6398
  if (gradient.type === "radial") {
6072
- const focus = FOCUS_CORNERS[typeof gradient.focus === "string" ? gradient.focus : "topLeft"] ?? FOCUS_CORNERS.topLeft;
6399
+ const focus = FOCUS_CORNERS[typeof gradient.focus === "string" ? gradient.focus : "center"] ?? FOCUS_CORNERS.center;
6073
6400
  const radius = Math.hypot(widthUnits, heightUnits) / 2;
6074
6401
  if (radius === 0) return 0;
6075
6402
  const distance = Math.hypot(
@@ -6174,7 +6501,13 @@ function collectSlideNodes(component, path4, text, richText, surfaces, boxes, co
6174
6501
  const content = typeof props.text === "string" ? props.text : void 0;
6175
6502
  if (content !== void 0 && content.trim() !== "" && props.runs === void 0) {
6176
6503
  if (rec.name === "text" || rec.name === "shape") {
6177
- text.push({ props, path: path4, text: content, order });
6504
+ text.push({
6505
+ props,
6506
+ path: path4,
6507
+ componentName: rec.name,
6508
+ text: content,
6509
+ order
6510
+ });
6178
6511
  }
6179
6512
  }
6180
6513
  if (Array.isArray(props.runs) && (rec.name === "text" || rec.name === "shape")) {
@@ -6182,7 +6515,13 @@ function collectSlideNodes(component, path4, text, richText, surfaces, boxes, co
6182
6515
  (run) => run && typeof run.text === "string" ? `${run.text}${run.breakLine === true ? "\n" : ""}` : ""
6183
6516
  ).join("");
6184
6517
  if (joined.trim() !== "")
6185
- richText.push({ props, path: path4, text: joined, order });
6518
+ richText.push({
6519
+ props,
6520
+ path: path4,
6521
+ componentName: String(rec.name),
6522
+ text: joined,
6523
+ order
6524
+ });
6186
6525
  }
6187
6526
  const componentName = typeof rec.name === "string" ? rec.name : "";
6188
6527
  if (componentName === "chart" || componentName === "highcharts" || componentName === "table") {
@@ -6357,6 +6696,16 @@ function tableFact(node, slidePath, authoredProps) {
6357
6696
  }
6358
6697
  return { ...base, columns };
6359
6698
  }
6699
+ function authoredNameAtPointer(root, pointer) {
6700
+ let node = root;
6701
+ for (const token of pointer.split("/").slice(1)) {
6702
+ const key = token.replace(/~1/g, "/").replace(/~0/g, "~");
6703
+ node = Array.isArray(node) ? node[Number(key)] : asRecord(node)?.[key];
6704
+ if (node === void 0) return void 0;
6705
+ }
6706
+ const name = asRecord(node)?.name;
6707
+ return typeof name === "string" ? name : void 0;
6708
+ }
6360
6709
  function authoredPropsAtPointer(root, pointer) {
6361
6710
  let node = root;
6362
6711
  for (const token of pointer.split("/").slice(1)) {
@@ -6374,7 +6723,7 @@ function authoredPropsAtPointer(root, pointer) {
6374
6723
  }
6375
6724
  return asRecord(asRecord(node)?.props);
6376
6725
  }
6377
- function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn, slideHeightIn, ctx, theme, slideBackground, analyzedTextPaths, analyzedContentPaths, paletteTokens, authoredPropsAt, statedPropsAt, authoredPath, addFact) {
6726
+ function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn, slideHeightIn, ctx, theme, slideBackground, analyzedTextPaths, analyzedContentPaths, paletteTokens, authoredPropsAt, statedPropsAt, authoredPath, addFact, ownsProps = () => true) {
6378
6727
  const nodes = [];
6379
6728
  const richNodes = [];
6380
6729
  const surfaces = [];
@@ -6406,6 +6755,8 @@ function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn
6406
6755
  statedPropsAt
6407
6756
  );
6408
6757
  if (!isCompleteBox(box)) return;
6758
+ const onSlide = /^\/children\/\d+\/children\/\d+$/.test(node.path) && authoredPath(node.path) === node.path;
6759
+ const authored = onSlide ? authoredPropsAt(node.path) : void 0;
6409
6760
  addFact({
6410
6761
  id: `pptx:box:${renderedIndex}:${boxIndex}:${node.path}`,
6411
6762
  kind: "pptx/box",
@@ -6418,7 +6769,10 @@ function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn
6418
6769
  xPt: box.xPt,
6419
6770
  yPt: box.yPt,
6420
6771
  widthPt: box.widthPt,
6421
- heightPt: box.heightPt
6772
+ heightPt: box.heightPt,
6773
+ ...typeof authored?.x === "number" && typeof authored?.y === "number" && authored.grid === void 0 && {
6774
+ authoredPositionIn: { x: authored.x, y: authored.y }
6775
+ }
6422
6776
  });
6423
6777
  });
6424
6778
  const surfaceBoxes = surfaces.flatMap((surface) => {
@@ -6522,6 +6876,8 @@ function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn
6522
6876
  }
6523
6877
  }
6524
6878
  const colorHex = typeof node.props.color === "string" ? resolveColor(node.props.color, theme)?.toUpperCase() : void 0;
6879
+ const stated = statedPropsAt(node.path);
6880
+ const setSize = stated && node.props.fit !== void 0 ? resolveTypography(stated, ctx).fontSize : void 0;
6525
6881
  addFact({
6526
6882
  id: `pptx:text:${renderedIndex}:${nodeIndex}:${node.path}`,
6527
6883
  kind: "pptx/text",
@@ -6531,6 +6887,7 @@ function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn
6531
6887
  nodePath: node.path,
6532
6888
  text: node.text,
6533
6889
  fontSizePt: typography.fontSize,
6890
+ ...setSize !== void 0 && setSize !== typography.fontSize && { fitFromPt: setSize },
6534
6891
  lineSpacingPt: typography.lineSpacing,
6535
6892
  paraSpaceBeforePt: typography.paraSpaceBefore,
6536
6893
  paraSpaceAfterPt: typography.paraSpaceAfter,
@@ -6539,6 +6896,7 @@ function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn
6539
6896
  ...boxYPt !== void 0 && { boxYPt },
6540
6897
  ...boxWidthPt !== void 0 && boxWidthPt > 0 && { boxWidthPt },
6541
6898
  ...boxHeightPt !== void 0 && boxHeightPt > 0 && { boxHeightPt },
6899
+ insetsPt: textInsetsPt(node.props, node.componentName),
6542
6900
  verticalAlign: node.props.valign === "middle" || node.props.valign === "bottom" ? node.props.valign : "top",
6543
6901
  align: horizontalAlign(node.props, ctx),
6544
6902
  rotationDeg: asNumber(node.props.rotate) ?? 0,
@@ -6550,6 +6908,7 @@ function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn
6550
6908
  sizePath: `${node.path}/props/fontSize`
6551
6909
  },
6552
6910
  generated: authoredPath(node.path) !== node.path,
6911
+ ownsProps: ownsProps(node.path),
6553
6912
  autoFit: node.props.h === void 0 && gridPos === void 0,
6554
6913
  ...colorHex !== void 0 && { colorHex },
6555
6914
  ...!backgroundUnknown && backgroundHexes.length > 0 && { backgroundHexes }
@@ -6630,7 +6989,10 @@ function addSlideFacts(roots, slidePath, renderedIndex, page, grid, slideWidthIn
6630
6989
  ...stretched && isCompleteBox(resolved) && resolved.heightPt > 0 && {
6631
6990
  drawnRatio: resolved.widthPt / resolved.heightPt
6632
6991
  },
6633
- ...natural !== void 0 && { naturalRatio: natural }
6992
+ ...natural !== void 0 && { naturalRatio: natural },
6993
+ ...stretched && typeof authored.w === "number" && typeof authored.h === "number" && {
6994
+ authoredSizeIn: { w: authored.w, h: authored.h }
6995
+ }
6634
6996
  });
6635
6997
  }
6636
6998
  const bullets = bulletItems(box.props);
@@ -6651,7 +7013,7 @@ function preparePptxQualityDocument(document, options = {}) {
6651
7013
  const facts = [];
6652
7014
  const provenance = {};
6653
7015
  let sourceMap = {};
6654
- const authoredPath = (path4) => toAuthoredPointer(sourceMap, path4);
7016
+ const authoredPath = (path4) => toAuthoredPointer(sourceMap, path4, options.expanded?.pluginOutputs);
6655
7017
  const addFact = (raw) => {
6656
7018
  const fact = {
6657
7019
  ...raw,
@@ -6698,12 +7060,12 @@ function preparePptxQualityDocument(document, options = {}) {
6698
7060
  });
6699
7061
  });
6700
7062
  const warnings = options.warnings ?? [];
6701
- const context = resolveThemeContext(document, {
7063
+ const context = options.context ?? resolveThemeContext(document, {
6702
7064
  customThemes: options.customThemes,
6703
7065
  fonts: options.fonts,
6704
7066
  warnings
6705
7067
  });
6706
- const expanded = expandPptxBlocks(context.document, context.theme);
7068
+ const expanded = options.expanded ?? expandPptxBlocks(context.document, context.theme);
6707
7069
  sourceMap = expanded.sourceMap;
6708
7070
  const processed = processPresentation(expanded.document, {
6709
7071
  theme: context.theme,
@@ -6797,6 +7159,10 @@ function preparePptxQualityDocument(document, options = {}) {
6797
7159
  (value) => `#${resolveColor(value, processed.theme)}`
6798
7160
  ) ?? SERIES_COLOR_TOKENS.filter((token) => paletteHexes[token] !== void 0);
6799
7161
  const authoredPropsAt = (pointer) => authoredPropsAtPointer(document, authoredPath(pointer));
7162
+ const pluginOutputs = options.expanded?.pluginOutputs ?? [];
7163
+ const ownsProps = (pointer) => !pluginOutputs.some(
7164
+ (prefix) => pointer === prefix || pointer.startsWith(`${prefix}/`)
7165
+ ) && authoredNameAtPointer(document, authoredPath(pointer)) !== "block";
6800
7166
  const statedPropsAt = (pointer) => authoredPropsAtPointer(expanded.document, pointer);
6801
7167
  let exportedPages = 0;
6802
7168
  processed.slides.forEach((slide, renderedIndex) => {
@@ -6834,7 +7200,8 @@ function preparePptxQualityDocument(document, options = {}) {
6834
7200
  authoredPropsAt,
6835
7201
  statedPropsAt,
6836
7202
  authoredPath,
6837
- addFact
7203
+ addFact,
7204
+ ownsProps
6838
7205
  );
6839
7206
  });
6840
7207
  for (const budget of blockSlotBudgets(context.document, expanded.blocks)) {
@@ -6844,10 +7211,10 @@ function preparePptxQualityDocument(document, options = {}) {
6844
7211
  ...budget
6845
7212
  });
6846
7213
  }
6847
- const slotTextNodes = textNodesBySlot(processed, slideIndexes, sourceMap);
7214
+ const slotNodes = nodesBySlot(processed, slideIndexes, sourceMap);
6848
7215
  for (const role of blockSlotRoles(context.document, expanded.blocks)) {
6849
7216
  const present = role.value !== void 0 && role.value !== null && role.value !== false && (typeof role.value !== "string" || role.value.trim() !== "") && (!Array.isArray(role.value) || role.value.length > 0);
6850
- const bound = slotTextNodes.get(role.path);
7217
+ const bound = slotNodes.get(role.path);
6851
7218
  let measured;
6852
7219
  if (bound && typeof bound.props.text === "string") {
6853
7220
  const typography = resolveTypography(bound.props, ctx);
@@ -6885,6 +7252,30 @@ function preparePptxQualityDocument(document, options = {}) {
6885
7252
  ...measured
6886
7253
  });
6887
7254
  }
7255
+ const footerType = typeof processed.theme.chrome?.confidentialFooter?.type === "string" ? processed.theme.chrome.confidentialFooter.type : "footer";
7256
+ processed.slides.forEach((slide, renderedIndex) => {
7257
+ const authoredIndex = slideIndexes[renderedIndex];
7258
+ if (authoredIndex === void 0) return;
7259
+ const slidePath = `/children/${authoredIndex}`;
7260
+ const onSlide = (fact) => "slidePath" in fact && fact.slidePath === slidePath;
7261
+ const texts = facts.filter(
7262
+ (fact) => (fact.kind === "pptx/text" || fact.kind === "pptx/rich-text") && onSlide(fact)
7263
+ );
7264
+ addFact({
7265
+ id: `pptx:slide-chrome:${slidePath}`,
7266
+ kind: "pptx/slide-chrome",
7267
+ path: slidePath,
7268
+ index: renderedIndex,
7269
+ ...slide.hidden === true && { hidden: true },
7270
+ blocks: expanded.blocks.filter(
7271
+ (pointer) => pointer.startsWith(`${slidePath}/children/`)
7272
+ ).length,
7273
+ pageNumber: texts.some((fact) => fact.text.includes("{PAGE_NUMBER}")),
7274
+ footer: texts.some((fact) => fact.styleName === footerType) || facts.some(
7275
+ (fact) => fact.kind === "pptx/chrome-slot" && fact.role === "footer" && fact.present && onSlide(fact)
7276
+ )
7277
+ });
7278
+ });
6888
7279
  return {
6889
7280
  format: "pptx",
6890
7281
  model: {
@@ -6907,7 +7298,7 @@ function preparePptxQualityDocument(document, options = {}) {
6907
7298
  }
6908
7299
  };
6909
7300
  }
6910
- function textNodesBySlot(processed, slideIndexes, sourceMap) {
7301
+ function nodesBySlot(processed, slideIndexes, sourceMap) {
6911
7302
  const found = /* @__PURE__ */ new Map();
6912
7303
  const visit = (component, path4) => {
6913
7304
  if (component.enabled === false) return;
@@ -6915,6 +7306,10 @@ function textNodesBySlot(processed, slideIndexes, sourceMap) {
6915
7306
  const origin = toAuthoredPointer(sourceMap, `${path4}/props/text`);
6916
7307
  if (origin !== `${path4}/props/text` && !found.has(origin))
6917
7308
  found.set(origin, { props: asRecord(component.props) ?? {}, path: path4 });
7309
+ } else {
7310
+ const origin = toAuthoredPointer(sourceMap, path4);
7311
+ if (origin !== path4 && origin.includes("/props/slots/") && !found.has(origin))
7312
+ found.set(origin, { props: asRecord(component.props) ?? {}, path: path4 });
6918
7313
  }
6919
7314
  (component.children ?? []).forEach(
6920
7315
  (child, index) => visit(child, `${path4}/children/${index}`)
@@ -7673,6 +8068,8 @@ import {
7673
8068
  // src/quality/rules.ts
7674
8069
  import {
7675
8070
  chartInfoDesignFindings,
8071
+ configurationLabel,
8072
+ configurationSource,
7676
8073
  DEFAULT_MAXIMUM_CHART_SERIES,
7677
8074
  DEFAULT_MAXIMUM_PIE_SLICES,
7678
8075
  fontCountFinding,
@@ -7813,19 +8210,24 @@ var pptxMinimumFontRule = {
7813
8210
  );
7814
8211
  return textFacts(facts).filter((fact) => fact.fontSizePt < minimum).map((fact) => ({
7815
8212
  message: `Effective font size is ${fact.fontSizePt}pt \u2014 unreadable on a projected slide.`,
7816
- path: `${fact.path}/props`,
8213
+ // Text a definition or a plugin generated reports at the invocation
8214
+ // the author wrote, whose props are the block's or the plugin's: no
8215
+ // fontSize of theirs to lift, so no patch.
8216
+ path: fact.ownsProps ? `${fact.path}/props` : fact.path,
7817
8217
  suggestion: `Use at least ${minimum}pt; captions rarely work below 10pt.`,
7818
8218
  context: { fontSize: fact.fontSizePt, threshold: minimum },
7819
8219
  evidence: { actual: fact.fontSizePt, expected: minimum, unit: "pt" },
7820
8220
  // `add` replaces an existing member, so this lifts an explicit
7821
8221
  // fontSize and overrides an inherited style value alike.
7822
- fixes: [
7823
- {
7824
- op: "add",
7825
- path: `${fact.path}/props/fontSize`,
7826
- value: minimum
7827
- }
7828
- ]
8222
+ ...fact.ownsProps && {
8223
+ fixes: [
8224
+ {
8225
+ op: "add",
8226
+ path: `${fact.path}/props/fontSize`,
8227
+ value: minimum
8228
+ }
8229
+ ]
8230
+ }
7829
8231
  }));
7830
8232
  }
7831
8233
  };
@@ -8429,18 +8831,67 @@ var pptxRequiredChromeRule = {
8429
8831
  if (required.length === 0) return [];
8430
8832
  return facts.filter(
8431
8833
  (fact) => fact.kind === "pptx/chrome-slot"
8432
- ).filter((fact) => required.includes(fact.role) && !fact.present).map((fact) => ({
8433
- path: fact.path,
8434
- relatedPaths: [fact.invocation],
8435
- message: `${fact.block} states no ${fact.role} in its "${fact.slot}" slot; the ${profile?.id ?? "selected"} profile expects one on every ${fact.block}.`,
8436
- suggestion: `Fill the "${fact.slot}" slot. The theme already styles it.`,
8437
- context: { block: fact.block, slot: fact.slot, role: fact.role },
8438
- evidence: {
8439
- actual: "empty",
8440
- expected: fact.role,
8441
- values: { source: "profile", required }
8442
- }
8443
- }));
8834
+ ).filter((fact) => required.includes(fact.role) && !fact.present).map((fact) => {
8835
+ const source = configurationSource(configuration, "required");
8836
+ return {
8837
+ path: fact.path,
8838
+ relatedPaths: [fact.invocation],
8839
+ message: `${fact.block} states no ${fact.role} in its "${fact.slot}" slot; ${configurationLabel(source, profile)} expects one on every ${fact.block}.`,
8840
+ suggestion: `Fill the "${fact.slot}" slot. The theme already styles it.`,
8841
+ context: { block: fact.block, slot: fact.slot, role: fact.role },
8842
+ evidence: {
8843
+ actual: "empty",
8844
+ expected: fact.role,
8845
+ values: { source, required }
8846
+ }
8847
+ };
8848
+ });
8849
+ }
8850
+ };
8851
+ var SLIDE_CHROME_PARTS = ["pageNumber", "footer"];
8852
+ var pptxSlideFooterRule = {
8853
+ id: "pptx/slide-footer",
8854
+ description: "A block-built slide without the running chrome a profile or policy expects: a page number, a footer line. Off until one names parts.",
8855
+ code: QUALITY_CODES.CHROME_MISSING,
8856
+ category: "consistency",
8857
+ defaultSeverity: "warning",
8858
+ defaultCertainty: "deterministic",
8859
+ formats: ["pptx"],
8860
+ defaultParameters: { required: [], fromSlide: 1 },
8861
+ evaluate: ({ facts, configuration, profile }) => {
8862
+ const required = stringListParameter(
8863
+ configuration.parameters,
8864
+ "required"
8865
+ ).filter(
8866
+ (part) => SLIDE_CHROME_PARTS.includes(part)
8867
+ );
8868
+ if (required.length === 0) return [];
8869
+ const from = numberParameter(configuration.parameters, "fromSlide", 1);
8870
+ return facts.filter(
8871
+ (fact) => fact.kind === "pptx/slide-chrome" && fact.blocks > 0 && fact.hidden !== true && fact.index >= from
8872
+ ).flatMap((fact) => {
8873
+ const missing = required.filter((part) => !fact[part]);
8874
+ if (missing.length === 0) return [];
8875
+ const parts = missing.map((part) => part === "pageNumber" ? "page number" : "footer").join(" or ");
8876
+ const source = configurationSource(
8877
+ configuration,
8878
+ "required",
8879
+ "fromSlide"
8880
+ );
8881
+ return [
8882
+ {
8883
+ path: fact.path,
8884
+ message: `Slide ${fact.index + 1} carries no ${parts}; ${configurationLabel(source, profile)} expects one on every slide a block builds from slide ${from + 1} on.`,
8885
+ suggestion: "Build the slide with a block that draws the running footer \u2014 the house blocks set {PAGE_NUMBER} / {PAGE_COUNT} in the footer role \u2014 or add that text to the definition the slide invokes.",
8886
+ context: { slide: fact.index, missing },
8887
+ evidence: {
8888
+ actual: required.filter((part) => fact[part]),
8889
+ expected: required,
8890
+ values: { source, fromSlide: from }
8891
+ }
8892
+ }
8893
+ ];
8894
+ });
8444
8895
  }
8445
8896
  };
8446
8897
  var pptxActionTitleRule = {
@@ -8473,6 +8924,8 @@ var pptxActionTitleRule = {
8473
8924
  }
8474
8925
  };
8475
8926
  var TITLE_DRIFT_TOLERANCE_PT = 2;
8927
+ var FIRST_BASELINE_EM = 0.8;
8928
+ var TITLE_PLACEMENT_LINES = 2;
8476
8929
  var PPTX_TYPE_VOCABULARY = {
8477
8930
  subject: "deck",
8478
8931
  keepTo: "Keep to the theme styles \u2014 title, heading, body, label, statistic \u2014 and drop the ad-hoc sizes."
@@ -8549,11 +9002,15 @@ var pptxSizeCountRule = {
8549
9002
  defaultParameters: { maximumSizes: 8, maximumSizesPerSlide: 0 },
8550
9003
  evaluate: ({ facts, configuration, profile }) => {
8551
9004
  const sizes = paintedSizes(facts);
9005
+ const setBy = (parameter) => {
9006
+ const source = configurationSource(configuration, parameter);
9007
+ return { source, label: configurationLabel(source, profile) };
9008
+ };
8552
9009
  const findings = sizeCountFinding(
8553
9010
  sizes,
8554
9011
  numberParameter(configuration.parameters, "maximumSizes", 8),
8555
9012
  pptxThemeFact(facts)?.path ?? "/props",
8556
- profile?.id,
9013
+ setBy("maximumSizes"),
8557
9014
  PPTX_TYPE_VOCABULARY
8558
9015
  ).map((finding) => ({
8559
9016
  ...finding,
@@ -8576,7 +9033,7 @@ var pptxSizeCountRule = {
8576
9033
  onSlide,
8577
9034
  perSlide,
8578
9035
  slidePath,
8579
- profile?.id,
9036
+ setBy("maximumSizesPerSlide"),
8580
9037
  PPTX_SLIDE_TYPE_VOCABULARY
8581
9038
  ))
8582
9039
  findings.push({
@@ -8600,53 +9057,107 @@ var pptxRoleDriftRule = {
8600
9057
  return theme ? roleDriftFindings(paintedSizes(facts), theme.roleSizesPt) : [];
8601
9058
  }
8602
9059
  };
8603
- function titleGroups(facts, styles) {
8604
- const texts = textFacts(facts).filter(
8605
- (fact) => fact.boxXPt !== void 0 && fact.boxYPt !== void 0
9060
+ var EDGE_NAMES = {
9061
+ left: "left edge",
9062
+ center: "centre line",
9063
+ right: "right edge"
9064
+ };
9065
+ function laidOutTitle(fact, path4, order) {
9066
+ if (fact.boxXPt === void 0 || fact.boxYPt === void 0 || fact.boxWidthPt === void 0 || fact.rotationDeg % 360 !== 0)
9067
+ return void 0;
9068
+ const [top, right, bottom, left] = fact.insetsPt;
9069
+ const innerWidth = Math.max(1, fact.boxWidthPt - left - right);
9070
+ const { heightPt, lines } = estimateTextHeightPt(
9071
+ fact.text,
9072
+ innerWidth,
9073
+ fact.fontSizePt,
9074
+ fact.lineSpacingPt,
9075
+ fact.paraSpaceBeforePt,
9076
+ fact.paraSpaceAfterPt
8606
9077
  );
8607
- const groups = /* @__PURE__ */ new Map();
9078
+ let textTop = fact.boxYPt + top;
9079
+ if (fact.boxHeightPt !== void 0 && !fact.autoFit) {
9080
+ const room = fact.boxHeightPt - top - bottom - heightPt;
9081
+ if (fact.verticalAlign === "middle") textTop += room / 2;
9082
+ else if (fact.verticalAlign === "bottom") textTop += room;
9083
+ }
9084
+ const align = fact.align === "justify" ? "left" : fact.align;
9085
+ const edgePt = align === "center" ? fact.boxXPt + left + innerWidth / 2 : align === "right" ? fact.boxXPt + fact.boxWidthPt - right : fact.boxXPt + left;
9086
+ const style = fact.styleName ?? "unstyled";
9087
+ const setSizePt = fact.fitFromPt ?? fact.fontSizePt;
9088
+ return {
9089
+ fact,
9090
+ path: path4,
9091
+ kind: `${style}|${setSizePt}|${align}`,
9092
+ style,
9093
+ setSizePt,
9094
+ align,
9095
+ lines,
9096
+ baselinePt: textTop + FIRST_BASELINE_EM * fact.fontSizePt,
9097
+ edgePt,
9098
+ order
9099
+ };
9100
+ }
9101
+ function deckTitles(facts, styles) {
9102
+ const texts = textFacts(facts).filter((fact) => !fact.slideHidden);
9103
+ const slideIndex = (fact) => Number(/^\/children\/(\d+)/.exec(fact.slidePath)?.[1] ?? 0);
9104
+ const order = new Map(
9105
+ texts.map((fact, index) => [fact, slideIndex(fact) * 1e4 + index])
9106
+ );
9107
+ const byNode = new Map(texts.map((fact) => [fact.nodePath, fact]));
9108
+ const titles = [];
8608
9109
  const claimed = /* @__PURE__ */ new Set();
8609
- const push = (key, fact) => {
9110
+ const add = (fact, path4) => {
8610
9111
  if (claimed.has(fact)) return;
8611
9112
  claimed.add(fact);
8612
- groups.set(key, [...groups.get(key) ?? [], fact]);
9113
+ const title = laidOutTitle(fact, path4, order.get(fact));
9114
+ if (title) titles.push(title);
8613
9115
  };
8614
- for (const slot of facts) {
8615
- if (slot.kind !== "pptx/chrome-slot" || slot.role !== "actionTitle")
8616
- continue;
8617
- const chrome = slot;
8618
- if (chrome.text === void 0) continue;
8619
- const painted = texts.find(
8620
- (fact) => fact.slidePath === chrome.slidePath && fact.text === chrome.text
8621
- );
8622
- if (painted) push(chrome.block, painted);
8623
- }
9116
+ for (const slot of facts)
9117
+ if (slot.kind === "pptx/chrome-slot" && slot.role === "actionTitle" && slot.nodePath !== void 0) {
9118
+ const painted = byNode.get(slot.nodePath);
9119
+ if (painted) add(painted, slot.path);
9120
+ }
8624
9121
  for (const fact of texts)
8625
9122
  if (fact.styleName !== void 0 && styles.includes(fact.styleName))
8626
- push(`style:${fact.styleName}`, fact);
8627
- return groups;
8628
- }
8629
- function prevailing(values) {
8630
- const counts = /* @__PURE__ */ new Map();
8631
- for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1);
8632
- let best;
8633
- let bestCount = 0;
8634
- for (const value of values) {
8635
- const count = counts.get(value);
8636
- if (count > bestCount) {
8637
- best = value;
8638
- bestCount = count;
9123
+ add(fact, fact.path);
9124
+ return titles.sort((a, b) => a.order - b.order);
9125
+ }
9126
+ function titlePlacements(titles, tolerance) {
9127
+ const lands = (a, b) => Math.abs(a.baselinePt - b.baselinePt) <= tolerance && Math.abs(a.edgePt - b.edgePt) <= tolerance;
9128
+ const byKind = /* @__PURE__ */ new Map();
9129
+ for (const title of titles)
9130
+ byKind.set(title.kind, [...byKind.get(title.kind) ?? [], title]);
9131
+ const placements = [];
9132
+ for (const kind of byKind.values()) {
9133
+ let pending = kind;
9134
+ while (pending.length > 0) {
9135
+ let reference = pending[0];
9136
+ let most = 0;
9137
+ for (const candidate of pending) {
9138
+ const count = pending.filter((other) => lands(other, candidate)).length;
9139
+ if (count > most) {
9140
+ reference = candidate;
9141
+ most = count;
9142
+ }
9143
+ }
9144
+ const reach = TITLE_PLACEMENT_LINES * reference.fact.lineSpacingPt;
9145
+ const members = pending.filter(
9146
+ (title) => Math.abs(title.baselinePt - reference.baselinePt) <= reach && Math.abs(title.edgePt - reference.edgePt) <= reach
9147
+ );
9148
+ placements.push({ reference, members });
9149
+ pending = pending.filter((title) => !members.includes(title));
8639
9150
  }
8640
9151
  }
8641
- return best;
9152
+ return placements;
8642
9153
  }
8643
9154
  var pptxTitleDriftRule = {
8644
9155
  id: "pptx/title-drift",
8645
- description: "A slide title away from the left edge or baseline the deck\u2019s other titles of that kind share. Off until a profile or policy enables it.",
9156
+ description: "A slide title whose laid-out first baseline or aligned edge is away from where the deck\u2019s other titles of that kind land. Off until a profile or policy enables it.",
8646
9157
  code: QUALITY_CODES.TITLE_DRIFT,
8647
9158
  category: "consistency",
8648
9159
  defaultSeverity: "warning",
8649
- defaultCertainty: "deterministic",
9160
+ defaultCertainty: "estimated",
8650
9161
  formats: ["pptx"],
8651
9162
  defaultEnabled: false,
8652
9163
  defaultParameters: {
@@ -8661,40 +9172,53 @@ var pptxTitleDriftRule = {
8661
9172
  TITLE_DRIFT_TOLERANCE_PT
8662
9173
  );
8663
9174
  const findings = [];
8664
- for (const [kind, titles] of titleGroups(facts, styles)) {
8665
- if (titles.length < 2) continue;
8666
- const axes = [
8667
- {
8668
- key: "left edge",
8669
- prop: "boxXPt",
8670
- value: prevailing(titles.map((fact) => fact.boxXPt))
8671
- },
8672
- {
8673
- key: "baseline",
8674
- prop: "boxYPt",
8675
- value: prevailing(titles.map((fact) => fact.boxYPt))
8676
- }
8677
- ];
8678
- for (const fact of titles) {
8679
- const off = axes.filter(
8680
- (axis) => axis.value !== void 0 && Math.abs(fact[axis.prop] - axis.value) > tolerance
9175
+ for (const { reference, members } of titlePlacements(
9176
+ deckTitles(facts, styles),
9177
+ tolerance
9178
+ )) {
9179
+ if (members.length < 2) continue;
9180
+ const agreeing = members.filter(
9181
+ (title) => Math.abs(title.baselinePt - reference.baselinePt) <= tolerance && Math.abs(title.edgePt - reference.edgePt) <= tolerance
9182
+ );
9183
+ for (const title of members) {
9184
+ if (agreeing.includes(title)) continue;
9185
+ const edge = EDGE_NAMES[title.align];
9186
+ const off = [
9187
+ {
9188
+ axis: "first baseline",
9189
+ actual: title.baselinePt,
9190
+ expected: reference.baselinePt
9191
+ },
9192
+ { axis: edge, actual: title.edgePt, expected: reference.edgePt }
9193
+ ].filter(
9194
+ (entry) => Math.abs(entry.actual - entry.expected) > tolerance
8681
9195
  );
8682
- if (off.length === 0) continue;
8683
9196
  const [first] = off;
9197
+ const size = title.fact.fontSizePt !== reference.fact.fontSizePt ? ` The fit pass sets it at ${title.fact.fontSizePt}pt, where they set at ${reference.fact.fontSizePt}pt.` : "";
8684
9198
  findings.push({
8685
- path: fact.path,
8686
- message: `This ${kind.startsWith("style:") ? kind.slice(6) : kind} title sits at ${off.map((axis) => `${axis.key} ${round(fact[axis.prop])}pt`).join(", ")}; the deck's others share ${off.map((axis) => `${axis.key} ${round(axis.value)}pt`).join(", ")}.`,
8687
- suggestion: "Place every title of one kind with the same block or the same coordinates, so the deck holds one line down the page.",
9199
+ path: title.path,
9200
+ relatedPaths: agreeing.map((other) => other.path),
9201
+ message: `This ${title.style} title lays out at ${off.map((entry) => `${entry.axis} ${round(entry.actual)}pt`).join(", ")}; the deck's other titles of that kind land at ${off.map((entry) => `${entry.axis} ${round(entry.expected)}pt`).join(", ")}.${size}`,
9202
+ suggestion: "Place every title of one kind with the same block or the same coordinates, anchor them alike, and keep each short enough to set at the size the others keep.",
8688
9203
  context: {
8689
- kind,
8690
- axes: off.map((axis) => axis.key),
8691
- slide: fact.slidePath
9204
+ style: title.style,
9205
+ setSizePt: title.setSizePt,
9206
+ align: title.align,
9207
+ axes: off.map((entry) => entry.axis),
9208
+ slide: title.fact.slidePath
8692
9209
  },
8693
9210
  evidence: {
8694
- actual: round(fact[first.prop]),
8695
- expected: round(first.value),
9211
+ summary: "estimated first baseline and aligned edge",
9212
+ actual: round(first.actual),
9213
+ expected: round(first.expected),
8696
9214
  unit: "pt",
8697
- values: { axis: first.key, source: "profile" }
9215
+ values: {
9216
+ axis: first.axis,
9217
+ lines: title.lines,
9218
+ fontSizePt: title.fact.fontSizePt,
9219
+ verticalAlign: title.fact.verticalAlign,
9220
+ source: configurationSource(configuration, "tolerancePt")
9221
+ }
8698
9222
  }
8699
9223
  });
8700
9224
  }
@@ -8747,7 +9271,9 @@ var pptxBulletRule = {
8747
9271
  actual: bullets.items,
8748
9272
  expected: maximumBullets,
8749
9273
  unit: "bullets",
8750
- values: { source: "profile" }
9274
+ values: {
9275
+ source: configurationSource(configuration, "maximumBullets")
9276
+ }
8751
9277
  }
8752
9278
  });
8753
9279
  if (maximumWords > 0 && bullets.longestWords > maximumWords)
@@ -8761,16 +9287,39 @@ var pptxBulletRule = {
8761
9287
  actual: bullets.longestWords,
8762
9288
  expected: maximumWords,
8763
9289
  unit: "words",
8764
- values: { source: "profile" }
9290
+ values: {
9291
+ source: configurationSource(
9292
+ configuration,
9293
+ "maximumWordsPerBullet"
9294
+ )
9295
+ }
8765
9296
  }
8766
9297
  });
8767
9298
  }
8768
9299
  return findings;
8769
9300
  }
8770
9301
  };
9302
+ function safeAreaFixes(fact, canvas, safe) {
9303
+ const position = fact.authoredPositionIn;
9304
+ if (position === void 0 || fact.widthPt > canvas.widthPt - 2 * safe || fact.heightPt > canvas.heightPt - 2 * safe)
9305
+ return [];
9306
+ const clamp = (valuePt, sizePt, extentPt) => {
9307
+ const low = Math.ceil(safe / 72 * 1e3) / 1e3;
9308
+ const high = Math.floor((extentPt - safe - sizePt) / 72 * 1e3) / 1e3;
9309
+ return Math.min(Math.max(valuePt / 72, low), high);
9310
+ };
9311
+ const fixes = [];
9312
+ const x = clamp(fact.xPt, fact.widthPt, canvas.widthPt);
9313
+ const y = clamp(fact.yPt, fact.heightPt, canvas.heightPt);
9314
+ if (Math.abs(x - position.x) > 5e-4)
9315
+ fixes.push({ op: "replace", path: `${fact.path}/props/x`, value: x });
9316
+ if (Math.abs(y - position.y) > 5e-4)
9317
+ fixes.push({ op: "replace", path: `${fact.path}/props/y`, value: y });
9318
+ return fixes;
9319
+ }
8771
9320
  var pptxSafeAreaRule = {
8772
9321
  id: "pptx/safe-area",
8773
- description: "Content outside the theme\u2019s safe area that is neither chrome nor a full bleed. Off until a profile or policy enables it.",
9322
+ description: "Content outside the theme\u2019s safe area that is neither chrome (a tracker, footer, source or logo) nor a full bleed. Off until a profile or policy enables it.",
8774
9323
  code: QUALITY_CODES.SAFE_AREA,
8775
9324
  category: "composition",
8776
9325
  defaultSeverity: "warning",
@@ -8791,7 +9340,7 @@ var pptxSafeAreaRule = {
8791
9340
  if (canvas === void 0 || safe === void 0 || safe <= 0) return [];
8792
9341
  const chromeNodes = new Set(
8793
9342
  facts.filter(
8794
- (fact) => fact.kind === "pptx/chrome-slot" && (fact.role === "tracker" || fact.role === "footer" || fact.role === "source")
9343
+ (fact) => fact.kind === "pptx/chrome-slot" && (fact.role === "tracker" || fact.role === "footer" || fact.role === "source" || fact.role === "logo")
8795
9344
  ).flatMap((fact) => fact.nodePath ? [fact.nodePath] : [])
8796
9345
  );
8797
9346
  const chromeStyles = /* @__PURE__ */ new Set(["footer", "tracker", "source"]);
@@ -8817,6 +9366,7 @@ var pptxSafeAreaRule = {
8817
9366
  right - (canvas.widthPt - safe),
8818
9367
  bottom - (canvas.heightPt - safe)
8819
9368
  );
9369
+ const fixes = safeAreaFixes(fact, canvas, safe);
8820
9370
  return [
8821
9371
  {
8822
9372
  path: fact.path,
@@ -8828,7 +9378,8 @@ var pptxSafeAreaRule = {
8828
9378
  expected: 0,
8829
9379
  unit: "pt",
8830
9380
  values: { source: "theme" }
8831
- }
9381
+ },
9382
+ ...fixes.length > 0 && { fixes }
8832
9383
  }
8833
9384
  ];
8834
9385
  });
@@ -8864,7 +9415,7 @@ var pptxSlideTitleRule = {
8864
9415
  actual: 0,
8865
9416
  expected: 1,
8866
9417
  unit: "titles",
8867
- values: { source: "profile" }
9418
+ values: { source: configurationSource(configuration) }
8868
9419
  }
8869
9420
  }));
8870
9421
  }
@@ -8913,7 +9464,20 @@ var pptxImageAspectRule = {
8913
9464
  {
8914
9465
  path: fact.path,
8915
9466
  drawn: fact.drawnRatio,
8916
- natural: fact.naturalRatio
9467
+ natural: fact.naturalRatio,
9468
+ ...fact.authoredSizeIn && {
9469
+ sides: {
9470
+ width: {
9471
+ path: `${fact.path}/props/w`,
9472
+ value: fact.authoredSizeIn.w
9473
+ },
9474
+ height: {
9475
+ path: `${fact.path}/props/h`,
9476
+ value: fact.authoredSizeIn.h
9477
+ },
9478
+ decimals: 3
9479
+ }
9480
+ }
8917
9481
  },
8918
9482
  "slide",
8919
9483
  tolerance
@@ -8939,6 +9503,7 @@ var PPTX_QUALITY_RULES = {
8939
9503
  pptxOffCanvasRule,
8940
9504
  pptxSlotBudgetRule,
8941
9505
  pptxRequiredChromeRule,
9506
+ pptxSlideFooterRule,
8942
9507
  pptxActionTitleRule,
8943
9508
  pptxTypeScaleRule,
8944
9509
  pptxSizeCountRule,
@@ -8973,11 +9538,12 @@ var PPTX_QUALITY_PROFILES = {
8973
9538
  "consulting-deck": {
8974
9539
  id: "consulting-deck",
8975
9540
  formats: ["pptx"],
8976
- description: "Consulting readout: every content slide leads with a two-line action title, every chart carries a takeaway and a source, content stays inside the theme\u2019s safe area, a box holds at most five bullets of at most twelve words, every figure carries alt text, every size is on the theme scale with at most nine in the deck and six on a slide, and titles of one kind hold one line.",
9541
+ description: "Consulting readout: every content slide leads with a two-line action title, every chart carries a takeaway and a source, every slide a block builds after the cover carries its page number, content stays inside the theme\u2019s safe area, a box holds at most five bullets of at most twelve words, every figure carries alt text, every size is on the theme scale with at most nine in the deck and six on a slide, and titles of one kind hold one line.",
8977
9542
  rules: {
8978
9543
  "pptx/required-chrome": {
8979
9544
  parameters: { required: ["takeaway", "source"] }
8980
9545
  },
9546
+ "pptx/slide-footer": { parameters: { required: ["pageNumber"] } },
8981
9547
  "pptx/action-title": { parameters: { maxLines: 2 } },
8982
9548
  "pptx/slide-density": { parameters: { maximumBodyWords: 90 } },
8983
9549
  "pptx/type-scale": { enabled: true },
@@ -9270,97 +9836,152 @@ function createBuilderImpl(state) {
9270
9836
  newState
9271
9837
  );
9272
9838
  }
9273
- async function generate(document, options) {
9274
- try {
9275
- let internalDocument = document;
9276
- const renderer = options?.renderer ?? state.renderer ?? internalDocument.renderer;
9277
- const validationDocument = renderer === void 0 ? internalDocument : { ...internalDocument, renderer };
9278
- const validationOptions = {
9279
- ...state.validation,
9280
- ...options?.validation
9281
- };
9282
- if (validationOptions.enabled !== false) {
9283
- const result = validatePresentation(
9284
- validationDocument,
9285
- state.components,
9286
- { allowUnknownFields: validationOptions.allowUnknownFields }
9839
+ async function expandPresentation(document, options) {
9840
+ let internalDocument = document;
9841
+ const renderer = options?.renderer ?? state.renderer ?? internalDocument.renderer;
9842
+ const validationDocument = renderer === void 0 ? internalDocument : { ...internalDocument, renderer };
9843
+ const validationOptions = {
9844
+ ...state.validation,
9845
+ ...options?.validation
9846
+ };
9847
+ if (validationOptions.enabled !== false) {
9848
+ const result = validatePresentation(
9849
+ validationDocument,
9850
+ state.components,
9851
+ { allowUnknownFields: validationOptions.allowUnknownFields }
9852
+ );
9853
+ if (!result.valid) {
9854
+ throw new ComponentValidationError3(result.errors, internalDocument);
9855
+ }
9856
+ } else if (!internalDocument || internalDocument.name !== "pptx") {
9857
+ throw new Error("Top-level component must be a pptx component");
9858
+ }
9859
+ const warnings = [];
9860
+ const context = resolveThemeContext(internalDocument, {
9861
+ customThemes: state.customThemes,
9862
+ fonts: state.fonts,
9863
+ warnings,
9864
+ defaultThemeName: typeof state.theme === "string" ? state.theme : void 0,
9865
+ resolveNamedTheme: (name, authored) => state.customThemes?.[name] ?? (authored && hasPptxTheme(name) ? getPptxTheme(name) : void 0) ?? (typeof state.theme === "object" && state.theme !== null ? state.theme : getPptxTheme(name))
9866
+ });
9867
+ const modedRoot = context.document;
9868
+ const resolvedTheme = context.theme;
9869
+ const validateEmitted = validationOptions.enabled === false ? void 0 : (emitted, componentLabel, parentName) => {
9870
+ let validationDocument2;
9871
+ if (parentName === "pptx") {
9872
+ validationDocument2 = {
9873
+ ...modedRoot,
9874
+ ...renderer !== void 0 ? { renderer } : {},
9875
+ children: emitted
9876
+ };
9877
+ } else if (parentName === "slide") {
9878
+ validationDocument2 = {
9879
+ ...modedRoot,
9880
+ ...renderer !== void 0 ? { renderer } : {},
9881
+ children: [{ name: "slide", props: {}, children: emitted }]
9882
+ };
9883
+ } else {
9884
+ return;
9885
+ }
9886
+ const result = validatePresentation(
9887
+ validationDocument2,
9888
+ state.components,
9889
+ { allowUnknownFields: validationOptions.allowUnknownFields }
9890
+ );
9891
+ if (!result.valid) {
9892
+ throw new ComponentValidationError3(
9893
+ result.errors.map((error) => ({
9894
+ ...error,
9895
+ message: `custom component '${componentLabel}' emitted invalid output \u2014 ${error.message}`
9896
+ })),
9897
+ emitted
9287
9898
  );
9288
- if (!result.valid) {
9289
- throw new ComponentValidationError3(result.errors, internalDocument);
9899
+ }
9900
+ };
9901
+ const expanded = await expandPptxBlocksWithPlugins(
9902
+ modedRoot,
9903
+ resolvedTheme,
9904
+ new Set(componentMap.keys()),
9905
+ pluginRenderer(warnings, resolvedTheme, validateEmitted)
9906
+ );
9907
+ const processedDocument = expanded.document;
9908
+ if (validationOptions.enabled !== false) {
9909
+ const result = validatePresentation(
9910
+ {
9911
+ ...processedDocument,
9912
+ ...renderer !== void 0 ? { renderer } : {}
9913
+ },
9914
+ [],
9915
+ {
9916
+ allowUnknownFields: validationOptions.allowUnknownFields
9290
9917
  }
9291
- } else if (!internalDocument || internalDocument.name !== "pptx") {
9292
- throw new Error("Top-level component must be a pptx component");
9918
+ );
9919
+ if (!result.valid) {
9920
+ throw new ComponentValidationError3(
9921
+ result.errors.map((error) => ({
9922
+ ...error,
9923
+ message: `expanded plugin output failed validation \u2014 ${error.message}`
9924
+ })),
9925
+ processedDocument
9926
+ );
9293
9927
  }
9294
- const warnings = [];
9295
- const context = resolveThemeContext(internalDocument, {
9928
+ }
9929
+ return { context, expanded, warnings, renderer };
9930
+ }
9931
+ async function prepareQuality(document, options) {
9932
+ const { context, expanded, warnings, renderer } = await expandPresentation(
9933
+ document,
9934
+ options
9935
+ );
9936
+ const prepared = preparePptxQualityDocument(
9937
+ document,
9938
+ {
9939
+ context,
9940
+ expanded,
9296
9941
  customThemes: state.customThemes,
9297
9942
  fonts: state.fonts,
9298
- warnings,
9299
- defaultThemeName: typeof state.theme === "string" ? state.theme : void 0,
9300
- resolveNamedTheme: (name, authored) => state.customThemes?.[name] ?? (authored && hasPptxTheme(name) ? getPptxTheme(name) : void 0) ?? (typeof state.theme === "object" && state.theme !== null ? state.theme : getPptxTheme(name))
9301
- });
9302
- const modedRoot = context.document;
9303
- const resolvedTheme = context.theme;
9304
- const validateEmitted = validationOptions.enabled === false ? void 0 : (emitted, componentLabel, parentName) => {
9305
- let validationDocument2;
9306
- if (parentName === "pptx") {
9307
- validationDocument2 = {
9308
- ...modedRoot,
9309
- ...renderer !== void 0 ? { renderer } : {},
9310
- children: emitted
9311
- };
9312
- } else if (parentName === "slide") {
9313
- validationDocument2 = {
9314
- ...modedRoot,
9315
- ...renderer !== void 0 ? { renderer } : {},
9316
- children: [{ name: "slide", props: {}, children: emitted }]
9317
- };
9318
- } else {
9319
- return;
9320
- }
9321
- const result = validatePresentation(
9322
- validationDocument2,
9323
- state.components,
9324
- { allowUnknownFields: validationOptions.allowUnknownFields }
9943
+ services: state.services,
9944
+ ...renderer !== void 0 && { renderer },
9945
+ warnings
9946
+ }
9947
+ );
9948
+ return { prepared, warnings };
9949
+ }
9950
+ async function generateFromPrepared(options) {
9951
+ const { prepared } = options;
9952
+ const { document: processedDocument, theme, processed } = prepared.model;
9953
+ const warnings = [];
9954
+ const renderer = options.renderer ?? state.renderer ?? prepared.renderer;
9955
+ const hasHighcharts = containsHighcharts(processed);
9956
+ const resolvedFonts = await resolveDocumentFonts(
9957
+ processedDocument,
9958
+ theme,
9959
+ warnings,
9960
+ state.fonts,
9961
+ hasHighcharts
9962
+ );
9963
+ const chartFonts = hasHighcharts ? toChartFontFaces2(resolvedFonts) : [];
9964
+ const buffer = await runWithBaseDir(
9965
+ options.baseDir ?? state.baseDir,
9966
+ () => renderProcessedViaIr(processed, warnings, {
9967
+ renderer,
9968
+ services: state.services,
9969
+ deterministic: options.deterministic ?? state.packaging.deterministic,
9970
+ generatedAt: options.generatedAt ?? state.packaging.generatedAt,
9971
+ ...chartFonts.length > 0 ? { chartFonts } : {}
9972
+ })
9973
+ );
9974
+ return { buffer, warnings };
9975
+ }
9976
+ async function generate(document, options) {
9977
+ try {
9978
+ if (options?.prepared)
9979
+ return await generateFromPrepared(
9980
+ options
9325
9981
  );
9326
- if (!result.valid) {
9327
- throw new ComponentValidationError3(
9328
- result.errors.map((error) => ({
9329
- ...error,
9330
- message: `custom component '${componentLabel}' emitted invalid output \u2014 ${error.message}`
9331
- })),
9332
- emitted
9333
- );
9334
- }
9335
- };
9336
- const expanded = await expandPptxBlocksWithPlugins(
9337
- modedRoot,
9338
- resolvedTheme,
9339
- new Set(componentMap.keys()),
9340
- pluginRenderer(warnings, resolvedTheme, validateEmitted)
9341
- );
9982
+ const { context, expanded, warnings, renderer } = await expandPresentation(document, options);
9983
+ const resolvedTheme = context.theme;
9342
9984
  const processedDocument = expanded.document;
9343
- if (validationOptions.enabled !== false) {
9344
- const result = validatePresentation(
9345
- {
9346
- ...processedDocument,
9347
- ...renderer !== void 0 ? { renderer } : {}
9348
- },
9349
- [],
9350
- {
9351
- allowUnknownFields: validationOptions.allowUnknownFields
9352
- }
9353
- );
9354
- if (!result.valid) {
9355
- throw new ComponentValidationError3(
9356
- result.errors.map((error) => ({
9357
- ...error,
9358
- message: `expanded plugin output failed validation \u2014 ${error.message}`
9359
- })),
9360
- processedDocument
9361
- );
9362
- }
9363
- }
9364
9985
  const processed = processPresentation(processedDocument, {
9365
9986
  theme: resolvedTheme,
9366
9987
  services: state.services,
@@ -9459,6 +10080,7 @@ function createBuilderImpl(state) {
9459
10080
  generate,
9460
10081
  generateBuffer: generate,
9461
10082
  generateFile,
10083
+ prepareQuality,
9462
10084
  getComponentNames,
9463
10085
  validate,
9464
10086
  generateSchema,