@openpresentation/opf-pptx 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPENDENCY-NOTES.md +3 -1
- package/README.md +19 -3
- package/dist/card-provenance.js +51 -0
- package/dist/chart-workbook.js +81 -0
- package/dist/code-provenance.js +180 -0
- package/dist/heading-provenance.js +44 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +241 -82
- package/dist/metric-provenance.js +94 -0
- package/dist/text-provenance.js +54 -0
- package/dist/timeline-provenance.js +81 -0
- package/package.json +12 -8
package/dist/index.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import {importTableFrames} from './table-import.js';
|
|
2
|
+
import {readChartCategoryHeading,writeChartCategoryHeading} from './chart-workbook.js';
|
|
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';
|
|
2
9
|
import {importImageOrientation} from './image-import.js';
|
|
3
10
|
import {nativeBackgroundFill} from './background.js';
|
|
4
11
|
import {importBackground} from './background-import.js';
|
|
5
12
|
import { webpToPng } from '#image-fallback';
|
|
6
13
|
import { rasterMetadata, pictureTransform, normalizeImageOrientation } from './image-geometry.js';
|
|
7
|
-
import { layoutTable, composeSlide, fitText, fitRichText, textWidthMeasurer, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle } from "@openpresentation/opf/composition";
|
|
14
|
+
import { layoutTable, composeSlide, fitText, fitRichText, textWidthMeasurer, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textColorForFill, chartColorForFill } from "@openpresentation/opf/composition";
|
|
8
15
|
import PptxGenJS from "../vendor/pptxgenjs/pptxgen.es.js";
|
|
9
16
|
import { unzipSync, zipSync } from "fflate";
|
|
10
17
|
import { XMLParser } from "fast-xml-parser";
|
|
@@ -170,6 +177,13 @@ export async function toPptx(input, options = {}) {
|
|
|
170
177
|
context.tableCells = new Map();
|
|
171
178
|
context.imagePlacements = new Map();
|
|
172
179
|
context.backgroundFills = new Map();
|
|
180
|
+
context.cardTags = new Map();
|
|
181
|
+
context.headingTags = new Map();
|
|
182
|
+
context.plainTextTags = new Map();
|
|
183
|
+
context.timelineTags = new Map();
|
|
184
|
+
context.codeTags = new Map();
|
|
185
|
+
context.metricTags = new Map();
|
|
186
|
+
context.chartHeadings = new Map();
|
|
173
187
|
context.imageFormat = options.imageFormat ?? "compatible";
|
|
174
188
|
const pptx = new PptxGenJS();
|
|
175
189
|
configurePresentation(pptx, presentation, {...context,fonts:resolveSlideContext(presentation,presentation.slides[0],context,options).fonts});
|
|
@@ -382,10 +396,16 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
382
396
|
|
|
383
397
|
const items = collectSlideItems(entries, slideRoot, slidePath, relationships, dimensions, options, slideIndex)
|
|
384
398
|
.sort(comparePositionedItems);
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
399
|
+
// Complete OPF heading roles are authoritative. Nearby body lines must not
|
|
400
|
+
// fill an absent role by geometry; explicit native placeholders still apply.
|
|
401
|
+
const inferHeadings = !items.some(item => item.heading||item.sourceText);
|
|
402
|
+
const heading = field => {const index=items.findIndex(item=>item.heading===field);return index<0?null:items.splice(index,1)[0];};
|
|
403
|
+
const tagItem=heading('tag');
|
|
404
|
+
if(tagItem)slide.tag=tagItem.text;
|
|
405
|
+
const titleItem = heading('title')??takeTitleItem(items, dimensions, inferHeadings);
|
|
406
|
+
if (titleItem) slide.title = titleItem.text;
|
|
407
|
+
const subtitleItem = heading('subtitle')??takeSubtitleItem(items, titleItem, dimensions, inferHeadings);
|
|
408
|
+
if (subtitleItem) slide.subtitle = subtitleItem.text;
|
|
389
409
|
|
|
390
410
|
const blocks = mergeAdjacentBulletShapes(items)
|
|
391
411
|
.map((item) => payloadFromSlideItem(item))
|
|
@@ -401,9 +421,33 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
401
421
|
function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensions, options, slideIndex) {
|
|
402
422
|
const tree = slideRoot["p:cSld"]?.["p:spTree"];
|
|
403
423
|
const items = [];
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
424
|
+
const shapes = nativeTextShapes(tree);
|
|
425
|
+
if (tree?.['p:grpSp']) options.onDiagnostic?.({code:'grouped-text-reflow',path:`slides.${slideIndex}`,message:'Grouped native text is retained, but group transforms and non-text group members are not reconstructed; review the reflowed OPF.'});
|
|
426
|
+
const paragraphs = nativeShapeParagraphs(decodeText(entries[slidePath]));
|
|
427
|
+
const code = importCodeGroups(shapes, paragraphs, relationships, entries, diagnostic => options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}.code`}));
|
|
428
|
+
const metric = importMetricGroups(shapes, paragraphs, relationships, entries, diagnostic => options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}.metric`}));
|
|
429
|
+
const cards = importCardFrames(shapes, paragraphs, relationships, entries, diagnostic => options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}`}));
|
|
430
|
+
const headings=importHeadingGroups(shapes,paragraphs,relationships,entries,diagnostic=>options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}`}));
|
|
431
|
+
const plainText=importPlainTextGroups(shapes,paragraphs,relationships,entries,diagnostic=>options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}`}));
|
|
432
|
+
const timelines=importTimelineGroups(shapes,paragraphs,relationships,entries,diagnostic=>options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}`}));
|
|
433
|
+
for(const group of [...headings.items,...plainText.items]) {
|
|
434
|
+
const item=importShape(group.shapes[0],dimensions,group.paragraphs,true);
|
|
435
|
+
if(item){
|
|
436
|
+
const bounds=group.shapes.map(shape=>shapeBounds(shape['p:spPr']?.['a:xfrm'])).filter(Boolean);
|
|
437
|
+
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};}
|
|
438
|
+
items.push({...item,heading:group.field,sourceText:!group.field});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
for(const group of timelines.items){
|
|
442
|
+
const bounds=group.shapes.map(shape=>shapeBounds(shape['p:spPr']?.['a:xfrm'])).filter(Boolean);
|
|
443
|
+
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};}}
|
|
444
|
+
items.push({kind:'timeline',sourceText:true,bounds:union,payload:group.payload});
|
|
445
|
+
}
|
|
446
|
+
for (const item of code.items) items.push({kind:'code',bounds:shapeBounds(item.shape['p:spPr']?.['a:xfrm']),payload:item.payload});
|
|
447
|
+
for (const item of metric.items) items.push({kind:'metric',bounds:shapeBounds(item.shape['p:spPr']?.['a:xfrm']),payload:item.payload});
|
|
448
|
+
for (const [index,shape] of shapes.entries()) {
|
|
449
|
+
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;
|
|
450
|
+
const item = importShape(shape, dimensions, paragraphs[index]);
|
|
407
451
|
if (item) items.push(item);
|
|
408
452
|
}
|
|
409
453
|
|
|
@@ -426,14 +470,13 @@ function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensi
|
|
|
426
470
|
return items;
|
|
427
471
|
}
|
|
428
472
|
|
|
429
|
-
function importShape(shape, dimensions) {
|
|
430
|
-
const
|
|
431
|
-
const text = paragraphs.map((paragraph) => paragraph.text).filter(Boolean).join("\n").trim();
|
|
473
|
+
function importShape(shape, dimensions, paragraphs = readParagraphs(shape["p:txBody"]), allowEmptyText = false) {
|
|
474
|
+
const text = paragraphs.map((paragraph) => paragraph.text).join("\n");
|
|
432
475
|
const placeholder = shapePlaceholderType(shape);
|
|
433
476
|
const bounds = shapeBounds(shape["p:spPr"]?.["a:xfrm"]);
|
|
434
477
|
const name = scalarText(shape["p:nvSpPr"]?.["p:cNvPr"]?.name).trim();
|
|
435
478
|
|
|
436
|
-
if (text) {
|
|
479
|
+
if (text || allowEmptyText) {
|
|
437
480
|
return {
|
|
438
481
|
kind: "text",
|
|
439
482
|
text,
|
|
@@ -544,13 +587,12 @@ function readParagraphs(txBody) {
|
|
|
544
587
|
if (Number.isFinite(size)) sizes.push(size / 100);
|
|
545
588
|
}
|
|
546
589
|
return {
|
|
547
|
-
text: texts.join("")
|
|
590
|
+
text: texts.join(""),
|
|
548
591
|
bullet: asArray(paragraph?.["a:pPr"]).some(props => props?.["a:buChar"] !== undefined || props?.["a:buAutoNum"] !== undefined),
|
|
549
592
|
level: Number(asArray(paragraph?.["a:pPr"])[0]?.lvl ?? 0),
|
|
550
593
|
maxFontSize: sizes.length > 0 ? Math.max(...sizes) : 0
|
|
551
594
|
};
|
|
552
|
-
})
|
|
553
|
-
.filter((paragraph) => paragraph.text);
|
|
595
|
+
});
|
|
554
596
|
}
|
|
555
597
|
|
|
556
598
|
function shapePlaceholderType(shape) {
|
|
@@ -567,13 +609,14 @@ function shapeBounds(xfrm) {
|
|
|
567
609
|
return { x, y, w, h };
|
|
568
610
|
}
|
|
569
611
|
|
|
570
|
-
function takeTitleItem(items, dimensions) {
|
|
612
|
+
function takeTitleItem(items, dimensions, inferHeadings) {
|
|
571
613
|
const explicitIndex = items.findIndex((item) => ["title", "ctrTitle"].includes(item.placeholder));
|
|
572
614
|
if (explicitIndex >= 0) return items.splice(explicitIndex, 1)[0];
|
|
615
|
+
if (!inferHeadings) return null;
|
|
573
616
|
|
|
574
617
|
const titleLimit = dimensions.heightInches * 0.28;
|
|
575
618
|
const candidateIndex = items.findIndex((item) => {
|
|
576
|
-
if (item.kind !== "text" || !item.text || item.paragraphs.some(p=>p.bullet)) return false;
|
|
619
|
+
if (item.heading || item.kind !== "text" || !item.text || item.paragraphs.some(p=>p.bullet)) return false;
|
|
577
620
|
const y = item.bounds?.y ?? 0;
|
|
578
621
|
return y <= titleLimit && (item.maxFontSize >= 20 || /^title\b/i.test(item.name ?? ""));
|
|
579
622
|
});
|
|
@@ -581,15 +624,15 @@ function takeTitleItem(items, dimensions) {
|
|
|
581
624
|
return null;
|
|
582
625
|
}
|
|
583
626
|
|
|
584
|
-
function takeSubtitleItem(items, titleItem, dimensions) {
|
|
627
|
+
function takeSubtitleItem(items, titleItem, dimensions, inferHeadings) {
|
|
585
628
|
const explicitIndex = items.findIndex((item) => item.placeholder === "subTitle");
|
|
586
629
|
if (explicitIndex >= 0) return items.splice(explicitIndex, 1)[0];
|
|
587
|
-
if (!titleItem) return null;
|
|
630
|
+
if (!inferHeadings || !titleItem) return null;
|
|
588
631
|
|
|
589
632
|
const titleBottom = (titleItem.bounds?.y ?? 0) + (titleItem.bounds?.h ?? 0);
|
|
590
633
|
const subtitleLimit = Math.min(dimensions.heightInches * 0.34, 1.45);
|
|
591
634
|
const candidateIndex = items.findIndex((item) => {
|
|
592
|
-
if (item.kind !== "text" || !item.text || item.paragraphs.some(p=>p.bullet)) return false;
|
|
635
|
+
if (item.heading || item.kind !== "text" || !item.text || item.paragraphs.some(p=>p.bullet)) return false;
|
|
593
636
|
const y = item.bounds?.y ?? 0;
|
|
594
637
|
const h = item.bounds?.h ?? 0;
|
|
595
638
|
return y >= titleBottom - 0.05
|
|
@@ -624,7 +667,7 @@ function mergeAdjacentBulletShapes(items) {
|
|
|
624
667
|
function payloadFromSlideItem(item) {
|
|
625
668
|
if (item.payload) return item.payload;
|
|
626
669
|
if (item.kind === "text") {
|
|
627
|
-
if (item.paragraphs.
|
|
670
|
+
if (item.paragraphs.some(p=>p.bullet)) {
|
|
628
671
|
return {
|
|
629
672
|
type: "list",
|
|
630
673
|
items: item.paragraphs.map((paragraph) => (
|
|
@@ -669,7 +712,7 @@ function chartFromRelationship(entries, slidePath, relationships, relId) {
|
|
|
669
712
|
return {
|
|
670
713
|
type: chartNode.type,
|
|
671
714
|
data: {
|
|
672
|
-
columns: ["Category", ...names],
|
|
715
|
+
columns: [readChartCategoryHeading(entries,relationship.path) ?? "Category", ...names],
|
|
673
716
|
rows
|
|
674
717
|
}
|
|
675
718
|
};
|
|
@@ -883,31 +926,47 @@ async function addSlide(pptx, presentation, opfSlide, slideIndex, context, optio
|
|
|
883
926
|
const { widthInches, heightInches } = slideContext.dimensions;
|
|
884
927
|
const layout = resolveCatalogRecord(presentation, "layouts", opfSlide.layout, "blank") ?? {};
|
|
885
928
|
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` });
|
|
886
|
-
const geometry = composeSlide(opfSlide, { width: widthInches * 96, height: heightInches * 96, layout, slideIndex, fonts: slideContext.fonts, textMeasurement: options.textMeasurement });
|
|
929
|
+
const geometry = composeSlide(opfSlide, { width: widthInches * 96, height: heightInches * 96, layout, 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 });
|
|
887
930
|
for (const diagnostic of geometry.diagnostics) options.onDiagnostic?.(diagnostic);
|
|
888
931
|
for (const item of geometry.items) {
|
|
889
932
|
const region = { x: item.box.x / 96, y: item.box.y / 96, w: item.box.width / 96, h: item.box.height / 96 };
|
|
890
|
-
if (
|
|
933
|
+
if (item.frameBox) {
|
|
934
|
+
const frame=item.frameBox;
|
|
935
|
+
context.cardTags.set(`OPF card ${item.path}`,item.path);
|
|
936
|
+
const paint=value=>({color:normalizeHex(value),transparency:/^#[0-9a-f]{8}$/i.test(value)?(1-parseInt(value.slice(7),16)/255)*100:0});
|
|
937
|
+
const surface=slideContext.colorScheme[isDarkHex(slideContext.colors.background)?'dark2':'light2']??`#${slideContext.colors.surface}`;
|
|
938
|
+
slide.addShape('roundRect',{x:frame.x/96,y:frame.y/96,w:frame.width/96,h:frame.height/96,
|
|
939
|
+
rectRadius:8*Math.min(widthInches,heightInches)/720,
|
|
940
|
+
fill:paint(surface),line:{...paint(slideContext.colorScheme.accent5??`#${slideContext.colors.border}`),pt:.75},objectName:`OPF card ${item.path}`});
|
|
941
|
+
}
|
|
942
|
+
if(['text','title','subtitle','tag'].includes(item.field)&&(item.text?.placement||item.text?.sourceLines)&&!item.text.richLines) {
|
|
943
|
+
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});
|
|
944
|
+
} else if (["title", "subtitle", "tag"].includes(item.field)) {
|
|
945
|
+
// Estimated-font headings use one native text box, which still needs a
|
|
946
|
+
// role tag. Role recovery must not depend on outline measurement support.
|
|
947
|
+
const objectName = `OPF heading ${item.path} line 0`;
|
|
948
|
+
context.headingTags.set(objectName,{v:1,group:item.path,field:item.field,line:0,count:1});
|
|
891
949
|
slide.addText(item.text.lines.join("\n"), {
|
|
892
950
|
...textBoxOptions(region, slideContext, item.text.fontSize * 0.75),
|
|
893
|
-
|
|
894
|
-
bold: item.textStyle.fontWeight >= 600,
|
|
895
|
-
italic: item.textStyle.italic,
|
|
951
|
+
...nativeFontOptions(item.textStyle),
|
|
896
952
|
color: item.field === "tag" ? slideContext.colors.accent : slideContext.colors.text,
|
|
953
|
+
objectName,
|
|
897
954
|
breakLine: false
|
|
898
955
|
});
|
|
899
956
|
} else if ((item.field === "items" || item.field === "bullets") && item.text?.listEntries) {
|
|
900
957
|
addMeasuredList(slide,item.text,slideContext);
|
|
901
958
|
} else if (item.field === "text" && item.text?.richLines) {
|
|
902
|
-
const alignment=opfSlide.design?.contentAlignment??presentation.design?.contentAlignment??'left';
|
|
903
|
-
for(const line of item.text.richLines){
|
|
904
|
-
const runs=line.fragments.map(fragment=>({text:fragment.text,options:{
|
|
905
|
-
|
|
959
|
+
const alignment=item.text.placement?.alignment??opfSlide.design?.contentAlignment??presentation.design?.contentAlignment??'left';
|
|
960
|
+
for(const [index,line] of item.text.richLines.entries()){
|
|
961
|
+
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}}));
|
|
962
|
+
const placed=item.text.placement?.lines[index],factor=alignment==='right'?1:alignment==='center'?.5:0;
|
|
963
|
+
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};
|
|
964
|
+
if(runs.length)slide.addText(runs,{...textBoxOptions(area,slideContext,item.text.fontSize*.75),align:alignment,fit:'none',wrap:false,lineSpacingMultiple:1});
|
|
906
965
|
}
|
|
907
966
|
} else if (item.field === "text" && typeof item.value === "string") {
|
|
908
|
-
slide.addText(item.text.lines.join("\n"), {...textBoxOptions(region, slideContext, item.text.fontSize * 0.75)
|
|
967
|
+
slide.addText(item.text.lines.join("\n"), {...textBoxOptions(region, slideContext, item.text.fontSize * 0.75),...nativeFontOptions(item.textStyle)});
|
|
909
968
|
} else {
|
|
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);
|
|
969
|
+
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);
|
|
911
970
|
}
|
|
912
971
|
}
|
|
913
972
|
|
|
@@ -928,7 +987,7 @@ function fieldToType(field) {
|
|
|
928
987
|
return field === "items" || field === "bullets" ? "list" : field;
|
|
929
988
|
}
|
|
930
989
|
|
|
931
|
-
async function addPayload(slide, presentation, payload, region, path, context, options, quoteLayout) {
|
|
990
|
+
async function addPayload(slide, presentation, payload, region, path, context, options, quoteLayout, codeLayout,metricLayout,timelineLayout) {
|
|
932
991
|
const kind = inferPayloadKind(payload);
|
|
933
992
|
switch (kind) {
|
|
934
993
|
case "text":
|
|
@@ -950,16 +1009,16 @@ async function addPayload(slide, presentation, payload, region, path, context, o
|
|
|
950
1009
|
addTablePayload(slide, payload.table, region, context, options, path);
|
|
951
1010
|
break;
|
|
952
1011
|
case "code":
|
|
953
|
-
addCodePayload(slide, payload.code, region, context,
|
|
1012
|
+
addCodePayload(slide, payload.code, codeLayout, region, context, path);
|
|
954
1013
|
break;
|
|
955
1014
|
case "metric":
|
|
956
|
-
addMetricPayload(slide, payload.metric,
|
|
1015
|
+
addMetricPayload(slide, payload.metric,metricLayout,context,path);
|
|
957
1016
|
break;
|
|
958
1017
|
case "quote":
|
|
959
1018
|
addQuotePayload(slide, quoteLayout, context, options, path);
|
|
960
1019
|
break;
|
|
961
1020
|
case "timeline":
|
|
962
|
-
addTimelinePayload(slide, payload.timeline,
|
|
1021
|
+
addTimelinePayload(slide, payload.timeline, timelineLayout, context, options, path);
|
|
963
1022
|
break;
|
|
964
1023
|
default:
|
|
965
1024
|
addPlaceholderPayload(slide, "Unsupported OPF payload", payload, region, context);
|
|
@@ -989,7 +1048,7 @@ function addTextPayload(slide, value, region, context) {
|
|
|
989
1048
|
}
|
|
990
1049
|
|
|
991
1050
|
function richLineRuns(line,color) {
|
|
992
|
-
return line.fragments.map(fragment=>({text:fragment.text,options:{
|
|
1051
|
+
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}}));
|
|
993
1052
|
}
|
|
994
1053
|
function addMeasuredList(slide,fit,context) {
|
|
995
1054
|
for(const entry of fit.listEntries){
|
|
@@ -1005,7 +1064,7 @@ function addMeasuredList(slide,fit,context) {
|
|
|
1005
1064
|
// Keep paragraph intent identical across runs. ZIP normalization below
|
|
1006
1065
|
// removes the duplicate paragraph-property nodes emitted by PptxGenJS.
|
|
1007
1066
|
for(const run of runs)Object.assign(run.options,paragraph);
|
|
1008
|
-
slide.addText(runs,{...textBoxOptions(region,context,text.fontSize*.75),fontFace:entry.marker.style.
|
|
1067
|
+
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});
|
|
1009
1068
|
});
|
|
1010
1069
|
};
|
|
1011
1070
|
addLines(entry.text,entry.textBox,context.colors.text,true);
|
|
@@ -1078,21 +1137,36 @@ function addChartPayload(slide, chart, region, context) {
|
|
|
1078
1137
|
return;
|
|
1079
1138
|
}
|
|
1080
1139
|
|
|
1140
|
+
// Keep the resolved palette surface (including alpha) explicit in native
|
|
1141
|
+
// chart/plot areas, so inherited labels are assessed against their own panel.
|
|
1142
|
+
const panelFill = context.colorScheme.surface ?? context.colorScheme[isDarkHex(context.colors.background) ? 'dark2' : 'light2'] ?? `#${context.colors.surface}`;
|
|
1143
|
+
const labelColor = normalizeHex(textColorForFill(panelFill, `#${context.colors.text}`));
|
|
1144
|
+
const transparency = /^#[0-9a-f]{8}$/i.test(panelFill) ? (1 - parseInt(panelFill.slice(7), 16) / 255) * 100 : 0;
|
|
1145
|
+
const fill = {color:normalizeHex(panelFill),transparency};
|
|
1146
|
+
const objectName = `OPF chart ${context.chartHeadings.size + 1}`;
|
|
1147
|
+
const circular = chartData.type === 'pie' || chartData.type === 'doughnut';
|
|
1148
|
+
context.chartHeadings.set(objectName,{heading:chartData.type === 'scatter' ? undefined : chart.data.columns[0],labelColor});
|
|
1081
1149
|
slide.addChart(chartData.type, chartData.series, {
|
|
1150
|
+
objectName,
|
|
1082
1151
|
x: region.x,
|
|
1083
1152
|
y: region.y,
|
|
1084
1153
|
w: region.w,
|
|
1085
1154
|
h: region.h,
|
|
1086
|
-
showLegend: chartData.series.length > 1,
|
|
1155
|
+
showLegend: circular || chartData.series.length > 1,
|
|
1087
1156
|
showTitle: false,
|
|
1088
|
-
chartColors: CHART_COLORS,
|
|
1157
|
+
chartColors: CHART_COLORS.map(color=>normalizeHex(chartColorForFill(panelFill,`#${color}`))),
|
|
1158
|
+
chartArea: {fill:{...fill},roundedCorners:false},
|
|
1159
|
+
// Paint alpha once in the chart area, rather than stacking two alpha fills.
|
|
1160
|
+
plotArea: {fill:{color:null}},
|
|
1089
1161
|
catAxisLabelFontFace: context.fonts.body,
|
|
1090
1162
|
catAxisLabelFontSize: 9,
|
|
1091
|
-
catAxisLabelColor:
|
|
1163
|
+
catAxisLabelColor: labelColor,
|
|
1092
1164
|
valAxisLabelFontFace: context.fonts.body,
|
|
1093
1165
|
valAxisLabelFontSize: 9,
|
|
1094
|
-
valAxisLabelColor:
|
|
1095
|
-
legendColor:
|
|
1166
|
+
valAxisLabelColor: labelColor,
|
|
1167
|
+
legendColor: labelColor,
|
|
1168
|
+
legendFontFace: context.fonts.body,
|
|
1169
|
+
dataLabelColor: labelColor,
|
|
1096
1170
|
showValue: false,
|
|
1097
1171
|
valGridLine: { color: context.colors.border, transparency: 30, size: 1 },
|
|
1098
1172
|
barDir: chartData.barDir,
|
|
@@ -1116,8 +1190,8 @@ function addTablePayload(slide, table, region, context, options, path) {
|
|
|
1116
1190
|
const rows = layout.rows.map(row => row.cells.map(cell => {
|
|
1117
1191
|
const {header,rich,fit} = cell;
|
|
1118
1192
|
const cellStyle = cell.style ?? {};
|
|
1119
|
-
const baseColor = (cellStyle.color ?? (header ? '#FFFFFF' : '#' + context.colors.text)).replace(/^#/, '');
|
|
1120
1193
|
const baseFill = (cellStyle.fill ?? (header ? '#' + context.colors.accent : '#' + context.colors.surface)).replace(/^#/, '');
|
|
1194
|
+
const baseColor = (cellStyle.color ?? textColorForFill('#' + baseFill, header ? '#FFFFFF' : '#' + context.colors.text)).replace(/^#/, '');
|
|
1121
1195
|
const alpha = value => value.length === 8 ? (1 - parseInt(value.slice(6), 16) / 255) * 100 : 0;
|
|
1122
1196
|
const text = stringifyText(cell.value), style = cell.textStyle;
|
|
1123
1197
|
const fragments = rich ? fit.richLines.flatMap(line => line.fragments) : [];
|
|
@@ -1128,8 +1202,7 @@ function addTablePayload(slide, table, region, context, options, path) {
|
|
|
1128
1202
|
const rawColor = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(run.color ?? '') ? run.color.slice(1) : baseColor;
|
|
1129
1203
|
const color = normalizeHex(rawColor), transparency = rawColor.length === 8 ? (1 - parseInt(rawColor.slice(6), 16) / 255) * 100 : 0;
|
|
1130
1204
|
const runOptions = {
|
|
1131
|
-
|
|
1132
|
-
bold:runStyle.fontWeight >= 600,italic:runStyle.italic,
|
|
1205
|
+
...nativeFontOptions(runStyle),fontSize:fragment ? fragment.fontSize * .75 : fit.fontSize * .75,
|
|
1133
1206
|
underline:run.underline ? {color} : undefined,strike:run.strikethrough ? 'sngStrike' : undefined,
|
|
1134
1207
|
color,transparency,baseline:fragment?.baselineShift ? -fragment.baselineShift / fragment.fontSize * 2000 : undefined,
|
|
1135
1208
|
hyperlink:run.link && /^(https?:|mailto:)/i.test(run.link) ? {url:run.link} : undefined,
|
|
@@ -1145,13 +1218,12 @@ function addTablePayload(slide, table, region, context, options, path) {
|
|
|
1145
1218
|
// soft wraps into the text would change a later import or copy operation.
|
|
1146
1219
|
text: rich ? runs : text,
|
|
1147
1220
|
options: {
|
|
1148
|
-
|
|
1221
|
+
...nativeFontOptions(style),
|
|
1149
1222
|
fontSize: fit.fontSize * 0.75,
|
|
1150
1223
|
// PptxGenJS fills falsy run options from cell defaults. Rich runs
|
|
1151
1224
|
// carry their resolved weight, so a bold header default must not turn
|
|
1152
1225
|
// an explicit bold:false run back on.
|
|
1153
|
-
|
|
1154
|
-
italic: rich ? false : style.italic,
|
|
1226
|
+
...(rich ? {bold:false,italic:false} : {}),
|
|
1155
1227
|
lineSpacing: fit.lineHeight * 0.75,
|
|
1156
1228
|
paraSpaceAfter: 0,
|
|
1157
1229
|
align: cellStyle.align ?? context.contentAlignment,
|
|
@@ -1194,6 +1266,8 @@ function pixelBox(region) {
|
|
|
1194
1266
|
// Match the published renderer's payload geometry and shared core text fitting.
|
|
1195
1267
|
// Each fitted line remains native editable text, without PowerPoint rewrapping it.
|
|
1196
1268
|
function addMeasuredPayloadText(slide, text, box, context, options, config) {
|
|
1269
|
+
const invalid=/[\u0000-\u0008\u000B\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/u.exec(String(text??''));
|
|
1270
|
+
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});
|
|
1197
1271
|
const scale = Math.min(context.dimensions.widthInches, context.dimensions.heightInches) * 96 / 720;
|
|
1198
1272
|
const style = config.textStyle ?? resolveTextStyle({fontFamily: config.fontFamily ?? context.fonts.body, fontWeight: config.fontWeight ?? 400, path: config.path}, options.textMeasurement);
|
|
1199
1273
|
const fit = config.fit ?? fitText(String(text ?? ''), box, config.fontSize * scale, (context.composition?.minFontSize ?? 16) * scale, textWidthMeasurer(style, options.textMeasurement));
|
|
@@ -1203,32 +1277,78 @@ function addMeasuredPayloadText(slide, text, box, context, options, config) {
|
|
|
1203
1277
|
if (context.composition?.overflow === 'error') throw new OPFPptxError('layout-overflow', diagnostic.message, {path: config.path, issues: [diagnostic]});
|
|
1204
1278
|
}
|
|
1205
1279
|
for (const [index, line] of fit.lines.entries()) {
|
|
1206
|
-
if (!line) continue;
|
|
1280
|
+
if (!line&&!config.heading&&!config.sourceText&&!config.timeline) continue;
|
|
1281
|
+
const alignment=fit.placement?.alignment??config.align??context.contentAlignment??'left',placed=fit.placement?.lines[index],factor=alignment==='right'?1:alignment==='center'?.5:0;
|
|
1282
|
+
const sourceLine=fit.sourceLines?.[index],boundary=sourceLine?{boundary:sourceLine.boundary,separator:String(text).slice(sourceLine.end,sourceLine.nextStart)}:{};
|
|
1283
|
+
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}`:undefined;
|
|
1284
|
+
if(config.heading)context.headingTags.set(objectName,{v:1,group:config.path,field:config.heading,line:index,count:fit.lines.length,...boundary});
|
|
1285
|
+
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}:{})});
|
|
1286
|
+
else if(config.sourceText)context.plainTextTags.set(objectName,{v:1,group:config.path,line:index,count:fit.lines.length,...boundary});
|
|
1287
|
+
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};
|
|
1207
1288
|
slide.addText(line, {
|
|
1208
|
-
...textBoxOptions(
|
|
1209
|
-
|
|
1210
|
-
color: normalizeHex(config.color ?? context.colors.text), align:
|
|
1289
|
+
...textBoxOptions(area, context, fit.fontSize * .75),
|
|
1290
|
+
...nativeFontOptions(style),
|
|
1291
|
+
color: normalizeHex(config.color ?? context.colors.text), align: alignment,
|
|
1292
|
+
tabStops:sourceLine?.segments.filter(segment=>segment.kind==='tab').map(segment=>({position:(segment.x+segment.width)/96,alignment:'l'})),
|
|
1293
|
+
objectName,
|
|
1211
1294
|
fit: 'none', wrap: false, lineSpacingMultiple: 1,
|
|
1212
1295
|
});
|
|
1213
1296
|
}
|
|
1214
1297
|
}
|
|
1215
1298
|
|
|
1216
|
-
function addCodePayload(slide, value, region, context,
|
|
1217
|
-
|
|
1218
|
-
const
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1299
|
+
function addCodePayload(slide, value, layout, region, context, path) {
|
|
1300
|
+
if (!layout) throw new OPFPptxError('missing-code-layout', 'Code export requires a coordinated core build with shared code geometry.', {path});
|
|
1301
|
+
for (const part of layout.parts) {
|
|
1302
|
+
// XML 1.0 Char excludes controls and unpaired UTF-16 surrogates. The u flag
|
|
1303
|
+
// keeps valid supplementary characters (surrogate pairs) accepted.
|
|
1304
|
+
const invalid = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/u.exec(part.text);
|
|
1305
|
+
if (invalid) throw new OPFPptxError('invalid-code-text', `Code 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});
|
|
1306
|
+
}
|
|
1307
|
+
const group = String(context.codeTags.size + 1), panelName = `OPF code ${group} panel`;
|
|
1308
|
+
for (const part of layout.parts) if (!part.fit) throw new OPFPptxError('layout-overflow', 'Code content has no usable internal space; increase its cell size before exporting.', {path:part.path,issues:layout.diagnostics});
|
|
1309
|
+
context.codeTags.set(panelName,codeManifest(value,layout,group));
|
|
1310
|
+
slide.addShape('rect', {...region, fill: {color: '111827'}, line: {color: '334155', pt: .75}, objectName:panelName});
|
|
1311
|
+
for (const [partIndex,part] of layout.parts.entries()) {
|
|
1312
|
+
if (!part.fit) throw new OPFPptxError('layout-overflow', 'Code content has no usable internal space; increase its cell size before exporting.', {path:part.path,issues:layout.diagnostics});
|
|
1313
|
+
for (const [index,line] of part.fit.sourceLines.entries()) {
|
|
1314
|
+
const tabStops=line.segments.filter(segment=>segment.kind==='tab').map(segment=>({position:(segment.x+segment.width)/96,alignment:'l'}));
|
|
1315
|
+
const objectName = `OPF code ${group} ${part.role} line ${index+1}`;
|
|
1316
|
+
context.codeTags.set(objectName,{v:1,group,role:'line',part:partIndex,line:index});
|
|
1317
|
+
slide.addText(part.text.slice(line.start,line.end),{
|
|
1318
|
+
...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),
|
|
1319
|
+
...nativeFontOptions(part.style),
|
|
1320
|
+
color:part.role==='body'?'E5E7EB':'93C5FD',align:'left',fit:'none',wrap:false,lineSpacingMultiple:1,
|
|
1321
|
+
tabStops:tabStops.length?tabStops:undefined,objectName,
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1226
1325
|
}
|
|
1227
1326
|
|
|
1228
|
-
function addMetricPayload(slide,
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1327
|
+
function addMetricPayload(slide,value,layout,context,path) {
|
|
1328
|
+
if (!layout) throw new OPFPptxError('missing-metric-layout','Metric export requires coordinated core metric geometry.',{path});
|
|
1329
|
+
const group=String(context.metricTags.size+1),manifest=metricManifest(value,layout,group);
|
|
1330
|
+
for (const [partIndex,part] of layout.parts.entries()) {
|
|
1331
|
+
const invalid=/[\u0000-\u0008\u000B\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/u.exec(part.text);
|
|
1332
|
+
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});
|
|
1333
|
+
if (!part.visible) continue;
|
|
1334
|
+
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});
|
|
1335
|
+
for (const [index,line] of part.fit.sourceLines.entries()) {
|
|
1336
|
+
const origin=part.linePositions[index],objectName=`OPF metric ${group} ${part.role} line ${index+1}`;
|
|
1337
|
+
context.metricTags.set(objectName,partIndex===0&&index===0?manifest:{v:1,group,role:'line',part:partIndex,line:index});
|
|
1338
|
+
const tabStops=line.segments.filter(segment=>segment.kind==='tab').map(segment=>({position:(segment.x+segment.width)/96,alignment:'l'}));
|
|
1339
|
+
// Anchor the native paragraph to the same accepted left/center/right point.
|
|
1340
|
+
// Left-aligning at a measured glyph origin loses the intended edge when the
|
|
1341
|
+
// native shaper has a slightly different advance. No re-fitting is needed.
|
|
1342
|
+
const factor=layout.alignment==='right'?1:layout.alignment==='center'?.5:0;
|
|
1343
|
+
const anchor=origin.x+line.width*factor;
|
|
1344
|
+
slide.addText(part.text.slice(line.start,line.end),{
|
|
1345
|
+
...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),
|
|
1346
|
+
...nativeFontOptions(part.style),
|
|
1347
|
+
color:part.role==='value'?context.colors.accent:context.colors.text,align:layout.alignment,fit:'none',wrap:false,lineSpacingMultiple:1,
|
|
1348
|
+
tabStops:tabStops.length?tabStops:undefined,objectName,
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1232
1352
|
}
|
|
1233
1353
|
|
|
1234
1354
|
function addQuotePayload(slide, layout, context, options, path) {
|
|
@@ -1242,20 +1362,23 @@ function addQuotePayload(slide, layout, context, options, path) {
|
|
|
1242
1362
|
}
|
|
1243
1363
|
}
|
|
1244
1364
|
|
|
1245
|
-
function addTimelinePayload(slide, value,
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
const
|
|
1250
|
-
const
|
|
1251
|
-
const
|
|
1252
|
-
const
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
for
|
|
1256
|
-
const
|
|
1257
|
-
slide.addShape('ellipse',
|
|
1258
|
-
|
|
1365
|
+
function addTimelinePayload(slide, value, layout, context, options, path) {
|
|
1366
|
+
if(!layout)throw new OPFPptxError('missing-timeline-layout','Timeline export requires a coordinated core build with shared timeline geometry.',{path});
|
|
1367
|
+
// Validate optional field fits before the provenance manifest dereferences
|
|
1368
|
+
// source lines or any drawing/tag mutation is performed.
|
|
1369
|
+
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});
|
|
1370
|
+
const scale=Math.min(context.dimensions.widthInches,context.dimensions.heightInches)*96/720;
|
|
1371
|
+
const group=String(context.timelineTags.size),anchor=timelineManifest(value,layout),connectorName=`OPF timeline ${group} connector`;
|
|
1372
|
+
const {x1,y1,x2,y2}=layout.connector;
|
|
1373
|
+
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}});
|
|
1374
|
+
context.timelineTags.set(connectorName,{v:1,group,role:'connector'});
|
|
1375
|
+
for(const marker of layout.markers){
|
|
1376
|
+
const objectName=`OPF timeline ${group} marker ${marker.eventIndex}`;
|
|
1377
|
+
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}});
|
|
1378
|
+
context.timelineTags.set(objectName,{v:1,group,role:'marker',eventIndex:marker.eventIndex});
|
|
1379
|
+
}
|
|
1380
|
+
for(const [index,part]of layout.parts.entries()){
|
|
1381
|
+
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}});
|
|
1259
1382
|
}
|
|
1260
1383
|
}
|
|
1261
1384
|
|
|
@@ -1283,6 +1406,16 @@ function addPlaceholderPayload(slide, label, value, region, context) {
|
|
|
1283
1406
|
});
|
|
1284
1407
|
}
|
|
1285
1408
|
|
|
1409
|
+
function nativeFontOptions(style) {
|
|
1410
|
+
const face=style.fontFace;
|
|
1411
|
+
if(face!==undefined) {
|
|
1412
|
+
if(!face || typeof face.family!=='string' || !face.family.trim() || typeof face.bold!=='boolean' || typeof face.italic!=='boolean')
|
|
1413
|
+
throw new OPFPptxError('invalid-font-selection','The font provider must supply a family and explicit bold/italic style-link flags.',{path:style.path});
|
|
1414
|
+
return {fontFace:face.family,bold:face.bold,italic:face.italic};
|
|
1415
|
+
}
|
|
1416
|
+
return {fontFace:style.fontFamily,bold:style.fontWeight>=600,italic:style.italic};
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1286
1419
|
function textBoxOptions(region, context, fontSize) {
|
|
1287
1420
|
return {
|
|
1288
1421
|
x: region.x,
|
|
@@ -1553,6 +1686,27 @@ async function normalizePptxZip(raw, context) {
|
|
|
1553
1686
|
});
|
|
1554
1687
|
}
|
|
1555
1688
|
|
|
1689
|
+
attachCodeTags(entries, context.codeTags);
|
|
1690
|
+
attachMetricTags(entries,context.metricTags);
|
|
1691
|
+
attachCardTags(entries,context.cardTags);
|
|
1692
|
+
attachHeadingTags(entries,context.headingTags);
|
|
1693
|
+
attachPlainTextTags(entries,context.plainTextTags);
|
|
1694
|
+
attachTimelineTags(entries,context.timelineTags);
|
|
1695
|
+
for(const [part,bytes]of Object.entries(entries)){
|
|
1696
|
+
if(!/^ppt\/slides\/slide\d+\.xml$/.test(part))continue;
|
|
1697
|
+
const relationships=parseRelationships(entries,part);
|
|
1698
|
+
for(const [frame]of decodeText(bytes).matchAll(/<p:graphicFrame>[\s\S]*?<\/p:graphicFrame>/g)){
|
|
1699
|
+
const name=frame.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1];
|
|
1700
|
+
if(!context.chartHeadings.has(name))continue;
|
|
1701
|
+
const id=frame.match(/<c:chart\b[^>]*\br:id="([^"]+)"/)?.[1],chartPart=relationships.get(id)?.path;
|
|
1702
|
+
if(!chartPart)throw new OPFPptxError('packaging-failed','Generated chart relationship is missing.');
|
|
1703
|
+
const {heading,labelColor}=context.chartHeadings.get(name);
|
|
1704
|
+
if(heading!==undefined)writeChartCategoryHeading(entries,chartPart,heading);
|
|
1705
|
+
// PptxGenJS hardcodes a black fallback in pie/doughnut label properties.
|
|
1706
|
+
// Normalize only our generated chart text styles; point/series fills stay intact.
|
|
1707
|
+
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>`)));
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1556
1710
|
const imageSources = new Map();
|
|
1557
1711
|
for (const [part, bytes] of Object.entries(entries)) {
|
|
1558
1712
|
if (!/^ppt\/slides\/slide\d+\.xml$/.test(part)) continue;
|
|
@@ -1628,6 +1782,11 @@ function normalizePartBytes(path, bytes, context, renameMaps, entries, imageMeta
|
|
|
1628
1782
|
if (isXmlPart(path)) {
|
|
1629
1783
|
let xml=decodeText(bytes);
|
|
1630
1784
|
if (path === '[Content_Types].xml') {
|
|
1785
|
+
// PptxGenJS 4.0.1 emits one slide-master override per slide even
|
|
1786
|
+
// though it creates only the actual master parts. Omit phantom master
|
|
1787
|
+
// declarations without changing any existing part or relationship.
|
|
1788
|
+
xml = xml.replace(/<Override PartName="\/(ppt\/slideMasters\/slideMaster\d+\.xml)"[^>]*\/>/g,
|
|
1789
|
+
(override, part) => Object.hasOwn(entries, part) ? override : '');
|
|
1631
1790
|
// Explicit per-part types also correct PptxGenJS's image/jpg default.
|
|
1632
1791
|
const overrides = [...imageMetadata].filter(([, metadata]) => metadata).map(([part, metadata]) =>
|
|
1633
1792
|
`<Override PartName="/${part}" ContentType="${metadata.mediaType}"/>`).join('');
|