@openpresentation/opf-pptx 0.7.0 → 0.8.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.
- package/DEPENDENCY-NOTES.md +3 -1
- package/README.md +25 -3
- package/dist/card-provenance.js +51 -0
- package/dist/chart-workbook.js +81 -0
- package/dist/code-provenance.js +20 -10
- package/dist/furniture-provenance.js +214 -0
- package/dist/heading-provenance.js +44 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +261 -81
- package/dist/metric-provenance.js +94 -0
- package/dist/text-provenance.js +54 -0
- package/dist/timeline-provenance.js +81 -0
- package/package.json +13 -9
package/dist/index.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import {importTableFrames} from './table-import.js';
|
|
2
|
+
import {readChartCategoryHeading,writeChartCategoryHeading} from './chart-workbook.js';
|
|
2
3
|
import {attachCodeTags, codeManifest, importCodeGroups, nativeShapeParagraphs, nativeTextShapes} from './code-provenance.js';
|
|
4
|
+
import {attachMetricTags,metricManifest,importMetricGroups} from './metric-provenance.js';
|
|
5
|
+
import {attachCardTags,importCardFrames} from './card-provenance.js';
|
|
6
|
+
import {attachHeadingTags,importHeadingGroups} from './heading-provenance.js';
|
|
7
|
+
import {attachPlainTextTags,importPlainTextGroups} from './text-provenance.js';
|
|
8
|
+
import {attachTimelineTags,timelineManifest,importTimelineGroups} from './timeline-provenance.js';
|
|
9
|
+
import {attachFurnitureTags, furnitureManifest, importFurniture} from './furniture-provenance.js';
|
|
3
10
|
import {importImageOrientation} from './image-import.js';
|
|
4
11
|
import {nativeBackgroundFill} from './background.js';
|
|
5
12
|
import {importBackground} from './background-import.js';
|
|
6
13
|
import { webpToPng } from '#image-fallback';
|
|
7
14
|
import { rasterMetadata, pictureTransform, normalizeImageOrientation } from './image-geometry.js';
|
|
8
|
-
import { layoutTable, composeSlide, fitText, fitRichText, textWidthMeasurer, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle } from "@openpresentation/opf/composition";
|
|
15
|
+
import { layoutTable, composeSlide, fitText, fitRichText, textWidthMeasurer, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textColorForFill, chartColorForFill } from "@openpresentation/opf/composition";
|
|
9
16
|
import PptxGenJS from "../vendor/pptxgenjs/pptxgen.es.js";
|
|
10
17
|
import { unzipSync, zipSync } from "fflate";
|
|
11
18
|
import { XMLParser } from "fast-xml-parser";
|
|
@@ -171,7 +178,15 @@ export async function toPptx(input, options = {}) {
|
|
|
171
178
|
context.tableCells = new Map();
|
|
172
179
|
context.imagePlacements = new Map();
|
|
173
180
|
context.backgroundFills = new Map();
|
|
181
|
+
context.cardTags = new Map();
|
|
182
|
+
context.headingTags = new Map();
|
|
183
|
+
context.plainTextTags = new Map();
|
|
184
|
+
context.timelineTags = new Map();
|
|
185
|
+
context.furnitureTags = new Map();
|
|
186
|
+
context.furnitureManifests = new Map();
|
|
174
187
|
context.codeTags = new Map();
|
|
188
|
+
context.metricTags = new Map();
|
|
189
|
+
context.chartHeadings = new Map();
|
|
175
190
|
context.imageFormat = options.imageFormat ?? "compatible";
|
|
176
191
|
const pptx = new PptxGenJS();
|
|
177
192
|
configurePresentation(pptx, presentation, {...context,fonts:resolveSlideContext(presentation,presentation.slides[0],context,options).fonts});
|
|
@@ -221,8 +236,21 @@ export async function fromPptx(input, options = {}) {
|
|
|
221
236
|
if (core.author) imported.author = core.author;
|
|
222
237
|
if (dimensions) imported.design = { dimensions };
|
|
223
238
|
|
|
239
|
+
const furnitureContexts = slidePaths.map((slidePath, index) => {
|
|
240
|
+
const root = parseRequiredXml(entries, slidePath)['p:sld'];
|
|
241
|
+
if (!root) throw new OPFPptxError('invalid-pptx', `PPTX slide is not a PresentationML slide: ${slidePath}.`, {path: slidePath});
|
|
242
|
+
const tree = root['p:cSld']?.['p:spTree'], relationships = parseRelationships(entries, slidePath);
|
|
243
|
+
return {root, relationships, shapes: nativeTextShapes(tree), pictures: nativePictures(tree),
|
|
244
|
+
paragraphs: nativeShapeParagraphs(decodeText(entries[slidePath])),
|
|
245
|
+
readPicture: picture => importPicture(entries, picture, slidePath, relationships,
|
|
246
|
+
diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${index}.design`}))};
|
|
247
|
+
});
|
|
248
|
+
const furniture = importFurniture(furnitureContexts, entries, options.onDiagnostic);
|
|
249
|
+
if (Object.keys(furniture.design).length) imported.design = {...imported.design, ...furniture.design};
|
|
250
|
+
if (furniture.organization) imported.organization = furniture.organization;
|
|
251
|
+
|
|
224
252
|
for (let index = 0; index < slidePaths.length; index += 1) {
|
|
225
|
-
imported.slides.push(importSlide(entries, slidePaths[index], index, dimensions, options));
|
|
253
|
+
imported.slides.push(importSlide(entries, slidePaths[index], index, dimensions, options, furniture.slides[index], furnitureContexts[index]));
|
|
226
254
|
}
|
|
227
255
|
|
|
228
256
|
const result = validatePresentation(imported);
|
|
@@ -363,16 +391,9 @@ function dimensionsFromPresentation(presentationRoot) {
|
|
|
363
391
|
};
|
|
364
392
|
}
|
|
365
393
|
|
|
366
|
-
function importSlide(entries, slidePath, slideIndex, presentationDimensions, options) {
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
if (!slideRoot) {
|
|
370
|
-
throw new OPFPptxError("invalid-pptx", `PPTX slide is not a PresentationML slide: ${slidePath}.`, {
|
|
371
|
-
path: slidePath
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const relationships = parseRelationships(entries, slidePath);
|
|
394
|
+
function importSlide(entries, slidePath, slideIndex, presentationDimensions, options, furniture, nativeContext) {
|
|
395
|
+
const slideRoot = nativeContext.root;
|
|
396
|
+
const relationships = nativeContext.relationships;
|
|
376
397
|
const dimensions = presentationDimensions ?? DIMENSION_PRESETS.widescreen;
|
|
377
398
|
const slide = {};
|
|
378
399
|
if (slideRoot.show === "0") slide.hidden = true;
|
|
@@ -381,12 +402,20 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
381
402
|
part: (path, parser) => parseRequiredXml(entries, path, parser), relationships: path => parseRelationships(entries, path), bytes: path => entries[path]
|
|
382
403
|
}, diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${slideIndex}.design.background`}));
|
|
383
404
|
if (background) slide.design = {background};
|
|
405
|
+
if (Object.keys(furniture.design).length) slide.design = {...slide.design, ...furniture.design};
|
|
406
|
+
if (furniture.section !== undefined) slide.section = furniture.section;
|
|
384
407
|
|
|
385
|
-
const items = collectSlideItems(entries, slideRoot, slidePath, relationships, dimensions, options, slideIndex)
|
|
408
|
+
const items = collectSlideItems(entries, slideRoot, slidePath, relationships, dimensions, options, slideIndex, furniture, nativeContext)
|
|
386
409
|
.sort(comparePositionedItems);
|
|
387
|
-
|
|
410
|
+
// Complete OPF heading roles are authoritative. Nearby body lines must not
|
|
411
|
+
// fill an absent role by geometry; explicit native placeholders still apply.
|
|
412
|
+
const inferHeadings = !items.some(item => item.heading||item.sourceText);
|
|
413
|
+
const heading = field => {const index=items.findIndex(item=>item.heading===field);return index<0?null:items.splice(index,1)[0];};
|
|
414
|
+
const tagItem=heading('tag');
|
|
415
|
+
if(tagItem)slide.tag=tagItem.text;
|
|
416
|
+
const titleItem = heading('title')??takeTitleItem(items, dimensions, inferHeadings);
|
|
388
417
|
if (titleItem) slide.title = titleItem.text;
|
|
389
|
-
const subtitleItem = takeSubtitleItem(items, titleItem, dimensions);
|
|
418
|
+
const subtitleItem = heading('subtitle')??takeSubtitleItem(items, titleItem, dimensions, inferHeadings);
|
|
390
419
|
if (subtitleItem) slide.subtitle = subtitleItem.text;
|
|
391
420
|
|
|
392
421
|
const blocks = mergeAdjacentBulletShapes(items)
|
|
@@ -400,17 +429,42 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
400
429
|
return slide;
|
|
401
430
|
}
|
|
402
431
|
|
|
403
|
-
|
|
432
|
+
const nativePictures = tree => [...asArray(tree?.['p:pic']), ...asArray(tree?.['p:grpSp']).flatMap(nativePictures)];
|
|
433
|
+
|
|
434
|
+
function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensions, options, slideIndex, furniture, nativeContext) {
|
|
404
435
|
const tree = slideRoot["p:cSld"]?.["p:spTree"];
|
|
405
436
|
const items = [];
|
|
406
|
-
const shapes =
|
|
407
|
-
if (tree?.['p:grpSp']) options.onDiagnostic?.({code:'grouped-text-reflow',path:`slides.${slideIndex}`,message:'Grouped native text
|
|
408
|
-
const paragraphs =
|
|
437
|
+
const shapes = nativeContext.shapes;
|
|
438
|
+
if (tree?.['p:grpSp']) options.onDiagnostic?.({code:'grouped-text-reflow',path:`slides.${slideIndex}`,message:'Grouped native text and pictures are retained, but group transforms and unsupported group members are not reconstructed; review the reflowed OPF.'});
|
|
439
|
+
const paragraphs = nativeContext.paragraphs;
|
|
409
440
|
const code = importCodeGroups(shapes, paragraphs, relationships, entries, diagnostic => options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}.code`}));
|
|
441
|
+
const metric = importMetricGroups(shapes, paragraphs, relationships, entries, diagnostic => options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}.metric`}));
|
|
442
|
+
const cards = importCardFrames(shapes, paragraphs, relationships, entries, diagnostic => options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}`}));
|
|
443
|
+
const headings=importHeadingGroups(shapes,paragraphs,relationships,entries,diagnostic=>options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}`}));
|
|
444
|
+
const plainText=importPlainTextGroups(shapes,paragraphs,relationships,entries,diagnostic=>options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}`}));
|
|
445
|
+
const timelines=importTimelineGroups(shapes,paragraphs,relationships,entries,diagnostic=>options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}`}));
|
|
446
|
+
for(const group of [...headings.items,...plainText.items]) {
|
|
447
|
+
const item=importShape(group.shapes[0],dimensions,group.paragraphs,true);
|
|
448
|
+
if(item){
|
|
449
|
+
const bounds=group.shapes.map(shape=>shapeBounds(shape['p:spPr']?.['a:xfrm'])).filter(Boolean);
|
|
450
|
+
if(bounds.length){const x=Math.min(...bounds.map(b=>b.x)),y=Math.min(...bounds.map(b=>b.y));item.bounds={x,y,w:Math.max(...bounds.map(b=>b.x+b.w))-x,h:Math.max(...bounds.map(b=>b.y+b.h))-y};}
|
|
451
|
+
items.push({...item,heading:group.field,sourceText:!group.field});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
for(const group of timelines.items){
|
|
455
|
+
const bounds=group.shapes.map(shape=>shapeBounds(shape['p:spPr']?.['a:xfrm'])).filter(Boolean);
|
|
456
|
+
let union;for(const bound of bounds){if(!union)union={...bound};else{const x=Math.min(union.x,bound.x),y=Math.min(union.y,bound.y);union={x,y,w:Math.max(union.x+union.w,bound.x+bound.w)-x,h:Math.max(union.y+union.h,bound.y+bound.h)-y};}}
|
|
457
|
+
items.push({kind:'timeline',sourceText:true,bounds:union,payload:group.payload});
|
|
458
|
+
}
|
|
410
459
|
for (const item of code.items) items.push({kind:'code',bounds:shapeBounds(item.shape['p:spPr']?.['a:xfrm']),payload:item.payload});
|
|
460
|
+
for (const item of metric.items) items.push({kind:'metric',bounds:shapeBounds(item.shape['p:spPr']?.['a:xfrm']),payload:item.payload});
|
|
411
461
|
for (const [index,shape] of shapes.entries()) {
|
|
412
|
-
if (
|
|
413
|
-
|
|
462
|
+
if (furniture.text.has(index)) continue;
|
|
463
|
+
if (code.consumed.has(shape)||metric.consumed.has(shape)||cards.has(shape)||headings.consumed.has(shape)||plainText.consumed.has(shape)||timelines.consumed.has(shape)) continue;
|
|
464
|
+
const item = importShape(shape, dimensions, paragraphs[index], furniture.taggedText.has(index));
|
|
465
|
+
// A damaged/edited furniture group falls back to current native text,
|
|
466
|
+
// including cleared text boxes, without inventing a title or shape label.
|
|
467
|
+
if (item && furniture.taggedText.has(index)) item.sourceText = true;
|
|
414
468
|
if (item) items.push(item);
|
|
415
469
|
}
|
|
416
470
|
|
|
@@ -424,7 +478,8 @@ function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensi
|
|
|
424
478
|
if (item) items.push(item);
|
|
425
479
|
}
|
|
426
480
|
|
|
427
|
-
for (const [index, picture] of
|
|
481
|
+
for (const [index, picture] of nativeContext.pictures.entries()) {
|
|
482
|
+
if (furniture.pictures.has(index)) continue;
|
|
428
483
|
const report = diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${slideIndex}.pictures.${index}`});
|
|
429
484
|
const item = importPicture(entries, picture, slidePath, relationships, report);
|
|
430
485
|
if (item) items.push(item);
|
|
@@ -433,13 +488,13 @@ function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensi
|
|
|
433
488
|
return items;
|
|
434
489
|
}
|
|
435
490
|
|
|
436
|
-
function importShape(shape, dimensions, paragraphs = readParagraphs(shape["p:txBody"])) {
|
|
491
|
+
function importShape(shape, dimensions, paragraphs = readParagraphs(shape["p:txBody"]), allowEmptyText = false) {
|
|
437
492
|
const text = paragraphs.map((paragraph) => paragraph.text).join("\n");
|
|
438
493
|
const placeholder = shapePlaceholderType(shape);
|
|
439
494
|
const bounds = shapeBounds(shape["p:spPr"]?.["a:xfrm"]);
|
|
440
495
|
const name = scalarText(shape["p:nvSpPr"]?.["p:cNvPr"]?.name).trim();
|
|
441
496
|
|
|
442
|
-
if (text) {
|
|
497
|
+
if (text || allowEmptyText) {
|
|
443
498
|
return {
|
|
444
499
|
kind: "text",
|
|
445
500
|
text,
|
|
@@ -501,7 +556,7 @@ function importGraphicFrame(entries, frame, slidePath, relationships, importedTa
|
|
|
501
556
|
function importPicture(entries, picture, slidePath, relationships, report) {
|
|
502
557
|
const bounds = shapeBounds(picture["p:spPr"]?.["a:xfrm"]);
|
|
503
558
|
const name = scalarText(picture["p:nvPicPr"]?.["p:cNvPr"]?.name).trim();
|
|
504
|
-
const alt = scalarText(picture["p:nvPicPr"]?.["p:cNvPr"]?.descr)
|
|
559
|
+
const alt = scalarText(picture["p:nvPicPr"]?.["p:cNvPr"]?.descr);
|
|
505
560
|
const relId = picture["p:blipFill"]?.["a:blip"]?.["r:embed"];
|
|
506
561
|
const relationship = relationships.get(relId);
|
|
507
562
|
let bytes = relationship?.path ? entries[relationship.path] : null;
|
|
@@ -572,13 +627,14 @@ function shapeBounds(xfrm) {
|
|
|
572
627
|
return { x, y, w, h };
|
|
573
628
|
}
|
|
574
629
|
|
|
575
|
-
function takeTitleItem(items, dimensions) {
|
|
630
|
+
function takeTitleItem(items, dimensions, inferHeadings) {
|
|
576
631
|
const explicitIndex = items.findIndex((item) => ["title", "ctrTitle"].includes(item.placeholder));
|
|
577
632
|
if (explicitIndex >= 0) return items.splice(explicitIndex, 1)[0];
|
|
633
|
+
if (!inferHeadings) return null;
|
|
578
634
|
|
|
579
635
|
const titleLimit = dimensions.heightInches * 0.28;
|
|
580
636
|
const candidateIndex = items.findIndex((item) => {
|
|
581
|
-
if (item.kind !== "text" || !item.text || item.paragraphs.some(p=>p.bullet)) return false;
|
|
637
|
+
if (item.heading || item.kind !== "text" || !item.text || item.paragraphs.some(p=>p.bullet)) return false;
|
|
582
638
|
const y = item.bounds?.y ?? 0;
|
|
583
639
|
return y <= titleLimit && (item.maxFontSize >= 20 || /^title\b/i.test(item.name ?? ""));
|
|
584
640
|
});
|
|
@@ -586,15 +642,15 @@ function takeTitleItem(items, dimensions) {
|
|
|
586
642
|
return null;
|
|
587
643
|
}
|
|
588
644
|
|
|
589
|
-
function takeSubtitleItem(items, titleItem, dimensions) {
|
|
645
|
+
function takeSubtitleItem(items, titleItem, dimensions, inferHeadings) {
|
|
590
646
|
const explicitIndex = items.findIndex((item) => item.placeholder === "subTitle");
|
|
591
647
|
if (explicitIndex >= 0) return items.splice(explicitIndex, 1)[0];
|
|
592
|
-
if (!titleItem) return null;
|
|
648
|
+
if (!inferHeadings || !titleItem) return null;
|
|
593
649
|
|
|
594
650
|
const titleBottom = (titleItem.bounds?.y ?? 0) + (titleItem.bounds?.h ?? 0);
|
|
595
651
|
const subtitleLimit = Math.min(dimensions.heightInches * 0.34, 1.45);
|
|
596
652
|
const candidateIndex = items.findIndex((item) => {
|
|
597
|
-
if (item.kind !== "text" || !item.text || item.paragraphs.some(p=>p.bullet)) return false;
|
|
653
|
+
if (item.heading || item.kind !== "text" || !item.text || item.paragraphs.some(p=>p.bullet)) return false;
|
|
598
654
|
const y = item.bounds?.y ?? 0;
|
|
599
655
|
const h = item.bounds?.h ?? 0;
|
|
600
656
|
return y >= titleBottom - 0.05
|
|
@@ -674,7 +730,7 @@ function chartFromRelationship(entries, slidePath, relationships, relId) {
|
|
|
674
730
|
return {
|
|
675
731
|
type: chartNode.type,
|
|
676
732
|
data: {
|
|
677
|
-
columns: ["Category", ...names],
|
|
733
|
+
columns: [readChartCategoryHeading(entries,relationship.path) ?? "Category", ...names],
|
|
678
734
|
rows
|
|
679
735
|
}
|
|
680
736
|
};
|
|
@@ -888,31 +944,48 @@ async function addSlide(pptx, presentation, opfSlide, slideIndex, context, optio
|
|
|
888
944
|
const { widthInches, heightInches } = slideContext.dimensions;
|
|
889
945
|
const layout = resolveCatalogRecord(presentation, "layouts", opfSlide.layout, "blank") ?? {};
|
|
890
946
|
if (opfSlide.layout && layout.id !== opfSlide.layout) throw new OPFPptxError("catalog-resolution-failed", `Layout '${opfSlide.layout}' needs an inline or bundled catalog record.`, { path: `slides.${slideIndex}.layout` });
|
|
891
|
-
const geometry = composeSlide(opfSlide, { width: widthInches * 96, height: heightInches * 96, layout, slideIndex, fonts: slideContext.fonts, textMeasurement: options.textMeasurement });
|
|
947
|
+
const geometry = composeSlide(opfSlide, { width: widthInches * 96, height: heightInches * 96, layout, presentation, slideIndex, fonts: slideContext.fonts, contentAlignment:opfSlide.design?.contentAlignment??presentation.design?.contentAlignment, titleAlignment:opfSlide.design?.titleAlignment??presentation.design?.titleAlignment, textRasterPadding:options.textRasterPadding, contentBox:opfSlide.design?.contentBox??presentation.design?.contentBox, textMeasurement: options.textMeasurement });
|
|
892
948
|
for (const diagnostic of geometry.diagnostics) options.onDiagnostic?.(diagnostic);
|
|
949
|
+
await addFurniture(slide,presentation,opfSlide,geometry.furniture,slideContext,options,slideIndex);
|
|
893
950
|
for (const item of geometry.items) {
|
|
894
951
|
const region = { x: item.box.x / 96, y: item.box.y / 96, w: item.box.width / 96, h: item.box.height / 96 };
|
|
895
|
-
if (
|
|
952
|
+
if (item.frameBox) {
|
|
953
|
+
const frame=item.frameBox;
|
|
954
|
+
context.cardTags.set(`OPF card ${item.path}`,item.path);
|
|
955
|
+
const paint=value=>({color:normalizeHex(value),transparency:/^#[0-9a-f]{8}$/i.test(value)?(1-parseInt(value.slice(7),16)/255)*100:0});
|
|
956
|
+
const surface=slideContext.colorScheme[isDarkHex(slideContext.colors.background)?'dark2':'light2']??`#${slideContext.colors.surface}`;
|
|
957
|
+
slide.addShape('roundRect',{x:frame.x/96,y:frame.y/96,w:frame.width/96,h:frame.height/96,
|
|
958
|
+
rectRadius:8*Math.min(widthInches,heightInches)/720,
|
|
959
|
+
fill:paint(surface),line:{...paint(slideContext.colorScheme.accent5??`#${slideContext.colors.border}`),pt:.75},objectName:`OPF card ${item.path}`});
|
|
960
|
+
}
|
|
961
|
+
if(['text','title','subtitle','tag'].includes(item.field)&&(item.text?.placement||item.text?.sourceLines)&&!item.text.richLines) {
|
|
962
|
+
addMeasuredPayloadText(slide,item.value,item.box,slideContext,options,{path:item.path,fit:item.text,textStyle:item.textStyle,sourceText:item.field==='text'&&!!item.text.sourceLines,diagnosticsHandled:true,heading:['title','subtitle','tag'].includes(item.field)?item.field:undefined,color:item.field==='tag'?slideContext.colors.accent:slideContext.colors.text});
|
|
963
|
+
} else if (["title", "subtitle", "tag"].includes(item.field)) {
|
|
964
|
+
// Estimated-font headings use one native text box, which still needs a
|
|
965
|
+
// role tag. Role recovery must not depend on outline measurement support.
|
|
966
|
+
const objectName = `OPF heading ${item.path} line 0`;
|
|
967
|
+
context.headingTags.set(objectName,{v:1,group:item.path,field:item.field,line:0,count:1});
|
|
896
968
|
slide.addText(item.text.lines.join("\n"), {
|
|
897
969
|
...textBoxOptions(region, slideContext, item.text.fontSize * 0.75),
|
|
898
|
-
|
|
899
|
-
bold: item.textStyle.fontWeight >= 600,
|
|
900
|
-
italic: item.textStyle.italic,
|
|
970
|
+
...nativeFontOptions(item.textStyle),
|
|
901
971
|
color: item.field === "tag" ? slideContext.colors.accent : slideContext.colors.text,
|
|
972
|
+
objectName,
|
|
902
973
|
breakLine: false
|
|
903
974
|
});
|
|
904
975
|
} else if ((item.field === "items" || item.field === "bullets") && item.text?.listEntries) {
|
|
905
976
|
addMeasuredList(slide,item.text,slideContext);
|
|
906
977
|
} else if (item.field === "text" && item.text?.richLines) {
|
|
907
|
-
const alignment=opfSlide.design?.contentAlignment??presentation.design?.contentAlignment??'left';
|
|
908
|
-
for(const line of item.text.richLines){
|
|
909
|
-
const runs=line.fragments.map(fragment=>({text:fragment.text,options:{
|
|
910
|
-
|
|
978
|
+
const alignment=item.text.placement?.alignment??opfSlide.design?.contentAlignment??presentation.design?.contentAlignment??'left';
|
|
979
|
+
for(const [index,line] of item.text.richLines.entries()){
|
|
980
|
+
const runs=line.fragments.map(fragment=>({text:fragment.text,options:{...nativeFontOptions(fragment.style),fontSize:fragment.fontSize*.75,color:normalizeHex(fragment.run.color??slideContext.colors.text),underline:fragment.run.underline?{color:normalizeHex(fragment.run.color??slideContext.colors.text)}:undefined,strike:fragment.run.strikethrough?'sngStrike':undefined,baseline:fragment.baselineShift?-fragment.baselineShift/fragment.fontSize*2000:undefined,hyperlink:fragment.run.link&&/^(https?:|mailto:)/i.test(fragment.run.link)?{url:fragment.run.link}:undefined}}));
|
|
981
|
+
const placed=item.text.placement?.lines[index],factor=alignment==='right'?1:alignment==='center'?.5:0;
|
|
982
|
+
const area=placed?{...region,x:(placed.x+line.width*factor-item.box.width*factor)/96,y:placed.y/96,h:placed.height/96}:{...region,y:region.y+line.y/96,h:line.height/96};
|
|
983
|
+
if(runs.length)slide.addText(runs,{...textBoxOptions(area,slideContext,item.text.fontSize*.75),align:alignment,fit:'none',wrap:false,lineSpacingMultiple:1});
|
|
911
984
|
}
|
|
912
985
|
} else if (item.field === "text" && typeof item.value === "string") {
|
|
913
|
-
slide.addText(item.text.lines.join("\n"), {...textBoxOptions(region, slideContext, item.text.fontSize * 0.75)
|
|
986
|
+
slide.addText(item.text.lines.join("\n"), {...textBoxOptions(region, slideContext, item.text.fontSize * 0.75),...nativeFontOptions(item.textStyle)});
|
|
914
987
|
} else {
|
|
915
|
-
await addPayload(slide, presentation, item.payload, region, item.path, { ...slideContext, composition: item.composition, contentAlignment: opfSlide.design?.contentAlignment ?? presentation.design?.contentAlignment ?? "left" }, options, item.quoteLayout, item.codeLayout);
|
|
988
|
+
await addPayload(slide, presentation, item.payload, region, item.path, { ...slideContext, composition: item.composition, contentAlignment: opfSlide.design?.contentAlignment ?? presentation.design?.contentAlignment ?? "left" }, options, item.quoteLayout, item.codeLayout,item.metricLayout,item.timelineLayout);
|
|
916
989
|
}
|
|
917
990
|
}
|
|
918
991
|
|
|
@@ -933,7 +1006,7 @@ function fieldToType(field) {
|
|
|
933
1006
|
return field === "items" || field === "bullets" ? "list" : field;
|
|
934
1007
|
}
|
|
935
1008
|
|
|
936
|
-
async function addPayload(slide, presentation, payload, region, path, context, options, quoteLayout, codeLayout) {
|
|
1009
|
+
async function addPayload(slide, presentation, payload, region, path, context, options, quoteLayout, codeLayout,metricLayout,timelineLayout) {
|
|
937
1010
|
const kind = inferPayloadKind(payload);
|
|
938
1011
|
switch (kind) {
|
|
939
1012
|
case "text":
|
|
@@ -958,13 +1031,13 @@ async function addPayload(slide, presentation, payload, region, path, context, o
|
|
|
958
1031
|
addCodePayload(slide, payload.code, codeLayout, region, context, path);
|
|
959
1032
|
break;
|
|
960
1033
|
case "metric":
|
|
961
|
-
addMetricPayload(slide, payload.metric,
|
|
1034
|
+
addMetricPayload(slide, payload.metric,metricLayout,context,path);
|
|
962
1035
|
break;
|
|
963
1036
|
case "quote":
|
|
964
1037
|
addQuotePayload(slide, quoteLayout, context, options, path);
|
|
965
1038
|
break;
|
|
966
1039
|
case "timeline":
|
|
967
|
-
addTimelinePayload(slide, payload.timeline,
|
|
1040
|
+
addTimelinePayload(slide, payload.timeline, timelineLayout, context, options, path);
|
|
968
1041
|
break;
|
|
969
1042
|
default:
|
|
970
1043
|
addPlaceholderPayload(slide, "Unsupported OPF payload", payload, region, context);
|
|
@@ -994,7 +1067,7 @@ function addTextPayload(slide, value, region, context) {
|
|
|
994
1067
|
}
|
|
995
1068
|
|
|
996
1069
|
function richLineRuns(line,color) {
|
|
997
|
-
return line.fragments.map(fragment=>({text:fragment.text,options:{
|
|
1070
|
+
return line.fragments.map(fragment=>({text:fragment.text,options:{...nativeFontOptions(fragment.style),fontSize:fragment.fontSize*.75,color:normalizeHex(fragment.run.color??color),underline:fragment.run.underline?{color:normalizeHex(fragment.run.color??color)}:undefined,strike:fragment.run.strikethrough?'sngStrike':undefined,baseline:fragment.baselineShift?-fragment.baselineShift/fragment.fontSize*2000:undefined,hyperlink:fragment.run.link&&/^(https?:|mailto:)/i.test(fragment.run.link)?{url:fragment.run.link}:undefined}}));
|
|
998
1071
|
}
|
|
999
1072
|
function addMeasuredList(slide,fit,context) {
|
|
1000
1073
|
for(const entry of fit.listEntries){
|
|
@@ -1010,7 +1083,7 @@ function addMeasuredList(slide,fit,context) {
|
|
|
1010
1083
|
// Keep paragraph intent identical across runs. ZIP normalization below
|
|
1011
1084
|
// removes the duplicate paragraph-property nodes emitted by PptxGenJS.
|
|
1012
1085
|
for(const run of runs)Object.assign(run.options,paragraph);
|
|
1013
|
-
slide.addText(runs,{...textBoxOptions(region,context,text.fontSize*.75),fontFace:entry.marker.style.
|
|
1086
|
+
slide.addText(runs,{...textBoxOptions(region,context,text.fontSize*.75),fontFace:nativeFontOptions(entry.marker.style).fontFace,objectName,align:'left',fit:'none',wrap:false,lineSpacingMultiple:1,...paragraph});
|
|
1014
1087
|
});
|
|
1015
1088
|
};
|
|
1016
1089
|
addLines(entry.text,entry.textBox,context.colors.text,true);
|
|
@@ -1074,6 +1147,7 @@ async function addImagePayload(slide, presentation, asset, region, path, context
|
|
|
1074
1147
|
h: region.h,
|
|
1075
1148
|
altText: assetAlt(asset, presentation)
|
|
1076
1149
|
});
|
|
1150
|
+
return objectName;
|
|
1077
1151
|
}
|
|
1078
1152
|
|
|
1079
1153
|
function addChartPayload(slide, chart, region, context) {
|
|
@@ -1083,21 +1157,36 @@ function addChartPayload(slide, chart, region, context) {
|
|
|
1083
1157
|
return;
|
|
1084
1158
|
}
|
|
1085
1159
|
|
|
1160
|
+
// Keep the resolved palette surface (including alpha) explicit in native
|
|
1161
|
+
// chart/plot areas, so inherited labels are assessed against their own panel.
|
|
1162
|
+
const panelFill = context.colorScheme.surface ?? context.colorScheme[isDarkHex(context.colors.background) ? 'dark2' : 'light2'] ?? `#${context.colors.surface}`;
|
|
1163
|
+
const labelColor = normalizeHex(textColorForFill(panelFill, `#${context.colors.text}`));
|
|
1164
|
+
const transparency = /^#[0-9a-f]{8}$/i.test(panelFill) ? (1 - parseInt(panelFill.slice(7), 16) / 255) * 100 : 0;
|
|
1165
|
+
const fill = {color:normalizeHex(panelFill),transparency};
|
|
1166
|
+
const objectName = `OPF chart ${context.chartHeadings.size + 1}`;
|
|
1167
|
+
const circular = chartData.type === 'pie' || chartData.type === 'doughnut';
|
|
1168
|
+
context.chartHeadings.set(objectName,{heading:chartData.type === 'scatter' ? undefined : chart.data.columns[0],labelColor});
|
|
1086
1169
|
slide.addChart(chartData.type, chartData.series, {
|
|
1170
|
+
objectName,
|
|
1087
1171
|
x: region.x,
|
|
1088
1172
|
y: region.y,
|
|
1089
1173
|
w: region.w,
|
|
1090
1174
|
h: region.h,
|
|
1091
|
-
showLegend: chartData.series.length > 1,
|
|
1175
|
+
showLegend: circular || chartData.series.length > 1,
|
|
1092
1176
|
showTitle: false,
|
|
1093
|
-
chartColors: CHART_COLORS,
|
|
1177
|
+
chartColors: CHART_COLORS.map(color=>normalizeHex(chartColorForFill(panelFill,`#${color}`))),
|
|
1178
|
+
chartArea: {fill:{...fill},roundedCorners:false},
|
|
1179
|
+
// Paint alpha once in the chart area, rather than stacking two alpha fills.
|
|
1180
|
+
plotArea: {fill:{color:null}},
|
|
1094
1181
|
catAxisLabelFontFace: context.fonts.body,
|
|
1095
1182
|
catAxisLabelFontSize: 9,
|
|
1096
|
-
catAxisLabelColor:
|
|
1183
|
+
catAxisLabelColor: labelColor,
|
|
1097
1184
|
valAxisLabelFontFace: context.fonts.body,
|
|
1098
1185
|
valAxisLabelFontSize: 9,
|
|
1099
|
-
valAxisLabelColor:
|
|
1100
|
-
legendColor:
|
|
1186
|
+
valAxisLabelColor: labelColor,
|
|
1187
|
+
legendColor: labelColor,
|
|
1188
|
+
legendFontFace: context.fonts.body,
|
|
1189
|
+
dataLabelColor: labelColor,
|
|
1101
1190
|
showValue: false,
|
|
1102
1191
|
valGridLine: { color: context.colors.border, transparency: 30, size: 1 },
|
|
1103
1192
|
barDir: chartData.barDir,
|
|
@@ -1121,8 +1210,8 @@ function addTablePayload(slide, table, region, context, options, path) {
|
|
|
1121
1210
|
const rows = layout.rows.map(row => row.cells.map(cell => {
|
|
1122
1211
|
const {header,rich,fit} = cell;
|
|
1123
1212
|
const cellStyle = cell.style ?? {};
|
|
1124
|
-
const baseColor = (cellStyle.color ?? (header ? '#FFFFFF' : '#' + context.colors.text)).replace(/^#/, '');
|
|
1125
1213
|
const baseFill = (cellStyle.fill ?? (header ? '#' + context.colors.accent : '#' + context.colors.surface)).replace(/^#/, '');
|
|
1214
|
+
const baseColor = (cellStyle.color ?? textColorForFill('#' + baseFill, header ? '#FFFFFF' : '#' + context.colors.text)).replace(/^#/, '');
|
|
1126
1215
|
const alpha = value => value.length === 8 ? (1 - parseInt(value.slice(6), 16) / 255) * 100 : 0;
|
|
1127
1216
|
const text = stringifyText(cell.value), style = cell.textStyle;
|
|
1128
1217
|
const fragments = rich ? fit.richLines.flatMap(line => line.fragments) : [];
|
|
@@ -1133,8 +1222,7 @@ function addTablePayload(slide, table, region, context, options, path) {
|
|
|
1133
1222
|
const rawColor = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(run.color ?? '') ? run.color.slice(1) : baseColor;
|
|
1134
1223
|
const color = normalizeHex(rawColor), transparency = rawColor.length === 8 ? (1 - parseInt(rawColor.slice(6), 16) / 255) * 100 : 0;
|
|
1135
1224
|
const runOptions = {
|
|
1136
|
-
|
|
1137
|
-
bold:runStyle.fontWeight >= 600,italic:runStyle.italic,
|
|
1225
|
+
...nativeFontOptions(runStyle),fontSize:fragment ? fragment.fontSize * .75 : fit.fontSize * .75,
|
|
1138
1226
|
underline:run.underline ? {color} : undefined,strike:run.strikethrough ? 'sngStrike' : undefined,
|
|
1139
1227
|
color,transparency,baseline:fragment?.baselineShift ? -fragment.baselineShift / fragment.fontSize * 2000 : undefined,
|
|
1140
1228
|
hyperlink:run.link && /^(https?:|mailto:)/i.test(run.link) ? {url:run.link} : undefined,
|
|
@@ -1150,13 +1238,12 @@ function addTablePayload(slide, table, region, context, options, path) {
|
|
|
1150
1238
|
// soft wraps into the text would change a later import or copy operation.
|
|
1151
1239
|
text: rich ? runs : text,
|
|
1152
1240
|
options: {
|
|
1153
|
-
|
|
1241
|
+
...nativeFontOptions(style),
|
|
1154
1242
|
fontSize: fit.fontSize * 0.75,
|
|
1155
1243
|
// PptxGenJS fills falsy run options from cell defaults. Rich runs
|
|
1156
1244
|
// carry their resolved weight, so a bold header default must not turn
|
|
1157
1245
|
// an explicit bold:false run back on.
|
|
1158
|
-
|
|
1159
|
-
italic: rich ? false : style.italic,
|
|
1246
|
+
...(rich ? {bold:false,italic:false} : {}),
|
|
1160
1247
|
lineSpacing: fit.lineHeight * 0.75,
|
|
1161
1248
|
paraSpaceAfter: 0,
|
|
1162
1249
|
align: cellStyle.align ?? context.contentAlignment,
|
|
@@ -1199,6 +1286,8 @@ function pixelBox(region) {
|
|
|
1199
1286
|
// Match the published renderer's payload geometry and shared core text fitting.
|
|
1200
1287
|
// Each fitted line remains native editable text, without PowerPoint rewrapping it.
|
|
1201
1288
|
function addMeasuredPayloadText(slide, text, box, context, options, config) {
|
|
1289
|
+
const invalid=/[\u0000-\u0008\u000B\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/u.exec(String(text??''));
|
|
1290
|
+
if(invalid)throw new OPFPptxError('invalid-text',`Text contains U+${invalid[0].codePointAt(0).toString(16).toUpperCase().padStart(4,'0')} at UTF-16 offset ${invalid.index}, which DrawingML XML cannot represent.`,{path:config.path});
|
|
1202
1291
|
const scale = Math.min(context.dimensions.widthInches, context.dimensions.heightInches) * 96 / 720;
|
|
1203
1292
|
const style = config.textStyle ?? resolveTextStyle({fontFamily: config.fontFamily ?? context.fonts.body, fontWeight: config.fontWeight ?? 400, path: config.path}, options.textMeasurement);
|
|
1204
1293
|
const fit = config.fit ?? fitText(String(text ?? ''), box, config.fontSize * scale, (context.composition?.minFontSize ?? 16) * scale, textWidthMeasurer(style, options.textMeasurement));
|
|
@@ -1208,16 +1297,47 @@ function addMeasuredPayloadText(slide, text, box, context, options, config) {
|
|
|
1208
1297
|
if (context.composition?.overflow === 'error') throw new OPFPptxError('layout-overflow', diagnostic.message, {path: config.path, issues: [diagnostic]});
|
|
1209
1298
|
}
|
|
1210
1299
|
for (const [index, line] of fit.lines.entries()) {
|
|
1211
|
-
if (!line) continue;
|
|
1300
|
+
if (!line&&!config.heading&&!config.sourceText&&!config.timeline&&!config.keepEmpty) continue;
|
|
1301
|
+
const alignment=fit.placement?.alignment??config.align??context.contentAlignment??'left',placed=fit.placement?.lines[index],factor=alignment==='right'?1:alignment==='center'?.5:0;
|
|
1302
|
+
const sourceLine=fit.sourceLines?.[index],boundary=sourceLine?{boundary:sourceLine.boundary,separator:String(text).slice(sourceLine.end,sourceLine.nextStart)}:{};
|
|
1303
|
+
const objectName=config.heading?`OPF heading ${config.path} line ${index}`:config.timeline?`OPF timeline ${config.timeline.group} part ${config.timeline.part} line ${index}`:config.sourceText?`OPF text ${config.path} line ${index}`:config.objectName?`${config.objectName} line ${index}`:undefined;
|
|
1304
|
+
if(config.heading)context.headingTags.set(objectName,{v:1,group:config.path,field:config.heading,line:index,count:fit.lines.length,...boundary});
|
|
1305
|
+
else if(config.timeline)context.timelineTags.set(objectName,{v:1,role:'text',group:config.timeline.group,part:config.timeline.part,line:index,count:fit.lines.length,...boundary,...(config.timeline.part===0&&index===0?{anchor:config.timeline.anchor}:{})});
|
|
1306
|
+
else if(config.furniture)context.furnitureTags.set(objectName,{v:1,role:'text',...config.furniture,line:index,count:fit.lines.length,...boundary});
|
|
1307
|
+
else if(config.sourceText)context.plainTextTags.set(objectName,{v:1,group:config.path,line:index,count:fit.lines.length,...boundary});
|
|
1308
|
+
const area=placed?{x:(placed.x+placed.width*factor-box.width*factor)/96,y:(placed.baseline-fit.fontSize)/96,w:box.width/96,h:placed.height/96}:{x:box.x/96,y:(box.y+index*fit.lineHeight)/96,w:box.width/96,h:fit.lineHeight/96};
|
|
1212
1309
|
slide.addText(line, {
|
|
1213
|
-
...textBoxOptions(
|
|
1214
|
-
|
|
1215
|
-
color: normalizeHex(config.color ?? context.colors.text), align:
|
|
1310
|
+
...textBoxOptions(area, context, fit.fontSize * .75),
|
|
1311
|
+
...nativeFontOptions(style),
|
|
1312
|
+
color: normalizeHex(config.color ?? context.colors.text), align: alignment,
|
|
1313
|
+
tabStops:sourceLine?.segments.filter(segment=>segment.kind==='tab').map(segment=>({position:(segment.x+segment.width)/96,alignment:'l'})),
|
|
1314
|
+
objectName,
|
|
1216
1315
|
fit: 'none', wrap: false, lineSpacingMultiple: 1,
|
|
1217
1316
|
});
|
|
1218
1317
|
}
|
|
1219
1318
|
}
|
|
1220
1319
|
|
|
1320
|
+
async function addFurniture(slide,presentation,source,layout,context,options,slideIndex) {
|
|
1321
|
+
if(!layout){
|
|
1322
|
+
if(['header','footer'].some(kind=>(source.design?.[kind]??presentation.design?.[kind])))throw new OPFPptxError('missing-furniture-layout','Header/footer export requires coordinated core furniture geometry.',{path:`slides.${slideIndex}.design`});
|
|
1323
|
+
const manifest = furnitureManifest(presentation, source, {parts: []}, slideIndex);
|
|
1324
|
+
if (manifest) context.furnitureManifests.set(`ppt/slides/slide${slideIndex + 1}.xml`, manifest);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
for(const [index,part]of layout.parts.entries()){
|
|
1328
|
+
if(part.type==='image'){
|
|
1329
|
+
const region={x:part.box.x/96,y:part.box.y/96,w:part.box.width/96,h:part.box.height/96};
|
|
1330
|
+
const objectName = await addImagePayload(slide,presentation,part.image,region,part.path,{...context,imageFill:'fit'},options);
|
|
1331
|
+
if (objectName) context.furnitureTags.set(objectName, {v:1, role:'image', group:String(slideIndex), part:index});
|
|
1332
|
+
}else{
|
|
1333
|
+
if(!part.fit?.sourceLines)throw new OPFPptxError('missing-furniture-layout','Repeated text requires accepted source lines from core.',{path:part.path});
|
|
1334
|
+
addMeasuredPayloadText(slide,part.text,part.box,context,options,{path:part.path,fit:part.fit,textStyle:part.style,align:part.alignment,diagnosticsHandled:true,color:context.colors.mutedText,keepEmpty:true,objectName:`OPF furniture ${slideIndex} part ${index}`,furniture:{group:String(slideIndex),part:index}});
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
const manifest = furnitureManifest(presentation, source, layout, slideIndex);
|
|
1338
|
+
if (manifest) context.furnitureManifests.set(`ppt/slides/slide${slideIndex + 1}.xml`, manifest);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1221
1341
|
function addCodePayload(slide, value, layout, region, context, path) {
|
|
1222
1342
|
if (!layout) throw new OPFPptxError('missing-code-layout', 'Code export requires a coordinated core build with shared code geometry.', {path});
|
|
1223
1343
|
for (const part of layout.parts) {
|
|
@@ -1238,7 +1358,7 @@ function addCodePayload(slide, value, layout, region, context, path) {
|
|
|
1238
1358
|
context.codeTags.set(objectName,{v:1,group,role:'line',part:partIndex,line:index});
|
|
1239
1359
|
slide.addText(part.text.slice(line.start,line.end),{
|
|
1240
1360
|
...textBoxOptions({x:part.box.x/96,y:(part.box.y+index*part.fit.lineHeight)/96,w:part.box.width/96,h:part.fit.lineHeight/96},context,part.fit.fontSize*.75),
|
|
1241
|
-
|
|
1361
|
+
...nativeFontOptions(part.style),
|
|
1242
1362
|
color:part.role==='body'?'E5E7EB':'93C5FD',align:'left',fit:'none',wrap:false,lineSpacingMultiple:1,
|
|
1243
1363
|
tabStops:tabStops.length?tabStops:undefined,objectName,
|
|
1244
1364
|
});
|
|
@@ -1246,10 +1366,31 @@ function addCodePayload(slide, value, layout, region, context, path) {
|
|
|
1246
1366
|
}
|
|
1247
1367
|
}
|
|
1248
1368
|
|
|
1249
|
-
function addMetricPayload(slide,
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1369
|
+
function addMetricPayload(slide,value,layout,context,path) {
|
|
1370
|
+
if (!layout) throw new OPFPptxError('missing-metric-layout','Metric export requires coordinated core metric geometry.',{path});
|
|
1371
|
+
const group=String(context.metricTags.size+1),manifest=metricManifest(value,layout,group);
|
|
1372
|
+
for (const [partIndex,part] of layout.parts.entries()) {
|
|
1373
|
+
const invalid=/[\u0000-\u0008\u000B\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/u.exec(part.text);
|
|
1374
|
+
if (invalid) throw new OPFPptxError('invalid-metric-text',`Metric text contains U+${invalid[0].codePointAt(0).toString(16).toUpperCase().padStart(4,'0')} at UTF-16 offset ${invalid.index}, which XML cannot represent; edit that character before exporting.`,{path:part.path});
|
|
1375
|
+
if (!part.visible) continue;
|
|
1376
|
+
if (!part.fit||part.linePositions?.length!==part.fit.sourceLines.length) throw new OPFPptxError('layout-overflow','Metric content has no accepted internal line positions; increase its cell size or coordinate package versions.',{path:part.path,issues:layout.diagnostics});
|
|
1377
|
+
for (const [index,line] of part.fit.sourceLines.entries()) {
|
|
1378
|
+
const origin=part.linePositions[index],objectName=`OPF metric ${group} ${part.role} line ${index+1}`;
|
|
1379
|
+
context.metricTags.set(objectName,partIndex===0&&index===0?manifest:{v:1,group,role:'line',part:partIndex,line:index});
|
|
1380
|
+
const tabStops=line.segments.filter(segment=>segment.kind==='tab').map(segment=>({position:(segment.x+segment.width)/96,alignment:'l'}));
|
|
1381
|
+
// Anchor the native paragraph to the same accepted left/center/right point.
|
|
1382
|
+
// Left-aligning at a measured glyph origin loses the intended edge when the
|
|
1383
|
+
// native shaper has a slightly different advance. No re-fitting is needed.
|
|
1384
|
+
const factor=layout.alignment==='right'?1:layout.alignment==='center'?.5:0;
|
|
1385
|
+
const anchor=origin.x+line.width*factor;
|
|
1386
|
+
slide.addText(part.text.slice(line.start,line.end),{
|
|
1387
|
+
...textBoxOptions({x:(anchor-part.box.width*factor)/96,y:(origin.baseline-part.fit.fontSize)/96,w:part.box.width/96,h:part.fit.lineHeight/96},context,part.fit.fontSize*.75),
|
|
1388
|
+
...nativeFontOptions(part.style),
|
|
1389
|
+
color:part.role==='value'?context.colors.accent:context.colors.text,align:layout.alignment,fit:'none',wrap:false,lineSpacingMultiple:1,
|
|
1390
|
+
tabStops:tabStops.length?tabStops:undefined,objectName,
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1253
1394
|
}
|
|
1254
1395
|
|
|
1255
1396
|
function addQuotePayload(slide, layout, context, options, path) {
|
|
@@ -1263,20 +1404,23 @@ function addQuotePayload(slide, layout, context, options, path) {
|
|
|
1263
1404
|
}
|
|
1264
1405
|
}
|
|
1265
1406
|
|
|
1266
|
-
function addTimelinePayload(slide, value,
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
const
|
|
1271
|
-
const
|
|
1272
|
-
const
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
for
|
|
1277
|
-
const
|
|
1278
|
-
slide.addShape('ellipse',
|
|
1279
|
-
|
|
1407
|
+
function addTimelinePayload(slide, value, layout, context, options, path) {
|
|
1408
|
+
if(!layout)throw new OPFPptxError('missing-timeline-layout','Timeline export requires a coordinated core build with shared timeline geometry.',{path});
|
|
1409
|
+
// Validate optional field fits before the provenance manifest dereferences
|
|
1410
|
+
// source lines or any drawing/tag mutation is performed.
|
|
1411
|
+
for(const part of layout.parts)if(!part.fit)throw new OPFPptxError('layout-overflow','Timeline field has no usable space; change the arrangement or paginate events.',{path:part.path,issues:layout.diagnostics});
|
|
1412
|
+
const scale=Math.min(context.dimensions.widthInches,context.dimensions.heightInches)*96/720;
|
|
1413
|
+
const group=String(context.timelineTags.size),anchor=timelineManifest(value,layout),connectorName=`OPF timeline ${group} connector`;
|
|
1414
|
+
const {x1,y1,x2,y2}=layout.connector;
|
|
1415
|
+
slide.addShape('line',{objectName:connectorName,x:x1/96,y:y1/96,w:(x2-x1)/96,h:(y2-y1)/96,line:{color:context.colors.border,pt:3*scale*.75}});
|
|
1416
|
+
context.timelineTags.set(connectorName,{v:1,group,role:'connector'});
|
|
1417
|
+
for(const marker of layout.markers){
|
|
1418
|
+
const objectName=`OPF timeline ${group} marker ${marker.eventIndex}`;
|
|
1419
|
+
slide.addShape('ellipse',{objectName,x:(marker.x-marker.radius)/96,y:(marker.y-marker.radius)/96,w:marker.radius*2/96,h:marker.radius*2/96,fill:{color:context.colors.accent},line:{transparency:100}});
|
|
1420
|
+
context.timelineTags.set(objectName,{v:1,group,role:'marker',eventIndex:marker.eventIndex});
|
|
1421
|
+
}
|
|
1422
|
+
for(const [index,part]of layout.parts.entries()){
|
|
1423
|
+
addMeasuredPayloadText(slide,part.text,part.box,context,options,{path:part.path,fit:part.fit,textStyle:part.style,align:part.alignment,diagnosticsHandled:true,timeline:{group,part:index,anchor}});
|
|
1280
1424
|
}
|
|
1281
1425
|
}
|
|
1282
1426
|
|
|
@@ -1304,6 +1448,16 @@ function addPlaceholderPayload(slide, label, value, region, context) {
|
|
|
1304
1448
|
});
|
|
1305
1449
|
}
|
|
1306
1450
|
|
|
1451
|
+
function nativeFontOptions(style) {
|
|
1452
|
+
const face=style.fontFace;
|
|
1453
|
+
if(face!==undefined) {
|
|
1454
|
+
if(!face || typeof face.family!=='string' || !face.family.trim() || typeof face.bold!=='boolean' || typeof face.italic!=='boolean')
|
|
1455
|
+
throw new OPFPptxError('invalid-font-selection','The font provider must supply a family and explicit bold/italic style-link flags.',{path:style.path});
|
|
1456
|
+
return {fontFace:face.family,bold:face.bold,italic:face.italic};
|
|
1457
|
+
}
|
|
1458
|
+
return {fontFace:style.fontFamily,bold:style.fontWeight>=600,italic:style.italic};
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1307
1461
|
function textBoxOptions(region, context, fontSize) {
|
|
1308
1462
|
return {
|
|
1309
1463
|
x: region.x,
|
|
@@ -1575,6 +1729,27 @@ async function normalizePptxZip(raw, context) {
|
|
|
1575
1729
|
}
|
|
1576
1730
|
|
|
1577
1731
|
attachCodeTags(entries, context.codeTags);
|
|
1732
|
+
attachMetricTags(entries,context.metricTags);
|
|
1733
|
+
attachCardTags(entries,context.cardTags);
|
|
1734
|
+
attachHeadingTags(entries,context.headingTags);
|
|
1735
|
+
attachPlainTextTags(entries,context.plainTextTags);
|
|
1736
|
+
attachTimelineTags(entries,context.timelineTags);
|
|
1737
|
+
attachFurnitureTags(entries,context.furnitureTags,context.furnitureManifests);
|
|
1738
|
+
for(const [part,bytes]of Object.entries(entries)){
|
|
1739
|
+
if(!/^ppt\/slides\/slide\d+\.xml$/.test(part))continue;
|
|
1740
|
+
const relationships=parseRelationships(entries,part);
|
|
1741
|
+
for(const [frame]of decodeText(bytes).matchAll(/<p:graphicFrame>[\s\S]*?<\/p:graphicFrame>/g)){
|
|
1742
|
+
const name=frame.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1];
|
|
1743
|
+
if(!context.chartHeadings.has(name))continue;
|
|
1744
|
+
const id=frame.match(/<c:chart\b[^>]*\br:id="([^"]+)"/)?.[1],chartPart=relationships.get(id)?.path;
|
|
1745
|
+
if(!chartPart)throw new OPFPptxError('packaging-failed','Generated chart relationship is missing.');
|
|
1746
|
+
const {heading,labelColor}=context.chartHeadings.get(name);
|
|
1747
|
+
if(heading!==undefined)writeChartCategoryHeading(entries,chartPart,heading);
|
|
1748
|
+
// PptxGenJS hardcodes a black fallback in pie/doughnut label properties.
|
|
1749
|
+
// Normalize only our generated chart text styles; point/series fills stay intact.
|
|
1750
|
+
entries[chartPart]=encodeText(decodeText(entries[chartPart]).replace(/<c:txPr>[\s\S]*?<\/c:txPr>/g,properties=>properties.replace(/<a:solidFill>[\s\S]*?<\/a:solidFill>/g,()=>`<a:solidFill><a:srgbClr val="${labelColor}"/></a:solidFill>`)));
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1578
1753
|
const imageSources = new Map();
|
|
1579
1754
|
for (const [part, bytes] of Object.entries(entries)) {
|
|
1580
1755
|
if (!/^ppt\/slides\/slide\d+\.xml$/.test(part)) continue;
|
|
@@ -1650,6 +1825,11 @@ function normalizePartBytes(path, bytes, context, renameMaps, entries, imageMeta
|
|
|
1650
1825
|
if (isXmlPart(path)) {
|
|
1651
1826
|
let xml=decodeText(bytes);
|
|
1652
1827
|
if (path === '[Content_Types].xml') {
|
|
1828
|
+
// PptxGenJS 4.0.1 emits one slide-master override per slide even
|
|
1829
|
+
// though it creates only the actual master parts. Omit phantom master
|
|
1830
|
+
// declarations without changing any existing part or relationship.
|
|
1831
|
+
xml = xml.replace(/<Override PartName="\/(ppt\/slideMasters\/slideMaster\d+\.xml)"[^>]*\/>/g,
|
|
1832
|
+
(override, part) => Object.hasOwn(entries, part) ? override : '');
|
|
1653
1833
|
// Explicit per-part types also correct PptxGenJS's image/jpg default.
|
|
1654
1834
|
const overrides = [...imageMetadata].filter(([, metadata]) => metadata).map(([part, metadata]) =>
|
|
1655
1835
|
`<Override PartName="/${part}" ContentType="${metadata.mediaType}"/>`).join('');
|