@openpresentation/opf-pptx 0.11.1 → 0.11.3

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Coordinated `@openpresentation/opf` pin
4
4
 
5
- PPTX 0.11.1 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.
5
+ PPTX 0.11.3 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. The deck color scheme is written as the theme `clrScheme`, and named slot and role colors (text, table cells and borders, hyperlinks, backgrounds) are written as `schemeClr` where the deck theme holds exactly that color; card, timeline and media colors, literal hex, variables, translucent colors and slide-override colors stay literal `srgbClr`.
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.3 adds the opt-in `toPptx({ chartex: 'native' })` export of the seven chartex chart types and always imports chartex charts (no API removed; the default output is unchanged). 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,8 +80,8 @@ 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)).
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.
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. The chartex types (treemap, histogram, pareto, box & whisker, waterfall, funnel, map) export by default as clustered columns and report `chart-data-adapted` (`chartex-fallback`); with `chartex: "native"` they are written as native Office 2016 chartex parts (see [Chartex charts](#chartex-charts-ff-22b), opt-in pending native PowerPoint confirmation). A pie or doughnut chart (and, natively, a one-series chartex construct) plots only its first series and reports `chart-data-adapted` (`series-dropped`) when it has more.
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`: by default a histogram is binned into equal-width bins (Sturges' count, at most 50) and written as a column chart of the counts (`histogram-binned`; the binned counts do not round-trip), and any other chart type plots the values against their row numbers (`row-numbers`). With `chartex: "native"` a histogram or Pareto chart bins the values itself (PowerPoint's histogram with an explicit automatic bin count, no diagnostic) and re-imports as authored. Cells parse as in every chart ("12%", "$5" and "1,234" count) and cells that hold no number are skipped, not plotted as 0. 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).
87
87
  - The package names only the document's chosen fonts. Chart text (data labels, axes, legend, titles) uses the chart slide's body font in `latin`/`ea`/`cs`; each embedded chart workbook uses the same fonts in its styles and theme; run `pitchFamily` follows the font scheme type (monospace is fixed pitch, serif is roman); and `docProps/app.xml` "Fonts Used" lists the fonts the package actually uses. The theme keeps PptxGenJS's per-script supplements (`THEME_SCRIPT_SUPPLEMENTS`) and empty `ea`/`cs` slots.
@@ -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 basic OPF chart blocks.
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.
@@ -125,6 +125,24 @@ The first importer is mechanical and schema-compatible:
125
125
 
126
126
  There is no AI classification pass in the OSS runtime. Hosts can run optional cleanup or semantic remapping after `fromPptx` returns.
127
127
 
128
+ ## Chartex charts (FF-22b)
129
+
130
+ **Opt-in.** `toPptx(document, {chartex: "native"})` enables the native chartex export described here. The default, `chartex: "fallback"`, keeps the clustered column export of the seven chartex ids byte for byte (`test/fixtures/chartex-fallback-main.json` pins it) with the `chartex-fallback` and `histogram-binned` diagnostics, because a malformed chartex part would make PowerPoint offer to repair the whole deck, and the parts are not yet confirmed in native PowerPoint (no Open XML SDK validation either). The default flips once the program's native check of the FF-22b deck set passes. Import of chartex parts is always on.
131
+
132
+ With `chartex: "native"`, the seven kept chart types with no ECMA-376 construct are written as Office 2016 chartex parts (`ppt/charts/chartExN.xml`, `cx:chartSpace`, content type `application/vnd.ms-office.chartex+xml`), each with its chart style and colour style parts (`styleN.xml`, `colorsN.xml`) and the embedded workbook the data came from. PptxGenJS cannot write these parts, so the exporter first writes the classic clustered column chart of the same data and then post-processes the package (`src/chartex.js`): the slide's chart frame becomes an `mc:AlternateContent` whose `mc:Choice` (requiring the construct's chartex namespace) references the chartex part and whose `mc:Fallback` keeps the classic chart frame, so a reader without chartex support still shows the data as columns. Both parts share the workbook. `cx:series uniqueId` values are derived from the chart number, so the bytes stay deterministic.
133
+
134
+ | OPF id | `cx:series layoutId` | Data (category-major) | Notes |
135
+ |---|---|---|---|
136
+ | `treemap` | `treemap` | `[Category, Value]`, first series | one tile per category, category data labels, one palette colour per tile |
137
+ | `histogram` | `clusteredColumn` | `[Value]` or `[Category, Value]`, first series | a lone value column is binned by PowerPoint with an explicit automatic bin count (Scott's rule, `cx:binCount`); a category column bins by category (`cx:aggregation`) |
138
+ | `pareto` | `clusteredColumn` owning a `paretoLine` | as histogram | the Office Pareto chart: sorted columns plus the cumulative-percentage line on a percentage axis |
139
+ | `box-and-whisker` | `boxWhisker` | `[Category, S1, S2, ...]`, every series | rows with the same category form one box per series; exclusive quartiles, mean markers, outliers |
140
+ | `waterfall` | `waterfall` | `[Category, Value]`, first series | connector lines; increases and decreases in the first two palette colours; no subtotals |
141
+ | `funnel` | `funnel` | `[Category, Value]`, first series | value data labels |
142
+ | `world` | `regionMap` | `[Region, Value]`, first series | no `cx:geoCache`: PowerPoint matches the region names and fetches the shapes from Bing Maps when the deck is opened online, and reports `chart-map-geodata`; offline, or where Office map data is disabled, the map shows no regions |
143
+
144
+ The chart area, label colour, label font (`latin`/`ea`/`cs` in the chart's body font) and series colours follow the classic charts. Re-import (`fromPptx`) reads the chartex part first: the layoutIds name the OPF id (an owned `paretoLine` is `pareto`), `cx:strDim type="cat"` restores the categories and each `cx:numDim` a series, under the same 100,000-point and 1,000,000-cell bounds as classic chart caches; a lone value column comes back as authored. A chartex part with no OPF construct (sunburst) falls back to the `mc:Fallback` chart, or to a text placeholder when PowerPoint's own text fallback is all there is. Native PowerPoint rendering of each construct is confirmed through the program's bounded native sample, not by this package's tests.
145
+
128
146
  ## Languages, right-to-left text and script fonts
129
147
 
130
148
  The exporter reads the presentation `language` through core `resolveScriptFonts()` (FF-07; the model is core `docs/programs/font-fidelity-everywhere/script-font-model.md`):
@@ -268,15 +286,22 @@ Import reads `a:pattFill` with a DrawingML preset and resolvable foreground/back
268
286
 
269
287
  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
288
 
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-level `design.colorScheme` that changes a slot keeps that color literal. These stay `a:srgbClr`:
289
+ 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.
290
+
291
+ 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.
292
+
293
+ 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.
294
+
295
+ 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.
296
+
297
+ These stay `a:srgbClr`:
272
298
 
273
299
  - literal hex colors, even when they equal a scheme slot;
274
300
  - `var:<id>` variables;
275
301
  - translucent colors;
276
- - engine-derived chrome: default text, headings, list markers, muted text, card and chart panels, chart series, and default table header fill. These colors are contrast-selected per slide, not named by the document.
277
- - `hyperlink` and `followedHyperlink` run and cell colors, because PptxGenJS 4.0.1 cannot emit `hlink`/`folHlink`. Borders and backgrounds can.
302
+ - 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
303
 
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.
304
+ 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
305
 
281
306
  ## JPEG orientation on import
282
307
 
@@ -0,0 +1,212 @@
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
+ // Office 2016 chartex constructs (cx:chartSpace, one cx:series per plot,
33
+ // named by layoutId). `requires` is the markup-compatibility namespace the
34
+ // slide's mc:Choice names, so older readers take the classic fallback.
35
+ // `series` is the number of value columns the construct plots.
36
+ treemap: chartex('treemap', 'Treemap', { requires: 'cx1', dimension: 'size', series: 1 }),
37
+ histogram: chartex('clusteredColumn', 'Histogram', { requires: 'cx1', binning: true, series: 1, axes: true }),
38
+ pareto: chartex('paretoLine', 'ParetoLine', { requires: 'cx1', binning: true, owner: 'clusteredColumn', series: 1, axes: true }),
39
+ 'box-and-whisker': chartex('boxWhisker', 'BoxAndWhisker', { requires: 'cx1', series: Infinity, axes: true }),
40
+ waterfall: chartex('waterfall', 'Waterfall', { requires: 'cx1', series: 1, axes: true }),
41
+ funnel: chartex('funnel', 'Funnel', { requires: 'cx2', series: 1, axes: true }),
42
+ world: chartex('regionMap', 'Map', { requires: 'cx5', dimension: 'colorVal', series: 1 }),
43
+ });
44
+
45
+ function chartex(layoutId, aspose, extra) {
46
+ return { family: 'chartex', layoutId, aspose, dimension: 'val', ...extra };
47
+ }
48
+
49
+ // The classic construct written as the mc:Fallback of every chartex chart, so a
50
+ // reader without chartex support shows a clustered column chart of the same
51
+ // workbook data.
52
+ export const CHARTEX_FALLBACK = CHART_TYPES.column;
53
+
54
+ // Markup-compatibility namespaces for `requires` (MS-ODRAWXML chartex
55
+ // versions). PowerPoint 2016 and later understand all of them.
56
+ export const CHARTEX_NAMESPACES = Object.freeze({
57
+ cx: 'http://schemas.microsoft.com/office/drawing/2014/chartex',
58
+ cx1: 'http://schemas.microsoft.com/office/drawing/2015/9/8/chartex',
59
+ cx2: 'http://schemas.microsoft.com/office/drawing/2015/10/21/chartex',
60
+ cx3: 'http://schemas.microsoft.com/office/drawing/2016/5/9/chartex',
61
+ cx4: 'http://schemas.microsoft.com/office/drawing/2016/5/10/chartex',
62
+ cx5: 'http://schemas.microsoft.com/office/drawing/2016/5/11/chartex',
63
+ });
64
+
65
+ // cx:series layoutIds of a chartex part (an owned paretoLine marks the Office
66
+ // Pareto chart) -> kept OPF id, for import; null for sunburst and unknown layouts.
67
+ export function chartTypeFromChartex(layoutIds) {
68
+ const ids = new Set(layoutIds);
69
+ if (ids.has('paretoLine')) return 'pareto';
70
+ for (const [id, spec] of Object.entries(CHART_TYPES)) if (spec.family === 'chartex' && ids.has(spec.layoutId)) return id;
71
+ return null;
72
+ }
73
+
74
+ const variants = (base, target) => Object.fromEntries([base, `${base}-2x`, `${base}-3x`].map((id) => [id, target]));
75
+
76
+ // Deprecated core ids (opf 0.12.0 removes them) -> replacement id.
77
+ export const DEPRECATED_CHART_TYPES = Object.freeze({
78
+ ...variants('100pct-bullet-bar', '100pct-stacked-bar-3x'),
79
+ '100pct-progress-bar': '100pct-stacked-bar-3x',
80
+ '100pct-stacked-bar-2x': '100pct-stacked-bar-3x',
81
+ ...variants('100pct-bullet-column', '100pct-stacked-column-3x'),
82
+ '100pct-stacked-column-2x': '100pct-stacked-column-3x',
83
+ '100pct-stacked-area-2x': '100pct-stacked-area-3x',
84
+ australia: 'world',
85
+ canada: 'world',
86
+ 'united-kingdom': 'world',
87
+ 'united-states': 'world',
88
+ 'box-and-whisker-2x': 'box-and-whisker',
89
+ 'box-and-whisker-3x': 'box-and-whisker',
90
+ ...variants('bullet-bar', 'bar'),
91
+ 'clustered-bar-2x': 'bar',
92
+ ...variants('bullet-column', 'column'),
93
+ 'clustered-column': 'column',
94
+ ...Object.fromEntries(['', '-2x', '-3x', '-4x', '-5x', '-6x'].map((suffix) => [`dot-plot${suffix}`, 'scatter'])),
95
+ dumbbell: 'scatter',
96
+ 'line-2x': 'line',
97
+ 'line-3x': 'line',
98
+ 'line-with-high-low': 'line',
99
+ ...Object.fromEntries(['', '-2x', '-3x', '-4x', '-5x', '-6x'].map((suffix) => [`sparkline${suffix}`, 'line'])),
100
+ 'line-with-high-low-and-markers': 'line-with-markers',
101
+ 'line-with-markers-2x': 'line-with-markers',
102
+ 'line-with-markers-3x': 'line-with-markers',
103
+ 'stacked-area-2x': 'stacked-area-3x',
104
+ 'stacked-bar-2x': 'stacked-bar-3x',
105
+ 'stacked-column-2x': 'stacked-column-3x',
106
+ 'stacked-line-2x': 'stacked-line-3x',
107
+ 'stacked-line-with-markers-2x': 'stacked-line-with-markers-3x',
108
+ 'treemap-2x': 'treemap',
109
+ 'treemap-3x': 'treemap',
110
+ });
111
+
112
+ const ALIASES = Object.freeze({ donut: 'doughnut' });
113
+
114
+ // Resolve an OPF chart type id to {id, spec}. Kept and deprecated ids resolve
115
+ // to a kept id; anything else returns the legacy heuristic with id: null.
116
+ export function resolveChartType(type) {
117
+ const raw = String(type ?? '').trim().toLowerCase();
118
+ const id = ALIASES[raw] ?? DEPRECATED_CHART_TYPES[raw] ?? raw;
119
+ if (Object.hasOwn(CHART_TYPES, id)) return { id, spec: CHART_TYPES[id] };
120
+ return { id: null, spec: legacyChartType(raw) };
121
+ }
122
+
123
+ // The pre-FF-22 heuristic for ids outside the core catalog.
124
+ function legacyChartType(normalized) {
125
+ if (normalized.includes('pie')) return CHART_TYPES.pie;
126
+ if (normalized.includes('doughnut') || normalized.includes('donut')) return CHART_TYPES.doughnut;
127
+ if (normalized.includes('area')) return { ...CHART_TYPES.area, legacy: true };
128
+ if (normalized.includes('line')) return { ...CHART_TYPES['line-with-markers'], legacy: true };
129
+ if (normalized.includes('scatter')) return CHART_TYPES.scatter;
130
+ if (normalized.includes('radar')) return { ...CHART_TYPES.radar, legacy: true };
131
+ const grouping = normalized.includes('stacked') ? 'stacked' : 'clustered';
132
+ return category('bar', { barDir: normalized.includes('bar') ? 'bar' : 'col', grouping, legacy: true });
133
+ }
134
+
135
+ // Native construct -> kept OPF id, for import. `node` is the parsed chart-type
136
+ // element (fast-xml-parser, attributes as plain keys).
137
+ export function chartTypeFromNative(element, node) {
138
+ const attr = (name) => node?.[`c:${name}`]?.val;
139
+ const series = asArray(node?.['c:ser']);
140
+ const markers = !series.length || series.some((ser) => ser?.['c:marker']?.['c:symbol']?.val !== 'none');
141
+ const grouping = attr('grouping') ?? 'standard';
142
+ const stacked = grouping === 'stacked' || grouping === 'percentStacked';
143
+ const percent = grouping === 'percentStacked';
144
+ switch (element) {
145
+ case 'barChart':
146
+ case 'bar3DChart': {
147
+ const direction = attr('barDir') === 'bar' ? 'bar' : 'column';
148
+ if (percent) return `100pct-stacked-${direction}-3x`;
149
+ if (stacked) return `stacked-${direction}-3x`;
150
+ return direction;
151
+ }
152
+ case 'lineChart':
153
+ case 'line3DChart':
154
+ if (stacked) return markers ? 'stacked-line-with-markers-3x' : 'stacked-line-3x';
155
+ return markers ? 'line-with-markers' : 'line';
156
+ case 'areaChart':
157
+ case 'area3DChart':
158
+ return percent ? '100pct-stacked-area-3x' : stacked ? 'stacked-area-3x' : 'area';
159
+ case 'pieChart':
160
+ case 'pie3DChart':
161
+ case 'ofPieChart':
162
+ return 'pie';
163
+ case 'doughnutChart':
164
+ return 'doughnut';
165
+ case 'scatterChart':
166
+ return 'scatter';
167
+ case 'radarChart': {
168
+ const style = attr('radarStyle');
169
+ if (style === 'filled') return 'filled-radar';
170
+ if (style === 'marker' && markers) return 'radar-with-markers';
171
+ return 'radar';
172
+ }
173
+ default:
174
+ return null;
175
+ }
176
+ }
177
+
178
+ export const NATIVE_CHART_ELEMENTS = Object.freeze([
179
+ 'barChart', 'bar3DChart', 'lineChart', 'line3DChart', 'pieChart', 'pie3DChart', 'ofPieChart',
180
+ 'doughnutChart', 'areaChart', 'area3DChart', 'scatterChart', 'radarChart',
181
+ ]);
182
+
183
+ function asArray(value) {
184
+ return value === undefined ? [] : Array.isArray(value) ? value : [value];
185
+ }
186
+
187
+ // PptxGenJS omits or cannot express parts of these constructs. Rewrite only
188
+ // the generated chart-type element so the part states the exact grouping.
189
+ export function applyChartConstruct(xml, spec) {
190
+ // ScatterWithMarkers: the core catalog records scatterStyle "marker" (markers, no connecting line). PptxGenJS writes lineMarker and relies on the series line being noFill.
191
+ if (spec?.family === 'xy') return xml.replace(/<c:scatterStyle val="[^"]*"\/>/, '<c:scatterStyle val="marker"/>');
192
+ if (!spec || spec.family !== 'category') return xml;
193
+ const element = { bar: 'barChart', line: 'lineChart', area: 'areaChart', radar: 'radarChart' }[spec.pptx];
194
+ if (!element || element === 'radarChart') return xml;
195
+ const open = `<c:${element}>`;
196
+ const start = xml.indexOf(open);
197
+ if (start < 0) return xml;
198
+ const end = xml.indexOf(`</c:${element}>`, start);
199
+ let body = xml.slice(start + open.length, end);
200
+ const grouping = `<c:grouping val="${spec.grouping}"/>`;
201
+ if (element === 'barChart') {
202
+ body = body.replace(/<c:grouping val="[^"]*"\/>/, grouping);
203
+ } else {
204
+ body = grouping + body.replace(/<c:grouping val="[^"]*"\/>/g, '');
205
+ // CT_LineSer/CT_AreaSer have no invertIfNegative (bar series only).
206
+ body = body.replace(/<c:invertIfNegative val="[^"]*"\/>/g, '');
207
+ }
208
+ if (spec.grouping === 'stacked' || spec.grouping === 'percentStacked') {
209
+ body = body.replace(/<c:overlap val="[^"]*"\/>/, '<c:overlap val="100"/>');
210
+ }
211
+ return xml.slice(0, start) + open + body + xml.slice(end);
212
+ }
@@ -21,7 +21,8 @@ function relationship(entries,source,id){
21
21
  }
22
22
  function workbookContext(entries,chartPart){
23
23
  const chart=xml(entries,chartPart)?.chartSpace;
24
- const ref=relationship(entries,chartPart,chart?.externalData?.id);
24
+ // A classic part keeps c:externalData under c:chartSpace; a chartex part keeps cx:externalData under cx:chartData.
25
+ const ref=relationship(entries,chartPart,(chart?.externalData??chart?.chartData?.externalData)?.id);
25
26
  if(!ref?.type?.endsWith('/package')||!entries[ref.path])return undefined;
26
27
  // Read only bounded spreadsheet metadata. Never follow external workbook links.
27
28
  let size=0;
@@ -29,7 +30,8 @@ function workbookContext(entries,chartPart){
29
30
  if(!/^xl\/(workbook\.xml|_rels\/workbook\.xml\.rels|sharedStrings\.xml|worksheets\/[^/]+\.xml)$/.test(file.name))return false;
30
31
  size+=file.originalSize;if(size>16*1024*1024)throw Error('Chart workbook metadata exceeds the limit.');return true;
31
32
  }});
32
- const category=find(chart,'cat')[0],formula=category?.strRef?.f??category?.multiLvlStrRef?.f??category?.numRef?.f;
33
+ // The category range: c:cat/c:xVal references, or the chartex cx:strDim type="cat" formula.
34
+ const category=find(chart,'cat')[0]??find(chart,'xVal')[0]??find(chart,'strDim').find(dim=>dim?.type==='cat'),formula=category?.strRef?.f??category?.multiLvlStrRef?.f??category?.numRef?.f??(category?.type==='cat'?category?.f:undefined);
33
35
  // Infer a heading only for one contiguous vertical category range with a row
34
36
  // above it. Other chart/workbook arrangements remain explicitly unrecovered.
35
37
  const range=/^(?:'((?:[^']|'')+)'|([^'!]+))!\$?([A-Z]+)\$?(\d+):\$?([A-Z]+)\$?(\d+)$/i.exec(text(formula));