@openpresentation/opf-pptx 0.20.4 → 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/body-text-import.js +64 -7
- package/dist/chart-scheme-colors.js +47 -0
- package/dist/code-provenance.js +34 -4
- 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 +121 -46
- 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/template-layouts.js +30 -14
- package/dist/text-provenance.js +3 -3
- package/dist/timeline-provenance.js +3 -3
- 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";
|
|
@@ -199,21 +201,6 @@ const DIMENSION_PRESETS = Object.freeze({
|
|
|
199
201
|
a4: Object.freeze({ widthInches: 11.69, heightInches: 8.27 })
|
|
200
202
|
});
|
|
201
203
|
|
|
202
|
-
const CHART_COLORS = [
|
|
203
|
-
"2874A6",
|
|
204
|
-
"1B4F72",
|
|
205
|
-
"5499C7",
|
|
206
|
-
"7BDBB2",
|
|
207
|
-
"3AC67A",
|
|
208
|
-
"24A89E",
|
|
209
|
-
"F59E0B",
|
|
210
|
-
"EF4444",
|
|
211
|
-
"8B5CF6",
|
|
212
|
-
"14B8A6",
|
|
213
|
-
"0F172A",
|
|
214
|
-
"64748B"
|
|
215
|
-
];
|
|
216
|
-
|
|
217
204
|
// The `catalogs` option is core's registered catalogs (`Catalog[]`). A value that is no array is core's
|
|
218
205
|
// OPFCatalogsOptionError (code `invalid-catalogs`) up front; core checks each entry where it resolves a reference.
|
|
219
206
|
function checkCatalogs(entry, catalogs) {
|
|
@@ -221,12 +208,21 @@ function checkCatalogs(entry, catalogs) {
|
|
|
221
208
|
}
|
|
222
209
|
|
|
223
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
|
+
}
|
|
224
214
|
if (options.imageFormat !== undefined && !['compatible', 'preserve'].includes(options.imageFormat)) {
|
|
225
215
|
throw new OPFPptxError('invalid-image-format', 'imageFormat must be compatible or preserve.', {path: 'options.imageFormat'});
|
|
226
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
|
+
}
|
|
227
220
|
if (options.provenance !== undefined && !['full', 'references-only', false].includes(options.provenance)) {
|
|
228
221
|
throw new OPFPptxError('invalid-provenance-option', "provenance must be 'full', 'references-only' or false.", {path: 'options.provenance'});
|
|
229
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
|
+
}
|
|
230
226
|
// OPF 0.15 (FA-23): the host's registered catalogs (core `Catalog[]`, matched by `source`, never fetched) reach every core
|
|
231
227
|
// call that resolves a reference. Strict export (`strictReferences`, default `strictAssets`) fails on a reference that
|
|
232
228
|
// resolves nowhere instead of composing automatically or drawing with the engine default.
|
|
@@ -272,6 +268,8 @@ export async function toPptx(input, options = {}) {
|
|
|
272
268
|
context.provenanceMode = options.provenance ?? "full";
|
|
273
269
|
context.headingTags = new Map();
|
|
274
270
|
context.plainTextTags = new Map();
|
|
271
|
+
context.nativeBodies = new Map();
|
|
272
|
+
context.nativeTitles = new Set();
|
|
275
273
|
context.timelineTags = new Map();
|
|
276
274
|
context.quoteTags = new Map();
|
|
277
275
|
context.quotePhotos = new Map();
|
|
@@ -281,10 +279,12 @@ export async function toPptx(input, options = {}) {
|
|
|
281
279
|
context.furnitureManifests = new Map();
|
|
282
280
|
context.nativeFurniture = new Map();
|
|
283
281
|
context.nativeFurnitureBands = new Map();
|
|
282
|
+
context.footerText = options.footerText ?? 'native';
|
|
284
283
|
// RR-72: each slide's furniture parts and the object names of their shapes, for the slide master and layouts.
|
|
285
284
|
context.furnitureParts = [];
|
|
286
285
|
// RR-81: the 0.19 template each slide uses (its native slide layout) and the objects bound to a region placeholder.
|
|
287
286
|
context.templateSlides = [];
|
|
287
|
+
context.staticSlideNumbers = options.slideNumberFields !== 'live';
|
|
288
288
|
context.automaticSlides = [];
|
|
289
289
|
context.placeholderBindings = new Map();
|
|
290
290
|
context.hostDate = options.date;
|
|
@@ -463,7 +463,7 @@ export async function fromPptx(input, options = {}) {
|
|
|
463
463
|
return match ? shapeBounds(match['p:spPr']?.['a:xfrm']) ?? undefined : undefined;
|
|
464
464
|
} : undefined,
|
|
465
465
|
// RR-11: native footer placeholders resolve their position through the layout and master.
|
|
466
|
-
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,
|
|
467
467
|
paragraphs: nativeShapeParagraphs(decodeText(entries[slidePath])),
|
|
468
468
|
readPicture: picture => importPicture(entries, picture, slidePath, relationships,
|
|
469
469
|
diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${index}.design`})),
|
|
@@ -801,6 +801,7 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
801
801
|
// The recorded crop, fit and focus are re-derived from the restored background.
|
|
802
802
|
if (diagnostic.code !== 'unsupported-image-crop') options.onDiagnostic?.({...diagnostic, path: `slides.${slideIndex}.design.background`});
|
|
803
803
|
}),
|
|
804
|
+
mediaKeyOf: picture => { const bytes = entries[relationships.get(picture?.['p:blipFill']?.['a:blip']?.['r:embed'])?.path]; return bytes ? mediaKey(bytes) : undefined; },
|
|
804
805
|
valid: (kind, value) => isValidFormat({slides: [kind === 'background' ? {design: {background: value}} : {blocks: [value]}]}),
|
|
805
806
|
report: diagnostic => options.onDiagnostic?.(diagnostic)});
|
|
806
807
|
const imageBackground = images.background ?? (background?.type === 'image' && images.backgroundOverlay ? {...background, overlay: images.backgroundOverlay} : background);
|
|
@@ -811,6 +812,7 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
811
812
|
if (imageBackground) slide.design = {background: imageBackground};
|
|
812
813
|
if (Object.keys(furniture.design).length) slide.design = {...slide.design, ...furniture.design};
|
|
813
814
|
nativeContext.imageBlocks = images.blocks;
|
|
815
|
+
nativeContext.imageSources = images.sources;
|
|
814
816
|
nativeContext.imagePictures = images.consumed;
|
|
815
817
|
// An overlay of a background that was not recovered stays an ordinary shape.
|
|
816
818
|
nativeContext.imageShapes = images.consumedShapes;
|
|
@@ -867,8 +869,10 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
867
869
|
for (const item of items) if (item.readNativeBody) {
|
|
868
870
|
const current = item.readNativeBody();
|
|
869
871
|
if (current) {
|
|
870
|
-
item.paragraphs = current.map((paragraph, index) => ({...item.paragraphs[index], ...paragraph}));
|
|
872
|
+
item.paragraphs = withDisplayedNumbers(current.map((paragraph, index) => ({...item.paragraphs[index], ...paragraph})));
|
|
871
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.'});
|
|
872
876
|
}
|
|
873
877
|
}
|
|
874
878
|
const listRecord = slideListRecord(entries, slidePath, slideRoot, relationships);
|
|
@@ -1040,7 +1044,7 @@ function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensi
|
|
|
1040
1044
|
const treatment = nativeContext.imageBlocks?.get(index);
|
|
1041
1045
|
const report = diagnostic => { if (!(treatment && diagnostic.code === 'unsupported-image-crop')) options.onDiagnostic?.({...diagnostic, path: `slides.${slideIndex}.pictures.${index}`}); };
|
|
1042
1046
|
const item = importPicture(entries, picture, slidePath, relationships, report);
|
|
1043
|
-
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)}} : {})};
|
|
1044
1048
|
else if (item?.nativeStretch) { item.payload.fit='stretch'; item.inferredStretch=true; }
|
|
1045
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.'});
|
|
1046
1050
|
if (item) items.push({...item, sources: [`pic:${index}`]});
|
|
@@ -1224,13 +1228,12 @@ function nativePictureStretch(picture,bytes,bounds,svg) {
|
|
|
1224
1228
|
if(!bounds||!(bounds.w>0&&bounds.h>0)||!metadata||!(metadata.width>0&&metadata.height>0)||metadata.orientation>1) return false;
|
|
1225
1229
|
if(stretch===undefined||fill?.['a:tile']!==undefined||Number(transform?.rot??0)!==0) return false;
|
|
1226
1230
|
if([crop,rect].some(value=>value&&['l','r','t','b'].some(key=>Number(value[key]??0)!==0)))return false;
|
|
1227
|
-
// Compare
|
|
1231
|
+
// Compare the current raster and current native frame, including only the
|
|
1232
|
+
// exporter's representable rounding cells (not a visual geometry tolerance).
|
|
1228
1233
|
const ext=transform?.['a:ext'];
|
|
1229
1234
|
const cx=Number(ext?.cx),cy=Number(ext?.cy);
|
|
1230
1235
|
if(!Number.isFinite(cx)||!Number.isFinite(cy))return false;
|
|
1231
|
-
|
|
1232
|
-
// reproduced by rounding the intrinsic ratio is still the same aspect.
|
|
1233
|
-
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);
|
|
1234
1237
|
}
|
|
1235
1238
|
|
|
1236
1239
|
// Provenance may rebuild flat blocks into groups or scalar fields. Remove only
|
|
@@ -1321,7 +1324,7 @@ function takeTitleItem(items, dimensions, inferHeadings) {
|
|
|
1321
1324
|
|
|
1322
1325
|
const titleLimit = dimensions.heightInches * 0.28;
|
|
1323
1326
|
const candidateIndex = items.findIndex((item) => {
|
|
1324
|
-
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;
|
|
1325
1328
|
const y = item.bounds?.y ?? 0;
|
|
1326
1329
|
return y <= titleLimit && (item.maxFontSize >= 20 || /^title\b/i.test(item.name ?? ""));
|
|
1327
1330
|
});
|
|
@@ -1337,7 +1340,7 @@ function takeSubtitleItem(items, titleItem, dimensions, inferHeadings) {
|
|
|
1337
1340
|
const titleBottom = (titleItem.bounds?.y ?? 0) + (titleItem.bounds?.h ?? 0);
|
|
1338
1341
|
const subtitleLimit = Math.min(dimensions.heightInches * 0.34, 1.45);
|
|
1339
1342
|
const candidateIndex = items.findIndex((item) => {
|
|
1340
|
-
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;
|
|
1341
1344
|
const y = item.bounds?.y ?? 0;
|
|
1342
1345
|
const h = item.bounds?.h ?? 0;
|
|
1343
1346
|
return y >= titleBottom - 0.05
|
|
@@ -1521,6 +1524,15 @@ function chartFromRelationship(entries, slidePath, relationships, relId, report)
|
|
|
1521
1524
|
if (chartNode.type === 'scatter') return withOptions(scatterFromSeries(entries, relationship.path, series, budget, cachePath, {codes: valueCodes, xCode, report}));
|
|
1522
1525
|
const labels = cachedValues(series[0]?.["c:cat"], cachePath(0, 'c:cat'), budget);
|
|
1523
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
|
+
}
|
|
1524
1536
|
const values = series.map((entry, index) => {
|
|
1525
1537
|
const role = entry?.["c:val"] !== undefined ? 'c:val' : 'c:yVal';
|
|
1526
1538
|
return cachedValues(entry?.[role], cachePath(index, role), budget, strictCacheValue(budget));
|
|
@@ -2128,9 +2140,12 @@ async function addSlide(pptx, presentation, authoredSlide, slideIndex, context,
|
|
|
2128
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 ?? [])};
|
|
2129
2141
|
// RR-81: a slide on a 0.19 template uses that template's native slide layout (src/template-layouts.js), whose placeholders
|
|
2130
2142
|
// core composes with this slide's options (canvas, the deck's header and footer, direction) and its placed-image bands.
|
|
2131
|
-
|
|
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};
|
|
2132
2147
|
if (template) context.templateSlides[slideIndex] = {template, areaOptions: {...composeOptions, placements: placedImageBands(opfSlide)}};
|
|
2133
|
-
else if (
|
|
2148
|
+
else if (automatic) context.automaticSlides[slideIndex] = {areaOptions: composeOptions, title: geometry.items.find(item => item.field === 'title'), ...(nativeBody ? {nativeBody} : {})};
|
|
2134
2149
|
// The default footer band (date left, text center, number right): where a footer added natively lands (RR-11).
|
|
2135
2150
|
if (slideIndex === 0) context.defaultFooterOptions = composeOptions;
|
|
2136
2151
|
slideContext.zoneBand = zoneBands(composeOptions);
|
|
@@ -2151,7 +2166,8 @@ async function addSlide(pptx, presentation, authoredSlide, slideIndex, context,
|
|
|
2151
2166
|
|
|
2152
2167
|
for (const diagnostic of geometry.diagnostics) options.onDiagnostic?.(diagnostic);
|
|
2153
2168
|
// The content topology (groups, regions, root form, block ids) with the leaf boxes this geometry draws.
|
|
2154
|
-
|
|
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);
|
|
2155
2171
|
// Paint order, as in the preview (FA-22 contract): canvas colour, the background picture and its overlay, design.watermark,
|
|
2156
2172
|
// the logo, then the items in core's order: the headings, the placed image blocks at their bands, then the flowed body.
|
|
2157
2173
|
if (geometry.backgroundImage) await addBackgroundImage(slide, presentation, geometry.backgroundImage, slideIndex, slideContext, context, options);
|
|
@@ -2168,7 +2184,10 @@ async function addSlide(pptx, presentation, authoredSlide, slideIndex, context,
|
|
|
2168
2184
|
// object or one drawn on a card stays a free shape: Change Layout would move it away from its caption or card.
|
|
2169
2185
|
const boundRegion = template && item.region !== undefined && ['image', 'chart', 'table'].includes(item.field) && !item.caption && !item.frameBox
|
|
2170
2186
|
? geometry.regions?.find(region => region.name === item.region && region.flow === 'none' && region.content?.length === 1 && !region.overflow?.length) : undefined;
|
|
2171
|
-
|
|
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)
|
|
2172
2191
|
context.placeholderBindings.set(`OPF heading ${item.path} line 0`, {slide: slideIndex, region: 'Title', object: 'title'});
|
|
2173
2192
|
const drawnBefore = boundRegion && {images: context.images.size, placeholders: context.imagePlaceholders.size, charts: context.chartHeadings.size, tables: context.tableHeaders.size};
|
|
2174
2193
|
// Card text sits on a literal card fill, not the slide background: keep it literal.
|
|
@@ -2183,7 +2202,15 @@ async function addSlide(pptx, presentation, authoredSlide, slideIndex, context,
|
|
|
2183
2202
|
rectRadius:8*Math.min(widthInches,heightInches)/720,
|
|
2184
2203
|
fill:paint(surface),line:{...paint(`#${itemContext.colors.border}`),width:.75},objectName:`OPF card ${item.path}`});
|
|
2185
2204
|
}
|
|
2186
|
-
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) {
|
|
2187
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});
|
|
2188
2215
|
} else if (["title", "subtitle", "tag"].includes(item.field) && !item.text?.richLines) {
|
|
2189
2216
|
// Estimated-font headings use one native text box, which still needs a
|
|
@@ -2221,7 +2248,7 @@ async function addSlide(pptx, presentation, authoredSlide, slideIndex, context,
|
|
|
2221
2248
|
} else {
|
|
2222
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.
|
|
2223
2250
|
const mediaNames = item.caption ? new Set([...context.images.keys(), ...context.imagePlaceholders.keys(), ...context.chartHeadings.keys(), ...context.tableHeaders.keys()]) : undefined;
|
|
2224
|
-
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);
|
|
2225
2252
|
if (item.caption) {
|
|
2226
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));
|
|
2227
2254
|
addCaption(slide, item, mediaName, itemContext, exportHelpers, (code, message) => new OPFPptxError(code, message, {path: item.caption.path}));
|
|
@@ -2289,7 +2316,7 @@ function fieldToType(field) {
|
|
|
2289
2316
|
return field === "items" || field === "bullets" ? "list" : field;
|
|
2290
2317
|
}
|
|
2291
2318
|
|
|
2292
|
-
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) {
|
|
2293
2320
|
const kind = inferPayloadKind(payload);
|
|
2294
2321
|
switch (kind) {
|
|
2295
2322
|
case "text":
|
|
@@ -2305,7 +2332,7 @@ async function addPayload(slide, presentation, payload, region, path, context, o
|
|
|
2305
2332
|
addMediaPayload(slide, presentation, payload.video, region, path, context, options);
|
|
2306
2333
|
break;
|
|
2307
2334
|
case "chart":
|
|
2308
|
-
addChartPayload(slide, payload.chart, region, context, options, path, presentation);
|
|
2335
|
+
addChartPayload(slide, payload.chart, region, context, options, path, presentation, chartLayout);
|
|
2309
2336
|
break;
|
|
2310
2337
|
case "table":
|
|
2311
2338
|
addTablePayload(slide, payload.table, region, context, options, path, presentation);
|
|
@@ -2472,7 +2499,7 @@ async function addImagePayload(slide, presentation, asset, region, path, context
|
|
|
2472
2499
|
// One native image picture at `box` (inches), fitted at packaging (src/image-provenance.js placeImages). `treatment` is
|
|
2473
2500
|
// what OPF_IMAGE_V1 stores (the authored block fields); without it the picture is untagged. Returns the picture's object
|
|
2474
2501
|
// name, or undefined when the source could not be embedded (the caller draws the unavailable-image panel).
|
|
2475
|
-
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} = {}) {
|
|
2476
2503
|
const outcome = { box, cover: fit === 'cover' };
|
|
2477
2504
|
const resolved = await resolveImage(asset, presentation, options, path, outcome);
|
|
2478
2505
|
if (!resolved) {
|
|
@@ -2490,7 +2517,8 @@ async function addImagePicture(slide, presentation, asset, box, path, context, o
|
|
|
2490
2517
|
else context.svgPictures.set(name, outcome.svg);
|
|
2491
2518
|
}
|
|
2492
2519
|
const text = alt !== undefined ? alt : assetAlt(asset, presentation);
|
|
2493
|
-
|
|
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} : {}) });
|
|
2494
2522
|
context.pictureText.set(name, pictureText(text, asset, presentation));
|
|
2495
2523
|
slide.addImage({ ...resolved, objectName: name, x: box.x, y: box.y, w: box.w, h: box.h, altText: text });
|
|
2496
2524
|
return name;
|
|
@@ -2538,11 +2566,13 @@ async function addImageItem(slide, presentation, item, slideIndex, context, opti
|
|
|
2538
2566
|
const treatment = Object.fromEntries(IMAGE_TREATMENT_KEYS.filter(key => key !== 'overlay' && block[key] !== undefined).map(key => [key, structuredClone(block[key])]));
|
|
2539
2567
|
const effects = imageEffects(image, context);
|
|
2540
2568
|
const treated = Boolean(effects.border || effects.opacity !== null || effects.recolor || image.overlay || (effects.shape && effects.shape.preset !== 'rect'));
|
|
2541
|
-
|
|
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);
|
|
2542
2572
|
// A plain contained picture in the flow is framed by the picture itself (0.14 content images); a treated or placed one
|
|
2543
2573
|
// keeps core's frame (0.14 slide images), so its mask, line and overlay apply to that frame.
|
|
2544
2574
|
const name = await addImagePicture(slide, presentation, item.value, box, item.path, { ...context, slidePath: `slides.${slideIndex}` }, options, {
|
|
2545
|
-
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
|
|
2546
2576
|
});
|
|
2547
2577
|
if (!name) {
|
|
2548
2578
|
const panel = addImagePlaceholder(slide, presentation, item.value, box, item.path, context, options);
|
|
@@ -2685,7 +2715,7 @@ function applyChartTextSize(xml, size) {
|
|
|
2685
2715
|
`<a:defRPr${/\bsz="[^"]*"/.test(attributes) ? attributes.replace(/\bsz="[^"]*"/, `sz="${size}"`) : `${attributes} sz="${size}"`}${close}>`));
|
|
2686
2716
|
}
|
|
2687
2717
|
|
|
2688
|
-
function addChartPayload(slide, chart, region, context, options = {}, path = "chart", presentation) {
|
|
2718
|
+
function addChartPayload(slide, chart, region, context, options = {}, path = "chart", presentation, chartLayout) {
|
|
2689
2719
|
const chartData = toPptxChartData(chart, context.chartexMode, presentation, path);
|
|
2690
2720
|
if (!chartData.series) {
|
|
2691
2721
|
// Never lose a chart silently: the placeholder frame stands in for it, and a diagnostic names the reason.
|
|
@@ -2725,8 +2755,15 @@ function addChartPayload(slide, chart, region, context, options = {}, path = "ch
|
|
|
2725
2755
|
context.chartFonts.set(objectName,{heading:context.fonts.heading,body:context.fonts.body});
|
|
2726
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.
|
|
2727
2757
|
if (typeof chart.alt === 'string') context.frameAlts.set(objectName, chart.alt);
|
|
2728
|
-
|
|
2729
|
-
|
|
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});
|
|
2730
2767
|
// FA-14: chart.highlight. A highlighted chart writes accent and muted colours over the series palette, and its data labels
|
|
2731
2768
|
// contrast with those colours (src/chart-highlight.js).
|
|
2732
2769
|
const highlight = chartData.chartex ? undefined : chartHighlightPlan({options: chartOptions, data: chartData.resolved, kind: chartTargetFor(chart.type)?.kind, panelFill, labelColor, primary: `#${context.colors.accent}`,
|
|
@@ -2779,6 +2816,9 @@ function addChartPayload(slide, chart, region, context, options = {}, path = "ch
|
|
|
2779
2816
|
...(chartData.series.every(entry => entry.values.every(value => value == null || value >= 0)) ? {valAxisMinVal: 0} : {}),
|
|
2780
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} : {})
|
|
2781
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} : {}),
|
|
2782
2822
|
barDir: chartData.barDir,
|
|
2783
2823
|
barGrouping: chartData.barGrouping,
|
|
2784
2824
|
// Right to left (RR-05): column, line and area charts reverse their categories (c:catAx orientation maxMin), which also moves the
|
|
@@ -2986,6 +3026,32 @@ function assertXmlText(text, path) {
|
|
|
2986
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
3056
|
assertXmlText(text, config.path);
|
|
2991
3057
|
const scale = Math.min(context.dimensions.widthInches, context.dimensions.heightInches) * 96 / 720;
|
|
@@ -3066,7 +3132,7 @@ async function addFurniture(slide,presentation,source,layout,context,options,sli
|
|
|
3066
3132
|
}
|
|
3067
3133
|
const staticDates = new Map();
|
|
3068
3134
|
// RR-11: the first footer text, date and slide number that fit one line are native placeholders (src/native-furniture.js).
|
|
3069
|
-
const natives = nativeFurnitureParts(layout);
|
|
3135
|
+
const natives = nativeFurnitureParts(layout, {staticNumbers: context.staticSlideNumbers});
|
|
3070
3136
|
for(const [index,part]of layout.parts.entries()){
|
|
3071
3137
|
const entry = {index, kind: part.kind, zone: part.zone, field: part.field, type: part.type, path: part.path, native: natives.has(index), names: []};
|
|
3072
3138
|
drawn.push(entry);
|
|
@@ -3087,6 +3153,8 @@ async function addFurniture(slide,presentation,source,layout,context,options,sli
|
|
|
3087
3153
|
// en-US field type is written as its laid-out text.
|
|
3088
3154
|
const fields=[];
|
|
3089
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;
|
|
3090
3158
|
const nativeType=nativeFieldType(field);
|
|
3091
3159
|
if(nativeType)fields.push({...field,nativeType});
|
|
3092
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.`});
|
|
@@ -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);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {XMLParser} from 'fast-xml-parser';
|
|
2
2
|
import {isValidFormat} from './format-check.js';
|
|
3
|
-
import {attachTextTags,decodeTextTag} from './code-provenance.js';
|
|
3
|
+
import {attachTextTags,decodeTextTag,groupUnchanged} from './code-provenance.js';
|
|
4
4
|
|
|
5
5
|
const TAG='OPF_METRIC_V1',REL='http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags';
|
|
6
6
|
const parser=new XMLParser({ignoreAttributes:false,attributeNamePrefix:'',parseTagValue:false,trimValues:false});
|
|
@@ -14,7 +14,7 @@ export function metricManifest(value,layout,group) {
|
|
|
14
14
|
return {v:1,group,role:'anchor',part:0,line:0,value,parts:layout.parts.map(part=>({role:part.role,visible:part.visible,
|
|
15
15
|
lines:part.fit?.sourceLines.map(({start,end,nextStart,boundary})=>({start,end,nextStart,boundary}))??[]}))};
|
|
16
16
|
}
|
|
17
|
-
export function attachMetricTags(entries,records,descriptions) {return attachTextTags(entries,records,TAG,'opfMetric','metric',{descriptions});}
|
|
17
|
+
export function attachMetricTags(entries,records,descriptions) {return attachTextTags(entries,records,TAG,'opfMetric','metric',{descriptions,signed:true});}
|
|
18
18
|
|
|
19
19
|
function validateManifest(manifest) {
|
|
20
20
|
if (!isValidFormat({slides:[{metric:manifest.value}]})) throw new Error('Invalid metric source.');
|
|
@@ -50,7 +50,7 @@ export function importMetricGroups(shapes,paragraphs,relationships,entries,repor
|
|
|
50
50
|
const data=decodeTextTag(tag.val);
|
|
51
51
|
if (data.v!==1||typeof data.group!=='string'||!/^\d+$/.test(data.group)) throw new Error('Invalid metric identity.');
|
|
52
52
|
const group=groups.get(data.group)??[];
|
|
53
|
-
group.push({data,shape,text:(paragraphs[index]??[]).map(p=>p.text).join('\n')});groups.set(data.group,group);
|
|
53
|
+
group.push({data,shape,index,text:(paragraphs[index]??[]).map(p=>p.text).join('\n')});groups.set(data.group,group);
|
|
54
54
|
} catch {report({code:'invalid-metric-provenance',message:'Invalid metric tags; visible native shapes were retained.'});}
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -91,7 +91,7 @@ export function importMetricGroups(shapes,paragraphs,relationships,entries,repor
|
|
|
91
91
|
for (const item of group) consumed.add(item.shape);
|
|
92
92
|
items.push({shape:anchor.shape,payload:{type:'metric',metric}});
|
|
93
93
|
if (typeChanges.length) report({code:'metric-value-type-changed',message:`Edited numeric ${typeChanges.join(', ')} retained as literal text to avoid discarding spelling or precision.`});
|
|
94
|
-
report({code:'metric-import-reflow',message:'Metric fields, source boundaries and native text were recovered. Native formatting, positioning, alignment and font theme are not reconstructed; review the reflowed OPF.'});
|
|
94
|
+
if(!groupUnchanged(group,paragraphs))report({code:'metric-import-reflow',message:'Metric fields, source boundaries and native text were recovered. Native formatting, positioning, alignment and font theme are not reconstructed; review the reflowed OPF.'});
|
|
95
95
|
} catch (error) {report({code:'invalid-metric-provenance',message:`${error.message} Visible native shapes were retained without restoring old source text.`});}
|
|
96
96
|
return {consumed,items};
|
|
97
97
|
}
|