* 从 TGS (Lottie) 动画渲染第一帧 PNG * 需要 rlottie-python 和 ffmpeg
(tgsBuffer: Buffer)
| 99 | /** |
| 100 | * 从 TGS (Lottie) 动画渲染第一帧 PNG |
| 101 | * 需要 rlottie-python 和 ffmpeg |
| 102 | */ |
| 103 | async function extractTgsFirstFrame(tgsBuffer: Buffer): Promise<Buffer | null> { |
| 104 | const uniqueId = Date.now().toString() + '_' + Math.random().toString(36).slice(2); |
| 105 | const tgsPath = path.join(XMSL_TEMP_DIR, `sticker_${uniqueId}.tgs`); |
| 106 | const gifPath = path.join(XMSL_TEMP_DIR, `sticker_${uniqueId}.gif`); |
| 107 | const pngPath = path.join(XMSL_TEMP_DIR, `sticker_${uniqueId}.png`); |
| 108 | |
| 109 | try { |
| 110 | fs.writeFileSync(tgsPath, tgsBuffer); |
| 111 | |
| 112 | // 使用 rlottie-python 渲染 TGS 到 GIF |
| 113 | const pythonScript = ` |
| 114 | import sys |
| 115 | from rlottie_python import LottieAnimation |
| 116 | anim = LottieAnimation.from_tgs(sys.argv[1]) |
| 117 | anim.save_animation(sys.argv[2]) |
| 118 | `; |
| 119 | await execFileAsync('python3', ['-c', pythonScript, tgsPath, gifPath]); |
| 120 | |
| 121 | // 从 GIF 提取第一帧 |
| 122 | await execFileAsync('ffmpeg', [ |
| 123 | '-i', gifPath, |
| 124 | '-vf', 'select=eq(n\\,0)', |
| 125 | '-vframes', '1', |
| 126 | '-y', |
| 127 | pngPath |
| 128 | ]); |
| 129 | |
| 130 | if (fs.existsSync(pngPath)) { |
| 131 | return fs.readFileSync(pngPath); |
| 132 | } |
| 133 | return null; |
| 134 | } catch (error) { |
| 135 | console.error('[xmsl] TGS 第一帧提取失败:', error); |
| 136 | return null; |
| 137 | } finally { |
| 138 | try { fs.unlinkSync(tgsPath); } catch {} |
| 139 | try { fs.unlinkSync(gifPath); } catch {} |
| 140 | try { fs.unlinkSync(pngPath); } catch {} |
| 141 | } |
| 142 | } |
| 143 |
no test coverage detected