| <!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Huggingface环境矢量图形修复测试</title> |
| <style> |
| body { |
| font-family: 'Microsoft YaHei', Arial, sans-serif; |
| margin: 0; |
| padding: 20px; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| color: #333; |
| min-height: 100vh; |
| } |
| |
| .container { |
| max-width: 1200px; |
| margin: 0 auto; |
| background: white; |
| border-radius: 15px; |
| box-shadow: 0 20px 40px rgba(0,0,0,0.1); |
| overflow: hidden; |
| } |
| |
| .header { |
| background: linear-gradient(135deg, #ff6b6b, #ee5a24); |
| color: white; |
| padding: 30px; |
| text-align: center; |
| } |
| |
| .header h1 { |
| margin: 0; |
| font-size: 2.5em; |
| font-weight: 300; |
| } |
| |
| .header p { |
| margin: 10px 0 0 0; |
| opacity: 0.9; |
| font-size: 1.1em; |
| } |
| |
| .content { |
| padding: 40px; |
| } |
| |
| .section { |
| margin-bottom: 40px; |
| padding: 25px; |
| border: 2px solid #f0f0f0; |
| border-radius: 10px; |
| background: #fafafa; |
| } |
| |
| .section h2 { |
| color: #2c3e50; |
| margin-top: 0; |
| border-bottom: 3px solid #3498db; |
| padding-bottom: 10px; |
| } |
| |
| .test-area { |
| display: flex; |
| gap: 20px; |
| flex-wrap: wrap; |
| margin: 20px 0; |
| } |
| |
| .svg-container { |
| flex: 1; |
| min-width: 200px; |
| padding: 20px; |
| border: 2px dashed #ddd; |
| border-radius: 8px; |
| text-align: center; |
| background: white; |
| } |
| |
| .svg-container h3 { |
| margin-top: 0; |
| color: #34495e; |
| } |
| |
| .test-svg { |
| margin: 10px; |
| border: 1px solid #eee; |
| } |
| |
| .controls { |
| margin: 20px 0; |
| text-align: center; |
| } |
| |
| .btn { |
| background: linear-gradient(135deg, #667eea, #764ba2); |
| color: white; |
| border: none; |
| padding: 12px 25px; |
| margin: 5px; |
| border-radius: 25px; |
| cursor: pointer; |
| font-size: 16px; |
| transition: all 0.3s ease; |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); |
| } |
| |
| .btn:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); |
| } |
| |
| .btn:active { |
| transform: translateY(0); |
| } |
| |
| .btn.success { |
| background: linear-gradient(135deg, #00b894, #00a085); |
| } |
| |
| .btn.warning { |
| background: linear-gradient(135deg, #fdcb6e, #e17055); |
| } |
| |
| .btn.danger { |
| background: linear-gradient(135deg, #fd79a8, #e84393); |
| } |
| |
| .results { |
| margin-top: 20px; |
| padding: 20px; |
| background: #f8f9fa; |
| border-radius: 8px; |
| border-left: 4px solid #007bff; |
| } |
| |
| .log { |
| background: #2c3e50; |
| color: #ecf0f1; |
| padding: 15px; |
| border-radius: 8px; |
| font-family: 'Courier New', monospace; |
| font-size: 14px; |
| max-height: 300px; |
| overflow-y: auto; |
| white-space: pre-wrap; |
| margin-top: 15px; |
| } |
| |
| .environment-info { |
| background: linear-gradient(135deg, #74b9ff, #0984e3); |
| color: white; |
| padding: 20px; |
| border-radius: 10px; |
| margin-bottom: 20px; |
| } |
| |
| .environment-info h3 { |
| margin-top: 0; |
| } |
| |
| .fix-list { |
| background: #d4edda; |
| border: 1px solid #c3e6cb; |
| border-radius: 8px; |
| padding: 20px; |
| margin: 20px 0; |
| } |
| |
| .fix-item { |
| margin: 8px 0; |
| padding: 8px 12px; |
| background: white; |
| border-radius: 5px; |
| border-left: 4px solid #28a745; |
| } |
| |
| .preview-container { |
| display: flex; |
| gap: 20px; |
| margin-top: 20px; |
| flex-wrap: wrap; |
| } |
| |
| .preview-item { |
| flex: 1; |
| min-width: 300px; |
| padding: 15px; |
| border: 1px solid #ddd; |
| border-radius: 8px; |
| background: white; |
| } |
| |
| .preview-item h4 { |
| margin-top: 0; |
| color: #495057; |
| } |
| |
| .preview-image { |
| max-width: 100%; |
| border: 1px solid #eee; |
| border-radius: 4px; |
| } |
| |
| .status { |
| display: inline-block; |
| padding: 4px 8px; |
| border-radius: 12px; |
| font-size: 12px; |
| font-weight: bold; |
| margin-left: 10px; |
| } |
| |
| .status.success { |
| background: #d4edda; |
| color: #155724; |
| } |
| |
| .status.error { |
| background: #f8d7da; |
| color: #721c24; |
| } |
| |
| .status.warning { |
| background: #fff3cd; |
| color: #856404; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <div class="header"> |
| <h1>🚀 Huggingface环境矢量图形修复测试</h1> |
| <p>专门针对Huggingface Spaces部署环境的矢量图形渲染优化</p> |
| </div> |
| |
| <div class="content"> |
| |
| <div class="environment-info"> |
| <h3>🌐 环境检测</h3> |
| <div id="environment-status">正在检测环境...</div> |
| </div> |
| |
| |
| <div class="section"> |
| <h2>🔧 Huggingface环境修复措施</h2> |
| <div class="fix-list"> |
| <div class="fix-item">✅ 禁用CORS检查,使用allowTaint模式</div> |
| <div class="fix-item">✅ 优化html2canvas配置,适配Docker Alpine环境</div> |
| <div class="fix-item">✅ 简化SVG序列化,移除problematic属性</div> |
| <div class="fix-item">✅ 使用btoa编码替代复杂的Base64实现</div> |
| <div class="fix-item">✅ 添加多级回退机制,确保渲染成功</div> |
| <div class="fix-item">✅ 针对Chromium浏览器优化SVG处理</div> |
| <div class="fix-item">✅ 禁用foreignObject渲染,避免跨域问题</div> |
| </div> |
| </div> |
| |
| |
| <div class="section"> |
| <h2>🎨 测试SVG图形</h2> |
| <div class="test-area"> |
| <div class="svg-container"> |
| <h3>几何图形</h3> |
| <svg class="test-svg" width="150" height="100" id="svg1"> |
| <rect x="10" y="10" width="60" height="40" fill="#ff6b6b" stroke="#c0392b" stroke-width="2"/> |
| <circle cx="100" cy="30" r="20" fill="#3498db" stroke="#2980b9" stroke-width="2"/> |
| <polygon points="80,60 100,80 120,60 110,90 90,90" fill="#2ecc71" stroke="#27ae60" stroke-width="2"/> |
| </svg> |
| </div> |
| |
| <div class="svg-container"> |
| <h3>路径图形</h3> |
| <svg class="test-svg" width="150" height="100" id="svg2"> |
| <path d="M20,50 Q40,20 60,50 T100,50" stroke="#9b59b6" stroke-width="3" fill="none"/> |
| <path d="M10,80 L50,60 L90,80 L130,60" stroke="#f39c12" stroke-width="2" fill="none"/> |
| <ellipse cx="75" cy="30" rx="30" ry="15" fill="#e74c3c" opacity="0.7"/> |
| </svg> |
| </div> |
| |
| <div class="svg-container"> |
| <h3>文本图形</h3> |
| <svg class="test-svg" width="150" height="100" id="svg3"> |
| <rect width="100%" height="100%" fill="#ecf0f1"/> |
| <text x="75" y="30" text-anchor="middle" font-family="Arial" font-size="14" fill="#2c3e50">SVG文本</text> |
| <text x="75" y="50" text-anchor="middle" font-family="Arial" font-size="12" fill="#7f8c8d">Huggingface</text> |
| <text x="75" y="70" text-anchor="middle" font-family="Arial" font-size="10" fill="#95a5a6">优化测试</text> |
| </svg> |
| </div> |
| |
| <div class="svg-container"> |
| <h3>复杂图形</h3> |
| <svg class="test-svg" width="150" height="100" id="svg4"> |
| <defs> |
| <linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%"> |
| <stop offset="0%" style="stop-color:#667eea;stop-opacity:1" /> |
| <stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" /> |
| </linearGradient> |
| </defs> |
| <rect width="100%" height="100%" fill="url(#grad1)"/> |
| <circle cx="75" cy="50" r="30" fill="white" opacity="0.8"/> |
| <text x="75" y="55" text-anchor="middle" font-family="Arial" font-size="16" fill="#333">HF</text> |
| </svg> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="section"> |
| <h2>🧪 测试控制</h2> |
| <div class="controls"> |
| <button class="btn" onclick="testEnvironmentDetection()">检测环境</button> |
| <button class="btn success" onclick="testSvgConversion()">测试SVG转换</button> |
| <button class="btn warning" onclick="testHuggingfaceRenderer()">测试HF渲染器</button> |
| <button class="btn danger" onclick="testExportFunction()">测试导出功能</button> |
| <button class="btn" onclick="clearResults()">清除结果</button> |
| </div> |
| </div> |
| |
| |
| <div class="section"> |
| <h2>📊 测试结果</h2> |
| <div id="test-results" class="results"> |
| <p>点击上方按钮开始测试...</p> |
| </div> |
| |
| |
| <div class="preview-container" id="preview-container" style="display: none;"> |
| <div class="preview-item"> |
| <h4>原始SVG</h4> |
| <div id="original-svg"></div> |
| </div> |
| <div class="preview-item"> |
| <h4>转换结果</h4> |
| <div id="converted-result"></div> |
| </div> |
| </div> |
| |
| |
| <div class="log" id="log-area"></div> |
| </div> |
| </div> |
| </div> |
| |
| <script> |
| |
| function isHuggingfaceEnvironment() { |
| return ( |
| window.location.hostname.includes('hf.space') || |
| window.location.hostname.includes('huggingface.co') || |
| window.location.hostname.includes('localhost') || |
| window.location.hostname.includes('127.0.0.1') |
| ); |
| } |
| |
| |
| function log(message, type = 'info') { |
| const logArea = document.getElementById('log-area'); |
| const timestamp = new Date().toLocaleTimeString(); |
| const logEntry = `[${timestamp}] ${type.toUpperCase()}: ${message}\n`; |
| logArea.textContent += logEntry; |
| logArea.scrollTop = logArea.scrollHeight; |
| console.log(logEntry); |
| } |
| |
| |
| function updateResults(content) { |
| document.getElementById('test-results').innerHTML = content; |
| } |
| |
| |
| function clearResults() { |
| document.getElementById('test-results').innerHTML = '<p>结果已清除,点击测试按钮开始新的测试...</p>'; |
| document.getElementById('log-area').textContent = ''; |
| document.getElementById('preview-container').style.display = 'none'; |
| } |
| |
| |
| function testEnvironmentDetection() { |
| log('开始环境检测测试'); |
| |
| const isHF = isHuggingfaceEnvironment(); |
| const userAgent = navigator.userAgent; |
| const hostname = window.location.hostname; |
| |
| const envInfo = { |
| isHuggingface: isHF, |
| hostname: hostname, |
| userAgent: userAgent, |
| isChromium: userAgent.includes('Chrome'), |
| isDocker: userAgent.includes('HeadlessChrome'), |
| viewport: `${window.innerWidth}x${window.innerHeight}` |
| }; |
| |
| log(`环境检测结果: ${JSON.stringify(envInfo, null, 2)}`); |
| |
| const statusHtml = ` |
| <h3>环境检测结果</h3> |
| <p><strong>Huggingface环境:</strong> <span class="status ${isHF ? 'success' : 'warning'}">${isHF ? '是' : '否'}</span></p> |
| <p><strong>主机名:</strong> ${hostname}</p> |
| <p><strong>浏览器:</strong> ${envInfo.isChromium ? 'Chromium/Chrome' : '其他'}</p> |
| <p><strong>Docker环境:</strong> ${envInfo.isDocker ? '是' : '否'}</p> |
| <p><strong>视口尺寸:</strong> ${envInfo.viewport}</p> |
| `; |
| |
| updateResults(statusHtml); |
| |
| |
| document.getElementById('environment-status').innerHTML = ` |
| <strong>当前环境:</strong> ${isHF ? 'Huggingface Spaces' : '本地开发'} |
| <span class="status ${isHF ? 'success' : 'warning'}">${isHF ? 'HF环境' : '本地环境'}</span> |
| `; |
| } |
| |
| |
| function huggingfaceOptimizedSvg2Base64(element) { |
| try { |
| log('开始Huggingface优化的SVG转换'); |
| |
| const clonedElement = element.cloneNode(true); |
| |
| if (clonedElement.tagName.toLowerCase() === 'svg') { |
| const svgElement = clonedElement; |
| |
| |
| svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); |
| |
| |
| const rect = element.getBoundingClientRect(); |
| const width = rect.width || 100; |
| const height = rect.height || 100; |
| |
| |
| svgElement.setAttribute('width', width.toString()); |
| svgElement.setAttribute('height', height.toString()); |
| svgElement.setAttribute('viewBox', `0 0 ${width} ${height}`); |
| |
| |
| const removeProblematicAttrs = (elem) => { |
| const problematicAttrs = ['vector-effect', 'xmlns:xlink']; |
| problematicAttrs.forEach(attr => { |
| if (elem.hasAttribute && elem.hasAttribute(attr)) { |
| elem.removeAttribute(attr); |
| } |
| }); |
| |
| if (elem.children) { |
| Array.from(elem.children).forEach(child => { |
| removeProblematicAttrs(child); |
| }); |
| } |
| }; |
| |
| removeProblematicAttrs(svgElement); |
| |
| |
| let svgString = svgElement.outerHTML; |
| |
| |
| svgString = svgString.replace(/vector-effect="[^"]*"/g, ''); |
| svgString = svgString.replace(/xmlns:xlink="[^"]*"/g, ''); |
| |
| |
| if (!svgString.includes('xmlns="http://www.w3.org/2000/svg"')) { |
| svgString = svgString.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"'); |
| } |
| |
| log(`SVG字符串准备完成: 长度=${svgString.length}`); |
| |
| |
| const base64 = btoa(unescape(encodeURIComponent(svgString))); |
| const result = `data:image/svg+xml;base64,${base64}`; |
| |
| log('SVG转换成功'); |
| return result; |
| } |
| |
| throw new Error('不是SVG元素'); |
| |
| } catch (error) { |
| log(`SVG转换失败: ${error.message}`, 'error'); |
| |
| |
| const rect = element.getBoundingClientRect(); |
| const width = rect.width || 100; |
| const height = rect.height || 100; |
| |
| const placeholderSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"> |
| <rect width="100%" height="100%" fill="#f5f5f5" stroke="#ddd" stroke-width="1"/> |
| <text x="50%" y="50%" text-anchor="middle" dy="0.3em" font-family="Arial, sans-serif" font-size="14" fill="#999">SVG</text> |
| </svg>`; |
| |
| const base64 = btoa(unescape(encodeURIComponent(placeholderSvg))); |
| log('使用占位符SVG'); |
| return `data:image/svg+xml;base64,${base64}`; |
| } |
| } |
| |
| |
| function testSvgConversion() { |
| log('开始SVG转换测试'); |
| |
| const svgElements = document.querySelectorAll('.test-svg'); |
| let successCount = 0; |
| let totalCount = svgElements.length; |
| const results = []; |
| |
| svgElements.forEach((svg, index) => { |
| try { |
| const startTime = performance.now(); |
| const base64Result = huggingfaceOptimizedSvg2Base64(svg); |
| const endTime = performance.now(); |
| const duration = (endTime - startTime).toFixed(2); |
| |
| if (base64Result && base64Result.length > 100) { |
| successCount++; |
| results.push({ |
| id: svg.id, |
| status: 'success', |
| duration: duration, |
| size: base64Result.length, |
| preview: base64Result.substring(0, 100) + '...' |
| }); |
| log(`SVG ${svg.id} 转换成功: ${duration}ms, 大小=${base64Result.length}`); |
| } else { |
| results.push({ |
| id: svg.id, |
| status: 'error', |
| error: '转换结果无效' |
| }); |
| log(`SVG ${svg.id} 转换失败: 结果无效`, 'error'); |
| } |
| } catch (error) { |
| results.push({ |
| id: svg.id, |
| status: 'error', |
| error: error.message |
| }); |
| log(`SVG ${svg.id} 转换失败: ${error.message}`, 'error'); |
| } |
| }); |
| |
| const successRate = ((successCount / totalCount) * 100).toFixed(1); |
| |
| let resultHtml = ` |
| <h3>SVG转换测试结果</h3> |
| <p><strong>成功率:</strong> ${successRate}% (${successCount}/${totalCount})</p> |
| <div style="margin-top: 15px;"> |
| `; |
| |
| results.forEach(result => { |
| const statusClass = result.status === 'success' ? 'success' : 'error'; |
| resultHtml += ` |
| <div style="margin: 10px 0; padding: 10px; border: 1px solid #ddd; border-radius: 5px;"> |
| <strong>${result.id}:</strong> |
| <span class="status ${statusClass}">${result.status}</span> |
| ${result.duration ? `<br>耗时: ${result.duration}ms` : ''} |
| ${result.size ? `<br>大小: ${result.size} 字符` : ''} |
| ${result.error ? `<br>错误: ${result.error}` : ''} |
| </div> |
| `; |
| }); |
| |
| resultHtml += '</div>'; |
| updateResults(resultHtml); |
| |
| log(`SVG转换测试完成: 成功率=${successRate}%`); |
| } |
| |
| |
| function testHuggingfaceRenderer() { |
| log('开始Huggingface渲染器测试'); |
| |
| const isHF = isHuggingfaceEnvironment(); |
| |
| |
| const config = { |
| useCORS: false, |
| allowTaint: true, |
| foreignObjectRendering: false, |
| scale: isHF ? 1 : 2, |
| logging: false, |
| timeout: 30000 |
| }; |
| |
| log(`渲染配置: ${JSON.stringify(config, null, 2)}`); |
| |
| |
| const svgElements = document.querySelectorAll('.test-svg'); |
| const testResults = []; |
| |
| svgElements.forEach((svg, index) => { |
| try { |
| const rect = svg.getBoundingClientRect(); |
| const hasValidDimensions = rect.width > 0 && rect.height > 0; |
| |
| testResults.push({ |
| id: svg.id, |
| dimensions: `${rect.width}x${rect.height}`, |
| hasValidDimensions, |
| childCount: svg.children.length, |
| status: hasValidDimensions ? 'ready' : 'warning' |
| }); |
| |
| log(`${svg.id}: 尺寸=${rect.width}x${rect.height}, 子元素=${svg.children.length}`); |
| } catch (error) { |
| testResults.push({ |
| id: svg.id, |
| status: 'error', |
| error: error.message |
| }); |
| log(`${svg.id}: 检查失败 - ${error.message}`, 'error'); |
| } |
| }); |
| |
| let resultHtml = ` |
| <h3>Huggingface渲染器测试</h3> |
| <p><strong>环境:</strong> ${isHF ? 'Huggingface Spaces' : '本地环境'}</p> |
| <p><strong>配置:</strong> CORS=${config.useCORS}, AllowTaint=${config.allowTaint}, Scale=${config.scale}</p> |
| <div style="margin-top: 15px;"> |
| `; |
| |
| testResults.forEach(result => { |
| const statusClass = result.status === 'ready' ? 'success' : |
| result.status === 'warning' ? 'warning' : 'error'; |
| resultHtml += ` |
| <div style="margin: 10px 0; padding: 10px; border: 1px solid #ddd; border-radius: 5px;"> |
| <strong>${result.id}:</strong> |
| <span class="status ${statusClass}">${result.status}</span> |
| ${result.dimensions ? `<br>尺寸: ${result.dimensions}` : ''} |
| ${result.childCount !== undefined ? `<br>子元素: ${result.childCount}` : ''} |
| ${result.error ? `<br>错误: ${result.error}` : ''} |
| </div> |
| `; |
| }); |
| |
| resultHtml += '</div>'; |
| updateResults(resultHtml); |
| |
| log('Huggingface渲染器测试完成'); |
| } |
| |
| |
| function testExportFunction() { |
| log('开始导出功能测试'); |
| |
| const testSvg = document.getElementById('svg1'); |
| if (!testSvg) { |
| log('找不到测试SVG元素', 'error'); |
| return; |
| } |
| |
| try { |
| |
| document.getElementById('preview-container').style.display = 'flex'; |
| |
| |
| const originalContainer = document.getElementById('original-svg'); |
| const svgClone = testSvg.cloneNode(true); |
| originalContainer.innerHTML = ''; |
| originalContainer.appendChild(svgClone); |
| |
| |
| const base64Result = huggingfaceOptimizedSvg2Base64(testSvg); |
| |
| |
| const resultContainer = document.getElementById('converted-result'); |
| const img = document.createElement('img'); |
| img.src = base64Result; |
| img.className = 'preview-image'; |
| img.onload = () => { |
| log('转换结果图片加载成功'); |
| }; |
| img.onerror = () => { |
| log('转换结果图片加载失败', 'error'); |
| }; |
| |
| resultContainer.innerHTML = ''; |
| resultContainer.appendChild(img); |
| |
| const resultHtml = ` |
| <h3>导出功能测试结果</h3> |
| <p><strong>状态:</strong> <span class="status success">成功</span></p> |
| <p><strong>Base64长度:</strong> ${base64Result.length} 字符</p> |
| <p><strong>数据类型:</strong> ${base64Result.substring(0, 30)}...</p> |
| <p>请查看下方预览区域对比原始SVG和转换结果。</p> |
| `; |
| |
| updateResults(resultHtml); |
| log('导出功能测试完成'); |
| |
| } catch (error) { |
| const resultHtml = ` |
| <h3>导出功能测试结果</h3> |
| <p><strong>状态:</strong> <span class="status error">失败</span></p> |
| <p><strong>错误:</strong> ${error.message}</p> |
| `; |
| |
| updateResults(resultHtml); |
| log(`导出功能测试失败: ${error.message}`, 'error'); |
| } |
| } |
| |
| |
| window.addEventListener('load', () => { |
| log('页面加载完成,开始初始化'); |
| testEnvironmentDetection(); |
| }); |
| </script> |
| </body> |
| </html> |