@openpresentation/opf-pptx 0.22.0 → 0.23.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 +15 -4
- package/dist/background-import.js +10 -7
- package/dist/background.js +18 -1
- package/dist/chart-scheme-colors.js +3 -2
- package/dist/chartex.js +2 -2
- package/dist/image-provenance.js +4 -1
- package/dist/index.js +215 -34
- package/dist/theme-colors.js +128 -37
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -431,14 +431,25 @@ Table chrome follows the theme too (FF-24c). With no theme override on the slide
|
|
|
431
431
|
|
|
432
432
|
When the deck background is a theme slot other than `light1`, the slide master gets that background and its title/body/other text styles use the paired text slot. The layout drops its own `bg1` background and inherits the master, so slides added in PowerPoint match the theme. `light1` decks keep the vendored master.
|
|
433
433
|
|
|
434
|
+
### Color variants (RR-96)
|
|
435
|
+
|
|
436
|
+
A deck's `design.variant` (a background slot string such as `"dark1"`, or `{background, accents?}` with `background` one of `light1`, `light2`, `dark1`, `dark2` and `accents` a permutation of `accent1`-`accent6`) decides what the master color map says. Export writes core's `clrMapAttributes` into the master's `<p:clrMap>`: `bg1`/`tx1`/`bg2`/`tx2` name the slots the variant puts on the background and on text, and the six `accentN` names point at the permuted accent slots. A slide whose variant differs from the deck's writes `<p:clrMapOvr><a:overrideClrMapping .../>` with its own map, so a dark slide in a light deck keeps `bg1`/`tx1` as its background and text. A deck whose theme background is dark gets the dark map without an explicit variant. In PowerPoint, switching a slide's Background Style or the deck's variant therefore recolors text, borders, tables, charts and code panels together.
|
|
437
|
+
|
|
438
|
+
Every color the exporter derives from the scheme is written as `a:schemeClr`: text, backgrounds, table fills and borders, card fills and edges, chart text, gridlines and axis lines, code panels and syntax colors, metric trend marks, tags and footers. The deck's neutrals are core's `roles.neutrals`: the text color mixed into the background at a fixed share (border 20%, gridline 15%, axis line 25%, chart text 65%). They are written as the text slot with `a:lumMod`/`a:lumOff` (a translucent mix as `a:alpha`), so a variant change keeps them in step. The pass runs over the finished slide, master, layout and chart parts, before provenance is fingerprinted.
|
|
439
|
+
|
|
440
|
+
A picture background that names its `tone` (`dark` or `light`) and has no overlay of its own is covered by one full-slide rectangle named `OPF background tone <tone> slides.<n>`, directly above the picture: the tone variant's `bg1` as `a:schemeClr` at 40% alpha (the preview's `data-opf-tone-overlay` scrim), so text keeps its contrast in PowerPoint and Keynote. The slide's `clrMapOvr` is the tone's map. Import takes that rectangle back into the background's `tone`.
|
|
441
|
+
|
|
434
442
|
These stay `a:srgbClr`:
|
|
435
443
|
|
|
436
|
-
- literal hex colors, even when they equal a scheme slot;
|
|
444
|
+
- literal hex colors the document wrote, even when they equal a scheme slot;
|
|
437
445
|
- `var:<id>` variables;
|
|
438
|
-
-
|
|
439
|
-
-
|
|
446
|
+
- colors on a slide with its own `design.colorScheme` or a slide theme that changes a slot (every color on that slide is literal);
|
|
447
|
+
- a translucent fill the theme cannot hold (`a:schemeClr` plus `a:alpha` is used where the slot color itself is opaque);
|
|
448
|
+
- colors that match no scheme slot or derived neutral, such as contrast-adjusted series colors and `hlink`/`folHlink` fallbacks.
|
|
449
|
+
|
|
450
|
+
Import reads the master color map into `design.variant` (the string shorthand when there are no accent changes), the slide's `clrMapOvr` into the slide's `design.variant`, and a `schemeClr` background fill into the theme-slot background. A `bg1` background imports as no background (the variant implies it); another light or dark slot imports as `{type:'theme', slot}`. A variant that core already implies from the deck's own design is dropped, so a dark-by-theme deck does not gain one. A color map that is not a permutation core's variants describe reports `unsupported-color-map` and is not applied. On a slide without exporter provenance, a table fill equal to a scheme slot returns as the slot name (`accent1`, `light2`, ...). `test/color-variants.mjs` covers the master map, the override with accents, the scheme neutrals and the import round trip. This establishes package structure and round-trip, not PowerPoint rendering.
|
|
440
451
|
|
|
441
|
-
Import reads the first slide master's theme and matches it against the host default catalog only (the first of `fromPptx`'s `catalogs`): recovered bare ids resolve in a document that declares no catalogs. Theme and colour-scheme recovery does not use the foreign-layout default: omitted catalogs and explicit `catalogs: []` both leave recovery unregistered. An exact twelve-slot match with one of its colour schemes returns that id; the `clrScheme` name breaks ties. Otherwise the importer returns inline slots, relative to the scheme the `clrScheme` is named after when there is one (`{id:'boost', accent1:'#123456'}`). A theme whose name equals one of its themes' names maps to `design.theme` only if the package's color scheme or heading/body fonts match that theme (its references resolved in its own group first); otherwise `theme-unverified` is reported. With omitted catalogs or explicit `catalogs: []`, or a match only in another registered catalog, the colour scheme imports inline and no theme id is recovered; an FF-32 stored reference still restores. A missing theme, or slots that are not opaque sRGB/system colors, report `unsupported-theme-colors` on `design.colorScheme`.
|
|
452
|
+
Import reads the first slide master's theme and matches it against the host default catalog only (the first of `fromPptx`'s `catalogs`): recovered bare ids resolve in a document that declares no catalogs. Theme and colour-scheme recovery does not use the foreign-layout default: omitted catalogs and explicit `catalogs: []` both leave recovery unregistered. An exact twelve-slot match with one of its colour schemes returns that id; the `clrScheme` name breaks ties. Otherwise the importer returns inline slots, relative to the scheme the `clrScheme` is named after when there is one (`{id:'boost', accent1:'#123456'}`). A theme whose name equals one of its themes' names maps to `design.theme` only if the package's color scheme or heading/body fonts match that theme (its references resolved in its own group first); otherwise `theme-unverified` is reported. With omitted catalogs or explicit `catalogs: []`, or a match only in another registered catalog, the colour scheme imports inline and no theme id is recovered; an FF-32 stored reference still restores. A missing theme, or slots that are not opaque sRGB/system colors, report `unsupported-theme-colors` on `design.colorScheme`. Other slide colors are still imported as resolved hex. Role overrides such as `primary` have no theme slot and are not recovered. `test/theme-colors.mjs` covers all 14 gallery schemes and 4 gallery themes (registered as `defaultCatalog`), import without catalogs, override, foreign and damaged themes, and the literal-color boundaries. `test/table-theme-colors.mjs` covers named and default table chrome on all 14 schemes and four backgrounds, literals, override slides, the link colors, re-import and a theme switch in the package. This establishes package structure and round-trip, not PowerPoint rendering.
|
|
442
453
|
|
|
443
454
|
## JPEG orientation on import
|
|
444
455
|
|
|
@@ -62,7 +62,7 @@ export function readSlideTheme(slidePath, {part, relationships, bytes}) {
|
|
|
62
62
|
if (elements?.['a:fmtScheme']) {format = elements['a:fmtScheme'];formatPath = themePath;}
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
return {chain, colors, mapping, fonts, format, formatPath, parsedPart};
|
|
65
|
+
return {chain, colors, mapping, masterMapping, fonts, format, formatPath, parsedPart};
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export function importBackground(slidePath, dimensions, archive, report) {
|
|
@@ -75,7 +75,7 @@ export function importBackground(slidePath, dimensions, archive, report) {
|
|
|
75
75
|
return undefined;
|
|
76
76
|
};
|
|
77
77
|
// Picture relationships belong to the part that holds the fill.
|
|
78
|
-
const context = {colors, mapping, image: fill => readImageBackground(fill, owner.path, archive, dimensions, report)};
|
|
78
|
+
const context = {colors, mapping, image: fill => readImageBackground(fill, owner.path, archive, dimensions, report, {colors, mapping})};
|
|
79
79
|
if (background['p:bgPr']) return readNativeBackground(background['p:bgPr'], dimensions, report, context);
|
|
80
80
|
const reference = background['p:bgRef'];
|
|
81
81
|
if (!/^\d+$/.test(reference?.idx ?? '')) return unsupported();
|
|
@@ -93,7 +93,7 @@ export function importBackground(slidePath, dimensions, archive, report) {
|
|
|
93
93
|
const style = styles?.[index < 1000 ? index - 1 : index - 1001];
|
|
94
94
|
if (!style) return unsupported();
|
|
95
95
|
context.placeholder = readBackgroundColor(reference, context);
|
|
96
|
-
context.image = fill => readImageBackground(fill, formatPath, archive, dimensions, report);
|
|
96
|
+
context.image = fill => readImageBackground(fill, formatPath, archive, dimensions, report, {colors, mapping});
|
|
97
97
|
return readNativeBackground(drawingObject([style]), dimensions, report, context);
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -108,7 +108,7 @@ const focusValue = value => Math.round(Math.max(0, Math.min(1, value)) * 10000)
|
|
|
108
108
|
// the focus was, so it reads as the edge-most focus that gives the same crop); centered fill-rectangle insets that keep
|
|
109
109
|
// the picture's aspect are `contain`; an uncropped fill of another aspect is `stretch`. Any other geometry or picture
|
|
110
110
|
// effect is imported as the closest fit and reported.
|
|
111
|
-
function readImageBackground(fill, partPath, {relationships, bytes}, dimensions, report) {
|
|
111
|
+
function readImageBackground(fill, partPath, {relationships, bytes}, dimensions, report, context = {}) {
|
|
112
112
|
const blip = fill['a:blip'] ?? {};
|
|
113
113
|
const relationship = blip['r:embed'] && relationships(partPath).get(blip['r:embed']);
|
|
114
114
|
const data = relationship && relationship.targetMode !== 'External' ? bytes(relationship.path) : undefined;
|
|
@@ -125,9 +125,12 @@ function readImageBackground(fill, partPath, {relationships, bytes}, dimensions,
|
|
|
125
125
|
if (blip['a:grayscl'] !== undefined) recolor = 'grayscale';
|
|
126
126
|
const duotone = blip['a:duotone'], literal = node => /^[\da-f]{6}$/i.test(node?.val ?? '') && Object.keys(node).every(key => key === 'val' || typeof key === 'symbol') ? `#${node.val.toUpperCase()}` : undefined;
|
|
127
127
|
if (duotone) {
|
|
128
|
-
|
|
129
|
-
const [
|
|
130
|
-
|
|
128
|
+
// RR-96: the two colors are literals or scheme colors (the export writes the scheme-derived ones as schemeClr); a scheme color reads as the color it stands for.
|
|
129
|
+
const kind = ['a:srgbClr', 'a:schemeClr'].find(name => Array.isArray(duotone[name]) && duotone[name].length === 2);
|
|
130
|
+
const colors = kind ? duotone[kind] : [];
|
|
131
|
+
const read = node => { if (kind === 'a:srgbClr') return literal(node); const resolved = readBackgroundColor({[kind]: node}, context); return resolved && resolved.alpha === 1 ? resolved.hex : undefined; };
|
|
132
|
+
const [dark, light] = colors.map(read);
|
|
133
|
+
if (colors.length === 2 && dark && light && Object.keys(duotone).every(key => key === kind || typeof key === 'symbol')) recolor = {dark, light};
|
|
131
134
|
else approximate('uses a duotone recolor that is not two literal colours');
|
|
132
135
|
}
|
|
133
136
|
if (recolor && blip['a:grayscl'] !== undefined && duotone) approximate('uses both a grayscale and a duotone recolor');
|
package/dist/background.js
CHANGED
|
@@ -184,11 +184,28 @@ function readNativePattern(pattern, report, context) {
|
|
|
184
184
|
...(uniform && foreground.alpha !== 1 ? {opacity: foreground.alpha} : {})};
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
// The OPF slot an `a:schemeClr` with no color transform stands for through the color map (`bg1` stays the role), or undefined.
|
|
188
|
+
// (OPF's theme-slot background is one of the four light and dark slots; an accent keeps its resolved color.)
|
|
189
|
+
const SLOT_NAMES_BY_OOXML = {lt1: 'light1', dk1: 'dark1', lt2: 'light2', dk2: 'dark2'};
|
|
190
|
+
function plainSchemeSlot(fill, context) {
|
|
191
|
+
const node = fill?.['a:schemeClr'];
|
|
192
|
+
if (!node || Array.isArray(node) || Object.keys(node).some(key => key.startsWith('a:'))) return undefined;
|
|
193
|
+
if (node.val === 'bg1') return !context.mapping?.bg1 || SLOT_NAMES_BY_OOXML[context.mapping.bg1] ? 'bg1' : undefined;
|
|
194
|
+
return SLOT_NAMES_BY_OOXML[context.mapping?.[node.val] ?? node.val];
|
|
195
|
+
}
|
|
196
|
+
|
|
187
197
|
export function readNativeBackground(properties, {width, height}, report = () => {}, context = {}) {
|
|
188
198
|
if (!properties) return undefined;
|
|
189
199
|
if (Object.hasOwn(properties, 'a:noFill')) return {type: 'solid', color: '#FFFFFF', opacity: 0};
|
|
190
200
|
const solid = readBackgroundColor(properties['a:solidFill'], context);
|
|
191
|
-
if (solid)
|
|
201
|
+
if (solid) {
|
|
202
|
+
// RR-96: a plain scheme color is the theme slot it reaches through the color map (the OPF theme-slot background); bg1 is the variant's own
|
|
203
|
+
// background, which design.variant already carries, so it imports as no background of its own.
|
|
204
|
+
const slot = plainSchemeSlot(properties['a:solidFill'], context);
|
|
205
|
+
if (slot === 'bg1' && solid.alpha === 1) return undefined;
|
|
206
|
+
if (slot && slot !== 'bg1') return {type: 'theme', slot, ...(solid.alpha === 1 ? {} : {opacity: solid.alpha})};
|
|
207
|
+
return {type: 'solid', color: solid.hex, ...(solid.alpha === 1 ? {} : {opacity: solid.alpha})};
|
|
208
|
+
}
|
|
192
209
|
if (properties['a:pattFill']) return readNativePattern(properties['a:pattFill'], report, context);
|
|
193
210
|
if (properties['a:blipFill'] && context.image) return context.image(properties['a:blipFill']);
|
|
194
211
|
const gradient = properties['a:gradFill'];
|
|
@@ -12,13 +12,14 @@ const hex6 = value => String(value).replace(/^#/, '').toUpperCase();
|
|
|
12
12
|
* maps a colour reference and its resolved hex to a scheme value when the deck theme holds that colour (theme-colors.js). A colour the
|
|
13
13
|
* panel adjusted, a hex or variable entry and a tint (series 7 to 12) have none.
|
|
14
14
|
*/
|
|
15
|
-
export function chartPaletteSchemes(colors, preferred, adjusted, schemeFor) {
|
|
15
|
+
export function chartPaletteSchemes(colors, preferred, adjusted, schemeFor, accents) {
|
|
16
16
|
const authored = Array.isArray(colors) ? colors : [];
|
|
17
17
|
return preferred.map((color, index) => {
|
|
18
18
|
if (hex6(color) !== hex6(adjusted[index])) return undefined;
|
|
19
19
|
const entry = typeof authored[index] === 'string' ? authored[index].trim() : undefined;
|
|
20
20
|
if (entry !== undefined) return /^[A-Za-z]/.test(entry) ? schemeFor(entry, hex6(color)) : undefined;
|
|
21
|
-
|
|
21
|
+
// RR-96: series i is the slot the variant's accent order puts at position i (accent i + 1 by default), and schemeFor names the role that slot has.
|
|
22
|
+
return index < 6 ? schemeFor(accents?.[index] ?? `accent${index + 1}`, hex6(color)) : undefined;
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
25
|
|
package/dist/chartex.js
CHANGED
|
@@ -171,7 +171,7 @@ const axisTitle = (value, text, run) => {
|
|
|
171
171
|
|
|
172
172
|
function axes(spec, gridColor, text, titles = {}, run = {}, code) {
|
|
173
173
|
if (!spec.axes) return '';
|
|
174
|
-
const gridlines = `<cx:majorGridlines><cx:spPr><a:ln w="9525"><a:solidFill><a:srgbClr val="${gridColor}"
|
|
174
|
+
const gridlines = `<cx:majorGridlines><cx:spPr><a:ln w="9525"><a:solidFill><a:srgbClr val="${gridColor}"/></a:solidFill></a:ln></cx:spPr></cx:majorGridlines>`;
|
|
175
175
|
const gapWidth = {clusteredColumn: '0.06', paretoLine: '0.06', boxWhisker: '1', waterfall: '0.5', funnel: '0.06'}[spec.layoutId] ?? '1';
|
|
176
176
|
// CT_Axis order: scaling, units, majorGridlines, tickLabels, spPr, txPr.
|
|
177
177
|
const category = `<cx:axis id="0"><cx:catScaling gapWidth="${gapWidth}"/>${axisTitle(titles.category, text, run)}<cx:tickLabels/>${text}</cx:axis>`;
|
|
@@ -254,7 +254,7 @@ export function chartStyleXml({labelColor = '000000', gridColor = '000000', font
|
|
|
254
254
|
// theme's tx1: PowerPoint applies the style part to chartex labels that carry no cx:txPr of their own.
|
|
255
255
|
const textColor = `<a:srgbClr val="${labelColor}"/>`;
|
|
256
256
|
const face = escapeXml(font);
|
|
257
|
-
const faintLine = `<a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:srgbClr val="${gridColor}"
|
|
257
|
+
const faintLine = `<a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:srgbClr val="${gridColor}"/></a:solidFill><a:round/></a:ln>`;
|
|
258
258
|
const entry = (name, {fillIdx = '0', fillColor = '', fontColor = textColor, spPr = '', size = '', mods = ''} = {}) =>
|
|
259
259
|
`<cs:${name}${mods ? ` mods="${mods}"` : ''}><cs:lnRef idx="0"/><cs:fillRef idx="${fillIdx}">${fillColor}</cs:fillRef><cs:effectRef idx="0"/><cs:fontRef idx="minor">${fontColor}</cs:fontRef>${spPr ? `<cs:spPr>${spPr}</cs:spPr>` : ''}${size ? `<cs:defRPr sz="${size}" kern="1200">${solidFill(labelColor)}<a:latin typeface="${face}"/><a:ea typeface="${face}"/><a:cs typeface="${face}"/></cs:defRPr>` : ''}</cs:${name}>`;
|
|
260
260
|
const entries = STYLE_ENTRIES.map(name => {
|
package/dist/image-provenance.js
CHANGED
|
@@ -151,7 +151,7 @@ export function pictureFrame(metadata, {box, fit, focus = {x: .5, y: .5}, shrink
|
|
|
151
151
|
* picture's objectName, or 'background' for the overlay of a native slide background fill; `overlay` (the authored value)
|
|
152
152
|
* is stored in OPF_IMAGE_OVERLAY_V1.
|
|
153
153
|
*/
|
|
154
|
-
export function placeImages(entries, images, overlays, {metadataFor, fail, fitImage, mediaKeyFor}) {
|
|
154
|
+
export function placeImages(entries, images, overlays, {metadataFor, fail, fitImage, mediaKeyFor, finish = (_part, xml) => xml}) {
|
|
155
155
|
if (!images.size && !overlays.size) return;
|
|
156
156
|
const manifests = new Map(), overlayManifests = new Map(), placed = new Set(), shaped = new Set();
|
|
157
157
|
for (const part of Object.keys(entries).filter(path => /^ppt\/slides\/slide\d+\.xml$/.test(path))) {
|
|
@@ -176,6 +176,8 @@ export function placeImages(entries, images, overlays, {metadataFor, fail, fitIm
|
|
|
176
176
|
const blip = blipEffects(effects);
|
|
177
177
|
if (blip) picture = picture.replace(/<a:blip\b([^>]*?)(?:\/>|>([\s\S]*?)<\/a:blip>)/, (_, attributes, inner = '') => `<a:blip${attributes}>${blip}${inner}</a:blip>`);
|
|
178
178
|
}
|
|
179
|
+
// RR-96: the colours the treatment wrote become scheme colours before the manifest fingerprints the shape.
|
|
180
|
+
picture = finish(part, picture);
|
|
179
181
|
placed.add(name);
|
|
180
182
|
if (image.treatment !== undefined) {
|
|
181
183
|
const node = parser.parse(picture)['p:pic'];
|
|
@@ -191,6 +193,7 @@ export function placeImages(entries, images, overlays, {metadataFor, fail, fitIm
|
|
|
191
193
|
if (!overlay) return shape;
|
|
192
194
|
const emu = value => Math.round(value / 96 * EMUS_PER_INCH);
|
|
193
195
|
shape = shape.replace(/<p:spPr>[\s\S]*?<\/p:spPr>/, `<p:spPr><a:xfrm><a:off x="${emu(overlay.box.x)}" y="${emu(overlay.box.y)}"/><a:ext cx="${emu(overlay.box.width)}" cy="${emu(overlay.box.height)}"/></a:xfrm>${presetXml(overlay.shape)}<a:solidFill>${colorXml(overlay, overlay.opacity)}</a:solidFill><a:ln><a:noFill/></a:ln></p:spPr>`);
|
|
196
|
+
shape = finish(part, shape);
|
|
194
197
|
const node = parser.parse(shape)['p:sp'];
|
|
195
198
|
overlayManifests.set(name, {v: 1, slide: overlay.slide, owner: overlay.owner, overlay: overlay.overlay, properties: node['p:spPr']});
|
|
196
199
|
shaped.add(name);
|
package/dist/index.js
CHANGED
|
@@ -42,17 +42,17 @@ import {dedupeMedia, mediaKey} from './media-dedupe.js';
|
|
|
42
42
|
import {placeWatermarks, importWatermark, importTextWatermark, tagTextWatermarks, watermarkName, watermarkTextName, watermarkBox, watermarkOpacity} from './watermark-provenance.js';
|
|
43
43
|
import {placeLogos, importLogo, importLogoPlaceholders, logoName, LOGO_TAG} from './logo-provenance.js';
|
|
44
44
|
import {nativeBackgroundFill, nativeImageBackgroundFill, nativePatternPreset} from './background.js';
|
|
45
|
-
import {importBackground} from './background-import.js';
|
|
45
|
+
import {importBackground, readSlideTheme} from './background-import.js';
|
|
46
46
|
import {chartHighlightPlan, applyChartHighlight} from './chart-highlight.js';
|
|
47
47
|
import {orderChartSeries} from './chart-order.js';
|
|
48
|
-
import {themeSlotColors, writeThemeColors, schemeColorValue, schemeBackgroundFill, schemeBackgroundValue, defaultTextSchemeValues, tableTextSchemeValue, solidColorXml, writeMasterBackground, inheritLayoutBackground, readThemeSlotColors, recoverColorScheme, recoverTheme, presentationThemePath} from './theme-colors.js';
|
|
48
|
+
import {themeSlotColors, schemeColorTable, writeSchemeColors, authoredLiteralColors, writeThemeColors, schemeColorValue, schemeBackgroundFill, schemeBackgroundValue, defaultTextSchemeValues, tableTextSchemeValue, solidColorXml, writeMasterBackground, inheritLayoutBackground, readThemeSlotColors, recoverColorScheme, recoverTheme, presentationThemePath, variantFromColorMap} from './theme-colors.js';
|
|
49
49
|
import {languageDiagnostics, observeLanguage, observedRtl, partScriptFonts, physicalAlignment, planScriptFonts, planSlideThemes, reconcileLanguage, reportPerSlideNotesScriptFonts, runLanguageFonts, runLanguageTag, stripRunScriptFonts, themeEastAsianFromLatin} from './script-fonts.js';
|
|
50
50
|
import { webpToPng, svgToPng, readLocalFile, localFileReadable } from '#image-fallback';
|
|
51
51
|
import { prepareSvg, svgDataUriBytes, svgRasterScale, svgBlipRelationship, attachSvgPictures } from './svg-image.js';
|
|
52
52
|
import { chartPaletteSchemes, applyChartSchemeColors } from './chart-scheme-colors.js';
|
|
53
53
|
import { rasterMetadata, pictureTransform, normalizeImageOrientation } from './image-geometry.js';
|
|
54
54
|
import {
|
|
55
|
-
catalogRecords, chartPaletteForFill, chartSeriesPalette, codeHighlightBands, codeHighlightColors, codeHighlightLines, codeLineNumbers, codeLineRuns, codeSyntaxPaletteForScheme, composeSlide,
|
|
55
|
+
catalogRecords, clrMapAttributes, normalizeColorVariant, chartPaletteForFill, chartSeriesPalette, chartTextSize as coreChartTextSize, codeHighlightBands, codeHighlightColors, codeHighlightLines, codeSyntaxPaletteForRoles, readableSchemeColor, codeLineNumbers, codeLineRuns, codeSyntaxPaletteForScheme, composeSlide,
|
|
56
56
|
ENGINE_DEFAULT_FONT_SCHEME, defaultSlideBackground, fitImage, fitText, layoutTable, layoutWatermark, metricTrendMark, resolveCanvasDimensions, resolveColorRoles, resolveFontFamilies,
|
|
57
57
|
resolveChartColors, resolveReference, resolveScriptFonts, resolveTextStyle, textColorForFill, textWidthMeasurer, timelineMarkerShapes, timelineTextColor, tokenizeCode
|
|
58
58
|
} from "@openpresentation/opf/composition";
|
|
@@ -334,6 +334,14 @@ export async function toPptx(input, options = {}) {
|
|
|
334
334
|
return slide.section;
|
|
335
335
|
});
|
|
336
336
|
context.masterBackground = masterBackground(context);
|
|
337
|
+
// RR-96: the master's clrMap is the deck variant's map (a dark deck is the dark map, not hand-inverted colors); a slide whose variant
|
|
338
|
+
// differs writes its own override (exportSlideContext).
|
|
339
|
+
context.clrMap = context.roles.map ? clrMapAttributes(context.roles.map) : undefined;
|
|
340
|
+
context.clrMapOverrides = new Map();
|
|
341
|
+
// RR-96: the scheme-derived colors of the deck (master and layouts) and of each slide (its parts and charts), rewritten to schemeClr in the finished parts.
|
|
342
|
+
const authoredLiterals = authoredLiteralColors(presentation.variables, {...presentation.design, colorScheme: undefined, theme: undefined});
|
|
343
|
+
context.deckSchemeColors = {table: schemeColorTable(context), keep: authoredLiterals};
|
|
344
|
+
context.slideSchemeColors = [];
|
|
337
345
|
context.linkSentinels = linkSentinels(presentation);
|
|
338
346
|
const pptx = new PptxGenJS();
|
|
339
347
|
configurePresentation(pptx, presentation, {...context,fonts:exportSlideContext(presentation,0,context,options).fonts});
|
|
@@ -567,6 +575,8 @@ export async function fromPptx(input, options = {}) {
|
|
|
567
575
|
restoreCitations(imported, furnitureContexts.map(context => context.annotations?.notes ?? []), report);
|
|
568
576
|
restoreLogoFallback(imported, furnitureContexts);
|
|
569
577
|
applyRunColors(imported, slideProvenance, restoredGroups, report, options);
|
|
578
|
+
restoreSlotFills(imported, slideProvenance, options);
|
|
579
|
+
dropImpliedVariants(imported, options);
|
|
570
580
|
applyNativeImageFits(imported, furnitureContexts, restoredGroups, options);
|
|
571
581
|
if (imported.design?.theme === undefined) for (const diagnostic of themeDesign.diagnostics) if (diagnostic.code === "theme-unverified") report(diagnostic);
|
|
572
582
|
languageDiagnostics(imported, observedLanguage, options.onDiagnostic && report);
|
|
@@ -607,6 +617,66 @@ function applyRunColors(imported, slideProvenance, restoredGroups, report, optio
|
|
|
607
617
|
restoreRunColors(imported.slides, info, resolve, report);
|
|
608
618
|
}
|
|
609
619
|
|
|
620
|
+
// RR-96: the variant a color map imports as is dropped where the document's own design already gives that map: core picks light1 or dark1 from the
|
|
621
|
+
// slide background (a dark theme is the dark map), so a deck that is dark by its theme, and a slide that is dark by its own background, need no `variant`.
|
|
622
|
+
function dropImpliedVariants(imported, options) {
|
|
623
|
+
const rolesVariant = design => {
|
|
624
|
+
try { return JSON.stringify(coreContext({...imported, design, slides: [{}]}, 0, {...options, textMeasurement: undefined}).resolved.roles?.variant); } catch { return undefined; }
|
|
625
|
+
};
|
|
626
|
+
const withoutVariant = design => { const {variant, ...rest} = design ?? {}; return rest; };
|
|
627
|
+
if (imported.design?.variant !== undefined) {
|
|
628
|
+
const implied = rolesVariant(withoutVariant(imported.design));
|
|
629
|
+
if (implied !== undefined && implied === JSON.stringify(normalizeColorVariant(imported.design.variant))) {
|
|
630
|
+
const {variant, ...rest} = imported.design;
|
|
631
|
+
if (Object.keys(rest).length) imported.design = rest; else delete imported.design;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
for (const slide of imported.slides) {
|
|
635
|
+
if (slide.design?.variant === undefined) continue;
|
|
636
|
+
const rest = withoutVariant(slide.design);
|
|
637
|
+
const implied = rolesVariant({...imported.design, ...rest});
|
|
638
|
+
if (implied !== undefined && implied === JSON.stringify(normalizeColorVariant(slide.design.variant))) {
|
|
639
|
+
if (Object.keys(rest).length) slide.design = rest; else delete slide.design;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// RR-96: a fill that is exactly a slot of the slide's color scheme comes back as that slot (`accent1`, `light2`, ...), the way a PowerPoint scheme
|
|
645
|
+
// fill is a theme color. Table cell and column fills only; a literal that is not a slot stays a literal.
|
|
646
|
+
const FILL_SLOT_ORDER = ['accent1', 'accent2', 'accent3', 'accent4', 'accent5', 'accent6', 'dark1', 'light1', 'dark2', 'light2'];
|
|
647
|
+
function restoreSlotFills(imported, slideProvenance, options) {
|
|
648
|
+
const names = new Map();
|
|
649
|
+
const slotsOf = slide => {
|
|
650
|
+
const key = slide.design ?? imported.design ?? null;
|
|
651
|
+
if (!names.has(key)) {
|
|
652
|
+
let table = new Map();
|
|
653
|
+
try {
|
|
654
|
+
const context = resolvePresentationContext(slide.design ? {...imported, design: {...imported.design, ...slide.design}} : imported, options);
|
|
655
|
+
const slots = themeSlotColors(context.colorScheme);
|
|
656
|
+
for (const slot of FILL_SLOT_ORDER) if (slots[slot] && !table.has(slots[slot])) table.set(slots[slot], slot);
|
|
657
|
+
} catch { table = new Map(); }
|
|
658
|
+
names.set(key, table);
|
|
659
|
+
}
|
|
660
|
+
return names.get(key);
|
|
661
|
+
};
|
|
662
|
+
const visit = (value, table) => {
|
|
663
|
+
if (Array.isArray(value)) { value.forEach(item => visit(item, table)); return; }
|
|
664
|
+
if (!isPlainObject(value)) return;
|
|
665
|
+
if (typeof value.fill === 'string') {
|
|
666
|
+
const hex = value.fill.match(/^#([0-9a-f]{6})$/i)?.[1]?.toUpperCase();
|
|
667
|
+
const slot = hex && table.get(hex);
|
|
668
|
+
if (slot) value.fill = slot;
|
|
669
|
+
}
|
|
670
|
+
for (const child of Object.values(value)) visit(child, table);
|
|
671
|
+
};
|
|
672
|
+
// A slide restored from provenance keeps its authored values (a literal that happens to equal a slot stays the literal).
|
|
673
|
+
for (const [index, slide] of imported.slides.entries()) {
|
|
674
|
+
if (slideProvenance[index]?.structure !== 'untagged') continue;
|
|
675
|
+
const containers = [slide.table, ...asArray(slide.blocks).map(block => block?.table), ...Object.values(slide).flatMap(region => isPlainObject(region) ? [region.table] : [])].filter(Boolean);
|
|
676
|
+
if (containers.length) { const table = slotsOf(slide); if (table.size) containers.forEach(container => visit(container, table)); }
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
610
680
|
// A consumed logo picture (OPF_LOGO_V1) never becomes content. Its own image restores the organization's logo only when the
|
|
611
681
|
// stored document did not (RR-71: logos live on the organization), so a package exported without provenance still keeps its
|
|
612
682
|
// logo: on the primary organization, else on a new one named after the deck. The picture is one drawn asset, so it becomes the
|
|
@@ -809,7 +879,31 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
|
|
|
809
879
|
images.consumedShapes.delete(images.backgroundOverlayShape);
|
|
810
880
|
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.'});
|
|
811
881
|
}
|
|
812
|
-
|
|
882
|
+
// RR-96: the tone scrim of a picture background is no content; it restores the background's tone.
|
|
883
|
+
let toned = imageBackground;
|
|
884
|
+
if (imageBackground?.type === 'image') {
|
|
885
|
+
for (const tone of ['dark', 'light']) {
|
|
886
|
+
const index = nativeContext.shapes.findIndex(shape => shape['p:nvSpPr']?.['p:cNvPr']?.name === toneScrimName(`slides.${slideIndex}`, tone));
|
|
887
|
+
if (index >= 0) { images.consumedShapes.add(index); toned = {...imageBackground, tone}; }
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (toned) slide.design = {background: toned};
|
|
891
|
+
// RR-96: a slide's own color map override (accents included) is its variant. An override that is the master's map imports nothing, one that is
|
|
892
|
+
// the default map on a dark master is the explicit `light1`.
|
|
893
|
+
{
|
|
894
|
+
const {mapping, masterMapping} = readSlideTheme(slidePath, {
|
|
895
|
+
part: (path, parser) => parseRequiredXml(entries, path, parser), relationships: path => parseRelationships(entries, path), bytes: path => entries[path]
|
|
896
|
+
});
|
|
897
|
+
const sameMap = [...new Set([...Object.keys(mapping), ...Object.keys(masterMapping)])].every(key => mapping[key] === masterMapping[key]);
|
|
898
|
+
if (!sameMap) {
|
|
899
|
+
const mapped = variantFromColorMap(mapping, {explicit: true});
|
|
900
|
+
// The variant a tone names is the tone's, not a second statement of it.
|
|
901
|
+
const tone = slide.design?.background?.tone, impliedByTone = tone && mapped.variant === (tone === 'dark' ? 'dark1' : 'light1');
|
|
902
|
+
if (impliedByTone) { /* the tone names this variant */ }
|
|
903
|
+
else if (mapped.variant) slide.design = {...slide.design, variant: mapped.variant};
|
|
904
|
+
else if (mapped.unsupported) options.onDiagnostic?.({code: "unsupported-color-map", path: `slides.${slideIndex}.design.variant`, message: "The slide's color map override is not a variant OPF can name, so slides[n].design.variant was not imported."});
|
|
905
|
+
}
|
|
906
|
+
}
|
|
813
907
|
if (Object.keys(furniture.design).length) slide.design = {...slide.design, ...furniture.design};
|
|
814
908
|
nativeContext.imageBlocks = images.blocks;
|
|
815
909
|
nativeContext.imageSources = images.sources;
|
|
@@ -1912,10 +2006,20 @@ function resolvePresentationContext(presentation, options, core = coreContext({.
|
|
|
1912
2006
|
const {theme, colorScheme} = core.resolved;
|
|
1913
2007
|
const dimensions = {widthInches: core.options.width / 96, heightInches: core.options.height / 96};
|
|
1914
2008
|
const variables = resolveVariableColors(presentation.variables);
|
|
1915
|
-
const
|
|
2009
|
+
const definition = design.background ?? theme?.background;
|
|
1916
2010
|
const fonts = exportFonts(core, options.textMeasurement);
|
|
1917
|
-
// One resolution for every color role
|
|
1918
|
-
|
|
2011
|
+
// One resolution for every color role, shared with the SVG preview and the audit: core's, which maps the roles through the slide's
|
|
2012
|
+
// variant (RR-96: bg1, tx1, bg2, tx2 and the accent order over the one scheme, the neutrals as slot plus luminance transform).
|
|
2013
|
+
// A core older than the variants has no `resolved.roles`; its roles are resolved from the background as before.
|
|
2014
|
+
let background = resolveBackground(definition, colorScheme, variables);
|
|
2015
|
+
let roles = core.resolved.roles ?? resolveColorRoles(colorScheme, {background: `#${background}`});
|
|
2016
|
+
// A single-color background is the slide's bg1 under its variant, which can differ from the background definition (a deck
|
|
2017
|
+
// variant hides the theme's background); a picture, gradient or pattern keeps its own base color.
|
|
2018
|
+
// A theme-slot background (the theme's own, or a slot name) with no variant keeps the slot's color: the scheme's `background` role does not
|
|
2019
|
+
// restyle it, as the preview draws it.
|
|
2020
|
+
const slotBackground = design.variant === undefined && (typeof definition === 'string' || definition?.type === 'theme');
|
|
2021
|
+
if (core.resolved.roles && isSingleColorBackground(definition) && !slotBackground) background = normalizeHex(roles.background);
|
|
2022
|
+
else if (slotBackground && roles.background !== `#${background}`) roles = {...roles, background: `#${background}`};
|
|
1919
2023
|
const textColor = normalizeHex(roles.text);
|
|
1920
2024
|
|
|
1921
2025
|
return {
|
|
@@ -2022,6 +2126,11 @@ function writeLinkSentinels(xml, sentinels) {
|
|
|
2022
2126
|
});
|
|
2023
2127
|
}
|
|
2024
2128
|
|
|
2129
|
+
// RR-96: the master's clrMap from the deck variant's map (core clrMapAttributes: bg1, tx1, bg2, tx2, the six accents and the two link slots).
|
|
2130
|
+
function writeColorMap(xml, attributes) {
|
|
2131
|
+
return xml.replace(/<p:clrMap\b[^>]*\/>/, `<p:clrMap${Object.entries(attributes).map(([role, slot]) => ` ${role}="${slot}"`).join('')}/>`);
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2025
2134
|
// PptxGenJS color option: a scheme value it can emit (tx1, bg2, ...) or RRGGBB.
|
|
2026
2135
|
function pptxColor(value) {
|
|
2027
2136
|
return /^(?:tx[12]|bg[12]|accent[1-6])$/.test(value) ? value : normalizeHex(value);
|
|
@@ -2064,6 +2173,16 @@ function hostDefaultRecords(kind, catalogs) {
|
|
|
2064
2173
|
function importThemeDesign(entries, presentationRoot, presentationRels, catalogs) {
|
|
2065
2174
|
const diagnostics = [], design = {};
|
|
2066
2175
|
const report = (code, path, message) => diagnostics.push({code, path, message});
|
|
2176
|
+
// RR-96: the first slide master's clrMap is the deck's variant (a dark deck is bg1 on dk1 and tx1 on lt1), as design.variant: the background slot
|
|
2177
|
+
// alone, or {background, accents} when the map re-points the accents. The default map imports nothing.
|
|
2178
|
+
const masterId = asArray(presentationRoot?.["p:sldMasterIdLst"]?.["p:sldMasterId"]).map(node => node?.["r:id"]).find(id => presentationRels.get(id)?.type?.endsWith("/slideMaster"));
|
|
2179
|
+
const masterPart = masterId ? presentationRels.get(masterId).path : undefined;
|
|
2180
|
+
const masterMap = masterPart && entries[masterPart] ? parseOptionalXml(entries, masterPart)?.["p:sldMaster"]?.["p:clrMap"] : undefined;
|
|
2181
|
+
if (masterMap) {
|
|
2182
|
+
const mapped = variantFromColorMap(masterMap);
|
|
2183
|
+
if (mapped.variant) design.variant = mapped.variant;
|
|
2184
|
+
else if (mapped.unsupported) report("unsupported-color-map", "design.variant", "The slide master's color map is not a variant OPF can name (a background and text on the light and dark slots, with an accent order that is a permutation of accent1 to accent6), so design.variant was not imported.");
|
|
2185
|
+
}
|
|
2067
2186
|
const themePath = presentationThemePath(presentationRoot, presentationRels, path => parseRelationships(entries, path), entries);
|
|
2068
2187
|
const theme = themePath ? parseOptionalXml(entries, themePath)?.["a:theme"] : undefined;
|
|
2069
2188
|
const elements = theme?.["a:themeElements"];
|
|
@@ -2300,6 +2419,11 @@ function exportSlideContext(presentation, slideIndex, baseContext, options) {
|
|
|
2300
2419
|
throw new OPFPptxError("mixed-slide-dimensions", "PowerPoint requires one canvas size per presentation. Set dimensions on the deck or export this slide separately.");
|
|
2301
2420
|
}
|
|
2302
2421
|
const slideContext = { ...baseContext, core, composeFonts: {...core.options.fontFamilies, heading: resolved.fonts.heading, body: resolved.fonts.body, code: resolved.fonts.code}, backgroundDefinition: resolved.backgroundDefinition, colorScheme: resolved.colorScheme, fonts: resolved.fonts, colors: resolved.colors, roles: resolved.roles, variables: resolved.variables };
|
|
2422
|
+
// RR-96: a slide whose variant differs from the deck's writes its own clrMapOvr (accent order included).
|
|
2423
|
+
if (resolved.roles.map && baseContext.clrMap) {
|
|
2424
|
+
const attributes = clrMapAttributes(resolved.roles.map);
|
|
2425
|
+
if (JSON.stringify(attributes) !== JSON.stringify(baseContext.clrMap)) baseContext.clrMapOverrides.set(`ppt/slides/slide${slideIndex + 1}.xml`, attributes);
|
|
2426
|
+
}
|
|
2303
2427
|
// A slide-level color scheme (directly or through a slide theme) pins every
|
|
2304
2428
|
// named color on that slide to literal sRGB; see schemeColorValue().
|
|
2305
2429
|
const slideScheme = themeSlotColors(resolved.colorScheme), deckScheme = baseContext.themeColors ?? {};
|
|
@@ -2309,6 +2433,7 @@ function exportSlideContext(presentation, slideIndex, baseContext, options) {
|
|
|
2309
2433
|
const text = defaultTextSchemeValues(slideContext);
|
|
2310
2434
|
slideContext.textColor = text.text ?? resolved.colors.text;
|
|
2311
2435
|
slideContext.mutedColor = text.muted ?? resolved.colors.mutedText;
|
|
2436
|
+
baseContext.slideSchemeColors[slideIndex] = {table: schemeColorTable(slideContext), keep: authoredLiteralColors(slide, presentation.variables, {...effective.design, colorScheme: undefined, theme: undefined})};
|
|
2312
2437
|
return slideContext;
|
|
2313
2438
|
}
|
|
2314
2439
|
|
|
@@ -2587,6 +2712,9 @@ async function addImageItem(slide, presentation, item, slideIndex, context, opti
|
|
|
2587
2712
|
return name;
|
|
2588
2713
|
}
|
|
2589
2714
|
|
|
2715
|
+
const TONE_SCRIM_OPACITY = 0.4;
|
|
2716
|
+
const toneScrimName = (slidePath, tone) => `OPF background tone ${tone} ${slidePath}`;
|
|
2717
|
+
|
|
2590
2718
|
// An image background (core SlideComposition.backgroundImage). With alt text it is a full-slide picture at the back of
|
|
2591
2719
|
// the slide over the canvas colour, carrying the alt text as its descr; without alt it is the native slide background
|
|
2592
2720
|
// fill (written at packaging). Its overlay is one shape directly above the picture.
|
|
@@ -2623,6 +2751,12 @@ async function addBackgroundImage(slide, presentation, background, slideIndex, s
|
|
|
2623
2751
|
width: box.w * 96, height: box.h * 96, report: options.onDiagnostic
|
|
2624
2752
|
} });
|
|
2625
2753
|
}
|
|
2754
|
+
// RR-96: a picture background that names its tone (and has no overlay of its own) is a picture the text must read on: the slide background
|
|
2755
|
+
// color of the tone's variant (bg1) covers it at 40%, the preview's scrim (opf-render `data-opf-tone-overlay`).
|
|
2756
|
+
const tone = isPlainObject(authored) ? authored.tone : undefined;
|
|
2757
|
+
if (!background.overlay && (tone === 'dark' || tone === 'light')) {
|
|
2758
|
+
slide.addShape('rect', { x: box.x, y: box.y, w: box.w, h: box.h, objectName: toneScrimName(slidePath, tone), fill: { color: normalizeHex(slideContext.roles?.background ?? `#${slideContext.colors.background}`), transparency: 100 - TONE_SCRIM_OPACITY * 100 } });
|
|
2759
|
+
}
|
|
2626
2760
|
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);
|
|
2627
2761
|
}
|
|
2628
2762
|
|
|
@@ -2695,17 +2829,17 @@ async function addLogo(slide, presentation, logo, slideIndex, slideContext, cont
|
|
|
2695
2829
|
slide.addImage({...resolved, objectName: logoName(), ...region, altText: assetAlt(logo.source, presentation) ?? 'Logo'});
|
|
2696
2830
|
}
|
|
2697
2831
|
|
|
2698
|
-
// FF-62: chart text is one size
|
|
2699
|
-
//
|
|
2700
|
-
//
|
|
2701
|
-
//
|
|
2702
|
-
//
|
|
2703
|
-
//
|
|
2704
|
-
function
|
|
2832
|
+
// FF-62, RR-90: chart text is one size, core's `chartTextSize` for the chart's box: 20 px at the 1280 x 720 reference (15 pt on a 13.33 x 7.5 in
|
|
2833
|
+
// slide), shrunk only when the box is small (at most 1/18 of its height and 1/30 of its width, whole pixels) and never below
|
|
2834
|
+
// `composition.minFontSize` (default 16), scaled by the slide's shorter side over 720 px. The preview (opf-render charts.js) and core's value-axis
|
|
2835
|
+
// density call the same function with the same inputs, so the axis core composed matches the text drawn. The export writes the size in hundredths
|
|
2836
|
+
// of a point (DrawingML `sz`) on core's 0.01 pt grid, as core composes every font size (RR-16): the request rounds down, never below the
|
|
2837
|
+
// grid-rounded floor (core `gridFontSize`, `snapFontSizeUp(floor)` against `snapFontSizeDown(request)`).
|
|
2838
|
+
function chartTextHundredths(context, region) {
|
|
2705
2839
|
const scale = Math.min(context.dimensions.widthInches, context.dimensions.heightInches) * 96 / 720;
|
|
2706
2840
|
const floor = context.composition?.minFontSize ?? 16;
|
|
2707
|
-
const
|
|
2708
|
-
return floor
|
|
2841
|
+
const px = coreChartTextSize({width: region.w * 96, height: region.h * 96, scale, minFontSize: floor});
|
|
2842
|
+
return Math.max(Math.ceil(floor * scale * 75 - 1e-6), Math.floor(px * scale * 75 + 1e-6));
|
|
2709
2843
|
}
|
|
2710
2844
|
|
|
2711
2845
|
// Every c:txPr of a generated classic chart carries the chart text size: PptxGenJS writes no size on the legend (PowerPoint's
|
|
@@ -2734,12 +2868,18 @@ function addChartPayload(slide, chart, region, context, options = {}, path = "ch
|
|
|
2734
2868
|
// Keep the resolved palette surface (including alpha) explicit in native
|
|
2735
2869
|
// chart/plot areas, so inherited labels are assessed against their own panel.
|
|
2736
2870
|
const panelFill = context.colorScheme.surface ?? context.colorScheme[context.roles.dark ? 'dark2' : 'light2'] ?? `#${context.colors.surface}`;
|
|
2737
|
-
|
|
2871
|
+
// RR-96: chart text, gridlines and axis lines are core's scheme-derived neutrals (tx1 mixed into bg1 at 65%, 15% and 25%), which the finished
|
|
2872
|
+
// part writes as schemeClr with lumMod/lumOff, so a variant or background style change in PowerPoint recolors them.
|
|
2873
|
+
const neutrals = context.roles.neutrals;
|
|
2874
|
+
const labelColor = normalizeHex(textColorForFill(panelFill, neutrals?.chartText?.hex ?? `#${context.colors.text}`));
|
|
2875
|
+
// Data labels and highlighted marks contrast with their own mark, from the full text colour (the chart-text neutral is for axes and legends).
|
|
2876
|
+
const markLabelColor = normalizeHex(textColorForFill(panelFill, `#${context.colors.text}`));
|
|
2877
|
+
const gridColor = normalizeHex(neutrals?.gridline?.hex ?? `#${context.colors.border}`), axisLineColor = normalizeHex(neutrals?.axisLine?.hex ?? `#${context.colors.border}`);
|
|
2738
2878
|
const transparency = /^#[0-9a-f]{8}$/i.test(panelFill) ? (1 - parseInt(panelFill.slice(7), 16) / 255) * 100 : 0;
|
|
2739
2879
|
const fill = {color:normalizeHex(panelFill),transparency};
|
|
2740
2880
|
const objectName = `OPF chart ${context.chartHeadings.size + 1}`;
|
|
2741
2881
|
const circular = chartData.type === 'pie' || chartData.type === 'doughnut';
|
|
2742
|
-
const textSize =
|
|
2882
|
+
const textSize = chartTextHundredths(context, region);
|
|
2743
2883
|
// RR-54: the columns' number formats (Excel codes per exported series) for the caches, labels, value axis and workbook.
|
|
2744
2884
|
// The value axis shows the first plotted series' format (General when it has none), as the preview does.
|
|
2745
2885
|
// A combo chart's secondary value axis shows its first series' format (FA-15).
|
|
@@ -2747,7 +2887,7 @@ function addChartPayload(slide, chart, region, context, options = {}, path = "ch
|
|
|
2747
2887
|
const numberFormats = chartData.formats && {...chartData.formats, axis: chartData.spec.grouping === 'percentStacked' ? undefined : chartData.formats.series[0],
|
|
2748
2888
|
...(secondaryIndex >= 0 ? {secondaryAxis: chartData.formats.series[secondaryIndex]} : {}),
|
|
2749
2889
|
labels: !chartOptions?.dataLabels?.content?.includes('percent'), scatter: chartData.type === 'scatter'};
|
|
2750
|
-
context.chartHeadings.set(objectName,{heading:chartData.type === 'scatter' ? undefined : chartData.heading,labelColor,spec:chartData.spec,textSize,
|
|
2890
|
+
context.chartHeadings.set(objectName,{heading:chartData.type === 'scatter' ? undefined : chartData.heading,labelColor,markLabelColor,spec:chartData.spec,textSize,
|
|
2751
2891
|
options:chartOptions,kind:chartTargetFor(chart.type)?.kind,pointCount:chartData.pointCount,numberFormats});
|
|
2752
2892
|
// Synthetic row-number categories are a native adaptation, not an authored
|
|
2753
2893
|
// data column. Full provenance restores the source form only on unchanged caches.
|
|
@@ -2758,15 +2898,15 @@ function addChartPayload(slide, chart, region, context, options = {}, path = "ch
|
|
|
2758
2898
|
// RR-89: the series colours are core's chartSeriesPalette (accent1 to accent6 of the deck scheme, then their tints, with the chart's own
|
|
2759
2899
|
// `colors` over them), adjusted for the chart panel like the preview does. A colour that is still an unadjusted theme accent (or a
|
|
2760
2900
|
// `chart.colors` scheme slot or role) is written as a schemeClr, so re-theming in PowerPoint recolours the series.
|
|
2761
|
-
const colorContext = {roles: context.roles, variables: context.variables};
|
|
2901
|
+
const colorContext = {roles: context.roles, variables: context.variables, accents: context.roles.map?.accents};
|
|
2762
2902
|
const preferredPalette = chartSeriesPalette(context.colorScheme, chart.colors, colorContext);
|
|
2763
2903
|
const adjustedPalette = chartPaletteForFill(panelFill, preferredPalette);
|
|
2764
2904
|
const palette = adjustedPalette.map(color=>normalizeHex(color));
|
|
2765
|
-
const paletteSchemes = chartPaletteSchemes(chart.colors, preferredPalette, adjustedPalette, (reference, hex) => schemeColorValue(reference, `#${hex}`, context));
|
|
2905
|
+
const paletteSchemes = chartPaletteSchemes(chart.colors, preferredPalette, adjustedPalette, (reference, hex) => schemeColorValue(reference, `#${hex}`, context), context.roles.map?.accents);
|
|
2766
2906
|
Object.assign(context.chartHeadings.get(objectName), {palette, paletteSchemes, circular});
|
|
2767
2907
|
// FA-14: chart.highlight. A highlighted chart writes accent and muted colours over the series palette, and its data labels
|
|
2768
2908
|
// contrast with those colours (src/chart-highlight.js).
|
|
2769
|
-
const highlight = chartData.chartex ? undefined : chartHighlightPlan({options: chartOptions, data: chartData.resolved, kind: chartTargetFor(chart.type)?.kind, panelFill, labelColor, primary: `#${context.colors.accent}`,
|
|
2909
|
+
const highlight = chartData.chartex ? undefined : chartHighlightPlan({options: chartOptions, data: chartData.resolved, kind: chartTargetFor(chart.type)?.kind, panelFill, labelColor: markLabelColor, primary: `#${context.colors.accent}`,
|
|
2770
2910
|
schemeFor: hex => schemeColorValue('primary', hex, context)});
|
|
2771
2911
|
context.chartPalettes.set(objectName,highlight ? highlight.seriesFills : palette);
|
|
2772
2912
|
if (highlight) context.chartHeadings.get(objectName).highlight = highlight;
|
|
@@ -2774,7 +2914,7 @@ function addChartPayload(slide, chart, region, context, options = {}, path = "ch
|
|
|
2774
2914
|
// The native chartex part is added when the package is normalized (attachChartexParts); the classic chart below becomes its fallback.
|
|
2775
2915
|
// PptxGenJS rewrites the series it is given (labels become nested levels), so the chartex part keeps its own copy.
|
|
2776
2916
|
const series = chartData.series.map((entry) => ({name: entry.name, labels: [...entry.labels], values: [...entry.values]}));
|
|
2777
|
-
context.chartex.set(objectName, {spec: chartData.chartex, series, hasCategories: chartData.hasCategories, fill, labelColor, gridColor
|
|
2917
|
+
context.chartex.set(objectName, {spec: chartData.chartex, series, hasCategories: chartData.hasCategories, fill, labelColor, gridColor, font: context.fonts.body, palette, textSize, options: chartOptions, ...(numberFormats ? {formats: numberFormats.series} : {})});
|
|
2778
2918
|
if (chartData.chartex.layoutId === 'regionMap') {
|
|
2779
2919
|
options.onDiagnostic?.({code: "chart-map-geodata", path, message: `The '${stringifyText(chart.type)}' chart is exported as a native PowerPoint map (chartex regionMap) without cached geography (no cx:geoCache; provider data is never fabricated): PowerPoint must fetch the region shapes from its online map service when the deck is opened, and until it does it shows "There was a problem getting the information for your map chart" and draws nothing. The clustered column fallback shows the same values in readers without chartex support.`});
|
|
2780
2920
|
}
|
|
@@ -2807,10 +2947,13 @@ function addChartPayload(slide, chart, region, context, options = {}, path = "ch
|
|
|
2807
2947
|
legendColor: labelColor,
|
|
2808
2948
|
legendFontFace: context.fonts.body,
|
|
2809
2949
|
legendFontSize: textSize / 100,
|
|
2810
|
-
dataLabelColor:
|
|
2950
|
+
dataLabelColor: markLabelColor,
|
|
2811
2951
|
dataLabelFontSize: textSize / 100,
|
|
2812
2952
|
showValue: false,
|
|
2813
|
-
|
|
2953
|
+
...(circular ? {dataBorder: {color: normalizeHex(neutrals?.outline?.hex ?? `#${context.colors.background}`), pt: 0.75}} : {}),
|
|
2954
|
+
valGridLine: { color: gridColor, size: 1 },
|
|
2955
|
+
catAxisLineColor: axisLineColor,
|
|
2956
|
+
valAxisLineColor: axisLineColor,
|
|
2814
2957
|
// Magnitude bars/columns/areas must not let Office auto-truncate a near-equal range.
|
|
2815
2958
|
...(chartData.spec.family === "category" && ["bar", "area"].includes(chartData.type) && chartData.spec.grouping !== "percentStacked" ? {
|
|
2816
2959
|
...(chartData.series.every(entry => entry.values.every(value => value == null || value >= 0)) ? {valAxisMinVal: 0} : {}),
|
|
@@ -2903,7 +3046,8 @@ function addTablePayload(slide, authoredTable, region, context, options, path, p
|
|
|
2903
3046
|
// FF-24c: engine chrome resolves from scheme roles (header accent1, body surface), so it follows the deck theme like a named fill.
|
|
2904
3047
|
const fillReference = cellStyle.fill ?? (header ? 'accent1' : 'surface');
|
|
2905
3048
|
const fillValue = schemeColorValue(fillReference, baseFill, context);
|
|
2906
|
-
|
|
3049
|
+
// RR-96: header text is the role (bg1 or tx1) that reads better on the fill, as the preview chooses it (core readableSchemeColor).
|
|
3050
|
+
const inheritedText = header && context.roles ? readableSchemeColor(`#${baseFill}`, context.roles).hex : textColorForFill(`#${baseFill}`, header ? "#FFFFFF" : `#${context.colors.text}`);
|
|
2907
3051
|
const baseColor = exportColor(cellStyle.color ?? inheritedText.replace(/^#/, ""), context, inheritedText.replace(/^#/, ""));
|
|
2908
3052
|
// Default cell text follows the theme only with a theme-referenced fill: paired slot on a light or dark fill, light1/dark1 on an accent fill.
|
|
2909
3053
|
const defaultText = cellStyle.color === undefined ? tableTextSchemeValue(fillValue, baseColor, context) : undefined;
|
|
@@ -3199,7 +3343,8 @@ function addCodePayload(slide, value, layout, region, context, path, options) {
|
|
|
3199
3343
|
const highlight = codeHighlight(value, layout, context);
|
|
3200
3344
|
context.codeTags.set(panelName,codeManifest(value,layout,group,highlight?.bands.length));
|
|
3201
3345
|
const syntax = codeSyntax(value, layout, context, path, options);
|
|
3202
|
-
|
|
3346
|
+
const palette = codePalette(context);
|
|
3347
|
+
slide.addShape('rect', {...region, fill: {color: codePanelColor(context).slice(1)}, line: {color: context.roles.neutrals?.codePanel ? context.colors.border : '334155', width: .75}, objectName:panelName});
|
|
3203
3348
|
if (highlight) {
|
|
3204
3349
|
const bodyPart = layout.parts.find(part => part.role === 'body');
|
|
3205
3350
|
highlight.bands.forEach((band, bandIndex) => {
|
|
@@ -3223,7 +3368,7 @@ function addCodePayload(slide, value, layout, region, context, path, options) {
|
|
|
3223
3368
|
const lineOptions = {
|
|
3224
3369
|
...textBoxOptions({x:part.box.x/96,y:(part.box.y+index*part.fit.lineHeight)/96,w:part.box.width/96,h:part.fit.lineHeight/96},context,part.fit.fontSize*.75),
|
|
3225
3370
|
...nativeFontOptions(part.style),
|
|
3226
|
-
color:part.role==='body'?(lineColors?.plain.slice(1)??
|
|
3371
|
+
color:part.role==='body'?(lineColors?.plain.slice(1)??palette.plain.slice(1)):palette.keyword.slice(1),align:'left',fit:'none',wrap:false,lineSpacingMultiple:1,
|
|
3227
3372
|
tabStops:tabStops.length?tabStops:undefined,objectName,
|
|
3228
3373
|
};
|
|
3229
3374
|
// A run's own options replace the line's, so every run repeats the font, size, alignment and tab stops and differs only in colour.
|
|
@@ -3240,7 +3385,7 @@ function codeHighlight(value, layout, context) {
|
|
|
3240
3385
|
if (!body?.fit) return undefined;
|
|
3241
3386
|
const lines = codeHighlightLines(value.highlight, body.text).lines;
|
|
3242
3387
|
if (!lines.length) return undefined;
|
|
3243
|
-
return {marked: new Set(lines), numbers: codeLineNumbers(body.fit.sourceLines), bands: codeHighlightBands(body.fit.sourceLines, lines), colors: codeHighlightColors(context.colorScheme)};
|
|
3388
|
+
return {marked: new Set(lines), numbers: codeLineNumbers(body.fit.sourceLines), bands: codeHighlightBands(body.fit.sourceLines, lines), colors: codeHighlightColors(context.colorScheme, codePalette(context), codePanelColor(context))};
|
|
3244
3389
|
}
|
|
3245
3390
|
|
|
3246
3391
|
// Token ranges and palette for the code body, from core (the preview calls the same functions); undefined for plain code.
|
|
@@ -3249,14 +3394,23 @@ function codeSyntax(value, layout, context, path, options) {
|
|
|
3249
3394
|
const language = typeof value?.language === 'string' ? value.language : layout.parts.find(part => part.role === 'language')?.text;
|
|
3250
3395
|
if (!body || !language) return undefined;
|
|
3251
3396
|
const tokens = tokenizeCode(body.text, language);
|
|
3252
|
-
return tokens.length ? {tokens, palette:
|
|
3397
|
+
return tokens.length ? {tokens, palette: codePalette(context)} : undefined;
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
// RR-96: the code panel, plain text, comment and syntax colors are the slide's own roles through its variant (core codeSyntaxPaletteForRoles, the
|
|
3401
|
+
// panel `roles.neutrals.codePanel`), written as scheme colors in the finished slide; an older core keeps the fixed palette.
|
|
3402
|
+
function codePalette(context) {
|
|
3403
|
+
return context.roles.neutrals?.codePanel ? codeSyntaxPaletteForRoles(context.roles) : codeSyntaxPaletteForScheme(context.colorScheme);
|
|
3404
|
+
}
|
|
3405
|
+
function codePanelColor(context) {
|
|
3406
|
+
return context.roles.neutrals?.codePanel?.hex ?? '#111827';
|
|
3253
3407
|
}
|
|
3254
3408
|
|
|
3255
3409
|
function addMetricPayload(slide,value,layout,context,path,options) {
|
|
3256
3410
|
if (!layout) throw new OPFPptxError('missing-metric-layout','Metric export requires coordinated core metric geometry.',{path});
|
|
3257
3411
|
const group=String(context.metricTags.size+1),manifest=metricManifest(value,layout,group);
|
|
3258
3412
|
// RR-07: a trend colours the trend and delta text and adds one native arrow shape, from core's accepted geometry.
|
|
3259
|
-
const trendMark=metricTrendMark(layout,{background:`#${normalizeHex(context.colors.background)}
|
|
3413
|
+
const trendMark=metricTrendMark(layout,{background:`#${normalizeHex(context.colors.background)}`,...(context.roles.neutrals?.trendNeutral ? {neutral: context.roles.neutrals.trendNeutral.hex} : {})});
|
|
3260
3414
|
const partColor=part=>trendMark&&(part.role==='trend'||part.role==='delta')?trendMark.color.slice(1):part.role==='value'?context.colors.accent:context.textColor;
|
|
3261
3415
|
for (const [partIndex,part] of layout.parts.entries()) {
|
|
3262
3416
|
const invalid=/[\u0000-\u0008\u000B\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/u.exec(part.text);
|
|
@@ -3385,7 +3539,7 @@ function addMediaPayload(slide, presentation, value, region, path, context, opti
|
|
|
3385
3539
|
}
|
|
3386
3540
|
slide.addShape("rect", {x: region.x, y: region.y, w: region.w, h: region.h, fill: {color: context.colors.surface}, line: {color: context.colors.border, width: 0.75}, hyperlink, objectName: names.frame});
|
|
3387
3541
|
slide.addShape("ellipse", {x: icon.x / 96, y: icon.y / 96, w: 72 / 96, h: 72 / 96, fill: {color: context.colors.accent}, line: {transparency: 100}, hyperlink: hyperlink && {url: source}, objectName: names.badge});
|
|
3388
|
-
slide.addShape("triangle", {x: (icon.x + 28) / 96, y: (icon.y + 22) / 96, w: 28 / 96, h: 28 / 96, rotate: 90, fill: {color: "FFFFFF"}, line: {transparency: 100}, hyperlink: hyperlink && {url: source}, objectName: names.play});
|
|
3542
|
+
slide.addShape("triangle", {x: (icon.x + 28) / 96, y: (icon.y + 22) / 96, w: 28 / 96, h: 28 / 96, rotate: 90, fill: {color: context.roles ? normalizeHex(readableSchemeColor(`#${context.colors.accent}`, context.roles).hex) : "FFFFFF"}, line: {transparency: 100}, hyperlink: hyperlink && {url: source}, objectName: names.play});
|
|
3389
3543
|
addMeasuredPayloadText(slide, mediaCaption(value), {...box, y: icon.y + 72 + 20, height: 50}, context, options,
|
|
3390
3544
|
{path, fontSize: 18, fontFamily: context.fonts.body, fontWeight: 600, align: "center", color: context.colors.mutedText, objectName: `OPF media ${path} caption`, media: true, keepEmpty: true, hyperlink});
|
|
3391
3545
|
}
|
|
@@ -4058,6 +4212,13 @@ function referenceId(reference) {
|
|
|
4058
4212
|
|
|
4059
4213
|
// A solid color or pattern background color is a ColorRef (hex, `var:` variable, colour-scheme slot or role), resolved
|
|
4060
4214
|
// as for table fills and run colours. Roles resolve through the colour scheme alone: the background cannot depend on itself.
|
|
4215
|
+
// A background that is one color: a color string, a theme slot, or a solid fill (not a picture, gradient or pattern).
|
|
4216
|
+
function isSingleColorBackground(value) {
|
|
4217
|
+
if (value === undefined || value === null) return true;
|
|
4218
|
+
if (typeof value === 'string') return true;
|
|
4219
|
+
return isPlainObject(value) && (value.type === 'solid' || value.type === 'theme');
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4061
4222
|
function resolveBackgroundColorRef(entry, colorScheme, variables = {}) {
|
|
4062
4223
|
return resolveColorRefValue(entry, {colorScheme, colors: {}, variables});
|
|
4063
4224
|
}
|
|
@@ -4125,6 +4286,12 @@ async function normalizePptxZip(raw, context) {
|
|
|
4125
4286
|
});
|
|
4126
4287
|
}
|
|
4127
4288
|
|
|
4289
|
+
// RR-96: slide colors that are scheme colors become schemeClr first, so every fingerprint of a shape's properties (card frames, picture
|
|
4290
|
+
// manifests) records the final XML.
|
|
4291
|
+
for (const path of Object.keys(entries)) {
|
|
4292
|
+
const match = /^ppt\/slides\/slide(\d+)\.xml$/.exec(path), entry = match && context.slideSchemeColors[Number(match[1]) - 1];
|
|
4293
|
+
if (entry) entries[path] = encodeText(writeSchemeColors(decodeText(entries[path]), entry.table, entry.keep));
|
|
4294
|
+
}
|
|
4128
4295
|
attachCodeTags(entries, context.codeTags);
|
|
4129
4296
|
attachMetricTags(entries,context.metricTags,context.metricDescriptions);
|
|
4130
4297
|
attachCardTags(entries,context.cardTags);
|
|
@@ -4150,7 +4317,7 @@ async function normalizePptxZip(raw, context) {
|
|
|
4150
4317
|
if(!context.chartHeadings.has(name))continue;
|
|
4151
4318
|
const id=frame.match(/<c:chart\b[^>]*\br:id="([^"]+)"/)?.[1],chartPart=relationships.get(id)?.path;
|
|
4152
4319
|
if(!chartPart)throw new OPFPptxError('packaging-failed','Generated chart relationship is missing.');
|
|
4153
|
-
const {heading,labelColor,spec,textSize}=context.chartHeadings.get(name);
|
|
4320
|
+
const {heading,labelColor,markLabelColor,spec,textSize}=context.chartHeadings.get(name);
|
|
4154
4321
|
if(heading!==undefined)writeChartCategoryHeading(entries,chartPart,heading);
|
|
4155
4322
|
entries[chartPart]=encodeText(omitEmptyNumberPoints(applyChartConstruct(decodeText(entries[chartPart]),spec)));
|
|
4156
4323
|
const highlight=context.chartHeadings.get(name).highlight;
|
|
@@ -4168,7 +4335,7 @@ async function normalizePptxZip(raw, context) {
|
|
|
4168
4335
|
entries[chartPart]=encodeText(applyChartTextSize(decodeText(entries[chartPart]),textSize));
|
|
4169
4336
|
// RR-35: data labels last, so their own text colours (contrast inside a mark) are not normalized away.
|
|
4170
4337
|
const {options:chartOptions,kind:optionKind,pointCount}=context.chartHeadings.get(name);
|
|
4171
|
-
if(chartOptions?.dataLabels)entries[chartPart]=encodeText(applyDataLabels(decodeText(entries[chartPart]),{resolved:chartOptions,kind:optionKind,palette:context.chartPalettes.get(name),labelColor,font:context.chartFonts.get(name)?.body??'Arial',textSize,pointCount,highlight}));
|
|
4338
|
+
if(chartOptions?.dataLabels)entries[chartPart]=encodeText(applyDataLabels(decodeText(entries[chartPart]),{resolved:chartOptions,kind:optionKind,palette:context.chartPalettes.get(name),labelColor:markLabelColor,font:context.chartFonts.get(name)?.body??'Arial',textSize,pointCount,highlight}));
|
|
4172
4339
|
// RR-54: the columns' number formats, after the labels: caches, label and value-axis formats, then the workbook cells.
|
|
4173
4340
|
const numberFormats=context.chartHeadings.get(name).numberFormats;
|
|
4174
4341
|
if(numberFormats){
|
|
@@ -4238,6 +4405,7 @@ async function normalizePptxZip(raw, context) {
|
|
|
4238
4405
|
try {
|
|
4239
4406
|
placeImages(entries, context.images, context.imageOverlays, {
|
|
4240
4407
|
metadataFor: (part, id) => imageMetadata.get(parseRelationships(entries, part).get(id)?.path), fitImage,
|
|
4408
|
+
finish: (part, xml) => { const slide = /^ppt\/slides\/slide(\d+)\.xml$/.exec(part), entry = slide && context.slideSchemeColors[Number(slide[1]) - 1]; return entry ? writeSchemeColors(xml, entry.table, entry.keep) : xml; },
|
|
4241
4409
|
// The key of the bytes the package finally holds: the picture's orientation is normalized when the parts are written.
|
|
4242
4410
|
mediaKeyFor: (part, id) => { const target = parseRelationships(entries, part).get(id)?.path, bytes = entries[target]; return bytes ? mediaKey(imageMetadata.get(target) ? normalizeImageOrientation(bytes, imageMetadata.get(target)) : bytes) : undefined; },
|
|
4243
4411
|
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 }); }
|
|
@@ -4259,12 +4427,17 @@ async function normalizePptxZip(raw, context) {
|
|
|
4259
4427
|
stampFurnitureImageKeys(entries, context.furnitureManifests, context.furnitureImages, parseRelationships);
|
|
4260
4428
|
// Charts and notes follow the language and fonts of the slide they belong to.
|
|
4261
4429
|
context.partSlides = new Map();
|
|
4430
|
+
context.styleSlides = new Map();
|
|
4262
4431
|
for (const part of Object.keys(entries)) {
|
|
4263
4432
|
const slide = /^ppt\/slides\/slide(\d+)\.xml$/.exec(part);
|
|
4264
4433
|
if (!slide) continue;
|
|
4265
4434
|
context.partSlides.set(part, Number(slide[1]) - 1);
|
|
4266
4435
|
for (const relationship of parseRelationships(entries, part).values()) {
|
|
4267
4436
|
if (/\/(?:chart|chartEx|notesSlide)$/.test(relationship.type)) context.partSlides.set(relationship.path, Number(slide[1]) - 1);
|
|
4437
|
+
// The chart style and color style parts of a native chart take the scheme colors of their slide.
|
|
4438
|
+
if (/\/(?:chart|chartEx)$/.test(relationship.type) && entries[relationship.path]) {
|
|
4439
|
+
for (const related of parseRelationships(entries, relationship.path).values()) if (/\/chartStyle$/.test(related.type)) context.styleSlides.set(related.path, Number(slide[1]) - 1);
|
|
4440
|
+
}
|
|
4268
4441
|
}
|
|
4269
4442
|
}
|
|
4270
4443
|
const output = {};
|
|
@@ -4410,12 +4583,15 @@ function normalizePartBytes(path, bytes, context, renameMaps, entries, imageMeta
|
|
|
4410
4583
|
xml = themeMasterBulletFonts(xml).replace(/<p:cSld\b([^>]*)>/, (_, attributes) =>
|
|
4411
4584
|
`<p:cSld${attributes.replace(/\s+name="[^"]*"/, '')} name="pptx.gallery">`);
|
|
4412
4585
|
}
|
|
4586
|
+
if (context.clrMap && /^ppt\/slideMasters\/slideMaster\d+\.xml$/.test(path)) xml = writeColorMap(xml, context.clrMap);
|
|
4413
4587
|
if (context.masterBackground && path === 'ppt/slideMasters/slideMaster1.xml') xml = writeMasterBackground(xml, context.masterBackground);
|
|
4414
4588
|
if (context.masterBackground && /^ppt\/slideLayouts\/slideLayout\d+\.xml$/.test(path)) xml = inheritLayoutBackground(xml);
|
|
4415
4589
|
if (path === 'ppt/presentation.xml') xml = presentationInSchemaOrder(xml);
|
|
4416
4590
|
if (path === 'ppt/theme/theme1.xml') xml = writeThemeColors(xml, {colors: context.themeColors, schemeName: context.schemeName, themeName: context.themeName});
|
|
4417
4591
|
if (/^ppt\/slides\/slide\d+\.xml$/.test(path)) {
|
|
4418
4592
|
xml = writeLinkSentinels(xml, context.linkSentinels);
|
|
4593
|
+
const override = context.clrMapOverrides.get(path);
|
|
4594
|
+
if (override) xml = xml.replace('<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>', `<p:clrMapOvr><a:overrideClrMapping${Object.entries(override).map(([role, slot]) => ` ${role}="${slot}"`).join('')}/></p:clrMapOvr>`);
|
|
4419
4595
|
const fill = context.backgroundFills.get(path);
|
|
4420
4596
|
if (typeof fill === 'string') xml = xml.replace(/<p:bg>[\s\S]*?<\/p:bg>/, `<p:bg><p:bgPr>${fill}<a:effectLst/></p:bgPr></p:bg>`);
|
|
4421
4597
|
else if (fill?.image) {
|
|
@@ -4567,6 +4743,11 @@ function normalizePartBytes(path, bytes, context, renameMaps, entries, imageMeta
|
|
|
4567
4743
|
xml = nativeTitleAutofit(nativeBodyDefaults(xml, context.nativeBodies), context.nativeTitles);
|
|
4568
4744
|
}
|
|
4569
4745
|
// theme1.xml: FF-24 colors and names above, then FF-07 fonts here; they touch disjoint elements.
|
|
4746
|
+
// RR-96: literal colors that are scheme colors become schemeClr (with lumMod/lumOff for the derived neutrals).
|
|
4747
|
+
if (/^ppt\/(?:slides\/slide|charts\/chart(?:Ex)?|charts\/style)\d+\.xml$/.test(path)) {
|
|
4748
|
+
const entry = context.slideSchemeColors[context.partSlides.get(path) ?? context.styleSlides.get(path)];
|
|
4749
|
+
if (entry) xml = writeSchemeColors(xml, entry.table, entry.keep);
|
|
4750
|
+
} else if (/^ppt\/(?:slideMasters\/slideMaster|slideLayouts\/slideLayout)\d+\.xml$/.test(path)) xml = writeSchemeColors(xml, context.deckSchemeColors.table, context.deckSchemeColors.keep);
|
|
4570
4751
|
if (context.scriptFonts) xml = partScriptFonts(path, xml, context.scriptFonts, context.partSlides.get(path) ?? 0);
|
|
4571
4752
|
else if (/^ppt\/theme\/theme\d+\.xml$/.test(path)) xml = themeEastAsianFromLatin(xml);
|
|
4572
4753
|
if (/^ppt\/(?:slides\/slide|notesSlides\/notesSlide)\d+\.xml$/.test(path)) xml = stripRunScriptFonts(xml);
|
package/dist/theme-colors.js
CHANGED
|
@@ -18,14 +18,29 @@ export const THEME_SLOTS = Object.freeze([
|
|
|
18
18
|
['hyperlink', 'hlink'], ['followedHyperlink', 'folHlink'],
|
|
19
19
|
]);
|
|
20
20
|
|
|
21
|
-
// Slide content reaches the theme through the master
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
// Slide content reaches the theme through the clrMap: the master's, and a slide's own override (RR-96). With the default map
|
|
22
|
+
// bg1=lt1, tx1=dk1, bg2=lt2, tx2=dk2 and every other slot maps to itself; a variant swaps or re-points them (core ColorMap, whose
|
|
23
|
+
// slots are named dark1, light1, dark2, light2 and accent1 to accent6).
|
|
24
|
+
const DEFAULT_MAP = Object.freeze({bg1: 'light1', tx1: 'dark1', bg2: 'light2', tx2: 'dark2', accents: ['accent1', 'accent2', 'accent3', 'accent4', 'accent5', 'accent6']});
|
|
25
|
+
const FIXED_VALUES = Object.freeze({hyperlink: 'hlink', followedHyperlink: 'folHlink'});
|
|
26
|
+
// Which scheme role a document color reference prefers when two roles map to one slot (light2 and dark2 variants map bg1 and bg2 to it).
|
|
27
|
+
const PREFERRED_ROLE = Object.freeze({background: 'bg1', surface: 'bg2', text: 'tx1', textSecondary: 'tx2'});
|
|
28
|
+
// The accent a color role names: the variant's first, second and third accent (the scheme's own order by default).
|
|
29
|
+
const ACCENT_ROLE_INDEX = Object.freeze({primary: 0, secondary: 1, accent: 2});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The slide-content scheme value (tx1, bg1, bg2, tx2, accent3, hlink, ...) that reaches a theme slot through a clrMap, or undefined
|
|
33
|
+
* when no role of the map points at the slot (light1 under a light2 variant). `prefer` names the role to use when several qualify.
|
|
34
|
+
*/
|
|
35
|
+
export function contentValueFor(slot, map = DEFAULT_MAP, prefer) {
|
|
36
|
+
if (Object.hasOwn(FIXED_VALUES, slot)) return FIXED_VALUES[slot];
|
|
37
|
+
if (/^accent[1-6]$/.test(slot)) {
|
|
38
|
+
const index = (map.accents ?? DEFAULT_MAP.accents).indexOf(slot);
|
|
39
|
+
return index >= 0 ? `accent${index + 1}` : undefined;
|
|
40
|
+
}
|
|
41
|
+
const roles = ['bg1', 'tx1', 'bg2', 'tx2'].filter(role => map[role] === slot);
|
|
42
|
+
return roles.includes(prefer) ? prefer : roles[0];
|
|
43
|
+
}
|
|
29
44
|
// PptxGenJS 4.0.1 createColorElement accepts only these scheme values (pptxgenjs-plus accepts more; the set is kept
|
|
30
45
|
// so exported colours stay as they were).
|
|
31
46
|
const VENDOR_VALUES = new Set(['tx1', 'tx2', 'bg1', 'bg2', 'accent1', 'accent2', 'accent3', 'accent4', 'accent5', 'accent6']);
|
|
@@ -111,14 +126,18 @@ export function schemeColorValue(reference, hex, context, {vendor = false} = {})
|
|
|
111
126
|
const deck = context.themeColors;
|
|
112
127
|
if (!resolved || !deck) return undefined;
|
|
113
128
|
let slots;
|
|
129
|
+
const map = context.roles?.map;
|
|
114
130
|
if (SLOT_NAMES.has(name)) slots = [name];
|
|
115
|
-
|
|
131
|
+
// RR-96: the slide's own roles come first (a variant puts bg1, tx1, bg2 and tx2 on other slots), then the earlier slot candidates.
|
|
132
|
+
else if (name === 'background') slots = [map?.bg1, map?.bg2, backgroundSlot(context.backgroundDefinition)].filter(slot => SLOT_NAMES.has(slot));
|
|
133
|
+
else if (map && ACCENT_ROLE_INDEX[name] !== undefined) slots = [map.accents?.[ACCENT_ROLE_INDEX[name]]].filter(Boolean);
|
|
134
|
+
else if (map && PREFERRED_ROLE[name]) slots = [map[PREFERRED_ROLE[name]], ...(ROLE_SLOTS[name] ?? [])];
|
|
116
135
|
else slots = ROLE_SLOTS[name] ?? [];
|
|
117
136
|
const effective = themeSlotColors(context.colorScheme);
|
|
118
137
|
for (const slot of slots) {
|
|
119
138
|
if (deck[slot] !== resolved || effective[slot] !== resolved) continue;
|
|
120
|
-
const value =
|
|
121
|
-
if (!vendor || VENDOR_VALUES.has(value)) return value;
|
|
139
|
+
const value = contentValueFor(slot, context.roles?.map, PREFERRED_ROLE[name]);
|
|
140
|
+
if (value && (!vendor || VENDOR_VALUES.has(value))) return value;
|
|
122
141
|
}
|
|
123
142
|
return undefined;
|
|
124
143
|
}
|
|
@@ -131,7 +150,9 @@ function backgroundOpacity(definition) {
|
|
|
131
150
|
/** Scheme value of a theme-slot slide background, or undefined when the background stays literal. */
|
|
132
151
|
export function schemeBackgroundValue(definition, context) {
|
|
133
152
|
const slot = backgroundSlot(definition);
|
|
134
|
-
|
|
153
|
+
if (slot) return schemeColorValue(slot, context.colors?.background, context) ?? schemeColorValue('background', context.colors?.background, context);
|
|
154
|
+
// RR-96: no background of its own (or none named): the slide's bg1 through its variant's map.
|
|
155
|
+
return definition === undefined ? schemeColorValue('background', context.colors?.background, context) : undefined;
|
|
135
156
|
}
|
|
136
157
|
|
|
137
158
|
/** Native slide background fill for a theme-slot background, or null when it must stay literal. */
|
|
@@ -142,30 +163,15 @@ export function schemeBackgroundFill(definition, context) {
|
|
|
142
163
|
return `<a:solidFill>${alpha === 100000 ? `<a:schemeClr val="${value}"/>` : `<a:schemeClr val="${value}"><a:alpha val="${alpha}"/></a:schemeClr>`}</a:solidFill>`;
|
|
143
164
|
}
|
|
144
165
|
|
|
145
|
-
// The exporter picks default text for contrast with the slide background:
|
|
146
|
-
// light1 on a dark background, else the scheme text (dark1); muted text is
|
|
147
|
-
// dark2/light2. When the background itself is a theme reference, the matching
|
|
148
|
-
// text slot is the semantic pair, so a PowerPoint theme switch moves both.
|
|
149
|
-
const TEXT_PAIRS = Object.freeze({
|
|
150
|
-
light1: {text: 'dark1', muted: 'dark2'}, light2: {text: 'dark1', muted: 'dark2'},
|
|
151
|
-
dark1: {text: 'light1', muted: 'light2'}, dark2: {text: 'light1', muted: 'light2'},
|
|
152
|
-
});
|
|
153
|
-
|
|
154
166
|
/**
|
|
155
|
-
* Scheme values for the exporter's default text and muted text on this slide.
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* resolved default color is exactly the paired slot in the deck theme.
|
|
167
|
+
* Scheme values for the exporter's default text and muted text on this slide: the roles tx1 and tx2 of the slide's map (RR-96). A dark
|
|
168
|
+
* slide is the dark variant's map with light text through tx1 and tx2, not hand-inverted colors; the values are used only where the
|
|
169
|
+
* resolved color is exactly what the map's slot holds in the deck theme, and a slide without its own scheme.
|
|
159
170
|
*/
|
|
160
171
|
export function defaultTextSchemeValues(context) {
|
|
161
|
-
const background = schemeBackgroundValue(context.backgroundDefinition, context);
|
|
162
|
-
if (!background || backgroundOpacity(context.backgroundDefinition) !== 1) return {};
|
|
163
|
-
const pair = TEXT_PAIRS[backgroundSlot(context.backgroundDefinition)];
|
|
164
|
-
if (!pair) return {};
|
|
165
172
|
const values = {};
|
|
166
|
-
for (const role of ['text', 'muted']) {
|
|
167
|
-
const
|
|
168
|
-
const value = schemeColorValue(pair[role], hex, context);
|
|
173
|
+
for (const [role, name, hex] of [['text', 'text', context.colors?.text], ['muted', 'textSecondary', context.colors?.mutedText]]) {
|
|
174
|
+
const value = schemeColorValue(name, hex, context);
|
|
169
175
|
if (value) values[role] = value;
|
|
170
176
|
}
|
|
171
177
|
return values;
|
|
@@ -173,16 +179,17 @@ export function defaultTextSchemeValues(context) {
|
|
|
173
179
|
|
|
174
180
|
/**
|
|
175
181
|
* Scheme value for the exporter's default text in a table cell whose fill is a
|
|
176
|
-
* theme reference (`fillValue`, from schemeColorValue). A
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* when the selection is exactly
|
|
182
|
+
* theme reference (`fillValue`, from schemeColorValue). A fill on the background
|
|
183
|
+
* side (bg1, bg2) pairs with the map's text slot, a text-side fill (tx1, tx2) with its background slot,
|
|
184
|
+
* and an accent fill has contrast-selected text, which stays a theme reference only
|
|
185
|
+
* when the selection is exactly one of those two slots. Anything else, and any
|
|
180
186
|
* fill that is not a theme reference, keeps literal text.
|
|
181
187
|
*/
|
|
182
188
|
export function tableTextSchemeValue(fillValue, hex, context) {
|
|
183
189
|
if (!fillValue) return undefined;
|
|
190
|
+
const map = context.roles?.map ?? DEFAULT_MAP;
|
|
184
191
|
const light = /^bg[12]$/.test(fillValue), dark = /^tx[12]$/.test(fillValue);
|
|
185
|
-
const candidates = light ? [
|
|
192
|
+
const candidates = light ? [map.tx1] : dark ? [map.bg1] : /^accent[1-6]$/.test(fillValue) ? [map.bg1, map.tx1] : [];
|
|
186
193
|
for (const slot of candidates) {
|
|
187
194
|
const value = schemeColorValue(slot, hex, context, {vendor: true});
|
|
188
195
|
if (value) return value;
|
|
@@ -190,6 +197,66 @@ export function tableTextSchemeValue(fillValue, hex, context) {
|
|
|
190
197
|
return undefined;
|
|
191
198
|
}
|
|
192
199
|
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// RR-96: scheme-derived colors in the finished parts
|
|
202
|
+
|
|
203
|
+
const hex6Of = value => sixDigitHex(value);
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The colors of one slide that are scheme colors, keyed by RRGGBB: the roles tx1, bg1, tx2 and bg2 and the accents through the slide's
|
|
207
|
+
* map, then every neutral core derived (border, gridline, axis line, chart text, outline, neutral trend, code panel, code text and code
|
|
208
|
+
* comment), each with the scheme value to write and its luminance transform (`lumMod`, `lumOff`, in thousandths of a percent).
|
|
209
|
+
* A color that two entries share keeps the first (plain roles before accents before neutrals). Empty for a slide with its own color scheme
|
|
210
|
+
* (its colors stay literal) or one whose roles carry no map (a core older than the variants).
|
|
211
|
+
*/
|
|
212
|
+
export function schemeColorTable(context) {
|
|
213
|
+
const table = new Map();
|
|
214
|
+
const map = context.roles?.map;
|
|
215
|
+
if (!map || context.schemeOverride) return table;
|
|
216
|
+
const slots = themeSlotColors(context.colorScheme);
|
|
217
|
+
const roleHex = role => /^accent([1-6])$/.test(role) ? slots[(map.accents ?? [])[Number(role.slice(6)) - 1]] : slots[map[role]];
|
|
218
|
+
const add = (hex, entry) => { const key = hex6Of(hex); if (key && !table.has(key)) table.set(key, entry); };
|
|
219
|
+
for (const role of ['tx1', 'bg1', 'tx2', 'bg2']) if (slots[map[role]]) add(slots[map[role]], {value: role});
|
|
220
|
+
(map.accents ?? []).forEach((slot, index) => { if (slots[slot]) add(slots[slot], {value: `accent${index + 1}`}); });
|
|
221
|
+
for (const neutral of Object.values(context.roles.neutrals ?? {})) {
|
|
222
|
+
if (!neutral?.slot || !/^(?:tx[12]|bg[12]|accent[1-6])$/.test(neutral.slot)) continue;
|
|
223
|
+
// A neutral that is a plain role (no luminance transform) is that role only where the scheme's slot has its color: on a slide with a literal
|
|
224
|
+
// background the outline is bg1 by name but the literal's color.
|
|
225
|
+
if (neutral.lumMod === undefined && neutral.lumOff === undefined && hex6Of(neutral.hex) !== roleHex(neutral.slot)) continue;
|
|
226
|
+
add(neutral.hex, {value: neutral.slot, ...(neutral.lumMod !== undefined ? {lumMod: neutral.lumMod} : {}), ...(neutral.lumOff !== undefined ? {lumOff: neutral.lumOff} : {})});
|
|
227
|
+
}
|
|
228
|
+
return table;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Rewrite the literal `a:srgbClr` colors of a finished slide, chart or master part that are scheme colors to `a:schemeClr`: the role with its
|
|
233
|
+
* `lumMod` and `lumOff` for a derived neutral, keeping any child the literal carried (an alpha). `table` is {@link schemeColorTable}; `keep` holds the
|
|
234
|
+
* RRGGBB colors the author wrote literally on the slide, which stay literal even when a scheme color happens to equal them, and a literal that
|
|
235
|
+
* already carries its own luminance transform is left alone.
|
|
236
|
+
*/
|
|
237
|
+
export function writeSchemeColors(xml, table, keep = new Set()) {
|
|
238
|
+
if (!table.size) return xml;
|
|
239
|
+
return xml.replace(/<a:srgbClr val="([0-9A-Fa-f]{6})"\s*(?:\/>|>([\s\S]*?)<\/a:srgbClr>)/g, (match, hex, inner = '') => {
|
|
240
|
+
const key = hex.toUpperCase(), entry = table.get(key);
|
|
241
|
+
if (!entry || keep.has(key) || /<a:(?:lumMod|lumOff|tint|shade|satMod)\b/.test(inner)) return match;
|
|
242
|
+
const transforms = `${entry.lumMod !== undefined ? `<a:lumMod val="${entry.lumMod}"/>` : ''}${entry.lumOff !== undefined ? `<a:lumOff val="${entry.lumOff}"/>` : ''}`;
|
|
243
|
+
return `<a:schemeClr val="${entry.value}">${transforms}${inner}</a:schemeClr>`;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** The RRGGBB colors written literally in a slide's authored content (and the document's variables), which a scheme color must not take over. */
|
|
248
|
+
export function authoredLiteralColors(...values) {
|
|
249
|
+
const found = new Set();
|
|
250
|
+
const visit = value => {
|
|
251
|
+
if (typeof value === 'string') { for (const match of value.matchAll(/#([0-9a-f]{6}|[0-9a-f]{3})(?:[0-9a-f]{2})?\b/gi)) found.add(sixDigitHex(`#${match[1]}`)); }
|
|
252
|
+
else if (Array.isArray(value)) value.forEach(visit);
|
|
253
|
+
else if (value && typeof value === 'object') Object.values(value).forEach(visit);
|
|
254
|
+
};
|
|
255
|
+
values.forEach(visit);
|
|
256
|
+
found.delete(undefined);
|
|
257
|
+
return found;
|
|
258
|
+
}
|
|
259
|
+
|
|
193
260
|
// ---------------------------------------------------------------------------
|
|
194
261
|
// Import
|
|
195
262
|
|
|
@@ -267,6 +334,30 @@ export function recoverTheme({themeName, colors, majorFont, minorFont}, {themes,
|
|
|
267
334
|
return colorMatch || fontMatch ? {id: theme.id} : {unverified: theme.id};
|
|
268
335
|
}
|
|
269
336
|
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// RR-96: the color map back to a variant
|
|
339
|
+
|
|
340
|
+
const MAP_BACKGROUNDS = Object.freeze({'lt1,dk1,lt2,dk2': 'light1', 'lt2,dk1,lt2,dk2': 'light2', 'dk1,lt1,dk2,lt2': 'dark1', 'dk2,lt1,dk2,lt2': 'dark2'});
|
|
341
|
+
const ACCENT_KEYS = Object.freeze(['accent1', 'accent2', 'accent3', 'accent4', 'accent5', 'accent6']);
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The variant a PowerPoint color map stands for (`mapping`: bg1, tx1, bg2, tx2 as lt1, dk1, lt2, dk2, and the six accents; a missing attribute is
|
|
345
|
+
* the default). `design.variant` as authored is the background slot alone (`"dark1"`) unless the accents are re-pointed, which needs the object form
|
|
346
|
+
* `{background, accents}`. Returns `{variant}` (the default map is `light1` with no accent order, written only when `explicit`, a slide
|
|
347
|
+
* override back to the default), `{} ` for the default map, and `{unsupported: true}` for a map that is no variant (bg1 on an accent, text on a light and
|
|
348
|
+
* background on a light slot, a repeated accent): the caller reports it and imports nothing.
|
|
349
|
+
*/
|
|
350
|
+
export function variantFromColorMap(mapping, {explicit = false} = {}) {
|
|
351
|
+
const at = (key, fallback) => (typeof mapping?.[key] === 'string' ? mapping[key] : fallback);
|
|
352
|
+
const background = MAP_BACKGROUNDS[[at('bg1', 'lt1'), at('tx1', 'dk1'), at('bg2', 'lt2'), at('tx2', 'dk2')].join(',')];
|
|
353
|
+
const accents = ACCENT_KEYS.map(key => at(key, key));
|
|
354
|
+
const permutation = accents.length === 6 && new Set(accents).size === 6 && accents.every(slot => ACCENT_KEYS.includes(slot));
|
|
355
|
+
if (!background || !permutation) return {unsupported: true};
|
|
356
|
+
const reordered = accents.some((slot, index) => slot !== ACCENT_KEYS[index]);
|
|
357
|
+
if (!reordered && background === 'light1' && !explicit) return {};
|
|
358
|
+
return {variant: reordered ? {background, accents} : background};
|
|
359
|
+
}
|
|
360
|
+
|
|
270
361
|
// ---------------------------------------------------------------------------
|
|
271
362
|
// Slide master
|
|
272
363
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-pptx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Pure local OPF to PPTX export and PPTX to OPF import tooling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@node-projects/jszip": "4.3.0",
|
|
68
68
|
"@openpresentation/gallery": "^3.0.0",
|
|
69
|
-
"@openpresentation/opf": "^0.
|
|
69
|
+
"@openpresentation/opf": "^0.23.0",
|
|
70
70
|
"@rgrove/parse-xml": "4.2.3",
|
|
71
71
|
"fast-xml-parser": "^5.8.0",
|
|
72
72
|
"fflate": "^0.8.3",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"sharp": "0.35.5"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@openpresentation/opf-render": "^0.
|
|
77
|
+
"@openpresentation/opf-render": "^0.23.0"
|
|
78
78
|
},
|
|
79
79
|
"peerDependenciesMeta": {
|
|
80
80
|
"@openpresentation/opf-render": {
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"@expo-google-fonts/roboto": "0.4.3",
|
|
99
99
|
"@expo-google-fonts/roboto-mono": "0.4.2",
|
|
100
100
|
"@expo-google-fonts/tinos": "0.4.2",
|
|
101
|
-
"@openpresentation/opf-render": "0.
|
|
101
|
+
"@openpresentation/opf-render": "0.23.0",
|
|
102
102
|
"@resvg/resvg-js": "2.6.2",
|
|
103
103
|
"esbuild": "0.28.2",
|
|
104
104
|
"playwright": "1.64.0"
|