@openpresentation/opf-pptx 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -10
- package/dist/chart-order.js +76 -0
- package/dist/content-topology.js +25 -0
- package/dist/data-provenance.js +71 -4
- package/dist/document-provenance.js +34 -16
- package/dist/furniture-provenance.js +179 -69
- package/dist/image-fallback-node.js +2 -2
- package/dist/image-provenance.js +54 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +204 -56
- package/dist/logo-provenance.js +8 -8
- package/dist/master-furniture.js +326 -0
- package/dist/media-dedupe.js +2 -0
- package/dist/native-furniture.js +6 -2
- package/dist/run-colors.js +28 -2
- package/package.json +9 -4
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {applyChartNumberFormats,chartNumber,excelCode,formattedColumns,inlineCha
|
|
|
10
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 {applyFrameAlt,readFrameAlt} from './frame-alt.js';
|
|
13
|
+
import {applyFrameAlt,readFrameAlt,writeFrameAlt} 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';
|
|
@@ -20,26 +20,28 @@ import {attachMediaTags,importMediaGroups,mediaCaption,mediaFrameRecord,mediaTex
|
|
|
20
20
|
import {addCaption, addFootnotes} from './annotation-export.js';
|
|
21
21
|
import {attachAnnotationTags, captionValue, importAnnotations, restoreCitations} from './annotation-provenance.js';
|
|
22
22
|
import {attachHeadingTags,importHeadingGroups} from './heading-provenance.js';
|
|
23
|
-
import {headingValue,joinRichLines} from './rich-heading.js';
|
|
23
|
+
import {cleanBase,headingValue,joinRichLines} from './rich-heading.js';
|
|
24
24
|
import {attachPlainTextTags,importPlainTextGroups} from './text-provenance.js';
|
|
25
25
|
import {attachTimelineTags,timelineManifest,importTimelineGroups} from './timeline-provenance.js';
|
|
26
26
|
import {attachQuoteTags,quoteManifest,importQuoteGroups,quotePhotoName} from './quote-provenance.js';
|
|
27
|
-
import {attachFurnitureTags, furnitureManifest, importFurniture,
|
|
27
|
+
import {attachFurnitureTags, furnitureManifest, importFurniture, logoShape, staticDateFallback, stampFurnitureImageKeys} from './furniture-provenance.js';
|
|
28
28
|
import {restoreRunColors} from './run-colors.js';
|
|
29
29
|
import {joinWrappedText} from './content-topology.js';
|
|
30
|
-
import {applyDocumentProvenance, attachDocumentProvenance, documentProvenance, recordContentTopology, restoreDocumentProvenance, joinAuthors, splitAuthors,
|
|
30
|
+
import {applyDocumentProvenance, attachDocumentProvenance, documentProvenance, recordContentTopology, recordListRuns, restoreDocumentProvenance, joinAuthors, splitAuthors, slideListRecord, storedMetadata, DEFAULT_AUTHOR, DEFAULT_TITLE} from './document-provenance.js';
|
|
31
31
|
import {INVALID_XML_CHARACTER, nativeSections, writeSectionList} from './sections.js';
|
|
32
32
|
import {attachFurnitureFields, furniturePartFields, lineFields, nativeFieldType} from './furniture-fields.js';
|
|
33
33
|
import {attachNativePlaceholders, defaultPlaceholderGeometry, nativeDateText, nativeFurnitureParts, nativePlaceholderForPart, writeNativeMasters} from './native-furniture.js';
|
|
34
|
+
import {liftMasterFurniture} from './master-furniture.js';
|
|
34
35
|
import {importImageOrientation} from './image-import.js';
|
|
35
36
|
import {extractSignals, normalizeSignalOptions, themeFactsFor} from './import-signals.js';
|
|
36
|
-
import {placeImages, importImages, imageName, imageOverlayName, backgroundImageName, backgroundOverlayName, IMAGE_TREATMENT_KEYS, BACKGROUND_IMAGE_KEYS} from './image-provenance.js';
|
|
37
|
+
import {placeImages, importImages, importImagePlaceholders, isImagePlaceholderGroup, imageName, imageOverlayName, imagePlaceholderGroupName, backgroundImageName, backgroundOverlayName, IMAGE_TREATMENT_KEYS, BACKGROUND_IMAGE_KEYS, IMAGE_PLACEHOLDER_TAG} from './image-provenance.js';
|
|
37
38
|
import {dedupeMedia} from './media-dedupe.js';
|
|
38
39
|
import {placeWatermarks, importWatermark, importTextWatermark, tagTextWatermarks, watermarkName, watermarkTextName, watermarkBox, watermarkOpacity} from './watermark-provenance.js';
|
|
39
40
|
import {placeLogos, importLogo, importLogoPlaceholders, logoName, LOGO_TAG} from './logo-provenance.js';
|
|
40
41
|
import {nativeBackgroundFill, nativeImageBackgroundFill, nativePatternPreset} from './background.js';
|
|
41
42
|
import {importBackground} from './background-import.js';
|
|
42
43
|
import {chartHighlightPlan, applyChartHighlight} from './chart-highlight.js';
|
|
44
|
+
import {orderChartSeries} from './chart-order.js';
|
|
43
45
|
import {themeSlotColors, writeThemeColors, schemeColorValue, schemeBackgroundFill, schemeBackgroundValue, defaultTextSchemeValues, tableTextSchemeValue, solidColorXml, writeMasterBackground, inheritLayoutBackground, readThemeSlotColors, recoverColorScheme, recoverTheme, presentationThemePath} from './theme-colors.js';
|
|
44
46
|
import {languageDiagnostics, observeLanguage, observedRtl, partScriptFonts, physicalAlignment, planScriptFonts, planSlideThemes, reconcileLanguage, reportPerSlideNotesScriptFonts, runLanguageFonts, runLanguageTag, stripRunScriptFonts, themeEastAsianFromLatin} from './script-fonts.js';
|
|
45
47
|
import { webpToPng, svgToPng, readLocalFile, localFileReadable } from '#image-fallback';
|
|
@@ -254,6 +256,9 @@ export async function toPptx(input, options = {}) {
|
|
|
254
256
|
const context = resolvePresentationContext(presentation, {...options,textMeasurement:undefined});
|
|
255
257
|
context.listMarkers = new Map();
|
|
256
258
|
context.imagePlaceholders = new Map();
|
|
259
|
+
// opf-pptx#221: the placeholder panels written as one native group, and the OPF_IMAGE_PLACEHOLDER_V1 records of image blocks.
|
|
260
|
+
context.imagePlaceholderGroups = new Set();
|
|
261
|
+
context.imagePlaceholderTags = new Map();
|
|
257
262
|
context.tableHeaders = new Map();
|
|
258
263
|
context.tableCells = new Map();
|
|
259
264
|
// FA-23: every exported image picture (image blocks, the alt-text form of an image background, timeline pictures) and
|
|
@@ -278,10 +283,13 @@ export async function toPptx(input, options = {}) {
|
|
|
278
283
|
context.quoteTags = new Map();
|
|
279
284
|
context.quotePhotos = new Map();
|
|
280
285
|
context.furnitureTags = new Map();
|
|
281
|
-
context.
|
|
286
|
+
context.furnitureImages = new Map();
|
|
282
287
|
context.furnitureFields = new Map();
|
|
283
288
|
context.furnitureManifests = new Map();
|
|
284
289
|
context.nativeFurniture = new Map();
|
|
290
|
+
context.nativeFurnitureBands = new Map();
|
|
291
|
+
// RR-72: each slide's furniture parts and the object names of their shapes, for the slide master and layouts.
|
|
292
|
+
context.furnitureParts = [];
|
|
285
293
|
context.hostDate = options.date;
|
|
286
294
|
context.authored = authored;
|
|
287
295
|
context.codeTags = new Map();
|
|
@@ -408,6 +416,19 @@ export async function fromPptx(input, options = {}) {
|
|
|
408
416
|
// Layouts and the master are read once however many slides inherit a placeholder from them (RR-11).
|
|
409
417
|
const cached = read => { const memo = new Map(); return path => { if (!memo.has(path)) memo.set(path, read(path)); return memo.get(path); }; };
|
|
410
418
|
const cachedRelationships = cached(path => parseRelationships(entries, path)), cachedPart = cached(path => parseOptionalXml(entries, path));
|
|
419
|
+
// RR-72: the furniture a slide shows from its layout and slide master (src/master-furniture.js), read once per part.
|
|
420
|
+
const inheritedParts = new Map();
|
|
421
|
+
const inheritedPart = path => {
|
|
422
|
+
if (!path) return undefined;
|
|
423
|
+
if (!inheritedParts.has(path)) {
|
|
424
|
+
const element = /slideMaster/.test(path) ? 'p:sldMaster' : 'p:sldLayout', root = cachedPart(path)?.[element];
|
|
425
|
+
const tree = root?.['p:cSld']?.['p:spTree'];
|
|
426
|
+
inheritedParts.set(path, root ? {path, root, relationships: cachedRelationships(path), shapes: nativeTextShapes(tree), pictures: nativePictures(tree),
|
|
427
|
+
paragraphs: nativeShapeParagraphs(decodeText(entries[path]), element)} : undefined);
|
|
428
|
+
}
|
|
429
|
+
return inheritedParts.get(path);
|
|
430
|
+
};
|
|
431
|
+
const relatedPath = (path, type) => path ? [...cachedRelationships(path).values()].find(rel => rel.type === `http://schemas.openxmlformats.org/officeDocument/2006/relationships/${type}` && rel.targetMode !== 'External' && entries[rel.path])?.path : undefined;
|
|
411
432
|
const furnitureContexts = slidePaths.map((slidePath, index) => {
|
|
412
433
|
const root = parseRequiredXml(entries, slidePath)['p:sld'];
|
|
413
434
|
if (!root) throw new OPFPptxError('invalid-pptx', `PPTX slide is not a PresentationML slide: ${slidePath}.`, {path: slidePath});
|
|
@@ -417,7 +438,12 @@ export async function fromPptx(input, options = {}) {
|
|
|
417
438
|
path: slidePath, slideWidth: Number(presentationRoot['p:sldSz']?.cx), relationshipsOf: cachedRelationships, readPart: cachedPart,
|
|
418
439
|
paragraphs: nativeShapeParagraphs(decodeText(entries[slidePath])),
|
|
419
440
|
readPicture: picture => importPicture(entries, picture, slidePath, relationships,
|
|
420
|
-
diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${index}.design`}))
|
|
441
|
+
diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${index}.design`})),
|
|
442
|
+
inherited: () => {
|
|
443
|
+
const layout = relatedPath(slidePath, 'slideLayout'), master = relatedPath(layout, 'slideMaster');
|
|
444
|
+
return Object.fromEntries([['layout', inheritedPart(layout)], ['master', inheritedPart(master)]].filter(([, part]) => part).map(([on, part]) => [on, {...part,
|
|
445
|
+
readPicture: picture => importPicture(entries, picture, part.path, part.relationships, diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${index}.design`}))}]));
|
|
446
|
+
}};
|
|
421
447
|
});
|
|
422
448
|
// Native sections (PowerPoint's own section list, `Default Section` = none) name each slide's `{{slide.section}}` for the footer text check
|
|
423
449
|
// and, after the slides, are reconciled with the stored value in restoreDocumentProvenance.
|
|
@@ -432,7 +458,7 @@ export async function fromPptx(input, options = {}) {
|
|
|
432
458
|
...(stored.organization !== undefined ? {organization: stored.organization} : {}),
|
|
433
459
|
...(stored.speaker !== undefined ? {speaker: stored.speaker} : {}),
|
|
434
460
|
};
|
|
435
|
-
const furniture = importFurniture(furnitureContexts, entries, options.onDiagnostic, {sections: slideSections ?? [], organizations: asArray(stored.organization).filter(item => isPlainObject(item) && typeof item.id === 'string' && typeof item.name === 'string').map(item => ({id: item.id, name: item.name})),
|
|
461
|
+
const furniture = importFurniture(furnitureContexts, entries, options.onDiagnostic, {sections: slideSections ?? [], organizations: asArray(stored.organization).filter(item => isPlainObject(item) && typeof item.id === 'string' && typeof item.name === 'string').map(item => ({id: item.id, name: item.name, ...(typeof item.role === 'string' ? {role: item.role} : {}), ...(logoShape(item.logo) !== undefined ? {logo: logoShape(item.logo)} : {})})),
|
|
436
462
|
draw: (template, {index, count, section}) => drawFurnitureText(template, {index, count, section}, importedMetadata)});
|
|
437
463
|
if (Object.keys(furniture.design).length) imported.design = {...imported.design, ...furniture.design};
|
|
438
464
|
if (furniture.organization) imported.organization = furniture.organization;
|
|
@@ -530,21 +556,19 @@ function applyRunColors(imported, slideProvenance, restoredGroups, report, optio
|
|
|
530
556
|
restoreRunColors(imported.slides, info, resolve, report);
|
|
531
557
|
}
|
|
532
558
|
|
|
533
|
-
// A consumed logo picture (OPF_LOGO_V1) never becomes content. Its own image restores the logo only when
|
|
534
|
-
//
|
|
535
|
-
//
|
|
559
|
+
// A consumed logo picture (OPF_LOGO_V1) never becomes content. Its own image restores the organization's logo only when the
|
|
560
|
+
// stored document did not (RR-71: logos live on the organization), so a package exported without provenance still keeps its
|
|
561
|
+
// logo: on the primary organization, else on a new one named after the deck. The picture is one drawn asset, so it becomes the
|
|
562
|
+
// organization's single logo for every shape.
|
|
536
563
|
function restoreLogoFallback(imported, contexts) {
|
|
537
564
|
const organizations = asArray(imported.organization);
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
if (fallback.path.startsWith('slides.')) { if (slide.design?.logo === undefined) slide.design = {...slide.design, logo: image}; }
|
|
546
|
-
else if (imported.design?.logo === undefined && !organizations.some(item => item?.logo !== undefined)) imported.design = {...imported.design, logo: image};
|
|
547
|
-
});
|
|
565
|
+
if (organizations.some(item => item?.logo !== undefined)) return;
|
|
566
|
+
const image = contexts.map(context => context.logoFallback?.image).find(Boolean);
|
|
567
|
+
if (!image) return;
|
|
568
|
+
const primary = organizations.find(item => item?.role === 'primary') ?? organizations[0];
|
|
569
|
+
if (!primary) { imported.organization = {id: 'brand', name: imported.name ?? 'Brand', logo: image}; return; }
|
|
570
|
+
const index = organizations.indexOf(primary), withLogo = {...primary, logo: image};
|
|
571
|
+
imported.organization = Array.isArray(imported.organization) ? imported.organization.map((item, at) => at === index ? withLogo : item) : withLogo;
|
|
548
572
|
}
|
|
549
573
|
|
|
550
574
|
function readPptxZip(input) {
|
|
@@ -752,7 +776,7 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
752
776
|
diagnostic => options.onDiagnostic?.({...diagnostic, path: watermarkPath}));
|
|
753
777
|
if (watermark.design) slide.design = {...slide.design, ...watermark.design};
|
|
754
778
|
nativeContext.watermarkPictures = watermark.consumed;
|
|
755
|
-
// The generated
|
|
779
|
+
// The generated cover logo picture is not content; the logo returns from the stored organization (or, without one, from the picture).
|
|
756
780
|
const logoPath = `slides.${slideIndex}.design.logo`;
|
|
757
781
|
const logo = importLogo(nativeContext.pictures, relationships, entries, slideIndex,
|
|
758
782
|
picture => importPicture(entries, picture, slidePath, relationships, diagnostic => {
|
|
@@ -795,8 +819,8 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
795
819
|
item.text = current.map(paragraph => paragraph.text).join('\n');
|
|
796
820
|
}
|
|
797
821
|
}
|
|
798
|
-
const
|
|
799
|
-
const content = mergeAdjacentBulletShapes(mergeOpfListShapes(items,
|
|
822
|
+
const listRecord = slideListRecord(entries, slidePath, slideRoot, relationships);
|
|
823
|
+
const content = mergeAdjacentBulletShapes(mergeOpfListShapes(items, listRecord.breaks, diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${slideIndex}`}), listRecord.runs))
|
|
800
824
|
.map((item) => ({payload: payloadFromSlideItem(item, diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${slideIndex}`})), bounds: item.visualBounds ?? item.bounds, sources: item.sources ?? []}))
|
|
801
825
|
.filter((entry) => entry.payload);
|
|
802
826
|
const blocks = content.map((entry) => entry.payload);
|
|
@@ -840,7 +864,8 @@ function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensi
|
|
|
840
864
|
const tree = slideRoot["p:cSld"]?.["p:spTree"];
|
|
841
865
|
const items = [];
|
|
842
866
|
const shapes = nativeContext.shapes;
|
|
843
|
-
|
|
867
|
+
// opf-pptx#221: an exported image placeholder group (identity transform) is not a reflow risk.
|
|
868
|
+
if (asArray(tree?.['p:grpSp']).some(group => !isImagePlaceholderGroup(group))) options.onDiagnostic?.({code:'grouped-text-reflow',path:`slides.${slideIndex}`,message:'Grouped native text and pictures are retained, but group transforms and unsupported group members are not reconstructed; review the reflowed OPF.'});
|
|
844
869
|
const paragraphs = nativeContext.paragraphs;
|
|
845
870
|
// FF-45: OMML equations (a14:m math zones) have no OPF model. Their fallback text is imported as plain text; the equation
|
|
846
871
|
// layout (fractions, radicals, limits, matrices) is lost, and that loss is diagnosed per shape instead of passing silently.
|
|
@@ -894,11 +919,17 @@ function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensi
|
|
|
894
919
|
for (const item of code.items) items.push({kind:'code',bounds:shapeBounds(item.shape['p:spPr']?.['a:xfrm']),payload:item.payload,sources:sourcesOf([item.shape])});
|
|
895
920
|
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])});
|
|
896
921
|
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])});
|
|
922
|
+
// opf-pptx#221: a tagged unavailable-image placeholder is its image block again; its panel and label shapes are consumed.
|
|
923
|
+
const placeholders = importImagePlaceholders({shapes, relationships, entries, slideIndex, valid: payload => isValidFormat({slides: [{blocks: [payload]}]}), report: diagnostic => options.onDiagnostic?.(diagnostic)});
|
|
924
|
+
for (const item of placeholders.items) {
|
|
925
|
+
for (const member of item.members) roles.set(`sp:${member}`, 'image');
|
|
926
|
+
items.push({kind: 'image', bounds: shapeBounds(shapes[item.panel]['p:spPr']?.['a:xfrm']), payload: item.payload, sources: item.members.map(member => `sp:${member}`)});
|
|
927
|
+
}
|
|
897
928
|
for (const [index,shape] of shapes.entries()) {
|
|
898
929
|
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'
|
|
899
930
|
: 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;
|
|
900
931
|
if (role) roles.set(shapeKeys.get(shape), role);
|
|
901
|
-
if (furniture.text.has(index) || nativeContext.imageShapes?.has(index) || nativeContext.watermarkShapes?.has(index) || nativeContext.logoPlaceholderShapes?.has(index)) continue;
|
|
932
|
+
if (furniture.text.has(index) || nativeContext.imageShapes?.has(index) || nativeContext.watermarkShapes?.has(index) || nativeContext.logoPlaceholderShapes?.has(index) || placeholders.consumed.has(index)) continue;
|
|
902
933
|
if (annotations.consumed.has(shape)) { roles.set(shapeKeys.get(shape), 'annotation'); continue; }
|
|
903
934
|
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;
|
|
904
935
|
const ordinaryBody = Object.hasOwn(shape, 'p:txBody') && !shape['p:nvSpPr']?.['p:nvPr']?.['p:custDataLst']?.['p:tags'];
|
|
@@ -1000,7 +1031,7 @@ function importGraphicFrame(entries, frame, slidePath, relationships, importedTa
|
|
|
1000
1031
|
};
|
|
1001
1032
|
if (table) {
|
|
1002
1033
|
const stored = importedTable && record('table');
|
|
1003
|
-
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;
|
|
1034
|
+
const restoredTable = stored ? restore('table', importedTable, () => restoreTableData(importedTable, stored, {evidence: tableCellEvidence(table, relationships), olderEvidence: [tableCellEvidence(table, relationships, {form: 'raw'}), tableCellEvidence(table, relationships, {form: 'saved'})], environment: tableEnvironmentEvidence(entries), datasets: data.datasets, report: diagnostic => data.report?.('table', diagnostic)})) : importedTable;
|
|
1004
1035
|
// FA-27: the frame's descr (or PowerPoint's decorative marker) is the table's text alternative.
|
|
1005
1036
|
const alt = restoredTable && typeof restoredTable === 'object' ? readFrameAlt(frame["p:nvGraphicFramePr"]?.["p:cNvPr"]) : undefined;
|
|
1006
1037
|
return {
|
|
@@ -1220,10 +1251,13 @@ function joinRich(a, b) {
|
|
|
1220
1251
|
}
|
|
1221
1252
|
return joined;
|
|
1222
1253
|
}
|
|
1223
|
-
// `breaks` (
|
|
1254
|
+
// `breaks` (slideListRecord) holds the whitespace of the hard breaks the export recorded, by list path and line number: the
|
|
1224
1255
|
// continuation line after a break joins with it, so a newline inside an item (or a description) returns. A blank line is a
|
|
1225
1256
|
// native shape with no text: it is never a description. Without a record the lines join without a separator, as before.
|
|
1226
|
-
|
|
1257
|
+
// `runs` (opf-pptx#212) holds the look export wrote on every run of a list ({text, description?}): a run value still equal to
|
|
1258
|
+
// it is inherited formatting, not authored run style, and is removed (as for a tagged heading), so an untouched round trip
|
|
1259
|
+
// adds no explicit sizes, families or colours; a value edited in PowerPoint differs and is kept. Without it runs are as read.
|
|
1260
|
+
function mergeOpfListShapes(items, breaks = null, report = () => {}, runs = null) {
|
|
1227
1261
|
const lists = new Map();
|
|
1228
1262
|
for (const item of items) {
|
|
1229
1263
|
const match = /^OPF list (.+) line (\d+)$/.exec(item.name ?? '');
|
|
@@ -1236,6 +1270,26 @@ function mergeOpfListShapes(items, breaks = null, report = () => {}) {
|
|
|
1236
1270
|
for (const [path, entries] of lists) {
|
|
1237
1271
|
entries.sort((a, b) => a.line - b.line);
|
|
1238
1272
|
if (!entries[0].item.paragraphs[0].bullet) continue;
|
|
1273
|
+
const look = runs?.get(path), textLook = cleanBase(look?.text), descriptionLook = cleanBase(look?.description);
|
|
1274
|
+
// The line's runs without the inherited look, or the paragraph's own richText when there is no record for this kind of line.
|
|
1275
|
+
// Each run is cleaned on its own and none merge, so the runs stay the slots the run colour record counts (src/run-colors.js);
|
|
1276
|
+
// they stay run objects until the lines are joined (as before), then `plain` gives the authored form back.
|
|
1277
|
+
const own = (paragraph, base) => {
|
|
1278
|
+
if (!base) return paragraph.richText;
|
|
1279
|
+
return richRuns(paragraph.richText ?? paragraph.text).flatMap(run => {
|
|
1280
|
+
const value = headingValue([run], base);
|
|
1281
|
+
const parts = value === '' ? [] : typeof value === 'string' ? [{text: value}] : value.map(part => typeof part === 'string' ? {text: part} : part);
|
|
1282
|
+
// A removed colour stays known until the run colour references are restored (restoreRunColors): an authored `text`
|
|
1283
|
+
// role equals the inherited colour natively.
|
|
1284
|
+
if (typeof run === 'object' && typeof run.color === 'string' && parts.length === 1 && parts[0].color === undefined) parts[0]._opfInherited = run.color;
|
|
1285
|
+
return parts;
|
|
1286
|
+
});
|
|
1287
|
+
};
|
|
1288
|
+
const plain = value => {
|
|
1289
|
+
if (!Array.isArray(value)) return value;
|
|
1290
|
+
const runs = value.map(run => typeof run === 'object' && Object.keys(run).length === 1 ? run.text : run);
|
|
1291
|
+
return runs.length === 0 ? '' : runs.length === 1 && typeof runs[0] === 'string' ? runs[0] : runs;
|
|
1292
|
+
};
|
|
1239
1293
|
const paragraphs = [];
|
|
1240
1294
|
const recorded = breaks?.get(path), applied = new Set();
|
|
1241
1295
|
let bounds, size = 0;
|
|
@@ -1243,16 +1297,16 @@ function mergeOpfListShapes(items, breaks = null, report = () => {}) {
|
|
|
1243
1297
|
const [paragraph] = item.paragraphs, current = paragraphs.at(-1);
|
|
1244
1298
|
const gap = recorded?.get(line);
|
|
1245
1299
|
const join = (a, b) => gap === undefined ? joinRich(a, b) : joinWrappedText([a, b], [gap]);
|
|
1246
|
-
if (paragraph.bullet) { paragraphs.push({...paragraph}); size = paragraph.maxFontSize; }
|
|
1300
|
+
if (paragraph.bullet) { paragraphs.push({...paragraph, richText: own(paragraph, textLook)}); size = paragraph.maxFontSize; }
|
|
1247
1301
|
else if (paragraph.text === '') { /* a blank line of the entry: no text, no description */ }
|
|
1248
1302
|
else if (paragraph.maxFontSize < size - 0.01) {
|
|
1249
|
-
const text = paragraph
|
|
1303
|
+
const text = own(paragraph, descriptionLook) ?? paragraph.text;
|
|
1250
1304
|
if (current.description === undefined) current.description = text;
|
|
1251
1305
|
else { current.description = join(current.description, text); applied.add(line); }
|
|
1252
1306
|
} else {
|
|
1253
|
-
const before = current.text;
|
|
1307
|
+
const before = current.text, rich = own(paragraph, textLook);
|
|
1254
1308
|
current.text += (gap === undefined ? '' : Array.isArray(gap) ? gap[0] : gap) + paragraph.text;
|
|
1255
|
-
if (current.richText !== undefined ||
|
|
1309
|
+
if (current.richText !== undefined || rich !== undefined) current.richText = join(current.richText ?? before, rich ?? paragraph.text);
|
|
1256
1310
|
applied.add(line);
|
|
1257
1311
|
}
|
|
1258
1312
|
const b = item.visualBounds ?? item.bounds;
|
|
@@ -1263,6 +1317,8 @@ function mergeOpfListShapes(items, breaks = null, report = () => {}) {
|
|
|
1263
1317
|
consumed.add(item);
|
|
1264
1318
|
}
|
|
1265
1319
|
if (recorded && [...recorded.keys()].some(line => !applied.has(line))) report({code: 'list-line-break-changed', message: `A hard line break recorded at export inside the list ${path} no longer has the continuation line it belonged to (a line was edited away, or became an entry), so that break was not restored; the list keeps the lines it has.`});
|
|
1320
|
+
if (textLook) for (const paragraph of paragraphs) paragraph.richText = plain(paragraph.richText);
|
|
1321
|
+
if (descriptionLook) for (const paragraph of paragraphs) if (paragraph.description !== undefined) paragraph.description = plain(paragraph.description);
|
|
1266
1322
|
const first = entries[0].item;
|
|
1267
1323
|
merged.set(first, {...first, paragraphs, text: paragraphs.map(paragraph => paragraph.text).join('\n'), bounds, visualBounds: undefined, sources: entries.flatMap(({item}) => item.sources ?? [])});
|
|
1268
1324
|
}
|
|
@@ -1948,6 +2004,7 @@ async function addSlide(pptx, presentation, authoredSlide, slideIndex, context,
|
|
|
1948
2004
|
const geometry = composeSlide(opfSlide, composeOptions);
|
|
1949
2005
|
// The default footer band (date left, text center, number right): where a footer added natively lands (RR-11).
|
|
1950
2006
|
if (slideIndex === 0) context.defaultFooterOptions = composeOptions;
|
|
2007
|
+
slideContext.zoneBand = zoneBands(composeOptions);
|
|
1951
2008
|
slide.background = { color: slideContext.colors.background };
|
|
1952
2009
|
// An image background is core's geometry.backgroundImage (slide, deck, then theme background); every other kind is the
|
|
1953
2010
|
// effective design or theme background definition.
|
|
@@ -2008,6 +2065,8 @@ async function addSlide(pptx, presentation, authoredSlide, slideIndex, context,
|
|
|
2008
2065
|
});
|
|
2009
2066
|
} else if ((item.field === "items" || item.field === "bullets") && item.text?.listEntries) {
|
|
2010
2067
|
await addMeasuredList(slide,item.text,itemContext,item.path,item.bulletImage,presentation,slideIndex,options,item.payload?.numbering!==undefined);
|
|
2068
|
+
// opf-pptx#212: the look every run of the list is written with where its source states none (full provenance only).
|
|
2069
|
+
if(item.textStyle)recordListRuns(context.documentProvenance,slideIndex,item.path,listRunLook(item.text,item.textStyle,itemContext));
|
|
2011
2070
|
} else if (["text", "title", "subtitle", "tag"].includes(item.field) && item.text?.richLines) {
|
|
2012
2071
|
// FA-10: a TextRun[] heading draws and exports like rich body text; its lines are named and tagged as a heading (the body's are not).
|
|
2013
2072
|
const heading = item.field !== 'text';
|
|
@@ -2142,6 +2201,14 @@ function addTextPayload(slide, value, region, context) {
|
|
|
2142
2201
|
slide.addText(stringifyText(value), textBoxOptions(region, context, 18));
|
|
2143
2202
|
}
|
|
2144
2203
|
|
|
2204
|
+
// opf-pptx#212: the look (richBase) the runs of a measured list are written with where the source states none: {text,
|
|
2205
|
+
// description?}, or undefined when its entries do not share one (import then reads every run value as observed).
|
|
2206
|
+
function listRunLook(fit,style,context){
|
|
2207
|
+
const looks=kind=>new Set(fit.listEntries.filter(entry=>entry[kind]).map(entry=>JSON.stringify(richBase(entry[kind],style,kind==='text'?context.colors.text:context.colors.mutedText))));
|
|
2208
|
+
const text=looks('text'),description=looks('description');
|
|
2209
|
+
if(text.size!==1||description.size>1)return undefined;
|
|
2210
|
+
return {text:JSON.parse([...text][0]),...(description.size?{description:JSON.parse([...description][0])}:{})};
|
|
2211
|
+
}
|
|
2145
2212
|
function richLineRuns(line,color,native,context) {
|
|
2146
2213
|
const fallback=color.replace(/^#/,'');
|
|
2147
2214
|
return line.fragments.map(fragment=>{
|
|
@@ -2245,9 +2312,10 @@ function addListPayload(slide, items, region, context) {
|
|
|
2245
2312
|
}
|
|
2246
2313
|
|
|
2247
2314
|
// A content picture outside an image block (a timeline event image): one native picture contained in its region.
|
|
2248
|
-
|
|
2315
|
+
// Generated furniture is never grouped (src/master-furniture.js), so a furniture part passes {group: false}.
|
|
2316
|
+
async function addImagePayload(slide, presentation, asset, region, path, context, options, {group = true} = {}) {
|
|
2249
2317
|
const name = await addImagePicture(slide, presentation, asset, region, path, context, options, {fit: 'contain'});
|
|
2250
|
-
if (!name) addImagePlaceholder(slide, presentation, asset, region, path, context, options);
|
|
2318
|
+
if (!name) addImagePlaceholder(slide, presentation, asset, region, path, context, options, {group});
|
|
2251
2319
|
return name;
|
|
2252
2320
|
}
|
|
2253
2321
|
|
|
@@ -2327,8 +2395,13 @@ async function addImageItem(slide, presentation, item, slideIndex, context, opti
|
|
|
2327
2395
|
fit: image.fit, focus: image.focus, effects: treated ? effects : null, shrink: !treated && !image.placement, treatment: tagged ? treatment : undefined
|
|
2328
2396
|
});
|
|
2329
2397
|
if (!name) {
|
|
2330
|
-
addImagePlaceholder(slide, presentation, item.value, box, item.path, context, options);
|
|
2331
|
-
|
|
2398
|
+
const panel = addImagePlaceholder(slide, presentation, item.value, box, item.path, context, options);
|
|
2399
|
+
// opf-pptx#221: in full mode the panel records the authored image and the block's own fields, so import restores the block.
|
|
2400
|
+
if (context.provenanceMode === 'full') {
|
|
2401
|
+
const fields = Object.fromEntries(IMAGE_TREATMENT_KEYS.filter(key => block[key] !== undefined).map(key => [key, structuredClone(block[key])]));
|
|
2402
|
+
context.imagePlaceholderTags.set(panel, {v: 1, slide: `slides.${slideIndex}`, path: item.path, image: structuredClone(item.value), ...(Object.keys(fields).length ? {treatment: fields} : {})});
|
|
2403
|
+
}
|
|
2404
|
+
return panel;
|
|
2332
2405
|
}
|
|
2333
2406
|
if (image.overlay) addImageOverlay(slide, image.overlay, structuredClone(block.overlay), imageOverlayName(name), name, { ...context, slidePath: `slides.${slideIndex}` }, effects);
|
|
2334
2407
|
return name;
|
|
@@ -2809,10 +2882,19 @@ function defaultFooterParts(composeOptions) {
|
|
|
2809
2882
|
}
|
|
2810
2883
|
}
|
|
2811
2884
|
|
|
2885
|
+
// The band core gives a lone text part in a zone (x and width in pixels): where a footer added natively sits.
|
|
2886
|
+
function zoneBands(composeOptions) {
|
|
2887
|
+
let bands;
|
|
2888
|
+
return zone => (bands ??= new Map((defaultFooterParts(composeOptions) ?? []).map(part => [part.zone, {x: part.box.x, width: part.box.width}]))).get(zone);
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2812
2891
|
// The slide and deck as given to toPptx (variables not yet resolved), for the furniture manifest's authored header and footer text.
|
|
2813
2892
|
const authoredFurniture = (context, slideIndex) => ({presentation: context.authored, slide: context.authored?.slides?.[slideIndex]});
|
|
2814
2893
|
|
|
2815
2894
|
async function addFurniture(slide,presentation,source,layout,context,options,slideIndex) {
|
|
2895
|
+
// RR-72: every slide is listed, a slide without furniture too (it may need a layout that hides the master's).
|
|
2896
|
+
const drawn = [];
|
|
2897
|
+
context.furnitureParts[slideIndex] = {path: `ppt/slides/slide${slideIndex + 1}.xml`, parts: drawn};
|
|
2816
2898
|
if(!layout){
|
|
2817
2899
|
if(['header','footer'].some(kind=>(source.design?.[kind]??presentation.design?.[kind])))throw new OPFPptxError('missing-furniture-layout','Header/footer export requires coordinated core furniture geometry.',{path:`slides.${slideIndex}.design`});
|
|
2818
2900
|
const manifest = furnitureManifest(presentation, source, {parts: []}, slideIndex, undefined, undefined, authoredFurniture(context, slideIndex));
|
|
@@ -2823,12 +2905,18 @@ async function addFurniture(slide,presentation,source,layout,context,options,sli
|
|
|
2823
2905
|
// RR-11: the first footer text, date and slide number that fit one line are native placeholders (src/native-furniture.js).
|
|
2824
2906
|
const natives = nativeFurnitureParts(layout);
|
|
2825
2907
|
for(const [index,part]of layout.parts.entries()){
|
|
2908
|
+
const entry = {index, kind: part.kind, zone: part.zone, field: part.field, type: part.type, path: part.path, native: natives.has(index), names: []};
|
|
2909
|
+
drawn.push(entry);
|
|
2826
2910
|
if(part.type==='image'){
|
|
2827
2911
|
const region={x:part.box.x/96,y:part.box.y/96,w:part.box.width/96,h:part.box.height/96};
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
if (objectName
|
|
2831
|
-
|
|
2912
|
+
// A logo reference draws the organization's asset: a source that cannot be drawn is reported where the asset is written.
|
|
2913
|
+
const objectName = await addImagePayload(slide,presentation,part.image,region,part.reference!==undefined?part.sourcePath:part.path,context,options,{group:false});
|
|
2914
|
+
if (objectName) {
|
|
2915
|
+
entry.names.push(objectName);
|
|
2916
|
+
context.furnitureTags.set(objectName, {v:1, role:'image', group:String(slideIndex), part:index});
|
|
2917
|
+
// RR-71: a part drawn from a logo reference (var:organization.logo.icon) records its picture, so import can tell it is unchanged.
|
|
2918
|
+
if (part.reference !== undefined) context.furnitureImages.set(`ppt/slides/slide${slideIndex + 1}.xml|${part.kind}.${part.zone}`, objectName);
|
|
2919
|
+
}
|
|
2832
2920
|
}else{
|
|
2833
2921
|
if(!part.fit?.sourceLines)throw new OPFPptxError('missing-furniture-layout','Repeated text requires accepted source lines from core.',{path:part.path});
|
|
2834
2922
|
// Slide numbers ({{slide.number}}) and current dates become native PowerPoint fields; {{deck.slideCount}},
|
|
@@ -2848,8 +2936,15 @@ async function addFurniture(slide,presentation,source,layout,context,options,sli
|
|
|
2848
2936
|
if(marker) staticDates.set(index,marker);
|
|
2849
2937
|
}
|
|
2850
2938
|
}
|
|
2851
|
-
addMeasuredPayloadText(slide,part.text,part.box,context,options,{path:part.path,fit:part.fit,textStyle:part.style,align:part.alignment,diagnosticsHandled:true,color:context.mutedColor,keepEmpty:true,objectName:`OPF furniture ${slideIndex} part ${index}`,furniture:{group:String(slideIndex),part:
|
|
2852
|
-
|
|
2939
|
+
addMeasuredPayloadText(slide,part.text,part.box,context,options,{path:part.path,fit:part.fit,textStyle:part.style,align:part.alignment,diagnosticsHandled:true,color:context.mutedColor,keepEmpty:true,objectName:`OPF furniture ${slideIndex} part ${index}`,furniture:{group:String(slideIndex),part:index},liveFields,links:part.links});
|
|
2940
|
+
entry.names.push(...part.fit.lines.map((line, number) => `OPF furniture ${slideIndex} part ${index} line ${number}`));
|
|
2941
|
+
if(natives.has(index)){
|
|
2942
|
+
const name=`OPF furniture ${slideIndex} part ${index} line 0`;
|
|
2943
|
+
context.nativeFurniture.set(name,natives.get(index));
|
|
2944
|
+
// RR-71: a part in a row with other parts is as wide as its text; the master's placeholder spans the zone.
|
|
2945
|
+
const band=context.zoneBand?.(part.zone);
|
|
2946
|
+
if(band&&part.box.width<band.width-.5)context.nativeFurnitureBands.set(name,band);
|
|
2947
|
+
}
|
|
2853
2948
|
}
|
|
2854
2949
|
}
|
|
2855
2950
|
const manifest = furnitureManifest(presentation, source, layout, slideIndex, staticDates, natives, authoredFurniture(context, slideIndex));
|
|
@@ -3087,9 +3182,10 @@ function placeholderAsset(asset, presentation) {
|
|
|
3087
3182
|
// minimum) in the readable text colour for the panel, or a cross when the label
|
|
3088
3183
|
// cannot fit even at that minimum. The panel carries the accessible name the
|
|
3089
3184
|
// preview gives its group, "Image unavailable: <description>".
|
|
3090
|
-
//
|
|
3091
|
-
//
|
|
3092
|
-
|
|
3185
|
+
// The panel's other shapes are named after it (OPF image placeholder N text line i / icon i) and the panel's name is returned,
|
|
3186
|
+
// so a logo placeholder can be tagged and consumed on import. opf-pptx#221: unless it is a logo's ({logo: true}) or furniture's
|
|
3187
|
+
// ({group: false}) placeholder, packaging writes the panel and those shapes as one native group carrying the accessible name.
|
|
3188
|
+
function addImagePlaceholder(slide, presentation, asset, region, path, context, options, {logo = false, group = !logo} = {}) {
|
|
3093
3189
|
const layered = placeholderAsset(asset, presentation);
|
|
3094
3190
|
const description = String(layered.alt ?? layered.title ?? "Image");
|
|
3095
3191
|
const label = `Image unavailable\n${description}`;
|
|
@@ -3110,6 +3206,8 @@ function addImagePlaceholder(slide, presentation, asset, region, path, context,
|
|
|
3110
3206
|
const fit = fitText(label, inner, 20 * scale, minimum, measurer);
|
|
3111
3207
|
const objectName = `OPF image placeholder ${context.imagePlaceholders.size + 1}`;
|
|
3112
3208
|
context.imagePlaceholders.set(objectName, `Image unavailable: ${description}`);
|
|
3209
|
+
if (group) context.imagePlaceholderGroups.add(objectName);
|
|
3210
|
+
const named = logo || group;
|
|
3113
3211
|
slide.addShape("rect", {
|
|
3114
3212
|
x: region.x, y: region.y, w: region.w, h: region.h,
|
|
3115
3213
|
fill: { color: surface },
|
|
@@ -3123,18 +3221,18 @@ function addImagePlaceholder(slide, presentation, asset, region, path, context,
|
|
|
3123
3221
|
const centered = { ...inner, y: inner.y + offset };
|
|
3124
3222
|
addMeasuredPayloadText(slide, label, centered, context, options, {
|
|
3125
3223
|
path, fit: fitText(label, centered, 20 * scale, minimum, measurer), textStyle: style,
|
|
3126
|
-
diagnosticsHandled: true, align: "center", color: textColor, ...(
|
|
3224
|
+
diagnosticsHandled: true, align: "center", color: textColor, ...(named ? {objectName: `${objectName} text`} : {})
|
|
3127
3225
|
});
|
|
3128
|
-
return
|
|
3226
|
+
return objectName;
|
|
3129
3227
|
}
|
|
3130
3228
|
// A status indicator, not shortened authored content: the full description stays
|
|
3131
3229
|
// in the panel's accessible name.
|
|
3132
3230
|
const size = Math.max(0, Math.min(inner.width, inner.height, 24 * scale));
|
|
3133
|
-
if (!size) return
|
|
3231
|
+
if (!size) return objectName;
|
|
3134
3232
|
const icon = { x: inner.x + (inner.width - size) / 2, y: inner.y + (inner.height - size) / 2 };
|
|
3135
3233
|
const line = { color: textColor, width: Math.min(2 * scale, size / 8) * .75 };
|
|
3136
|
-
for (const flipV of [false, true]) slide.addShape("line", { x: icon.x / 96, y: icon.y / 96, w: size / 96, h: size / 96, line, flipV, ...(
|
|
3137
|
-
return
|
|
3234
|
+
for (const flipV of [false, true]) slide.addShape("line", { x: icon.x / 96, y: icon.y / 96, w: size / 96, h: size / 96, line, flipV, ...(named ? {objectName: `${objectName} icon ${flipV ? 2 : 1}`} : {}) });
|
|
3235
|
+
return objectName;
|
|
3138
3236
|
}
|
|
3139
3237
|
|
|
3140
3238
|
// A placeholder names what is missing in plain words. It never dumps the source value, so no data or URL lands in slide text.
|
|
@@ -3555,7 +3653,7 @@ async function resolveImage(asset, presentation, options, path, outcome = {}) {
|
|
|
3555
3653
|
if (raster) return dataUriMediaType(resolved.data) === raster.mediaType ? resolved : {...resolved, data: `data:${raster.mediaType};base64,${bytesToBase64(bytes)}`};
|
|
3556
3654
|
const svg = svgDataUriBytes(resolved.data);
|
|
3557
3655
|
if (svg) return resolveSvgImage(svg, options, path, outcome);
|
|
3558
|
-
const message = `The image is not a readable PNG, JPEG, GIF or WebP (${dataUriMediaType(resolved.data) ?? "unknown type"}); supply a raster through imageResolver (for example opf-render
|
|
3656
|
+
const message = `The image is not a readable PNG, JPEG, GIF or WebP (${dataUriMediaType(resolved.data) ?? "unknown type"}); supply a raster through imageResolver (for example opf-render toPng).`;
|
|
3559
3657
|
if (options.strictAssets) throw new OPFPptxError("unsupported-image-dimensions", `Image fitting requires readable PNG, JPEG, GIF or WebP dimensions. ${message}`, { path });
|
|
3560
3658
|
options.onDiagnostic?.({ code: "unresolved-asset", path, reason: "unsupported-format", message });
|
|
3561
3659
|
outcome.reported = true;
|
|
@@ -3799,11 +3897,13 @@ async function normalizePptxZip(raw, context) {
|
|
|
3799
3897
|
attachQuoteTags(entries,context.quoteTags);
|
|
3800
3898
|
attachAnnotationTags(entries,context);
|
|
3801
3899
|
attachFurnitureFields(entries,context.furnitureFields);
|
|
3802
|
-
attachFurnitureTags(entries,context.furnitureTags,context.furnitureManifests
|
|
3900
|
+
attachFurnitureTags(entries,context.furnitureTags,context.furnitureManifests);
|
|
3803
3901
|
// RR-11: the recorded footer shapes become PowerPoint's date, footer and slide-number placeholders.
|
|
3804
|
-
context.nativePlaceholders = attachNativePlaceholders(entries,context.nativeFurniture);
|
|
3902
|
+
context.nativePlaceholders = attachNativePlaceholders(entries,context.nativeFurniture,context.nativeFurnitureBands);
|
|
3805
3903
|
// The "Image unavailable" panel of an unresolved logo: identity only, so import does not read it as content.
|
|
3806
3904
|
attachTextTags(entries,context.logoPlaceholderTags,LOGO_TAG,'opfLogoPlaceholder','logo placeholder');
|
|
3905
|
+
// opf-pptx#221: the image block of an unavailable-image placeholder, on its panel (full provenance).
|
|
3906
|
+
attachTextTags(entries,context.imagePlaceholderTags,IMAGE_PLACEHOLDER_TAG,'opfImagePlaceholder','image placeholder');
|
|
3807
3907
|
for(const [part,bytes]of Object.entries(entries)){
|
|
3808
3908
|
if(!/^ppt\/slides\/slide\d+\.xml$/.test(part))continue;
|
|
3809
3909
|
const relationships=parseRelationships(entries,part);
|
|
@@ -3836,6 +3936,12 @@ async function normalizePptxZip(raw, context) {
|
|
|
3836
3936
|
}
|
|
3837
3937
|
}
|
|
3838
3938
|
}
|
|
3939
|
+
// opf-pptx#175: after every chart rewrite, each classic series in its CT_*Ser sequence (src/chart-order.js).
|
|
3940
|
+
for(const part of Object.keys(entries)){
|
|
3941
|
+
if(!/^ppt\/charts\/chart\d+\.xml$/.test(part))continue;
|
|
3942
|
+
const xml=decodeText(entries[part]),ordered=orderChartSeries(xml);
|
|
3943
|
+
if(ordered!==xml)entries[part]=encodeText(ordered);
|
|
3944
|
+
}
|
|
3839
3945
|
applyChartFonts(entries,context.chartFonts,parseRelationships);
|
|
3840
3946
|
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); }
|
|
3841
3947
|
// Chartex charts: the native cx:chartSpace part, its style parts and the mc:AlternateContent frame (the classic chart above is the fallback).
|
|
@@ -3905,6 +4011,8 @@ async function normalizePptxZip(raw, context) {
|
|
|
3905
4011
|
});
|
|
3906
4012
|
// A picture repeated across slides embeds once (after fitting, which needs each slide's own relationship).
|
|
3907
4013
|
dedupeMedia(entries, imageMetadata, resolveRelationshipTarget);
|
|
4014
|
+
// The media keys of the logo-reference pictures, now that the package holds its final media (src/furniture-provenance.js).
|
|
4015
|
+
stampFurnitureImageKeys(entries, context.furnitureManifests, context.furnitureImages, parseRelationships);
|
|
3908
4016
|
// Charts and notes follow the language and fonts of the slide they belong to.
|
|
3909
4017
|
context.partSlides = new Map();
|
|
3910
4018
|
for (const part of Object.keys(entries)) {
|
|
@@ -3951,6 +4059,12 @@ async function normalizePptxZip(raw, context) {
|
|
|
3951
4059
|
}
|
|
3952
4060
|
reportPerSlideNotesScriptFonts(context.scriptFonts, slideThemes);
|
|
3953
4061
|
}
|
|
4062
|
+
// RR-72: furniture drawn the same on several slides moves to the slide master or a layout (src/master-furniture.js).
|
|
4063
|
+
try {
|
|
4064
|
+
liftMasterFurniture(output, context.furnitureParts, diagnostic => context.reportDiagnostic?.(diagnostic));
|
|
4065
|
+
} catch (error) {
|
|
4066
|
+
throw new OPFPptxError('packaging-failed', 'Header and footer furniture could not be written to the slide master and layouts.', {cause: errorMessage(error)});
|
|
4067
|
+
}
|
|
3954
4068
|
finalizeFontsUsed(output);
|
|
3955
4069
|
// Document references record evidence from the final normalized parts.
|
|
3956
4070
|
if (context.documentProvenance) {
|
|
@@ -4172,12 +4286,18 @@ function normalizePartBytes(path, bytes, context, renameMaps, entries, imageMeta
|
|
|
4172
4286
|
});
|
|
4173
4287
|
// PptxGenJS gives shapes no alternative text. An unavailable image's panel
|
|
4174
4288
|
// carries the accessible name the preview gives its group.
|
|
4289
|
+
const escapes={'&':'&','<':'<','>':'>','"':'"',"'":''','\r':' ','\n':' ','\t':'	'};
|
|
4290
|
+
const describe=description=>description.replace(/[&<>"'\r\n\t]/g,char=>escapes[char]);
|
|
4175
4291
|
xml=xml.replace(/<p:cNvPr id="(\d+)" name="(OPF image placeholder \d+)"(\/?)>/g,(node,id,name,close)=>{
|
|
4176
4292
|
const description=context.imagePlaceholders.get(name);
|
|
4177
4293
|
if(description===undefined)return node;
|
|
4178
|
-
|
|
4179
|
-
|
|
4294
|
+
// opf-pptx#221: a grouped placeholder's accessible name is on its group (below), as the preview names its <g>. The empty panel
|
|
4295
|
+
// then has no alt text of its own, so it is marked decorative (the adec:decorative extension, "Mark as decorative"), or
|
|
4296
|
+
// PowerPoint's Accessibility Checker flags it. The text lines carry text and are not marked. PptxGenJS writes this cNvPr self-closing.
|
|
4297
|
+
if(context.imagePlaceholderGroups.has(name))return close?writeFrameAlt(node,''):node;
|
|
4298
|
+
return `<p:cNvPr id="${id}" name="${name}" descr="${describe(description)}"${close}>`;
|
|
4180
4299
|
});
|
|
4300
|
+
xml=groupImagePlaceholders(xml,context.imagePlaceholderGroups,name=>describe(context.imagePlaceholders.get(name)));
|
|
4181
4301
|
// PptxGenJS 4.0.1 emitted pPr before each rich run (pptxgenjs-plus writes
|
|
4182
4302
|
// one). OOXML allows one pPr, before all runs. Paragraph options belong
|
|
4183
4303
|
// to the first run.
|
|
@@ -4205,6 +4325,34 @@ function normalizePartBytes(path, bytes, context, renameMaps, entries, imageMeta
|
|
|
4205
4325
|
// their explicit per-shape buFont; only the master bodyStyle is touched.
|
|
4206
4326
|
const VENDOR_MASTER_BULLET_FONT = '<a:buFont typeface="Arial" pitchFamily="34" charset="0"/>';
|
|
4207
4327
|
const THEME_MINOR_BULLET_FONT = '<a:buFont typeface="+mn-lt"/>';
|
|
4328
|
+
// opf-pptx#221: each unavailable-image placeholder (panel, then its label lines or cross strokes, written consecutively) becomes
|
|
4329
|
+
// one p:grpSp (CT_GroupShape: nvGrpSpPr, grpSpPr, members) named "<panel> group" with the accessible name as its descr. Its
|
|
4330
|
+
// transform is the identity (chOff/chExt equal off/ext, the members' union), so every member keeps its box and stays an
|
|
4331
|
+
// editable shape; tags and names stay on the members.
|
|
4332
|
+
function groupImagePlaceholders(xml,panels,description){
|
|
4333
|
+
if(!panels.size)return xml;
|
|
4334
|
+
let next=Math.max(0,...[...xml.matchAll(/<p:cNvPr\b[^>]*\bid="(\d+)"/g)].map(match=>Number(match[1])))+1;
|
|
4335
|
+
const nameOf=shape=>shape.match(/<p:cNvPr\b[^>]*\bname="([^"]*)"/)?.[1]??'';
|
|
4336
|
+
const memberOf=(panel,name)=>name.startsWith(`${panel} `)&&/^(?:text line|icon) \d+$/.test(name.slice(panel.length+1));
|
|
4337
|
+
return xml.replace(/<p:sp>[\s\S]*?<\/p:sp>(?:<p:sp>[\s\S]*?<\/p:sp>)*/g,run=>{
|
|
4338
|
+
const shapes=run.match(/<p:sp>[\s\S]*?<\/p:sp>/g);
|
|
4339
|
+
let out='';
|
|
4340
|
+
for(let index=0;index<shapes.length;index++){
|
|
4341
|
+
const panel=nameOf(shapes[index]);
|
|
4342
|
+
if(!panels.has(panel)){out+=shapes[index];continue;}
|
|
4343
|
+
const members=[shapes[index]];
|
|
4344
|
+
while(index+1<shapes.length&&memberOf(panel,nameOf(shapes[index+1])))members.push(shapes[++index]);
|
|
4345
|
+
const boxes=members.map(shape=>shape.match(/<a:off x="(-?\d+)" y="(-?\d+)"\/><a:ext cx="(\d+)" cy="(\d+)"\/>/)).filter(Boolean).map(match=>match.slice(1).map(Number));
|
|
4346
|
+
if(!boxes.length){out+=members.join('');continue;}
|
|
4347
|
+
const x=Math.min(...boxes.map(box=>box[0])),y=Math.min(...boxes.map(box=>box[1]));
|
|
4348
|
+
const cx=Math.max(...boxes.map(box=>box[0]+box[2]))-x,cy=Math.max(...boxes.map(box=>box[1]+box[3]))-y;
|
|
4349
|
+
out+=`<p:grpSp><p:nvGrpSpPr><p:cNvPr id="${next++}" name="${imagePlaceholderGroupName(panel)}" descr="${description(panel)}"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>`+
|
|
4350
|
+
`<p:grpSpPr><a:xfrm><a:off x="${x}" y="${y}"/><a:ext cx="${cx}" cy="${cy}"/><a:chOff x="${x}" y="${y}"/><a:chExt cx="${cx}" cy="${cy}"/></a:xfrm></p:grpSpPr>${members.join('')}</p:grpSp>`;
|
|
4351
|
+
}
|
|
4352
|
+
return out;
|
|
4353
|
+
});
|
|
4354
|
+
}
|
|
4355
|
+
|
|
4208
4356
|
function themeMasterBulletFonts(xml) {
|
|
4209
4357
|
return xml.replace(/<p:bodyStyle>[\s\S]*?<\/p:bodyStyle>/, bodyStyle =>
|
|
4210
4358
|
bodyStyle.split(VENDOR_MASTER_BULLET_FONT).join(THEME_MINOR_BULLET_FONT));
|
package/dist/logo-provenance.js
CHANGED
|
@@ -3,13 +3,13 @@ import {normalizeCrop, withoutSvgBlip} from './svg-image.js';
|
|
|
3
3
|
import {decodeTextTag, encodeTextTag} from './code-provenance.js';
|
|
4
4
|
import {pictureTransform} from './image-geometry.js';
|
|
5
5
|
|
|
6
|
-
// The
|
|
7
|
-
//
|
|
6
|
+
// The cover and section logo (core's `geometry.logo`: the primary organization's full logo, or the
|
|
7
|
+
// organization a `design.logo` reference names, RR-71) exports as one native
|
|
8
8
|
// picture per slide named `OPF logo`, after the watermark and before content. It
|
|
9
9
|
// is fitted into core's box without cropping, anchored left and vertically
|
|
10
10
|
// centered (the preview's `xMinYMid meet`). The native tag records the exact
|
|
11
11
|
// picture, so an unchanged picture is consumed on import (the logo itself returns
|
|
12
|
-
// from the document
|
|
12
|
+
// from the document record's organization) while an edited one stays an
|
|
13
13
|
// ordinary picture and reports `invalid-logo-provenance`.
|
|
14
14
|
const TAG = 'OPF_LOGO_V1', REL = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags';
|
|
15
15
|
const EMUS_PER_INCH = 914400;
|
|
@@ -19,8 +19,8 @@ const array = value => value === undefined ? [] : Array.isArray(value) ? value :
|
|
|
19
19
|
// Whitespace-only text between elements is not identity: the vendored writer indents, PowerPoint drops it on save.
|
|
20
20
|
const canonical = value => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
|
|
21
21
|
? Object.fromEntries(Object.keys(item).sort().filter(key => !(key === '#text' && typeof item[key] === 'string' && !item[key].trim())).map(key => [key, item[key]])) : item);
|
|
22
|
-
// The OPF paths core reports for a logo source.
|
|
23
|
-
const LOGO_PATH = /^
|
|
22
|
+
// The OPF paths core reports for a logo source: the organization's `logo`, then a shape and a tone below it.
|
|
23
|
+
const LOGO_PATH = /^organization(?:\.\d{1,9})?\.logo(?:\.[A-Za-z]{1,16}){0,2}$/;
|
|
24
24
|
|
|
25
25
|
export const LOGO_TAG = TAG;
|
|
26
26
|
export const logoName = () => 'OPF logo';
|
|
@@ -138,10 +138,10 @@ export function importLogoPlaceholders(shapes, relationships, entries, slideInde
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
|
-
* Consume an unchanged tagged logo picture: it is
|
|
142
|
-
*
|
|
141
|
+
* Consume an unchanged tagged logo picture: it is drawn from the organization's
|
|
142
|
+
* logo, never content. Returns the consumed picture indexes
|
|
143
143
|
* and `fallback`, the picture's own `{path, image}`: the caller uses it for the
|
|
144
|
-
* logo only when the stored document
|
|
144
|
+
* organization's logo only when the stored document did not restore one (an
|
|
145
145
|
* export without provenance, or one that omitted the field), so the logo is never
|
|
146
146
|
* lost. An edited, ambiguous or damaged tagged picture stays an ordinary picture
|
|
147
147
|
* and is reported. readPicture(picture) returns the ordinary imported payload.
|