@openpresentation/opf-pptx 0.14.0 → 0.16.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 +29 -26
- package/dist/background-import.js +33 -17
- package/dist/background.js +28 -25
- package/dist/chart-types.js +7 -22
- package/dist/data-provenance.js +68 -11
- package/dist/document-provenance.js +235 -132
- package/dist/{chart-alt.js → frame-alt.js} +4 -4
- package/dist/image-fallback-browser.js +5 -0
- package/dist/image-fallback-node.js +20 -2
- package/dist/image-provenance.js +273 -0
- package/dist/index.d.ts +39 -20
- package/dist/index.js +333 -196
- package/dist/script-fonts.js +41 -60
- package/dist/theme-colors.js +15 -10
- package/dist/typeface-inventory.js +1 -1
- package/package.json +15 -4
- package/dist/slide-image-provenance.js +0 -177
package/dist/index.js
CHANGED
|
@@ -7,10 +7,10 @@ import {giveNotesMastersOwnThemes, giveSlidesScriptMasters} from './master-theme
|
|
|
7
7
|
import {legacyVendorOutput} from './vendor-compat.js';
|
|
8
8
|
import {readChartCategoryHeading,writeChartCategoryHeading,writeChartWorkbookFormats,repairChartWorkbookRanges} from './chart-workbook.js';
|
|
9
9
|
import {applyChartNumberFormats,chartNumber,excelCode,formattedColumns,inlineChartData,inlineTableData,isDatasetRef,numericRanges,resolveChartData} from './chart-data.js';
|
|
10
|
-
import {attachDataProvenance,chartDataRecord,chartEvidence,readDataTag,readDatasetsTag,restoreChartData,restoreTableData,tableDataRecord} from './data-provenance.js';
|
|
10
|
+
import {attachDataProvenance,chartDataRecord,chartEvidence,readDataTag,readDatasetsTag,restoreChartData,restoreTableData,tableDataRecord,tableCellEvidence,tableEnvironmentEvidence} from './data-provenance.js';
|
|
11
11
|
import {CHARTEX_FALLBACK,resolveChartType,chartTypeFromNative,applyChartConstruct,comboFromNative,NATIVE_CHART_ELEMENTS} from './chart-types.js';
|
|
12
12
|
import {attachChartexParts,chartFromChartex,CHARTEX_GRAPHIC_DATA_URI} from './chartex.js';
|
|
13
|
-
import {
|
|
13
|
+
import {applyFrameAlt,readFrameAlt} from './frame-alt.js';
|
|
14
14
|
import {applyDataLabels,chartOptionsFromClassic,chartTargetFor,classicChartOptions,reportChartOptionDiagnostics,resolveChartOptionsFor} from './chart-options.js';
|
|
15
15
|
import {attachCodeTags, attachTextTags, codeManifest, importCodeGroups, nativeShapeParagraphs, nativeTextShapes} from './code-provenance.js';
|
|
16
16
|
import {attachMetricTags,metricManifest,importMetricGroups} from './metric-provenance.js';
|
|
@@ -33,7 +33,7 @@ import {attachFurnitureFields, furniturePartFields, lineFields, nativeFieldType}
|
|
|
33
33
|
import {attachNativePlaceholders, defaultPlaceholderGeometry, nativeDateText, nativeFurnitureParts, writeNativeMasters} from './native-furniture.js';
|
|
34
34
|
import {importImageOrientation} from './image-import.js';
|
|
35
35
|
import {extractSignals, normalizeSignalOptions, themeFactsFor} from './import-signals.js';
|
|
36
|
-
import {
|
|
36
|
+
import {placeImages, importImages, imageName, imageOverlayName, backgroundImageName, backgroundOverlayName, IMAGE_TREATMENT_KEYS, BACKGROUND_IMAGE_KEYS} from './image-provenance.js';
|
|
37
37
|
import {dedupeMedia} from './media-dedupe.js';
|
|
38
38
|
import {placeWatermarks, importWatermark, importTextWatermark, tagTextWatermarks, watermarkName, watermarkTextName, watermarkBox, watermarkOpacity} from './watermark-provenance.js';
|
|
39
39
|
import {placeLogos, importLogo, importLogoPlaceholders, logoName, LOGO_TAG} from './logo-provenance.js';
|
|
@@ -42,22 +42,22 @@ import {importBackground} from './background-import.js';
|
|
|
42
42
|
import {chartHighlightPlan, applyChartHighlight} from './chart-highlight.js';
|
|
43
43
|
import {themeSlotColors, writeThemeColors, schemeColorValue, schemeBackgroundFill, schemeBackgroundValue, defaultTextSchemeValues, tableTextSchemeValue, solidColorXml, writeMasterBackground, inheritLayoutBackground, readThemeSlotColors, recoverColorScheme, recoverTheme, presentationThemePath} from './theme-colors.js';
|
|
44
44
|
import {languageDiagnostics, observeLanguage, observedRtl, partScriptFonts, physicalAlignment, planScriptFonts, planSlideThemes, reconcileLanguage, reportPerSlideNotesScriptFonts, runLanguageFonts, runLanguageTag, stripRunScriptFonts, themeEastAsianFromLatin} from './script-fonts.js';
|
|
45
|
-
import { webpToPng, svgToPng, readLocalFile } from '#image-fallback';
|
|
45
|
+
import { webpToPng, svgToPng, readLocalFile, localFileReadable } from '#image-fallback';
|
|
46
46
|
import { prepareSvg, svgDataUriBytes, svgRasterScale, svgBlipRelationship, attachSvgPictures } from './svg-image.js';
|
|
47
47
|
import { rasterMetadata, pictureTransform, normalizeImageOrientation } from './image-geometry.js';
|
|
48
48
|
import {
|
|
49
|
-
chartPaletteForFill, codeHighlightBands, codeHighlightColors, codeHighlightLines, codeLineNumbers, codeLineRuns, codeSyntaxPaletteForScheme, composeSlide,
|
|
50
|
-
|
|
51
|
-
resolveTextStyle, textColorForFill, textWidthMeasurer, timelineMarkerShapes, timelineTextColor, tokenizeCode
|
|
49
|
+
catalogRecords, chartPaletteForFill, codeHighlightBands, codeHighlightColors, codeHighlightLines, codeLineNumbers, codeLineRuns, codeSyntaxPaletteForScheme, composeSlide,
|
|
50
|
+
ENGINE_DEFAULT_FONT_SCHEME, defaultSlideBackground, fitImage, fitText, layoutTable, layoutWatermark, metricTrendMark, resolveCanvasDimensions, resolveColorRoles, resolveFontFamilies,
|
|
51
|
+
resolveReference, resolveScriptFonts, resolveTextStyle, textColorForFill, textWidthMeasurer, timelineMarkerShapes, timelineTextColor, tokenizeCode
|
|
52
52
|
} from "@openpresentation/opf/composition";
|
|
53
53
|
import { colorContext, resolveColorRefValue, resolveExportColor, resolveVariableColors } from "./color-ref.js";
|
|
54
54
|
import PptxGenJS from "../vendor/pptxgenjs/pptxgen.es.js";
|
|
55
55
|
import { unzipSync, zipSync } from "fflate";
|
|
56
56
|
import { XMLParser } from "fast-xml-parser";
|
|
57
57
|
import {
|
|
58
|
-
catalogs as bundledCatalogs,
|
|
59
58
|
hasContentVariables,
|
|
60
59
|
isTemplate,
|
|
60
|
+
OPFCatalogsOptionError,
|
|
61
61
|
resolveSlideContext as resolveCoreSlideContext,
|
|
62
62
|
resolveVariables,
|
|
63
63
|
validate
|
|
@@ -218,6 +218,12 @@ const CHART_COLORS = [
|
|
|
218
218
|
"64748B"
|
|
219
219
|
];
|
|
220
220
|
|
|
221
|
+
// The `catalogs` option is core's registered catalogs (`Catalog[]`). A value that is no array is core's
|
|
222
|
+
// OPFCatalogsOptionError (code `invalid-catalogs`) up front; core checks each entry where it resolves a reference.
|
|
223
|
+
function checkCatalogs(entry, catalogs) {
|
|
224
|
+
if (catalogs !== undefined && !Array.isArray(catalogs)) throw new OPFCatalogsOptionError(entry, `got ${catalogs === null ? 'null' : typeof catalogs}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
221
227
|
export async function toPptx(input, options = {}) {
|
|
222
228
|
if (options.imageFormat !== undefined && !['compatible', 'preserve'].includes(options.imageFormat)) {
|
|
223
229
|
throw new OPFPptxError('invalid-image-format', 'imageFormat must be compatible or preserve.', {path: 'options.imageFormat'});
|
|
@@ -225,21 +231,34 @@ export async function toPptx(input, options = {}) {
|
|
|
225
231
|
if (options.provenance !== undefined && !['full', 'references-only', false].includes(options.provenance)) {
|
|
226
232
|
throw new OPFPptxError('invalid-provenance-option', "provenance must be 'full', 'references-only' or false.", {path: 'options.provenance'});
|
|
227
233
|
}
|
|
234
|
+
// OPF 0.15 (FA-23): the host's registered catalogs (core `Catalog[]`, matched by `source`, never fetched) reach every core
|
|
235
|
+
// call that resolves a reference. Strict export (`strictReferences`, default `strictAssets`) fails on a reference that
|
|
236
|
+
// resolves nowhere instead of composing automatically or drawing with the engine default.
|
|
237
|
+
checkCatalogs('toPptx', options.catalogs);
|
|
238
|
+
options = {...options, strictReferences: options.strictReferences ?? options.strictAssets === true};
|
|
228
239
|
const presentation = resolveTemplateInput(parseInput(input), options);
|
|
229
240
|
assertValidBoundary(presentation);
|
|
230
241
|
// `options.fonts` is the fonts handle (the renderer's loadFonts()); the exporter reads its measurement, wrapped so the package names the
|
|
231
242
|
// chosen family (FF-31). Internally `options.textMeasurement` is that wrapped measurement; it is not a public option.
|
|
232
243
|
options = {...options, textMeasurement: chosenFamilyMeasurement(options.fonts?.textMeasurement), svgRasters: new Map()};
|
|
244
|
+
// The renderer remains optional for custom measurement providers. Its public script planner supplies the same
|
|
245
|
+
// shaping/measurement segmentation as preview when installed; the selected native family wrapper stays outside it.
|
|
246
|
+
if (options.textMeasurement) {
|
|
247
|
+
try { options.createScriptFonts = (await import("@openpresentation/opf-render/fonts")).createScriptFonts; }
|
|
248
|
+
catch (error) { if (error?.code !== "ERR_MODULE_NOT_FOUND") throw error; }
|
|
249
|
+
}
|
|
233
250
|
|
|
234
251
|
const context = resolvePresentationContext(presentation, {...options,textMeasurement:undefined});
|
|
235
252
|
context.listMarkers = new Map();
|
|
236
253
|
context.imagePlaceholders = new Map();
|
|
237
254
|
context.tableHeaders = new Map();
|
|
238
255
|
context.tableCells = new Map();
|
|
239
|
-
|
|
256
|
+
// FA-23: every exported image picture (image blocks, the alt-text form of an image background, timeline pictures) and
|
|
257
|
+
// every image overlay shape, by object name (src/image-provenance.js).
|
|
258
|
+
context.images = new Map();
|
|
259
|
+
context.imageOverlays = new Map();
|
|
240
260
|
context.svgPictures = new Map();
|
|
241
261
|
context.pictureText = new Map();
|
|
242
|
-
context.slideImages = new Map();
|
|
243
262
|
context.watermarks = new Map();
|
|
244
263
|
context.textWatermarks = new Map();
|
|
245
264
|
context.logos = new Map();
|
|
@@ -267,7 +286,7 @@ export async function toPptx(input, options = {}) {
|
|
|
267
286
|
context.metricTags = new Map();
|
|
268
287
|
context.metricDescriptions = new Map();
|
|
269
288
|
context.chartHeadings = new Map();
|
|
270
|
-
context.
|
|
289
|
+
context.frameAlts = new Map();
|
|
271
290
|
context.chartFonts = new Map();
|
|
272
291
|
context.chartPalettes = new Map();
|
|
273
292
|
context.chartex = new Map();
|
|
@@ -284,15 +303,19 @@ export async function toPptx(input, options = {}) {
|
|
|
284
303
|
throw new OPFPptxError('invalid-chartex-mode', 'chartex must be auto, native or fallback.', {path: 'options.chartex'});
|
|
285
304
|
}
|
|
286
305
|
context.chartexMode = options.chartex ?? 'auto';
|
|
287
|
-
Object.assign(context, exportTheme(presentation, context));
|
|
306
|
+
Object.assign(context, exportTheme(presentation, context, options));
|
|
288
307
|
context.reportedReferences = new Set();
|
|
289
308
|
// Document references and metadata tags (FF-32, docs/document-roundtrip.md).
|
|
290
309
|
context.documentProvenance = options.provenance === false ? null : documentProvenance(presentation, {
|
|
291
310
|
mode: options.provenance ?? "full",
|
|
292
|
-
|
|
311
|
+
// Core's provenance of each slide's resolved layout (resolveSlideContext().resolved.provenance.layout) with its record.
|
|
312
|
+
layoutOf: index => {
|
|
313
|
+
const {resolved} = coreContext(presentation, index, options);
|
|
314
|
+
return resolved.provenance?.layout && resolved.layout ? {...resolved.provenance.layout, record: resolved.layout} : undefined;
|
|
315
|
+
},
|
|
293
316
|
report: diagnostic => options.onDiagnostic?.(diagnostic)
|
|
294
317
|
});
|
|
295
|
-
context.scriptFonts = planScriptFonts(presentation, options.onDiagnostic);
|
|
318
|
+
context.scriptFonts = planScriptFonts(presentation, options.onDiagnostic, {catalogs: options.catalogs});
|
|
296
319
|
// Slide section labels become PowerPoint's native section list (src/sections.js).
|
|
297
320
|
// A label is an XML attribute: the characters text runs reject are rejected here too.
|
|
298
321
|
context.sections = presentation.slides.map((slide, index) => {
|
|
@@ -310,9 +333,10 @@ export async function toPptx(input, options = {}) {
|
|
|
310
333
|
await addSlide(pptx, presentation, presentation.slides[index], index, context, options);
|
|
311
334
|
}
|
|
312
335
|
|
|
313
|
-
// FF-08: pitchFamily per exported family, from each slide's resolved scheme
|
|
336
|
+
// FF-08: pitchFamily per exported family, from each slide's resolved scheme, then the font schemes the document embeds
|
|
337
|
+
// and the registered catalogs its groups name (core catalogRecords).
|
|
314
338
|
context.fontPitch = fontPitchFamilies(presentation.slides.map((slide, index) => exportSlideContext(presentation, index, context, options).fonts),
|
|
315
|
-
|
|
339
|
+
catalogRecords(presentation, "fontSchemes", {catalogs: options.catalogs}).map(entry => entry.record));
|
|
316
340
|
|
|
317
341
|
let raw;
|
|
318
342
|
try {
|
|
@@ -332,6 +356,9 @@ export async function toPptx(input, options = {}) {
|
|
|
332
356
|
export async function fromPptx(input, options = {}) {
|
|
333
357
|
// Opt-in raw shape signals (import-signals.js). Validated first so a bad option fails before any work.
|
|
334
358
|
const signalLimits = normalizeSignalOptions(options.signals, (code, message, details) => new OPFPptxError(code, message, details));
|
|
359
|
+
// OPF 0.15 (FA-23): catalogs the host registered (core `Catalog[]`, never fetched). Theme and colour-scheme recovery and
|
|
360
|
+
// stored layout ids resolve against them; with none, a colour scheme imports inline and no theme id is recovered.
|
|
361
|
+
checkCatalogs('fromPptx', options.catalogs);
|
|
335
362
|
const entries = readPptxZip(input);
|
|
336
363
|
const presentationDoc = parseRequiredXml(entries, "ppt/presentation.xml");
|
|
337
364
|
const presentationRoot = presentationDoc["p:presentation"];
|
|
@@ -365,12 +392,12 @@ export async function fromPptx(input, options = {}) {
|
|
|
365
392
|
const theme = relatedPart(relatedPart(relatedPart(path, 'slideLayout'), 'slideMaster'), 'theme');
|
|
366
393
|
return theme ? {path: theme, xml: decodeText(entries[theme])} : null;
|
|
367
394
|
}),
|
|
368
|
-
catalogs:
|
|
395
|
+
catalogs: options.catalogs
|
|
369
396
|
});
|
|
370
397
|
if (observedLanguage.language !== undefined) imported.language = observedLanguage.language;
|
|
371
398
|
if (core.description) imported.description = core.description;
|
|
372
399
|
if (core.author) imported.author = splitAuthors(core.author);
|
|
373
|
-
const themeDesign = importThemeDesign(entries, presentationRoot, presentationRels);
|
|
400
|
+
const themeDesign = importThemeDesign(entries, presentationRoot, presentationRels, options.catalogs);
|
|
374
401
|
const design = {...themeDesign.design, ...(dimensions ? {dimensions} : {})};
|
|
375
402
|
if (Object.keys(design).length) imported.design = design;
|
|
376
403
|
|
|
@@ -399,7 +426,7 @@ export async function fromPptx(input, options = {}) {
|
|
|
399
426
|
const mediaRegistry = Object.create(null);
|
|
400
427
|
for (let index = 0; index < slidePaths.length; index += 1) {
|
|
401
428
|
furnitureContexts[index].mediaRegistry = mediaRegistry;
|
|
402
|
-
imported.slides.push(importSlide(entries, slidePaths[index], index, dimensions, {...options, rtlDeck: observedRtl(observedLanguage), deckLang: observedLanguage.lang, codeFamily: Object.keys(entries).some(path => /^ppt\/tags\/opf/i.test(path)) ? importedCodeFamily(imported.
|
|
429
|
+
imported.slides.push(importSlide(entries, slidePaths[index], index, dimensions, {...options, rtlDeck: observedRtl(observedLanguage), deckLang: observedLanguage.lang, codeFamily: Object.keys(entries).some(path => /^ppt\/tags\/opf/i.test(path)) ? importedCodeFamily(imported, options.catalogs) : undefined, dataProvenance: {datasets}}, furniture.slides[index], furnitureContexts[index]));
|
|
403
430
|
}
|
|
404
431
|
if (datasets) imported.datasets = datasets;
|
|
405
432
|
// Native sections (PowerPoint's own section list, `Default Section` = none)
|
|
@@ -430,9 +457,11 @@ export async function fromPptx(input, options = {}) {
|
|
|
430
457
|
let restoredGroups = [];
|
|
431
458
|
try {
|
|
432
459
|
const restored = restoreDocumentProvenance(imported, {entries, presentationRoot, presentationRels, organizationConflict: furniture.organizationConflict === true, speakerConflict: furniture.speakerConflict === true,
|
|
433
|
-
//
|
|
434
|
-
|
|
460
|
+
// Stored layout ids also resolve in the host's registered catalogs (FA-23).
|
|
461
|
+
catalogs: options.catalogs,
|
|
435
462
|
nativeSections: slideSections,
|
|
463
|
+
// An importer `schema` option names the document's $schema, so an absent source $schema does not remove it (RR-59).
|
|
464
|
+
schemaOption: options.schema !== undefined,
|
|
436
465
|
slides: slidePaths.map((path, index) => ({path, root: furnitureContexts[index].root, relationships: furnitureContexts[index].relationships, contentBounds: furnitureContexts[index].contentBounds}))}, report);
|
|
437
466
|
// The stored language wins while the runs still carry its tag (FF-07).
|
|
438
467
|
restored.groups = reconcileLanguage(restored.groups, observedLanguage, report);
|
|
@@ -632,13 +661,14 @@ function readCoreProperties(entries) {
|
|
|
632
661
|
}
|
|
633
662
|
|
|
634
663
|
// FA-13: the code family of the imported design's font scheme (a run in it is an inline code run). The scheme's own `code` role,
|
|
635
|
-
// else the
|
|
636
|
-
//
|
|
637
|
-
|
|
638
|
-
|
|
664
|
+
// else that of the record the reference resolves to (in the imported document or the host's registered catalogs), else the
|
|
665
|
+
// engine default's; heading and body families are never code. Only a package the exporter wrote (it carries OPF tag parts) is
|
|
666
|
+
// read this way; a deck from another tool keeps its runs' font families as they are.
|
|
667
|
+
function importedCodeFamily(imported, catalogs) {
|
|
668
|
+
const reference = imported.design?.fontScheme;
|
|
639
669
|
const id = referenceId(reference);
|
|
640
|
-
const
|
|
641
|
-
return resolveFontFamilies({...
|
|
670
|
+
const found = id ? resolveReference(imported, "fontSchemes", id, {catalogs: catalogs ?? []}) : undefined;
|
|
671
|
+
return resolveFontFamilies({...(found ? found.record : ENGINE_DEFAULT_FONT_SCHEME), ...(isPlainObject(reference) ? reference : {})}).code;
|
|
642
672
|
}
|
|
643
673
|
|
|
644
674
|
function dimensionsFromPresentation(presentationRoot) {
|
|
@@ -662,20 +692,28 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
662
692
|
const background = importBackground(slidePath, resolveCanvasDimensions(dimensions), {
|
|
663
693
|
part: (path, parser) => parseRequiredXml(entries, path, parser), relationships: path => parseRelationships(entries, path), bytes: path => entries[path]
|
|
664
694
|
}, diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${slideIndex}.design.background`}));
|
|
665
|
-
|
|
695
|
+
// FA-23: tagged image pictures and overlays (OPF_IMAGE_V1, OPF_IMAGE_OVERLAY_V1). An unchanged full-slide back picture is
|
|
696
|
+
// the slide's image background (its native fill is then only the canvas colour); an unchanged overlay of a native image
|
|
697
|
+
// background fill joins that background; an unchanged image block picture returns its treatment and placement.
|
|
698
|
+
const images = importImages({pictures: nativeContext.pictures, shapes: nativeContext.shapes, relationships, entries, slideIndex,
|
|
699
|
+
readPicture: picture => importPicture(entries, picture, slidePath, relationships, diagnostic => {
|
|
700
|
+
// The recorded crop, fit and focus are re-derived from the restored background.
|
|
701
|
+
if (diagnostic.code !== 'unsupported-image-crop') options.onDiagnostic?.({...diagnostic, path: `slides.${slideIndex}.design.background`});
|
|
702
|
+
}),
|
|
703
|
+
valid: (kind, value) => isValidFormat({slides: [kind === 'background' ? {design: {background: value}} : {blocks: [value]}]}),
|
|
704
|
+
report: diagnostic => options.onDiagnostic?.(diagnostic)});
|
|
705
|
+
const imageBackground = images.background ?? (background?.type === 'image' && images.backgroundOverlay ? {...background, overlay: images.backgroundOverlay} : background);
|
|
706
|
+
if (images.backgroundOverlay && imageBackground === background) {
|
|
707
|
+
images.consumedShapes.delete(images.backgroundOverlayShape);
|
|
708
|
+
options.onDiagnostic?.({code: 'invalid-image-provenance', path: `slides.${slideIndex}.design.background.overlay`, message: 'The tagged OPF background overlay no longer sits on an image background; it was imported as an ordinary shape.'});
|
|
709
|
+
}
|
|
710
|
+
if (imageBackground) slide.design = {background: imageBackground};
|
|
666
711
|
if (Object.keys(furniture.design).length) slide.design = {...slide.design, ...furniture.design};
|
|
667
712
|
if (furniture.section !== undefined) slide.section = furniture.section;
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
if (diagnostic.code !== 'unsupported-image-crop') options.onDiagnostic?.({...diagnostic, path: slideImagePath});
|
|
673
|
-
}),
|
|
674
|
-
diagnostic => options.onDiagnostic?.({...diagnostic, path: slideImagePath}));
|
|
675
|
-
if (slideImage.design) slide.design = {...slide.design, ...slideImage.design};
|
|
676
|
-
if (slideImage.image) slide.image = slideImage.image;
|
|
677
|
-
nativeContext.slideImagePictures = slideImage.consumed;
|
|
678
|
-
nativeContext.slideImageShapes = slideImage.consumedShapes;
|
|
713
|
+
nativeContext.imageBlocks = images.blocks;
|
|
714
|
+
nativeContext.imagePictures = images.consumed;
|
|
715
|
+
// An overlay of a background that was not recovered stays an ordinary shape.
|
|
716
|
+
nativeContext.imageShapes = images.consumedShapes;
|
|
679
717
|
const watermarkPath = `slides.${slideIndex}.design.watermark`;
|
|
680
718
|
// FA-13: a text watermark is a tagged native text box; it is not content and never imports as a text block.
|
|
681
719
|
const textWatermark = importTextWatermark(nativeContext.shapes, nativeContext.paragraphs.map(shapeParagraphs => shapeParagraphs.map(paragraph => ({text: paragraph.text}))), relationships, entries, slideIndex,
|
|
@@ -833,10 +871,10 @@ function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensi
|
|
|
833
871
|
for (const item of metric.items) items.push({kind:'metric',bounds:shapeBounds(item.shape['p:spPr']?.['a:xfrm']),payload:item.payload,sources:sourcesOf([item.shape])});
|
|
834
872
|
for (const item of media.items) items.push({kind:'media',sourceText:item.payload.type==='text',bounds:shapeBounds(item.shape['p:spPr']?.['a:xfrm']),payload:item.payload,sources:sourcesOf([item.shape])});
|
|
835
873
|
for (const [index,shape] of shapes.entries()) {
|
|
836
|
-
const role = furniture.text.has(index) ? 'furniture' : nativeContext.
|
|
874
|
+
const role = furniture.text.has(index) ? 'furniture' : nativeContext.imageShapes?.has(index) ? 'image-overlay' : nativeContext.watermarkShapes?.has(index) ? 'watermark' : nativeContext.logoPlaceholderShapes?.has(index) ? 'logo' : cards.has(shape) ? 'card-frame'
|
|
837
875
|
: code.consumed.has(shape) ? 'code' : metric.consumed.has(shape) ? 'metric' : media.consumed.has(shape) ? 'media' : timelines.consumed.has(shape) ? 'timeline' : quotes.consumed.has(shape) ? 'quote' : undefined;
|
|
838
876
|
if (role) roles.set(shapeKeys.get(shape), role);
|
|
839
|
-
if (furniture.text.has(index) || nativeContext.
|
|
877
|
+
if (furniture.text.has(index) || nativeContext.imageShapes?.has(index) || nativeContext.watermarkShapes?.has(index) || nativeContext.logoPlaceholderShapes?.has(index)) continue;
|
|
840
878
|
if (annotations.consumed.has(shape)) { roles.set(shapeKeys.get(shape), 'annotation'); continue; }
|
|
841
879
|
if (code.consumed.has(shape)||metric.consumed.has(shape)||cards.has(shape)||media.consumed.has(shape)||headings.consumed.has(shape)||plainText.consumed.has(shape)||timelines.consumed.has(shape)||quotes.consumed.has(shape)) continue;
|
|
842
880
|
const ordinaryBody = Object.hasOwn(shape, 'p:txBody') && !shape['p:nvSpPr']?.['p:nvPr']?.['p:custDataLst']?.['p:tags'];
|
|
@@ -872,10 +910,13 @@ function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensi
|
|
|
872
910
|
}
|
|
873
911
|
|
|
874
912
|
for (const [index, picture] of nativeContext.pictures.entries()) {
|
|
875
|
-
const pictureRole = furniture.pictures.has(index) ? 'furniture' : nativeContext.
|
|
913
|
+
const pictureRole = furniture.pictures.has(index) ? 'furniture' : nativeContext.imagePictures?.has(index) ? 'background' : nativeContext.watermarkPictures?.has(index) ? 'watermark' : nativeContext.logoPictures?.has(index) ? 'logo' : quotes.consumedPictures.has(picture) ? 'quote' : undefined;
|
|
876
914
|
if (pictureRole) { roles.set(`pic:${index}`, pictureRole); continue; }
|
|
877
|
-
|
|
915
|
+
// A tagged, unchanged image block brings back its own fields (fit, focus, treatment, placement); its crop is theirs.
|
|
916
|
+
const treatment = nativeContext.imageBlocks?.get(index);
|
|
917
|
+
const report = diagnostic => { if (!(treatment && diagnostic.code === 'unsupported-image-crop')) options.onDiagnostic?.({...diagnostic, path: `slides.${slideIndex}.pictures.${index}`}); };
|
|
878
918
|
const item = importPicture(entries, picture, slidePath, relationships, report);
|
|
919
|
+
if (item?.payload && treatment) item.payload = {...item.payload, ...structuredClone(treatment)};
|
|
879
920
|
if (item) items.push({...item, sources: [`pic:${index}`]});
|
|
880
921
|
}
|
|
881
922
|
|
|
@@ -935,13 +976,17 @@ function importGraphicFrame(entries, frame, slidePath, relationships, importedTa
|
|
|
935
976
|
};
|
|
936
977
|
if (table) {
|
|
937
978
|
const stored = importedTable && record('table');
|
|
979
|
+
const restoredTable = stored ? restore('table', importedTable, () => restoreTableData(importedTable, stored, {evidence: tableCellEvidence(table, relationships), environment: tableEnvironmentEvidence(entries), datasets: data.datasets, report: diagnostic => data.report?.('table', diagnostic)})) : importedTable;
|
|
980
|
+
// FA-27: the frame's descr (or PowerPoint's decorative marker) is the table's text alternative.
|
|
981
|
+
const alt = restoredTable && typeof restoredTable === 'object' ? readFrameAlt(frame["p:nvGraphicFramePr"]?.["p:cNvPr"]) : undefined;
|
|
938
982
|
return {
|
|
939
983
|
kind: "table",
|
|
940
984
|
bounds,
|
|
941
985
|
name,
|
|
942
986
|
payload: {
|
|
943
987
|
type: "table",
|
|
944
|
-
|
|
988
|
+
// RR-59: an authored table restored from provenance takes its alt from the frame too, so an alt removed in PowerPoint stays removed.
|
|
989
|
+
table: restoredTable && typeof restoredTable === 'object' ? (({alt: _recorded, ...rest}) => alt === undefined ? rest : {...rest, alt})(restoredTable) : restoredTable
|
|
945
990
|
}
|
|
946
991
|
};
|
|
947
992
|
}
|
|
@@ -1628,8 +1673,17 @@ function assertValidBoundary(presentation) {
|
|
|
1628
1673
|
// RR-55: what a slide (or, for the deck level, an empty slide) resolves to in core, the one chain every engine shares: canvas,
|
|
1629
1674
|
// layout, font families, alignment and content box as composeSlide's options, plus the theme, colour scheme and font scheme
|
|
1630
1675
|
// records they came from. `diagnostics` names references that matched no record.
|
|
1676
|
+
// Core's slide context with the host's registered catalogs. Under strict export core throws OPFUnresolvedReferenceError for a
|
|
1677
|
+
// reference that resolves nowhere; the export fails with `unresolved-reference` naming the first one (its path and every diagnostic).
|
|
1631
1678
|
function coreContext(presentation, index, options) {
|
|
1632
|
-
|
|
1679
|
+
try {
|
|
1680
|
+
return resolveCoreSlideContext(presentation, index, {fonts: options.textMeasurement ? {textMeasurement: options.textMeasurement} : undefined, date: options.date,
|
|
1681
|
+
...(options.catalogs !== undefined ? {catalogs: options.catalogs} : {}), strictReferences: options.strictReferences === true});
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
if (error?.name !== "OPFUnresolvedReferenceError" || !Array.isArray(error.diagnostics)) throw error;
|
|
1684
|
+
const [first] = error.diagnostics;
|
|
1685
|
+
throw new OPFPptxError("unresolved-reference", `Strict export: ${error.message}`, {path: first?.path, diagnostics: error.diagnostics});
|
|
1686
|
+
}
|
|
1633
1687
|
}
|
|
1634
1688
|
|
|
1635
1689
|
// The families composition and the package use: core's, with the measurement provider's own resolution of the chosen family
|
|
@@ -1644,8 +1698,8 @@ function exportFonts(core, textMeasurement) {
|
|
|
1644
1698
|
function resolvePresentationContext(presentation, options, core = coreContext({...presentation, slides: [{}]}, 0, {...options, textMeasurement: undefined})) {
|
|
1645
1699
|
const design = presentation.design ?? {};
|
|
1646
1700
|
// `core` defaults to the deck level: the resolution of a slide that sets nothing of its own.
|
|
1647
|
-
// The theme and colour scheme records come from core, which falls back to the
|
|
1648
|
-
//
|
|
1701
|
+
// The theme and colour scheme records come from core, which falls back to the engine defaults (reporting unresolved-reference)
|
|
1702
|
+
// for a reference that resolves nowhere.
|
|
1649
1703
|
const {theme, colorScheme} = core.resolved;
|
|
1650
1704
|
const dimensions = {widthInches: core.options.width / 96, heightInches: core.options.height / 96};
|
|
1651
1705
|
const variables = resolveVariableColors(presentation.variables);
|
|
@@ -1776,20 +1830,28 @@ function masterBackground(context) {
|
|
|
1776
1830
|
}
|
|
1777
1831
|
|
|
1778
1832
|
// The theme part carries the deck-level scheme (slide overrides stay literal)
|
|
1779
|
-
// and, for
|
|
1780
|
-
|
|
1833
|
+
// and, for a theme reference that resolves (embedded or in a registered catalog), that theme's name, which import
|
|
1834
|
+
// recovery matches against the registered catalogs (FF-24).
|
|
1835
|
+
function exportTheme(presentation, context, options) {
|
|
1781
1836
|
const reference = presentation.design?.theme;
|
|
1782
|
-
const
|
|
1783
|
-
const theme = id ? context.theme : null;
|
|
1837
|
+
const theme = typeof reference === "string" ? resolveReference(presentation, "themes", reference, {catalogs: options.catalogs}) : undefined;
|
|
1784
1838
|
const scheme = context.colorScheme;
|
|
1785
1839
|
return {
|
|
1786
1840
|
themeColors: themeSlotColors(scheme),
|
|
1787
1841
|
schemeName: scheme.name ?? scheme.id ?? "OpenPresentation",
|
|
1788
|
-
themeName: theme
|
|
1842
|
+
themeName: theme ? theme.record.name ?? theme.id : undefined
|
|
1789
1843
|
};
|
|
1790
1844
|
}
|
|
1791
1845
|
|
|
1792
|
-
|
|
1846
|
+
// FF-24 recovery against the host's registered catalogs (FA-23). Only the first registered catalog, the host default, can be
|
|
1847
|
+
// named by a bare id in a document that declares no catalogs, so only its records are matched: an exact match imports as
|
|
1848
|
+
// that bare id. Everything else (no catalog registered, a scheme from another registered catalog, no exact match) imports
|
|
1849
|
+
// as an inline colour scheme, and no theme id is recovered.
|
|
1850
|
+
function hostDefaultRecords(kind, catalogs) {
|
|
1851
|
+
return catalogRecords({}, kind, {catalogs: catalogs ?? []}).filter(entry => entry.group === "default").map(entry => ({...entry.record, id: entry.id}));
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
function importThemeDesign(entries, presentationRoot, presentationRels, catalogs) {
|
|
1793
1855
|
const diagnostics = [], design = {};
|
|
1794
1856
|
const report = (code, path, message) => diagnostics.push({code, path, message});
|
|
1795
1857
|
const themePath = presentationThemePath(presentationRoot, presentationRels, path => parseRelationships(entries, path), entries);
|
|
@@ -1801,16 +1863,22 @@ function importThemeDesign(entries, presentationRoot, presentationRels) {
|
|
|
1801
1863
|
return {design, diagnostics};
|
|
1802
1864
|
}
|
|
1803
1865
|
const {colors, unreadable} = readThemeSlotColors(clrScheme);
|
|
1804
|
-
const records =
|
|
1866
|
+
const records = hostDefaultRecords("colorSchemes", catalogs);
|
|
1805
1867
|
if (Object.keys(colors).length) design.colorScheme = recoverColorScheme({colors, unreadable, name: scalarText(clrScheme.name)}, records).value;
|
|
1806
1868
|
if (unreadable.length) report("unsupported-theme-colors", "design.colorScheme", `Theme color slot(s) ${unreadable.join(", ")} do not resolve to an opaque sRGB color and were not imported.`);
|
|
1807
1869
|
const latin = font => scalarText(elements?.["a:fontScheme"]?.[font]?.["a:latin"]?.typeface);
|
|
1870
|
+
// A theme's own colour and font scheme references resolve in the theme's group first (core resolveReference).
|
|
1871
|
+
const themeReference = (kind, reference) => {
|
|
1872
|
+
const id = referenceId(reference);
|
|
1873
|
+
const found = id ? resolveReference({}, kind, id, {catalogs: catalogs ?? [], group: "default"}) : undefined;
|
|
1874
|
+
return found ? {...found.record, ...(isPlainObject(reference) ? reference : {})} : null;
|
|
1875
|
+
};
|
|
1808
1876
|
const recoveredTheme = recoverTheme({themeName: scalarText(theme?.name), colors, majorFont: latin("a:majorFont"), minorFont: latin("a:minorFont")}, {
|
|
1809
|
-
themes:
|
|
1810
|
-
|
|
1811
|
-
fontFamilies:
|
|
1812
|
-
const
|
|
1813
|
-
return
|
|
1877
|
+
themes: hostDefaultRecords("themes", catalogs),
|
|
1878
|
+
colorScheme: record => themeReference("colorSchemes", record.colorScheme),
|
|
1879
|
+
fontFamilies: record => {
|
|
1880
|
+
const scheme = themeReference("fontSchemes", record.fontScheme);
|
|
1881
|
+
return scheme ? resolveFontFamilies(scheme) : null;
|
|
1814
1882
|
}
|
|
1815
1883
|
});
|
|
1816
1884
|
if (recoveredTheme.unverified) report("theme-unverified", "design.theme", `The native theme has the name of OPF catalog theme '${recoveredTheme.unverified}', but neither its color scheme nor its heading/body fonts match that theme, so design.theme was not set.`);
|
|
@@ -1836,55 +1904,47 @@ function configurePresentation(pptx, presentation, context) {
|
|
|
1836
1904
|
}
|
|
1837
1905
|
|
|
1838
1906
|
async function addSlide(pptx, presentation, opfSlide, slideIndex, context, options) {
|
|
1907
|
+
options = {...options, textMeasurement: slideTextMeasurement(presentation, slideIndex, options)};
|
|
1839
1908
|
const slide = pptx.addSlide();
|
|
1840
1909
|
const slideContext = exportSlideContext(presentation, slideIndex, context, options);
|
|
1841
1910
|
const { widthInches, heightInches } = slideContext.dimensions;
|
|
1842
|
-
// A slide with no layout, or one
|
|
1843
|
-
// as in the preview; core reports the
|
|
1911
|
+
// A slide with no layout, or one whose layout resolves nowhere, is composed with no layout record (automatic composition),
|
|
1912
|
+
// as in the preview; core reports the reference as unresolved-reference.
|
|
1844
1913
|
// Core's resolved options (canvas, layout, families, alignment, content box, darkBackground), with the exporter's own additions:
|
|
1845
|
-
// the families after the measurement provider's resolution of the chosen family
|
|
1846
|
-
|
|
1914
|
+
// the families after the measurement provider's resolution of the chosen family and the raster padding. Core formats socials
|
|
1915
|
+
// handles from its own SOCIAL_PLATFORMS vocabulary.
|
|
1916
|
+
const composeOptions = { ...slideContext.core.options, fontFamilies: slideContext.composeFonts, textRasterPadding:options.textRasterPadding };
|
|
1847
1917
|
// Core resolves every shared design key once (slide design, deck design, then the layout record's design): the text alignment of
|
|
1848
|
-
// each item (item.alignment), and
|
|
1918
|
+
// each item (item.alignment), and each image item's own fit and treatment (item.image).
|
|
1849
1919
|
const geometry = composeSlide(opfSlide, composeOptions);
|
|
1850
|
-
slideContext.imageFill = geometry.design.imageFill ?? "fit";
|
|
1851
1920
|
// The default footer band (date left, text center, number right): where a footer added natively lands (RR-11).
|
|
1852
1921
|
if (slideIndex === 0) context.defaultFooterOptions = composeOptions;
|
|
1853
1922
|
slide.background = { color: slideContext.colors.background };
|
|
1854
|
-
|
|
1923
|
+
// An image background is core's geometry.backgroundImage (slide, deck, then theme background); every other kind is the
|
|
1924
|
+
// effective design or theme background definition.
|
|
1925
|
+
const backgroundDefinition = geometry.backgroundImage ? undefined : slideContext.backgroundDefinition;
|
|
1855
1926
|
const backgroundPath = `${opfSlide.design?.background !== undefined ? `slides.${slideIndex}.` : ''}design.background`;
|
|
1856
|
-
const backgroundFill = schemeBackgroundFill(backgroundDefinition, slideContext) ?? nativeBackgroundFill(backgroundDefinition, {
|
|
1927
|
+
const backgroundFill = backgroundDefinition === undefined ? null : schemeBackgroundFill(backgroundDefinition, slideContext) ?? nativeBackgroundFill(backgroundDefinition, {
|
|
1857
1928
|
width: slideContext.dimensions.widthInches, height: slideContext.dimensions.heightInches
|
|
1858
1929
|
}, slideContext.colors.background, slideContext.colors.text, (reference, hex) => schemeColorValue(reference, hex, slideContext), reference => resolveBackgroundColorRef(reference, slideContext.colorScheme, slideContext.variables));
|
|
1859
1930
|
if (backgroundFill) context.backgroundFills.set(`ppt/slides/slide${slideIndex + 1}.xml`, backgroundFill);
|
|
1860
1931
|
if (backgroundDefinition?.type === 'pattern' && !nativePatternPreset(backgroundDefinition.pattern?.preset)) {
|
|
1861
1932
|
options.onDiagnostic?.({code: 'unsupported-pattern', path: `${backgroundPath}.pattern.preset`, message: `Pattern ${backgroundDefinition.pattern?.preset} has no DrawingML preset; only its background color was exported.`});
|
|
1862
1933
|
}
|
|
1863
|
-
if (backgroundDefinition?.type === 'image') {
|
|
1864
|
-
const imagePath = `${backgroundPath}.image`;
|
|
1865
|
-
// An SVG background is its PNG raster at the slide's size: a slide background picture fill carries no SVG.
|
|
1866
|
-
const outcome = { box: { w: slideContext.dimensions.widthInches, h: slideContext.dimensions.heightInches }, cover: (backgroundDefinition.image?.fit ?? 'cover') !== 'contain' };
|
|
1867
|
-
const resolved = await resolveImage(backgroundDefinition.image, presentation, options, imagePath, outcome);
|
|
1868
|
-
if (resolved) {
|
|
1869
|
-
// PptxGenJS embeds the raster and its relationship; packaging replaces
|
|
1870
|
-
// its stretched fill with the fitted native picture fill.
|
|
1871
|
-
slide.background = { ...resolved };
|
|
1872
|
-
context.backgroundFills.set(`ppt/slides/slide${slideIndex + 1}.xml`, { image: {
|
|
1873
|
-
fit: backgroundDefinition.image?.fit ?? 'cover', opacity: backgroundDefinition.opacity ?? 1, imageFill: slideContext.imageFill, path: imagePath,
|
|
1874
|
-
width: slideContext.dimensions.widthInches * 96, height: slideContext.dimensions.heightInches * 96, report: options.onDiagnostic
|
|
1875
|
-
} });
|
|
1876
|
-
} else if (!outcome.reported) {
|
|
1877
|
-
options.onDiagnostic?.({code: 'unresolved-asset', path: imagePath, message: 'The background image needs an embedded raster, a declared asset or a host imageResolver; the slide background color was exported instead.'});
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
1934
|
slide.color = slideContext.textColor;
|
|
1881
1935
|
if (opfSlide.hidden === true) slide.hidden = true;
|
|
1882
1936
|
|
|
1883
1937
|
for (const diagnostic of geometry.diagnostics) options.onDiagnostic?.(diagnostic);
|
|
1884
1938
|
// The content topology (groups, regions, root form, block ids) with the leaf boxes this geometry draws.
|
|
1885
1939
|
recordContentTopology(context.documentProvenance, opfSlide, slideIndex, geometry.items);
|
|
1886
|
-
//
|
|
1887
|
-
|
|
1940
|
+
// Paint order, as in the preview (FA-22 contract): canvas colour, the background picture and its overlay, design.watermark,
|
|
1941
|
+
// the logo, then the items in core's order: the headings, the placed image blocks at their bands, then the flowed body.
|
|
1942
|
+
if (geometry.backgroundImage) await addBackgroundImage(slide, presentation, geometry.backgroundImage, slideIndex, slideContext, context, options);
|
|
1943
|
+
// RR-34: a captioned image's caption band follows its picture as tagged text boxes linked to the picture by name.
|
|
1944
|
+
const drawImage = async (item, itemContext) => {
|
|
1945
|
+
const name = await addImageItem(slide, presentation, item, slideIndex, itemContext, options);
|
|
1946
|
+
if (item.caption) addCaption(slide, item, name, itemContext, exportHelpers, (code, message) => new OPFPptxError(code, message, {path: item.caption.path}));
|
|
1947
|
+
};
|
|
1888
1948
|
await addWatermark(slide, presentation, opfSlide, slideIndex, slideContext, context, options);
|
|
1889
1949
|
// Cover and section slides: the deck logo core composed at the top-left of the free area, after the watermark and before content.
|
|
1890
1950
|
if (geometry.logo) await addLogo(slide, presentation, geometry.logo, slideIndex, slideContext, context, options);
|
|
@@ -1932,12 +1992,14 @@ async function addSlide(pptx, presentation, opfSlide, slideIndex, context, optio
|
|
|
1932
1992
|
});
|
|
1933
1993
|
} else if (item.field === "text" && typeof item.value === "string") {
|
|
1934
1994
|
slide.addText(item.text.lines.join("\n"), {...textBoxOptions(region, itemContext, item.text.fontSize * 0.75),...nativeFontOptions(item.textStyle),align:physicalAlignment(item.alignment, item.text.directions?.[0])});
|
|
1995
|
+
} else if (item.field === "image" && item.image) {
|
|
1996
|
+
await drawImage(item, itemContext);
|
|
1935
1997
|
} else {
|
|
1936
1998
|
// 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.
|
|
1937
|
-
const mediaNames = item.caption ? new Set([...context.
|
|
1999
|
+
const mediaNames = item.caption ? new Set([...context.images.keys(), ...context.imagePlaceholders.keys(), ...context.chartHeadings.keys(), ...context.tableHeaders.keys()]) : undefined;
|
|
1938
2000
|
await addPayload(slide, presentation, item.payload, region, item.path, { ...itemContext, composition: item.composition, contentAlignment: item.alignment ?? "left", direction: geometry.direction }, options, item.quoteLayout, item.codeLayout,item.metricLayout,item.timelineLayout);
|
|
1939
2001
|
if (item.caption) {
|
|
1940
|
-
const mediaName = item.field === 'video' ? `OPF media ${item.path} frame` : [...context.
|
|
2002
|
+
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));
|
|
1941
2003
|
addCaption(slide, item, mediaName, itemContext, exportHelpers, (code, message) => new OPFPptxError(code, message, {path: item.caption.path}));
|
|
1942
2004
|
}
|
|
1943
2005
|
}
|
|
@@ -2153,29 +2215,137 @@ function addListPayload(slide, items, region, context) {
|
|
|
2153
2215
|
slide.addText(runs, textBoxOptions(region, context, 15));
|
|
2154
2216
|
}
|
|
2155
2217
|
|
|
2218
|
+
// A content picture outside an image block (a timeline event image): one native picture contained in its region.
|
|
2156
2219
|
async function addImagePayload(slide, presentation, asset, region, path, context, options) {
|
|
2157
|
-
const
|
|
2220
|
+
const name = await addImagePicture(slide, presentation, asset, region, path, context, options, {fit: 'contain'});
|
|
2221
|
+
if (!name) addImagePlaceholder(slide, presentation, asset, region, path, context, options);
|
|
2222
|
+
return name;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
// One native image picture at `box` (inches), fitted at packaging (src/image-provenance.js placeImages). `treatment` is
|
|
2226
|
+
// what OPF_IMAGE_V1 stores (the authored block fields); without it the picture is untagged. Returns the picture's object
|
|
2227
|
+
// name, or undefined when the source could not be embedded (the caller draws the unavailable-image panel).
|
|
2228
|
+
async function addImagePicture(slide, presentation, asset, box, path, context, options, {fit, focus, effects = null, shrink = true, treatment, alt, role = 'block', objectName} = {}) {
|
|
2229
|
+
const outcome = { box, cover: fit === 'cover' };
|
|
2158
2230
|
const resolved = await resolveImage(asset, presentation, options, path, outcome);
|
|
2159
2231
|
if (!resolved) {
|
|
2160
|
-
|
|
2161
|
-
|
|
2232
|
+
// RR-59 (AUTO-26): a content picture never vanishes silently. A background names its own fallback (the slide colour).
|
|
2233
|
+
if (!outcome.reported) options.onDiagnostic?.(role === 'background' ? backgroundUnresolved(path) : {code: "unresolved-asset", path, reason: "unresolved-source", message: "The image needs an embedded raster, a declared asset or a host imageResolver; the image exports as the unavailable-image placeholder. Remote images are never fetched."});
|
|
2234
|
+
return undefined;
|
|
2162
2235
|
}
|
|
2163
|
-
const
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2236
|
+
const name = objectName ?? imageName(1 + [...context.images.keys()].filter(key => /^OPF image \d+$/.test(key)).length);
|
|
2237
|
+
// PowerPoint applies opacity (a:alphaModFix), grayscale (a:grayscl) and the border (a:ln) to an SVG picture (native check
|
|
2238
|
+
// 2026-10-01). A duotone recolor and a non-rectangular mask are not confirmed on an SVG picture, and a tiled picture fill
|
|
2239
|
+
// cannot carry an SVG: with any of them the SVG stays its PNG raster, so the effect applies as in the preview.
|
|
2240
|
+
if (outcome.svg) {
|
|
2241
|
+
const treated = [effects?.recolor?.type === 'duotone' && 'duotone recolor', effects?.shape && effects.shape.preset !== 'rect' && 'shape', fit === 'tile' && 'tile fit'].filter(Boolean);
|
|
2242
|
+
if (treated.length) options.onDiagnostic?.({ code: 'svg-image-rasterized', path, message: `An SVG image with ${treated.join(', ')} exports as its PNG raster, so the effect applies as in the preview; PowerPoint has not been confirmed to apply it to an SVG picture.` });
|
|
2243
|
+
else context.svgPictures.set(name, outcome.svg);
|
|
2244
|
+
}
|
|
2245
|
+
const text = alt !== undefined ? alt : assetAlt(asset, presentation);
|
|
2246
|
+
context.images.set(name, { slide: context.slidePath, role, path, box, fit, focus, shrink, effects, ...(treatment !== undefined ? {treatment} : {}) });
|
|
2247
|
+
context.pictureText.set(name, pictureText(text, asset, presentation));
|
|
2248
|
+
slide.addImage({ ...resolved, objectName: name, x: box.x, y: box.y, w: box.w, h: box.h, altText: text });
|
|
2249
|
+
return name;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
// The native effects of an image treatment, resolved in the slide's colour context: core's mask (item.image.shape), the
|
|
2253
|
+
// border line, opacity and recolor.
|
|
2254
|
+
function imageEffects(image, context) {
|
|
2255
|
+
const paint = (entry, fallback) => {
|
|
2256
|
+
const raw = exportColor(entry, context, fallback).replace(/^#/, '');
|
|
2257
|
+
return { hex: normalizeHex(raw, fallback), alpha: /^[0-9a-f]{8}$/i.test(raw) ? parseInt(raw.slice(6), 16) / 255 : 1 };
|
|
2258
|
+
};
|
|
2259
|
+
return {
|
|
2260
|
+
shape: image.shape ?? null,
|
|
2261
|
+
border: image.border ? { ...paint(image.border.color, context.colors.border), width: image.border.width } : null,
|
|
2262
|
+
opacity: image.opacity ?? null,
|
|
2263
|
+
recolor: image.recolor?.type === 'grayscale' ? { type: 'grayscale' }
|
|
2264
|
+
: image.recolor?.type === 'duotone' ? { type: 'duotone', dark: paint(image.recolor.dark, '000000'), light: paint(image.recolor.light, 'FFFFFF') } : null,
|
|
2265
|
+
paint
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// An overlay (core ComposedOverlay) as one native shape directly above its picture: the frame's shape, or an edge band.
|
|
2270
|
+
function addImageOverlay(slide, overlay, authored, name, owner, context, effects) {
|
|
2271
|
+
const {hex, alpha} = effects.paint(overlay.color, context.colors.text);
|
|
2272
|
+
context.imageOverlays.set(name, { slide: context.slidePath, owner, box: overlay.box, shape: overlay.shape, hex, alpha, opacity: overlay.opacity, overlay: authored });
|
|
2273
|
+
slide.addShape('rect', { x: overlay.box.x / 96, y: overlay.box.y / 96, w: overlay.box.width / 96, h: overlay.box.height / 96, objectName: name, fill: { color: hex } });
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
// The authored value at an OPF path the composition names (`slides.N.blocks.I`, `slides.N.design.background`,
|
|
2277
|
+
// `design.background`); `theme` is the resolved theme record's background.
|
|
2278
|
+
function authoredAt(presentation, path, slideContext) {
|
|
2279
|
+
if (path === 'theme') return slideContext.core?.resolved?.theme?.background;
|
|
2280
|
+
let value = presentation;
|
|
2281
|
+
for (const key of path.split('.')) value = value?.[/^\d+$/.test(key) ? Number(key) : key];
|
|
2282
|
+
return value;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// An image block (core ComposedItem.image): the picture at core's frame with its fit, focus and treatment, then its
|
|
2286
|
+
// overlay directly above. An unchanged picture and overlay import back as the same block (OPF_IMAGE_V1).
|
|
2287
|
+
async function addImageItem(slide, presentation, item, slideIndex, context, options) {
|
|
2288
|
+
const image = item.image;
|
|
2289
|
+
const box = { x: image.box.x / 96, y: image.box.y / 96, w: image.box.width / 96, h: image.box.height / 96 };
|
|
2290
|
+
const block = isPlainObject(item.payload) ? item.payload : {};
|
|
2291
|
+
const treatment = Object.fromEntries(IMAGE_TREATMENT_KEYS.filter(key => key !== 'overlay' && block[key] !== undefined).map(key => [key, structuredClone(block[key])]));
|
|
2292
|
+
const effects = imageEffects(image, context);
|
|
2293
|
+
const treated = Boolean(effects.border || effects.opacity !== null || effects.recolor || image.overlay || (effects.shape && effects.shape.preset !== 'rect'));
|
|
2294
|
+
const tagged = Object.keys(treatment).length > 0 || block.overlay !== undefined;
|
|
2295
|
+
// A plain contained picture in the flow is framed by the picture itself (0.14 content images); a treated or placed one
|
|
2296
|
+
// keeps core's frame (0.14 slide images), so its mask, line and overlay apply to that frame.
|
|
2297
|
+
const name = await addImagePicture(slide, presentation, item.value, box, item.path, { ...context, slidePath: `slides.${slideIndex}` }, options, {
|
|
2298
|
+
fit: image.fit, focus: image.focus, effects: treated ? effects : null, shrink: !treated && !image.placement, treatment: tagged ? treatment : undefined
|
|
2175
2299
|
});
|
|
2176
|
-
|
|
2300
|
+
if (!name) {
|
|
2301
|
+
addImagePlaceholder(slide, presentation, item.value, box, item.path, context, options);
|
|
2302
|
+
return `OPF image placeholder ${context.imagePlaceholders.size}`;
|
|
2303
|
+
}
|
|
2304
|
+
if (image.overlay) addImageOverlay(slide, image.overlay, structuredClone(block.overlay), imageOverlayName(name), name, { ...context, slidePath: `slides.${slideIndex}` }, effects);
|
|
2305
|
+
return name;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// An image background (core SlideComposition.backgroundImage). With alt text it is a full-slide picture at the back of
|
|
2309
|
+
// the slide over the canvas colour, carrying the alt text as its descr; without alt it is the native slide background
|
|
2310
|
+
// fill (written at packaging). Its overlay is one shape directly above the picture.
|
|
2311
|
+
async function addBackgroundImage(slide, presentation, background, slideIndex, slideContext, context, options) {
|
|
2312
|
+
const slidePath = `slides.${slideIndex}`, part = `ppt/slides/slide${slideIndex + 1}.xml`;
|
|
2313
|
+
const box = { x: 0, y: 0, w: slideContext.dimensions.widthInches, h: slideContext.dimensions.heightInches };
|
|
2314
|
+
const authored = authoredAt(presentation, background.path, slideContext);
|
|
2315
|
+
const effects = imageEffects({ opacity: background.opacity, recolor: background.recolor }, slideContext);
|
|
2316
|
+
let owner = 'background', overlayName = backgroundOverlayName(slidePath);
|
|
2317
|
+
// Only the background's own alt text makes it meaningful (FA-22): a referenced asset's alt does not count.
|
|
2318
|
+
const alt = background.alt;
|
|
2319
|
+
if (typeof alt === 'string' && alt) {
|
|
2320
|
+
const treatment = isPlainObject(authored) ? Object.fromEntries(BACKGROUND_IMAGE_KEYS.filter(key => key !== 'overlay' && authored[key] !== undefined).map(key => [key, structuredClone(authored[key])])) : {};
|
|
2321
|
+
const name = await addImagePicture(slide, presentation, background.src, box, background.path, { ...slideContext, slidePath }, options, {
|
|
2322
|
+
fit: background.fit, focus: background.focus, effects: background.opacity === undefined && !background.recolor ? null : effects, shrink: false,
|
|
2323
|
+
treatment, alt, role: 'background', objectName: backgroundImageName(slidePath)
|
|
2324
|
+
});
|
|
2325
|
+
// addImagePicture reported the unresolved source once (opf-pptx#210).
|
|
2326
|
+
if (!name) return;
|
|
2327
|
+
owner = name;
|
|
2328
|
+
overlayName = imageOverlayName(name);
|
|
2329
|
+
} else {
|
|
2330
|
+
// An SVG background is its PNG raster at the slide's size: a slide background picture fill carries no SVG.
|
|
2331
|
+
const outcome = { box, cover: background.fit === 'cover' };
|
|
2332
|
+
const resolved = await resolveImage(background.src, presentation, options, background.path, outcome);
|
|
2333
|
+
if (!resolved) {
|
|
2334
|
+
if (!outcome.reported) options.onDiagnostic?.(backgroundUnresolved(background.path));
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
// PptxGenJS embeds the raster and its relationship; packaging replaces its stretched fill with the fitted native fill.
|
|
2338
|
+
slide.background = { ...resolved };
|
|
2339
|
+
context.backgroundFills.set(part, { image: {
|
|
2340
|
+
fit: background.fit, focus: background.focus, opacity: background.opacity ?? 1, recolor: effects.recolor, path: background.path,
|
|
2341
|
+
width: box.w * 96, height: box.h * 96, report: options.onDiagnostic
|
|
2342
|
+
} });
|
|
2343
|
+
}
|
|
2344
|
+
if (background.overlay) addImageOverlay(slide, background.overlay, structuredClone(isPlainObject(authored) ? authored.overlay : undefined) ?? {color: background.overlay.color, opacity: background.overlay.opacity}, overlayName, owner, { ...slideContext, slidePath }, effects);
|
|
2177
2345
|
}
|
|
2178
2346
|
|
|
2347
|
+
const backgroundUnresolved = path => ({code: 'unresolved-asset', path, reason: 'unresolved-source', message: 'The background image needs an embedded raster, a declared asset or a host imageResolver; the slide background color was exported instead.'});
|
|
2348
|
+
|
|
2179
2349
|
// design.watermark: one native picture per slide, added after the slide image
|
|
2180
2350
|
// (and its overlay) and before all content, the preview's paint order. The
|
|
2181
2351
|
// slide's own design.watermark replaces the deck's, and false suppresses it.
|
|
@@ -2243,54 +2413,6 @@ async function addLogo(slide, presentation, logo, slideIndex, slideContext, cont
|
|
|
2243
2413
|
slide.addImage({...resolved, objectName: logoName(), ...region, altText: assetAlt(logo.source, presentation) ?? 'Logo'});
|
|
2244
2414
|
}
|
|
2245
2415
|
|
|
2246
|
-
// design.slideImage: one native picture at the shared frame, beneath content.
|
|
2247
|
-
// Crop/fit and treatments are written after PptxGenJS embeds the bytes.
|
|
2248
|
-
async function addSlideImage(slide, presentation, image, slideIndex, context, options) {
|
|
2249
|
-
const box = { x: image.box.x / 96, y: image.box.y / 96, w: image.box.width / 96, h: image.box.height / 96 };
|
|
2250
|
-
const outcome = { box, cover: image.fill === 'crop' };
|
|
2251
|
-
const resolved = await resolveImage(image.value, presentation, options, image.sourcePath, outcome);
|
|
2252
|
-
if (!resolved) {
|
|
2253
|
-
addImagePlaceholder(slide, presentation, image.value, box, image.sourcePath, context, options);
|
|
2254
|
-
return;
|
|
2255
|
-
}
|
|
2256
|
-
const objectName = slideImageName(`slides.${slideIndex}`);
|
|
2257
|
-
const configured = image.path === 'design.slideImage' ? presentation.design?.slideImage : presentation.slides[slideIndex].design?.slideImage;
|
|
2258
|
-
const { src: _source, ...treatment } = isPlainObject(configured) && 'position' in configured ? configured : {};
|
|
2259
|
-
const paint = (entry, fallback) => {
|
|
2260
|
-
const hex = normalizeHex(exportColor(entry, context, fallback), fallback);
|
|
2261
|
-
const raw = exportColor(entry, context, fallback).replace(/^#/, '');
|
|
2262
|
-
return { hex, alpha: /^[0-9a-f]{8}$/i.test(raw) ? parseInt(raw.slice(6), 16) / 255 : 1 };
|
|
2263
|
-
};
|
|
2264
|
-
// Native effects are resolved here, in the slide's color context.
|
|
2265
|
-
const effects = {
|
|
2266
|
-
shape: image.shape ?? null,
|
|
2267
|
-
border: image.border ? { ...paint(image.border.color, context.colors.border), width: image.border.width } : null,
|
|
2268
|
-
opacity: image.opacity ?? null,
|
|
2269
|
-
recolor: image.recolor?.type === 'grayscale' ? { type: 'grayscale' }
|
|
2270
|
-
: image.recolor?.type === 'duotone' ? { type: 'duotone', dark: paint(image.recolor.dark, '000000'), light: paint(image.recolor.light, 'FFFFFF') } : null,
|
|
2271
|
-
overlay: image.overlay ? { ...paint(image.overlay.color, context.colors.text), opacity: image.overlay.opacity, box: image.overlay.box, shape: image.overlay.shape } : null,
|
|
2272
|
-
};
|
|
2273
|
-
// FF-53: a root `image` with the slide image's source is the slide image (core `replacesContent`); the
|
|
2274
|
-
// manifest records that so an unchanged picture imports back as both design.slideImage and slide.image.
|
|
2275
|
-
context.slideImages.set(objectName, { slide: `slides.${slideIndex}`, box, fill: image.fill, path: image.sourcePath, treatment: { ...treatment, position: image.position }, effects, content: image.replacesContent === true });
|
|
2276
|
-
// PowerPoint applies opacity (a:alphaModFix), grayscale (a:grayscl) and the border (a:ln) to an SVG picture (native check
|
|
2277
|
-
// 2026-10-01). A duotone recolor and a non-rectangular mask are not confirmed on an SVG picture: with either the SVG stays its
|
|
2278
|
-
// PNG raster, so the effect applies as in the preview.
|
|
2279
|
-
if (outcome.svg) {
|
|
2280
|
-
const treated = [effects.recolor?.type === 'duotone' && 'duotone recolor', effects.shape && effects.shape.preset !== 'rect' && 'shape'].filter(Boolean);
|
|
2281
|
-
if (treated.length) options.onDiagnostic?.({ code: 'svg-image-rasterized', path: image.sourcePath, message: `An SVG slide image with ${treated.join(', ')} exports as its PNG raster, so the effect applies as in the preview; PowerPoint has not been confirmed to apply it to an SVG picture.` });
|
|
2282
|
-
else context.svgPictures.set(objectName, outcome.svg);
|
|
2283
|
-
}
|
|
2284
|
-
const slideImageAlt = image.alt ?? assetAlt(image.value, presentation);
|
|
2285
|
-
context.pictureText.set(objectName, pictureText(slideImageAlt, image.value, presentation));
|
|
2286
|
-
slide.addImage({ ...resolved, objectName, ...box, altText: slideImageAlt });
|
|
2287
|
-
// The overlay scrim is a separate native shape directly above the picture.
|
|
2288
|
-
if (effects.overlay) {
|
|
2289
|
-
const overlay = effects.overlay.box;
|
|
2290
|
-
slide.addShape('rect', { x: overlay.x / 96, y: overlay.y / 96, w: overlay.width / 96, h: overlay.height / 96, objectName: slideImageOverlayName(`slides.${slideIndex}`), fill: { color: effects.overlay.hex } });
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2293
|
-
|
|
2294
2416
|
// FF-62: chart text is one size. The preview draws every chart label (axis, legend, data label) at
|
|
2295
2417
|
// max(14, composition.minFontSize ?? 16) px scaled by the slide's shorter side over 720 px (renderer charts.js, `fontPx`),
|
|
2296
2418
|
// the readability floor: 12 pt on a 13.33 x 7.5 in slide. The export writes that size, in hundredths of a point (DrawingML `sz`).
|
|
@@ -2348,7 +2470,7 @@ function addChartPayload(slide, chart, region, context, options = {}, path = "ch
|
|
|
2348
2470
|
recordPayloadData(context, objectName, 'chart', presentation, path);
|
|
2349
2471
|
context.chartFonts.set(objectName,{heading:context.fonts.heading,body:context.fonts.body});
|
|
2350
2472
|
// 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.
|
|
2351
|
-
if (typeof chart.alt === 'string') context.
|
|
2473
|
+
if (typeof chart.alt === 'string') context.frameAlts.set(objectName, chart.alt);
|
|
2352
2474
|
const preferredPalette = CHART_COLORS.map(color=>`#${color}`);
|
|
2353
2475
|
const palette = chartPaletteForFill(panelFill, preferredPalette).map(color=>normalizeHex(color));
|
|
2354
2476
|
// FA-14: chart.highlight. A highlighted chart writes accent and muted colours over the series palette, and its data labels
|
|
@@ -2535,6 +2657,8 @@ function addTablePayload(slide, authoredTable, region, context, options, path, p
|
|
|
2535
2657
|
const objectName = `OPF table ${context.tableHeaders.size + 1}`;
|
|
2536
2658
|
context.tableHeaders.set(objectName, hasHeaders);
|
|
2537
2659
|
recordPayloadData(context, objectName, 'table', presentation, path, layout);
|
|
2660
|
+
// FA-27: the table's text alternative (the frame's descr; the empty string is PowerPoint's decorative marker).
|
|
2661
|
+
if (typeof authoredTable?.alt === 'string') context.frameAlts.set(objectName, authoredTable.alt);
|
|
2538
2662
|
if (layout.rows.some(row => row.cells.some(cell => Object.keys(cell.style ?? {}).length))) context.tableCells.set(objectName, {layout, scale, context, defaultBorder:{color:"accent5",width:1/scale}});
|
|
2539
2663
|
slide.addTable(rows, {
|
|
2540
2664
|
objectName,
|
|
@@ -2669,7 +2793,7 @@ async function addFurniture(slide,presentation,source,layout,context,options,sli
|
|
|
2669
2793
|
for(const [index,part]of layout.parts.entries()){
|
|
2670
2794
|
if(part.type==='image'){
|
|
2671
2795
|
const region={x:part.box.x/96,y:part.box.y/96,w:part.box.width/96,h:part.box.height/96};
|
|
2672
|
-
const objectName = await addImagePayload(slide,presentation,part.image,region,part.path,
|
|
2796
|
+
const objectName = await addImagePayload(slide,presentation,part.image,region,part.path,context,options);
|
|
2673
2797
|
// A generated deck logo is listed beside the manifest topology (see furnitureManifest) and tagged as a logo.
|
|
2674
2798
|
if (objectName && part.field === 'logo') context.furnitureLogoTags.set(objectName, {v:1, role:'furniture', group:String(slideIndex), furniture:part.kind, zone:part.zone});
|
|
2675
2799
|
else if (objectName) context.furnitureTags.set(objectName, {v:1, role:'image', group:String(slideIndex), part:manifestPartIndex(layout.parts, index)});
|
|
@@ -2837,6 +2961,8 @@ async function addQuotePhoto(slide, presentation, photo, group, context, options
|
|
|
2837
2961
|
const outcome = { box, cover: true };
|
|
2838
2962
|
const resolved = await resolveImage(photo.value, presentation, options, photo.path, outcome);
|
|
2839
2963
|
if (!resolved) {
|
|
2964
|
+
// opf-pptx#210: the headshot placeholder reports its cause like every other picture, so strict export (--fail-on warning) gates it.
|
|
2965
|
+
if (!outcome.reported) options.onDiagnostic?.({code: 'unresolved-asset', path: photo.path, reason: 'unresolved-source', message: 'The quote photo needs an embedded raster, a declared asset or a host imageResolver; the photo exports as the unavailable-image placeholder. Remote images are never fetched.'});
|
|
2840
2966
|
addImagePlaceholder(slide, presentation, photo.value, box, photo.path, context, options);
|
|
2841
2967
|
return;
|
|
2842
2968
|
}
|
|
@@ -3016,6 +3142,35 @@ function sameTypeface(requested, resolved) {
|
|
|
3016
3142
|
// A legacy four-style family such as "Roboto Medium" is the chosen typeface.
|
|
3017
3143
|
return got === want || got.startsWith(`${want} `) && isFaceStyleSuffix(got.slice(want.length + 1));
|
|
3018
3144
|
}
|
|
3145
|
+
// Use each slide's core script profile, including a rich run's own language, for every fitting path.
|
|
3146
|
+
// Latin roles follow that slide's scheme; explicit run families remain under the planner's strict glyph policy.
|
|
3147
|
+
function slideTextMeasurement(presentation, index, options) {
|
|
3148
|
+
const measurement = options.fonts?.textMeasurement;
|
|
3149
|
+
if (!measurement || !options.createScriptFonts) return options.textMeasurement;
|
|
3150
|
+
const core = coreContext(presentation, index, {...options, textMeasurement: undefined});
|
|
3151
|
+
const languages = new Map();
|
|
3152
|
+
const planner = language => {
|
|
3153
|
+
const key = language ?? '';
|
|
3154
|
+
if (!languages.has(key)) {
|
|
3155
|
+
const resolved = resolveScriptFonts(presentation, {slideIndex: index, ...(options.catalogs !== undefined ? {catalogs: options.catalogs} : {}), ...(language ? {language} : {})});
|
|
3156
|
+
const slots = role => {
|
|
3157
|
+
const latin = core.options.fontFamilies[role];
|
|
3158
|
+
const slot = name => ['latin', 'schemeFamily'].includes(resolved.sources?.[name]) ? latin : resolved[role]?.[name] ?? latin;
|
|
3159
|
+
return {latin, eastAsian: slot('eastAsian'), complexScript: slot('complexScript')};
|
|
3160
|
+
};
|
|
3161
|
+
languages.set(key, options.createScriptFonts({...resolved, heading: slots('heading'), body: slots('body'), serif: core.resolved.fontScheme.type === 'serif'}, measurement).textMeasurement);
|
|
3162
|
+
}
|
|
3163
|
+
return languages.get(key);
|
|
3164
|
+
};
|
|
3165
|
+
const base = planner(), pick = style => planner(style?.lang) ?? base;
|
|
3166
|
+
const adapted = {...base, measure: (text, size, style) => pick(style).measure(text, size, style)};
|
|
3167
|
+
// The planner's measurement object spreads its provider; inherited class methods must keep their receiver.
|
|
3168
|
+
if (typeof measurement.resolveStyle === 'function') adapted.resolveStyle = style => measurement.resolveStyle(style);
|
|
3169
|
+
if (typeof measurement.resolveFont === 'function') adapted.resolveFont = style => measurement.resolveFont(style);
|
|
3170
|
+
if (typeof base.outlineBounds === 'function') adapted.outlineBounds = (text, size, style) => pick(style).outlineBounds(text, size, style);
|
|
3171
|
+
return chosenFamilyMeasurement(adapted);
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3019
3174
|
function chosenFamilyMeasurement(measurement) {
|
|
3020
3175
|
if (!measurement || typeof measurement.resolveStyle !== 'function') return measurement;
|
|
3021
3176
|
const resolveFont = typeof measurement.resolveFont === 'function' ? style => measurement.resolveFont(style) : undefined;
|
|
@@ -3307,6 +3462,14 @@ async function resolveImage(asset, presentation, options, path, outcome = {}) {
|
|
|
3307
3462
|
}
|
|
3308
3463
|
return resolveSvgImage(file, options, path, outcome);
|
|
3309
3464
|
}
|
|
3465
|
+
// opf-pptx#210: a local raster PptxGenJS cannot read used to fail the whole export (pptxgen-failed) on every picture path.
|
|
3466
|
+
// It is unresolved like any other source: the placeholder (or the path's own fallback) and one diagnostic.
|
|
3467
|
+
if (resolved?.path && !(await localFileReadable(resolved.path))) {
|
|
3468
|
+
if (options.strictAssets) throw new OPFPptxError("missing-asset", "The local image file could not be read.", { path, src: resolved.path, reason: "file-unreadable" });
|
|
3469
|
+
options.onDiagnostic?.({ code: "unresolved-asset", path, reason: "file-unreadable", message: "The local image file could not be read; the image exports as the unavailable-image placeholder (or the picture's fallback)." });
|
|
3470
|
+
outcome.reported = true;
|
|
3471
|
+
return null;
|
|
3472
|
+
}
|
|
3310
3473
|
if (!resolved?.data) return resolved;
|
|
3311
3474
|
const bytes = dataUriBytes(resolved.data);
|
|
3312
3475
|
const raster = bytes && rasterMetadata(bytes);
|
|
@@ -3352,7 +3515,7 @@ async function resolveSvgImage(source, options, path, outcome) {
|
|
|
3352
3515
|
} catch (error) {
|
|
3353
3516
|
const reason = error?.code === "svg-rasterizer-unavailable" ? "svg-rasterizer-unavailable" : "svg-render-failed";
|
|
3354
3517
|
return unresolved(reason, reason, reason === "svg-rasterizer-unavailable"
|
|
3355
|
-
?
|
|
3518
|
+
? `An SVG image needs a rasterizer for its PNG fallback: install the optional peer @openpresentation/opf-render${typeof error.install === "string" ? ` and its PNG converter (\`${error.install}\`)` : ""}, or pass options.svgRasterizer (or an imageResolver that returns a raster).`
|
|
3356
3519
|
: `The SVG image could not be rasterized for its PNG fallback (${errorMessage(error)}).`);
|
|
3357
3520
|
}
|
|
3358
3521
|
options.svgRasters.set(cacheKey, png);
|
|
@@ -3481,39 +3644,6 @@ function referenceId(reference) {
|
|
|
3481
3644
|
return null;
|
|
3482
3645
|
}
|
|
3483
3646
|
|
|
3484
|
-
const DEFAULT_SOURCE_PREFIX = "https://www.pptx.gallery/";
|
|
3485
|
-
|
|
3486
|
-
// Same order as opf-render's socialPlatformRecords so preview and export format
|
|
3487
|
-
// socials identically. Core applies inline document records first; then the
|
|
3488
|
-
// document catalog source (host-supplied options.catalogSources, or the bundled
|
|
3489
|
-
// catalog for pptx.gallery/pkg sources), injected options.catalogs, and the
|
|
3490
|
-
// bundled catalog (the engine default source also resolves to it).
|
|
3491
|
-
function socialPlatformRecords(presentation, options) {
|
|
3492
|
-
const kind = "socialPlatforms", declared = presentation.catalogs?.[kind]?.source;
|
|
3493
|
-
// `source` is one source or an ordered search path (an array): records of each source in order, first match wins.
|
|
3494
|
-
const sourceRecords = (Array.isArray(declared) ? declared : [declared]).filter(source => typeof source === "string").flatMap(source => {
|
|
3495
|
-
const bySource = options.catalogSources?.[source];
|
|
3496
|
-
return bySource ? normalizeRecords(bySource)
|
|
3497
|
-
: source.startsWith(DEFAULT_SOURCE_PREFIX) || source.startsWith("pkg:@openpresentation/opf/") ? defaultCatalog(kind) : [];
|
|
3498
|
-
});
|
|
3499
|
-
return [...sourceRecords, ...normalizeRecords(options.catalogs?.[kind]), ...defaultCatalog(kind)];
|
|
3500
|
-
}
|
|
3501
|
-
|
|
3502
|
-
function defaultCatalog(kind) {
|
|
3503
|
-
return Array.isArray(bundledCatalogs[kind]) ? bundledCatalogs[kind] : [];
|
|
3504
|
-
}
|
|
3505
|
-
|
|
3506
|
-
function normalizeRecords(catalog) {
|
|
3507
|
-
if (!catalog) return [];
|
|
3508
|
-
if (Array.isArray(catalog)) return catalog;
|
|
3509
|
-
if (Array.isArray(catalog.records)) return catalog.records;
|
|
3510
|
-
return [];
|
|
3511
|
-
}
|
|
3512
|
-
|
|
3513
|
-
function findById(records, id) {
|
|
3514
|
-
return records.find((record) => record?.id === id) ?? null;
|
|
3515
|
-
}
|
|
3516
|
-
|
|
3517
3647
|
// A solid color or pattern background color is a ColorRef (hex, `var:` variable, colour-scheme slot or role), resolved
|
|
3518
3648
|
// as for table fills and run colours. Roles resolve through the colour scheme alone: the background cannot depend on itself.
|
|
3519
3649
|
function resolveBackgroundColorRef(entry, colorScheme, variables = {}) {
|
|
@@ -3631,7 +3761,7 @@ async function normalizePptxZip(raw, context) {
|
|
|
3631
3761
|
}
|
|
3632
3762
|
}
|
|
3633
3763
|
applyChartFonts(entries,context.chartFonts,parseRelationships);
|
|
3634
|
-
if (context.
|
|
3764
|
+
if (context.frameAlts.size) for (const [part, bytes] of Object.entries(entries)) if (/^ppt\/slides\/slide\d+\.xml$/.test(part)) { const xml = decodeText(bytes), next = applyFrameAlt(xml, context.frameAlts); if (next !== xml) entries[part] = encodeText(next); }
|
|
3635
3765
|
// Chartex charts: the native cx:chartSpace part, its style parts and the mc:AlternateContent frame (the classic chart above is the fallback).
|
|
3636
3766
|
try {
|
|
3637
3767
|
attachChartexParts(entries, context.chartex, parseRelationships);
|
|
@@ -3650,7 +3780,7 @@ async function normalizePptxZip(raw, context) {
|
|
|
3650
3780
|
const relationships = parseRelationships(entries, part);
|
|
3651
3781
|
for (const [picture] of decodeText(bytes).matchAll(/<p:pic>[\s\S]*?<\/p:pic>/g)) {
|
|
3652
3782
|
const name = picture.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1];
|
|
3653
|
-
const placement = context.
|
|
3783
|
+
const placement = context.images.get(name) ?? context.quotePhotos.get(name) ?? (name === watermarkName() ? context.watermarks.get(part) : name === logoName() ? context.logos.get(part) : undefined);
|
|
3654
3784
|
const id = picture.match(/<a:blip\b[^>]*r:embed="([^"]+)"/)?.[1];
|
|
3655
3785
|
if (placement) imageSources.set(relationships.get(id)?.path, placement.path);
|
|
3656
3786
|
}
|
|
@@ -3679,11 +3809,17 @@ async function normalizePptxZip(raw, context) {
|
|
|
3679
3809
|
}
|
|
3680
3810
|
imageMetadata.set(part, metadata);
|
|
3681
3811
|
}
|
|
3682
|
-
//
|
|
3812
|
+
// Image pictures need the embedded dimensions (core's fitImage crops and pads them); document provenance (FF-32)
|
|
3683
3813
|
// later records the placed frames as slide geometry evidence.
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3814
|
+
try {
|
|
3815
|
+
placeImages(entries, context.images, context.imageOverlays, {
|
|
3816
|
+
metadataFor: (part, id) => imageMetadata.get(parseRelationships(entries, part).get(id)?.path), fitImage,
|
|
3817
|
+
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 }); }
|
|
3818
|
+
});
|
|
3819
|
+
} catch (error) {
|
|
3820
|
+
if (error instanceof OPFPptxError) throw error;
|
|
3821
|
+
throw new OPFPptxError("packaging-failed", `Image pictures could not be placed: ${errorMessage(error)}`);
|
|
3822
|
+
}
|
|
3687
3823
|
tagTextWatermarks(entries, context.textWatermarks);
|
|
3688
3824
|
placeWatermarks(entries, context.watermarks, (part, id) => imageMetadata.get(parseRelationships(entries, part).get(id)?.path), path => {
|
|
3689
3825
|
throw new OPFPptxError("unsupported-image-dimensions", "Image fitting requires readable PNG, JPEG, GIF or WebP dimensions. Supply a supported raster image through imageResolver.", { path });
|
|
@@ -3842,7 +3978,7 @@ function normalizePartBytes(path, bytes, context, renameMaps, entries, imageMeta
|
|
|
3842
3978
|
const metadata = imageMetadata.get(backgroundRelationships.get(id)?.path);
|
|
3843
3979
|
if (!id || !metadata) throw new OPFPptxError("unsupported-image-dimensions", "Background image fitting requires readable PNG, JPEG, GIF or WebP dimensions. Supply a supported raster image through imageResolver.", { path: fill.image.path });
|
|
3844
3980
|
if ((metadata.orientation ?? 1) !== 1) fill.image.report?.({code: 'unsupported-background-image-orientation', path: fill.image.path, message: 'A slide background picture fill cannot rotate or mirror its image; the JPEG EXIF orientation is not applied in the native background.'});
|
|
3845
|
-
return `<p:bg><p:bgPr>${nativeImageBackgroundFill(id, metadata, fill.image)}<a:effectLst/></p:bgPr></p:bg>`;
|
|
3981
|
+
return `<p:bg><p:bgPr>${nativeImageBackgroundFill(id, metadata, {...fill.image, fitImage})}<a:effectLst/></p:bgPr></p:bg>`;
|
|
3846
3982
|
});
|
|
3847
3983
|
}
|
|
3848
3984
|
// PptxGenJS table IDs can collide with other objects on the same slide.
|
|
@@ -3928,7 +4064,8 @@ function normalizePartBytes(path, bytes, context, renameMaps, entries, imageMeta
|
|
|
3928
4064
|
}
|
|
3929
4065
|
const text = context.pictureText.get(picture.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1]);
|
|
3930
4066
|
if (text) picture = writePictureText(picture, text);
|
|
3931
|
-
|
|
4067
|
+
// Image pictures were placed with the slide's other pictures (placeImages); quote photos are fitted here.
|
|
4068
|
+
const placement = context.quotePhotos.get(picture.match(/name="(OPF quote photo \d+)"/)?.[1]);
|
|
3932
4069
|
if (!placement) return picture;
|
|
3933
4070
|
const id = picture.match(/<a:blip\b[^>]*r:embed="([^"]+)"/)?.[1];
|
|
3934
4071
|
const dimensions = imageMetadata.get(relationships.get(id)?.path);
|