@openpresentation/opf-pptx 0.5.1 → 0.6.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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/index.js +71 -59
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -4,6 +4,8 @@ Pure local PowerPoint conversion tooling for Open Presentation Format documents.
4
4
 
5
5
  ## Scope
6
6
 
7
+ Version 0.6.0 requires core 0.8.0 and uses renderer 0.6.0 for coordinated preview/font measurement. Quotes export their accepted body/source lines and styles without another fitting pass. [Controlled Windows PowerPoint evidence](docs/evidence/shared-quote-integration/comparison.json) records glyph containment, separation, save/reopen and text reimport against its exact source/font hashes. Native quote import returns editable text blocks and does not restore the original OPF quote structure, typography or readability policy. Native chart geometry and general scalar-text wrapping also remain different from preview; editability and valid reimport do not establish raster equivalence.
8
+
7
9
  - Package: `@openpresentation/opf-pptx`
8
10
  - Repository: `OpenPresentation/opf-pptx`
9
11
  - License: MIT
package/dist/index.js CHANGED
@@ -907,7 +907,7 @@ async function addSlide(pptx, presentation, opfSlide, slideIndex, context, optio
907
907
  } else if (item.field === "text" && typeof item.value === "string") {
908
908
  slide.addText(item.text.lines.join("\n"), {...textBoxOptions(region, slideContext, item.text.fontSize * 0.75),fontFace:item.textStyle.fontFamily,bold:item.textStyle.fontWeight>=600,italic:item.textStyle.italic});
909
909
  } else {
910
- await addPayload(slide, presentation, item.payload, region, item.path, { ...slideContext, composition: item.composition, contentAlignment: opfSlide.design?.contentAlignment ?? presentation.design?.contentAlignment ?? "left" }, options);
910
+ await addPayload(slide, presentation, item.payload, region, item.path, { ...slideContext, composition: item.composition, contentAlignment: opfSlide.design?.contentAlignment ?? presentation.design?.contentAlignment ?? "left" }, options, item.quoteLayout);
911
911
  }
912
912
  }
913
913
 
@@ -928,7 +928,7 @@ function fieldToType(field) {
928
928
  return field === "items" || field === "bullets" ? "list" : field;
929
929
  }
930
930
 
