@openpresentation/opf-pptx 0.11.1 → 0.11.2
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/DEPENDENCY-NOTES.md +1 -1
- package/README.md +14 -7
- package/dist/chart-types.js +183 -0
- package/dist/chart-workbook.js +1 -1
- package/dist/index.js +174 -93
- package/dist/theme-colors.js +92 -6
- package/package.json +3 -3
package/DEPENDENCY-NOTES.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Coordinated `@openpresentation/opf` pin
|
|
4
4
|
|
|
5
|
-
PPTX 0.11.
|
|
5
|
+
PPTX 0.11.2 depends on published [`@openpresentation/opf@^0.11.2`](https://www.npmjs.com/package/@openpresentation/opf) (reference layer: ColorRef, document `variables`, exported `resolveColorRef()`). The optional renderer peer is [`@openpresentation/opf-render@^0.11.0`](https://www.npmjs.com/package/@openpresentation/opf-render) so editor 0.10.0 + render 0.11.0 + pptx 0.11.0 install together. CI checks out OPF [`b8a1faf`](https://github.com/OpenPresentation/opf/commit/b8a1faf24203635bd36b09ea19f44bcd53781f16) (`opf-v0.11.0`) for coordinated source linking. Named content colors resolve through core `resolveColorRef()`; eight-digit hex alpha stays a local export concern because core `normalizeHexColor` drops the alpha byte. Native `schemeClr` / theme `clrScheme` writes remain follow-up work.
|
|
6
6
|
|
|
7
7
|
OPF PPTX 0.5.1 ships the exact, unmodified PptxGenJS 4.0.1 ESM distribution in `vendor/pptxgenjs`, with its MIT license, upstream archive integrity and per-file SHA-256 hashes. Its actual JSZip dependency is declared directly. Ordinary npm installations therefore omit the unused image-size parser without requiring consumer overrides. This removes the affected dependency; it does not patch the parser. Published OPF PPTX 0.5.0 retains the older graph.
|
|
8
8
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OPF PPTX
|
|
2
2
|
|
|
3
|
-
Version 0.11.1 exports `design.watermark` as a native picture (it re-imports) and embeds byte-identical media parts once, with no API or dependency change. Version 0.11.0 requires core `^0.11.2` (cover slides are centered between header and footer furniture) and the optional `@openpresentation/opf-render` peer `^0.11.0`; install them together so export and preview resolve the same core. Version 0.10.0 required core `^0.11.1` and the peer `^0.10.0`. It adds native slide-number and date fields, socials, slide-image treatments, theme color schemes, script-slot fonts and RTL, re-import of design and metadata references, native underline and current-body formatting on import, and UTC-canonical `zipDate` (see the changelog for the intentional contract changes).
|
|
3
|
+
Version 0.11.2 exports the classic chart types as native chart constructs and writes named and default table colours, text pairs and hyperlink colours as theme references (no API or dependency change; resolved colours are unchanged). Version 0.11.1 exports `design.watermark` as a native picture (it re-imports) and embeds byte-identical media parts once, with no API or dependency change. Version 0.11.0 requires core `^0.11.2` (cover slides are centered between header and footer furniture) and the optional `@openpresentation/opf-render` peer `^0.11.0`; install them together so export and preview resolve the same core. Version 0.10.0 required core `^0.11.1` and the peer `^0.10.0`. It adds native slide-number and date fields, socials, slide-image treatments, theme color schemes, script-slot fonts and RTL, re-import of design and metadata references, native underline and current-body formatting on import, and UTC-canonical `zipDate` (see the changelog for the intentional contract changes).
|
|
4
4
|
|
|
5
5
|
Version 0.9.1 kept core `^0.11.0` and raised the optional `@openpresentation/opf-render` peer to `^0.9.0` so it coexists with editor 0.8.0. ColorRef / `variables` on content colors still hex-resolve through core `resolveColorRef()` before PptxGenJS `srgbClr` export. Unrecognized run colors such as `color:'invalid'` still validate and fall back to the theme text color. Native DrawingML `schemeClr` and theme `clrScheme` writes, and native `p:hf` headers/footers, are not in this release. Import still flattens theme colors to hex. Metric, quote and timeline layout placeholders and the corrected text-bullet contract from 0.8.1 are retained.
|
|
6
6
|
|
|
@@ -80,7 +80,7 @@ The first exporter keeps the public API stable while using `pptxgenjs` internall
|
|
|
80
80
|
|
|
81
81
|
- `Slide.title`, `Slide.subtitle`, and `Slide.tag` become editable text boxes, not PowerPoint master placeholders.
|
|
82
82
|
- Root payloads, `blocks[]`, and promoted region keys become editable slide objects in deterministic regions. Promoted keys use the OPF 3x3 region vocabulary (`top`, `middle`, `bottom`, `left`, `center`, `right`).
|
|
83
|
-
- Text, lists, metrics, quotes, timelines, code, tables, and inline-data charts are emitted as editable PowerPoint text, table, and chart objects. Content ColorRef values (hex, scheme slots/roles, and `var:<id>`) resolve through core `resolveColorRef()`. Slot and role names become native `a:schemeClr` references when the exported theme holds that exact color; hex, `var:<id>` and slide-override colors stay `a:srgbClr` (see [Theme color scheme](#theme-color-scheme)).
|
|
83
|
+
- Text, lists, metrics, quotes, timelines, code, tables, and inline-data charts are emitted as editable PowerPoint text, table, and chart objects. Content ColorRef values (hex, scheme slots/roles, and `var:<id>`) resolve through core `resolveColorRef()`. Slot and role names become native `a:schemeClr` references when the exported theme holds that exact color; hex, `var:<id>` and slide-override colors stay `a:srgbClr` (see [Theme color scheme](#theme-color-scheme)). Each classic chart type the core catalog keeps (one per Aspose.Slides `ChartType`: clustered/stacked/100% stacked column and bar, line and stacked line with or without markers, area/stacked/100% area, pie, doughnut, scatter with markers, radar/radar with markers/filled radar) is written as its exact native construct; deprecated core ids export as their replacement; other ids keep the previous best-effort mapping. Chartex types (treemap, histogram, pareto, box & whisker, waterfall, funnel, map) still export as clustered columns and report `chart-data-adapted` (`chartex-fallback`); a pie or doughnut chart plots only its first series and reports `chart-data-adapted` (`series-dropped`) when it has more.
|
|
84
84
|
- A chart whose data is one column of values (a histogram or dot plot) has no category column. It exports as a native chart and reports `chart-data-adapted`: a histogram is binned into equal-width bins (Sturges' count, at most 50) and written as a column chart of the counts, and any other chart type plots the values against their row numbers. PowerPoint's own histogram chart is not exported (histogram is a column chart at this engine). Cells parse as in every chart ("12%", "$5" and "1,234" count) and cells that hold no number are skipped, not plotted as 0. Binned counts do not round-trip: re-importing the file gives a `Bin`/`Frequency` column chart, not the raw values. Chart data that cannot be plotted (no numbers, no rows, or an external data source), an empty table and unsupported content keep a placeholder frame with a plain-language description (never a dump of the source data or URLs) and report `chart-data-unplottable` or `content-placeholder` with a `reason`; content is never dropped without a diagnostic.
|
|
85
85
|
- Image assets are embedded only when supplied as data URIs, local paths, or host-resolved bytes/paths. Remote asset URLs are never fetched by the runtime path.
|
|
86
86
|
- ZIP entries, generated chart/workbook part names, core-property timestamps, and nested chart workbook timestamps are normalized for reproducible bytes. [Export determinism](docs/export-determinism.md) lists the tested time zone, locale, clock and host-font controls and each known variance (WebP conversion, font registries, ICU segmentation, timestamps).
|
|
@@ -113,7 +113,7 @@ The first importer is mechanical and schema-compatible:
|
|
|
113
113
|
- The first slide master's theme `clrScheme` maps to `design.colorScheme`: a catalog id on an exact twelve-slot match, otherwise inline slots. A theme named like a catalog theme maps to `design.theme` when its colors or heading/body fonts corroborate it (see [Theme color scheme](#theme-color-scheme)).
|
|
114
114
|
- Native title/subtitle placeholders retain their roles. On slides without complete OPF heading tags, recognizable text-box positions and sizes provide a fallback. If any complete OPF heading role is recovered, untagged body text stays in `blocks[]` instead of being promoted into an absent heading role. Damaged tags retain visible text through ordinary import and diagnostics.
|
|
115
115
|
- Remaining text boxes map to `blocks[]` as text or list payloads, sorted by OOXML position.
|
|
116
|
-
- PowerPoint tables map to OPF table blocks, embedded images map to data URI image blocks, and cached chart series map to
|
|
116
|
+
- PowerPoint tables map to OPF table blocks, embedded images map to data URI image blocks, and cached chart series map to OPF chart blocks whose `type` is the core chart type id for the native construct (for example a `percentStacked` column chart imports as `100pct-stacked-column-3x`, a filled radar as `filled-radar`; a scatter chart imports as `[Point, X, ...series]`).
|
|
117
117
|
- Chart cache points are placed by their native `c:pt@idx`, with `c:ptCount` retaining trailing gaps. Missing labels and values become `null`; explicitly empty labels and series names stay empty strings, and an actual numeric zero stays zero. Empty numeric cache values remain missing. Malformed or duplicate indices, contradictory counts, competing caches and allocation limits reject with `invalid-chart-cache` and the chart-part/cache path; hierarchical category caches reject with `unsupported-chart-cache` instead of flattening labels. Each cache is limited to 100,000 positions, and combined caches and the emitted table each to 1,000,000 cells. Complete decimal scientific notation in numeric caches is read as a number; exponent overflow or nonzero underflow to zero rejects with `unsupported-chart-cache` and the chart part, series, cache and logical point path. Zero coefficients remain zero. This finite-number boundary does not classify rejected text as invalid OOXML. Malformed exponent strings and other nonblank numeric text retain the existing legacy conversion policy. This cache-only import does not repair embedded worksheets, restore unsupported scatter X/point labels, change exporter/workbook parsing or export/re-export missing-data handling, or establish native chart fidelity.
|
|
118
118
|
- Table imports retain empty rows. A native `firstRow` flag of `1` or `true` maps the first row to column labels; absent/false flags retain every row as data. New exports set this flag from OPF columns. Older exports without the flag retain their labels as the first data row rather than inferring headers.
|
|
119
119
|
- Native table text preserves run/field/break order, significant whitespace, and blank paragraphs. Cells return canonical `{value, style}` objects; `value` retains scalar text or supported rich runs. Covered merge positions are `null`. Explicit normal headers override OPF’s bold header default. Numeric/boolean/null source types cannot be reconstructed from native display text.
|
|
@@ -268,15 +268,22 @@ Import reads `a:pattFill` with a DrawingML preset and resolvable foreground/back
|
|
|
268
268
|
|
|
269
269
|
Export writes the deck's resolved color scheme into `ppt/theme/theme1.xml` `a:clrScheme`, named after the scheme. The OPF slots map one to one: `dark1`/`light1`/`dark2`/`light2` to `dk1`/`lt1`/`dk2`/`lt2`, `accent1`-`accent6` to themselves, and `hyperlink`/`followedHyperlink` to `hlink`/`folHlink`. An abstract role fills a slot only when the scheme leaves that slot unset. Previously every export carried the vendored Office palette (`accent1` `4472C4`). When the deck names a catalog `design.theme`, `a:theme` and its `thm15:themeFamily` take that theme's name. The rewrite happens during package normalization; the vendored PptxGenJS bytes are unchanged.
|
|
270
270
|
|
|
271
|
-
Document colors that name a slot or role (`accent2`, `textSecondary`, `surface`, ...) become `a:schemeClr` in runs, table cell fills and text, and table borders. Theme-slot backgrounds (`{type:'theme', slot}` or a slot name) do the same. Slide content reaches the theme through the master color map, so `dark1`, `light1`, `dark2` and `light2` are written as `tx1`, `bg1`, `tx2` and `bg2`. A reference becomes `schemeClr` only when the deck theme slot holds exactly the color the slide resolved. PowerPoint has one theme per master, so a slide
|
|
271
|
+
Document colors that name a slot or role (`accent2`, `textSecondary`, `surface`, ...) become `a:schemeClr` in runs, table cell fills and text, and table borders. Theme-slot backgrounds (`{type:'theme', slot}` or a slot name) do the same. Slide content reaches the theme through the master color map, so `dark1`, `light1`, `dark2` and `light2` are written as `tx1`, `bg1`, `tx2` and `bg2`. A reference becomes `schemeClr` only when the deck theme slot holds exactly the color the slide resolved. PowerPoint has one theme per master, so a slide with its own `design.colorScheme` (or a slide theme that changes any slot) keeps **every** color literal, including values that happen to equal the deck theme, so a theme edit never partially recolors it.
|
|
272
|
+
|
|
273
|
+
Default text follows the theme only where the background does. When a slide's background is an opaque theme reference (`light1`/`light2` or `dark1`/`dark2`, the only slots a theme background accepts), the exporter's default and muted text are written as the paired slot: `tx1`/`tx2` on `bg1`/`bg2`, and `bg1`/`bg2` on `tx1`/`tx2`. This applies only when the resolved color is exactly that deck slot, so a PowerPoint theme switch moves text and background together. It covers headings, body and list text, list markers, metric labels, quote text and footers, header/footer text and placeholders. Text on a literal background, text on a card fill and override slides stay literal.
|
|
274
|
+
|
|
275
|
+
Table chrome follows the theme too (FF-24c). With no theme override on the slide, the default header fill is `accent1`, the body fill is the `surface` slot (`bg2`, or `tx2` on a dark deck), and every cell border is `accent5`. Each is written as `a:schemeClr` when the deck theme slot holds exactly that color. Default cell text pairs with its fill: `tx1` on a `bg2` fill, `bg1` on a `tx2` fill, and light1 or dark1 (`bg1`/`tx1`) on an accent fill, again only when the contrast-selected color is exactly that slot. A named `fill`, `color` or border color on a cell (`accent2`, `surface`, `light2`, `textSecondary`, ...) is a scheme reference, `hyperlink` and `followedHyperlink` included, and a literal on the cell stays literal (default text on a literal fill included). PptxGenJS 4.0.1 cannot write `hlink`/`folHlink`, so those two are written as a reserved literal (`FE01A0`/`FE01A1`, or the next free pair) and rewritten to `a:schemeClr` in the finished slide part; a document that itself uses every reserved value keeps its link colors literal.
|
|
276
|
+
|
|
277
|
+
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.
|
|
278
|
+
|
|
279
|
+
These stay `a:srgbClr`:
|
|
272
280
|
|
|
273
281
|
- literal hex colors, even when they equal a scheme slot;
|
|
274
282
|
- `var:<id>` variables;
|
|
275
283
|
- translucent colors;
|
|
276
|
-
- engine-derived chrome:
|
|
277
|
-
- `hyperlink` and `followedHyperlink` run and cell colors, because PptxGenJS 4.0.1 cannot emit `hlink`/`folHlink`. Borders and backgrounds can.
|
|
284
|
+
- other engine-derived chrome: card fills and borders, chart panels, series and labels, timeline connectors and markers, and image and media placeholders, plus default text on a card or a literal background. These colors are contrast-selected or derived from roles, not named by the document, and are not yet theme references.
|
|
278
285
|
|
|
279
|
-
Import reads the first slide master's theme. An exact twelve-slot match with a bundled catalog scheme returns its id; the `clrScheme` name breaks ties. Otherwise the importer returns inline slots, relative to the catalog scheme the `clrScheme` is named after when there is one (`{id:'boost', accent1:'#123456'}`). A theme whose name equals a catalog theme's name maps to `design.theme` only if the package's color scheme or heading/body fonts match that theme; otherwise `theme-unverified` is reported. A missing theme, or slots that are not opaque sRGB/system colors, report `unsupported-theme-colors` on `design.colorScheme`. 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 catalog schemes and 4 catalog themes, override, foreign and damaged themes, and the literal-color boundaries. This establishes package structure and round-trip, not PowerPoint rendering.
|
|
286
|
+
Import reads the first slide master's theme. An exact twelve-slot match with a bundled catalog scheme returns its id; the `clrScheme` name breaks ties. Otherwise the importer returns inline slots, relative to the catalog scheme the `clrScheme` is named after when there is one (`{id:'boost', accent1:'#123456'}`). A theme whose name equals a catalog theme's name maps to `design.theme` only if the package's color scheme or heading/body fonts match that theme; otherwise `theme-unverified` is reported. A missing theme, or slots that are not opaque sRGB/system colors, report `unsupported-theme-colors` on `design.colorScheme`. 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 catalog schemes and 4 catalog themes, 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.
|
|
280
287
|
|
|
281
288
|
## JPEG orientation on import
|
|
282
289
|
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// OPF chart type ids -> native PowerPoint chart constructs (FF-22).
|
|
2
|
+
//
|
|
3
|
+
// The core catalog keeps one chart type per Aspose.Slides ChartType
|
|
4
|
+
// (spec/catalogs/chart-types, OpenPresentation/opf#121) and deprecates the
|
|
5
|
+
// rest with a replacement id. The published core package this exporter
|
|
6
|
+
// depends on predates that metadata, so the mapping is carried here. Kept ids
|
|
7
|
+
// map to the exact Office construct; deprecated ids resolve to their
|
|
8
|
+
// replacement; any other id keeps the legacy substring heuristic.
|
|
9
|
+
|
|
10
|
+
const category = (pptx, extra = {}) => ({ family: 'category', pptx, ...extra });
|
|
11
|
+
|
|
12
|
+
export const CHART_TYPES = Object.freeze({
|
|
13
|
+
column: category('bar', { aspose: 'ClusteredColumn', barDir: 'col', grouping: 'clustered' }),
|
|
14
|
+
'stacked-column-3x': category('bar', { aspose: 'StackedColumn', barDir: 'col', grouping: 'stacked' }),
|
|
15
|
+
'100pct-stacked-column-3x': category('bar', { aspose: 'PercentsStackedColumn', barDir: 'col', grouping: 'percentStacked' }),
|
|
16
|
+
bar: category('bar', { aspose: 'ClusteredBar', barDir: 'bar', grouping: 'clustered' }),
|
|
17
|
+
'stacked-bar-3x': category('bar', { aspose: 'StackedBar', barDir: 'bar', grouping: 'stacked' }),
|
|
18
|
+
'100pct-stacked-bar-3x': category('bar', { aspose: 'PercentsStackedBar', barDir: 'bar', grouping: 'percentStacked' }),
|
|
19
|
+
line: category('line', { aspose: 'Line', grouping: 'standard', markers: false }),
|
|
20
|
+
'line-with-markers': category('line', { aspose: 'LineWithMarkers', grouping: 'standard', markers: true }),
|
|
21
|
+
'stacked-line-3x': category('line', { aspose: 'StackedLine', grouping: 'stacked', markers: false }),
|
|
22
|
+
'stacked-line-with-markers-3x': category('line', { aspose: 'StackedLineWithMarkers', grouping: 'stacked', markers: true }),
|
|
23
|
+
area: category('area', { aspose: 'Area', grouping: 'standard' }),
|
|
24
|
+
'stacked-area-3x': category('area', { aspose: 'StackedArea', grouping: 'stacked' }),
|
|
25
|
+
'100pct-stacked-area-3x': category('area', { aspose: 'PercentsStackedArea', grouping: 'percentStacked' }),
|
|
26
|
+
pie: { family: 'circular', pptx: 'pie', aspose: 'Pie' },
|
|
27
|
+
doughnut: { family: 'circular', pptx: 'doughnut', aspose: 'Doughnut' },
|
|
28
|
+
scatter: { family: 'xy', pptx: 'scatter', aspose: 'ScatterWithMarkers' },
|
|
29
|
+
radar: category('radar', { aspose: 'Radar', radarStyle: 'standard', markers: false }),
|
|
30
|
+
'radar-with-markers': category('radar', { aspose: 'RadarWithMarkers', radarStyle: 'marker', markers: true }),
|
|
31
|
+
'filled-radar': category('radar', { aspose: 'FilledRadar', radarStyle: 'filled', markers: false }),
|
|
32
|
+
treemap: { family: 'chartex', layoutId: 'treemap', aspose: 'Treemap' },
|
|
33
|
+
histogram: { family: 'chartex', layoutId: 'clusteredColumn', aspose: 'Histogram' },
|
|
34
|
+
pareto: { family: 'chartex', layoutId: 'clusteredColumn', aspose: 'ParetoLine' },
|
|
35
|
+
'box-and-whisker': { family: 'chartex', layoutId: 'boxWhisker', aspose: 'BoxAndWhisker' },
|
|
36
|
+
waterfall: { family: 'chartex', layoutId: 'waterfall', aspose: 'Waterfall' },
|
|
37
|
+
funnel: { family: 'chartex', layoutId: 'funnel', aspose: 'Funnel' },
|
|
38
|
+
world: { family: 'chartex', layoutId: 'regionMap', aspose: 'Map' },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// A chartex id (treemap, histogram, ...) has no classic construct; the exporter
|
|
42
|
+
// writes it as a clustered column chart and reports chart-data-adapted.
|
|
43
|
+
export const CHARTEX_FALLBACK = CHART_TYPES.column;
|
|
44
|
+
|
|
45
|
+
const variants = (base, target) => Object.fromEntries([base, `${base}-2x`, `${base}-3x`].map((id) => [id, target]));
|
|
46
|
+
|
|
47
|
+
// Deprecated core ids (opf 0.12.0 removes them) -> replacement id.
|
|
48
|
+
export const DEPRECATED_CHART_TYPES = Object.freeze({
|
|
49
|
+
...variants('100pct-bullet-bar', '100pct-stacked-bar-3x'),
|
|
50
|
+
'100pct-progress-bar': '100pct-stacked-bar-3x',
|
|
51
|
+
'100pct-stacked-bar-2x': '100pct-stacked-bar-3x',
|
|
52
|
+
...variants('100pct-bullet-column', '100pct-stacked-column-3x'),
|
|
53
|
+
'100pct-stacked-column-2x': '100pct-stacked-column-3x',
|
|
54
|
+
'100pct-stacked-area-2x': '100pct-stacked-area-3x',
|
|
55
|
+
australia: 'world',
|
|
56
|
+
canada: 'world',
|
|
57
|
+
'united-kingdom': 'world',
|
|
58
|
+
'united-states': 'world',
|
|
59
|
+
'box-and-whisker-2x': 'box-and-whisker',
|
|
60
|
+
'box-and-whisker-3x': 'box-and-whisker',
|
|
61
|
+
...variants('bullet-bar', 'bar'),
|
|
62
|
+
'clustered-bar-2x': 'bar',
|
|
63
|
+
...variants('bullet-column', 'column'),
|
|
64
|
+
'clustered-column': 'column',
|
|
65
|
+
...Object.fromEntries(['', '-2x', '-3x', '-4x', '-5x', '-6x'].map((suffix) => [`dot-plot${suffix}`, 'scatter'])),
|
|
66
|
+
dumbbell: 'scatter',
|
|
67
|
+
'line-2x': 'line',
|
|
68
|
+
'line-3x': 'line',
|
|
69
|
+
'line-with-high-low': 'line',
|
|
70
|
+
...Object.fromEntries(['', '-2x', '-3x', '-4x', '-5x', '-6x'].map((suffix) => [`sparkline${suffix}`, 'line'])),
|
|
71
|
+
'line-with-high-low-and-markers': 'line-with-markers',
|
|
72
|
+
'line-with-markers-2x': 'line-with-markers',
|
|
73
|
+
'line-with-markers-3x': 'line-with-markers',
|
|
74
|
+
'stacked-area-2x': 'stacked-area-3x',
|
|
75
|
+
'stacked-bar-2x': 'stacked-bar-3x',
|
|
76
|
+
'stacked-column-2x': 'stacked-column-3x',
|
|
77
|
+
'stacked-line-2x': 'stacked-line-3x',
|
|
78
|
+
'stacked-line-with-markers-2x': 'stacked-line-with-markers-3x',
|
|
79
|
+
'treemap-2x': 'treemap',
|
|
80
|
+
'treemap-3x': 'treemap',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const ALIASES = Object.freeze({ donut: 'doughnut' });
|
|
84
|
+
|
|
85
|
+
// Resolve an OPF chart type id to {id, spec}. Kept and deprecated ids resolve
|
|
86
|
+
// to a kept id; anything else returns the legacy heuristic with id: null.
|
|
87
|
+
export function resolveChartType(type) {
|
|
88
|
+
const raw = String(type ?? '').trim().toLowerCase();
|
|
89
|
+
const id = ALIASES[raw] ?? DEPRECATED_CHART_TYPES[raw] ?? raw;
|
|
90
|
+
if (Object.hasOwn(CHART_TYPES, id)) return { id, spec: CHART_TYPES[id] };
|
|
91
|
+
return { id: null, spec: legacyChartType(raw) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The pre-FF-22 heuristic for ids outside the core catalog.
|
|
95
|
+
function legacyChartType(normalized) {
|
|
96
|
+
if (normalized.includes('pie')) return CHART_TYPES.pie;
|
|
97
|
+
if (normalized.includes('doughnut') || normalized.includes('donut')) return CHART_TYPES.doughnut;
|
|
98
|
+
if (normalized.includes('area')) return { ...CHART_TYPES.area, legacy: true };
|
|
99
|
+
if (normalized.includes('line')) return { ...CHART_TYPES['line-with-markers'], legacy: true };
|
|
100
|
+
if (normalized.includes('scatter')) return CHART_TYPES.scatter;
|
|
101
|
+
if (normalized.includes('radar')) return { ...CHART_TYPES.radar, legacy: true };
|
|
102
|
+
const grouping = normalized.includes('stacked') ? 'stacked' : 'clustered';
|
|
103
|
+
return category('bar', { barDir: normalized.includes('bar') ? 'bar' : 'col', grouping, legacy: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Native construct -> kept OPF id, for import. `node` is the parsed chart-type
|
|
107
|
+
// element (fast-xml-parser, attributes as plain keys).
|
|
108
|
+
export function chartTypeFromNative(element, node) {
|
|
109
|
+
const attr = (name) => node?.[`c:${name}`]?.val;
|
|
110
|
+
const series = asArray(node?.['c:ser']);
|
|
111
|
+
const markers = !series.length || series.some((ser) => ser?.['c:marker']?.['c:symbol']?.val !== 'none');
|
|
112
|
+
const grouping = attr('grouping') ?? 'standard';
|
|
113
|
+
const stacked = grouping === 'stacked' || grouping === 'percentStacked';
|
|
114
|
+
const percent = grouping === 'percentStacked';
|
|
115
|
+
switch (element) {
|
|
116
|
+
case 'barChart':
|
|
117
|
+
case 'bar3DChart': {
|
|
118
|
+
const direction = attr('barDir') === 'bar' ? 'bar' : 'column';
|
|
119
|
+
if (percent) return `100pct-stacked-${direction}-3x`;
|
|
120
|
+
if (stacked) return `stacked-${direction}-3x`;
|
|
121
|
+
return direction;
|
|
122
|
+
}
|
|
123
|
+
case 'lineChart':
|
|
124
|
+
case 'line3DChart':
|
|
125
|
+
if (stacked) return markers ? 'stacked-line-with-markers-3x' : 'stacked-line-3x';
|
|
126
|
+
return markers ? 'line-with-markers' : 'line';
|
|
127
|
+
case 'areaChart':
|
|
128
|
+
case 'area3DChart':
|
|
129
|
+
return percent ? '100pct-stacked-area-3x' : stacked ? 'stacked-area-3x' : 'area';
|
|
130
|
+
case 'pieChart':
|
|
131
|
+
case 'pie3DChart':
|
|
132
|
+
case 'ofPieChart':
|
|
133
|
+
return 'pie';
|
|
134
|
+
case 'doughnutChart':
|
|
135
|
+
return 'doughnut';
|
|
136
|
+
case 'scatterChart':
|
|
137
|
+
return 'scatter';
|
|
138
|
+
case 'radarChart': {
|
|
139
|
+
const style = attr('radarStyle');
|
|
140
|
+
if (style === 'filled') return 'filled-radar';
|
|
141
|
+
if (style === 'marker' && markers) return 'radar-with-markers';
|
|
142
|
+
return 'radar';
|
|
143
|
+
}
|
|
144
|
+
default:
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const NATIVE_CHART_ELEMENTS = Object.freeze([
|
|
150
|
+
'barChart', 'bar3DChart', 'lineChart', 'line3DChart', 'pieChart', 'pie3DChart', 'ofPieChart',
|
|
151
|
+
'doughnutChart', 'areaChart', 'area3DChart', 'scatterChart', 'radarChart',
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
function asArray(value) {
|
|
155
|
+
return value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// PptxGenJS omits or cannot express parts of these constructs. Rewrite only
|
|
159
|
+
// the generated chart-type element so the part states the exact grouping.
|
|
160
|
+
export function applyChartConstruct(xml, spec) {
|
|
161
|
+
// ScatterWithMarkers: the core catalog records scatterStyle "marker" (markers, no connecting line). PptxGenJS writes lineMarker and relies on the series line being noFill.
|
|
162
|
+
if (spec?.family === 'xy') return xml.replace(/<c:scatterStyle val="[^"]*"\/>/, '<c:scatterStyle val="marker"/>');
|
|
163
|
+
if (!spec || spec.family !== 'category') return xml;
|
|
164
|
+
const element = { bar: 'barChart', line: 'lineChart', area: 'areaChart', radar: 'radarChart' }[spec.pptx];
|
|
165
|
+
if (!element || element === 'radarChart') return xml;
|
|
166
|
+
const open = `<c:${element}>`;
|
|
167
|
+
const start = xml.indexOf(open);
|
|
168
|
+
if (start < 0) return xml;
|
|
169
|
+
const end = xml.indexOf(`</c:${element}>`, start);
|
|
170
|
+
let body = xml.slice(start + open.length, end);
|
|
171
|
+
const grouping = `<c:grouping val="${spec.grouping}"/>`;
|
|
172
|
+
if (element === 'barChart') {
|
|
173
|
+
body = body.replace(/<c:grouping val="[^"]*"\/>/, grouping);
|
|
174
|
+
} else {
|
|
175
|
+
body = grouping + body.replace(/<c:grouping val="[^"]*"\/>/g, '');
|
|
176
|
+
// CT_LineSer/CT_AreaSer have no invertIfNegative (bar series only).
|
|
177
|
+
body = body.replace(/<c:invertIfNegative val="[^"]*"\/>/g, '');
|
|
178
|
+
}
|
|
179
|
+
if (spec.grouping === 'stacked' || spec.grouping === 'percentStacked') {
|
|
180
|
+
body = body.replace(/<c:overlap val="[^"]*"\/>/, '<c:overlap val="100"/>');
|
|
181
|
+
}
|
|
182
|
+
return xml.slice(0, start) + open + body + xml.slice(end);
|
|
183
|
+
}
|
package/dist/chart-workbook.js
CHANGED
|
@@ -29,7 +29,7 @@ function workbookContext(entries,chartPart){
|
|
|
29
29
|
if(!/^xl\/(workbook\.xml|_rels\/workbook\.xml\.rels|sharedStrings\.xml|worksheets\/[^/]+\.xml)$/.test(file.name))return false;
|
|
30
30
|
size+=file.originalSize;if(size>16*1024*1024)throw Error('Chart workbook metadata exceeds the limit.');return true;
|
|
31
31
|
}});
|
|
32
|
-
const category=find(chart,'cat')[0],formula=category?.strRef?.f??category?.multiLvlStrRef?.f??category?.numRef?.f;
|
|
32
|
+
const category=find(chart,'cat')[0]??find(chart,'xVal')[0],formula=category?.strRef?.f??category?.multiLvlStrRef?.f??category?.numRef?.f;
|
|
33
33
|
// Infer a heading only for one contiguous vertical category range with a row
|
|
34
34
|
// above it. Other chart/workbook arrangements remain explicitly unrecovered.
|
|
35
35
|
const range=/^(?:'((?:[^']|'')+)'|([^'!]+))!\$?([A-Z]+)\$?(\d+):\$?([A-Z]+)\$?(\d+)$/i.exec(text(formula));
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import {isFaceStyleSuffix} from './font-weights.js';
|
|
|
3
3
|
import {importTableFrames} from './table-import.js';
|
|
4
4
|
import {applyChartFonts, applyPitchFamilies, finalizeFontsUsed, fontPitchFamilies} from './package-fonts.js';
|
|
5
5
|
import {readChartCategoryHeading,writeChartCategoryHeading} from './chart-workbook.js';
|
|
6
|
+
import {CHARTEX_FALLBACK,resolveChartType,chartTypeFromNative,applyChartConstruct,NATIVE_CHART_ELEMENTS} from './chart-types.js';
|
|
6
7
|
import {attachCodeTags, codeManifest, importCodeGroups, nativeShapeParagraphs, nativeTextShapes} from './code-provenance.js';
|
|
7
8
|
import {attachMetricTags,metricManifest,importMetricGroups} from './metric-provenance.js';
|
|
8
9
|
import {attachCardTags,importCardFrames} from './card-provenance.js';
|
|
@@ -19,7 +20,7 @@ import {dedupeMedia} from './media-dedupe.js';
|
|
|
19
20
|
import {placeWatermarks, importWatermark, watermarkName, watermarkBox, watermarkOpacity} from './watermark-provenance.js';
|
|
20
21
|
import {nativeBackgroundFill, nativeImageBackgroundFill, nativePatternPreset} from './background.js';
|
|
21
22
|
import {importBackground} from './background-import.js';
|
|
22
|
-
import {themeSlotColors, writeThemeColors, schemeColorValue, schemeBackgroundFill, readThemeSlotColors, recoverColorScheme, recoverTheme, presentationThemePath} from './theme-colors.js';
|
|
23
|
+
import {themeSlotColors, writeThemeColors, schemeColorValue, schemeBackgroundFill, schemeBackgroundValue, defaultTextSchemeValues, tableTextSchemeValue, solidColorXml, writeMasterBackground, inheritLayoutBackground, readThemeSlotColors, recoverColorScheme, recoverTheme, presentationThemePath} from './theme-colors.js';
|
|
23
24
|
import {languageDiagnostics, observeLanguage, partScriptFonts, planScriptFonts, reconcileLanguage} from './script-fonts.js';
|
|
24
25
|
import { webpToPng } from '#image-fallback';
|
|
25
26
|
import { rasterMetadata, pictureTransform, normalizeImageOrientation } from './image-geometry.js';
|
|
@@ -233,6 +234,8 @@ export async function toPptx(input, options = {}) {
|
|
|
233
234
|
report: diagnostic => options.onDiagnostic?.(diagnostic)
|
|
234
235
|
});
|
|
235
236
|
context.scriptFonts = planScriptFonts(presentation, options.onDiagnostic);
|
|
237
|
+
context.masterBackground = masterBackground(context);
|
|
238
|
+
context.linkSentinels = linkSentinels(presentation);
|
|
236
239
|
const pptx = new PptxGenJS();
|
|
237
240
|
configurePresentation(pptx, presentation, {...context,fonts:resolveSlideContext(presentation,presentation.slides[0],context,options).fonts});
|
|
238
241
|
|
|
@@ -851,6 +854,7 @@ function chartFromRelationship(entries, slidePath, relationships, relId) {
|
|
|
851
854
|
|
|
852
855
|
const budget = { cells: 0 };
|
|
853
856
|
const cachePath = (index, role) => `${relationship.path}#c:ser[${index}]/${role}`;
|
|
857
|
+
if (chartNode.type === 'scatter') return scatterFromSeries(entries, relationship.path, series, budget, cachePath);
|
|
854
858
|
const labels = cachedValues(series[0]?.["c:cat"], cachePath(0, 'c:cat'), budget);
|
|
855
859
|
const names = series.map((entry, index) => firstCachedValue(entry?.["c:tx"], cachePath(index, 'c:tx'), budget) ?? `Series ${index + 1}`);
|
|
856
860
|
const values = series.map((entry, index) => {
|
|
@@ -880,19 +884,31 @@ function chartFromRelationship(entries, slidePath, relationships, relId) {
|
|
|
880
884
|
};
|
|
881
885
|
}
|
|
882
886
|
|
|
887
|
+
// Scatter charts share X values (c:xVal) across Y series (c:yVal). OPF keeps
|
|
888
|
+
// them category-major: [point label, X, Y1, Y2, ...]; native charts carry no
|
|
889
|
+
// point labels, so points are numbered.
|
|
890
|
+
function scatterFromSeries(entries, chartPart, series, budget, cachePath) {
|
|
891
|
+
// The same cache helpers as every other chart: bounded, indexed by c:pt@idx, with gaps kept as null (never plotted as 0).
|
|
892
|
+
const xRole = series[0]?.["c:xVal"];
|
|
893
|
+
const xs = cachedValues(xRole, cachePath(0, "c:xVal"), budget, numericCacheValue);
|
|
894
|
+
const names = series.map((entry, index) => firstCachedValue(entry?.["c:tx"], cachePath(index, "c:tx"), budget) ?? `Series ${index + 1}`);
|
|
895
|
+
const values = series.map((entry, index) => cachedValues(entry?.["c:yVal"], cachePath(index, "c:yVal"), budget, numericCacheValue));
|
|
896
|
+
const rowCount = values.reduce((count, row) => Math.max(count, row.length), xs.length);
|
|
897
|
+
if (rowCount === 0) return null;
|
|
898
|
+
if (rowCount * (series.length + 2) > MAX_CHART_CACHE_CELLS) {
|
|
899
|
+
throw new OPFPptxError('invalid-chart-cache', 'Chart cache exceeds the 1,000,000-cell import limit; reduce its rows or series before importing.', {path: chartPart});
|
|
900
|
+
}
|
|
901
|
+
const rows = [];
|
|
902
|
+
// A chart without c:xVal plots against 1..n natively; a c:xVal gap stays a gap.
|
|
903
|
+
for (let index = 0; index < rowCount; index += 1) rows.push([String(index + 1), xRole === undefined ? index + 1 : xs[index] ?? null, ...values.map((row) => row[index] ?? null)]);
|
|
904
|
+
return { type: "scatter", data: { columns: ["Point", readChartCategoryHeading(entries, chartPart) ?? "X", ...names], rows } };
|
|
905
|
+
}
|
|
906
|
+
|
|
883
907
|
function firstChartNode(plotArea) {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
["c:doughnutChart", () => "doughnut"],
|
|
889
|
-
["c:areaChart", () => "area"],
|
|
890
|
-
["c:scatterChart", () => "scatter"],
|
|
891
|
-
["c:radarChart", () => "radar"]
|
|
892
|
-
];
|
|
893
|
-
for (const [key, type] of candidates) {
|
|
894
|
-
const node = asArray(plotArea[key])[0];
|
|
895
|
-
if (node) return { node, type: type(node) };
|
|
908
|
+
// Map the native construct back to the kept OPF chart type id (FF-22).
|
|
909
|
+
for (const element of NATIVE_CHART_ELEMENTS) {
|
|
910
|
+
const node = asArray(plotArea[`c:${element}`])[0];
|
|
911
|
+
if (node) return { node, type: chartTypeFromNative(element, node) };
|
|
896
912
|
}
|
|
897
913
|
return null;
|
|
898
914
|
}
|
|
@@ -1133,8 +1149,48 @@ function exportColor(entry, context, fallback) {
|
|
|
1133
1149
|
// PptxGenJS color for a document color reference: a theme scheme value when the
|
|
1134
1150
|
// reference names a scheme slot or role that the deck theme holds exactly,
|
|
1135
1151
|
// otherwise the resolved literal RRGGBB. Alpha stays in the caller's transparency.
|
|
1136
|
-
function nativeColor(reference, hex, context) {
|
|
1137
|
-
|
|
1152
|
+
function nativeColor(reference, hex, context, fallback) {
|
|
1153
|
+
if (fallback !== undefined && (reference === undefined || reference === null || reference === '')) return pptxColor(fallback);
|
|
1154
|
+
const value = schemeColorValue(reference, hex, context, {vendor: true});
|
|
1155
|
+
if (value) return value;
|
|
1156
|
+
// PptxGenJS cannot write hlink or folHlink: a reserved literal stands in and
|
|
1157
|
+
// is rewritten to the scheme color in the finished slide part.
|
|
1158
|
+
const link = schemeColorValue(reference, hex, context);
|
|
1159
|
+
return (link && context.linkSentinels?.[link]) ?? normalizeHex(hex);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// FF-24c: two literals no document color uses stand in for hlink and folHlink
|
|
1163
|
+
// (see nativeColor). When the document uses every candidate, those colors stay literal.
|
|
1164
|
+
const LINK_SENTINELS = [['FE01A0', 'FE01A1'], ['FE02B0', 'FE02B1'], ['FE03C0', 'FE03C1']];
|
|
1165
|
+
function linkSentinels(presentation) {
|
|
1166
|
+
const text = JSON.stringify(presentation).toUpperCase();
|
|
1167
|
+
const pair = LINK_SENTINELS.find(hexes => hexes.every(hex => !text.includes(hex)));
|
|
1168
|
+
return pair ? {hlink: pair[0], folHlink: pair[1]} : {};
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function writeLinkSentinels(xml, sentinels) {
|
|
1172
|
+
const values = Object.entries(sentinels ?? {});
|
|
1173
|
+
if (!values.length) return xml;
|
|
1174
|
+
return xml.replace(/<a:srgbClr val="([0-9A-F]{6})"(\/>|>[\s\S]*?<\/a:srgbClr>)/g, (node, hex, rest) => {
|
|
1175
|
+
const link = values.find(([, value]) => value === hex)?.[0];
|
|
1176
|
+
return link ? `<a:schemeClr val="${link}"${rest.replace(/<\/a:srgbClr>$/, '</a:schemeClr>')}` : node;
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// PptxGenJS color option: a scheme value it can emit (tx1, bg2, ...) or RRGGBB.
|
|
1181
|
+
function pptxColor(value) {
|
|
1182
|
+
return /^(?:tx[12]|bg[12])$/.test(value) ? value : normalizeHex(value);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Deck-level theme background for the slide master, so slides added in
|
|
1186
|
+
// PowerPoint match. The vendored layout already uses bg1, so only a theme
|
|
1187
|
+
// background on another slot (dark themes, light2, accents) changes the master.
|
|
1188
|
+
function masterBackground(context) {
|
|
1189
|
+
const deck = {...context, schemeOverride: false};
|
|
1190
|
+
const value = schemeBackgroundValue(deck.backgroundDefinition, deck);
|
|
1191
|
+
const fill = schemeBackgroundFill(deck.backgroundDefinition, deck);
|
|
1192
|
+
if (!value || value === 'bg1' || !fill || fill.includes('<a:alpha')) return null;
|
|
1193
|
+
return {fill, text: defaultTextSchemeValues(deck).text ?? normalizeHex(context.colors.text)};
|
|
1138
1194
|
}
|
|
1139
1195
|
|
|
1140
1196
|
// The theme part carries the deck-level scheme (slide overrides stay literal)
|
|
@@ -1225,7 +1281,7 @@ async function addSlide(pptx, presentation, opfSlide, slideIndex, context, optio
|
|
|
1225
1281
|
options.onDiagnostic?.({code: 'unresolved-asset', path: imagePath, message: 'The background image needs an embedded raster, a declared asset or a host imageResolver; the slide background color was exported instead.'});
|
|
1226
1282
|
}
|
|
1227
1283
|
}
|
|
1228
|
-
slide.color = slideContext.
|
|
1284
|
+
slide.color = slideContext.textColor;
|
|
1229
1285
|
if (opfSlide.hidden === true) slide.hidden = true;
|
|
1230
1286
|
|
|
1231
1287
|
const { widthInches, heightInches } = slideContext.dimensions;
|
|
@@ -1247,49 +1303,51 @@ async function addSlide(pptx, presentation, opfSlide, slideIndex, context, optio
|
|
|
1247
1303
|
if (geometry.slideImage) await addSlideImage(slide, presentation, geometry.slideImage, slideIndex, slideContext, options);
|
|
1248
1304
|
await addWatermark(slide, presentation, opfSlide, slideIndex, slideContext, context, options);
|
|
1249
1305
|
for (const item of geometry.items) {
|
|
1306
|
+
// Card text sits on a literal card fill, not the slide background: keep it literal.
|
|
1307
|
+
const itemContext = item.frameBox ? {...slideContext, textColor: slideContext.colors.text, mutedColor: slideContext.colors.mutedText} : slideContext;
|
|
1250
1308
|
const region = { x: item.box.x / 96, y: item.box.y / 96, w: item.box.width / 96, h: item.box.height / 96 };
|
|
1251
1309
|
if (item.frameBox) {
|
|
1252
1310
|
const frame=item.frameBox;
|
|
1253
1311
|
context.cardTags.set(`OPF card ${item.path}`,item.path);
|
|
1254
1312
|
const paint=value=>({color:normalizeHex(value),transparency:/^#[0-9a-f]{8}$/i.test(value)?(1-parseInt(value.slice(7),16)/255)*100:0});
|
|
1255
|
-
const surface=
|
|
1313
|
+
const surface=itemContext.colorScheme[isDarkHex(itemContext.colors.background)?'dark2':'light2']??`#${itemContext.colors.surface}`;
|
|
1256
1314
|
slide.addShape('roundRect',{x:frame.x/96,y:frame.y/96,w:frame.width/96,h:frame.height/96,
|
|
1257
1315
|
rectRadius:8*Math.min(widthInches,heightInches)/720,
|
|
1258
|
-
fill:paint(surface),line:{...paint(
|
|
1316
|
+
fill:paint(surface),line:{...paint(itemContext.colorScheme.accent5??`#${itemContext.colors.border}`),width:.75},objectName:`OPF card ${item.path}`});
|
|
1259
1317
|
}
|
|
1260
1318
|
if(['text','title','subtitle','tag'].includes(item.field)&&(item.text?.placement||item.text?.sourceLines)&&!item.text.richLines) {
|
|
1261
|
-
addMeasuredPayloadText(slide,item.value,item.box,
|
|
1319
|
+
addMeasuredPayloadText(slide,item.value,item.box,itemContext,options,{path:item.path,fit:item.text,textStyle:item.textStyle,sourceText:item.field==='text'&&!!item.text.sourceLines,align:alignmentFor(item),diagnosticsHandled:true,heading:['title','subtitle','tag'].includes(item.field)?item.field:undefined,color:item.field==='tag'?itemContext.colors.accent:itemContext.textColor});
|
|
1262
1320
|
} else if (["title", "subtitle", "tag"].includes(item.field)) {
|
|
1263
1321
|
// Estimated-font headings use one native text box, which still needs a
|
|
1264
1322
|
// role tag. Role recovery must not depend on outline measurement support.
|
|
1265
1323
|
const objectName = `OPF heading ${item.path} line 0`;
|
|
1266
1324
|
context.headingTags.set(objectName,{v:1,group:item.path,field:item.field,line:0,count:1});
|
|
1267
1325
|
slide.addText(item.text.lines.join("\n"), {
|
|
1268
|
-
...textBoxOptions(region,
|
|
1326
|
+
...textBoxOptions(region, itemContext, item.text.fontSize * 0.75),
|
|
1269
1327
|
...nativeFontOptions(item.textStyle),
|
|
1270
|
-
color: item.field === "tag" ?
|
|
1328
|
+
color: item.field === "tag" ? itemContext.colors.accent : itemContext.textColor,
|
|
1271
1329
|
align: alignmentFor(item),
|
|
1272
1330
|
objectName,
|
|
1273
1331
|
breakLine: false
|
|
1274
1332
|
});
|
|
1275
1333
|
} else if ((item.field === "items" || item.field === "bullets") && item.text?.listEntries) {
|
|
1276
|
-
addMeasuredList(slide,item.text,
|
|
1334
|
+
addMeasuredList(slide,item.text,itemContext,item.path);
|
|
1277
1335
|
} else if (item.field === "text" && item.text?.richLines) {
|
|
1278
1336
|
const alignment=item.text.placement?.alignment??alignmentFor(item)??'left';
|
|
1279
1337
|
for(const [index,line] of item.text.richLines.entries()){
|
|
1280
1338
|
const runs=line.fragments.map(fragment=>{
|
|
1281
|
-
const runColor=exportColor(fragment.run.color,
|
|
1282
|
-
const color=nativeColor(fragment.run.color,runColor,
|
|
1339
|
+
const runColor=exportColor(fragment.run.color,itemContext,itemContext.colors.text);
|
|
1340
|
+
const color=nativeColor(fragment.run.color,runColor,itemContext,itemContext.textColor);
|
|
1283
1341
|
return {text:fragment.text,options:{...nativeFontOptions(fragment.style),fontSize:fragment.fontSize*.75,color,underline:fragment.run.underline?{style:'sng',color}:undefined,strike:fragment.run.strikethrough?'sngStrike':undefined,baseline:fragment.baselineShift?-fragment.baselineShift/fragment.fontSize*2000:undefined,hyperlink:fragment.run.link&&/^(https?:|mailto:)/i.test(fragment.run.link)?{url:fragment.run.link}:undefined}};
|
|
1284
1342
|
});
|
|
1285
1343
|
const placed=item.text.placement?.lines[index],factor=alignment==='right'?1:alignment==='center'?.5:0;
|
|
1286
1344
|
const area=placed?{...region,x:(placed.x+line.width*factor-item.box.width*factor)/96,y:placed.y/96,h:placed.height/96}:{...region,y:region.y+line.y/96,h:line.height/96};
|
|
1287
|
-
if(runs.length)slide.addText(runs,{...textBoxOptions(area,
|
|
1345
|
+
if(runs.length)slide.addText(runs,{...textBoxOptions(area,itemContext,item.text.fontSize*.75),align:alignment,fit:'none',wrap:false,lineSpacingMultiple:1});
|
|
1288
1346
|
}
|
|
1289
1347
|
} else if (item.field === "text" && typeof item.value === "string") {
|
|
1290
|
-
slide.addText(item.text.lines.join("\n"), {...textBoxOptions(region,
|
|
1348
|
+
slide.addText(item.text.lines.join("\n"), {...textBoxOptions(region, itemContext, item.text.fontSize * 0.75),...nativeFontOptions(item.textStyle),align:alignmentFor(item)});
|
|
1291
1349
|
} else {
|
|
1292
|
-
await addPayload(slide, presentation, item.payload, region, item.path, { ...
|
|
1350
|
+
await addPayload(slide, presentation, item.payload, region, item.path, { ...itemContext, composition: item.composition, contentAlignment: alignmentFor(item) ?? "left" }, options, item.quoteLayout, item.codeLayout,item.metricLayout,item.timelineLayout);
|
|
1293
1351
|
}
|
|
1294
1352
|
}
|
|
1295
1353
|
// Core composes furniture above all content and opf-render paints it last, so
|
|
@@ -1321,7 +1379,17 @@ function resolveSlideContext(presentation, slide, baseContext, options, slideInd
|
|
|
1321
1379
|
|| Math.abs(resolved.dimensions.heightInches - baseContext.dimensions.heightInches) > 1e-6) {
|
|
1322
1380
|
throw new OPFPptxError("mixed-slide-dimensions", "PowerPoint requires one canvas size per presentation. Set dimensions on the deck or export this slide separately.");
|
|
1323
1381
|
}
|
|
1324
|
-
|
|
1382
|
+
const slideContext = { ...baseContext, backgroundDefinition: resolved.backgroundDefinition, colorScheme: resolved.colorScheme, fonts: resolved.fonts, colors: resolved.colors, variables: resolved.variables, imageFill: effective.design.imageFill ?? "fit" };
|
|
1383
|
+
// A slide-level color scheme (directly or through a slide theme) pins every
|
|
1384
|
+
// named color on that slide to literal sRGB; see schemeColorValue().
|
|
1385
|
+
const slideScheme = themeSlotColors(resolved.colorScheme), deckScheme = baseContext.themeColors ?? {};
|
|
1386
|
+
slideContext.schemeOverride = slide.design?.colorScheme !== undefined
|
|
1387
|
+
|| Object.keys({...slideScheme, ...deckScheme}).some(slot => slideScheme[slot] !== deckScheme[slot]);
|
|
1388
|
+
// Default and muted text follow the theme only where the background does.
|
|
1389
|
+
const text = defaultTextSchemeValues(slideContext);
|
|
1390
|
+
slideContext.textColor = text.text ?? resolved.colors.text;
|
|
1391
|
+
slideContext.mutedColor = text.muted ?? resolved.colors.mutedText;
|
|
1392
|
+
return slideContext;
|
|
1325
1393
|
}
|
|
1326
1394
|
|
|
1327
1395
|
function fieldToType(field) {
|
|
@@ -1389,10 +1457,10 @@ function addTextPayload(slide, value, region, context) {
|
|
|
1389
1457
|
slide.addText(stringifyText(value), textBoxOptions(region, context, 18));
|
|
1390
1458
|
}
|
|
1391
1459
|
|
|
1392
|
-
function richLineRuns(line,color,context) {
|
|
1460
|
+
function richLineRuns(line,color,native,context) {
|
|
1393
1461
|
const fallback=color.replace(/^#/,'');
|
|
1394
1462
|
return line.fragments.map(fragment=>{
|
|
1395
|
-
const runColor=exportColor(fragment.run.color,context,fallback),color=nativeColor(fragment.run.color,runColor,context);
|
|
1463
|
+
const runColor=exportColor(fragment.run.color,context,fallback),color=nativeColor(fragment.run.color,runColor,context,native);
|
|
1396
1464
|
return {text:fragment.text,options:{...nativeFontOptions(fragment.style),fontSize:fragment.fontSize*.75,color,underline:fragment.run.underline?{style:'sng',color}:undefined,strike:fragment.run.strikethrough?'sngStrike':undefined,baseline:fragment.baselineShift?-fragment.baselineShift/fragment.fontSize*2000:undefined,hyperlink:fragment.run.link&&/^(https?:|mailto:)/i.test(fragment.run.link)?{url:fragment.run.link}:undefined}};
|
|
1397
1465
|
});
|
|
1398
1466
|
}
|
|
@@ -1404,14 +1472,14 @@ function richLineRuns(line,color,context) {
|
|
|
1404
1472
|
function addMeasuredList(slide,fit,context,path) {
|
|
1405
1473
|
let lineNumber=0;
|
|
1406
1474
|
for(const entry of fit.listEntries){
|
|
1407
|
-
const addLines=(text,box,color,withBullet)=>{
|
|
1475
|
+
const addLines=(text,box,color,native,withBullet)=>{
|
|
1408
1476
|
text.richLines.forEach((line,index)=>{
|
|
1409
1477
|
const first=withBullet&&index===0,level=Math.min(8,entry.level),inset=first?entry.marker.indent*(level+1):0;
|
|
1410
1478
|
const region={x:(box.x-inset)/96,y:(box.y+line.y)/96,w:(box.width+inset)/96,h:line.height/96};
|
|
1411
1479
|
const objectName=`OPF list ${path} line ${lineNumber++}`;
|
|
1412
|
-
if(first)context.listMarkers.set(objectName,{fontFamily:entry.marker.style.fontFamily,fontSize:entry.marker.fontSize*.75,color:
|
|
1480
|
+
if(first)context.listMarkers.set(objectName,{fontFamily:entry.marker.style.fontFamily,fontSize:entry.marker.fontSize*.75,color:pptxColor(context.textColor)});
|
|
1413
1481
|
const paragraph=first?{bullet:{characterCode:entry.marker.text.codePointAt(0).toString(16).padStart(4,'0'),indent:entry.marker.indent*.75},indentLevel:level}:{bullet:false};
|
|
1414
|
-
const runs=richLineRuns(line,color,context);
|
|
1482
|
+
const runs=richLineRuns(line,color,native,context);
|
|
1415
1483
|
if(!runs.length)runs.push({text:'',options:{}});
|
|
1416
1484
|
// Keep paragraph intent identical across runs. ZIP normalization below
|
|
1417
1485
|
// removes the duplicate paragraph-property nodes emitted by PptxGenJS.
|
|
@@ -1419,8 +1487,8 @@ function addMeasuredList(slide,fit,context,path) {
|
|
|
1419
1487
|
slide.addText(runs,{...textBoxOptions(region,context,text.fontSize*.75),fontFace:nativeFontOptions(entry.marker.style).fontFace,objectName,align:'left',fit:'none',wrap:false,lineSpacingMultiple:1,...paragraph});
|
|
1420
1488
|
});
|
|
1421
1489
|
};
|
|
1422
|
-
addLines(entry.text,entry.textBox,context.colors.text,true);
|
|
1423
|
-
if(entry.description)addLines(entry.description,entry.descriptionBox,context.colors.mutedText,false);
|
|
1490
|
+
addLines(entry.text,entry.textBox,context.colors.text,context.textColor,true);
|
|
1491
|
+
if(entry.description)addLines(entry.description,entry.descriptionBox,context.colors.mutedText,context.mutedColor,false);
|
|
1424
1492
|
}
|
|
1425
1493
|
}
|
|
1426
1494
|
|
|
@@ -1440,7 +1508,7 @@ function addListPayload(slide, items, region, context) {
|
|
|
1440
1508
|
options: {
|
|
1441
1509
|
bullet: { type: "bullet", indent: 14 + level * 14 },
|
|
1442
1510
|
breakLine: index < list.length - 1 || Boolean(isPlainObject(item) && item.description),
|
|
1443
|
-
color: context.
|
|
1511
|
+
color: context.textColor,
|
|
1444
1512
|
fontFace: context.fonts.body,
|
|
1445
1513
|
fontSize: 15,
|
|
1446
1514
|
hanging: 4 + level * 10
|
|
@@ -1451,7 +1519,7 @@ function addListPayload(slide, items, region, context) {
|
|
|
1451
1519
|
text: stringifyText(item.description),
|
|
1452
1520
|
options: {
|
|
1453
1521
|
breakLine: index < list.length - 1,
|
|
1454
|
-
color: context.
|
|
1522
|
+
color: context.mutedColor,
|
|
1455
1523
|
fontFace: context.fonts.body,
|
|
1456
1524
|
fontSize: 11,
|
|
1457
1525
|
margin: [0, 0, 0, 18 + level * 14]
|
|
@@ -1553,7 +1621,7 @@ function addChartPayload(slide, chart, region, context, options = {}, path = "ch
|
|
|
1553
1621
|
addPlaceholderPayload(slide, "Chart", chartData.summary, region, context);
|
|
1554
1622
|
return;
|
|
1555
1623
|
}
|
|
1556
|
-
|
|
1624
|
+
for (const {adaptation, message} of chartData.adaptations ?? []) options.onDiagnostic?.({code: "chart-data-adapted", path, message, adaptation});
|
|
1557
1625
|
|
|
1558
1626
|
// Keep the resolved palette surface (including alpha) explicit in native
|
|
1559
1627
|
// chart/plot areas, so inherited labels are assessed against their own panel.
|
|
@@ -1563,15 +1631,16 @@ function addChartPayload(slide, chart, region, context, options = {}, path = "ch
|
|
|
1563
1631
|
const fill = {color:normalizeHex(panelFill),transparency};
|
|
1564
1632
|
const objectName = `OPF chart ${context.chartHeadings.size + 1}`;
|
|
1565
1633
|
const circular = chartData.type === 'pie' || chartData.type === 'doughnut';
|
|
1566
|
-
context.chartHeadings.set(objectName,{heading:chartData.type === 'scatter' ? undefined : chartData.heading ?? chart.data.columns[0],labelColor});
|
|
1634
|
+
context.chartHeadings.set(objectName,{heading:chartData.type === 'scatter' ? undefined : chartData.heading ?? chart.data.columns[0],labelColor,spec:chartData.spec});
|
|
1567
1635
|
context.chartFonts.set(objectName,{heading:context.fonts.heading,body:context.fonts.body});
|
|
1636
|
+
const percent = chartData.spec.grouping === 'percentStacked';
|
|
1568
1637
|
slide.addChart(chartData.type, chartData.series, {
|
|
1569
1638
|
objectName,
|
|
1570
1639
|
x: region.x,
|
|
1571
1640
|
y: region.y,
|
|
1572
1641
|
w: region.w,
|
|
1573
1642
|
h: region.h,
|
|
1574
|
-
showLegend: circular || chartData.series.length > 1,
|
|
1643
|
+
showLegend: circular || chartData.series.length > (chartData.type === 'scatter' ? 2 : 1),
|
|
1575
1644
|
showTitle: false,
|
|
1576
1645
|
chartColors: CHART_COLORS.map(color=>normalizeHex(chartColorForFill(panelFill,`#${color}`))),
|
|
1577
1646
|
chartArea: {fill:{...fill},roundedCorners:false},
|
|
@@ -1589,7 +1658,12 @@ function addChartPayload(slide, chart, region, context, options = {}, path = "ch
|
|
|
1589
1658
|
showValue: false,
|
|
1590
1659
|
valGridLine: { color: context.colors.border, transparency: 30, size: 1 },
|
|
1591
1660
|
barDir: chartData.barDir,
|
|
1592
|
-
barGrouping: chartData.barGrouping
|
|
1661
|
+
barGrouping: chartData.barGrouping,
|
|
1662
|
+
...(percent ? { valAxisLabelFormatCode: '0%' } : {}),
|
|
1663
|
+
...(chartData.spec.markers === undefined ? {} : { lineDataSymbol: chartData.spec.markers ? 'circle' : 'none' }),
|
|
1664
|
+
...(chartData.spec.radarStyle ? { radarStyle: chartData.spec.radarStyle } : {}),
|
|
1665
|
+
// ScatterWithMarkers: markers only, no connecting line.
|
|
1666
|
+
...(chartData.type === 'scatter' ? { lineSize: 0, lineDataSymbol: 'circle' } : {})
|
|
1593
1667
|
});
|
|
1594
1668
|
}
|
|
1595
1669
|
|
|
@@ -1612,8 +1686,13 @@ function addTablePayload(slide, table, region, context, options, path) {
|
|
|
1612
1686
|
const cellStyle = cell.style ?? {};
|
|
1613
1687
|
const defaultFillHex = header ? context.colors.accent : context.colors.surface;
|
|
1614
1688
|
const baseFill = exportColor(cellStyle.fill ?? defaultFillHex, context, defaultFillHex);
|
|
1689
|
+
// FF-24c: engine chrome resolves from scheme roles (header accent1, body surface), so it follows the deck theme like a named fill.
|
|
1690
|
+
const fillReference = cellStyle.fill ?? (header ? 'accent1' : 'surface');
|
|
1691
|
+
const fillValue = schemeColorValue(fillReference, baseFill, context);
|
|
1615
1692
|
const inheritedText = textColorForFill(`#${baseFill}`, header ? "#FFFFFF" : `#${context.colors.text}`);
|
|
1616
1693
|
const baseColor = exportColor(cellStyle.color ?? inheritedText.replace(/^#/, ""), context, inheritedText.replace(/^#/, ""));
|
|
1694
|
+
// 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.
|
|
1695
|
+
const defaultText = cellStyle.color === undefined ? tableTextSchemeValue(fillValue, baseColor, context) : undefined;
|
|
1617
1696
|
const alpha = value => value.length === 8 ? (1 - parseInt(value.slice(6), 16) / 255) * 100 : 0;
|
|
1618
1697
|
const text = stringifyText(cell.value), style = cell.textStyle;
|
|
1619
1698
|
const fragments = rich ? fit.richLines.flatMap(line => line.fragments) : [];
|
|
@@ -1622,7 +1701,7 @@ function addTablePayload(slide, table, region, context, options, path) {
|
|
|
1622
1701
|
const fragment = fragments.find(item => item.runIndex === index);
|
|
1623
1702
|
const runStyle = fragment?.style ?? resolveTextStyle({...style,fontFamily:run.fontFamily ?? style.fontFamily,fontWeight:run.bold === undefined ? style.fontWeight : run.bold ? 700 : 400,italic:run.italic ?? style.italic}, options.textMeasurement);
|
|
1624
1703
|
const rawColor = run.color !== undefined && run.color !== '' ? exportColor(run.color, context, baseColor) : baseColor;
|
|
1625
|
-
const color = nativeColor(run.color !== undefined && run.color !== '' ? run.color : cellStyle.color, rawColor, context), transparency = rawColor.length === 8 ? (1 - parseInt(rawColor.slice(6), 16) / 255) * 100 : 0;
|
|
1704
|
+
const color = nativeColor(run.color !== undefined && run.color !== '' ? run.color : cellStyle.color, rawColor, context, defaultText), transparency = rawColor.length === 8 ? (1 - parseInt(rawColor.slice(6), 16) / 255) * 100 : 0;
|
|
1626
1705
|
const runOptions = {
|
|
1627
1706
|
...nativeFontOptions(runStyle),fontSize:fragment ? fragment.fontSize * .75 : fit.fontSize * .75,
|
|
1628
1707
|
underline:run.underline ? {style:'sng',color} : undefined,strike:run.strikethrough ? 'sngStrike' : undefined,
|
|
@@ -1652,17 +1731,17 @@ function addTablePayload(slide, table, region, context, options, path) {
|
|
|
1652
1731
|
valign: cellStyle.verticalAlign ?? 'top',
|
|
1653
1732
|
...(cell.colSpan > 1 ? {colspan:cell.colSpan} : {}),
|
|
1654
1733
|
...(cell.rowSpan > 1 ? {rowspan:cell.rowSpan} : {}),
|
|
1655
|
-
color: nativeColor(cellStyle.color, baseColor, context),
|
|
1734
|
+
color: nativeColor(cellStyle.color, baseColor, context, defaultText),
|
|
1656
1735
|
// Rich runs carry resolved alpha. A translucent cell default would
|
|
1657
1736
|
// overwrite an explicit opaque run because PptxGenJS inherits falsy 0.
|
|
1658
1737
|
...(cellStyle.color && !rich ? {transparency:alpha(baseColor)} : {}),
|
|
1659
|
-
fill: { color: nativeColor(
|
|
1738
|
+
fill: { color: nativeColor(fillReference, baseFill, context), ...(cellStyle.fill ? {transparency:alpha(baseFill)} : {}) },
|
|
1660
1739
|
},
|
|
1661
1740
|
};
|
|
1662
1741
|
}));
|
|
1663
1742
|
const objectName = `OPF table ${context.tableHeaders.size + 1}`;
|
|
1664
1743
|
context.tableHeaders.set(objectName, hasHeaders);
|
|
1665
|
-
if (layout.rows.some(row => row.cells.some(cell => Object.keys(cell.style ?? {}).length))) context.tableCells.set(objectName, {layout, scale, defaultBorder:{color:"
|
|
1744
|
+
if (layout.rows.some(row => row.cells.some(cell => Object.keys(cell.style ?? {}).length))) context.tableCells.set(objectName, {layout, scale, context, defaultBorder:{color:"accent5",width:1/scale}});
|
|
1666
1745
|
slide.addTable(rows, {
|
|
1667
1746
|
objectName,
|
|
1668
1747
|
x: region.x,
|
|
@@ -1678,7 +1757,7 @@ function addTablePayload(slide, table, region, context, options, path) {
|
|
|
1678
1757
|
fontFace: context.fonts.body,
|
|
1679
1758
|
fontSize: 15 * scale * 0.75,
|
|
1680
1759
|
color: context.colors.text,
|
|
1681
|
-
border: { type: "solid", color: context.colors.border, pt: 0.75 },
|
|
1760
|
+
border: { type: "solid", color: nativeColor("accent5", context.colors.border, context), pt: 0.75 },
|
|
1682
1761
|
margin: [6 * scale, 7.5 * scale, 3 * scale, 7.5 * scale],
|
|
1683
1762
|
valign: "top"
|
|
1684
1763
|
});
|
|
@@ -1720,7 +1799,7 @@ function addMeasuredPayloadText(slide, text, box, context, options, config) {
|
|
|
1720
1799
|
else if(config.sourceText)context.plainTextTags.set(objectName,{v:1,group:config.path,line:index,count:fit.lines.length,...boundary});
|
|
1721
1800
|
const area=placed?{x:(placed.x+placed.width*factor-box.width*factor)/96,y:(placed.baseline-fit.fontSize)/96,w:box.width/96,h:placed.height/96}:{x:box.x/96,y:(box.y+index*fit.lineHeight)/96,w:box.width/96,h:fit.lineHeight/96};
|
|
1722
1801
|
// A run-level link keeps the muted, non-underlined furniture look of the preview (hlinkClr=tx, u=none).
|
|
1723
|
-
const lineColor =
|
|
1802
|
+
const lineColor = pptxColor(config.color ?? context.textColor);
|
|
1724
1803
|
slide.addText(link?.href && line ? [{text: line, options: {hyperlink: {url: link.href}, color: lineColor, underline: {style: 'none'}}}] : line, {
|
|
1725
1804
|
...textBoxOptions(area, context, fit.fontSize * .75),
|
|
1726
1805
|
...nativeFontOptions(style),
|
|
@@ -1768,7 +1847,7 @@ async function addFurniture(slide,presentation,source,layout,context,options,sli
|
|
|
1768
1847
|
if(marker) staticDates.set(index,marker);
|
|
1769
1848
|
}
|
|
1770
1849
|
}
|
|
1771
|
-
addMeasuredPayloadText(slide,part.text,part.box,context,options,{path:part.path,fit:part.fit,textStyle:part.style,align:part.alignment,diagnosticsHandled:true,color:context.
|
|
1850
|
+
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});
|
|
1772
1851
|
}
|
|
1773
1852
|
}
|
|
1774
1853
|
const manifest = furnitureManifest(presentation, source, layout, slideIndex, staticDates);
|
|
@@ -1823,7 +1902,7 @@ function addMetricPayload(slide,value,layout,context,path) {
|
|
|
1823
1902
|
slide.addText(part.text.slice(line.start,line.end),{
|
|
1824
1903
|
...textBoxOptions({x:(anchor-part.box.width*factor)/96,y:(origin.baseline-part.fit.fontSize)/96,w:part.box.width/96,h:part.fit.lineHeight/96},context,part.fit.fontSize*.75),
|
|
1825
1904
|
...nativeFontOptions(part.style),
|
|
1826
|
-
color:part.role==='value'?context.colors.accent:context.
|
|
1905
|
+
color:part.role==='value'?context.colors.accent:context.textColor,align:layout.alignment,fit:'none',wrap:false,lineSpacingMultiple:1,
|
|
1827
1906
|
tabStops:tabStops.length?tabStops:undefined,objectName,
|
|
1828
1907
|
});
|
|
1829
1908
|
}
|
|
@@ -1836,7 +1915,7 @@ function addQuotePayload(slide, layout, context, options, path) {
|
|
|
1836
1915
|
if (!part.fit) throw new OPFPptxError('layout-overflow', 'Quote content has no usable internal space; increase its cell size before exporting.', {path:part.path,issues:layout.diagnostics});
|
|
1837
1916
|
addMeasuredPayloadText(slide,part.text,part.box,context,options,{
|
|
1838
1917
|
path:part.path,fit:part.fit,textStyle:part.style,diagnosticsHandled:true,
|
|
1839
|
-
color:part.role==='footer'?context.
|
|
1918
|
+
color:part.role==='footer'?context.mutedColor:context.textColor,
|
|
1840
1919
|
});
|
|
1841
1920
|
}
|
|
1842
1921
|
}
|
|
@@ -1972,7 +2051,7 @@ function addPlaceholderPayload(slide, label, description, region, context) {
|
|
|
1972
2051
|
margin: 0,
|
|
1973
2052
|
fontFace: context.fonts.body,
|
|
1974
2053
|
fontSize: 11,
|
|
1975
|
-
color: context.
|
|
2054
|
+
color: context.mutedColor,
|
|
1976
2055
|
fit: "shrink",
|
|
1977
2056
|
valign: "mid",
|
|
1978
2057
|
align: "center"
|
|
@@ -2040,7 +2119,7 @@ function textBoxOptions(region, context, fontSize) {
|
|
|
2040
2119
|
lineSpacingMultiple: 1.22,
|
|
2041
2120
|
fontFace: context.fonts.body,
|
|
2042
2121
|
fontSize,
|
|
2043
|
-
color: context.
|
|
2122
|
+
color: context.textColor,
|
|
2044
2123
|
breakLine: false,
|
|
2045
2124
|
fit: "shrink",
|
|
2046
2125
|
valign: "top"
|
|
@@ -2054,7 +2133,7 @@ function textRuns(value, context, fallbackFontSize) {
|
|
|
2054
2133
|
return {
|
|
2055
2134
|
text: run,
|
|
2056
2135
|
options: {
|
|
2057
|
-
color: context.
|
|
2136
|
+
color: context.textColor,
|
|
2058
2137
|
fontFace: context.fonts.body,
|
|
2059
2138
|
fontSize: fallbackFontSize
|
|
2060
2139
|
}
|
|
@@ -2065,9 +2144,9 @@ function textRuns(value, context, fallbackFontSize) {
|
|
|
2065
2144
|
options: {
|
|
2066
2145
|
bold: run?.bold,
|
|
2067
2146
|
italic: run?.italic,
|
|
2068
|
-
underline: run?.underline ? { style: "sng", color: nativeColor(run?.color, exportColor(run?.color, context, context.colors.text), context) } : undefined,
|
|
2147
|
+
underline: run?.underline ? { style: "sng", color: nativeColor(run?.color, exportColor(run?.color, context, context.colors.text), context, context.textColor) } : undefined,
|
|
2069
2148
|
strike: run?.strikethrough ? "sngStrike" : undefined,
|
|
2070
|
-
color: nativeColor(run?.color, exportColor(run?.color, context, context.colors.text), context),
|
|
2149
|
+
color: nativeColor(run?.color, exportColor(run?.color, context, context.colors.text), context, context.textColor),
|
|
2071
2150
|
fontFace: run?.fontFamily ?? context.fonts.body,
|
|
2072
2151
|
fontSize: run?.fontSize ?? fallbackFontSize,
|
|
2073
2152
|
superscript: run?.superscript,
|
|
@@ -2093,7 +2172,16 @@ function toPptxChartData(chart) {
|
|
|
2093
2172
|
}
|
|
2094
2173
|
if (data.rows.length === 0) return unplottable("no-rows", "The chart has no data rows.", "The chart data has no rows.");
|
|
2095
2174
|
if (data.columns.length === 0) return unplottable("no-columns", "The chart has no data columns.", "The chart data has no columns.");
|
|
2096
|
-
const
|
|
2175
|
+
const resolved = resolveChartType(chart.type).spec;
|
|
2176
|
+
// Chartex types (treemap, box & whisker, waterfall, ...) have no classic construct here: they export as a clustered column chart, never silently.
|
|
2177
|
+
const chartex = resolved.family === 'chartex';
|
|
2178
|
+
const spec = chartex ? CHARTEX_FALLBACK : resolved;
|
|
2179
|
+
const typeName = stringifyText(chart.type);
|
|
2180
|
+
const adaptations = [];
|
|
2181
|
+
if (chartex) {
|
|
2182
|
+
adaptations.push({adaptation: "chartex-fallback", message: `The '${typeName}' chart is a PowerPoint extension (chartex) chart that this exporter does not write; its data is exported as a native clustered column chart instead.`});
|
|
2183
|
+
}
|
|
2184
|
+
const mapped = {type: spec.pptx, spec, barDir: spec.barDir, barGrouping: spec.pptx === 'bar' || spec.pptx === 'area' ? spec.grouping : undefined};
|
|
2097
2185
|
if (data.columns.length === 1) {
|
|
2098
2186
|
const heading = stringifyText(data.columns[0]);
|
|
2099
2187
|
const cell = (row) => Array.isArray(row) ? row[0] : row;
|
|
@@ -2107,10 +2195,12 @@ function toPptxChartData(chart) {
|
|
|
2107
2195
|
if (String(chart.type ?? "").toLowerCase() === "histogram") {
|
|
2108
2196
|
const bins = histogramBins(points.map((point) => point.value));
|
|
2109
2197
|
return {
|
|
2110
|
-
type: "bar", barDir: "col", barGrouping: "clustered", heading: "Bin",
|
|
2198
|
+
type: "bar", spec: CHARTEX_FALLBACK, barDir: "col", barGrouping: "clustered", heading: "Bin",
|
|
2111
2199
|
series: [{name: "Frequency", labels: bins.map((bin) => bin.label), values: bins.map((bin) => bin.count)}],
|
|
2112
|
-
|
|
2113
|
-
|
|
2200
|
+
adaptations: [{
|
|
2201
|
+
adaptation: "histogram-binned",
|
|
2202
|
+
message: `The histogram's single data column '${heading}' (${points.length} values${skippedNote}) was binned into ${bins.length} equal-width bins and exported as a column chart of the counts; PowerPoint's own histogram chart is not exported, and the binned counts do not restore the raw values on re-import.`
|
|
2203
|
+
}]
|
|
2114
2204
|
};
|
|
2115
2205
|
}
|
|
2116
2206
|
// Rows keep their own row numbers, so a skipped cell leaves a gap in the numbering.
|
|
@@ -2119,22 +2209,31 @@ function toPptxChartData(chart) {
|
|
|
2119
2209
|
const series = mapped.type === "scatter"
|
|
2120
2210
|
? [{name: "Row", labels, values: points.map((point) => point.row)}, {name: heading, labels, values: numbers}]
|
|
2121
2211
|
: [{name: heading, labels, values: numbers}];
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
message: `The chart's single data column '${heading}' has no category column, so its ${points.length} values${skippedNote} are plotted against their row numbers.`
|
|
2125
|
-
};
|
|
2212
|
+
adaptations.push({adaptation: "row-numbers", message: `The chart's single data column '${heading}' has no category column, so its ${points.length} values${skippedNote} are plotted against their row numbers.`});
|
|
2213
|
+
return {...mapped, series, heading: "Row", adaptations};
|
|
2126
2214
|
}
|
|
2127
2215
|
|
|
2128
2216
|
const labels = data.rows.map((row) => stringifyText(row?.[0]));
|
|
2129
|
-
|
|
2217
|
+
let series = data.columns.slice(1).map((name, seriesIndex) => ({
|
|
2130
2218
|
name: stringifyText(name),
|
|
2131
2219
|
labels,
|
|
2132
2220
|
values: data.rows.map((row) => numericValue(row?.[seriesIndex + 1]))
|
|
2133
2221
|
}));
|
|
2134
|
-
|
|
2135
|
-
|
|
2222
|
+
if (spec.family === 'circular' && series.length > 1) {
|
|
2223
|
+
// A pie or doughnut plots one series; name the ones left out instead of dropping them silently.
|
|
2224
|
+
const dropped = series.slice(1).map((entry) => `'${entry.name}'`);
|
|
2225
|
+
adaptations.push({adaptation: "series-dropped", message: `The ${typeName} chart plots one series, so its first series '${series[0].name}' is exported and the other ${dropped.length} (${dropped.join(", ")}) ${dropped.length === 1 ? "is" : "are"} not.`});
|
|
2226
|
+
series = series.slice(0, 1);
|
|
2227
|
+
}
|
|
2228
|
+
if (spec.family === 'xy' && series.length === 1) {
|
|
2229
|
+
// [Point, X, Y...] carries its own X column; a lone value column is plotted against the row numbers.
|
|
2230
|
+
series = [{name: 'X', labels, values: data.rows.map((_, index) => index + 1)}, ...series];
|
|
2231
|
+
adaptations.push({adaptation: "row-numbers", message: `The scatter chart has no X column (a point label column and one value column), so its ${data.rows.length} values are plotted against their row numbers.`});
|
|
2232
|
+
}
|
|
2233
|
+
return {type: spec.pptx, spec, series, barDir: spec.barDir, barGrouping: spec.pptx === 'bar' || spec.pptx === 'area' ? spec.grouping : undefined, adaptations};
|
|
2136
2234
|
}
|
|
2137
2235
|
|
|
2236
|
+
|
|
2138
2237
|
/**
|
|
2139
2238
|
* Equal-width bins (Sturges' count, at most 50) from the minimum to the maximum; every bin but the last is [low, high).
|
|
2140
2239
|
* Arithmetic stays finite for any finite input (values of +-1e308 and denormals included): the bin edges are
|
|
@@ -2160,28 +2259,6 @@ function histogramBins(values) {
|
|
|
2160
2259
|
}
|
|
2161
2260
|
}
|
|
2162
2261
|
|
|
2163
|
-
function mapChartType(type) {
|
|
2164
|
-
const normalized = String(type ?? "").toLowerCase();
|
|
2165
|
-
if (normalized.includes("pie")) return { type: "pie" };
|
|
2166
|
-
if (normalized.includes("doughnut") || normalized.includes("donut")) return { type: "doughnut" };
|
|
2167
|
-
if (normalized.includes("area")) return { type: "area" };
|
|
2168
|
-
if (normalized.includes("line") || normalized.includes("sparkline")) return { type: "line" };
|
|
2169
|
-
if (normalized.includes("scatter")) return { type: "scatter" };
|
|
2170
|
-
if (normalized.includes("radar")) return { type: "radar" };
|
|
2171
|
-
if (normalized.includes("bar")) {
|
|
2172
|
-
return {
|
|
2173
|
-
type: "bar",
|
|
2174
|
-
barDir: "bar",
|
|
2175
|
-
barGrouping: normalized.includes("stacked") ? "stacked" : "clustered"
|
|
2176
|
-
};
|
|
2177
|
-
}
|
|
2178
|
-
return {
|
|
2179
|
-
type: "bar",
|
|
2180
|
-
barDir: "col",
|
|
2181
|
-
barGrouping: normalized.includes("stacked") ? "stacked" : "clustered"
|
|
2182
|
-
};
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
2262
|
async function resolveImage(asset, presentation, options, path) {
|
|
2186
2263
|
const assetObject = dereferenceAsset(asset, presentation, path);
|
|
2187
2264
|
const src = typeof assetObject === "string" ? assetObject : assetObject?.src;
|
|
@@ -2400,8 +2477,9 @@ async function normalizePptxZip(raw, context) {
|
|
|
2400
2477
|
if(!context.chartHeadings.has(name))continue;
|
|
2401
2478
|
const id=frame.match(/<c:chart\b[^>]*\br:id="([^"]+)"/)?.[1],chartPart=relationships.get(id)?.path;
|
|
2402
2479
|
if(!chartPart)throw new OPFPptxError('packaging-failed','Generated chart relationship is missing.');
|
|
2403
|
-
const {heading,labelColor}=context.chartHeadings.get(name);
|
|
2480
|
+
const {heading,labelColor,spec}=context.chartHeadings.get(name);
|
|
2404
2481
|
if(heading!==undefined)writeChartCategoryHeading(entries,chartPart,heading);
|
|
2482
|
+
entries[chartPart]=encodeText(applyChartConstruct(decodeText(entries[chartPart]),spec));
|
|
2405
2483
|
// PptxGenJS hardcodes a black fallback in pie/doughnut label properties.
|
|
2406
2484
|
// Normalize only our generated chart text styles; point/series fills stay intact.
|
|
2407
2485
|
entries[chartPart]=encodeText(decodeText(entries[chartPart]).replace(/<c:txPr>[\s\S]*?<\/c:txPr>/g,properties=>properties.replace(/<a:solidFill>[\s\S]*?<\/a:solidFill>/g,()=>`<a:solidFill><a:srgbClr val="${labelColor}"/></a:solidFill>`)));
|
|
@@ -2559,8 +2637,11 @@ function normalizePartBytes(path, bytes, context, renameMaps, entries, imageMeta
|
|
|
2559
2637
|
xml = xml.replace('</Types>', `${overrides}</Types>`);
|
|
2560
2638
|
}
|
|
2561
2639
|
if (/^ppt\/slideMasters\/slideMaster\d+\.xml$/.test(path)) xml = themeMasterBulletFonts(xml);
|
|
2640
|
+
if (context.masterBackground && path === 'ppt/slideMasters/slideMaster1.xml') xml = writeMasterBackground(xml, context.masterBackground);
|
|
2641
|
+
if (context.masterBackground && /^ppt\/slideLayouts\/slideLayout\d+\.xml$/.test(path)) xml = inheritLayoutBackground(xml);
|
|
2562
2642
|
if (path === 'ppt/theme/theme1.xml') xml = writeThemeColors(xml, {colors: context.themeColors, schemeName: context.schemeName, themeName: context.themeName});
|
|
2563
2643
|
if (/^ppt\/slides\/slide\d+\.xml$/.test(path)) {
|
|
2644
|
+
xml = writeLinkSentinels(xml, context.linkSentinels);
|
|
2564
2645
|
const fill = context.backgroundFills.get(path);
|
|
2565
2646
|
if (typeof fill === 'string') xml = xml.replace(/<p:bg>[\s\S]*?<\/p:bg>/, `<p:bg><p:bgPr>${fill}<a:effectLst/></p:bgPr></p:bg>`);
|
|
2566
2647
|
else if (fill?.image) {
|
|
@@ -2631,10 +2712,10 @@ function normalizePartBytes(path, bytes, context, renameMaps, entries, imageMeta
|
|
|
2631
2712
|
for (const [edge, native] of [['left','lnL'],['right','lnR'],['top','lnT'],['bottom','lnB']]) {
|
|
2632
2713
|
const border = style.borders?.[edge];
|
|
2633
2714
|
if (!border) continue;
|
|
2634
|
-
const borderHex = exportColor(border.color,
|
|
2715
|
+
const slideContext = table.context, borderHex = exportColor(border.color, slideContext, slideContext.colors.border);
|
|
2635
2716
|
const borderOpacity = borderHex.length === 8 ? parseInt(borderHex.slice(6), 16) / 255 : 1;
|
|
2636
|
-
const borderScheme = borderOpacity === 1 ? schemeColorValue(border.color, borderHex,
|
|
2637
|
-
const fill = border.width === 0 ? '<a:noFill/>' : borderScheme ? `<a:solidFill><a:schemeClr val="${borderScheme}"/></a:solidFill>` : nativeBackgroundFill({type:'solid',color:'#' + borderHex.slice(0, 6), opacity: borderOpacity},{width:1,height:1},
|
|
2717
|
+
const borderScheme = borderOpacity === 1 ? schemeColorValue(border.color, borderHex, slideContext) : undefined;
|
|
2718
|
+
const fill = border.width === 0 ? '<a:noFill/>' : borderScheme ? `<a:solidFill><a:schemeClr val="${borderScheme}"/></a:solidFill>` : nativeBackgroundFill({type:'solid',color:'#' + borderHex.slice(0, 6), opacity: borderOpacity},{width:1,height:1}, slideContext.colors.border);
|
|
2638
2719
|
const dash = {solid:'solid',dash:'dash',dot:'sysDot'}[border.dash ?? 'solid'];
|
|
2639
2720
|
const line = `<a:${native} w="${Math.round(border.width * table.scale * 9525)}" cap="flat" cmpd="sng" algn="ctr">${fill}<a:prstDash val="${dash}"/></a:${native}>`;
|
|
2640
2721
|
const existing = new RegExp(`<a:${native}\\b[^>]*>[\\s\\S]*?<\\/a:${native}>`);
|
|
@@ -2670,7 +2751,7 @@ function normalizePartBytes(path, bytes, context, renameMaps, entries, imageMeta
|
|
|
2670
2751
|
const marker=context.listMarkers.get(shape.match(/name="(OPF list [^"]* line \d+)"/)?.[1]);
|
|
2671
2752
|
if(!marker)return shape;
|
|
2672
2753
|
const family=marker.fontFamily.replace(/[&<>"']/g,char=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[char]));
|
|
2673
|
-
return shape.replace(/<a:buSzPct val="100000"\/>/g,`<a:buClr
|
|
2754
|
+
return shape.replace(/<a:buSzPct val="100000"\/>/g,`<a:buClr>${solidColorXml(marker.color)}</a:buClr><a:buSzPts val="${Math.round(marker.fontSize*100)}"/><a:buFont typeface="${family}"/>`);
|
|
2674
2755
|
});
|
|
2675
2756
|
// PptxGenJS gives shapes no alternative text. An unavailable image's panel
|
|
2676
2757
|
// carries the accessible name the preview gives its group.
|
package/dist/theme-colors.js
CHANGED
|
@@ -102,7 +102,9 @@ function backgroundSlot(definition) {
|
|
|
102
102
|
* `vendor` limits the result to scheme values PptxGenJS 4.0.1 can emit.
|
|
103
103
|
*/
|
|
104
104
|
export function schemeColorValue(reference, hex, context, {vendor = false} = {}) {
|
|
105
|
-
|
|
105
|
+
// A slide with its own color scheme is pinned to literal colors: a theme edit
|
|
106
|
+
// in PowerPoint must not partially recolor it, even where a value coincides.
|
|
107
|
+
if (typeof reference !== 'string' || context.schemeOverride) return undefined;
|
|
106
108
|
const name = reference.trim();
|
|
107
109
|
const resolved = sixDigitHex(hex);
|
|
108
110
|
const deck = context.themeColors;
|
|
@@ -120,17 +122,73 @@ export function schemeColorValue(reference, hex, context, {vendor = false} = {})
|
|
|
120
122
|
return undefined;
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
function backgroundOpacity(definition) {
|
|
126
|
+
const opacity = typeof definition === 'object' && Number.isFinite(definition?.opacity) ? definition.opacity : 1;
|
|
127
|
+
return Math.max(0, Math.min(1, opacity));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Scheme value of a theme-slot slide background, or undefined when the background stays literal. */
|
|
131
|
+
export function schemeBackgroundValue(definition, context) {
|
|
132
|
+
const slot = backgroundSlot(definition);
|
|
133
|
+
return slot ? schemeColorValue(slot, context.colors?.background, context) : undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
123
136
|
/** Native slide background fill for a theme-slot background, or null when it must stay literal. */
|
|
124
137
|
export function schemeBackgroundFill(definition, context) {
|
|
125
|
-
const
|
|
126
|
-
if (!slot) return null;
|
|
127
|
-
const value = schemeColorValue(slot, context.colors?.background, context);
|
|
138
|
+
const value = schemeBackgroundValue(definition, context);
|
|
128
139
|
if (!value) return null;
|
|
129
|
-
const
|
|
130
|
-
const alpha = Math.round(Math.max(0, Math.min(1, opacity)) * 100000);
|
|
140
|
+
const alpha = Math.round(backgroundOpacity(definition) * 100000);
|
|
131
141
|
return `<a:solidFill>${alpha === 100000 ? `<a:schemeClr val="${value}"/>` : `<a:schemeClr val="${value}"><a:alpha val="${alpha}"/></a:schemeClr>`}</a:solidFill>`;
|
|
132
142
|
}
|
|
133
143
|
|
|
144
|
+
// The exporter picks default text for contrast with the slide background:
|
|
145
|
+
// light1 on a dark background, else the scheme text (dark1); muted text is
|
|
146
|
+
// dark2/light2. When the background itself is a theme reference, the matching
|
|
147
|
+
// text slot is the semantic pair, so a PowerPoint theme switch moves both.
|
|
148
|
+
const TEXT_PAIRS = Object.freeze({
|
|
149
|
+
light1: {text: 'dark1', muted: 'dark2'}, light2: {text: 'dark1', muted: 'dark2'},
|
|
150
|
+
dark1: {text: 'light1', muted: 'light2'}, dark2: {text: 'light1', muted: 'light2'},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Scheme values for the exporter's default text and muted text on this slide.
|
|
155
|
+
* A role gets a scheme value only when the slide background is an opaque theme
|
|
156
|
+
* reference to a light or dark slot, the slide has no scheme override, and the
|
|
157
|
+
* resolved default color is exactly the paired slot in the deck theme.
|
|
158
|
+
*/
|
|
159
|
+
export function defaultTextSchemeValues(context) {
|
|
160
|
+
const background = schemeBackgroundValue(context.backgroundDefinition, context);
|
|
161
|
+
if (!background || backgroundOpacity(context.backgroundDefinition) !== 1) return {};
|
|
162
|
+
const pair = TEXT_PAIRS[backgroundSlot(context.backgroundDefinition)];
|
|
163
|
+
if (!pair) return {};
|
|
164
|
+
const values = {};
|
|
165
|
+
for (const role of ['text', 'muted']) {
|
|
166
|
+
const hex = role === 'text' ? context.colors?.text : context.colors?.mutedText;
|
|
167
|
+
const value = schemeColorValue(pair[role], hex, context);
|
|
168
|
+
if (value) values[role] = value;
|
|
169
|
+
}
|
|
170
|
+
return values;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Scheme value for the exporter's default text in a table cell whose fill is a
|
|
175
|
+
* theme reference (`fillValue`, from schemeColorValue). A light or dark fill (bg1,
|
|
176
|
+
* bg2, tx1, tx2) pairs with the opposite text slot (dark1 on bg, light1 on tx).
|
|
177
|
+
* An accent fill has contrast-selected text, which stays a theme reference only
|
|
178
|
+
* when the selection is exactly the deck light1 or dark1. Anything else, and any
|
|
179
|
+
* fill that is not a theme reference, keeps literal text.
|
|
180
|
+
*/
|
|
181
|
+
export function tableTextSchemeValue(fillValue, hex, context) {
|
|
182
|
+
if (!fillValue) return undefined;
|
|
183
|
+
const light = /^bg[12]$/.test(fillValue), dark = /^tx[12]$/.test(fillValue);
|
|
184
|
+
const candidates = light ? ['dark1'] : dark ? ['light1'] : /^accent[1-6]$/.test(fillValue) ? ['light1', 'dark1'] : [];
|
|
185
|
+
for (const slot of candidates) {
|
|
186
|
+
const value = schemeColorValue(slot, hex, context, {vendor: true});
|
|
187
|
+
if (value) return value;
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
134
192
|
// ---------------------------------------------------------------------------
|
|
135
193
|
// Import
|
|
136
194
|
|
|
@@ -202,3 +260,31 @@ export function recoverTheme({themeName, colors, majorFont, minorFont}, {themes,
|
|
|
202
260
|
const fontMatch = !!fonts && fonts.heading === majorFont && fonts.body === minorFont;
|
|
203
261
|
return colorMatch || fontMatch ? {id: theme.id} : {unverified: theme.id};
|
|
204
262
|
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Slide master
|
|
266
|
+
|
|
267
|
+
/** a:solidFill child for a pptx color value: a scheme value or RRGGBB. */
|
|
268
|
+
export function solidColorXml(value) {
|
|
269
|
+
return /^(?:tx[12]|bg[12]|accent[1-6]|hlink|folHlink)$/.test(value) ? `<a:schemeClr val="${value}"/>` : `<a:srgbClr val="${value}"/>`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Give the slide master the deck's theme background and matching default text
|
|
274
|
+
* so slides added in PowerPoint follow the chosen theme. The vendored master
|
|
275
|
+
* has no background (its layout uses the bg1 background style) and tx1 text.
|
|
276
|
+
*/
|
|
277
|
+
export function writeMasterBackground(xml, {fill, text}) {
|
|
278
|
+
let result = xml.replace(/<p:bg>[\s\S]*?<\/p:bg>/, '');
|
|
279
|
+
result = result.replace(/<p:cSld\b([^>]*)>/, `<p:cSld$1><p:bg><p:bgPr>${fill}<a:effectLst/></p:bgPr></p:bg>`);
|
|
280
|
+
if (text && text !== 'tx1') {
|
|
281
|
+
result = result.replace(/<p:txStyles>[\s\S]*?<\/p:txStyles>/, styles =>
|
|
282
|
+
styles.split('<a:solidFill><a:schemeClr val="tx1"/></a:solidFill>').join(`<a:solidFill>${solidColorXml(text)}</a:solidFill>`));
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The vendored layout's own bg1 background would hide the master background. */
|
|
288
|
+
export function inheritLayoutBackground(xml) {
|
|
289
|
+
return xml.replace(/<p:bg>[\s\S]*?<\/p:bg>/, '');
|
|
290
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-pptx",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "Pure local OPF to PPTX export and PPTX to OPF import tooling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"sideEffects": false,
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "node scripts/build.mjs",
|
|
40
|
-
"typecheck": "node --check src/index.js && node --check test/design-alignment.mjs && node --check src/color-ref.js && node --check src/theme-colors.js && node --check test/theme-colors.mjs && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check src/image-geometry.js && node --check src/slide-image-provenance.js && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check src/watermark-provenance.js && node --check src/media-dedupe.js && node --check test/media-dedupe.mjs && node --check test/watermark.mjs && node --check test/image-fit.mjs && node --check test/image-orientation.mjs && node --check test/table-layout.mjs && node --check test/table-import.mjs && node --check test/object-ids.mjs && node --check test/export-corpus.mjs && node --check test/image-media-type.mjs && node --check src/image-fallback-node.js && node --check src/image-fallback-browser.js && node --check test/webp-fallback.mjs && node --check scripts/build-browser-check.mjs && node --check test/webp-fallback-browser.js && node --check test/image-fallback-boundary.mjs && node --check src/background.js && node --check test/background.mjs && node --check src/image-import.js && node --check test/image-import.mjs && node --check test/native-background.mjs && node --check src/background-import.js && node --check test/background-inheritance.mjs && node --check test/rich-table.mjs && node --check src/table-import.js && node --check test/rich-table-import.mjs && node --check test/rich-table-import-browser.js && node --check test/table-row-sizing.mjs && node --check test/table-styles.mjs && node --check test/table-styles-browser.js && node --check test/styled-table.mjs && node --check src/table-cell-import.js && node --check test/styled-table-import.mjs && node --check test/styled-table-import-browser.js && node --check src/table-border-import.js && node --check test/table-border-styles.mjs && node --check src/card-provenance.js && node --check test/content-cards.mjs && node --check src/chart-workbook.js && node --check test/chart-colors.mjs && node --check test/chart-workbook.mjs && node --check test/single-column-chart.mjs && node --check test/native-chart-colors.mjs && node --check src/heading-provenance.js && node --check test/accepted-text.mjs && node --check test/heading-provenance.mjs && node --check src/text-provenance.js && node --check test/plain-whitespace.mjs && node --check src/timeline-provenance.js && node --check test/shared-timeline.mjs && node --check test/shared-furniture.mjs && node --check src/furniture-provenance.js && node --check src/document-provenance.js && node --check test/document-provenance.mjs && node --check test/furniture-provenance.mjs && node --check src/furniture-fields.js && node --check test/furniture-fields.mjs && node --check test/furniture-wrapped-date.mjs && node --check test/native-picture.mjs && node --check test/compare-native-picture.mjs && node --check test/compare-native-picture-edit.mjs && node --check test/compare-native-picture-edit-controls.mjs && node --check test/native-font-embed-audit.mjs && node --check test/native-font-embed-controls.mjs && node --check test/native-font-embed-fixture-source.mjs && node --check test/native-font-edit-fixture.mjs && node --check test/native-mixed-edit-audit.mjs && node --check test/native-mixed-edit-controls.mjs && node --check test/master-bullet-font.mjs && node --check test/native-font-inventory-audit.mjs && node --check test/native-font-inventory-controls.mjs && node --check test/default-font-scheme.mjs && node --check test/export-chosen-fonts.mjs && node --check test/background-fills.mjs && node --check src/typeface-inventory.js && node --check src/package-fonts.js && node --check test/typeface-inventory.mjs && node --check src/script-fonts.js && node --check test/script-fonts.mjs && node --check test/powershell-scan.mjs && node --check test/powershell-scan-probes.mjs && node --check test/socials-furniture.mjs && node --check src/media-provenance.js && node --check test/layout-parity.mjs && node --check test/cover-centering.mjs && node --check test/media-placeholder.mjs && node --check test/image-placeholder.mjs && node --check test/list-shape-names.mjs && node --check test/layout-intent.mjs && node --check test/chart-cache-import.mjs && node --check test/zip-date.mjs && node --check test/native-text-whitespace.mjs && node --check test/native-underline.mjs && node --check src/native-text-style.js && node --check src/font-weights.js && node --check test/font-weight-import.mjs && node --check src/body-text-import.js && node --check test/native-body-rich-text.mjs && node --check test/native-body-rich-text-browser.js && node --check test/native-quote-import-contract.mjs && node --check test/native-quote-import-controls.mjs && node --check test/native-quote.mjs && node --check test/determinism.mjs && node --check test/determinism-fixtures.mjs && node --check test/table-frame-furniture-order.mjs && node --check test/shape-border-widths.mjs",
|
|
41
|
-
"test": "npm run build && node test/zip-date.mjs && node test/background-fills.mjs && npm run test:furniture && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/watermark.mjs && node test/media-dedupe.mjs && node test/document-provenance.mjs && node test/layout-intent.mjs && node test/master-bullet-font.mjs && node test/default-font-scheme.mjs && node test/script-fonts.mjs && node test/export-chosen-fonts.mjs && node test/typeface-inventory.mjs && node test/color-ref-export.mjs && node test/theme-colors.mjs && node test/plain-whitespace.mjs && node test/native-text-whitespace.mjs && node test/native-body-rich-text.mjs && node test/native-underline.mjs && node test/font-weight-import.mjs && node test/design-alignment.mjs && node test/accepted-text.mjs && node test/heading-provenance.mjs && node test/native-quote-import-controls.mjs && node test/chart-colors.mjs && node test/content-cards.mjs && node test/chart-workbook.mjs && node test/single-column-chart.mjs && node --test test/chart-cache-import.mjs && node test/dependency-boundary.mjs && npm run build:browser-check && npm run test:styled-table && node test/content-layout.mjs && node test/layout-parity.mjs && node test/cover-centering.mjs && node test/table-frame-furniture-order.mjs && node test/media-placeholder.mjs && node test/shape-border-widths.mjs && node test/image-placeholder.mjs && node test/list-shape-names.mjs && node test/shared-quote.mjs && npm run test:timeline && npm run test:determinism",
|
|
40
|
+
"typecheck": "node --check src/index.js && node --check src/chart-types.js && node --check test/chart-types.mjs && node --check test/design-alignment.mjs && node --check src/color-ref.js && node --check src/theme-colors.js && node --check test/theme-colors.mjs && node --check test/table-theme-colors.mjs && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check src/image-geometry.js && node --check src/slide-image-provenance.js && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check src/watermark-provenance.js && node --check src/media-dedupe.js && node --check test/media-dedupe.mjs && node --check test/watermark.mjs && node --check test/image-fit.mjs && node --check test/image-orientation.mjs && node --check test/table-layout.mjs && node --check test/table-import.mjs && node --check test/object-ids.mjs && node --check test/export-corpus.mjs && node --check test/image-media-type.mjs && node --check src/image-fallback-node.js && node --check src/image-fallback-browser.js && node --check test/webp-fallback.mjs && node --check scripts/build-browser-check.mjs && node --check test/webp-fallback-browser.js && node --check test/image-fallback-boundary.mjs && node --check src/background.js && node --check test/background.mjs && node --check src/image-import.js && node --check test/image-import.mjs && node --check test/native-background.mjs && node --check src/background-import.js && node --check test/background-inheritance.mjs && node --check test/rich-table.mjs && node --check src/table-import.js && node --check test/rich-table-import.mjs && node --check test/rich-table-import-browser.js && node --check test/table-row-sizing.mjs && node --check test/table-styles.mjs && node --check test/table-styles-browser.js && node --check test/styled-table.mjs && node --check src/table-cell-import.js && node --check test/styled-table-import.mjs && node --check test/styled-table-import-browser.js && node --check src/table-border-import.js && node --check test/table-border-styles.mjs && node --check src/card-provenance.js && node --check test/content-cards.mjs && node --check src/chart-workbook.js && node --check test/chart-colors.mjs && node --check test/chart-workbook.mjs && node --check test/single-column-chart.mjs && node --check test/native-chart-colors.mjs && node --check src/heading-provenance.js && node --check test/accepted-text.mjs && node --check test/heading-provenance.mjs && node --check src/text-provenance.js && node --check test/plain-whitespace.mjs && node --check src/timeline-provenance.js && node --check test/shared-timeline.mjs && node --check test/shared-furniture.mjs && node --check src/furniture-provenance.js && node --check src/document-provenance.js && node --check test/document-provenance.mjs && node --check test/furniture-provenance.mjs && node --check src/furniture-fields.js && node --check test/furniture-fields.mjs && node --check test/furniture-wrapped-date.mjs && node --check test/native-picture.mjs && node --check test/compare-native-picture.mjs && node --check test/compare-native-picture-edit.mjs && node --check test/compare-native-picture-edit-controls.mjs && node --check test/native-font-embed-audit.mjs && node --check test/native-font-embed-controls.mjs && node --check test/native-font-embed-fixture-source.mjs && node --check test/native-font-edit-fixture.mjs && node --check test/native-mixed-edit-audit.mjs && node --check test/native-mixed-edit-controls.mjs && node --check test/master-bullet-font.mjs && node --check test/native-font-inventory-audit.mjs && node --check test/native-font-inventory-controls.mjs && node --check test/default-font-scheme.mjs && node --check test/export-chosen-fonts.mjs && node --check test/background-fills.mjs && node --check src/typeface-inventory.js && node --check src/package-fonts.js && node --check test/typeface-inventory.mjs && node --check src/script-fonts.js && node --check test/script-fonts.mjs && node --check test/powershell-scan.mjs && node --check test/powershell-scan-probes.mjs && node --check test/socials-furniture.mjs && node --check src/media-provenance.js && node --check test/layout-parity.mjs && node --check test/cover-centering.mjs && node --check test/media-placeholder.mjs && node --check test/image-placeholder.mjs && node --check test/list-shape-names.mjs && node --check test/layout-intent.mjs && node --check test/chart-cache-import.mjs && node --check test/zip-date.mjs && node --check test/native-text-whitespace.mjs && node --check test/native-underline.mjs && node --check src/native-text-style.js && node --check src/font-weights.js && node --check test/font-weight-import.mjs && node --check src/body-text-import.js && node --check test/native-body-rich-text.mjs && node --check test/native-body-rich-text-browser.js && node --check test/native-quote-import-contract.mjs && node --check test/native-quote-import-controls.mjs && node --check test/native-quote.mjs && node --check test/determinism.mjs && node --check test/determinism-fixtures.mjs && node --check test/table-frame-furniture-order.mjs && node --check test/shape-border-widths.mjs",
|
|
41
|
+
"test": "npm run build && node test/chart-types.mjs && node test/zip-date.mjs && node test/background-fills.mjs && npm run test:furniture && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/watermark.mjs && node test/media-dedupe.mjs && node test/document-provenance.mjs && node test/layout-intent.mjs && node test/master-bullet-font.mjs && node test/default-font-scheme.mjs && node test/script-fonts.mjs && node test/export-chosen-fonts.mjs && node test/typeface-inventory.mjs && node test/color-ref-export.mjs && node test/theme-colors.mjs && node test/table-theme-colors.mjs && node test/plain-whitespace.mjs && node test/native-text-whitespace.mjs && node test/native-body-rich-text.mjs && node test/native-underline.mjs && node test/font-weight-import.mjs && node test/design-alignment.mjs && node test/accepted-text.mjs && node test/heading-provenance.mjs && node test/native-quote-import-controls.mjs && node test/chart-colors.mjs && node test/content-cards.mjs && node test/chart-workbook.mjs && node test/single-column-chart.mjs && node --test test/chart-cache-import.mjs && node test/dependency-boundary.mjs && npm run build:browser-check && npm run test:styled-table && node test/content-layout.mjs && node test/layout-parity.mjs && node test/cover-centering.mjs && node test/table-frame-furniture-order.mjs && node test/media-placeholder.mjs && node test/shape-border-widths.mjs && node test/image-placeholder.mjs && node test/list-shape-names.mjs && node test/shared-quote.mjs && npm run test:timeline && npm run test:determinism",
|
|
42
42
|
"validate": "node scripts/validate-package.mjs",
|
|
43
43
|
"test:packed": "node test/packed-install.mjs",
|
|
44
44
|
"test:compare-published": "node test/compare-published.mjs",
|