@openpresentation/opf-pptx 0.5.1 → 0.5.2

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 +66 -57
  3. package/package.json +4 -4
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.5.2 improves native metrics, quotes, code panels, timelines and chart-label contrast, with renderer 0.5.1 compatibility and fitted quote footer separation. See [native content evidence and limitations](docs/native-content-candidate.md). Native chart geometry and general scalar-text wrapping 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
@@ -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, payload.quote, region, 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,73 @@ 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 = resolveTextStyle({fontFamily: config.fontFamily ?? context.fonts.body, fontWeight: config.fontWeight ?? 400, path: config.path}, options.textMeasurement);
1199
+ const fit = fitText(String(text ?? ''), box, config.fontSize * scale, (context.composition?.minFontSize ?? 16) * scale, textWidthMeasurer(style, options.textMeasurement));
1200
+ if (fit.overflow) {
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});
1237
1232
  }
1238
1233
 
1239
- function addTimelinePayload(slide, value, region, context) {
1240
- const timeline = Array.isArray(value) ? { events: value } : value;
1234
+ function addQuotePayload(slide, value, region, context, options, path) {
1235
+ const quote = isPlainObject(value) ? value : {text: value}, box = pixelBox(region);
1236
+ const attribution = [quote.attribution, quote.source].filter(Boolean).join(' - ');
1237
+ // Keep the footer and an 18px gap outside the body's text fitting area.
1238
+ addMeasuredPayloadText(slide, `"${quote.text ?? ''}"`, {x: box.x + 18, y: box.y + 18, width: Math.max(1, box.width - 36), height: Math.max(1, box.height - (attribution ? 94 : 36))}, context, options, {path: path + '.text', fontSize: 28, fontFamily: context.fonts.heading, fontWeight: 600});
1239
+ addMeasuredPayloadText(slide, attribution, {x: box.x + 18, y: box.y + box.height - 58, width: box.width - 36, height: 40}, context, options, {path, fontSize: 17, fontWeight: 500, color: context.colors.mutedText});
1240
+ }
1241
+
1242
+ function addTimelinePayload(slide, value, region, context, options, path) {
1243
+ const timeline = Array.isArray(value) ? {events: value} : value;
1241
1244
  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));
1245
+ if (!events.length) return;
1246
+ const box = pixelBox(region), labelWidth = box.width / Math.max(2, events.length);
1247
+ const gap = (box.width - labelWidth) / Math.max(1, events.length - 1);
1248
+ const start = events.length === 1 ? box.x + box.width / 2 : box.x + labelWidth / 2;
1249
+ const end = events.length === 1 ? start : box.x + box.width - labelWidth / 2;
1250
+ const y = box.y + box.height * .46, radius = Math.min(9, labelWidth / 5, box.height * .04);
1251
+ slide.addShape('line', {x: start / 96, y: y / 96, w: (end - start) / 96, h: 0, line: {color: context.colors.border, pt: 3 * .75}});
1252
+ for (const [index, event] of events.entries()) {
1253
+ const x = start + index * gap;
1254
+ 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}});
1255
+ 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'});
1256
+ }
1248
1257
  }
1249
1258
 
1250
1259
  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.5.2",
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",
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",
@@ -55,7 +55,7 @@
55
55
  "sharp": "0.35.4"
56
56
  },
57
57
  "peerDependencies": {
58
- "@openpresentation/opf-render": "^0.5.0"
58
+ "@openpresentation/opf-render": "^0.5.1"
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.5.1",
67
67
  "esbuild": "0.28.2",
68
68
  "playwright": "1.63.0"
69
69
  },