@openpresentation/opf-pptx 0.20.3 → 0.22.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/README.md +65 -3
- package/dist/annotation-provenance.js +11 -2
- package/dist/body-text-import.js +64 -7
- package/dist/chart-scheme-colors.js +47 -0
- package/dist/chart-workbook.js +7 -5
- package/dist/code-provenance.js +34 -4
- package/dist/errors.js +11 -0
- package/dist/furniture-fields.js +14 -1
- package/dist/furniture-provenance.js +13 -5
- package/dist/heading-provenance.js +3 -3
- package/dist/image-provenance.js +65 -7
- package/dist/index.d.ts +37 -1
- package/dist/index.js +145 -70
- package/dist/input-safety.js +115 -0
- package/dist/metric-provenance.js +4 -4
- package/dist/native-body.js +147 -0
- package/dist/native-footer-classification.js +104 -0
- package/dist/native-furniture.js +52 -17
- package/dist/native-text-style.js +7 -2
- package/dist/quote-provenance.js +3 -3
- package/dist/svg-image.js +9 -2
- package/dist/template-layouts.js +30 -14
- package/dist/text-provenance.js +3 -3
- package/dist/timeline-provenance.js +3 -3
- package/dist/typeface-inventory.js +5 -5
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {nativeBodyReader, joinNativeParagraphs} from './body-text-import.js';
|
|
2
|
+
import {addNativeBody, nativeBodyDefaults, nativeTitleAutofit, planNativeBody} from './native-body.js';
|
|
2
3
|
import {autoNumScheme, deriveListNumbering, withDisplayedNumbers} from './numbered-list.js';
|
|
3
4
|
import {isFaceStyleSuffix} from './font-weights.js';
|
|
4
5
|
import {importTableFrames} from './table-import.js';
|
|
@@ -29,15 +30,15 @@ import {restoreRunColors} from './run-colors.js';
|
|
|
29
30
|
import {joinWrappedText} from './content-topology.js';
|
|
30
31
|
import {applyDocumentProvenance, attachDocumentProvenance, documentProvenance, recordContentTopology, recordListRuns, restoreDocumentProvenance, joinAuthors, splitAuthors, slideListRecord, storedMetadata, DEFAULT_AUTHOR, DEFAULT_TITLE} from './document-provenance.js';
|
|
31
32
|
import {INVALID_XML_CHARACTER, nativeSections, writeSectionList} from './sections.js';
|
|
32
|
-
import {attachFurnitureFields, furniturePartFields, lineFields, nativeFieldType} from './furniture-fields.js';
|
|
33
|
+
import {attachFurnitureFields, furniturePartFields, isBareSlideNumber, lineFields, nativeFieldType} from './furniture-fields.js';
|
|
33
34
|
import {attachNativePlaceholders, defaultPlaceholderGeometry, nativeDateText, nativeFurnitureParts, nativePlaceholderForPart, writeNativeMasters} from './native-furniture.js';
|
|
34
35
|
import {liftMasterFurniture} from './master-furniture.js';
|
|
35
36
|
import {gallery} from '@openpresentation/gallery';
|
|
36
37
|
import {applyTemplateLayouts, deckTemplates, foreignLayoutReference, placeholderRegion, readLayoutTag, slideTemplate, writeTemplateLayouts} from './template-layouts.js';
|
|
37
38
|
import {importImageOrientation} from './image-import.js';
|
|
38
39
|
import {extractSignals, normalizeSignalOptions, themeFactsFor} from './import-signals.js';
|
|
39
|
-
import {placeImages, importImages, importImagePlaceholders, isImagePlaceholderGroup, imageName, imageOverlayName, imagePlaceholderGroupName, backgroundImageName, backgroundOverlayName, IMAGE_TREATMENT_KEYS, BACKGROUND_IMAGE_KEYS, IMAGE_PLACEHOLDER_TAG} from './image-provenance.js';
|
|
40
|
-
import {dedupeMedia} from './media-dedupe.js';
|
|
40
|
+
import {recordableImageSource, placeImages, importImages, importImagePlaceholders, isImagePlaceholderGroup, samePictureAspect, imageName, imageOverlayName, imagePlaceholderGroupName, backgroundImageName, backgroundOverlayName, IMAGE_TREATMENT_KEYS, BACKGROUND_IMAGE_KEYS, IMAGE_PLACEHOLDER_TAG} from './image-provenance.js';
|
|
41
|
+
import {dedupeMedia, mediaKey} from './media-dedupe.js';
|
|
41
42
|
import {placeWatermarks, importWatermark, importTextWatermark, tagTextWatermarks, watermarkName, watermarkTextName, watermarkBox, watermarkOpacity} from './watermark-provenance.js';
|
|
42
43
|
import {placeLogos, importLogo, importLogoPlaceholders, logoName, LOGO_TAG} from './logo-provenance.js';
|
|
43
44
|
import {nativeBackgroundFill, nativeImageBackgroundFill, nativePatternPreset} from './background.js';
|
|
@@ -48,11 +49,12 @@ import {themeSlotColors, writeThemeColors, schemeColorValue, schemeBackgroundFil
|
|
|
48
49
|
import {languageDiagnostics, observeLanguage, observedRtl, partScriptFonts, physicalAlignment, planScriptFonts, planSlideThemes, reconcileLanguage, reportPerSlideNotesScriptFonts, runLanguageFonts, runLanguageTag, stripRunScriptFonts, themeEastAsianFromLatin} from './script-fonts.js';
|
|
49
50
|
import { webpToPng, svgToPng, readLocalFile, localFileReadable } from '#image-fallback';
|
|
50
51
|
import { prepareSvg, svgDataUriBytes, svgRasterScale, svgBlipRelationship, attachSvgPictures } from './svg-image.js';
|
|
52
|
+
import { chartPaletteSchemes, applyChartSchemeColors } from './chart-scheme-colors.js';
|
|
51
53
|
import { rasterMetadata, pictureTransform, normalizeImageOrientation } from './image-geometry.js';
|
|
52
54
|
import {
|
|
53
|
-
catalogRecords, chartPaletteForFill, codeHighlightBands, codeHighlightColors, codeHighlightLines, codeLineNumbers, codeLineRuns, codeSyntaxPaletteForScheme, composeSlide,
|
|
55
|
+
catalogRecords, chartPaletteForFill, chartSeriesPalette, codeHighlightBands, codeHighlightColors, codeHighlightLines, codeLineNumbers, codeLineRuns, codeSyntaxPaletteForScheme, composeSlide,
|
|
54
56
|
ENGINE_DEFAULT_FONT_SCHEME, defaultSlideBackground, fitImage, fitText, layoutTable, layoutWatermark, metricTrendMark, resolveCanvasDimensions, resolveColorRoles, resolveFontFamilies,
|
|
55
|
-
resolveReference, resolveScriptFonts, resolveTextStyle, textColorForFill, textWidthMeasurer, timelineMarkerShapes, timelineTextColor, tokenizeCode
|
|
57
|
+
resolveChartColors, resolveReference, resolveScriptFonts, resolveTextStyle, textColorForFill, textWidthMeasurer, timelineMarkerShapes, timelineTextColor, tokenizeCode
|
|
56
58
|
} from "@openpresentation/opf/composition";
|
|
57
59
|
import { colorContext, resolveColorRefValue, resolveExportColor, resolveVariableColors } from "./color-ref.js";
|
|
58
60
|
import PptxGenJS from "../vendor/pptxgenjs/pptxgen.es.js";
|
|
@@ -68,6 +70,9 @@ import {
|
|
|
68
70
|
validate
|
|
69
71
|
} from "@openpresentation/opf";
|
|
70
72
|
import {checkFormat, isValidFormat} from "./format-check.js";
|
|
73
|
+
import {OPFPptxError} from './errors.js';
|
|
74
|
+
import {readInputZip} from './input-safety.js';
|
|
75
|
+
export {OPFPptxError} from './errors.js';
|
|
71
76
|
|
|
72
77
|
export {checkTypefaces, inventoryTypefaces, packageFontsUsed, THEME_SCRIPT_SUPPLEMENTS} from './typeface-inventory.js';
|
|
73
78
|
export {DEFAULT_SIGNAL_LIMITS, SIGNALS_VERSION, isMonospaceFamily} from './import-signals.js';
|
|
@@ -91,18 +96,6 @@ export const runtimePolicy = Object.freeze({
|
|
|
91
96
|
deterministicLocalExecution: true
|
|
92
97
|
});
|
|
93
98
|
|
|
94
|
-
export class OPFPptxError extends Error {
|
|
95
|
-
constructor(code, message, details = {}) {
|
|
96
|
-
super(message);
|
|
97
|
-
this.name = "OPFPptxError";
|
|
98
|
-
this.code = code;
|
|
99
|
-
this.details = details;
|
|
100
|
-
if (details.findings) this.findings = details.findings;
|
|
101
|
-
if (details.diagnostics) this.diagnostics = details.diagnostics;
|
|
102
|
-
if (details.path) this.path = details.path;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
99
|
const FIXED_TIMESTAMP = "1980-01-01T00:00:00Z";
|
|
107
100
|
// fflate derives the DOS zip date from local-time fields, so build this from
|
|
108
101
|
// local components: a UTC instant rolls back to 1979 west of UTC (below
|
|
@@ -208,21 +201,6 @@ const DIMENSION_PRESETS = Object.freeze({
|
|
|
208
201
|
a4: Object.freeze({ widthInches: 11.69, heightInches: 8.27 })
|
|
209
202
|
});
|
|
210
203
|
|
|
211
|
-
const CHART_COLORS = [
|
|
212
|
-
"2874A6",
|
|
213
|
-
"1B4F72",
|
|
214
|
-
"5499C7",
|
|
215
|
-
"7BDBB2",
|
|
216
|
-
"3AC67A",
|
|
217
|
-
"24A89E",
|
|
218
|
-
"F59E0B",
|
|
219
|
-
"EF4444",
|
|
220
|
-
"8B5CF6",
|
|
221
|
-
"14B8A6",
|
|
222
|
-
"0F172A",
|
|
223
|
-
"64748B"
|
|
224
|
-
];
|
|
225
|
-
|
|
226
204
|
// The `catalogs` option is core's registered catalogs (`Catalog[]`). A value that is no array is core's
|
|
227
205
|
// OPFCatalogsOptionError (code `invalid-catalogs`) up front; core checks each entry where it resolves a reference.
|
|
228
206
|
function checkCatalogs(entry, catalogs) {
|
|
@@ -230,12 +208,21 @@ function checkCatalogs(entry, catalogs) {
|
|
|
230
208
|
}
|
|
231
209
|
|
|
232
210
|
export async function toPptx(input, options = {}) {
|
|
211
|
+
if (options.bodyText !== undefined && !['measured', 'native'].includes(options.bodyText)) {
|
|
212
|
+
throw new OPFPptxError('invalid-body-text-mode', 'bodyText must be measured or native.', {path: 'options.bodyText'});
|
|
213
|
+
}
|
|
233
214
|
if (options.imageFormat !== undefined && !['compatible', 'preserve'].includes(options.imageFormat)) {
|
|
234
215
|
throw new OPFPptxError('invalid-image-format', 'imageFormat must be compatible or preserve.', {path: 'options.imageFormat'});
|
|
235
216
|
}
|
|
217
|
+
if (options.slideNumberFields !== undefined && !['static', 'live'].includes(options.slideNumberFields)) {
|
|
218
|
+
throw new OPFPptxError('invalid-slide-number-option', "slideNumberFields must be 'static' or 'live'.", {path: 'options.slideNumberFields'});
|
|
219
|
+
}
|
|
236
220
|
if (options.provenance !== undefined && !['full', 'references-only', false].includes(options.provenance)) {
|
|
237
221
|
throw new OPFPptxError('invalid-provenance-option', "provenance must be 'full', 'references-only' or false.", {path: 'options.provenance'});
|
|
238
222
|
}
|
|
223
|
+
if (options.footerText !== undefined && !['native', 'shape'].includes(options.footerText)) {
|
|
224
|
+
throw new OPFPptxError('invalid-footer-text-option', "footerText must be 'native' or 'shape'.", {path: 'options.footerText'});
|
|
225
|
+
}
|
|
239
226
|
// OPF 0.15 (FA-23): the host's registered catalogs (core `Catalog[]`, matched by `source`, never fetched) reach every core
|
|
240
227
|
// call that resolves a reference. Strict export (`strictReferences`, default `strictAssets`) fails on a reference that
|
|
241
228
|
// resolves nowhere instead of composing automatically or drawing with the engine default.
|
|
@@ -281,6 +268,8 @@ export async function toPptx(input, options = {}) {
|
|
|
281
268
|
context.provenanceMode = options.provenance ?? "full";
|
|
282
269
|
context.headingTags = new Map();
|
|
283
270
|
context.plainTextTags = new Map();
|
|
271
|
+
context.nativeBodies = new Map();
|
|
272
|
+
context.nativeTitles = new Set();
|
|
284
273
|
context.timelineTags = new Map();
|
|
285
274
|
context.quoteTags = new Map();
|
|
286
275
|
context.quotePhotos = new Map();
|
|
@@ -290,10 +279,12 @@ export async function toPptx(input, options = {}) {
|
|
|
290
279
|
context.furnitureManifests = new Map();
|
|
291
280
|
context.nativeFurniture = new Map();
|
|
292
281
|
context.nativeFurnitureBands = new Map();
|
|
282
|
+
context.footerText = options.footerText ?? 'native';
|
|
293
283
|
// RR-72: each slide's furniture parts and the object names of their shapes, for the slide master and layouts.
|
|
294
284
|
context.furnitureParts = [];
|
|
295
285
|
// RR-81: the 0.19 template each slide uses (its native slide layout) and the objects bound to a region placeholder.
|
|
296
286
|
context.templateSlides = [];
|
|
287
|
+
context.staticSlideNumbers = options.slideNumberFields !== 'live';
|
|
297
288
|
context.automaticSlides = [];
|
|
298
289
|
context.placeholderBindings = new Map();
|
|
299
290
|
context.hostDate = options.date;
|
|
@@ -472,7 +463,7 @@ export async function fromPptx(input, options = {}) {
|
|
|
472
463
|
return match ? shapeBounds(match['p:spPr']?.['a:xfrm']) ?? undefined : undefined;
|
|
473
464
|
} : undefined,
|
|
474
465
|
// RR-11: native footer placeholders resolve their position through the layout and master.
|
|
475
|
-
path: slidePath, slideWidth: Number(presentationRoot['p:sldSz']?.cx), relationshipsOf: cachedRelationships, readPart: cachedPart,
|
|
466
|
+
path: slidePath, slideWidth: Number(presentationRoot['p:sldSz']?.cx), slideHeight: Number(presentationRoot['p:sldSz']?.cy), defaultTextStyle: presentationRoot['p:defaultTextStyle'], relationshipsOf: cachedRelationships, readPart: cachedPart,
|
|
476
467
|
paragraphs: nativeShapeParagraphs(decodeText(entries[slidePath])),
|
|
477
468
|
readPicture: picture => importPicture(entries, picture, slidePath, relationships,
|
|
478
469
|
diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${index}.design`})),
|
|
@@ -642,8 +633,9 @@ function readPptxZip(input) {
|
|
|
642
633
|
}
|
|
643
634
|
|
|
644
635
|
try {
|
|
645
|
-
return
|
|
636
|
+
return readInputZip(bytes);
|
|
646
637
|
} catch (error) {
|
|
638
|
+
if (error instanceof OPFPptxError) throw error;
|
|
647
639
|
throw new OPFPptxError("invalid-pptx", "PPTX input is not a readable ZIP archive.", {
|
|
648
640
|
cause: errorMessage(error)
|
|
649
641
|
});
|
|
@@ -809,6 +801,7 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
809
801
|
// The recorded crop, fit and focus are re-derived from the restored background.
|
|
810
802
|
if (diagnostic.code !== 'unsupported-image-crop') options.onDiagnostic?.({...diagnostic, path: `slides.${slideIndex}.design.background`});
|
|
811
803
|
}),
|
|
804
|
+
mediaKeyOf: picture => { const bytes = entries[relationships.get(picture?.['p:blipFill']?.['a:blip']?.['r:embed'])?.path]; return bytes ? mediaKey(bytes) : undefined; },
|
|
812
805
|
valid: (kind, value) => isValidFormat({slides: [kind === 'background' ? {design: {background: value}} : {blocks: [value]}]}),
|
|
813
806
|
report: diagnostic => options.onDiagnostic?.(diagnostic)});
|
|
814
807
|
const imageBackground = images.background ?? (background?.type === 'image' && images.backgroundOverlay ? {...background, overlay: images.backgroundOverlay} : background);
|
|
@@ -819,6 +812,7 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
819
812
|
if (imageBackground) slide.design = {background: imageBackground};
|
|
820
813
|
if (Object.keys(furniture.design).length) slide.design = {...slide.design, ...furniture.design};
|
|
821
814
|
nativeContext.imageBlocks = images.blocks;
|
|
815
|
+
nativeContext.imageSources = images.sources;
|
|
822
816
|
nativeContext.imagePictures = images.consumed;
|
|
823
817
|
// An overlay of a background that was not recovered stays an ordinary shape.
|
|
824
818
|
nativeContext.imageShapes = images.consumedShapes;
|
|
@@ -875,8 +869,10 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
875
869
|
for (const item of items) if (item.readNativeBody) {
|
|
876
870
|
const current = item.readNativeBody();
|
|
877
871
|
if (current) {
|
|
878
|
-
item.paragraphs = current.map((paragraph, index) => ({...item.paragraphs[index], ...paragraph}));
|
|
872
|
+
item.paragraphs = withDisplayedNumbers(current.map((paragraph, index) => ({...item.paragraphs[index], ...paragraph})));
|
|
879
873
|
item.text = current.map(paragraph => paragraph.text).join('\n');
|
|
874
|
+
if (item.paragraphs.some(paragraph => paragraph.bullet) && item.paragraphs.some(paragraph => !paragraph.bullet))
|
|
875
|
+
options.onDiagnostic?.({code: 'mixed-body-paragraphs', path: `slides.${slideIndex}`, message: 'A native body mixes bulleted and plain paragraphs; current text is retained as list entries because OPF has no mixed-paragraph body model.'});
|
|
880
876
|
}
|
|
881
877
|
}
|
|
882
878
|
const listRecord = slideListRecord(entries, slidePath, slideRoot, relationships);
|
|
@@ -1048,7 +1044,7 @@ function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensi
|
|
|
1048
1044
|
const treatment = nativeContext.imageBlocks?.get(index);
|
|
1049
1045
|
const report = diagnostic => { if (!(treatment && diagnostic.code === 'unsupported-image-crop')) options.onDiagnostic?.({...diagnostic, path: `slides.${slideIndex}.pictures.${index}`}); };
|
|
1050
1046
|
const item = importPicture(entries, picture, slidePath, relationships, report);
|
|
1051
|
-
if (item?.payload && treatment) item.payload = {...item.payload, ...structuredClone(treatment)};
|
|
1047
|
+
if (item?.payload && treatment) item.payload = {...item.payload, ...structuredClone(treatment), ...(nativeContext.imageSources?.has(index) ? {image: {...item.payload.image, src: nativeContext.imageSources.get(index)}} : {})};
|
|
1052
1048
|
else if (item?.nativeStretch) { item.payload.fit='stretch'; item.inferredStretch=true; }
|
|
1053
1049
|
else if (item?.nativeFitUnsupported) report({code:'unsupported-image-fit',message:'Native picture tiling or an offset stretch fill is not represented by the imported image block; review the reflowed image.'});
|
|
1054
1050
|
if (item) items.push({...item, sources: [`pic:${index}`]});
|
|
@@ -1232,13 +1228,12 @@ function nativePictureStretch(picture,bytes,bounds,svg) {
|
|
|
1232
1228
|
if(!bounds||!(bounds.w>0&&bounds.h>0)||!metadata||!(metadata.width>0&&metadata.height>0)||metadata.orientation>1) return false;
|
|
1233
1229
|
if(stretch===undefined||fill?.['a:tile']!==undefined||Number(transform?.rot??0)!==0) return false;
|
|
1234
1230
|
if([crop,rect].some(value=>value&&['l','r','t','b'].some(key=>Number(value[key]??0)!==0)))return false;
|
|
1235
|
-
// Compare
|
|
1231
|
+
// Compare the current raster and current native frame, including only the
|
|
1232
|
+
// exporter's representable rounding cells (not a visual geometry tolerance).
|
|
1236
1233
|
const ext=transform?.['a:ext'];
|
|
1237
1234
|
const cx=Number(ext?.cx),cy=Number(ext?.cy);
|
|
1238
1235
|
if(!Number.isFinite(cx)||!Number.isFinite(cy))return false;
|
|
1239
|
-
|
|
1240
|
-
// reproduced by rounding the intrinsic ratio is still the same aspect.
|
|
1241
|
-
return Math.round(cx*metadata.height/metadata.width)!==cy&&Math.round(cy*metadata.width/metadata.height)!==cx;
|
|
1236
|
+
return !samePictureAspect(cx,cy,metadata.width,metadata.height);
|
|
1242
1237
|
}
|
|
1243
1238
|
|
|
1244
1239
|
// Provenance may rebuild flat blocks into groups or scalar fields. Remove only
|
|
@@ -1329,7 +1324,7 @@ function takeTitleItem(items, dimensions, inferHeadings) {
|
|
|
1329
1324
|
|
|
1330
1325
|
const titleLimit = dimensions.heightInches * 0.28;
|
|
1331
1326
|
const candidateIndex = items.findIndex((item) => {
|
|
1332
|
-
if (item.heading || item.kind !== "text" || !item.text || item.paragraphs.some(p=>p.bullet)) return false;
|
|
1327
|
+
if (item.heading || item.placeholder === 'body' || item.kind !== "text" || !item.text || item.paragraphs.some(p=>p.bullet)) return false;
|
|
1333
1328
|
const y = item.bounds?.y ?? 0;
|
|
1334
1329
|
return y <= titleLimit && (item.maxFontSize >= 20 || /^title\b/i.test(item.name ?? ""));
|
|
1335
1330
|
});
|
|
@@ -1345,7 +1340,7 @@ function takeSubtitleItem(items, titleItem, dimensions, inferHeadings) {
|
|
|
1345
1340
|
const titleBottom = (titleItem.bounds?.y ?? 0) + (titleItem.bounds?.h ?? 0);
|
|
1346
1341
|
const subtitleLimit = Math.min(dimensions.heightInches * 0.34, 1.45);
|
|
1347
1342
|
const candidateIndex = items.findIndex((item) => {
|
|
1348
|
-
if (item.heading || item.kind !== "text" || !item.text || item.paragraphs.some(p=>p.bullet)) return false;
|
|
1343
|
+
if (item.heading || item.placeholder === 'body' || item.kind !== "text" || !item.text || item.paragraphs.some(p=>p.bullet)) return false;
|
|
1349
1344
|
const y = item.bounds?.y ?? 0;
|
|
1350
1345
|
const h = item.bounds?.h ?? 0;
|
|
1351
1346
|
return y >= titleBottom - 0.05
|
|
@@ -1529,6 +1524,15 @@ function chartFromRelationship(entries, slidePath, relationships, relId, report)
|
|
|
1529
1524
|
if (chartNode.type === 'scatter') return withOptions(scatterFromSeries(entries, relationship.path, series, budget, cachePath, {codes: valueCodes, xCode, report}));
|
|
1530
1525
|
const labels = cachedValues(series[0]?.["c:cat"], cachePath(0, 'c:cat'), budget);
|
|
1531
1526
|
const names = series.map((entry, index) => firstCachedValue(entry?.["c:tx"], cachePath(index, 'c:tx'), budget) ?? `Series ${index + 1}`);
|
|
1527
|
+
// RR-86: explicit current line paint has no per-series OPF field. Report the native locator without
|
|
1528
|
+
// inferring semantic highlight, rewriting the theme or claiming that these colors differ visually.
|
|
1529
|
+
for (const [index, entry] of series.entries()) {
|
|
1530
|
+
const line = comboSeries ? comboSeries[index].role === 'line' : asArray(plotArea['c:lineChart']).includes(chartNode.node);
|
|
1531
|
+
const solid = line && entry?.['c:spPr']?.['a:ln']?.['a:solidFill'];
|
|
1532
|
+
if (solid && ['a:srgbClr', 'a:schemeClr', 'a:sysClr', 'a:prstClr', 'a:scrgbClr', 'a:hslClr'].some(color => Object.hasOwn(solid, color))) {
|
|
1533
|
+
report?.({option: `series.${index}.line.color`, message: `Line series '${names[index]}' has explicit native solid paint that is not retained as per-series formatting in OPF; imported chart colors follow its design and semantic highlight.`});
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1532
1536
|
const values = series.map((entry, index) => {
|
|
1533
1537
|
const role = entry?.["c:val"] !== undefined ? 'c:val' : 'c:yVal';
|
|
1534
1538
|
return cachedValues(entry?.[role], cachePath(index, role), budget, strictCacheValue(budget));
|
|
@@ -2136,9 +2140,12 @@ async function addSlide(pptx, presentation, authoredSlide, slideIndex, context,
|
|
|
2136
2140
|
for (const item of geometry.items) if (item.listColumns?.length > 1 && item.text?.listEntries) item.text = {...item.text, listEntries: item.listColumns.flatMap(column => column.text?.listEntries ?? [])};
|
|
2137
2141
|
// RR-81: a slide on a 0.19 template uses that template's native slide layout (src/template-layouts.js), whose placeholders
|
|
2138
2142
|
// core composes with this slide's options (canvas, the deck's header and footer, direction) and its placed-image bands.
|
|
2139
|
-
|
|
2143
|
+
let template = slideTemplate(slideContext.core);
|
|
2144
|
+
const automatic = opfSlide.layout === undefined || opfSlide.layout === 'auto';
|
|
2145
|
+
const nativeBody = options.bodyText === 'native' ? planNativeBody(opfSlide, geometry, {automatic, report: options.onDiagnostic}) : undefined;
|
|
2146
|
+
if (template && nativeBody) template = {...template, key: `${template.key}\u0000native-body:${nativeBody.region}`, nativeBodyRegion: nativeBody.region};
|
|
2140
2147
|
if (template) context.templateSlides[slideIndex] = {template, areaOptions: {...composeOptions, placements: placedImageBands(opfSlide)}};
|
|
2141
|
-
else if (
|
|
2148
|
+
else if (automatic) context.automaticSlides[slideIndex] = {areaOptions: composeOptions, title: geometry.items.find(item => item.field === 'title'), ...(nativeBody ? {nativeBody} : {})};
|
|
2142
2149
|
// The default footer band (date left, text center, number right): where a footer added natively lands (RR-11).
|
|
2143
2150
|
if (slideIndex === 0) context.defaultFooterOptions = composeOptions;
|
|
2144
2151
|
slideContext.zoneBand = zoneBands(composeOptions);
|
|
@@ -2159,7 +2166,8 @@ async function addSlide(pptx, presentation, authoredSlide, slideIndex, context,
|
|
|
2159
2166
|
|
|
2160
2167
|
for (const diagnostic of geometry.diagnostics) options.onDiagnostic?.(diagnostic);
|
|
2161
2168
|
// The content topology (groups, regions, root form, block ids) with the leaf boxes this geometry draws.
|
|
2162
|
-
|
|
2169
|
+
const nativePlans = nativeBody ? [nativeBody, ...nativeBody.others] : [];
|
|
2170
|
+
recordContentTopology(context.documentProvenance, opfSlide, slideIndex, nativeBody ? geometry.items.map(item => { const plan = nativePlans.find(plan => plan.item === item); return plan ? {...item, box: plan.box, text: undefined} : item; }) : geometry.items);
|
|
2163
2171
|
// Paint order, as in the preview (FA-22 contract): canvas colour, the background picture and its overlay, design.watermark,
|
|
2164
2172
|
// the logo, then the items in core's order: the headings, the placed image blocks at their bands, then the flowed body.
|
|
2165
2173
|
if (geometry.backgroundImage) await addBackgroundImage(slide, presentation, geometry.backgroundImage, slideIndex, slideContext, context, options);
|
|
@@ -2176,7 +2184,10 @@ async function addSlide(pptx, presentation, authoredSlide, slideIndex, context,
|
|
|
2176
2184
|
// object or one drawn on a card stays a free shape: Change Layout would move it away from its caption or card.
|
|
2177
2185
|
const boundRegion = template && item.region !== undefined && ['image', 'chart', 'table'].includes(item.field) && !item.caption && !item.frameBox
|
|
2178
2186
|
? geometry.regions?.find(region => region.name === item.region && region.flow === 'none' && region.content?.length === 1 && !region.overflow?.length) : undefined;
|
|
2179
|
-
|
|
2187
|
+
// RR-88: under bodyText 'native' a plain-string title that wraps is one native text box (native wrap, no autofit) at its measured
|
|
2188
|
+
// block, so it can bind to the Title placeholder like a one-line title (Keynote reports `title showing` only for a bound title).
|
|
2189
|
+
const nativeTitle = options.bodyText === 'native' && (template || context.automaticSlides[slideIndex]) && isNativeTitle(item);
|
|
2190
|
+
if ((template || context.automaticSlides[slideIndex]) && item.field === 'title' && typeof item.value === 'string' && (item.text?.lines?.length === 1 || nativeTitle) && !item.text.overflow && !item.text.richLines)
|
|
2180
2191
|
context.placeholderBindings.set(`OPF heading ${item.path} line 0`, {slide: slideIndex, region: 'Title', object: 'title'});
|
|
2181
2192
|
const drawnBefore = boundRegion && {images: context.images.size, placeholders: context.imagePlaceholders.size, charts: context.chartHeadings.size, tables: context.tableHeaders.size};
|
|
2182
2193
|
// Card text sits on a literal card fill, not the slide background: keep it literal.
|
|
@@ -2191,7 +2202,15 @@ async function addSlide(pptx, presentation, authoredSlide, slideIndex, context,
|
|
|
2191
2202
|
rectRadius:8*Math.min(widthInches,heightInches)/720,
|
|
2192
2203
|
fill:paint(surface),line:{...paint(`#${itemContext.colors.border}`),width:.75},objectName:`OPF card ${item.path}`});
|
|
2193
2204
|
}
|
|
2194
|
-
if(
|
|
2205
|
+
if (nativeTitle) {
|
|
2206
|
+
addNativeTitle(slide, item, itemContext, options, context);
|
|
2207
|
+
} else if (nativePlans.some(plan => plan.item === item)) {
|
|
2208
|
+
// RR-88: the first native body is the Body placeholder; every further one is an ordinary native text box at its region.
|
|
2209
|
+
const plan = nativePlans.find(plan => plan.item === item);
|
|
2210
|
+
const body = addNativeBody(slide, plan, itemContext, options, {textBoxOptions, nativeFontOptions, exportColor, nativeColor, linkColor, langOptions, solidColorXml});
|
|
2211
|
+
context.nativeBodies.set(body.objectName, body.defaults);
|
|
2212
|
+
if (plan === nativeBody) context.placeholderBindings.set(body.objectName, {slide: slideIndex, region: nativeBody.region, object: 'body'});
|
|
2213
|
+
} else if(['text','title','subtitle','tag'].includes(item.field)&&(item.text?.placement||item.text?.sourceLines)&&!item.text.richLines) {
|
|
2195
2214
|
addMeasuredPayloadText(slide,item.value,item.box,itemContext,options,{path:item.path,fit:item.text,textStyle:item.textStyle,sourceText:item.field==='text'&&!!item.text.sourceLines,align:item.alignment,diagnosticsHandled:true,heading:['title','subtitle','tag'].includes(item.field)?item.field:undefined,color:item.field==='tag'?tagColor(itemContext):itemContext.textColor});
|
|
2196
2215
|
} else if (["title", "subtitle", "tag"].includes(item.field) && !item.text?.richLines) {
|
|
2197
2216
|
// Estimated-font headings use one native text box, which still needs a
|
|
@@ -2215,7 +2234,7 @@ async function addSlide(pptx, presentation, authoredSlide, slideIndex, context,
|
|
|
2215
2234
|
// FA-10: a TextRun[] heading draws and exports like rich body text; its lines are named and tagged as a heading (the body's are not).
|
|
2216
2235
|
const heading = item.field !== 'text';
|
|
2217
2236
|
addRichFitLines(slide, item.text, item.box, itemContext, {
|
|
2218
|
-
value: item.value, alignment: item.text.placement?.alignment ?? item.alignment ?? 'left',
|
|
2237
|
+
value: item.value, path: item.path, alignment: item.text.placement?.alignment ?? item.alignment ?? 'left',
|
|
2219
2238
|
fallback: item.field === 'tag' ? tagColor(itemContext) : itemContext.textColor,
|
|
2220
2239
|
...(heading ? {
|
|
2221
2240
|
objectName: index => `OPF heading ${item.path} line ${index}`,
|
|
@@ -2229,7 +2248,7 @@ async function addSlide(pptx, presentation, authoredSlide, slideIndex, context,
|
|
|
2229
2248
|
} else {
|
|
2230
2249
|
// RR-34: a captioned item draws its media in item.box; the caption band follows as tagged text boxes linked to the media shape by name.
|
|
2231
2250
|
const mediaNames = item.caption ? new Set([...context.images.keys(), ...context.imagePlaceholders.keys(), ...context.chartHeadings.keys(), ...context.tableHeaders.keys()]) : undefined;
|
|
2232
|
-
await addPayload(slide, presentation, item.payload, region, item.path, { ...itemContext, composition: item.composition, contentAlignment: item.alignment ?? "left", tableAlignment: geometry.design.contentAlignment, direction: geometry.direction }, options, item.quoteLayout, item.codeLayout,item.metricLayout,item.timelineLayout);
|
|
2251
|
+
await addPayload(slide, presentation, item.payload, region, item.path, { ...itemContext, composition: item.composition, contentAlignment: item.alignment ?? "left", tableAlignment: geometry.design.contentAlignment, direction: geometry.direction }, options, item.quoteLayout, item.codeLayout,item.metricLayout,item.timelineLayout, item.chart);
|
|
2233
2252
|
if (item.caption) {
|
|
2234
2253
|
const mediaName = item.field === 'video' ? `OPF media ${item.path} frame` : [...context.images.keys(), ...context.imagePlaceholders.keys(), ...context.chartHeadings.keys(), ...context.tableHeaders.keys()].find(name => !mediaNames.has(name));
|
|
2235
2254
|
addCaption(slide, item, mediaName, itemContext, exportHelpers, (code, message) => new OPFPptxError(code, message, {path: item.caption.path}));
|
|
@@ -2297,11 +2316,11 @@ function fieldToType(field) {
|
|
|
2297
2316
|
return field === "items" || field === "bullets" ? "list" : field;
|
|
2298
2317
|
}
|
|
2299
2318
|
|
|
2300
|
-
async function addPayload(slide, presentation, payload, region, path, context, options, quoteLayout, codeLayout,metricLayout,timelineLayout) {
|
|
2319
|
+
async function addPayload(slide, presentation, payload, region, path, context, options, quoteLayout, codeLayout,metricLayout,timelineLayout,chartLayout) {
|
|
2301
2320
|
const kind = inferPayloadKind(payload);
|
|
2302
2321
|
switch (kind) {
|
|
2303
2322
|
case "text":
|
|
2304
|
-
addTextPayload(slide, payload.text ?? payload.bullets, region, context);
|
|
2323
|
+
addTextPayload(slide, payload.text ?? payload.bullets, region, context, path);
|
|
2305
2324
|
break;
|
|
2306
2325
|
case "list":
|
|
2307
2326
|
addListPayload(slide, payload.items ?? payload.bullets, region, context);
|
|
@@ -2313,7 +2332,7 @@ async function addPayload(slide, presentation, payload, region, path, context, o
|
|
|
2313
2332
|
addMediaPayload(slide, presentation, payload.video, region, path, context, options);
|
|
2314
2333
|
break;
|
|
2315
2334
|
case "chart":
|
|
2316
|
-
addChartPayload(slide, payload.chart, region, context, options, path, presentation);
|
|
2335
|
+
addChartPayload(slide, payload.chart, region, context, options, path, presentation, chartLayout);
|
|
2317
2336
|
break;
|
|
2318
2337
|
case "table":
|
|
2319
2338
|
addTablePayload(slide, payload.table, region, context, options, path, presentation);
|
|
@@ -2346,9 +2365,9 @@ function inferPayloadKind(payload) {
|
|
|
2346
2365
|
return "unknown";
|
|
2347
2366
|
}
|
|
2348
2367
|
|
|
2349
|
-
function addTextPayload(slide, value, region, context) {
|
|
2368
|
+
function addTextPayload(slide, value, region, context, path) {
|
|
2350
2369
|
if (Array.isArray(value)) {
|
|
2351
|
-
slide.addText(textRuns(value, context, 18), textBoxOptions(region, context, 18));
|
|
2370
|
+
slide.addText(textRuns(value, context, 18, path), textBoxOptions(region, context, 18));
|
|
2352
2371
|
return;
|
|
2353
2372
|
}
|
|
2354
2373
|
if (Array.isArray(value?.bullets)) {
|
|
@@ -2366,9 +2385,10 @@ function listRunLook(fit,style,context){
|
|
|
2366
2385
|
if(text.size!==1||description.size>1)return undefined;
|
|
2367
2386
|
return {text:JSON.parse([...text][0]),...(description.size?{description:JSON.parse([...description][0])}:{})};
|
|
2368
2387
|
}
|
|
2369
|
-
function richLineRuns(line,color,native,context) {
|
|
2388
|
+
function richLineRuns(line,color,native,context,path) {
|
|
2370
2389
|
const fallback=color.replace(/^#/,'');
|
|
2371
2390
|
return line.fragments.map(fragment=>{
|
|
2391
|
+
assertXmlText(fragment.run.text ?? fragment.text, path);
|
|
2372
2392
|
const runColor=exportColor(fragment.run.color,context,fallback),color=nativeColor(fragment.run.color,runColor,context,native);
|
|
2373
2393
|
return {text:fragment.text,options:{...nativeFontOptions(fragment.style),fontSize:(fragment.nominalSize??fragment.fontSize)*.75,color:linkColor(fragment.run,color,context),underline:fragment.run.underline?{style:'sng',color:linkColor(fragment.run,color,context)}:undefined,strike:fragment.run.strikethrough?'sngStrike':undefined,baseline:fragment.baselineShift?-fragment.baselineShift/(fragment.nominalSize??fragment.fontSize)*2000:undefined,hyperlink:fragment.run.link&&/^(https?:|mailto:|tel:)/i.test(fragment.run.link)?{url:fragment.run.link}:undefined,...langOptions(context,fragment.style.lang)}};
|
|
2374
2394
|
});
|
|
@@ -2416,7 +2436,7 @@ async function addMeasuredList(slide,fit,context,path,bulletImage,presentation,s
|
|
|
2416
2436
|
const number=first?entry.marker.number:undefined;
|
|
2417
2437
|
if(first)context.listMarkers.set(objectName,{fontFamily:entry.marker.style.fontFamily,fontSize:entry.marker.fontSize*.75,color:pptxColor(context.textColor),picture:number?false:picture});
|
|
2418
2438
|
const paragraph=first?{bullet:number?{type:'number',style:autoNumScheme(number.style,number.suffix),startAt:number.value,indent:entry.marker.indent*.75}:{characterCode:entry.marker.text.codePointAt(0).toString(16).padStart(4,'0'),indent:entry.marker.indent*.75},indentLevel:level}:{bullet:false};
|
|
2419
|
-
const runs=richLineRuns(line,color,native,context);
|
|
2439
|
+
const runs=richLineRuns(line,color,native,context,path);
|
|
2420
2440
|
if(!runs.length)runs.push({text:'',options:{}});
|
|
2421
2441
|
// Keep paragraph intent identical across runs. ZIP normalization below
|
|
2422
2442
|
// removes the duplicate paragraph-property nodes emitted by PptxGenJS.
|
|
@@ -2479,7 +2499,7 @@ async function addImagePayload(slide, presentation, asset, region, path, context
|
|
|
2479
2499
|
// One native image picture at `box` (inches), fitted at packaging (src/image-provenance.js placeImages). `treatment` is
|
|
2480
2500
|
// what OPF_IMAGE_V1 stores (the authored block fields); without it the picture is untagged. Returns the picture's object
|
|
2481
2501
|
// name, or undefined when the source could not be embedded (the caller draws the unavailable-image panel).
|
|
2482
|
-
async function addImagePicture(slide, presentation, asset, box, path, context, options, {fit, focus, effects = null, shrink = true, treatment, alt, role = 'block', objectName} = {}) {
|
|
2502
|
+
async function addImagePicture(slide, presentation, asset, box, path, context, options, {fit, focus, effects = null, shrink = true, treatment, alt, role = 'block', objectName, src} = {}) {
|
|
2483
2503
|
const outcome = { box, cover: fit === 'cover' };
|
|
2484
2504
|
const resolved = await resolveImage(asset, presentation, options, path, outcome);
|
|
2485
2505
|
if (!resolved) {
|
|
@@ -2497,7 +2517,8 @@ async function addImagePicture(slide, presentation, asset, box, path, context, o
|
|
|
2497
2517
|
else context.svgPictures.set(name, outcome.svg);
|
|
2498
2518
|
}
|
|
2499
2519
|
const text = alt !== undefined ? alt : assetAlt(asset, presentation);
|
|
2500
|
-
|
|
2520
|
+
// RR-94: the authored relative path or URL is recorded with the picture's media key; an SVG imports from its own part, so it is not.
|
|
2521
|
+
context.images.set(name, { slide: context.slidePath, role, path, box, fit, focus, shrink, effects, ...(treatment !== undefined ? {treatment} : {}), ...(src !== undefined && !outcome.svg ? {src} : {}) });
|
|
2501
2522
|
context.pictureText.set(name, pictureText(text, asset, presentation));
|
|
2502
2523
|
slide.addImage({ ...resolved, objectName: name, x: box.x, y: box.y, w: box.w, h: box.h, altText: text });
|
|
2503
2524
|
return name;
|
|
@@ -2545,11 +2566,13 @@ async function addImageItem(slide, presentation, item, slideIndex, context, opti
|
|
|
2545
2566
|
const treatment = Object.fromEntries(IMAGE_TREATMENT_KEYS.filter(key => key !== 'overlay' && block[key] !== undefined).map(key => [key, structuredClone(block[key])]));
|
|
2546
2567
|
const effects = imageEffects(image, context);
|
|
2547
2568
|
const treated = Boolean(effects.border || effects.opacity !== null || effects.recolor || image.overlay || (effects.shape && effects.shape.preset !== 'rect'));
|
|
2548
|
-
|
|
2569
|
+
// RR-94: every image block is tagged (an empty treatment when it has none), so an unchanged picture re-imports without a crop
|
|
2570
|
+
// diagnostic, and one authored from a relative path or URL records that source to give it back.
|
|
2571
|
+
const source = recordableImageSource(item.value);
|
|
2549
2572
|
// A plain contained picture in the flow is framed by the picture itself (0.14 content images); a treated or placed one
|
|
2550
2573
|
// keeps core's frame (0.14 slide images), so its mask, line and overlay apply to that frame.
|
|
2551
2574
|
const name = await addImagePicture(slide, presentation, item.value, box, item.path, { ...context, slidePath: `slides.${slideIndex}` }, options, {
|
|
2552
|
-
fit: image.fit, focus: image.focus, effects: treated ? effects : null, shrink: !treated && !image.placement, treatment
|
|
2575
|
+
fit: image.fit, focus: image.focus, effects: treated ? effects : null, shrink: !treated && !image.placement, treatment, src: source
|
|
2553
2576
|
});
|
|
2554
2577
|
if (!name) {
|
|
2555
2578
|
const panel = addImagePlaceholder(slide, presentation, item.value, box, item.path, context, options);
|
|
@@ -2692,7 +2715,7 @@ function applyChartTextSize(xml, size) {
|
|
|
2692
2715
|
`<a:defRPr${/\bsz="[^"]*"/.test(attributes) ? attributes.replace(/\bsz="[^"]*"/, `sz="${size}"`) : `${attributes} sz="${size}"`}${close}>`));
|
|
2693
2716
|
}
|
|
2694
2717
|
|
|
2695
|
-
function addChartPayload(slide, chart, region, context, options = {}, path = "chart", presentation) {
|
|
2718
|
+
function addChartPayload(slide, chart, region, context, options = {}, path = "chart", presentation, chartLayout) {
|
|
2696
2719
|
const chartData = toPptxChartData(chart, context.chartexMode, presentation, path);
|
|
2697
2720
|
if (!chartData.series) {
|
|
2698
2721
|
// Never lose a chart silently: the placeholder frame stands in for it, and a diagnostic names the reason.
|
|
@@ -2732,8 +2755,15 @@ function addChartPayload(slide, chart, region, context, options = {}, path = "ch
|
|
|
2732
2755
|
context.chartFonts.set(objectName,{heading:context.fonts.heading,body:context.fonts.body});
|
|
2733
2756
|
// FA-09: the chart's text alternative (the frame's descr; the empty string is PowerPoint's decorative marker), applied before the chartex frame copies the head.
|
|
2734
2757
|
if (typeof chart.alt === 'string') context.frameAlts.set(objectName, chart.alt);
|
|
2735
|
-
|
|
2736
|
-
|
|
2758
|
+
// RR-89: the series colours are core's chartSeriesPalette (accent1 to accent6 of the deck scheme, then their tints, with the chart's own
|
|
2759
|
+
// `colors` over them), adjusted for the chart panel like the preview does. A colour that is still an unadjusted theme accent (or a
|
|
2760
|
+
// `chart.colors` scheme slot or role) is written as a schemeClr, so re-theming in PowerPoint recolours the series.
|
|
2761
|
+
const colorContext = {roles: context.roles, variables: context.variables};
|
|
2762
|
+
const preferredPalette = chartSeriesPalette(context.colorScheme, chart.colors, colorContext);
|
|
2763
|
+
const adjustedPalette = chartPaletteForFill(panelFill, preferredPalette);
|
|
2764
|
+
const palette = adjustedPalette.map(color=>normalizeHex(color));
|
|
2765
|
+
const paletteSchemes = chartPaletteSchemes(chart.colors, preferredPalette, adjustedPalette, (reference, hex) => schemeColorValue(reference, `#${hex}`, context));
|
|
2766
|
+
Object.assign(context.chartHeadings.get(objectName), {palette, paletteSchemes, circular});
|
|
2737
2767
|
// FA-14: chart.highlight. A highlighted chart writes accent and muted colours over the series palette, and its data labels
|
|
2738
2768
|
// contrast with those colours (src/chart-highlight.js).
|
|
2739
2769
|
const highlight = chartData.chartex ? undefined : chartHighlightPlan({options: chartOptions, data: chartData.resolved, kind: chartTargetFor(chart.type)?.kind, panelFill, labelColor, primary: `#${context.colors.accent}`,
|
|
@@ -2786,6 +2816,9 @@ function addChartPayload(slide, chart, region, context, options = {}, path = "ch
|
|
|
2786
2816
|
...(chartData.series.every(entry => entry.values.every(value => value == null || value >= 0)) ? {valAxisMinVal: 0} : {}),
|
|
2787
2817
|
...(chartData.series.some(entry => entry.values.some(value => value < 0)) && chartData.series.every(entry => entry.values.every(value => value == null || value <= 0)) ? {valAxisMaxVal: 0} : {})
|
|
2788
2818
|
} : {}),
|
|
2819
|
+
// RR-92: the value axis core composed (column, bar, line and area charts), as the preview draws it: PowerPoint and Keynote
|
|
2820
|
+
// otherwise choose their own bounds and major unit. A 100% stacked chart's axis is a fraction.
|
|
2821
|
+
...(chartLayout?.valueAxis ? {valAxisMinVal: chartLayout.valueAxis.min, valAxisMaxVal: chartLayout.valueAxis.max, valAxisMajorUnit: chartLayout.valueAxis.majorUnit} : {}),
|
|
2789
2822
|
barDir: chartData.barDir,
|
|
2790
2823
|
barGrouping: chartData.barGrouping,
|
|
2791
2824
|
// Right to left (RR-05): column, line and area charts reverse their categories (c:catAx orientation maxMin), which also moves the
|
|
@@ -2876,6 +2909,7 @@ function addTablePayload(slide, authoredTable, region, context, options, path, p
|
|
|
2876
2909
|
const defaultText = cellStyle.color === undefined ? tableTextSchemeValue(fillValue, baseColor, context) : undefined;
|
|
2877
2910
|
const alpha = value => value.length === 8 ? (1 - parseInt(value.slice(6), 16) / 255) * 100 : 0;
|
|
2878
2911
|
const text = stringifyText(cell.value), style = cell.textStyle;
|
|
2912
|
+
assertXmlText(text, cell.path ?? path);
|
|
2879
2913
|
const fragments = rich ? fit.richLines.flatMap(line => line.fragments) : [];
|
|
2880
2914
|
const runs = rich ? cell.value.flatMap((value, index) => {
|
|
2881
2915
|
const run = typeof value === 'string' ? {text:value} : value;
|
|
@@ -2955,10 +2989,11 @@ function pixelBox(region) {
|
|
|
2955
2989
|
// super/subscript (a citation marker is a baseline run) and http(s)/mailto links. The body's lines stay untagged; a heading or quote
|
|
2956
2990
|
// body passes `objectName` and `tag`, which name and tag every line with the boundary between wrapped lines (a soft wrap inserts
|
|
2957
2991
|
// nothing, a hard line break keeps its separator), recovered from the flattened text. `fallback` is the color of runs without one.
|
|
2958
|
-
function addRichFitLines(slide, fit, box, context, {value, alignment: logicalAlignment, fallback, objectName, tag}) {
|
|
2992
|
+
function addRichFitLines(slide, fit, box, context, {value, path, alignment: logicalAlignment, fallback, objectName, tag}) {
|
|
2959
2993
|
const region = {x: box.x / 96, y: box.y / 96, w: box.width / 96, h: box.height / 96};
|
|
2960
2994
|
const runText = run => typeof run === 'string' ? run : run.text;
|
|
2961
2995
|
const whole = typeof value === 'string' ? value : value.map(runText).join('');
|
|
2996
|
+
assertXmlText(whole, path);
|
|
2962
2997
|
const count = fit.richLines.length;
|
|
2963
2998
|
let cursor = 0;
|
|
2964
2999
|
for (const [index, line] of fit.richLines.entries()) {
|
|
@@ -2984,11 +3019,41 @@ function addRichFitLines(slide, fit, box, context, {value, alignment: logicalAli
|
|
|
2984
3019
|
}
|
|
2985
3020
|
}
|
|
2986
3021
|
|
|
3022
|
+
function assertXmlText(text, path) {
|
|
3023
|
+
const invalid=/[\u0000-\u0008\u000B\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/u.exec(String(text??''));
|
|
3024
|
+
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});
|
|
3025
|
+
}
|
|
3026
|
+
|
|
2987
3027
|
// Match the published renderer's payload geometry and shared core text fitting.
|
|
2988
3028
|
// Each fitted line remains native editable text, without PowerPoint rewrapping it.
|
|
3029
|
+
// RR-88: a plain-string title that wraps over several lines, with no hard break, overflow, rich runs or right-to-left line.
|
|
3030
|
+
function isNativeTitle(item) {
|
|
3031
|
+
return item.field === 'title' && typeof item.value === 'string' && item.value.length > 0 && !/[\r\n\u2028\u2029]/.test(item.value) && item.text?.lines?.length > 1
|
|
3032
|
+
&& !item.text.overflow && !item.text.richLines && !item.text.directions?.includes('rtl') && !item.text.sourceLines?.some(line => line.boundary === 'hard' || line.segments?.some(segment => segment.kind === 'tab'));
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
// RR-88: the wrapped title as one native text box: the whole string, native wrap, no autofit, at the block its measured lines fill
|
|
3036
|
+
// (top of the first line to the bottom of the last), with the measured line pitch as exact spacing. Tagged as one heading line,
|
|
3037
|
+
// so import reads the current words as the title. The caller binds it to the Title placeholder.
|
|
3038
|
+
function addNativeTitle(slide, item, context, options, deckContext) {
|
|
3039
|
+
const fit = item.text, box = item.box, lines = fit.placement?.lines;
|
|
3040
|
+
const tops = fit.lines.map((_, index) => lines?.[index] ? lines[index].baseline - fit.fontSize : box.y + index * fit.lineHeight);
|
|
3041
|
+
const bottoms = fit.lines.map((_, index) => lines?.[index] ? lines[index].baseline - fit.fontSize + lines[index].height : box.y + (index + 1) * fit.lineHeight);
|
|
3042
|
+
const top = Math.min(...tops), height = Math.max(...bottoms) - top;
|
|
3043
|
+
const objectName = `OPF heading ${item.path} line 0`;
|
|
3044
|
+
deckContext.headingTags.set(objectName, {v: 1, group: item.path, field: 'title', line: 0, count: 1, boundary: 'end', separator: ''});
|
|
3045
|
+
deckContext.nativeTitles.add(objectName);
|
|
3046
|
+
const style = item.textStyle ?? resolveTextStyle({fontFamily: context.fonts.body, fontWeight: 400, path: item.path}, options.textMeasurement);
|
|
3047
|
+
slide.addText(item.value, {
|
|
3048
|
+
...textBoxOptions({x: box.x / 96, y: top / 96, w: box.width / 96, h: height / 96}, context, fit.fontSize * .75),
|
|
3049
|
+
...nativeFontOptions(style),
|
|
3050
|
+
color: pptxColor(context.textColor), align: physicalAlignment(fit.placement?.alignment ?? item.alignment ?? 'left', fit.directions?.[0]),
|
|
3051
|
+
objectName, fit: 'none', wrap: true, lineSpacing: fit.lineHeight * .75, lineSpacingMultiple: undefined
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
|
|
2989
3055
|
function addMeasuredPayloadText(slide, text, box, context, options, config) {
|
|
2990
|
-
|
|
2991
|
-
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});
|
|
3056
|
+
assertXmlText(text, config.path);
|
|
2992
3057
|
const scale = Math.min(context.dimensions.widthInches, context.dimensions.heightInches) * 96 / 720;
|
|
2993
3058
|
const style = config.textStyle ?? resolveTextStyle({fontFamily: config.fontFamily ?? context.fonts.body, fontWeight: config.fontWeight ?? 400, path: config.path}, options.textMeasurement);
|
|
2994
3059
|
const fit = config.fit ?? fitText(String(text ?? ''), box, config.fontSize * scale, (context.composition?.minFontSize ?? 16) * scale, textWidthMeasurer(style, options.textMeasurement));
|
|
@@ -3067,7 +3132,7 @@ async function addFurniture(slide,presentation,source,layout,context,options,sli
|
|
|
3067
3132
|
}
|
|
3068
3133
|
const staticDates = new Map();
|
|
3069
3134
|
// RR-11: the first footer text, date and slide number that fit one line are native placeholders (src/native-furniture.js).
|
|
3070
|
-
const natives = nativeFurnitureParts(layout);
|
|
3135
|
+
const natives = nativeFurnitureParts(layout, {staticNumbers: context.staticSlideNumbers});
|
|
3071
3136
|
for(const [index,part]of layout.parts.entries()){
|
|
3072
3137
|
const entry = {index, kind: part.kind, zone: part.zone, field: part.field, type: part.type, path: part.path, native: natives.has(index), names: []};
|
|
3073
3138
|
drawn.push(entry);
|
|
@@ -3088,6 +3153,8 @@ async function addFurniture(slide,presentation,source,layout,context,options,sli
|
|
|
3088
3153
|
// en-US field type is written as its laid-out text.
|
|
3089
3154
|
const fields=[];
|
|
3090
3155
|
for(const field of furniturePartFields(part)){
|
|
3156
|
+
// RR-91 (slideNumberFields, default 'static'): a number with words around it is fixed text; Keynote drops the words next to a live number.
|
|
3157
|
+
if(field.type==='slideNumber'&&context.staticSlideNumbers&&!isBareSlideNumber(part))continue;
|
|
3091
3158
|
const nativeType=nativeFieldType(field);
|
|
3092
3159
|
if(nativeType)fields.push({...field,nativeType});
|
|
3093
3160
|
else options.onDiagnostic?.({code:'furniture-date-fixed',path:part.path,message:`dateFormat '${field.format}' has no PowerPoint en-US date field; the current date is exported as fixed text that PowerPoint will not update.`});
|
|
@@ -3230,7 +3297,7 @@ async function addQuotePayload(slide, presentation, value, layout, context, opti
|
|
|
3230
3297
|
// FA-10: a TextRun[] quote body (core's rich lines, quotation marks joined to its first and last run) is exported line by line like rich body text, with the same quote tags.
|
|
3231
3298
|
if (part.runs && part.fit.richLines) {
|
|
3232
3299
|
addRichFitLines(slide, part.fit, part.box, context, {
|
|
3233
|
-
value: part.text, alignment: part.fit.placement?.alignment ?? context.contentAlignment ?? 'left', fallback: context.textColor,
|
|
3300
|
+
value: part.text, path: part.path, alignment: part.fit.placement?.alignment ?? context.contentAlignment ?? 'left', fallback: context.textColor,
|
|
3234
3301
|
objectName: line => `OPF quote ${group} part ${index} line ${line}`,
|
|
3235
3302
|
tag: (name, line, count, boundary) => context.quoteTags.set(name, {v: 1, role: 'text', group, part: index, line, count, ...boundary, base: richBase(part.fit, part.style, context.colors.text), ...(index === 0 && line === 0 ? {anchor} : {})})
|
|
3236
3303
|
});
|
|
@@ -3574,9 +3641,10 @@ function textBoxOptions(region, context, fontSize) {
|
|
|
3574
3641
|
// The private text helpers annotation-export.js writes captions and footnote lines with (RR-34).
|
|
3575
3642
|
const exportHelpers = {textBoxOptions, nativeFontOptions, exportColor, nativeColor};
|
|
3576
3643
|
|
|
3577
|
-
function textRuns(value, context, fallbackFontSize) {
|
|
3644
|
+
function textRuns(value, context, fallbackFontSize, path) {
|
|
3578
3645
|
const runs = Array.isArray(value) ? value : [value];
|
|
3579
3646
|
return runs.map((run) => {
|
|
3647
|
+
assertXmlText(typeof run === "string" ? run : run?.text, path);
|
|
3580
3648
|
if (typeof run === "string") {
|
|
3581
3649
|
return {
|
|
3582
3650
|
text: run,
|
|
@@ -4069,7 +4137,7 @@ async function normalizePptxZip(raw, context) {
|
|
|
4069
4137
|
attachFurnitureFields(entries,context.furnitureFields);
|
|
4070
4138
|
attachFurnitureTags(entries,context.furnitureTags,context.furnitureManifests);
|
|
4071
4139
|
// RR-11: the recorded footer shapes become PowerPoint's date, footer and slide-number placeholders.
|
|
4072
|
-
context.nativePlaceholders = attachNativePlaceholders(entries,context.nativeFurniture,context.nativeFurnitureBands);
|
|
4140
|
+
context.nativePlaceholders = attachNativePlaceholders(entries,context.nativeFurniture,context.nativeFurnitureBands,context.footerText);
|
|
4073
4141
|
// The "Image unavailable" panel of an unresolved logo: identity only, so import does not read it as content.
|
|
4074
4142
|
attachTextTags(entries,context.logoPlaceholderTags,LOGO_TAG,'opfLogoPlaceholder','logo placeholder');
|
|
4075
4143
|
// opf-pptx#221: the image block of an unavailable-image placeholder, on its panel (full provenance).
|
|
@@ -4089,6 +4157,10 @@ async function normalizePptxZip(raw, context) {
|
|
|
4089
4157
|
if(highlight){
|
|
4090
4158
|
try{entries[chartPart]=encodeText(applyChartHighlight(decodeText(entries[chartPart]),highlight));}
|
|
4091
4159
|
catch(error){throw new OPFPptxError('packaging-failed',`Chart highlight could not be written: ${errorMessage(error)}`);}
|
|
4160
|
+
}else{
|
|
4161
|
+
// RR-89: series that keep a theme accent (or a chart.colors scheme slot) are written as theme colours.
|
|
4162
|
+
const {palette,paletteSchemes,circular}=context.chartHeadings.get(name);
|
|
4163
|
+
if(paletteSchemes)entries[chartPart]=encodeText(applyChartSchemeColors(decodeText(entries[chartPart]),palette,paletteSchemes,{circular}));
|
|
4092
4164
|
}
|
|
4093
4165
|
// PptxGenJS hardcodes a black fallback in pie/doughnut label properties.
|
|
4094
4166
|
// Normalize only our generated chart text styles; point/series fills stay intact.
|
|
@@ -4166,6 +4238,8 @@ async function normalizePptxZip(raw, context) {
|
|
|
4166
4238
|
try {
|
|
4167
4239
|
placeImages(entries, context.images, context.imageOverlays, {
|
|
4168
4240
|
metadataFor: (part, id) => imageMetadata.get(parseRelationships(entries, part).get(id)?.path), fitImage,
|
|
4241
|
+
// The key of the bytes the package finally holds: the picture's orientation is normalized when the parts are written.
|
|
4242
|
+
mediaKeyFor: (part, id) => { const target = parseRelationships(entries, part).get(id)?.path, bytes = entries[target]; return bytes ? mediaKey(imageMetadata.get(target) ? normalizeImageOrientation(bytes, imageMetadata.get(target)) : bytes) : undefined; },
|
|
4169
4243
|
fail: path => { throw new OPFPptxError("unsupported-image-dimensions", "Image fitting requires readable PNG, JPEG, GIF or WebP dimensions. Supply a supported raster image through imageResolver.", { path }); }
|
|
4170
4244
|
});
|
|
4171
4245
|
} catch (error) {
|
|
@@ -4490,6 +4564,7 @@ function normalizePartBytes(path, bytes, context, renameMaps, entries, imageMeta
|
|
|
4490
4564
|
const content=body.replace(/<a:pPr\b[^>]*(?:\/>|>[\s\S]*?<\/a:pPr>)/g,node=>{properties ||= node;return '';});
|
|
4491
4565
|
return `<a:p>${properties}${content}</a:p>`;
|
|
4492
4566
|
});
|
|
4567
|
+
xml = nativeTitleAutofit(nativeBodyDefaults(xml, context.nativeBodies), context.nativeTitles);
|
|
4493
4568
|
}
|
|
4494
4569
|
// theme1.xml: FF-24 colors and names above, then FF-07 fonts here; they touch disjoint elements.
|
|
4495
4570
|
if (context.scriptFonts) xml = partScriptFonts(path, xml, context.scriptFonts, context.partSlides.get(path) ?? 0);
|