修复图片解密

This commit is contained in:
xuncha
2026-02-28 16:44:55 +08:00
parent 4d5c744583
commit c88aa2c9d8

View File

@@ -301,7 +301,7 @@ export class ImageDecryptService {
if (finalExt === '.hevc') { if (finalExt === '.hevc') {
return { return {
success: false, success: false,
error: '此图片为微信新格式(wxgf)需要安装 ffmpeg 才能显示', error: '此图片为微信新格式(wxgf)ffmpeg 转换失败,请检查日志',
isThumb: this.isThumbnailPath(datPath) isThumb: this.isThumbnailPath(datPath)
} }
} }
@@ -1833,21 +1833,24 @@ export class ImageDecryptService {
// 提取 HEVC NALU 裸流 // 提取 HEVC NALU 裸流
const hevcData = this.extractHevcNalu(buffer) const hevcData = this.extractHevcNalu(buffer)
if (!hevcData || hevcData.length < 100) { // 优先用提取的 NALU 裸流,提取失败则跳过 wxgf 头部直接用原始数据
return { data: buffer, isWxgf: true } const feedData = (hevcData && hevcData.length >= 100) ? hevcData : buffer.subarray(4)
} this.logInfo('unwrapWxgf: 准备 ffmpeg 转换', {
naluExtracted: !!(hevcData && hevcData.length >= 100),
feedSize: feedData.length
})
// 尝试用 ffmpeg 转换 // 尝试用 ffmpeg 转换
try { try {
const jpgData = await this.convertHevcToJpg(hevcData) const jpgData = await this.convertHevcToJpg(feedData)
if (jpgData && jpgData.length > 0) { if (jpgData && jpgData.length > 0) {
return { data: jpgData, isWxgf: false } return { data: jpgData, isWxgf: false }
} }
} catch { } catch (e) {
// ffmpeg 转换失败 this.logError('unwrapWxgf: ffmpeg 转换失败', e)
} }
return { data: hevcData, isWxgf: true } return { data: feedData, isWxgf: true }
} }
/** /**
@@ -1914,50 +1917,92 @@ export class ImageDecryptService {
/** /**
* 使用 ffmpeg 将 HEVC 裸流转换为 JPG * 使用 ffmpeg 将 HEVC 裸流转换为 JPG
*/ */
private convertHevcToJpg(hevcData: Buffer): Promise<Buffer | null> { private async convertHevcToJpg(hevcData: Buffer): Promise<Buffer | null> {
const ffmpeg = this.getFfmpegPath() const ffmpeg = this.getFfmpegPath()
this.logInfo('ffmpeg 转换开始', { ffmpegPath: ffmpeg, hevcSize: hevcData.length }) this.logInfo('ffmpeg 转换开始', { ffmpegPath: ffmpeg, hevcSize: hevcData.length })
const tmpDir = join(app.getPath('temp'), 'weflow_hevc')
if (!existsSync(tmpDir)) mkdirSync(tmpDir, { recursive: true })
const ts = Date.now()
const tmpInput = join(tmpDir, `hevc_${ts}.hevc`)
const tmpOutput = join(tmpDir, `hevc_${ts}.jpg`)
try {
await writeFile(tmpInput, hevcData)
// 依次尝试: 1) -f hevc 裸流 2) 不指定格式让 ffmpeg 自动检测
const attempts: { label: string; inputArgs: string[] }[] = [
{ label: 'hevc raw', inputArgs: ['-f', 'hevc', '-i', tmpInput] },
{ label: 'auto detect', inputArgs: ['-i', tmpInput] },
]
for (const attempt of attempts) {
// 清理上一轮的输出
try { if (existsSync(tmpOutput)) require('fs').unlinkSync(tmpOutput) } catch {}
const result = await this.runFfmpegConvert(ffmpeg, attempt.inputArgs, tmpOutput, attempt.label)
if (result) return result
}
return null
} catch (e) {
this.logError('ffmpeg 转换异常', e)
return null
} finally {
try { if (existsSync(tmpInput)) require('fs').unlinkSync(tmpInput) } catch {}
try { if (existsSync(tmpOutput)) require('fs').unlinkSync(tmpOutput) } catch {}
}
}
private runFfmpegConvert(ffmpeg: string, inputArgs: string[], tmpOutput: string, label: string): Promise<Buffer | null> {
return new Promise((resolve) => { return new Promise((resolve) => {
const { spawn } = require('child_process') const { spawn } = require('child_process')
const chunks: Buffer[] = []
const errChunks: Buffer[] = [] const errChunks: Buffer[] = []
const proc = spawn(ffmpeg, [ const args = [
'-hide_banner', '-hide_banner', '-loglevel', 'error',
'-loglevel', 'error', ...inputArgs,
'-f', 'hevc', '-vframes', '1', '-q:v', '2', '-f', 'image2', tmpOutput
'-i', 'pipe:0', ]
'-vframes', '1', this.logInfo(`ffmpeg 尝试 [${label}]`, { args: args.join(' ') })
'-q:v', '3',
'-f', 'mjpeg', const proc = spawn(ffmpeg, args, {
'pipe:1' stdio: ['ignore', 'ignore', 'pipe'],
], {
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true windowsHide: true
}) })
proc.stdout.on('data', (chunk: Buffer) => chunks.push(chunk))
proc.stderr.on('data', (chunk: Buffer) => errChunks.push(chunk)) proc.stderr.on('data', (chunk: Buffer) => errChunks.push(chunk))
proc.on('close', (code: number) => { const timer = setTimeout(() => {
if (code === 0 && chunks.length > 0) { proc.kill('SIGKILL')
this.logInfo('ffmpeg 转换成功', { outputSize: Buffer.concat(chunks).length }) this.logError(`ffmpeg [${label}] 超时(15s)`)
resolve(Buffer.concat(chunks)) resolve(null)
} else { }, 15000)
const errMsg = Buffer.concat(errChunks).toString()
this.logInfo('ffmpeg 转换失败', { code, error: errMsg })
resolve(null)
}
})
proc.on('error', (err: Error) => { proc.on('close', (code: number) => {
this.logInfo('ffmpeg 进程错误', { error: err.message }) clearTimeout(timer)
if (code === 0 && existsSync(tmpOutput)) {
try {
const jpgBuf = readFileSync(tmpOutput)
if (jpgBuf.length > 0) {
this.logInfo(`ffmpeg [${label}] 成功`, { outputSize: jpgBuf.length })
resolve(jpgBuf)
return
}
} catch (e) {
this.logError(`ffmpeg [${label}] 读取输出失败`, e)
}
}
const errMsg = Buffer.concat(errChunks).toString().trim()
this.logInfo(`ffmpeg [${label}] 失败`, { code, error: errMsg })
resolve(null) resolve(null)
}) })
proc.stdin.write(hevcData) proc.on('error', (err: Error) => {
proc.stdin.end() clearTimeout(timer)
this.logError(`ffmpeg [${label}] 进程错误`, err)
resolve(null)
})
}) })
} }