@antv/infographic 0.1.0 → 0.1.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.
Files changed (110) hide show
  1. package/README.md +8 -12
  2. package/README.zh-CN.md +8 -9
  3. package/dist/infographic.min.js +45 -34
  4. package/dist/infographic.min.js.map +1 -1
  5. package/esm/designs/components/BtnsGroup.js +1 -1
  6. package/esm/designs/structures/hierarchy-tree.js +13 -6
  7. package/esm/exporter/font.d.ts +18 -0
  8. package/esm/exporter/font.js +202 -0
  9. package/esm/exporter/index.d.ts +3 -0
  10. package/esm/exporter/index.js +2 -0
  11. package/esm/exporter/png.d.ts +2 -0
  12. package/esm/exporter/png.js +42 -0
  13. package/esm/exporter/svg.d.ts +3 -0
  14. package/esm/exporter/svg.js +72 -0
  15. package/esm/exporter/types.d.ts +17 -0
  16. package/esm/index.d.ts +2 -2
  17. package/esm/renderer/composites/text.js +1 -3
  18. package/esm/renderer/fonts/built-in.d.ts +1 -1
  19. package/esm/renderer/fonts/index.d.ts +0 -2
  20. package/esm/renderer/fonts/index.js +0 -1
  21. package/esm/renderer/fonts/loader.js +1 -2
  22. package/esm/renderer/fonts/registry.d.ts +1 -1
  23. package/esm/renderer/fonts/registry.js +1 -1
  24. package/esm/renderer/renderer.js +1 -50
  25. package/esm/resource/utils/ref.js +1 -1
  26. package/esm/runtime/Infographic.d.ts +10 -0
  27. package/esm/runtime/Infographic.js +23 -2
  28. package/esm/types/font.js +1 -0
  29. package/esm/types/index.d.ts +1 -0
  30. package/{lib/renderer/fonts/utils.d.ts → esm/utils/font.d.ts} +1 -1
  31. package/esm/utils/index.d.ts +2 -0
  32. package/esm/utils/index.js +2 -0
  33. package/esm/utils/is-node.d.ts +4 -0
  34. package/esm/utils/is-node.js +6 -0
  35. package/esm/utils/padding.d.ts +1 -0
  36. package/esm/utils/padding.js +70 -0
  37. package/esm/utils/svg.d.ts +0 -1
  38. package/esm/utils/svg.js +3 -18
  39. package/esm/utils/text.js +1 -1
  40. package/esm/utils/viewbox.d.ts +6 -0
  41. package/esm/utils/viewbox.js +12 -0
  42. package/lib/designs/components/BtnsGroup.js +1 -1
  43. package/lib/designs/structures/hierarchy-tree.js +31 -24
  44. package/lib/exporter/font.d.ts +18 -0
  45. package/lib/exporter/font.js +210 -0
  46. package/lib/exporter/index.d.ts +3 -0
  47. package/lib/exporter/index.js +7 -0
  48. package/lib/exporter/png.d.ts +2 -0
  49. package/lib/exporter/png.js +45 -0
  50. package/lib/exporter/svg.d.ts +3 -0
  51. package/lib/exporter/svg.js +76 -0
  52. package/lib/exporter/types.d.ts +17 -0
  53. package/lib/index.d.ts +2 -2
  54. package/lib/renderer/composites/text.js +1 -3
  55. package/lib/renderer/fonts/built-in.d.ts +1 -1
  56. package/lib/renderer/fonts/index.d.ts +0 -2
  57. package/lib/renderer/fonts/index.js +1 -4
  58. package/lib/renderer/fonts/loader.js +1 -2
  59. package/lib/renderer/fonts/registry.d.ts +1 -1
  60. package/lib/renderer/fonts/registry.js +1 -1
  61. package/lib/renderer/renderer.js +1 -50
  62. package/lib/resource/utils/ref.js +1 -1
  63. package/lib/runtime/Infographic.d.ts +10 -0
  64. package/lib/runtime/Infographic.js +23 -2
  65. package/lib/types/font.js +2 -0
  66. package/lib/types/index.d.ts +1 -0
  67. package/{esm/renderer/fonts/utils.d.ts → lib/utils/font.d.ts} +1 -1
  68. package/lib/utils/index.d.ts +2 -0
  69. package/lib/utils/index.js +2 -0
  70. package/lib/utils/is-node.d.ts +4 -0
  71. package/lib/utils/is-node.js +9 -0
  72. package/lib/utils/padding.d.ts +1 -0
  73. package/lib/utils/padding.js +71 -0
  74. package/lib/utils/svg.d.ts +0 -1
  75. package/lib/utils/svg.js +3 -19
  76. package/lib/utils/text.js +2 -2
  77. package/lib/utils/viewbox.d.ts +6 -0
  78. package/lib/utils/viewbox.js +15 -0
  79. package/package.json +5 -2
  80. package/src/designs/components/BtnsGroup.tsx +7 -1
  81. package/src/designs/structures/hierarchy-tree.tsx +14 -8
  82. package/src/exporter/font.ts +273 -0
  83. package/src/exporter/index.ts +7 -0
  84. package/src/exporter/png.ts +58 -0
  85. package/src/exporter/svg.ts +94 -0
  86. package/src/exporter/types.ts +19 -0
  87. package/src/index.ts +7 -3
  88. package/src/renderer/composites/text.ts +1 -2
  89. package/src/renderer/fonts/built-in.ts +1 -1
  90. package/src/renderer/fonts/index.ts +1 -3
  91. package/src/renderer/fonts/loader.ts +1 -2
  92. package/src/renderer/fonts/registry.ts +2 -2
  93. package/src/renderer/renderer.ts +1 -69
  94. package/src/resource/utils/ref.ts +1 -1
  95. package/src/runtime/Infographic.tsx +30 -2
  96. package/src/types/index.ts +1 -0
  97. package/src/{renderer/fonts/utils.ts → utils/font.ts} +1 -1
  98. package/src/utils/index.ts +2 -0
  99. package/src/utils/is-node.ts +8 -0
  100. package/src/utils/padding.ts +79 -0
  101. package/src/utils/svg.ts +5 -19
  102. package/src/utils/text.ts +2 -2
  103. package/src/utils/viewbox.ts +12 -0
  104. /package/esm/{renderer/fonts → exporter}/types.js +0 -0
  105. /package/esm/{renderer/fonts/types.d.ts → types/font.d.ts} +0 -0
  106. /package/esm/{renderer/fonts/utils.js → utils/font.js} +0 -0
  107. /package/lib/{renderer/fonts → exporter}/types.js +0 -0
  108. /package/lib/{renderer/fonts/types.d.ts → types/font.d.ts} +0 -0
  109. /package/lib/{renderer/fonts/utils.js → utils/font.js} +0 -0
  110. /package/src/{renderer/fonts/types.ts → types/font.ts} +0 -0
