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
JavaScript
(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 });
}));