dom-to-vector-pdf 1.0.0 → 1.0.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 XZ
3
+ Copyright (c) 2025 xzboss
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,2 +1,117 @@
1
1
  # dom-to-vector-pdf
2
- DOM转矢量PDF
2
+
3
+ A tool for converting DOM elements to vector PDFs using jsPDF, dom-to-svg and svg2pdf.js.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install dom-to-vector-pdf
9
+ ```
10
+
11
+ ## Configuration Options
12
+
13
+ ### Export Options
14
+
15
+ | Option | Type | Default | Description |
16
+ |--------|------|---------|-------------|
17
+ | selector | string | required | CSS selector for DOM element to export |
18
+ | filename | string | required | Exported PDF file name |
19
+ | orientation | 'portrait' \| 'landscape' | 'portrait' | PDF orientation |
20
+ | unit | 'px' | Unit for measurements(only px) |
21
+ | beforeSvgConvert | (svgElement: SVGElement) => void | - | Custom hook for processing SVG elements |
22
+ | beforePdfSave | (pdf: jsPDF) => void | - | Custom hook for processing PDF document |
23
+
24
+ ### Font Options
25
+
26
+ | Option | Type | Default | Description |
27
+ |--------|------|---------|-------------|
28
+ | font | string | required | Font file path or URL |
29
+ | fontId | string | required | Font ID for identifying the font |
30
+ | fontStyle | 'normal' \| 'italic' | 'normal' | Font style |
31
+ | fontWeight | string \| number | - | Font weight (100-900) |
32
+
33
+ ### Lifecycle Hooks
34
+
35
+ | Hook | Type | Description |
36
+ |------|------|-------------|
37
+ | afterDomClone | (clonedElement: HTMLElement) => void | Triggered after DOM clone |
38
+ | beforeSvgConvert | (svgElement: SVGElement) => void | Triggered before SVG conversion |
39
+ | beforePdfGenerate | (pdf: jsPDF) => void | Triggered before PDF generation |
40
+ | beforePdfSave | (pdf: jsPDF) => void | Triggered before PDF save |
41
+
42
+ ## Basic Usage
43
+
44
+ ```javascript
45
+ import vectorInstance from "dom-to-vector-pdf";
46
+
47
+ export const ExportToPDF = (selector, title) => {
48
+ vectorInstance.registerFont([
49
+ {
50
+ font: PingFangRegular,
51
+ fontId: "PingFang",
52
+ fontWeight: "400",
53
+ fontStyle: "normal",
54
+ },
55
+ {
56
+ font: PingFangHeavy,
57
+ fontId: "PingFang",
58
+ fontWeight: "700",
59
+ fontStyle: "normal",
60
+ },
61
+ ]);
62
+ vectorInstance.exportPDF({
63
+ selector,
64
+ filename: title,
65
+ });
66
+ };
67
+ ```
68
+
69
+ ## Features
70
+
71
+ - Converts DOM elements to vector PDFs
72
+ - Preserves vector graphics and text
73
+ - Supports SVG elements
74
+ - Maintains font styles and weights
75
+ - Handles complex layouts
76
+
77
+ ## Todo List
78
+
79
+ ### DOM Cloning
80
+ - [ ] Inline style handling
81
+ - [ ] Style priority management
82
+ - [ ] Shadow DOM support
83
+ - [ ] iframe support
84
+
85
+ ### Icon Fonts
86
+ - [ ] Current implementation uses 16px as base font size for scaling
87
+ - [ ] Need to improve icon font size handling
88
+
89
+ ### SVG Support
90
+ - [ ] Currently only supports inline styles where property names match element attributes
91
+ - [ ] Need to enhance SVG style handling
92
+
93
+ ### Text Alignment
94
+ - [ ] Text appears slightly lower than background
95
+ - Current workaround: Shift all text up by 3 pixels
96
+
97
+ ### Unsupported Features
98
+ - [ ✅ ] Image background export
99
+ - [ ] Canvas export
100
+ - [ ] other unit
101
+
102
+ ### Font Support
103
+ - [ ] Currently limited to single font family
104
+ - Font ID must be consistent during registration
105
+ - [ ] Need to add support for multiple fonts
106
+ - [ ] Consider WOFF2 format compatibility
107
+
108
+ ### Image Export
109
+ - [ ] Image export quality needs improvement
110
+
111
+ ## Contributing
112
+
113
+ Contributions are welcome! Please feel free to submit a Pull Request.
114
+
115
+ ## License
116
+
117
+ MIT
@@ -0,0 +1,117 @@
1
+ # dom-to-vector-pdf
2
+
3
+ 一个使用 jsPDF、dom-to-svg 和 svg2pdf.js 将 DOM 元素转换为矢量 PDF 的工具。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install dom-to-vector-pdf
9
+ ```
10
+
11
+ ## 配置选项
12
+
13
+ ### 导出选项
14
+
15
+ | 选项 | 类型 | 默认值 | 说明 |
16
+ |------|------|--------|------|
17
+ | selector | string | 必填 | 要导出的DOM元素的CSS选择器 |
18
+ | filename | string | 必填 | 导出的PDF文件名 |
19
+ | orientation | 'portrait' \| 'landscape' | 'portrait' | PDF方向 |
20
+ | unit | 'px' | 测量单位(只支持px) |
21
+ | beforeSvgConvert | (svgElement: SVGElement) => void | - | SVG元素处理钩子 |
22
+ | beforePdfSave | (pdf: jsPDF) => void | - | PDF文档处理钩子 |
23
+
24
+ ### 字体选项
25
+
26
+ | 选项 | 类型 | 默认值 | 说明 |
27
+ |------|------|--------|------|
28
+ | font | string | 必填 | 字体文件路径或URL |
29
+ | fontId | string | 必填 | 字体ID |
30
+ | fontStyle | 'normal' \| 'italic' | 'normal' | 字体样式 |
31
+ | fontWeight | string \| number | - | 字体粗细(100-900) |
32
+
33
+ ### 生命周期钩子
34
+
35
+ | 钩子 | 类型 | 说明 |
36
+ |------|------|------|
37
+ | afterDomClone | (clonedElement: HTMLElement) => void | DOM克隆后触发 |
38
+ | beforeSvgConvert | (svgElement: SVGElement) => void | SVG转换前触发 |
39
+ | beforePdfGenerate | (pdf: jsPDF) => void | PDF生成前触发 |
40
+ | beforePdfSave | (pdf: jsPDF) => void | PDF保存前触发 |
41
+
42
+ ## 基本用法
43
+
44
+ ```javascript
45
+ import vectorInstance from "dom-to-vector-pdf";
46
+
47
+ export const ExportToPDF = (selector, title) => {
48
+ vectorInstance.registerFont([
49
+ {
50
+ font: PingFangRegular,
51
+ fontId: "PingFang",
52
+ fontWeight: "400",
53
+ fontStyle: "normal",
54
+ },
55
+ {
56
+ font: PingFangHeavy,
57
+ fontId: "PingFang",
58
+ fontWeight: "700",
59
+ fontStyle: "normal",
60
+ },
61
+ ]);
62
+ vectorInstance.exportPDF({
63
+ selector,
64
+ filename: title,
65
+ });
66
+ };
67
+ ```
68
+
69
+ ## 特性
70
+
71
+ - 将DOM元素转换为矢量PDF
72
+ - 保持矢量图形和文本
73
+ - 支持SVG元素
74
+ - 保持字体样式和粗细
75
+ - 处理复杂布局
76
+
77
+ ## 待办事项
78
+
79
+ ### DOM克隆
80
+ - [ ] 内联样式处理
81
+ - [ ] 样式优先级管理
82
+ - [ ] Shadow DOM支持
83
+ - [ ] iframe支持
84
+
85
+ ### 图标字体
86
+ - [ ] 当前实现使用16px作为基础字体大小进行缩放
87
+ - [ ] 需要改进图标字体大小处理
88
+
89
+ ### SVG支持
90
+ - [ ] 目前仅支持属性名与元素属性匹配的内联样式
91
+ - [ ] 需要增强SVG样式处理
92
+
93
+ ### 文本对齐
94
+ - [ ] 文本位置略低于背景
95
+ - 当前解决方案:将所有文本向上偏移3像素
96
+
97
+ ### 不支持的功能
98
+ - [ ✅ ] 图片背景导出
99
+ - [ ] Canvas导出
100
+ - [ ] 其他单位支持
101
+
102
+ ### 字体支持
103
+ - [ ] 目前仅限于单个字体系列
104
+ - 注册时字体ID必须保持一致
105
+ - [ ] 需要添加多字体支持
106
+ - [ ] 考虑WOFF2格式兼容性
107
+
108
+ ### 图片导出
109
+ - [ ] 图片导出质量需要改进
110
+
111
+ ## 贡献
112
+
113
+ 欢迎贡献!请随时提交Pull Request。
114
+
115
+ ## 许可证
116
+
117
+ MIT
@@ -0,0 +1,23 @@
1
+ import type { ExportPdfOptions, FontRegisterOptions, LifecycleHooks } from './types';
2
+ export type { ExportPdfOptions, FontRegisterOptions, LifecycleHooks } from './types';
3
+ /**
4
+ * DOM to PDF tool instance
5
+ */
6
+ declare class DOMToPDF {
7
+ private converter;
8
+ private fontManager;
9
+ constructor();
10
+ /**
11
+ * Export PDF
12
+ * @param options Export configuration
13
+ * @param hooks Lifecycle hooks
14
+ */
15
+ exportPDF(options: ExportPdfOptions, hooks?: LifecycleHooks): Promise<void>;
16
+ /**
17
+ * Register font
18
+ * @param options Font registration options
19
+ */
20
+ registerFont(options: FontRegisterOptions | FontRegisterOptions[]): void;
21
+ }
22
+ export declare const instance: DOMToPDF;
23
+ export default instance;
@@ -0,0 +1,349 @@
1
+ import { jsPDF } from 'jspdf';
2
+ import { elementToSVG } from 'dom-to-svg';
3
+ import { svg2pdf } from 'svg2pdf.js';
4
+
5
+ /**
6
+ * Convert font weight
7
+ * @param weight Font weight
8
+ * @returns Normalized font weight
9
+ */
10
+ function normalizeFontWeight(weight) {
11
+ const weightMap = {
12
+ normal: '400',
13
+ bold: '700',
14
+ };
15
+ return weightMap[weight?.toString() || 'normal'] || weight?.toString() || '400';
16
+ }
17
+ /**
18
+ * Calculate SVG symbol scale ratio
19
+ */
20
+ function calculateSymbolScale(symbol) {
21
+ const viewBox = symbol.getAttribute('viewBox');
22
+ if (!viewBox) {
23
+ return 1;
24
+ }
25
+ const [, , width] = viewBox.split(' ').map(Number);
26
+ // 1em 通常计算的像素值
27
+ const expectedSize = 16;
28
+ return expectedSize / width;
29
+ }
30
+ /**
31
+ * Symbol element in inline SVG
32
+ */
33
+ function inlineSvgSymbols(element) {
34
+ const uses = element.querySelectorAll('use');
35
+ uses.forEach((use) => {
36
+ const href = use.getAttribute('xlink:href') || use.getAttribute('href');
37
+ if (!href) {
38
+ return;
39
+ }
40
+ const symbol = document.querySelector(href);
41
+ if (!symbol) {
42
+ return;
43
+ }
44
+ // Create <g> container preserving all attributes
45
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
46
+ // Copy all attributes except href
47
+ Array.from(use.attributes).forEach((attr) => {
48
+ if (attr.name !== 'xlink:href' && attr.name !== 'href') {
49
+ g.setAttribute(attr.name, attr.value);
50
+ }
51
+ });
52
+ // Insert scaled path
53
+ g.innerHTML = `
54
+ <g transform="scale(${calculateSymbolScale(symbol)})">
55
+ ${symbol.innerHTML}
56
+ </g>
57
+ `;
58
+ // Replace and preserve parent SVG dimensions
59
+ use.replaceWith(g);
60
+ });
61
+ }
62
+ /**
63
+ * Recursively process SVG element font attributes
64
+ */
65
+ function processSvgFonts(element, fontManager) {
66
+ if (element.classList.contains('no-print')) {
67
+ element.remove();
68
+ return;
69
+ }
70
+ if (element.tagName === 'text' || element.tagName === 'tspan') {
71
+ // Parse style string
72
+ const style = element.getAttribute('style');
73
+ if (style) {
74
+ style.split(';').forEach((css) => {
75
+ const [key, value] = css.split(':');
76
+ if (!key)
77
+ return;
78
+ element.setAttribute(key.trim(), value?.trim());
79
+ });
80
+ }
81
+ element.removeAttribute('style');
82
+ const fontFamily = element.getAttribute('font-family');
83
+ const fontWeight = element.getAttribute('font-weight');
84
+ // TODO
85
+ if (fontFamily) {
86
+ element.setAttribute('font-family', fontManager.getFontId());
87
+ element.setAttribute('font-weight', normalizeFontWeight(fontWeight));
88
+ }
89
+ // Adjust y coordinate
90
+ const y = element.getAttribute('y');
91
+ if (y) {
92
+ element.setAttribute('y', String(Number(y) - 3));
93
+ }
94
+ }
95
+ // Recursively process child elements
96
+ Array.from(element.children).forEach((child) => processSvgFonts(child, fontManager));
97
+ }
98
+
99
+ /**
100
+ * Font manager
101
+ */
102
+ class FontManager {
103
+ constructor() {
104
+ this.registeredFonts = new Map();
105
+ this.callbackList = [];
106
+ this.fontId = 'PingFang';
107
+ }
108
+ /**
109
+ * Get font ID
110
+ */
111
+ getFontId() {
112
+ return this.fontId;
113
+ }
114
+ /**
115
+ * Get font manager singleton
116
+ */
117
+ static getInstance() {
118
+ if (!FontManager.instance) {
119
+ FontManager.instance = new FontManager();
120
+ }
121
+ return FontManager.instance;
122
+ }
123
+ /**
124
+ * Set PDF instance
125
+ */
126
+ setPdfInstance(pdf) {
127
+ this.pdfInstance = pdf;
128
+ this.callbackList.forEach((callback) => callback());
129
+ this.callbackList = [];
130
+ }
131
+ /**
132
+ * Register font
133
+ */
134
+ registerFont(options) {
135
+ this.fontId = options.fontId;
136
+ this.addFontToPdf(options);
137
+ }
138
+ /**
139
+ * Batch register fonts
140
+ */
141
+ registerFonts(options) {
142
+ options.map((font) => this.registerFont(font));
143
+ }
144
+ /**
145
+ * Add font to PDF instance
146
+ */
147
+ addFontToPdf(options) {
148
+ if (!this.pdfInstance) {
149
+ this.callbackList.push(() => this.addFontToPdf(options));
150
+ return;
151
+ }
152
+ this.pdfInstance.addFont(options.font, options.fontId, options.fontStyle || 'normal', normalizeFontWeight(options.fontWeight));
153
+ }
154
+ }
155
+
156
+ const isNullOrUndefined = (value) => {
157
+ return value === null || value === undefined;
158
+ };
159
+
160
+ /**
161
+ * DOM to PDF Converter
162
+ */
163
+ class DomToPdfConverter {
164
+ constructor() {
165
+ this.resourceQueue = [];
166
+ this.exportOptions = null;
167
+ this.fontManager = FontManager.getInstance();
168
+ }
169
+ setExportOptions(options) {
170
+ this.exportOptions = options;
171
+ }
172
+ /**
173
+ * Export PDF
174
+ */
175
+ async exportPdf(options, hooks) {
176
+ try {
177
+ this.setExportOptions(options);
178
+ // 1. Get and clone DOM element
179
+ const { element, parentElement } = this.prepareDomElement(options.selector);
180
+ console.log(1);
181
+ // Call lifecycle hook
182
+ hooks?.afterDomClone?.(element);
183
+ console.log(2);
184
+ // 2. Process SVG symbols
185
+ inlineSvgSymbols(element);
186
+ console.log(3);
187
+ // 3. Load resource
188
+ await this.loadResource(element);
189
+ console.log(4);
190
+ // 4. Convert to SVG
191
+ const svgDocument = elementToSVG(element);
192
+ parentElement?.removeChild(element);
193
+ console.log(5);
194
+ const svgElement = svgDocument.documentElement;
195
+ document.body.appendChild(svgElement);
196
+ this.prepareSvgElement(svgElement);
197
+ console.log(6);
198
+ // 5. Process SVG fonts
199
+ processSvgFonts(svgElement, this.fontManager);
200
+ console.log(7);
201
+ // Call lifecycle hook
202
+ hooks?.beforeSvgConvert?.(svgElement);
203
+ console.log(8);
204
+ // 6. Create PDF document
205
+ const pdf = this.createPdfDocument(svgElement);
206
+ this.fontManager.setPdfInstance(pdf);
207
+ console.log(9);
208
+ // 7. Draw SVG content to PDF
209
+ await this.renderSvgToPdf(svgElement, pdf);
210
+ console.log(10);
211
+ // Call lifecycle hook
212
+ hooks?.beforePdfGenerate?.(pdf);
213
+ hooks?.beforePdfSave?.(pdf);
214
+ console.log(11);
215
+ // 8. Save PDF
216
+ pdf.save(`${options.filename}.pdf`);
217
+ console.log(12);
218
+ // 9. Clean up temporary elements
219
+ svgElement.remove();
220
+ this.fontManager.setPdfInstance(null);
221
+ console.log(13);
222
+ }
223
+ catch (error) {
224
+ console.error('生成PDF失败:', error);
225
+ throw error;
226
+ }
227
+ }
228
+ /**
229
+ * Prepare DOM element
230
+ */
231
+ prepareDomElement(selector) {
232
+ const originElement = document.querySelector(selector);
233
+ if (!originElement) {
234
+ throw new Error(`Element with selector "${selector}" not found`);
235
+ }
236
+ const parentElement = originElement.parentElement;
237
+ const element = originElement.cloneNode(true);
238
+ // Set cloned element styles
239
+ element.style.cssText = `
240
+ z-index: -999999;
241
+ position: absolute;
242
+ top: 0;
243
+ left: 0;
244
+ `;
245
+ parentElement?.appendChild(element);
246
+ return { element, parentElement };
247
+ }
248
+ /**
249
+ * Prepare SVG element
250
+ */
251
+ prepareSvgElement(svgElement) {
252
+ svgElement.style.cssText = `
253
+ all: unset;
254
+ width: 100%;
255
+ position: absolute;
256
+ top: 0;
257
+ left: 0;
258
+ z-index: -999999;
259
+ `;
260
+ // Add XML declaration
261
+ const utf8Declaration = document.createTextNode('<?xml version="1.0" encoding="utf-8"?>');
262
+ svgElement.insertBefore(utf8Declaration, svgElement.firstChild);
263
+ }
264
+ /**
265
+ * Create PDF document
266
+ */
267
+ createPdfDocument(svgElement) {
268
+ const { width, height } = svgElement.getBoundingClientRect();
269
+ return new jsPDF({
270
+ orientation: 'portrait',
271
+ unit: 'px',
272
+ format: [width, height],
273
+ });
274
+ }
275
+ /**
276
+ * Render SVG to PDF
277
+ */
278
+ async renderSvgToPdf(svgElement, pdf) {
279
+ await svg2pdf(svgElement, pdf, {
280
+ x: 0,
281
+ y: 0,
282
+ width: pdf.internal.pageSize.getWidth(),
283
+ height: pdf.internal.pageSize.getHeight(),
284
+ });
285
+ }
286
+ /**
287
+ * Load resource
288
+ */
289
+ async loadResource(element) {
290
+ this.resourceQueue = [];
291
+ const resources = element.querySelectorAll('img');
292
+ resources.forEach((resource) => {
293
+ this.resourceQueue.push(new Promise((resolve) => {
294
+ let done = false;
295
+ resource.onload = () => {
296
+ done = true;
297
+ resolve(resource.src);
298
+ };
299
+ resource.onerror = () => {
300
+ done = true;
301
+ resolve();
302
+ };
303
+ setTimeout(() => {
304
+ if (!done) {
305
+ resolve(void 0);
306
+ }
307
+ }, isNullOrUndefined(this.exportOptions?.resourceTimeout)
308
+ ? 5000
309
+ : this.exportOptions?.resourceTimeout);
310
+ }));
311
+ });
312
+ console.log(this.resourceQueue, 'this.resourceQueue');
313
+ return Promise.allSettled(this.resourceQueue);
314
+ }
315
+ }
316
+
317
+ /**
318
+ * DOM to PDF tool instance
319
+ */
320
+ class DOMToPDF {
321
+ constructor() {
322
+ this.converter = new DomToPdfConverter();
323
+ this.fontManager = FontManager.getInstance();
324
+ }
325
+ /**
326
+ * Export PDF
327
+ * @param options Export configuration
328
+ * @param hooks Lifecycle hooks
329
+ */
330
+ async exportPDF(options, hooks) {
331
+ await this.converter.exportPdf(options, hooks);
332
+ }
333
+ /**
334
+ * Register font
335
+ * @param options Font registration options
336
+ */
337
+ registerFont(options) {
338
+ if (Array.isArray(options)) {
339
+ this.fontManager.registerFonts(options);
340
+ }
341
+ else {
342
+ this.fontManager.registerFont(options);
343
+ }
344
+ }
345
+ }
346
+ // Export singleton instance
347
+ const instance = new DOMToPDF();
348
+
349
+ export { instance as default, instance };