@@ -9,7 +9,6 @@ import {
9
9
  Polygon,
10
10
  } from '../../jsx';
11
11
  import { Data } from '../../types';
12
- import { getDatumByIndexes } from '../../utils';
13
12
  import {
14
13
  BtnAdd,
15
14
  BtnRemove,
@@ -176,19 +175,28 @@ export const HierarchyTree: ComponentType<HierarchyTreeProps> = (props) => {
176
175
  };
177
176
 
178
177
  // 内置工具方法:计算各层节点边界
179
- const computeLevelBounds = (maxLevels: number) => {
178
+ const computeLevelBounds = (rootNode: d3.HierarchyNode<any>) => {
180
179
  let maxWidth = 0,
181
180
  maxHeight = 0;
182
181
  const levelBounds = new Map<number, any>();
182
+ const sampleDatumByLevel = new Map<number, any>();
183
183
 
184
- for (let level = 0; level < maxLevels; level++) {
184
+ // 记录每个深度遇到的首个节点,用于计算该层的尺寸
185
+ rootNode.each((node) => {
186
+ if (!sampleDatumByLevel.has(node.depth)) {
187
+ sampleDatumByLevel.set(node.depth, node.data);
188
+ }
189
+ });
190
+
191
+ for (let level = 0; level < rootNode.height + 1; level++) {
185
192
  const ItemComponent = getItemComponent(Items, level);
186
- const indexes = Array(level + 1).fill(0);
193
+ const sampleDatum = sampleDatumByLevel.get(level) ?? {};
194
+ const indexes = sampleDatum._originalIndex ?? Array(level + 1).fill(0);
187
195
  const bounds = getElementBounds(
188
196
  <ItemComponent
189
197
  indexes={indexes}
190
198
  data={data}
191
- datum={getDatumByIndexes(items, indexes)}
199
+ datum={sampleDatum}
192
200
  positionH="center"
193
201
  />,
194
202
  );
@@ -529,9 +537,7 @@ export const HierarchyTree: ComponentType<HierarchyTreeProps> = (props) => {
529
537
  // 构建和布局
530
538
  const hierarchyData = buildHierarchyData(items);
531
539
  const root = d3.hierarchy(hierarchyData);
532
- const { levelBounds, maxWidth, maxHeight } = computeLevelBounds(
533
- root.height + 1,
534
- );
540
+ const { levelBounds, maxWidth, maxHeight } = computeLevelBounds(root);
535
541
 
536
542
  const treeLayout = d3
537
543
  .tree<any>()
@@ -0,0 +1,273 @@
1
+ import type { FontFace as CSSFontFace, Declaration, Stylesheet } from 'css';
2
+ // @ts-expect-error ignore
3
+ import parse from 'css/lib/parse';
4
+ import { getFontURLs, getWoff2BaseURL } from '../renderer';
5
+ import {
6
+ createElement,
7
+ decodeFontFamily,
8
+ join,
9
+ normalizeFontWeightName,
10
+ } from '../utils';
11
+
12
+ interface FontFaceAttributes {
13
+ 'font-family': string;
14
+ src: string;
15
+ 'font-style': string;
16
+ 'font-display': string;
17
+ 'font-weight': string;
18
+ 'unicode-range': string;
19
+ }
20
+
21
+ const fontDataUrlCache = new Map<string, string>();
22
+
23
+ export async function embedFonts(svg: SVGSVGElement, embedResources = true) {
24
+ // 1. 收集使用到的 font-family
25
+ const usedFonts = collectUsedFonts(svg);
26
+ if (usedFonts.size === 0) return;
27
+
28
+ const parsedFontsFaces: FontFaceAttributes[] = [];
29
+
30
+ // 2. 对每个使用到的字体,解析 CSS + 结合 document.fonts 的实际加载子集
31
+ await Promise.all(
32
+ Array.from(usedFonts).map(async (fontFamily) => {
33
+ const loadedFonts = getActualLoadedFontFace(fontFamily);
34
+ if (!loadedFonts.length) return;
35
+
36
+ const cssFontFaces = await parseFontFamily(fontFamily);
37
+ if (!cssFontFaces.length) return;
38
+
39
+ const processed = await Promise.all(
40
+ cssFontFaces.map(async (rawFace) => {
41
+ const fontFace = normalizeFontFace(rawFace);
42
+
43
+ const unicodeRange = fontFace['unicode-range'].replace(/\s/g, '');
44
+ const subset = loadedFonts.find(
45
+ (font) =>
46
+ font.unicodeRange &&
47
+ font.unicodeRange.replace(/\s/g, '') === unicodeRange,
48
+ );
49
+
50
+ // 如果找不到对应子集,就不处理这个 font-face
51
+ if (!subset) return null;
52
+
53
+ const baseURL = getWoff2BaseURL(
54
+ fontFace['font-family'],
55
+ normalizeFontWeightName(fontFace['font-weight']),
56
+ );
57
+ if (!baseURL) return null;
58
+
59
+ // 更宽松地从 src 中提取 .woff2 URL 片段
60
+ const urlMatch = fontFace.src.match(
61
+ /url\(["']?(.*?\.woff2)[^"']*["']?\)/,
62
+ );
63
+ if (!urlMatch?.[1]) return null;
64
+
65
+ const woff2URL = join(baseURL, urlMatch[1]);
66
+
67
+ if (embedResources) {
68
+ const woff2DataUrl = await loadWoff2(woff2URL);
69
+ fontFace.src = `url(${woff2DataUrl}) format('woff2')`;
70
+ } else {
71
+ fontFace.src = `url(${woff2URL}) format('woff2')`;
72
+ }
73
+
74
+ return fontFace;
75
+ }),
76
+ );
77
+
78
+ parsedFontsFaces.push(
79
+ ...((processed.filter(Boolean) as FontFaceAttributes[]) || []),
80
+ );
81
+ }),
82
+ );
83
+
84
+ // 3. 创建 <style>@font-face...</style> 并插入 SVG
85
+ if (parsedFontsFaces.length > 0) {
86
+ insertFontStyle(svg, parsedFontsFaces);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * 收集 SVG 中用到的 font-family
92
+ */
93
+ function collectUsedFonts(svg: SVGSVGElement) {
94
+ const usedFonts = new Set<string>();
95
+
96
+ const svgFontFamily = svg.getAttribute('font-family');
97
+ if (svgFontFamily) {
98
+ const decodedFontFamily = decodeFontFamily(svgFontFamily);
99
+ if (decodedFontFamily) {
100
+ usedFonts.add(decodedFontFamily);
101
+ }
102
+ }
103
+
104
+ const textElements =
105
+ svg.querySelectorAll<HTMLSpanElement>('foreignObject span');
106
+
107
+ for (const span of textElements) {
108
+ const fontFamily = decodeFontFamily(span.style.fontFamily);
109
+ if (fontFamily) usedFonts.add(fontFamily);
110
+ }
111
+
112
+ return usedFonts;
113
+ }
114
+
115
+ /**
116
+ * 解析给定 font-family 对应的 CSS @font-face
117
+ */
118
+ export async function parseFontFamily(fontFamily: string) {
119
+ const urls = getFontURLs(fontFamily);
120
+
121
+ const fontFaces: Partial<FontFaceAttributes>[] = [];
122
+
123
+ await Promise.allSettled(
124
+ urls.map(async (url) => {
125
+ const css = await fetch(url)
126
+ .then((res) => res.text())
127
+ .then((text) => parse(text) as Stylesheet)
128
+ .catch(() => {
129
+ console.error(`Failed to fetch or parse font CSS: ${url}`);
130
+ return null;
131
+ });
132
+
133
+ css?.stylesheet?.rules.forEach((rule) => {
134
+ if (rule.type === 'font-face') {
135
+ const fontFace = parseFontFace(rule as CSSFontFace);
136
+ fontFaces.push(fontFace);
137
+ }
138
+ });
139
+ }),
140
+ );
141
+
142
+ return fontFaces;
143
+ }
144
+
145
+ /**
146
+ * 从 document.fonts 中获取给定 family 且已加载的 FontFace
147
+ */
148
+ export function getActualLoadedFontFace(fontFamily: string) {
149
+ const fonts: FontFace[] = [];
150
+
151
+ document.fonts.forEach((font) => {
152
+ if (
153
+ decodeFontFamily(font.family) === decodeFontFamily(fontFamily) &&
154
+ font.status === 'loaded'
155
+ ) {
156
+ fonts.push(font);
157
+ }
158
+ });
159
+
160
+ return fonts;
161
+ }
162
+
163
+ /**
164
+ * 从 css 的 FontFace 规则中提取声明
165
+ */
166
+ function parseFontFace(rule: CSSFontFace) {
167
+ const declarations = (rule.declarations || []) as Declaration[];
168
+ const attrs: Record<string, string> = {};
169
+
170
+ declarations.forEach((declaration) => {
171
+ const { property, value } = declaration;
172
+ if (property && value) {
173
+ attrs[property] = value;
174
+ }
175
+ });
176
+
177
+ // 这里返回 Partial,后面统一 normalize
178
+ return attrs as Partial<FontFaceAttributes>;
179
+ }
180
+
181
+ /**
182
+ * 将不完整的 FontFaceAttributes 补全为完整结构,给后续逻辑使用
183
+ */
184
+ function normalizeFontFace(
185
+ face: Partial<FontFaceAttributes>,
186
+ ): FontFaceAttributes {
187
+ return {
188
+ 'font-family': face['font-family'] ?? '',
189
+ src: face.src ?? '',
190
+ 'font-style': face['font-style'] ?? 'normal',
191
+ 'font-display': face['font-display'] ?? 'swap',
192
+ 'font-weight': face['font-weight'] ?? '400',
193
+ 'unicode-range': face['unicode-range'] ?? 'U+0-FFFF',
194
+ };
195
+ }
196
+
197
+ /**
198
+ * 将 @font-face 写入 <style>,插入到 SVG 中合适的位置
199
+ */
200
+ function insertFontStyle(svg: SVGSVGElement, fontFaces: FontFaceAttributes[]) {
201
+ // 简单去重:相同 family + weight + style + unicode-range + src 只保留一条
202
+ const unique: FontFaceAttributes[] = [];
203
+ const seen = new Set<string>();
204
+
205
+ for (const f of fontFaces) {
206
+ const key = [
207
+ f['font-family'],
208
+ f['font-weight'],
209
+ f['font-style'],
210
+ f['unicode-range'],
211
+ f.src,
212
+ ].join('|');
213
+
214
+ if (!seen.has(key)) {
215
+ seen.add(key);
216
+ unique.push(f);
217
+ }
218
+ }
219
+
220
+ if (unique.length === 0) return;
221
+
222
+ const style = createElement('style', { type: 'text/css' });
223
+
224
+ style.innerHTML = unique
225
+ .map((f) =>
226
+ `
227
+ @font-face {
228
+ font-family: ${f['font-family']};
229
+ src: ${f.src};
230
+ font-style: ${f['font-style']};
231
+ font-weight: ${f['font-weight']};
232
+ font-display: ${f['font-display']};
233
+ unicode-range: ${f['unicode-range']};
234
+ }
235
+ `.trim(),
236
+ )
237
+ .join('\n');
238
+
239
+ // 尽量插在 <defs> 后面,否则插在第一个子节点前
240
+ const defs = svg.querySelector('defs');
241
+ if (defs && defs.parentNode) {
242
+ defs.parentNode.insertBefore(style, defs.nextSibling);
243
+ } else {
244
+ svg.insertBefore(style, svg.firstChild);
245
+ }
246
+ }
247
+
248
+ /**
249
+ * 加载 woff2 并转为 DataURL,带缓存
250
+ */
251
+ async function loadWoff2(url: string) {
252
+ const cached = fontDataUrlCache.get(url);
253
+ if (cached) return cached;
254
+
255
+ const response = await fetch(url);
256
+ if (!response.ok) {
257
+ throw new Error(`Failed to load font: ${url}`);
258
+ }
259
+
260
+ const blob = await response.blob();
261
+
262
+ const dataUrl = await new Promise<string>((resolve, reject) => {
263
+ const reader = new FileReader();
264
+ reader.onloadend = () => {
265
+ resolve(reader.result as string);
266
+ };
267
+ reader.onerror = reject;
268
+ reader.readAsDataURL(blob);
269
+ });
270
+
271
+ fontDataUrlCache.set(url, dataUrl);
272
+ return dataUrl;
273
+ }
@@ -0,0 +1,7 @@
1
+ export { exportToPNGString } from './png';
2
+ export { exportToSVGString } from './svg';
3
+ export type {
4
+ ExportOptions,
5
+ PNGExportOptions,
6
+ SVGExportOptions,
7
+ } from './types';
@@ -0,0 +1,58 @@
1
+ import { getViewBox } from '../utils';
2
+ import { exportToSVG } from './svg';
3
+ import { PNGExportOptions } from './types';
4
+
5
+ export async function exportToPNGString(
6
+ svg: SVGSVGElement,
7
+ options: Omit<PNGExportOptions, 'type'> = {},
8
+ ): Promise<string> {
9
+ const { dpr = globalThis.devicePixelRatio ?? 2 } = options;
10
+ const node = await exportToSVG(svg);
11
+
12
+ const { width, height } = getViewBox(node);
13
+
14
+ return new Promise<string>((resolve, reject) => {
15
+ try {
16
+ const canvas = document.createElement('canvas');
17
+ canvas.width = width * dpr;
18
+ canvas.height = height * dpr;
19
+
20
+ const ctx = canvas.getContext('2d');
21
+ if (!ctx) {
22
+ reject(new Error('Failed to get canvas context'));
23
+ return;
24
+ }
25
+
26
+ // 应用 DPR 缩放
27
+ ctx.scale(dpr, dpr);
28
+
29
+ ctx.imageSmoothingEnabled = true;
30
+ ctx.imageSmoothingQuality = 'high';
31
+
32
+ node.setAttribute('width', String(width));
33
+ node.setAttribute('height', String(height));
34
+
35
+ const updatedSvgData = new XMLSerializer().serializeToString(node);
36
+ const svgURL =
37
+ 'data:image/svg+xml;charset=utf-8,' +
38
+ encodeURIComponent(updatedSvgData);
39
+
40
+ const img = new Image();
41
+ img.onload = function () {
42
+ ctx.clearRect(0, 0, width, height);
43
+ ctx.drawImage(img, 0, 0, width, height);
44
+
45
+ const pngURL = canvas.toDataURL('image/png');
46
+ resolve(pngURL);
47
+ };
48
+
49
+ img.onerror = function (error) {
50
+ reject(new Error('Image load failed: ' + error));
51
+ };
52
+
53
+ img.src = svgURL;
54
+ } catch (error) {
55
+ reject(error);
56
+ }
57
+ });
58
+ }
@@ -0,0 +1,94 @@
1
+ import { createElement, getViewBox, setAttributes, traverse } from '../utils';
2
+ import { embedFonts } from './font';
3
+ import type { SVGExportOptions } from './types';
4
+
5
+ export async function exportToSVGString(
6
+ svg: SVGSVGElement,
7
+ options: Omit<SVGExportOptions, 'type'> = {},
8
+ ) {
9
+ const node = await exportToSVG(svg, options);
10
+ const str = new XMLSerializer().serializeToString(node);
11
+ return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(str);
12
+ }
13
+
14
+ export async function exportToSVG(
15
+ svg: SVGSVGElement,
16
+ options: Omit<SVGExportOptions, 'type'> = {},
17
+ ) {
18
+ const { embedResources = true } = options;
19
+ const clonedSVG = svg.cloneNode(true) as SVGSVGElement;
20
+ const { width, height } = getViewBox(svg);
21
+ setAttributes(clonedSVG, { width, height });
22
+
23
+ await embedIcons(clonedSVG);
24
+ await embedFonts(clonedSVG, embedResources);
25
+
26
+ cleanSVG(clonedSVG);
27
+
28
+ return clonedSVG;
29
+ }
30
+
31
+ async function embedIcons(svg: SVGSVGElement) {
32
+ const icons = svg.querySelectorAll('use');
33
+ const defs = getDefs(svg);
34
+
35
+ icons.forEach((icon) => {
36
+ const href = icon.getAttribute('href');
37
+ if (!href) return;
38
+ const existsSymbol = svg.querySelector(href);
39
+
40
+ if (!existsSymbol) {
41
+ const symbolElement = document.querySelector(href);
42
+ if (symbolElement) defs.appendChild(symbolElement.cloneNode(true));
43
+ }
44
+ });
45
+ }
46
+
47
+ function getDefs(svg: SVGSVGElement) {
48
+ const defs = svg.querySelector('defs[id="icon-defs"]');
49
+ if (defs) return defs;
50
+ const _defs = createElement('defs', { id: 'icon-defs' });
51
+ svg.prepend(_defs);
52
+ return _defs;
53
+ }
54
+
55
+ function cleanSVG(svg: SVGSVGElement) {
56
+ removeBtnGroup(svg);
57
+ removeTransientContainer(svg);
58
+ removeUselessAttrs(svg);
59
+
60
+ clearDataset(svg);
61
+ }
62
+
63
+ function removeBtnGroup(svg: SVGSVGElement) {
64
+ const btnGroup = svg.querySelector('[data-element-type=btns-group]');
65
+ btnGroup?.remove();
66
+
67
+ const btnIconDefs = svg.querySelector('#btn-icon-defs');
68
+ btnIconDefs?.remove();
69
+ }
70
+
71
+ function removeTransientContainer(svg: SVGSVGElement) {
72
+ const transientContainer = svg.querySelector(
73
+ '[data-element-type=transient-container]',
74
+ );
75
+ transientContainer?.remove();
76
+ }
77
+
78
+ function removeUselessAttrs(svg: SVGSVGElement) {
79
+ const groups = svg.querySelectorAll('g');
80
+ groups.forEach((group) => {
81
+ group.removeAttribute('x');
82
+ group.removeAttribute('y');
83
+ group.removeAttribute('width');
84
+ group.removeAttribute('height');
85
+ });
86
+ }
87
+
88
+ function clearDataset(svg: SVGSVGElement) {
89
+ traverse(svg, (node) => {
90
+ for (const key in node.dataset) {
91
+ delete node.dataset[key];
92
+ }
93
+ });
94
+ }
@@ -0,0 +1,19 @@
1
+ export interface SVGExportOptions {
2
+ type: 'svg';
3
+ /**
4
+ * 是否将远程资源嵌入到 SVG 中
5
+ * @default true
6
+ */
7
+ embedResources?: boolean;
8
+ }
9
+
10
+ export interface PNGExportOptions {
11
+ type: 'png';
12
+ /**
13
+ * 设备像素比,默认为浏览器的 devicePixelRatio
14
+ * @default globalThis.devicePixelRatio || 2
15
+ */
16
+ dpr?: number;
17
+ }
18
+
19
+ export type ExportOptions = SVGExportOptions | PNGExportOptions;
package/src/index.ts CHANGED
@@ -61,8 +61,6 @@ export type {
61
61
  } from './jsx';
62
62
  export type { InfographicOptions, ParsedInfographicOptions } from './options';
63
63
  export type {
64
- Font,
65
- FontWeightName,
66
64
  GradientConfig,
67
65
  IRenderer,
68
66
  LinearGradient,
@@ -79,4 +77,10 @@ export type {
79
77
  } from './renderer';
80
78
  export type { ParsedTemplateOptions, TemplateOptions } from './templates';
81
79
  export type { ThemeColors, ThemeConfig, ThemeSeed } from './themes';
82
- export type { Data, ImageResource, ItemDatum } from './types';
80
+ export type {
81
+ Data,
82
+ Font,
83
+ FontWeightName,
84
+ ImageResource,
85
+ ItemDatum,
86
+ } from './types';
@@ -4,12 +4,12 @@ import type { DynamicAttributes } from '../../themes';
4
4
  import type { TextAttributes } from '../../types';
5
5
  import {
6
6
  createTextElement,
7
+ encodeFontFamily,
7
8
  getAttributes,
8
9
  getDatumByIndexes,
9
10
  getItemIndexes,
10
11
  setAttributes,
11
12
  } from '../../utils';
12
- import { encodeFontFamily } from '../fonts';
13
13
  import { parseDynamicAttributes } from '../utils';
14
14
 
15
15
  export function renderText(
@@ -34,7 +34,6 @@ export function renderText(
34
34
  );
35
35
  }
36
36
 
37
- renderedText.setAttribute('id', node.getAttribute('id')!);
38
37
  return renderedText;
39
38
  }
40
39
 
@@ -1,4 +1,4 @@
1
- import { Font } from './types';
1
+ import type { Font } from '../../types';
2
2
 
3
3
  const BASE_FONT_URL = 'https://assets.antv.antgroup.com';
4
4
 
@@ -1,4 +1,4 @@
1
- export { getFontURLs, getWoff2BaseURL, loadFont,loadFonts } from './loader';
1
+ export { getFontURLs, getWoff2BaseURL, loadFont, loadFonts } from './loader';
2
2
  export {
3
3
  DEFAULT_FONT,
4
4
  getFont,
@@ -6,8 +6,6 @@ export {
6
6
  registerFont,
7
7
  setDefaultFont,
8
8
  } from './registry';
9
- export type * from './types';
10
- export { decodeFontFamily, encodeFontFamily } from './utils';
11
9
 
12
10
  import { BUILT_IN_FONTS } from './built-in';
13
11
  import { registerFont } from './registry';
@@ -1,6 +1,5 @@
1
- import { join } from '../../utils';
1
+ import { join, normalizeFontWeightName } from '../../utils';
2
2
  import { getFont, getFonts } from './registry';
3
- import { normalizeFontWeightName } from './utils';
4
3
 
5
4
  export function getFontURLs(font: string): string[] {
6
5
  const config = getFont(font);
@@ -1,5 +1,5 @@
1
- import type { Font } from './types';
2
- import { decodeFontFamily, encodeFontFamily } from './utils';
1
+ import type { Font } from '../../types';
2
+ import { decodeFontFamily, encodeFontFamily } from '../../utils';
3
3
 
4
4
  const FONT_REGISTRY: Map<string, Font> = new Map();
5
5
 
@@ -1,9 +1,7 @@
1
1
  import type { ParsedInfographicOptions } from '../options';
2
- import type { ParsedPadding } from '../types';
3
2
  import {
4
3
  getDatumByIndexes,
5
4
  getItemIndexes,
6
- getSizeBaseVal,
7
5
  isBtnsGroup,
8
6
  isDesc,
9
7
  isGroup,
@@ -19,6 +17,7 @@ import {
19
17
  isTitle,
20
18
  parsePadding,
21
19
  setAttributes,
20
+ setSVGPadding,
22
21
  } from '../utils';
23
22
  import {
24
23
  renderBackground,
@@ -213,70 +212,3 @@ function setView(svg: SVGSVGElement, options: ParsedInfographicOptions) {
213
212
  setSVGPadding(svg, parsePadding(padding));
214
213
  }
215
214
  }
216
-
217
- interface SVGPaddingOptions {
218
- /** 是否保持宽高比 (默认: true) */
219
- preserveAspectRatio?: boolean;
220
- }
221
-
222
- function setSVGPadding(
223
- svg: SVGSVGElement,
224
- padding: ParsedPadding,
225
- options: SVGPaddingOptions = {},
226
- ): boolean {
227
- const { preserveAspectRatio = false } = options;
228
-
229
- if (!svg.isConnected) return false;
230
-
231
- try {
232
- const bbox = svg.getBBox();
233
-
234
- // 检查包围盒是否有效
235
- if (bbox.width === 0 || bbox.height === 0) {
236
- return false;
237
- }
238
- const [widthBaseVal, heightBaseVal] = getSizeBaseVal(svg);
239
- const svgWidth = widthBaseVal || svg.clientWidth || 0;
240
- const svgHeight = heightBaseVal || svg.clientHeight || 0;
241
-
242
- const parentElement = svg.parentElement;
243
- const effectiveWidth =
244
- svgWidth || (parentElement ? parentElement.clientWidth : 300);
245
- const effectiveHeight =
246
- svgHeight || (parentElement ? parentElement.clientHeight : 150);
247
-
248
- let viewBoxPadding: number[];
249
-
250
- if (effectiveWidth > 0 && effectiveHeight > 0) {
251
- const scaleX = bbox.width / effectiveWidth;
252
- const scaleY = bbox.height / effectiveHeight;
253
-
254
- if (preserveAspectRatio) {
255
- const scale = Math.max(scaleX, scaleY);
256
- viewBoxPadding = padding.map((p) => p * scale);
257
- } else {
258
- viewBoxPadding = [
259
- padding[0] * scaleY,
260
- padding[1] * scaleX,
261
- padding[2] * scaleY,
262
- padding[3] * scaleX,
263
- ];
264
- }
265
- } else {
266
- viewBoxPadding = [...padding];
267
- }
268
-
269
- const newViewBox = [
270
- bbox.x - viewBoxPadding[3],
271
- bbox.y - viewBoxPadding[0],
272
- bbox.width + viewBoxPadding[1] + viewBoxPadding[3],
273
- bbox.height + viewBoxPadding[0] + viewBoxPadding[2],
274
- ].join(' ');
275
-
276
- svg.setAttribute('viewBox', newViewBox);
277
-
278
- return true;
279
- } catch {
280
- return false;
281
- }
282
- }
@@ -5,7 +5,7 @@ import { getSimpleHash } from './hash';
5
5
  export function getResourceId(config: string | ResourceConfig): string | null {
6
6
  const cfg = typeof config === 'string' ? parseDataURI(config) : config;
7
7
  if (!cfg) return null;
8
- return getSimpleHash(JSON.stringify(cfg));
8
+ return 'rsc-' + getSimpleHash(JSON.stringify(cfg));
9
9
  }
10
10
 
11
11
  export function getResourceHref(config: string | ResourceConfig) {