@openpresentation/opf-pptx 0.16.1 → 0.18.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 +13 -11
- package/dist/code-provenance.js +1 -1
- package/dist/document-provenance.js +37 -40
- package/dist/furniture-fields.js +41 -9
- package/dist/furniture-provenance.js +242 -135
- package/dist/image-fallback-node.js +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +127 -44
- 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 +46 -25
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -24,13 +24,14 @@ import {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, slideListBreaks, DEFAULT_AUTHOR} from './document-provenance.js';
|
|
30
|
+
import {applyDocumentProvenance, attachDocumentProvenance, documentProvenance, recordContentTopology, restoreDocumentProvenance, joinAuthors, splitAuthors, slideListBreaks, 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
|
-
import {attachNativePlaceholders, defaultPlaceholderGeometry, nativeDateText, nativeFurnitureParts, writeNativeMasters} from './native-furniture.js';
|
|
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
37
|
import {placeImages, importImages, imageName, imageOverlayName, backgroundImageName, backgroundOverlayName, IMAGE_TREATMENT_KEYS, BACKGROUND_IMAGE_KEYS} from './image-provenance.js';
|
|
@@ -59,6 +60,7 @@ import {
|
|
|
59
60
|
isTemplate,
|
|
60
61
|
OPFCatalogsOptionError,
|
|
61
62
|
resolveSlideContext as resolveCoreSlideContext,
|
|
63
|
+
resolveSlideVariables,
|
|
62
64
|
resolveVariables,
|
|
63
65
|
validate
|
|
64
66
|
} from "@openpresentation/opf";
|
|
@@ -236,7 +238,9 @@ export async function toPptx(input, options = {}) {
|
|
|
236
238
|
// resolves nowhere instead of composing automatically or drawing with the engine default.
|
|
237
239
|
checkCatalogs('toPptx', options.catalogs);
|
|
238
240
|
options = {...options, strictReferences: options.strictReferences ?? options.strictAssets === true};
|
|
239
|
-
|
|
241
|
+
// The document as given, before the deck-wide variable pass: the furniture manifest records the authored header and footer text.
|
|
242
|
+
const authored = parseInput(input);
|
|
243
|
+
const presentation = resolveTemplateInput(authored, options);
|
|
240
244
|
assertValidBoundary(presentation);
|
|
241
245
|
// `options.fonts` is the fonts handle (the renderer's loadFonts()); the exporter reads its measurement, wrapped so the package names the
|
|
242
246
|
// chosen family (FF-31). Internally `options.textMeasurement` is that wrapped measurement; it is not a public option.
|
|
@@ -275,11 +279,15 @@ export async function toPptx(input, options = {}) {
|
|
|
275
279
|
context.quoteTags = new Map();
|
|
276
280
|
context.quotePhotos = new Map();
|
|
277
281
|
context.furnitureTags = new Map();
|
|
278
|
-
context.
|
|
282
|
+
context.furnitureImages = new Map();
|
|
279
283
|
context.furnitureFields = new Map();
|
|
280
284
|
context.furnitureManifests = new Map();
|
|
281
285
|
context.nativeFurniture = new Map();
|
|
286
|
+
context.nativeFurnitureBands = new Map();
|
|
287
|
+
// RR-72: each slide's furniture parts and the object names of their shapes, for the slide master and layouts.
|
|
288
|
+
context.furnitureParts = [];
|
|
282
289
|
context.hostDate = options.date;
|
|
290
|
+
context.authored = authored;
|
|
283
291
|
context.codeTags = new Map();
|
|
284
292
|
context.captionTags = new Map();
|
|
285
293
|
context.footnoteTags = new Map();
|
|
@@ -404,6 +412,19 @@ export async function fromPptx(input, options = {}) {
|
|
|
404
412
|
// Layouts and the master are read once however many slides inherit a placeholder from them (RR-11).
|
|
405
413
|
const cached = read => { const memo = new Map(); return path => { if (!memo.has(path)) memo.set(path, read(path)); return memo.get(path); }; };
|
|
406
414
|
const cachedRelationships = cached(path => parseRelationships(entries, path)), cachedPart = cached(path => parseOptionalXml(entries, path));
|
|
415
|
+
// RR-72: the furniture a slide shows from its layout and slide master (src/master-furniture.js), read once per part.
|
|
416
|
+
const inheritedParts = new Map();
|
|
417
|
+
const inheritedPart = path => {
|
|
418
|
+
if (!path) return undefined;
|
|
419
|
+
if (!inheritedParts.has(path)) {
|
|
420
|
+
const element = /slideMaster/.test(path) ? 'p:sldMaster' : 'p:sldLayout', root = cachedPart(path)?.[element];
|
|
421
|
+
const tree = root?.['p:cSld']?.['p:spTree'];
|
|
422
|
+
inheritedParts.set(path, root ? {path, root, relationships: cachedRelationships(path), shapes: nativeTextShapes(tree), pictures: nativePictures(tree),
|
|
423
|
+
paragraphs: nativeShapeParagraphs(decodeText(entries[path]), element)} : undefined);
|
|
424
|
+
}
|
|
425
|
+
return inheritedParts.get(path);
|
|
426
|
+
};
|
|
427
|
+
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;
|
|
407
428
|
const furnitureContexts = slidePaths.map((slidePath, index) => {
|
|
408
429
|
const root = parseRequiredXml(entries, slidePath)['p:sld'];
|
|
409
430
|
if (!root) throw new OPFPptxError('invalid-pptx', `PPTX slide is not a PresentationML slide: ${slidePath}.`, {path: slidePath});
|
|
@@ -413,12 +434,30 @@ export async function fromPptx(input, options = {}) {
|
|
|
413
434
|
path: slidePath, slideWidth: Number(presentationRoot['p:sldSz']?.cx), relationshipsOf: cachedRelationships, readPart: cachedPart,
|
|
414
435
|
paragraphs: nativeShapeParagraphs(decodeText(entries[slidePath])),
|
|
415
436
|
readPicture: picture => importPicture(entries, picture, slidePath, relationships,
|
|
416
|
-
diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${index}.design`}))
|
|
437
|
+
diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${index}.design`})),
|
|
438
|
+
inherited: () => {
|
|
439
|
+
const layout = relatedPath(slidePath, 'slideLayout'), master = relatedPath(layout, 'slideMaster');
|
|
440
|
+
return Object.fromEntries([['layout', inheritedPart(layout)], ['master', inheritedPart(master)]].filter(([, part]) => part).map(([on, part]) => [on, {...part,
|
|
441
|
+
readPicture: picture => importPicture(entries, picture, part.path, part.relationships, diagnostic => options.onDiagnostic?.({...diagnostic, path: `slides.${index}.design`}))}]));
|
|
442
|
+
}};
|
|
417
443
|
});
|
|
418
|
-
|
|
444
|
+
// Native sections (PowerPoint's own section list, `Default Section` = none) name each slide's `{{slide.section}}` for the footer text check
|
|
445
|
+
// and, after the slides, are reconciled with the stored value in restoreDocumentProvenance.
|
|
446
|
+
const slideSections = nativeSections(presentationRoot, slideIdsInOrder(presentationRoot, presentationRels, entries, slidePaths));
|
|
447
|
+
const stored = storedMetadata(entries, presentationRoot, presentationRels);
|
|
448
|
+
// A stored zone text with built-in variables is drawn from the document's own metadata, as toPptx resolved it: the stored record
|
|
449
|
+
// (organization, speaker ...) and the observed name, description and author (a default the exporter writes is no value).
|
|
450
|
+
const importedMetadata = {
|
|
451
|
+
...(imported.name && imported.name !== DEFAULT_TITLE ? {name: imported.name} : {}),
|
|
452
|
+
...(imported.description ? {description: imported.description} : {}),
|
|
453
|
+
...(imported.author && joinAuthors(imported.author) !== DEFAULT_AUTHOR ? {author: imported.author} : {}),
|
|
454
|
+
...(stored.organization !== undefined ? {organization: stored.organization} : {}),
|
|
455
|
+
...(stored.speaker !== undefined ? {speaker: stored.speaker} : {}),
|
|
456
|
+
};
|
|
457
|
+
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)} : {})})),
|
|
458
|
+
draw: (template, {index, count, section}) => drawFurnitureText(template, {index, count, section}, importedMetadata)});
|
|
419
459
|
if (Object.keys(furniture.design).length) imported.design = {...imported.design, ...furniture.design};
|
|
420
460
|
if (furniture.organization) imported.organization = furniture.organization;
|
|
421
|
-
if (furniture.speaker) imported.speaker = furniture.speaker;
|
|
422
461
|
|
|
423
462
|
// RR-54: the datasets recorded at export (OPF_DATASETS_V1). Chart and table frames restore their dataset references against
|
|
424
463
|
// them, so the document carries them before document provenance validates the restored document.
|
|
@@ -429,9 +468,6 @@ export async function fromPptx(input, options = {}) {
|
|
|
429
468
|
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]));
|
|
430
469
|
}
|
|
431
470
|
if (datasets) imported.datasets = datasets;
|
|
432
|
-
// Native sections (PowerPoint's own section list, `Default Section` = none)
|
|
433
|
-
// are reconciled with the footer text and the stored value in restoreDocumentProvenance.
|
|
434
|
-
const slideSections = nativeSections(presentationRoot, slideIdsInOrder(presentationRoot, presentationRels, entries, slidePaths));
|
|
435
471
|
// A watermark carried identically by every slide is the deck's design.watermark.
|
|
436
472
|
const carried = imported.slides.map(slide => slide.design?.watermark);
|
|
437
473
|
if (carried.length && carried[0] && carried.every(value => JSON.stringify(value) === JSON.stringify(carried[0]))) {
|
|
@@ -456,7 +492,7 @@ export async function fromPptx(input, options = {}) {
|
|
|
456
492
|
let slideProvenance = slidePaths.map(() => ({structure: "untagged"}));
|
|
457
493
|
let restoredGroups = [];
|
|
458
494
|
try {
|
|
459
|
-
const restored = restoreDocumentProvenance(imported, {entries, presentationRoot, presentationRels,
|
|
495
|
+
const restored = restoreDocumentProvenance(imported, {entries, presentationRoot, presentationRels,
|
|
460
496
|
// Stored layout ids also resolve in the host's registered catalogs (FA-23).
|
|
461
497
|
catalogs: options.catalogs,
|
|
462
498
|
nativeSections: slideSections,
|
|
@@ -516,21 +552,19 @@ function applyRunColors(imported, slideProvenance, restoredGroups, report, optio
|
|
|
516
552
|
restoreRunColors(imported.slides, info, resolve, report);
|
|
517
553
|
}
|
|
518
554
|
|
|
519
|
-
// A consumed logo picture (OPF_LOGO_V1) never becomes content. Its own image restores the logo only when
|
|
520
|
-
//
|
|
521
|
-
//
|
|
555
|
+
// A consumed logo picture (OPF_LOGO_V1) never becomes content. Its own image restores the organization's logo only when the
|
|
556
|
+
// stored document did not (RR-71: logos live on the organization), so a package exported without provenance still keeps its
|
|
557
|
+
// logo: on the primary organization, else on a new one named after the deck. The picture is one drawn asset, so it becomes the
|
|
558
|
+
// organization's single logo for every shape.
|
|
522
559
|
function restoreLogoFallback(imported, contexts) {
|
|
523
560
|
const organizations = asArray(imported.organization);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
if (fallback.path.startsWith('slides.')) { if (slide.design?.logo === undefined) slide.design = {...slide.design, logo: image}; }
|
|
532
|
-
else if (imported.design?.logo === undefined && !organizations.some(item => item?.logo !== undefined)) imported.design = {...imported.design, logo: image};
|
|
533
|
-
});
|
|
561
|
+
if (organizations.some(item => item?.logo !== undefined)) return;
|
|
562
|
+
const image = contexts.map(context => context.logoFallback?.image).find(Boolean);
|
|
563
|
+
if (!image) return;
|
|
564
|
+
const primary = organizations.find(item => item?.role === 'primary') ?? organizations[0];
|
|
565
|
+
if (!primary) { imported.organization = {id: 'brand', name: imported.name ?? 'Brand', logo: image}; return; }
|
|
566
|
+
const index = organizations.indexOf(primary), withLogo = {...primary, logo: image};
|
|
567
|
+
imported.organization = Array.isArray(imported.organization) ? imported.organization.map((item, at) => at === index ? withLogo : item) : withLogo;
|
|
534
568
|
}
|
|
535
569
|
|
|
536
570
|
function readPptxZip(input) {
|
|
@@ -614,6 +648,17 @@ function resolveRelationshipTarget(sourcePartPath, target) {
|
|
|
614
648
|
return parts.join("/");
|
|
615
649
|
}
|
|
616
650
|
|
|
651
|
+
// A header or footer text as the exporter drew it for one slide: deck-wide built-ins resolved from `metadata` (core
|
|
652
|
+
// resolveVariables), then the slide-scoped tokens for the slide's position, count and section. undefined when core refuses.
|
|
653
|
+
function drawFurnitureText(template, {index, count, section}, metadata) {
|
|
654
|
+
try {
|
|
655
|
+
const slide = resolveVariables({...metadata, slides: [{...(section !== undefined ? {section} : {}), text: template}]}).presentation.slides[0];
|
|
656
|
+
return resolveSlideVariables(slide, {slideNumber: index + 1, slideCount: count}).text;
|
|
657
|
+
} catch {
|
|
658
|
+
return undefined;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
617
662
|
function resolveSlidePaths(entries, presentationRoot, relationships) {
|
|
618
663
|
const slideIds = asArray(presentationRoot["p:sldIdLst"]?.["p:sldId"]);
|
|
619
664
|
const paths = [];
|
|
@@ -709,7 +754,6 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
709
754
|
}
|
|
710
755
|
if (imageBackground) slide.design = {background: imageBackground};
|
|
711
756
|
if (Object.keys(furniture.design).length) slide.design = {...slide.design, ...furniture.design};
|
|
712
|
-
if (furniture.section !== undefined) slide.section = furniture.section;
|
|
713
757
|
nativeContext.imageBlocks = images.blocks;
|
|
714
758
|
nativeContext.imagePictures = images.consumed;
|
|
715
759
|
// An overlay of a background that was not recovered stays an ordinary shape.
|
|
@@ -728,7 +772,7 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
728
772
|
diagnostic => options.onDiagnostic?.({...diagnostic, path: watermarkPath}));
|
|
729
773
|
if (watermark.design) slide.design = {...slide.design, ...watermark.design};
|
|
730
774
|
nativeContext.watermarkPictures = watermark.consumed;
|
|
731
|
-
// The generated
|
|
775
|
+
// The generated cover logo picture is not content; the logo returns from the stored organization (or, without one, from the picture).
|
|
732
776
|
const logoPath = `slides.${slideIndex}.design.logo`;
|
|
733
777
|
const logo = importLogo(nativeContext.pictures, relationships, entries, slideIndex,
|
|
734
778
|
picture => importPicture(entries, picture, slidePath, relationships, diagnostic => {
|
|
@@ -1903,10 +1947,15 @@ function configurePresentation(pptx, presentation, context) {
|
|
|
1903
1947
|
};
|
|
1904
1948
|
}
|
|
1905
1949
|
|
|
1906
|
-
async function addSlide(pptx, presentation,
|
|
1950
|
+
async function addSlide(pptx, presentation, authoredSlide, slideIndex, context, options) {
|
|
1907
1951
|
options = {...options, textMeasurement: slideTextMeasurement(presentation, slideIndex, options)};
|
|
1908
1952
|
const slide = pptx.addSlide();
|
|
1909
1953
|
const slideContext = exportSlideContext(presentation, slideIndex, context, options);
|
|
1954
|
+
// FA-31: the slide is composed and drawn with its slide-scoped variables substituted ({{slide.number}}, {{slide.section}},
|
|
1955
|
+
// {{deck.slideCount}} for this position in the exported deck, in body text as fixed text). The authored slide still holds the
|
|
1956
|
+
// tokens: header and footer text is substituted by core's furniture layout (a slide number stays a native field), and the
|
|
1957
|
+
// furniture manifest records the authored text.
|
|
1958
|
+
const opfSlide = slideContext.core.slide;
|
|
1910
1959
|
const { widthInches, heightInches } = slideContext.dimensions;
|
|
1911
1960
|
// A slide with no layout, or one whose layout resolves nowhere, is composed with no layout record (automatic composition),
|
|
1912
1961
|
// as in the preview; core reports the reference as unresolved-reference.
|
|
@@ -1919,6 +1968,7 @@ async function addSlide(pptx, presentation, opfSlide, slideIndex, context, optio
|
|
|
1919
1968
|
const geometry = composeSlide(opfSlide, composeOptions);
|
|
1920
1969
|
// The default footer band (date left, text center, number right): where a footer added natively lands (RR-11).
|
|
1921
1970
|
if (slideIndex === 0) context.defaultFooterOptions = composeOptions;
|
|
1971
|
+
slideContext.zoneBand = zoneBands(composeOptions);
|
|
1922
1972
|
slide.background = { color: slideContext.colors.background };
|
|
1923
1973
|
// An image background is core's geometry.backgroundImage (slide, deck, then theme background); every other kind is the
|
|
1924
1974
|
// effective design or theme background definition.
|
|
@@ -2009,7 +2059,7 @@ async function addSlide(pptx, presentation, opfSlide, slideIndex, context, optio
|
|
|
2009
2059
|
// Core composes furniture above all content and opf-render paints it last, so
|
|
2010
2060
|
// spTree order (PowerPoint's z-order) matches: header/footer parts come after
|
|
2011
2061
|
// every content item, and an overlapping footer stays visible over content.
|
|
2012
|
-
await addFurniture(slide,presentation,
|
|
2062
|
+
await addFurniture(slide,presentation,authoredSlide,geometry.furniture,slideContext,options,slideIndex);
|
|
2013
2063
|
|
|
2014
2064
|
if (opfSlide.notes) {
|
|
2015
2065
|
const notes = String(opfSlide.notes);
|
|
@@ -2772,7 +2822,7 @@ function addMeasuredPayloadText(slide, text, box, context, options, config) {
|
|
|
2772
2822
|
// The default footer band core composes for a deck with no footer of its own: where a footer added natively lands.
|
|
2773
2823
|
function defaultFooterParts(composeOptions) {
|
|
2774
2824
|
try {
|
|
2775
|
-
const slide = {design: {footer: {left: {date: '2026-01-01'}, center: {text: 'Footer'}, right: {
|
|
2825
|
+
const slide = {design: {footer: {left: {date: '2026-01-01'}, center: {text: 'Footer'}, right: {text: '{{slide.number}}'}}}};
|
|
2776
2826
|
// Only the zone boxes and line height are used, and they do not depend on text widths: estimated measurement keeps the host's measurer (and its call count) out of this.
|
|
2777
2827
|
return (composeSlide(slide, {...composeOptions, textMeasurement: undefined}).furniture?.parts ?? []).filter(part => part.kind === 'footer' && part.type === 'text');
|
|
2778
2828
|
} catch {
|
|
@@ -2780,10 +2830,22 @@ function defaultFooterParts(composeOptions) {
|
|
|
2780
2830
|
}
|
|
2781
2831
|
}
|
|
2782
2832
|
|
|
2833
|
+
// The band core gives a lone text part in a zone (x and width in pixels): where a footer added natively sits.
|
|
2834
|
+
function zoneBands(composeOptions) {
|
|
2835
|
+
let bands;
|
|
2836
|
+
return zone => (bands ??= new Map((defaultFooterParts(composeOptions) ?? []).map(part => [part.zone, {x: part.box.x, width: part.box.width}]))).get(zone);
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
// The slide and deck as given to toPptx (variables not yet resolved), for the furniture manifest's authored header and footer text.
|
|
2840
|
+
const authoredFurniture = (context, slideIndex) => ({presentation: context.authored, slide: context.authored?.slides?.[slideIndex]});
|
|
2841
|
+
|
|
2783
2842
|
async function addFurniture(slide,presentation,source,layout,context,options,slideIndex) {
|
|
2843
|
+
// RR-72: every slide is listed, a slide without furniture too (it may need a layout that hides the master's).
|
|
2844
|
+
const drawn = [];
|
|
2845
|
+
context.furnitureParts[slideIndex] = {path: `ppt/slides/slide${slideIndex + 1}.xml`, parts: drawn};
|
|
2784
2846
|
if(!layout){
|
|
2785
2847
|
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`});
|
|
2786
|
-
const manifest = furnitureManifest(presentation, source, {parts: []}, slideIndex);
|
|
2848
|
+
const manifest = furnitureManifest(presentation, source, {parts: []}, slideIndex, undefined, undefined, authoredFurniture(context, slideIndex));
|
|
2787
2849
|
if (manifest) context.furnitureManifests.set(`ppt/slides/slide${slideIndex + 1}.xml`, manifest);
|
|
2788
2850
|
return;
|
|
2789
2851
|
}
|
|
@@ -2791,16 +2853,22 @@ async function addFurniture(slide,presentation,source,layout,context,options,sli
|
|
|
2791
2853
|
// RR-11: the first footer text, date and slide number that fit one line are native placeholders (src/native-furniture.js).
|
|
2792
2854
|
const natives = nativeFurnitureParts(layout);
|
|
2793
2855
|
for(const [index,part]of layout.parts.entries()){
|
|
2856
|
+
const entry = {index, kind: part.kind, zone: part.zone, field: part.field, type: part.type, path: part.path, native: natives.has(index), names: []};
|
|
2857
|
+
drawn.push(entry);
|
|
2794
2858
|
if(part.type==='image'){
|
|
2795
2859
|
const region={x:part.box.x/96,y:part.box.y/96,w:part.box.width/96,h:part.box.height/96};
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
if (objectName
|
|
2799
|
-
|
|
2860
|
+
// A logo reference draws the organization's asset: a source that cannot be drawn is reported where the asset is written.
|
|
2861
|
+
const objectName = await addImagePayload(slide,presentation,part.image,region,part.reference!==undefined?part.sourcePath:part.path,context,options);
|
|
2862
|
+
if (objectName) {
|
|
2863
|
+
entry.names.push(objectName);
|
|
2864
|
+
context.furnitureTags.set(objectName, {v:1, role:'image', group:String(slideIndex), part:index});
|
|
2865
|
+
// RR-71: a part drawn from a logo reference (var:organization.logo.icon) records its picture, so import can tell it is unchanged.
|
|
2866
|
+
if (part.reference !== undefined) context.furnitureImages.set(`ppt/slides/slide${slideIndex + 1}.xml|${part.kind}.${part.zone}`, objectName);
|
|
2867
|
+
}
|
|
2800
2868
|
}else{
|
|
2801
2869
|
if(!part.fit?.sourceLines)throw new OPFPptxError('missing-furniture-layout','Repeated text requires accepted source lines from core.',{path:part.path});
|
|
2802
|
-
// Slide numbers and current dates become native PowerPoint fields; {
|
|
2803
|
-
// and fixed dates stay fixed text. A current date whose pattern has no
|
|
2870
|
+
// Slide numbers ({{slide.number}}) and current dates become native PowerPoint fields; {{deck.slideCount}},
|
|
2871
|
+
// {{slide.section}} and fixed dates stay fixed text. A current date whose pattern has no
|
|
2804
2872
|
// en-US field type is written as its laid-out text.
|
|
2805
2873
|
const fields=[];
|
|
2806
2874
|
for(const field of furniturePartFields(part)){
|
|
@@ -2816,11 +2884,18 @@ async function addFurniture(slide,presentation,source,layout,context,options,sli
|
|
|
2816
2884
|
if(marker) staticDates.set(index,marker);
|
|
2817
2885
|
}
|
|
2818
2886
|
}
|
|
2819
|
-
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:
|
|
2820
|
-
|
|
2887
|
+
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});
|
|
2888
|
+
entry.names.push(...part.fit.lines.map((line, number) => `OPF furniture ${slideIndex} part ${index} line ${number}`));
|
|
2889
|
+
if(natives.has(index)){
|
|
2890
|
+
const name=`OPF furniture ${slideIndex} part ${index} line 0`;
|
|
2891
|
+
context.nativeFurniture.set(name,natives.get(index));
|
|
2892
|
+
// RR-71: a part in a row with other parts is as wide as its text; the master's placeholder spans the zone.
|
|
2893
|
+
const band=context.zoneBand?.(part.zone);
|
|
2894
|
+
if(band&&part.box.width<band.width-.5)context.nativeFurnitureBands.set(name,band);
|
|
2895
|
+
}
|
|
2821
2896
|
}
|
|
2822
2897
|
}
|
|
2823
|
-
const manifest = furnitureManifest(presentation, source, layout, slideIndex, staticDates, natives);
|
|
2898
|
+
const manifest = furnitureManifest(presentation, source, layout, slideIndex, staticDates, natives, authoredFurniture(context, slideIndex));
|
|
2824
2899
|
if (manifest) context.furnitureManifests.set(`ppt/slides/slide${slideIndex + 1}.xml`, manifest);
|
|
2825
2900
|
}
|
|
2826
2901
|
|
|
@@ -3523,7 +3598,7 @@ async function resolveImage(asset, presentation, options, path, outcome = {}) {
|
|
|
3523
3598
|
if (raster) return dataUriMediaType(resolved.data) === raster.mediaType ? resolved : {...resolved, data: `data:${raster.mediaType};base64,${bytesToBase64(bytes)}`};
|
|
3524
3599
|
const svg = svgDataUriBytes(resolved.data);
|
|
3525
3600
|
if (svg) return resolveSvgImage(svg, options, path, outcome);
|
|
3526
|
-
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
|
|
3601
|
+
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).`;
|
|
3527
3602
|
if (options.strictAssets) throw new OPFPptxError("unsupported-image-dimensions", `Image fitting requires readable PNG, JPEG, GIF or WebP dimensions. ${message}`, { path });
|
|
3528
3603
|
options.onDiagnostic?.({ code: "unresolved-asset", path, reason: "unsupported-format", message });
|
|
3529
3604
|
outcome.reported = true;
|
|
@@ -3730,7 +3805,7 @@ function normalizeHex(value, fallback = "000000") {
|
|
|
3730
3805
|
function writeNativeFurnitureMasters(output, context) {
|
|
3731
3806
|
const size = /<p:sldSz\b[^>]*\bcx="(\d+)"[^>]*\bcy="(\d+)"/.exec(decodeText(output['ppt/presentation.xml'][0]));
|
|
3732
3807
|
const defaults = new Map((context.defaultFooterOptions ? defaultFooterParts(context.defaultFooterOptions) ?? [] : []).flatMap(part => {
|
|
3733
|
-
const ph =
|
|
3808
|
+
const ph = nativePlaceholderForPart(part);
|
|
3734
3809
|
const geometry = ph && defaultPlaceholderGeometry(part);
|
|
3735
3810
|
return geometry ? [[ph, geometry]] : [];
|
|
3736
3811
|
}));
|
|
@@ -3767,9 +3842,9 @@ async function normalizePptxZip(raw, context) {
|
|
|
3767
3842
|
attachQuoteTags(entries,context.quoteTags);
|
|
3768
3843
|
attachAnnotationTags(entries,context);
|
|
3769
3844
|
attachFurnitureFields(entries,context.furnitureFields);
|
|
3770
|
-
attachFurnitureTags(entries,context.furnitureTags,context.furnitureManifests
|
|
3845
|
+
attachFurnitureTags(entries,context.furnitureTags,context.furnitureManifests);
|
|
3771
3846
|
// RR-11: the recorded footer shapes become PowerPoint's date, footer and slide-number placeholders.
|
|
3772
|
-
context.nativePlaceholders = attachNativePlaceholders(entries,context.nativeFurniture);
|
|
3847
|
+
context.nativePlaceholders = attachNativePlaceholders(entries,context.nativeFurniture,context.nativeFurnitureBands);
|
|
3773
3848
|
// The "Image unavailable" panel of an unresolved logo: identity only, so import does not read it as content.
|
|
3774
3849
|
attachTextTags(entries,context.logoPlaceholderTags,LOGO_TAG,'opfLogoPlaceholder','logo placeholder');
|
|
3775
3850
|
for(const [part,bytes]of Object.entries(entries)){
|
|
@@ -3873,6 +3948,8 @@ async function normalizePptxZip(raw, context) {
|
|
|
3873
3948
|
});
|
|
3874
3949
|
// A picture repeated across slides embeds once (after fitting, which needs each slide's own relationship).
|
|
3875
3950
|
dedupeMedia(entries, imageMetadata, resolveRelationshipTarget);
|
|
3951
|
+
// The media keys of the logo-reference pictures, now that the package holds its final media (src/furniture-provenance.js).
|
|
3952
|
+
stampFurnitureImageKeys(entries, context.furnitureManifests, context.furnitureImages, parseRelationships);
|
|
3876
3953
|
// Charts and notes follow the language and fonts of the slide they belong to.
|
|
3877
3954
|
context.partSlides = new Map();
|
|
3878
3955
|
for (const part of Object.keys(entries)) {
|
|
@@ -3919,6 +3996,12 @@ async function normalizePptxZip(raw, context) {
|
|
|
3919
3996
|
}
|
|
3920
3997
|
reportPerSlideNotesScriptFonts(context.scriptFonts, slideThemes);
|
|
3921
3998
|
}
|
|
3999
|
+
// RR-72: furniture drawn the same on several slides moves to the slide master or a layout (src/master-furniture.js).
|
|
4000
|
+
try {
|
|
4001
|
+
liftMasterFurniture(output, context.furnitureParts, diagnostic => context.reportDiagnostic?.(diagnostic));
|
|
4002
|
+
} catch (error) {
|
|
4003
|
+
throw new OPFPptxError('packaging-failed', 'Header and footer furniture could not be written to the slide master and layouts.', {cause: errorMessage(error)});
|
|
4004
|
+
}
|
|
3922
4005
|
finalizeFontsUsed(output);
|
|
3923
4006
|
// Document references record evidence from the final normalized parts.
|
|
3924
4007
|
if (context.documentProvenance) {
|
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.
|