UNPKG

@web-js/dom-screenshot

Version:

A lightweight DOM screenshot library based on @zumer/snapdom and html2canvas.

787 lines (778 loc) 31.6 kB
import html2canvas from 'html2canvas'; import { preCache, snapdom } from '@zumer/snapdom'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /** * 处理SVG元素并返回恢复函数 * @param element - 要处理的DOM元素 * @returns 恢复原始状态的函数 */ function processSvgElementsWithRestore(element) { const svgElements = element.querySelectorAll("svg"); const restoreData = []; svgElements.forEach((svg) => { const originalWidth = svg.getAttribute("width"); const originalHeight = svg.getAttribute("height"); const originalXmlns = svg.getAttribute("xmlns"); const styleChanges = []; // 确保SVG有明确的尺寸 if (!originalWidth && !originalHeight) { const rect = svg.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { svg.setAttribute("width", rect.width.toString()); svg.setAttribute("height", rect.height.toString()); } } // 设置SVG的xmlns属性 if (!originalXmlns) { svg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); } // 处理SVG内部的样式 const styleElements = svg.querySelectorAll("style"); styleElements.forEach((style) => { const originalContent = style.textContent; if (originalContent) { const trimmedContent = originalContent.trim(); if (trimmedContent !== originalContent) { style.textContent = trimmedContent; styleChanges.push({ style, originalContent }); } } }); restoreData.push({ svg, originalWidth, originalHeight, originalXmlns, styleChanges, }); }); // 返回恢复函数 return () => { restoreData.forEach(({ svg, originalWidth, originalHeight, originalXmlns, styleChanges }) => { // 恢复尺寸属性 if (originalWidth === null) { svg.removeAttribute("width"); } else { svg.setAttribute("width", originalWidth); } if (originalHeight === null) { svg.removeAttribute("height"); } else { svg.setAttribute("height", originalHeight); } // 恢复xmlns属性 if (originalXmlns === null) { svg.removeAttribute("xmlns"); } else { svg.setAttribute("xmlns", originalXmlns); } // 恢复样式内容 styleChanges.forEach(({ style, originalContent }) => { style.textContent = originalContent; }); }); }; } /** * 处理CSS渐变文本并返回恢复函数 * 使用Canvas绘制渐变文字替代原始元素,确保截图中显示渐变文字效果 * @param element - 要处理的DOM元素 * @returns 恢复原始状态的函数 */ function processGradientTextWithRestore(element) { const walker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT, null); const elementsToRestore = []; let node; // 收集所有需要处理的元素 while ((node = walker.nextNode())) { const el = node; const computedStyle = window.getComputedStyle(el); // 检查是否有渐变背景或渐变文本 const backgroundImage = computedStyle.backgroundImage; const webkitBackgroundClip = computedStyle.webkitBackgroundClip; if (backgroundImage && backgroundImage.includes("gradient") && (webkitBackgroundClip === "text" || computedStyle.backgroundClip === "text")) { try { // 创建Canvas元素绘制渐变文字 const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (!ctx) { console.warn("无法创建Canvas上下文"); continue; } // 获取元素的尺寸和文本内容 const rect = el.getBoundingClientRect(); const text = el.textContent || el.innerText || ""; if (!text.trim()) { continue; // 跳过空文本元素 } const cssText = el.style.cssText; const className = el.className; // 复制元素的类名和内联样式 canvas.className = className; canvas.style.cssText = cssText; canvas.style.background = "unset"; canvas.style.pointerEvents = "none"; // 设置Canvas尺寸(使用设备像素比以获得清晰效果) const dpr = window.devicePixelRatio || 1; canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; canvas.style.width = rect.width + "px"; canvas.style.height = rect.height + "px"; // 缩放上下文以适应设备像素比 ctx.scale(dpr, dpr); // 获取字体样式 const fontSize = parseFloat(computedStyle.fontSize); const fontFamily = computedStyle.fontFamily; const fontWeight = computedStyle.fontWeight; const fontStyle = computedStyle.fontStyle; const lineHeight = parseFloat(computedStyle.lineHeight) || fontSize * 1.2; ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`; ctx.textBaseline = "top"; // 创建渐变 const gradient = createCanvasGradient(ctx, backgroundImage, parseFloat(computedStyle.width) || rect.width, parseFloat(computedStyle.height) || rect.height); if (gradient) { ctx.fillStyle = gradient; } else { // 降级方案:使用纯色 ctx.fillStyle = computedStyle.color || "#000000"; } // 绘制文本(支持多行) const lines = text.split("\n"); const textAlign = computedStyle.textAlign; lines.forEach((line, index) => { let x = 0; if (textAlign === "center") { x = rect.width / 2; ctx.textAlign = "center"; } else if (textAlign === "right") { x = rect.width; ctx.textAlign = "right"; } else { ctx.textAlign = "left"; } const y = index * lineHeight; ctx.fillText(line, x, y); }); // 保存原始状态 const originalDisplay = el.style.display; const originalVisibility = el.style.visibility; // 隐藏原始元素 el.style.display = "none"; el.style.visibility = "hidden"; // 将Canvas插入到元素的父容器中 if (el.parentNode) { // 设置相对定位的包装器 el.parentNode.insertBefore(canvas, el); } elementsToRestore.push({ element: el, canvas, originalDisplay, originalVisibility, }); } catch (error) { console.warn("处理渐变文本时出错:", error); } } } // 返回恢复函数 return () => { elementsToRestore.forEach(({ element: el, canvas, originalDisplay, originalVisibility }) => { try { // 移除Canvas及其包装器 const wrapper = canvas.parentNode; if (wrapper) { wrapper.removeChild(canvas); } // 恢复原始元素的显示状态 if (originalDisplay) { el.style.display = originalDisplay; } else { el.style.removeProperty("display"); } if (originalVisibility) { el.style.visibility = originalVisibility; } else { el.style.removeProperty("visibility"); } } catch (error) { console.warn("恢复渐变文本时出错:", error); } }); }; } /** * 解析颜色停止点,提取颜色和位置信息 * @param stopStr - 颜色停止点字符串,如 'red 50%' 或 'rgb(255,0,0) 25%' * @returns 包含颜色和位置的对象 */ function parseColorStop(stopStr) { const trimmed = stopStr.trim(); // 更精确的正则表达式:匹配末尾的位置信息,支持空格分隔 const positionMatch = trimmed.match(/^(.+?)\s+(\d+(?:\.\d+)?)(%|px)\s*$/); let color = null; let position = null; if (positionMatch) { // 有位置信息 color = positionMatch[1].trim(); const posValue = parseFloat(positionMatch[2]); const unit = positionMatch[3]; // 转换为0-1之间的值 if (unit === "%") { position = Math.max(0, Math.min(1, posValue / 100)); } else if (unit === "px") { // 像素值需要相对于容器尺寸计算,这里简化处理 position = Math.max(0, Math.min(1, posValue / 100)); } } else { // 没有位置信息,整个字符串都是颜色 color = trimmed; } // 验证颜色值是否有效 if (color && !isValidColor(color)) { console.warn(`无效的颜色格式: ${color}`); color = null; } return { color, position }; } /** * 验证颜色值是否有效 * @param color - 颜色字符串 * @returns 是否为有效颜色 */ function isValidColor(color) { // 基本的颜色格式验证 const colorPatterns = [ /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, // hex /^rgb\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/, // rgb /^rgba\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[0-9.]+\s*\)$/, // rgba /^hsl\s*\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*\)$/, // hsl /^hsla\s*\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*,\s*[0-9.]+\s*\)$/, // hsla /^(red|green|blue|yellow|orange|purple|pink|brown|black|white|gray|grey|transparent|currentColor)$/i, // 命名颜色 ]; return colorPatterns.some((pattern) => pattern.test(color.trim())); } /** * 智能分割CSS渐变内容,正确处理包含逗号的颜色值 * @param content - 渐变内容字符串 * @returns 分割后的部分数组 */ function smartSplitGradient(content) { const parts = []; let current = ""; let depth = 0; let inQuotes = false; let quoteChar = ""; for (let i = 0; i < content.length; i++) { const char = content[i]; if (!inQuotes && (char === '"' || char === "'")) { inQuotes = true; quoteChar = char; } else if (inQuotes && char === quoteChar) { inQuotes = false; quoteChar = ""; } else if (!inQuotes && char === "(") { depth++; } else if (!inQuotes && char === ")") { depth--; } else if (!inQuotes && char === "," && depth === 0) { parts.push(current.trim()); current = ""; continue; } current += char; } if (current.trim()) { parts.push(current.trim()); } return parts; } /** * 从CSS渐变字符串创建Canvas渐变 * @param ctx - Canvas 2D上下文 * @param gradientStr - CSS渐变字符串 * @param width - 渐变宽度 * @param height - 渐变高度 * @returns Canvas渐变对象或null */ function createCanvasGradient(ctx, gradientStr, width, height) { try { // 解析线性渐变 const linearMatch = gradientStr.match(/linear-gradient\((.+)\)$/); if (linearMatch) { const gradientContent = linearMatch[1]; const parts = smartSplitGradient(gradientContent); // 默认方向为从左到右 let x0 = 0, y0 = 0, x1 = width, y1 = height; // 解析方向 const firstPart = parts[0]; let colorStartIndex = 0; if (firstPart.includes("deg")) { const angle = parseFloat(firstPart); // 调整角度使其符合CSS渐变的方向定义(45deg = 左下到右上) const adjustedAngle = 90 - angle; const rad = (adjustedAngle * Math.PI) / 180; // 计算渐变起点和终点,确保完全覆盖元素 const centerX = width / 2; const centerY = height / 2; // 计算最大渐变距离 // 从中心点到四个角的最大距离 const distances = [ Math.abs(centerX / Math.cos(rad)), Math.abs(centerY / Math.sin(rad)), Math.abs((width - centerX) / Math.cos(rad)), Math.abs((height - centerY) / Math.sin(rad)), ].filter((d) => isFinite(d)); const maxDistance = Math.max(...distances) * 2; // 乘2确保覆盖整个元素 // 计算渐变起点和终点 x0 = centerX - (maxDistance * Math.cos(rad)) / 2; y0 = centerY - (maxDistance * Math.sin(rad)) / 2; x1 = centerX + (maxDistance * Math.cos(rad)) / 2; y1 = centerY + (maxDistance * Math.sin(rad)) / 2; colorStartIndex = 1; } else if (firstPart.includes("to right")) { x1 = width; y1 = 0; colorStartIndex = 1; } else if (firstPart.includes("to left")) { x0 = width; y0 = 0; x1 = 0; y1 = 0; colorStartIndex = 1; } else if (firstPart.includes("to bottom")) { // 默认方向,不需要改变 colorStartIndex = 1; } else if (firstPart.includes("to top")) { x0 = 0; y0 = height; x1 = 0; y1 = 0; colorStartIndex = 1; } else if (firstPart.includes("to ")) { // 其他方向关键词,跳过 colorStartIndex = 1; } const gradient = ctx.createLinearGradient(x0, y0, x1, y1); // 解析颜色停止点 const colorStops = parts.slice(colorStartIndex); if (colorStops.length === 0) { console.warn("没有找到颜色停止点"); return null; } // 解析每个颜色停止点的颜色和位置 const parsedStops = colorStops .map((stop, index) => { const trimmedStop = stop.trim(); // 尝试解析颜色和位置 // 支持格式:'red', 'red 50%', 'rgb(255,0,0)', 'rgb(255,0,0) 25%', 'rgba(255,0,0,0.5) 75%' const colorWithPosition = parseColorStop(trimmedStop); // 如果没有指定位置,使用平均分布 if (colorWithPosition.position === null) { colorWithPosition.position = colorStops.length === 1 ? 0 : index / (colorStops.length - 1); } return colorWithPosition; }) .filter((stop) => stop.color !== null); // 按位置排序 parsedStops.sort((a, b) => a.position - b.position); // 添加颜色停止点到渐变 parsedStops.forEach((stop) => { try { gradient.addColorStop(stop.position, stop.color); } catch (colorError) { console.warn(`无效的颜色值: ${stop.color}`, colorError); // 尝试使用默认颜色 try { gradient.addColorStop(stop.position, "#000000"); } catch (fallbackError) { console.warn("添加默认颜色也失败:", fallbackError); } } }); return gradient; } // 解析径向渐变(简化版本) const radialMatch = gradientStr.match(/radial-gradient\((.+)\)$/); if (radialMatch) { const gradient = ctx.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, Math.max(width, height) / 2); const gradientContent = radialMatch[1]; const parts = smartSplitGradient(gradientContent); // 跳过可能的形状和位置参数,直接处理颜色 let colorStartIndex = 0; if (parts[0] && (parts[0].includes("circle") || parts[0].includes("ellipse") || parts[0].includes("at "))) { colorStartIndex = 1; } const colorStops = parts.slice(colorStartIndex); if (colorStops.length === 0) { console.warn("径向渐变中没有找到颜色停止点"); return null; } // 解析每个颜色停止点的颜色和位置(径向渐变) const parsedStops = colorStops .map((stop, index) => { const trimmedStop = stop.trim(); const colorWithPosition = parseColorStop(trimmedStop); // 如果没有指定位置,使用平均分布 if (colorWithPosition.position === null) { colorWithPosition.position = colorStops.length === 1 ? 0 : index / (colorStops.length - 1); } return colorWithPosition; }) .filter((stop) => stop.color !== null); // 按位置排序 parsedStops.sort((a, b) => a.position - b.position); // 添加颜色停止点到径向渐变 parsedStops.forEach((stop) => { try { gradient.addColorStop(stop.position, stop.color); } catch (colorError) { console.warn(`径向渐变中无效的颜色值: ${stop.color}`, colorError); // 尝试使用默认颜色 try { gradient.addColorStop(stop.position, "#000000"); } catch (fallbackError) { console.warn("径向渐变添加默认颜色也失败:", fallbackError); } } }); return gradient; } } catch (error) { console.warn("解析CSS渐变失败:", error); } return null; } /** * 预处理DOM元素以提高截图质量,并返回恢复函数 * @param element - 要处理的DOM元素 * @param options - 截图选项 * @returns 恢复原始状态的函数 */ function preprocessElement(element, options) { const restoreFunctions = []; // 处理SVG元素 if (options.processSvg !== false) { const svgRestore = processSvgElementsWithRestore(element); restoreFunctions.push(svgRestore); } // 处理渐变文本 if (options.processGradientText !== false) { const gradientRestore = processGradientTextWithRestore(element); restoreFunctions.push(gradientRestore); } // 确保所有图片都已加载 const images = element.querySelectorAll("img"); images.forEach((img) => { if (!img.complete) { console.warn("图片未完全加载,可能影响截图质量"); } }); // 返回恢复所有修改的函数 return () => { restoreFunctions.forEach((restore) => { try { restore(); } catch (error) { console.warn("恢复元素状态时出错:", error); } }); }; } /** * 使用snapdom进行截图 * @param element - 要截图的DOM元素 * @param options - 截图配置选项 * @returns Promise<HTMLCanvasElement> */ function captureWithSnapdom(element, options) { return __awaiter(this, void 0, void 0, function* () { // 等待字体加载完成 if (document.fonts && document.fonts.ready) { yield document.fonts.ready; } // 预加载字体和其他资源以提高渲染准确性 if (options.preCacheFonts !== false) { try { yield preCache(element, { embedFonts: options.embedFonts !== false }); } catch (preCacheError) { // preCache失败不应阻止截图,只记录警告 console.warn('snapdom preCache failed:', preCacheError); } } const canvas = yield snapdom.toCanvas(element, Object.assign({ backgroundColor: options.backgroundColor, scale: options.scale, width: options.width, height: options.height, embedFonts: options.embedFonts !== false, compress: true, fast: false }, options.snapdomOptions)); return canvas; }); } /** * 使用html2canvas进行截图 * @param element - 要截图的DOM元素 * @param options - 截图配置选项 * @returns Promise<HTMLCanvasElement> */ function captureWithHtml2Canvas(element, options) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c; // 预处理DOM元素并获取恢复函数 const restoreElement = preprocessElement(element, options); try { const canvas = yield html2canvas(element, Object.assign({ backgroundColor: options.backgroundColor, allowTaint: (_a = options.allowTaint) !== null && _a !== void 0 ? _a : true, useCORS: (_b = options.useCORS) !== null && _b !== void 0 ? _b : true, scale: (_c = options.scale) !== null && _c !== void 0 ? _c : 1, width: options.width, height: options.height, ignoreElements: options.ignoreElements }, options.html2canvasOptions)); return canvas; } finally { // 无论成功还是失败,都要恢复原始状态 restoreElement(); } }); } /** * 根据优先级进行截图,失败时自动降级 * @param element - 要截图的DOM元素 * @param options - 截图配置选项 * @returns Promise<HTMLCanvasElement> */ function captureWithFallback(element, options) { return __awaiter(this, void 0, void 0, function* () { const priority = options.enginePriority || "snapdom"; if (priority === "snapdom") { try { // 优先使用snapdom const canvas = yield captureWithSnapdom(element, options); return canvas; } catch (snapdomError) { console.warn("snapdom截图失败,降级使用html2canvas:", snapdomError); try { // 降级使用html2canvas const canvas = yield captureWithHtml2Canvas(element, options); return canvas; } catch (html2canvasError) { throw new Error(`截图失败: snapdom和html2canvas都无法工作。snapdom错误: ${snapdomError instanceof Error ? snapdomError.message : "未知错误"}; html2canvas错误: ${html2canvasError instanceof Error ? html2canvasError.message : "未知错误"}`); } } } else { try { // 优先使用html2canvas const canvas = yield captureWithHtml2Canvas(element, options); return canvas; } catch (html2canvasError) { console.warn("html2canvas截图失败,降级使用snapdom:", html2canvasError); try { // 降级使用snapdom const canvas = yield captureWithSnapdom(element, options); return canvas; } catch (snapdomError) { throw new Error(`截图失败: html2canvas和snapdom都无法工作。html2canvas错误: ${html2canvasError instanceof Error ? html2canvasError.message : "未知错误"}; snapdom错误: ${snapdomError instanceof Error ? snapdomError.message : "未知错误"}`); } } } }); } /** * DOM截图类 */ class DomScreenshot { constructor() { this.defaultOptions = { quality: 1, format: "png", backgroundColor: "#ffffff", allowTaint: false, useCORS: true, scale: 1, processSvg: true, processGradientText: true, enginePriority: "snapdom", embedFonts: true, preCacheFonts: true, }; } /** * 截取DOM元素为Canvas * @param element - 要截图的DOM元素 * @param options - 截图配置选项 * @returns Promise<HTMLCanvasElement> */ captureToCanvas(element_1) { return __awaiter(this, arguments, void 0, function* (element, options = {}) { const mergedOptions = Object.assign(Object.assign({}, this.defaultOptions), options); return yield captureWithFallback(element, mergedOptions); }); } /** * 截取DOM元素为Base64字符串 * @param element - 要截图的DOM元素 * @param options - 截图配置选项 * @returns Promise<string> */ captureToBase64(element_1) { return __awaiter(this, arguments, void 0, function* (element, options = {}) { const canvas = yield this.captureToCanvas(element, options); const mergedOptions = Object.assign(Object.assign({}, this.defaultOptions), options); return canvas.toDataURL(`image/${mergedOptions.format}`, mergedOptions.quality); }); } /** * 截取DOM元素为Blob对象 * @param element - 要截图的DOM元素 * @param options - 截图配置选项 * @returns Promise<Blob> */ captureToBlob(element_1) { return __awaiter(this, arguments, void 0, function* (element, options = {}) { const canvas = yield this.captureToCanvas(element, options); const mergedOptions = Object.assign(Object.assign({}, this.defaultOptions), options); return new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (blob) { resolve(blob); } else { reject(new Error("生成Blob失败")); } }, `image/${mergedOptions.format}`, mergedOptions.quality); }); }); } /** * 下载DOM元素截图 * @param element - 要截图的DOM元素 * @param filename - 下载文件名 * @param options - 截图配置选项 */ downloadScreenshot(element_1) { return __awaiter(this, arguments, void 0, function* (element, filename = "screenshot", options = {}) { const base64 = yield this.captureToBase64(element, options); const mergedOptions = Object.assign(Object.assign({}, this.defaultOptions), options); const link = document.createElement("a"); link.download = `${filename}.${mergedOptions.format}`; link.href = base64; document.body.appendChild(link); link.click(); document.body.removeChild(link); }); } } /** * 创建DOM截图实例 * @returns DomScreenshot实例 */ function createScreenshot() { return new DomScreenshot(); } /** * 快速截图函数 - 返回Base64 * @param element - 要截图的DOM元素 * @param options - 截图配置选项 * @returns Promise<string> */ function screenshot(element_1) { return __awaiter(this, arguments, void 0, function* (element, options = {}) { const instance = new DomScreenshot(); return instance.captureToBase64(element, options); }); } /** * 快速截图函数 - 返回Canvas * @param element - 要截图的DOM元素 * @param options - 截图配置选项 * @returns Promise<HTMLCanvasElement> */ function screenshotToCanvas(element_1) { return __awaiter(this, arguments, void 0, function* (element, options = {}) { const instance = new DomScreenshot(); return instance.captureToCanvas(element, options); }); } /** * 快速截图函数 - 返回Blob * @param element - 要截图的DOM元素 * @param options - 截图配置选项 * @returns Promise<Blob> */ function screenshotToBlob(element_1) { return __awaiter(this, arguments, void 0, function* (element, options = {}) { const instance = new DomScreenshot(); return instance.captureToBlob(element, options); }); } // 默认导出 var index = { DomScreenshot, createScreenshot, screenshot, screenshotToCanvas, screenshotToBlob, }; export { DomScreenshot, createScreenshot, index as default, screenshot, screenshotToBlob, screenshotToCanvas };