@file-viewer/pptx 3.2.0 → 3.2.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/README.en.md CHANGED
@@ -1,7 +1,34 @@
1
1
  # @file-viewer/pptx
2
2
 
3
- Runtime package version 3.2.0.
3
+ Native PPTX rendering engine extracted from Flyfish File Viewer. It is maintained as a standalone package so presentation rendering can evolve independently from framework adapters.
4
4
 
5
- Usage and API documentation: https://doc.file-viewer.app/
5
+ ## Highlights
6
6
 
7
- The package's own license is Apache-2.0. Included third-party notices apply to their respective components.
7
+ - Pure TypeScript/DOM API with no Vue, React, or iframe dependency.
8
+ - Web Worker based PPTX parsing with progressive slide output.
9
+ - Preserves the latest native Flyfish rendering path for themes, images, shapes, tables, text, and basic charts.
10
+ - Supports `fitMode`, `zoomPercent`, custom Worker URLs, and Worker factories for private deployment and strict CSP environments.
11
+
12
+ `resolvePptxPackageWorkerUrl()` lets integrations identify an emitted or directly served package Worker URL. It returns `undefined` when the location is not recognized, not as proof that a file is missing; integrations can then use their asset-copy strategy or explicit Worker configuration.
13
+
14
+ When `PptxViewer` is used directly, Vite or Webpack can serve a recognized package Worker; an unrecognized build never guesses an application-relative path. Angular, IIFE, offline static directories, and strict CSP deployments should use `file-viewer-copy-assets` or an explicit `workerUrl` so the final static origin serves the Worker reliably.
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import { PptxViewer } from '@file-viewer/pptx'
20
+
21
+ const buffer = await file.arrayBuffer()
22
+ const viewer = await PptxViewer.open(buffer, document.querySelector('#app')!, {
23
+ fitMode: 'contain',
24
+ zoomPercent: 100,
25
+ onSlideRendered(index) {
26
+ console.log('slide rendered', index)
27
+ },
28
+ })
29
+
30
+ await viewer.setZoom(125)
31
+ viewer.destroy()
32
+ ```
33
+
34
+ Most users should consume PPTX through `@file-viewer/core` or a standard framework package. The core package lazy-loads this renderer only when a presentation is opened.
package/README.md CHANGED
@@ -1,7 +1,34 @@
1
1
  # @file-viewer/pptx
2
2
 
3
- Runtime package version 3.2.0.
3
+ Flyfish File Viewer 的原生 PPTX 渲染引擎包,从历史稳定实现中独立拆分,便于持续维护和单独发布。
4
4
 
5
- Usage and API documentation: https://doc.file-viewer.app/
5
+ ## 特性
6
6
 
7
- The package's own license is Apache-2.0. Included third-party notices apply to their respective components.
7
+ - 纯 TypeScript/DOM 入口,不依赖 Vue、React 或 iframe。
8
+ - 使用 Web Worker 解析 PPTX,按页渐进输出,避免阻塞主线程。
9
+ - 保留历史原生解析链路中的主题、图片、形状、表格、文本和基础图表处理。
10
+ - 支持 `fitMode`、`zoomPercent`、自定义 Worker URL / Worker factory,方便内网、离线和 CSP 场景。
11
+
12
+ `resolvePptxPackageWorkerUrl()` 可供集成层识别打包器已输出或包目录直接提供的 Worker URL。未识别时返回 `undefined`,不代表文件不存在;集成层仍可使用自己的资产复制策略或显式 Worker 配置。
13
+
14
+ 直接使用 `PptxViewer` 时,Vite 或 Webpack 可直接提供已识别的包内 Worker;无法识别的构建不会猜测应用相对路径。Angular、IIFE、离线静态目录或严格 CSP 部署应使用 `file-viewer-copy-assets` 或显式 `workerUrl`,让 Worker 由最终静态域名稳定提供。
15
+
16
+ ## 使用
17
+
18
+ ```ts
19
+ import { PptxViewer } from '@file-viewer/pptx'
20
+
21
+ const buffer = await file.arrayBuffer()
22
+ const viewer = await PptxViewer.open(buffer, document.querySelector('#app')!, {
23
+ fitMode: 'contain',
24
+ zoomPercent: 100,
25
+ onSlideRendered(index) {
26
+ console.log('slide rendered', index)
27
+ },
28
+ })
29
+
30
+ await viewer.setZoom(125)
31
+ viewer.destroy()
32
+ ```
33
+
34
+ 在 Flyfish File Viewer 中通常无需直接使用本包,`@file-viewer/core` 会按 PPTX 类型自动按需加载。
@@ -0,0 +1,59 @@
1
+ import type { PptxChartModel } from "./chart-types.js";
2
+ export declare const formatPptxChartNumber: (value: number, format?: string) => string;
3
+ export declare const createPptxCartesianChart: (model: PptxChartModel, library: Record<string, any>) => {
4
+ interaction: {
5
+ enabled: boolean;
6
+ };
7
+ title?: {
8
+ text: string;
9
+ } | undefined;
10
+ bar?: {
11
+ width: {
12
+ ratio: number;
13
+ };
14
+ } | undefined;
15
+ data: {
16
+ labels?: {
17
+ format: (value: number, id: string, position: number) => string;
18
+ } | undefined;
19
+ stack?: {
20
+ normalize: boolean;
21
+ } | undefined;
22
+ groups?: string[][] | undefined;
23
+ order?: null | undefined;
24
+ colors?: {
25
+ [k: string]: string;
26
+ } | undefined;
27
+ columns: (string | number | null)[][];
28
+ types: {
29
+ [k: string]: any;
30
+ };
31
+ type: any;
32
+ axes: {
33
+ [k: string]: string;
34
+ };
35
+ names: {
36
+ [k: string]: string;
37
+ };
38
+ };
39
+ axis: {
40
+ y2?: {} | undefined;
41
+ rotated: boolean;
42
+ x: {
43
+ type: string;
44
+ categories: string[];
45
+ tick: {
46
+ multiline: boolean;
47
+ };
48
+ };
49
+ y: {};
50
+ };
51
+ grid: {
52
+ x: {
53
+ show: boolean;
54
+ };
55
+ y: {
56
+ show: boolean;
57
+ };
58
+ };
59
+ } | null;
@@ -0,0 +1,137 @@
1
+ export const formatPptxChartNumber = (value, format = 'General') => {
2
+ if (!Number.isFinite(value))
3
+ return '';
4
+ const match = /^([$€£]?)[#0,]+(?:\.([0#]+))?(%)?$/.exec(format);
5
+ if (!match)
6
+ return String(value);
7
+ const decimal = (match[2] || '').slice(0, 20);
8
+ return match[1] + new Intl.NumberFormat('en-US', {
9
+ minimumFractionDigits: (decimal.match(/0/g) || []).length,
10
+ maximumFractionDigits: decimal.length,
11
+ useGrouping: format.includes(','),
12
+ style: match[3] ? 'percent' : 'decimal',
13
+ }).format(value);
14
+ };
15
+ const axisOptions = (axis) => {
16
+ if (!axis)
17
+ return {};
18
+ const bounds = axis.min === undefined || axis.max === undefined || axis.min < axis.max
19
+ ? { min: axis.min, max: axis.max } : {};
20
+ return {
21
+ ...bounds,
22
+ ...(axis.min !== undefined || axis.max !== undefined ? { padding: { top: 0, bottom: 0 } } : {}),
23
+ show: !axis.deleted,
24
+ inverted: axis.reversed,
25
+ ...(axis.logBase ? { type: 'log' } : {}),
26
+ ...(axis.title ? { label: { text: axis.title, position: 'outer-middle' } } : {}),
27
+ ...(axis.numberFormat ? { tick: { format: (value) => formatPptxChartNumber(value, axis.numberFormat.code) } } : {}),
28
+ };
29
+ };
30
+ const defined = (labels) => Object.fromEntries(Object.entries(labels || {}).filter(([, value]) => value !== undefined));
31
+ export const createPptxCartesianChart = (model, library) => {
32
+ var _a, _b, _c, _d, _e;
33
+ if (!model.groups.length || model.groups.some(group => !['lineChart', 'barChart', 'areaChart'].includes(group.chartType)))
34
+ return null;
35
+ const rows = model.groups.flatMap((group, groupIndex) => group.series.map((series, index) => ({
36
+ group, groupIndex, series, index, id: `pptx-series-${groupIndex}-${index}`,
37
+ })));
38
+ const coordinates = new Set();
39
+ const labels = new Map();
40
+ for (const { series } of rows)
41
+ for (const [position, point] of series.values.entries()) {
42
+ const coordinate = String(point.x);
43
+ coordinates.add(coordinate);
44
+ if (!labels.has(coordinate))
45
+ labels.set(coordinate, String((_a = series.xlabels[position]) !== null && _a !== void 0 ? _a : coordinate));
46
+ }
47
+ const indices = [...coordinates].sort((a, b) => Number(a) - Number(b));
48
+ const categories = indices.map(index => labels.get(index));
49
+ const valueFor = (series) => {
50
+ const points = new Map(series.values.map(point => [String(point.x), point.y]));
51
+ return indices.map(index => { var _a; return (_a = points.get(index)) !== null && _a !== void 0 ? _a : null; });
52
+ };
53
+ const axesFor = (group) => model.axes.filter(axis => group.axisIds.includes(axis.id));
54
+ const primary = (_b = axesFor(model.groups[0]).find(axis => axis.kind === 'value')) !== null && _b !== void 0 ? _b : model.axes.find(axis => axis.kind === 'value');
55
+ const secondary = rows.flatMap(row => axesFor(row.group)).find(axis => axis.kind === 'value' && axis.id !== (primary === null || primary === void 0 ? void 0 : primary.id));
56
+ const category = (_c = axesFor(model.groups[0]).find(axis => axis.kind === 'category' || axis.kind === 'date')) !== null && _c !== void 0 ? _c : model.axes.find(axis => axis.kind === 'category' || axis.kind === 'date');
57
+ const factory = { barChart: library.bar, lineChart: library.line, areaChart: library.area };
58
+ if (rows.some(row => typeof factory[row.group.chartType] !== 'function'))
59
+ return null;
60
+ const originalValues = rows.map(row => valueFor(row.series));
61
+ const columns = rows.map(({ id }, index) => [id, ...originalValues[index]]);
62
+ const types = Object.fromEntries(rows.map(row => [row.id, factory[row.group.chartType]()]));
63
+ const dataAxes = Object.fromEntries(rows.map(row => [row.id,
64
+ secondary && row.group.axisIds.includes(secondary.id) ? 'y2' : 'y',
65
+ ]));
66
+ const groups = model.groups.flatMap((group, groupIndex) => group.options.grouping === 'stacked' || group.options.grouping === 'percentStacked'
67
+ ? [rows.filter(row => row.groupIndex === groupIndex).map(row => row.id)] : []);
68
+ const normalizeAll = model.groups.length === 1 && model.groups[0].options.grouping === 'percentStacked';
69
+ const totals = new Map();
70
+ rows.forEach((row, rowIndex) => {
71
+ if (normalizeAll || row.group.options.grouping !== 'percentStacked')
72
+ return;
73
+ let total = totals.get(row.groupIndex);
74
+ if (!total) {
75
+ total = indices.map(() => 0);
76
+ totals.set(row.groupIndex, total);
77
+ }
78
+ originalValues[rowIndex].forEach((value, index) => { total[index] += Math.abs(value || 0); });
79
+ });
80
+ rows.forEach((row, rowIndex) => {
81
+ if (normalizeAll || row.group.options.grouping !== 'percentStacked')
82
+ return;
83
+ for (let i = 1; i < columns[rowIndex].length; i++) {
84
+ const total = totals.get(row.groupIndex)[i - 1];
85
+ if (typeof columns[rowIndex][i] === 'number' && total)
86
+ columns[rowIndex][i] = Number(columns[rowIndex][i]) / total * 100;
87
+ }
88
+ });
89
+ const colors = Object.fromEntries(rows.flatMap(row => {
90
+ var _a, _b;
91
+ const color = (_b = (_a = row.group.options.series) === null || _a === void 0 ? void 0 : _a[row.index]) === null || _b === void 0 ? void 0 : _b.color;
92
+ return color && /^(?:#[\da-f]{6}(?:[\da-f]{2})?|transparent)$/i.test(color) ? [[row.id, color]] : [];
93
+ }));
94
+ const gap = (_d = model.groups.find(group => group.chartType === 'barChart')) === null || _d === void 0 ? void 0 : _d.options.gapWidth;
95
+ const hasLabels = rows.some(row => { var _a, _b; return row.group.options.labels || ((_b = (_a = row.group.options.series) === null || _a === void 0 ? void 0 : _a[row.index]) === null || _b === void 0 ? void 0 : _b.labels); });
96
+ const byId = new Map(rows.map(row => [row.id, row]));
97
+ const labelFormat = (value, id, position) => {
98
+ var _a, _b, _c;
99
+ const row = byId.get(id);
100
+ if (!row)
101
+ return '';
102
+ const properties = { ...defined(row.group.options.labels), ...defined((_b = (_a = row.group.options.series) === null || _a === void 0 ? void 0 : _a[row.index]) === null || _b === void 0 ? void 0 : _b.labels) };
103
+ const originalIndex = indices[position];
104
+ const point = (_c = properties.points) === null || _c === void 0 ? void 0 : _c[originalIndex];
105
+ Object.assign(properties, defined(point));
106
+ if (properties.deleted)
107
+ return '';
108
+ const result = [];
109
+ if (properties.showSeries)
110
+ result.push(String(row.series.key));
111
+ if (properties.showCategory)
112
+ result.push(categories[position] || '');
113
+ if (properties.showValue)
114
+ result.push(formatPptxChartNumber(value, properties.formatCode));
115
+ return result.join(' ');
116
+ };
117
+ return {
118
+ data: {
119
+ columns, types, type: types[(_e = rows[0]) === null || _e === void 0 ? void 0 : _e.id], axes: dataAxes,
120
+ names: Object.fromEntries(rows.map(row => [row.id, String(row.series.key)])),
121
+ ...(Object.keys(colors).length ? { colors } : {}),
122
+ ...(groups.length ? { groups, order: null } : {}),
123
+ ...(normalizeAll ? { stack: { normalize: true } } : {}),
124
+ ...(hasLabels ? { labels: { format: labelFormat } } : {}),
125
+ },
126
+ axis: {
127
+ rotated: model.groups.some(group => group.barDirection === 'bar'),
128
+ x: { ...axisOptions(category), type: 'category', categories, tick: { multiline: true } },
129
+ y: axisOptions(primary),
130
+ ...(secondary ? { y2: axisOptions(secondary) } : {}),
131
+ },
132
+ grid: { x: { show: (category === null || category === void 0 ? void 0 : category.majorGridlines) === true }, y: { show: (primary === null || primary === void 0 ? void 0 : primary.majorGridlines) === true } },
133
+ ...(gap !== undefined ? { bar: { width: { ratio: 100 / (100 + gap) } } } : {}),
134
+ ...(model.title ? { title: { text: model.title } } : {}),
135
+ interaction: { enabled: true },
136
+ };
137
+ };
@@ -0,0 +1,98 @@
1
+ export type PptxChartType = 'lineChart' | 'barChart' | 'areaChart' | 'scatterChart' | 'pieChart' | 'pie3DChart' | 'doughnutChart';
2
+ export interface PptxChartLabels {
3
+ showValue?: boolean;
4
+ showCategory?: boolean;
5
+ showSeries?: boolean;
6
+ showPercent?: boolean;
7
+ deleted?: boolean;
8
+ position?: string;
9
+ formatCode?: string;
10
+ sourceLinked?: boolean;
11
+ points?: Record<string, PptxChartLabels>;
12
+ }
13
+ export interface PptxChartOptions {
14
+ grouping?: 'standard' | 'clustered' | 'stacked' | 'percentStacked';
15
+ holeSize?: number;
16
+ firstSliceAngle?: number;
17
+ gapWidth?: number;
18
+ overlap?: number;
19
+ legend?: {
20
+ show?: boolean;
21
+ position?: string;
22
+ };
23
+ labels?: PptxChartLabels;
24
+ series?: Array<{
25
+ color?: string;
26
+ points?: Record<string, string>;
27
+ labels?: PptxChartLabels;
28
+ }>;
29
+ }
30
+ export interface PptxChartSeries {
31
+ key: string | number;
32
+ values: Array<{
33
+ x: string | number | null;
34
+ y: number | null;
35
+ }>;
36
+ xlabels: Record<string, string | number>;
37
+ categoryLevels?: Array<Record<string, string>>;
38
+ }
39
+ export interface PptxChartAxis {
40
+ id: string;
41
+ kind: 'category' | 'value' | 'date' | 'series';
42
+ position?: 'l' | 'r' | 't' | 'b';
43
+ deleted: boolean;
44
+ reversed: boolean;
45
+ min?: number;
46
+ max?: number;
47
+ logBase?: number;
48
+ crossAxisId?: string;
49
+ crosses?: 'autoZero' | 'min' | 'max';
50
+ crossesAt?: number;
51
+ numberFormat?: {
52
+ code: string;
53
+ sourceLinked?: boolean;
54
+ };
55
+ title?: string;
56
+ majorGridlines: boolean;
57
+ font?: {
58
+ family?: string;
59
+ sizePt?: number;
60
+ };
61
+ }
62
+ export interface PptxChartGroup {
63
+ chartType: PptxChartType;
64
+ barDirection?: 'bar' | 'col';
65
+ axisIds: string[];
66
+ series: PptxChartSeries[];
67
+ options: PptxChartOptions;
68
+ }
69
+ export interface PptxChartModel {
70
+ schemaVersion: 1;
71
+ groups: PptxChartGroup[];
72
+ axes: PptxChartAxis[];
73
+ title?: string;
74
+ font?: {
75
+ family?: string;
76
+ sizePt?: number;
77
+ };
78
+ }
79
+ export interface PptxChartMessage {
80
+ type?: string;
81
+ data?: {
82
+ chartID?: string;
83
+ chartType?: PptxChartType | 'comboChart';
84
+ barDirection?: string;
85
+ chartData?: any;
86
+ chartOptions?: PptxChartOptions;
87
+ chartModel?: PptxChartModel;
88
+ };
89
+ }
90
+ export interface PptxChartCollection {
91
+ MsgQueue: PptxChartMessage[];
92
+ isDone?: boolean;
93
+ }
94
+ export type PptxChartRenderer = (model: PptxChartModel, target: HTMLElement) => {
95
+ destroy?: () => void;
96
+ } | void | Promise<{
97
+ destroy?: () => void;
98
+ } | void>;
@@ -0,0 +1 @@
1
+ export {};
package/dist/chart.d.ts CHANGED
@@ -1,8 +1,10 @@
1
+ import type { PptxChartRenderer } from "./chart-types.js";
1
2
  export type PptxChartLibraries = {
2
3
  billboard: any;
3
4
  d3Format: typeof import('d3-format');
4
5
  };
5
6
  export type PptxChartLibraryLoader = () => Promise<PptxChartLibraries>;
7
+ export declare const registerPptxChartRenderer: (renderer: PptxChartRenderer | null) => void;
6
8
  export declare const registerPptxChartLibraryLoader: (loader: PptxChartLibraryLoader | null) => void;
7
9
  export type PptxPostProcessingHandle = {
8
10
  destroy: () => void;