931
- async function addPayload(slide, presentation, payload, region, path, context, options) {
931
+ async function addPayload(slide, presentation, payload, region, path, context, options, quoteLayout) {
932
932
  const kind = inferPayloadKind(payload);
933
933
  switch (kind) {
934
934
  case "text":
@@ -950,16 +950,16 @@ async function addPayload(slide, presentation, payload, region, path, context, o
950
950
  addTablePayload(slide, payload.table, region, context, options, path);
951
951
  break;
952
952
  case "code":
953
- addCodePayload(slide, payload.code, region, context);
953
+ addCodePayload(slide, payload.code, region, context, options, path);
954
954
  break;
955
955
  case "metric":
956
- addMetricPayload(slide, payload.metric, region, context);
956
+ addMetricPayload(slide, payload.metric, region, context, options, path);
957
957
  break;
958
958
  case "quote":
959
- addQuotePayload(slide, payload.quote, region, context);
959
+ addQuotePayload(slide, quoteLayout, context, options, path);
960
960
  break;
961
961
  case "timeline":
962
- addTimelinePayload(slide, payload.timeline, region, context);
962
+ addTimelinePayload(slide, payload.timeline, region, context, options, path);
963
963
  break;
964
964
  default:
965
965
  addPlaceholderPayload(slide, "Unsupported OPF payload", payload, region, context);
@@ -1088,8 +1088,11 @@ function addChartPayload(slide, chart, region, context) {
1088
1088
  chartColors: CHART_COLORS,
1089
1089
  catAxisLabelFontFace: context.fonts.body,
1090
1090
  catAxisLabelFontSize: 9,
1091
+ catAxisLabelColor: context.colors.text,
1091
1092
  valAxisLabelFontFace: context.fonts.body,
1092
1093
  valAxisLabelFontSize: 9,
1094
+ valAxisLabelColor: context.colors.text,
1095
+ legendColor: context.colors.text,
1093
1096
  showValue: false,
1094
1097
  valGridLine: { color: context.colors.border, transparency: 30, size: 1 },
1095
1098
  barDir: chartData.barDir,
@@ -1184,67 +1187,76 @@ function addTablePayload(slide, table, region, context, options, path) {
1184
1187
  });
1185
1188
  }
1186
1189
 
1187
- function addCodePayload(slide, value, region, context) {
1188
- const code = typeof value === "string" ? { source: value } : value;
1189
- const title = code?.filename ? `${code.filename}${code.language ? ` (${code.language})` : ""}` : code?.language;
1190
- const body = [title, code?.source].filter(Boolean).join("\n");
1191
- slide.addText(body, {
1192
- ...textBoxOptions(region, context, 11),
1193
- fontFace: context.fonts.code,
1194
- fill: { color: context.colors.surface },
1195
- line: { color: context.colors.border, pt: 0.75 },
1196
- margin: 8,
1197
- fit: "shrink"
1198
- });
1190
+ function pixelBox(region) {
1191
+ return {x: region.x * 96, y: region.y * 96, width: region.w * 96, height: region.h * 96};
1192
+ }
1193
+
1194
+ // Match the published renderer's payload geometry and shared core text fitting.
1195
+ // Each fitted line remains native editable text, without PowerPoint rewrapping it.
1196
+ function addMeasuredPayloadText(slide, text, box, context, options, config) {
1197
+ const scale = Math.min(context.dimensions.widthInches, context.dimensions.heightInches) * 96 / 720;
1198
+ const style = config.textStyle ?? resolveTextStyle({fontFamily: config.fontFamily ?? context.fonts.body, fontWeight: config.fontWeight ?? 400, path: config.path}, options.textMeasurement);
1199
+ const fit = config.fit ?? fitText(String(text ?? ''), box, config.fontSize * scale, (context.composition?.minFontSize ?? 16) * scale, textWidthMeasurer(style, options.textMeasurement));
1200
+ if (fit.overflow && !config.diagnosticsHandled) {
1201
+ const diagnostic = {code: 'text-overflow', path: config.path, message: 'Text exceeds its cell at the minimum font size; shorten it, increase its space, or split the slide.'};
1202
+ options.onDiagnostic?.(diagnostic);
1203
+ if (context.composition?.overflow === 'error') throw new OPFPptxError('layout-overflow', diagnostic.message, {path: config.path, issues: [diagnostic]});
1204
+ }
1205
+ for (const [index, line] of fit.lines.entries()) {
1206
+ if (!line) continue;
1207
+ slide.addText(line, {
1208
+ ...textBoxOptions({x: box.x / 96, y: (box.y + index * fit.lineHeight) / 96, w: box.width / 96, h: fit.lineHeight / 96}, context, fit.fontSize * .75),
1209
+ fontFace: style.fontFamily, bold: style.fontWeight >= 600, italic: style.italic,
1210
+ color: normalizeHex(config.color ?? context.colors.text), align: config.align ?? context.contentAlignment ?? 'left',
1211
+ fit: 'none', wrap: false, lineSpacingMultiple: 1,
1212
+ });
1213
+ }
1199
1214
  }
1200
1215
 
1201
- function addMetricPayload(slide, value, region, context) {
1202
- const metric = isPlainObject(value) ? value : { value };
1203
- slide.addText(String(metric.value ?? ""), {
1204
- x: region.x,
1205
- y: region.y,
1206
- w: region.w,
1207
- h: Math.min(region.h, 0.68),
1208
- margin: 0,
1209
- fontFace: context.fonts.heading,
1210
- fontSize: 30,
1211
- bold: true,
1212
- color: context.colors.accent,
1213
- fit: "shrink"
1214
- });
1215
- slide.addText([metric.label, metric.description, metric.delta].filter(Boolean).join("\n"), {
1216
- x: region.x,
1217
- y: region.y + 0.76,
1218
- w: region.w,
1219
- h: Math.max(0.3, region.h - 0.78),
1220
- margin: 0,
1221
- fontFace: context.fonts.body,
1222
- fontSize: 12,
1223
- color: context.colors.text,
1224
- fit: "shrink"
1216
+ function addCodePayload(slide, value, region, context, options, path) {
1217
+ const box = pixelBox(region), code = typeof value === 'string' ? value : value?.source ?? JSON.stringify(value);
1218
+ const label = typeof value === 'object' && value?.language ? String(value.language) : 'code';
1219
+ slide.addShape('rect', {...region, fill: {color: '111827'}, line: {color: '334155', pt: .75}});
1220
+ // The label has a fixed baseline in the SVG renderer; it does not participate in fitting.
1221
+ slide.addText(label.toUpperCase(), {
1222
+ ...textBoxOptions({x: (box.x + 18) / 96, y: (box.y + 14) / 96, w: Math.max(1, box.width - 36) / 96, h: 20 / 96}, context, 14 * .75),
1223
+ fontFace: context.fonts.code, bold: true, color: '93C5FD', fit: 'none', wrap: false,
1225
1224
  });
1225
+ addMeasuredPayloadText(slide, code, {x: box.x + 18, y: box.y + 46, width: box.width - 36, height: box.height - 64}, context, options, {path, fontSize: 18, fontFamily: context.fonts.code, color: 'E5E7EB'});
1226
1226
  }
1227
1227
 
1228
- function addQuotePayload(slide, value, region, context) {
1229
- const quote = typeof value === "string" ? { text: value } : value;
1230
- const attribution = quote?.attribution ? `\n- ${quote.attribution}` : "";
1231
- slide.addText(`${quote?.text ?? ""}${attribution}`, {
1232
- ...textBoxOptions(region, context, 17),
1233
- italic: true,
1234
- color: context.colors.text,
1235
- fit: "shrink"
1236
- });
1228
+ function addMetricPayload(slide, value, region, context, options, path) {
1229
+ const metric = isPlainObject(value) ? value : {value}, box = pixelBox(region);
1230
+ addMeasuredPayloadText(slide, metric.value, {...box, height: box.height * .45}, context, options, {path: path + '.value', fontSize: Math.min(76, box.height * .28), fontFamily: context.fonts.heading, fontWeight: 800, color: context.colors.accent});
1231
+ addMeasuredPayloadText(slide, [metric.label, metric.description, metric.delta].filter(Boolean).join('\n'), {x: box.x, y: box.y + box.height * .45, width: box.width, height: box.height * .55}, context, options, {path, fontSize: 23, fontWeight: 500});
1232
+ }
1233
+
1234
+ function addQuotePayload(slide, layout, context, options, path) {
1235
+ if (!layout) throw new OPFPptxError('missing-quote-layout', 'Quote export requires a coordinated core build with shared quote geometry.', {path});
1236
+ for (const part of layout.parts) {
1237
+ if (!part.fit) throw new OPFPptxError('layout-overflow', 'Quote content has no usable internal space; increase its cell size before exporting.', {path:part.path,issues:layout.diagnostics});
1238
+ addMeasuredPayloadText(slide,part.text,part.box,context,options,{
1239
+ path:part.path,fit:part.fit,textStyle:part.style,diagnosticsHandled:true,
1240
+ color:part.role==='footer'?context.colors.mutedText:context.colors.text,
1241
+ });
1242
+ }
1237
1243
  }
1238
1244
 
1239
- function addTimelinePayload(slide, value, region, context) {
1240
- const timeline = Array.isArray(value) ? { events: value } : value;
1245
+ function addTimelinePayload(slide, value, region, context, options, path) {
1246
+ const timeline = Array.isArray(value) ? {events: value} : value;
1241
1247
  const events = Array.isArray(timeline?.events) ? timeline.events : [];
1242
- const lines = events.map((event) => {
1243
- const when = event.when ? `${event.when}: ` : "";
1244
- const detail = event.description ? ` - ${event.description}` : "";
1245
- return `${when}${event.what ?? ""}${detail}`;
1246
- });
1247
- slide.addText(lines.join("\n"), textBoxOptions(region, context, 13));
1248
+ if (!events.length) return;
1249
+ const box = pixelBox(region), labelWidth = box.width / Math.max(2, events.length);
1250
+ const gap = (box.width - labelWidth) / Math.max(1, events.length - 1);
1251
+ const start = events.length === 1 ? box.x + box.width / 2 : box.x + labelWidth / 2;
1252
+ const end = events.length === 1 ? start : box.x + box.width - labelWidth / 2;
1253
+ const y = box.y + box.height * .46, radius = Math.min(9, labelWidth / 5, box.height * .04);
1254
+ slide.addShape('line', {x: start / 96, y: y / 96, w: (end - start) / 96, h: 0, line: {color: context.colors.border, pt: 3 * .75}});
1255
+ for (const [index, event] of events.entries()) {
1256
+ const x = start + index * gap;
1257
+ slide.addShape('ellipse', {x: (x - radius) / 96, y: (y - radius) / 96, w: radius * 2 / 96, h: radius * 2 / 96, fill: {color: context.colors.accent}, line: {transparency: 100}});
1258
+ addMeasuredPayloadText(slide, [event.when, event.what, event.description].filter(Boolean).join('\n'), {x: x - labelWidth / 2, y: index % 2 === 0 ? box.y : y + 24, width: labelWidth, height: box.height * .38}, context, options, {path: `${path}.events.${index}`, fontSize: 16, fontWeight: 500, align: 'center'});
1259
+ }
1248
1260
  }
1249
1261
 
1250
1262
  function addPlaceholderPayload(slide, label, value, region, context) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-pptx",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Pure local OPF to PPTX export and PPTX to OPF import tooling.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,7 +38,7 @@
38
38
  "scripts": {
39
39
  "build": "node scripts/build.mjs",
40
40
  "typecheck": "node --check src/index.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check src/image-geometry.js && node --check test/image-fit.mjs && node --check test/image-orientation.mjs && node --check test/table-layout.mjs && node --check test/table-import.mjs && node --check test/object-ids.mjs && node --check test/export-corpus.mjs && node --check test/image-media-type.mjs && node --check src/image-fallback-node.js && node --check src/image-fallback-browser.js && node --check test/webp-fallback.mjs && node --check scripts/build-browser-check.mjs && node --check test/webp-fallback-browser.js && node --check test/image-fallback-boundary.mjs && node --check src/background.js && node --check test/background.mjs && node --check src/image-import.js && node --check test/image-import.mjs && node --check test/native-background.mjs && node --check src/background-import.js && node --check test/background-inheritance.mjs && node --check test/rich-table.mjs && node --check src/table-import.js && node --check test/rich-table-import.mjs && node --check test/rich-table-import-browser.js && node --check test/table-row-sizing.mjs && node --check test/table-styles.mjs && node --check test/table-styles-browser.js && node --check test/styled-table.mjs && node --check src/table-cell-import.js && node --check test/styled-table-import.mjs && node --check test/styled-table-import-browser.js && node --check src/table-border-import.js && node --check test/table-border-styles.mjs",
41
- "test": "npm run build && node test/dependency-boundary.mjs && npm run build:browser-check && npm run test:styled-table",
41
+ "test": "npm run build && node test/dependency-boundary.mjs && npm run build:browser-check && npm run test:styled-table && node test/content-layout.mjs && node test/shared-quote.mjs",
42
42
  "validate": "node scripts/validate-package.mjs",
43
43
  "test:packed": "node test/packed-install.mjs",
44
44
  "test:compare-published": "node test/compare-published.mjs",
@@ -48,14 +48,14 @@
48
48
  "test:styled-table": "node test/styled-table.mjs && node test/styled-table-import.mjs"
49
49
  },
50
50
  "dependencies": {
51
- "@openpresentation/opf": "^0.7.0",
51
+ "@openpresentation/opf": "^0.8.0",
52
52
  "fast-xml-parser": "^5.8.0",
53
53
  "fflate": "^0.8.3",
54
54
  "jszip": "3.10.1",
55
55
  "sharp": "0.35.4"
56
56
  },
57
57
  "peerDependencies": {
58
- "@openpresentation/opf-render": "^0.5.0"
58
+ "@openpresentation/opf-render": "^0.6.0"
59
59
  },
60
60
  "peerDependenciesMeta": {
61
61
  "@openpresentation/opf-render": {
@@ -63,7 +63,7 @@
63
63
  }
64
64
  },
65
65
  "devDependencies": {
66
- "@openpresentation/opf-render": "0.5.0",
66
+ "@openpresentation/opf-render": "0.6.0",
67
67
  "esbuild": "0.28.2",
68
68
  "playwright": "1.63.0"
69
69
  },