UNPKG

dom-to-vector-pdf

Version:

Convert DOM elements to vector PDFs using jsPDF, dom-to-svg and svg2pdf.js

320 lines (312 loc) 10.8 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('jspdf'), require('dom-to-svg'), require('svg2pdf.js')) : typeof define === 'function' && define.amd ? define(['exports', 'jspdf', 'dom-to-svg', 'svg2pdf.js'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.DOMToPDF = {}, global.jspdf, global.domToSvg, global.svg2pdf_js)); })(this, (function (exports, jspdf, domToSvg, svg2pdf_js) { 'use strict'; /** * Convert font weight * @param weight Font weight * @returns Normalized font weight */ function normalizeFontWeight(weight) { const weightMap = { normal: '400', bold: '700', }; return weightMap[weight?.toString() || 'normal'] || weight?.toString() || '400'; } /** * Calculate SVG symbol scale ratio */ function calculateSymbolScale(symbol) { const viewBox = symbol.getAttribute('viewBox'); if (!viewBox) { return 1; } const [, , width] = viewBox.split(' ').map(Number); // 1em 通常计算的像素值 const expectedSize = 16; return expectedSize / width; } /** * Symbol element in inline SVG */ function inlineSvgSymbols(element) { const uses = element.querySelectorAll('use'); uses.forEach((use) => { const href = use.getAttribute('xlink:href') || use.getAttribute('href'); if (!href) { return; } const symbol = document.querySelector(href); if (!symbol) { return; } // Create <g> container preserving all attributes const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); // Copy all attributes except href Array.from(use.attributes).forEach((attr) => { if (attr.name !== 'xlink:href' && attr.name !== 'href') { g.setAttribute(attr.name, attr.value); } }); // Insert scaled path g.innerHTML = ` <g transform="scale(${calculateSymbolScale(symbol)})"> ${symbol.innerHTML} </g> `; // Replace and preserve parent SVG dimensions use.replaceWith(g); }); } /** * Recursively process SVG element font attributes */ function processSvgFonts(element, fontManager) { if (element.classList.contains('no-print')) { element.remove(); return; } if (element.tagName === 'text' || element.tagName === 'tspan') { // Parse style string const style = element.getAttribute('style'); if (style) { style.split(';').forEach((css) => { const [key, value] = css.split(':'); if (!key) return; element.setAttribute(key.trim(), value?.trim()); }); } element.removeAttribute('style'); const fontFamily = element.getAttribute('font-family'); const fontWeight = element.getAttribute('font-weight'); // TODO if (fontFamily) { element.setAttribute('font-family', fontManager.getFontId()); element.setAttribute('font-weight', normalizeFontWeight(fontWeight)); } // Adjust y coordinate const y = element.getAttribute('y'); if (y) { element.setAttribute('y', String(Number(y) - 3)); } } // Recursively process child elements Array.from(element.children).forEach((child) => processSvgFonts(child, fontManager)); } /** * Font manager */ class FontManager { constructor() { this.registeredFonts = new Map(); this.callbackList = []; this.fontId = 'PingFang'; } /** * Get font ID */ getFontId() { return this.fontId; } /** * Get font manager singleton */ static getInstance() { if (!FontManager.instance) { FontManager.instance = new FontManager(); } return FontManager.instance; } /** * Set PDF instance */ setPdfInstance(pdf) { this.pdfInstance = pdf; this.callbackList.forEach((callback) => callback()); this.callbackList = []; } /** * Register font */ registerFont(options) { this.fontId = options.fontId; this.addFontToPdf(options); } /** * Batch register fonts */ registerFonts(options) { options.map((font) => this.registerFont(font)); } /** * Add font to PDF instance */ addFontToPdf(options) { if (!this.pdfInstance) { this.callbackList.push(() => this.addFontToPdf(options)); return; } this.pdfInstance.addFont(options.font, options.fontId, options.fontStyle || 'normal', normalizeFontWeight(options.fontWeight)); } } /** * DOM to PDF Converter */ class DomToPdfConverter { constructor() { this.resourceQueue = []; this.fontManager = FontManager.getInstance(); } /** * Export PDF */ async exportPdf(options, hooks) { try { // 1. Get and clone DOM element const { element, parentElement } = this.prepareDomElement(options.id); // Call lifecycle hook hooks?.afterDomClone?.(element); // 2. Process SVG symbols inlineSvgSymbols(element); // 3. Load resource await this.loadResource(element); // 4. Convert to SVG const svgDocument = domToSvg.elementToSVG(element); parentElement?.removeChild(element); const svgElement = svgDocument.documentElement; document.body.appendChild(svgElement); this.prepareSvgElement(svgElement); // 5. Process SVG fonts processSvgFonts(svgElement, this.fontManager); // Call lifecycle hook hooks?.beforeSvgConvert?.(svgElement); // 6. Create PDF document const pdf = this.createPdfDocument(svgElement); this.fontManager.setPdfInstance(pdf); // 7. Draw SVG content to PDF await this.renderSvgToPdf(svgElement, pdf); // Call lifecycle hook hooks?.beforePdfGenerate?.(pdf); hooks?.beforePdfSave?.(pdf); // 8. Save PDF pdf.save(`${options.filename}.pdf`); // 9. Clean up temporary elements svgElement.remove(); this.fontManager.setPdfInstance(null); } catch (error) { console.error('生成PDF失败:', error); throw error; } } /** * Prepare DOM element */ prepareDomElement(id) { const originElement = document.querySelector(id); if (!originElement) { throw new Error(`Element with id "${id}" not found`); } const parentElement = originElement.parentElement; const element = originElement.cloneNode(true); // Set cloned element styles element.style.cssText = ` z-index: -999999; position: absolute; top: 0; left: 0; `; console.log(parentElement, '??????'); parentElement?.appendChild(element); return { element, parentElement }; } /** * Prepare SVG element */ prepareSvgElement(svgElement) { svgElement.style.cssText = ` all: unset; width: 100%; position: absolute; top: 0; left: 0; z-index: -999999; `; // Add XML declaration const utf8Declaration = document.createTextNode('<?xml version="1.0" encoding="utf-8"?>'); svgElement.insertBefore(utf8Declaration, svgElement.firstChild); } /** * Create PDF document */ createPdfDocument(svgElement) { const { width, height } = svgElement.getBoundingClientRect(); return new jspdf.jsPDF({ orientation: 'portrait', unit: 'px', format: [width, height], }); } /** * Render SVG to PDF */ async renderSvgToPdf(svgElement, pdf) { await svg2pdf_js.svg2pdf(svgElement, pdf, { x: 0, y: 0, width: pdf.internal.pageSize.getWidth(), height: pdf.internal.pageSize.getHeight(), }); } /** * Load resource */ async loadResource(element) { this.resourceQueue = []; const resources = element.querySelectorAll('img'); resources.forEach((resource) => { this.resourceQueue.push(new Promise((resolve) => { resource.onload = () => resolve(void 0); })); }); return Promise.allSettled(this.resourceQueue); } } /** * DOM to PDF tool instance */ class DOMToPDF { constructor() { this.converter = new DomToPdfConverter(); this.fontManager = FontManager.getInstance(); } /** * Export PDF * @param options Export configuration * @param hooks Lifecycle hooks */ async exportPDF(options, hooks) { await this.converter.exportPdf(options, hooks); } /** * Register font * @param options Font registration options */ registerFont(options) { if (Array.isArray(options)) { this.fontManager.registerFonts(options); } else { this.fontManager.registerFont(options); } } } // Export singleton instance const instance = new DOMToPDF(); exports["default"] = instance; exports.instance = instance; Object.defineProperty(exports, '__esModule', { value: true }); }));