@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.
- package/README.md +8 -12
- package/README.zh-CN.md +8 -9
- package/dist/infographic.min.js +45 -34
- package/dist/infographic.min.js.map +1 -1
- package/esm/designs/components/BtnsGroup.js +1 -1
- package/esm/designs/structures/hierarchy-tree.js +13 -6
- package/esm/exporter/font.d.ts +18 -0
- package/esm/exporter/font.js +202 -0
- package/esm/exporter/index.d.ts +3 -0
- package/esm/exporter/index.js +2 -0
- package/esm/exporter/png.d.ts +2 -0
- package/esm/exporter/png.js +42 -0
- package/esm/exporter/svg.d.ts +3 -0
- package/esm/exporter/svg.js +72 -0
- package/esm/exporter/types.d.ts +17 -0
- package/esm/index.d.ts +2 -2
- package/esm/renderer/composites/text.js +1 -3
- package/esm/renderer/fonts/built-in.d.ts +1 -1
- package/esm/renderer/fonts/index.d.ts +0 -2
- package/esm/renderer/fonts/index.js +0 -1
- package/esm/renderer/fonts/loader.js +1 -2
- package/esm/renderer/fonts/registry.d.ts +1 -1
- package/esm/renderer/fonts/registry.js +1 -1
- package/esm/renderer/renderer.js +1 -50
- package/esm/resource/utils/ref.js +1 -1
- package/esm/runtime/Infographic.d.ts +10 -0
- package/esm/runtime/Infographic.js +23 -2
- package/esm/types/font.js +1 -0
- package/esm/types/index.d.ts +1 -0
- package/{lib/renderer/fonts/utils.d.ts → esm/utils/font.d.ts} +1 -1
- package/esm/utils/index.d.ts +2 -0
- package/esm/utils/index.js +2 -0
- package/esm/utils/is-node.d.ts +4 -0
- package/esm/utils/is-node.js +6 -0
- package/esm/utils/padding.d.ts +1 -0
- package/esm/utils/padding.js +70 -0
- package/esm/utils/svg.d.ts +0 -1
- package/esm/utils/svg.js +3 -18
- package/esm/utils/text.js +1 -1
- package/esm/utils/viewbox.d.ts +6 -0
- package/esm/utils/viewbox.js +12 -0
- package/lib/designs/components/BtnsGroup.js +1 -1
- package/lib/designs/structures/hierarchy-tree.js +31 -24
- package/lib/exporter/font.d.ts +18 -0
- package/lib/exporter/font.js +210 -0
- package/lib/exporter/index.d.ts +3 -0
- package/lib/exporter/index.js +7 -0
- package/lib/exporter/png.d.ts +2 -0
- package/lib/exporter/png.js +45 -0
- package/lib/exporter/svg.d.ts +3 -0
- package/lib/exporter/svg.js +76 -0
- package/lib/exporter/types.d.ts +17 -0
- package/lib/index.d.ts +2 -2
- package/lib/renderer/composites/text.js +1 -3
- package/lib/renderer/fonts/built-in.d.ts +1 -1
- package/lib/renderer/fonts/index.d.ts +0 -2
- package/lib/renderer/fonts/index.js +1 -4
- package/lib/renderer/fonts/loader.js +1 -2
- package/lib/renderer/fonts/registry.d.ts +1 -1
- package/lib/renderer/fonts/registry.js +1 -1
- package/lib/renderer/renderer.js +1 -50
- package/lib/resource/utils/ref.js +1 -1
- package/lib/runtime/Infographic.d.ts +10 -0
- package/lib/runtime/Infographic.js +23 -2
- package/lib/types/font.js +2 -0
- package/lib/types/index.d.ts +1 -0
- package/{esm/renderer/fonts/utils.d.ts → lib/utils/font.d.ts} +1 -1
- package/lib/utils/index.d.ts +2 -0
- package/lib/utils/index.js +2 -0
- package/lib/utils/is-node.d.ts +4 -0
- package/lib/utils/is-node.js +9 -0
- package/lib/utils/padding.d.ts +1 -0
- package/lib/utils/padding.js +71 -0
- package/lib/utils/svg.d.ts +0 -1
- package/lib/utils/svg.js +3 -19
- package/lib/utils/text.js +2 -2
- package/lib/utils/viewbox.d.ts +6 -0
- package/lib/utils/viewbox.js +15 -0
- package/package.json +5 -2
- package/src/designs/components/BtnsGroup.tsx +7 -1
- package/src/designs/structures/hierarchy-tree.tsx +14 -8
- package/src/exporter/font.ts +273 -0
- package/src/exporter/index.ts +7 -0
- package/src/exporter/png.ts +58 -0
- package/src/exporter/svg.ts +94 -0
- package/src/exporter/types.ts +19 -0
- package/src/index.ts +7 -3
- package/src/renderer/composites/text.ts +1 -2
- package/src/renderer/fonts/built-in.ts +1 -1
- package/src/renderer/fonts/index.ts +1 -3
- package/src/renderer/fonts/loader.ts +1 -2
- package/src/renderer/fonts/registry.ts +2 -2
- package/src/renderer/renderer.ts +1 -69
- package/src/resource/utils/ref.ts +1 -1
- package/src/runtime/Infographic.tsx +30 -2
- package/src/types/index.ts +1 -0
- package/src/{renderer/fonts/utils.ts → utils/font.ts} +1 -1
- package/src/utils/index.ts +2 -0
- package/src/utils/is-node.ts +8 -0
- package/src/utils/padding.ts +79 -0
- package/src/utils/svg.ts +5 -19
- package/src/utils/text.ts +2 -2
- package/src/utils/viewbox.ts +12 -0
- /package/esm/{renderer/fonts → exporter}/types.js +0 -0
- /package/esm/{renderer/fonts/types.d.ts → types/font.d.ts} +0 -0
- /package/esm/{renderer/fonts/utils.js → utils/font.js} +0 -0
- /package/lib/{renderer/fonts → exporter}/types.js +0 -0
- /package/lib/{renderer/fonts/types.d.ts → types/font.d.ts} +0 -0
- /package/lib/{renderer/fonts/utils.js → utils/font.js} +0 -0
- /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 = (
|
|
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
|
-
|
|
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
|
|
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={
|
|
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,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 {
|
|
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
|
-
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 '
|
|
2
|
-
import { decodeFontFamily, encodeFontFamily } from '
|
|
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
|
|
package/src/renderer/renderer.ts
CHANGED
|
@@ -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) {
|