Download frontend/src/utils/VectorRenderManager.ts from CatPtain/web_ppt: direct link, hf CLI and curl.
- Browser
- Download file 13.6 kB
-
https://huggingface.co/spaces/CatPtain/web_ppt/resolve/main/frontend/src/utils/VectorRenderManager.ts
- Command line
-
hf download hf://spaces/CatPtain/web_ppt/frontend/src/utils/VectorRenderManager.ts
-
curl -L -o VectorRenderManager.ts https://huggingface.co/spaces/CatPtain/web_ppt/resolve/main/frontend/src/utils/VectorRenderManager.ts
13.6 kB
| /** | |
| * 生产环境矢量图形渲染管理器 | |
| * 统一管理所有矢量图形的渲染策略和错误处理 | |
| */ | |
| import { VECTOR_EXPORT_CONFIG } from '@/config/vectorExportConfig'; | |
| import { svg2Base64 } from './svg2Base64'; | |
| import { renderElementToBase64, getElementDimensions } from './canvasRenderer'; | |
| // 渲染策略枚举 | |
| export enum RenderStrategy { | |
| SVG_SERIALIZATION = 'svg_serialization', | |
| CANVAS_RENDER = 'canvas_render', | |
| SIMPLIFIED_SVG = 'simplified_svg', | |
| PLACEHOLDER = 'placeholder' | |
| } | |
| // 渲染结果接口 | |
| export interface RenderResult { | |
| success: boolean; | |
| data?: string; | |
| strategy: RenderStrategy; | |
| duration: number; | |
| error?: Error; | |
| metadata?: { | |
| width: number; | |
| height: number; | |
| format: string; | |
| size: number; | |
| }; | |
| } | |
| // 性能监控器 | |
| class PerformanceMonitor { | |
| private static instance: PerformanceMonitor; | |
| private metrics: Map<string, number[]> = new Map(); | |
| static getInstance(): PerformanceMonitor { | |
| if (!this.instance) { | |
| this.instance = new PerformanceMonitor(); | |
| } | |
| return this.instance; | |
| } | |
| recordMetric(operation: string, duration: number): void { | |
| if (!VECTOR_EXPORT_CONFIG.PERFORMANCE_CONFIG.MONITORING.ENABLED) return; | |
| if (!this.metrics.has(operation)) { | |
| this.metrics.set(operation, []); | |
| } | |
| const metrics = this.metrics.get(operation)!; | |
| metrics.push(duration); | |
| // 保持最近的记录数量 | |
| const maxCount = VECTOR_EXPORT_CONFIG.PERFORMANCE_CONFIG.MONITORING.MAX_METRICS_COUNT; | |
| if (metrics.length > maxCount) { | |
| metrics.shift(); | |
| } | |
| } | |
| getAverageTime(operation: string): number { | |
| const metrics = this.metrics.get(operation); | |
| if (!metrics || metrics.length === 0) return 0; | |
| return metrics.reduce((sum, time) => sum + time, 0) / metrics.length; | |
| } | |
| getMetrics(): Record<string, { average: number; count: number; latest: number }> { | |
| const result: Record<string, { average: number; count: number; latest: number }> = {}; | |
| for (const [operation, times] of this.metrics.entries()) { | |
| if (times.length > 0) { | |
| result[operation] = { | |
| average: this.getAverageTime(operation), | |
| count: times.length, | |
| latest: times[times.length - 1] | |
| }; | |
| } | |
| } | |
| return result; | |
| } | |
| } | |
| // 错误处理器 | |
| class ErrorHandler { | |
| static logError(error: Error, context: string, element?: Element): void { | |
| const config = VECTOR_EXPORT_CONFIG.ERROR_CONFIG.LOGGING; | |
| if (config.ERROR_ONLY && VECTOR_EXPORT_CONFIG.ENVIRONMENT.isProduction()) { | |
| console.error(`VectorRenderManager[${context}]: ${error.message}`); | |
| } else if (config.VERBOSE) { | |
| console.error(`VectorRenderManager[${context}]:`, { | |
| error: error.message, | |
| stack: config.INCLUDE_STACK_TRACE ? error.stack : undefined, | |
| element: element ? { | |
| tagName: element.tagName, | |
| className: element.className, | |
| id: element.id | |
| } : undefined | |
| }); | |
| } | |
| } | |
| static async retry<T>( | |
| operation: () => Promise<T>, | |
| context: string, | |
| maxAttempts: number = VECTOR_EXPORT_CONFIG.ERROR_CONFIG.RETRY.MAX_ATTEMPTS | |
| ): Promise<T> { | |
| let lastError: Error; | |
| for (let attempt = 1; attempt <= maxAttempts; attempt++) { | |
| try { | |
| return await operation(); | |
| } catch (error) { | |
| lastError = error as Error; | |
| if (attempt === maxAttempts) { | |
| this.logError(lastError, `${context} (final attempt ${attempt}/${maxAttempts})`); | |
| throw lastError; | |
| } | |
| const delay = VECTOR_EXPORT_CONFIG.ERROR_CONFIG.RETRY.DELAY_MS * | |
| Math.pow(VECTOR_EXPORT_CONFIG.ERROR_CONFIG.RETRY.BACKOFF_MULTIPLIER, attempt - 1); | |
| this.logError(lastError, `${context} (attempt ${attempt}/${maxAttempts}, retrying in ${delay}ms)`); | |
| await new Promise(resolve => setTimeout(resolve, delay)); | |
| } | |
| } | |
| throw lastError!; | |
| } | |
| } | |
| // 主渲染管理器 | |
| export class VectorRenderManager { | |
| private static instance: VectorRenderManager; | |
| private monitor: PerformanceMonitor; | |
| private constructor() { | |
| this.monitor = PerformanceMonitor.getInstance(); | |
| } | |
| static getInstance(): VectorRenderManager { | |
| if (!this.instance) { | |
| this.instance = new VectorRenderManager(); | |
| } | |
| return this.instance; | |
| } | |
| /** | |
| * 渲染元素为Base64图像 | |
| */ | |
| async renderElement(element: Element, preferredStrategy?: RenderStrategy): Promise<RenderResult> { | |
| const startTime = performance.now(); | |
| // 验证元素 | |
| const validation = this.validateElement(element); | |
| if (!validation.valid) { | |
| return { | |
| success: false, | |
| strategy: RenderStrategy.PLACEHOLDER, | |
| duration: performance.now() - startTime, | |
| error: new Error(validation.reason || 'Element validation failed') | |
| }; | |
| } | |
| // 确定渲染策略顺序 | |
| const strategies = this.getStrategies(element, preferredStrategy); | |
| // 尝试每个策略 | |
| for (const strategy of strategies) { | |
| try { | |
| const result = await this.executeStrategy(element, strategy); | |
| if (result.success) { | |
| const duration = performance.now() - startTime; | |
| this.monitor.recordMetric(`render_success_${strategy}`, duration); | |
| return { | |
| ...result, | |
| duration | |
| }; | |
| } | |
| } catch (error) { | |
| ErrorHandler.logError(error as Error, `Strategy ${strategy}`, element); | |
| continue; | |
| } | |
| } | |
| // 所有策略都失败,返回占位符 | |
| const duration = performance.now() - startTime; | |
| this.monitor.recordMetric('render_fallback_placeholder', duration); | |
| return this.createPlaceholder(element, duration); | |
| } | |
| /** | |
| * 验证元素是否可以渲染 | |
| */ | |
| private validateElement(element: Element): { valid: boolean; reason?: string } { | |
| if (!element) { | |
| return { valid: false, reason: 'Element is null or undefined' }; | |
| } | |
| // 检查元素尺寸 | |
| const dimensions = getElementDimensions(element as HTMLElement); | |
| const config = VECTOR_EXPORT_CONFIG.VALIDATION_CONFIG.DIMENSIONS; | |
| if (!dimensions.isValid) { | |
| return { valid: false, reason: 'Element has invalid dimensions' }; | |
| } | |
| if (dimensions.width < config.MIN_WIDTH || dimensions.height < config.MIN_HEIGHT) { | |
| return { valid: false, reason: 'Element dimensions too small' }; | |
| } | |
| if (dimensions.width > config.MAX_WIDTH || dimensions.height > config.MAX_HEIGHT) { | |
| return { valid: false, reason: 'Element dimensions too large' }; | |
| } | |
| return { valid: true }; | |
| } | |
| /** | |
| * 获取渲染策略顺序 | |
| */ | |
| private getStrategies(element: Element, preferred?: RenderStrategy): RenderStrategy[] { | |
| const isSVG = element.tagName.toLowerCase() === 'svg'; | |
| const isHuggingface = VECTOR_EXPORT_CONFIG.ENVIRONMENT.isHuggingface(); | |
| const canvasSupported = VECTOR_EXPORT_CONFIG.COMPATIBILITY_CONFIG.FEATURES.CANVAS_SUPPORT(); | |
| let strategies: RenderStrategy[] = []; | |
| // 如果指定了首选策略,优先使用 | |
| if (preferred) { | |
| strategies.push(preferred); | |
| } | |
| // 根据环境和元素类型确定策略顺序 | |
| if (isSVG) { | |
| if (!strategies.includes(RenderStrategy.SVG_SERIALIZATION)) { | |
| strategies.push(RenderStrategy.SVG_SERIALIZATION); | |
| } | |
| if (!strategies.includes(RenderStrategy.SIMPLIFIED_SVG)) { | |
| strategies.push(RenderStrategy.SIMPLIFIED_SVG); | |
| } | |
| } | |
| // Canvas渲染(如果支持且不是Huggingface环境) | |
| if (canvasSupported && !isHuggingface && !strategies.includes(RenderStrategy.CANVAS_RENDER)) { | |
| strategies.push(RenderStrategy.CANVAS_RENDER); | |
| } | |
| // 最后的占位符策略 | |
| if (!strategies.includes(RenderStrategy.PLACEHOLDER)) { | |
| strategies.push(RenderStrategy.PLACEHOLDER); | |
| } | |
| return strategies; | |
| } | |
| /** | |
| * 执行特定的渲染策略 | |
| */ | |
| private async executeStrategy(element: Element, strategy: RenderStrategy): Promise<RenderResult> { | |
| const startTime = performance.now(); | |
| switch (strategy) { | |
| case RenderStrategy.SVG_SERIALIZATION: | |
| return this.executeSVGSerialization(element, startTime); | |
| case RenderStrategy.CANVAS_RENDER: | |
| return this.executeCanvasRender(element, startTime); | |
| case RenderStrategy.SIMPLIFIED_SVG: | |
| return this.executeSimplifiedSVG(element, startTime); | |
| case RenderStrategy.PLACEHOLDER: | |
| return this.createPlaceholder(element, performance.now() - startTime); | |
| default: | |
| throw new Error(`Unknown strategy: ${strategy}`); | |
| } | |
| } | |
| /** | |
| * SVG序列化策略 | |
| */ | |
| private async executeSVGSerialization(element: Element, startTime: number): Promise<RenderResult> { | |
| try { | |
| const base64 = svg2Base64(element); | |
| const duration = performance.now() - startTime; | |
| return { | |
| success: true, | |
| data: base64, | |
| strategy: RenderStrategy.SVG_SERIALIZATION, | |
| duration, | |
| metadata: { | |
| width: element.getBoundingClientRect().width, | |
| height: element.getBoundingClientRect().height, | |
| format: 'svg', | |
| size: base64.length | |
| } | |
| }; | |
| } catch (error) { | |
| return { | |
| success: false, | |
| strategy: RenderStrategy.SVG_SERIALIZATION, | |
| duration: performance.now() - startTime, | |
| error: error as Error | |
| }; | |
| } | |
| } | |
| /** | |
| * Canvas渲染策略 | |
| */ | |
| private async executeCanvasRender(element: Element, startTime: number): Promise<RenderResult> { | |
| try { | |
| const base64 = await renderElementToBase64(element as HTMLElement, { | |
| scale: VECTOR_EXPORT_CONFIG.PERFORMANCE_CONFIG.CANVAS.DEFAULT_SCALE, | |
| backgroundColor: VECTOR_EXPORT_CONFIG.PERFORMANCE_CONFIG.CANVAS.BACKGROUND_COLOR, | |
| timeout: VECTOR_EXPORT_CONFIG.PERFORMANCE_CONFIG.RENDER_TIMEOUT | |
| }); | |
| const duration = performance.now() - startTime; | |
| return { | |
| success: true, | |
| data: base64, | |
| strategy: RenderStrategy.CANVAS_RENDER, | |
| duration, | |
| metadata: { | |
| width: element.getBoundingClientRect().width, | |
| height: element.getBoundingClientRect().height, | |
| format: 'png', | |
| size: base64.length | |
| } | |
| }; | |
| } catch (error) { | |
| return { | |
| success: false, | |
| strategy: RenderStrategy.CANVAS_RENDER, | |
| duration: performance.now() - startTime, | |
| error: error as Error | |
| }; | |
| } | |
| } | |
| /** | |
| * 简化SVG策略 | |
| */ | |
| private async executeSimplifiedSVG(element: Element, startTime: number): Promise<RenderResult> { | |
| try { | |
| const rect = element.getBoundingClientRect(); | |
| const width = rect.width || 100; | |
| const height = rect.height || 100; | |
| // 处理 vector-effect 相关的元素 | |
| const vectorEffectElements = element.querySelectorAll('[vector-effect], .vector-path-element'); | |
| vectorEffectElements.forEach(el => { | |
| // 移除 vector-effect 属性 | |
| el.removeAttribute('vector-effect'); | |
| // 移除 vector-path-element 类 | |
| el.classList.remove('vector-path-element'); | |
| }); | |
| // 创建简化的SVG | |
| const simplifiedSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"> | |
| <rect width="100%" height="100%" fill="#f0f0f0" stroke="#ccc" stroke-width="1"/> | |
| <text x="50%" y="50%" text-anchor="middle" dy="0.3em" font-size="14" fill="#666">SVG</text> | |
| </svg>`; | |
| const base64 = `data:image/svg+xml;base64,${btoa(simplifiedSvg)}`; | |
| const duration = performance.now() - startTime; | |
| return { | |
| success: true, | |
| data: base64, | |
| strategy: RenderStrategy.SIMPLIFIED_SVG, | |
| duration, | |
| metadata: { | |
| width, | |
| height, | |
| format: 'svg', | |
| size: base64.length | |
| } | |
| }; | |
| } catch (error) { | |
| return { | |
| success: false, | |
| strategy: RenderStrategy.SIMPLIFIED_SVG, | |
| duration: performance.now() - startTime, | |
| error: error as Error | |
| }; | |
| } | |
| } | |
| /** | |
| * 创建占位符 | |
| */ | |
| private createPlaceholder(element: Element, duration: number): RenderResult { | |
| const rect = element.getBoundingClientRect(); | |
| const width = rect.width || 100; | |
| const height = rect.height || 100; | |
| const config = VECTOR_EXPORT_CONFIG.ERROR_CONFIG.FALLBACK; | |
| 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="${config.PLACEHOLDER_COLOR}" stroke="#ddd" stroke-width="1"/> | |
| <text x="50%" y="50%" text-anchor="middle" dy="0.3em" font-size="12" fill="#999">${config.PLACEHOLDER_TEXT}</text> | |
| </svg>`; | |
| const base64 = `data:image/svg+xml;base64,${btoa(placeholderSvg)}`; | |
| return { | |
| success: true, | |
| data: base64, | |
| strategy: RenderStrategy.PLACEHOLDER, | |
| duration, | |
| metadata: { | |
| width, | |
| height, | |
| format: 'svg', | |
| size: base64.length | |
| } | |
| }; | |
| } | |
| /** | |
| * 获取性能指标 | |
| */ | |
| getPerformanceMetrics(): Record<string, { average: number; count: number; latest: number }> { | |
| return this.monitor.getMetrics(); | |
| } | |
| } | |
| // 导出单例实例 | |
| export const vectorRenderManager = VectorRenderManager.getInstance(); | |
| export default vectorRenderManager; |