.livp,.HEIC格式图片转换成jpg格式图片
苹果手机及苹果电脑无法识别安卓的webP格式图片,安卓手机无法识别苹果的.livp(实况图)和.HEIC格式图片(老系统的苹果手机实况图)。
.livp是一个容器格式,需要解压到本地后再转换。由于文件存储需要时间,所以第一转换基本都会失败,第二次转换才会成功。
.livp,.HEIC格式图片转换成jpg格式图片,质量0.8.
转换代码:
const OSSURL = 'https://test.oss-cn-hangzhou.aliyuncs.com';const msgCode = require('@app/config/error_config.js');
const FspConstant = require('@app/tools/fsp_constant.js');
const sharp = require('sharp');// 处理图片
const fs = require('fs');
const path = require('path');
const unzipper = require('unzipper');
const { execSync } = require('child_process');
const convert = require('heic-convert');
const { promisify } = require('util');
const convertHeicToJpeg = promisify(convert);ImageTool.convertHeicToJpegWithRetry = async function(heicBuffer, quality = 80, maxRetries = 3) {for (let attempt = 1; attempt <= maxRetries; attempt++) {try {console.log(`HEIC 转换尝试 ${attempt}/${maxRetries}`);const jpegBuffer = await convert({buffer: heicBuffer,format: 'JPEG',quality: quality / 100});console.log(`HEIC 转换成功,输出大小: ${jpegBuffer.length} bytes`);return jpegBuffer;} catch (error) {console.error(`HEIC 转换尝试 ${attempt} 失败:`, error.message);if (attempt === maxRetries) {logger.debug(`HEIC 转换失败,所有 ${maxRetries} 次尝试均失败: ${error.message}`);throw msgCode[39522]('上传');}// 等待后重试await new Promise(resolve => setTimeout(resolve, 1000 * attempt));}}
};
ImageTool.processHeicFile = async function(heicPath, quality = 80) {console.log('开始处理 HEIC 文件:', heicPath);try {// 验证文件const stats = fs.statSync(heicPath);if (stats.size === 0) {logger.debug('HEIC 文件为空');throw msgCode[39522]('上传');}// 读取文件const heicBuffer = fs.readFileSync(heicPath);console.log('HEIC 文件大小:', heicBuffer.length, 'bytes');// 转换const jpegBuffer = await ImageTool.convertHeicToJpegWithRetry(heicBuffer, quality);// 输出路径const outputPath = heicPath.replace(path.extname(heicPath), '.jpg');fs.writeFileSync(outputPath, jpegBuffer);// 验证输出const outputStats = fs.statSync(outputPath);if (outputStats.size === 0) {logger.debug('转换后的 JPEG 文件为空');throw msgCode[39522]('上传');}console.log('HEIC 处理完成:', outputPath);return outputPath;} catch (error) {console.error('HEIC 文件处理失败:', error);// throw error;throw msgCode[39522]('上传');}
};
ImageTool.convertLivpToJpegDirect = async function(livpPath, quality = 80) {let tempDir = null;try {console.log('使用直接系统命令转换 LIVP 文件');const extractDir = path.dirname(livpPath);tempDir = path.join(extractDir, 'temp_livp_' + Date.now());const outputPath = livpPath.replace('.livp', '.jpg');// 创建临时目录fs.mkdirSync(tempDir, { recursive: true });// 方法1: 直接使用 ImageMagick 处理 LIVP 文件try {console.log('尝试 ImageMagick 直接处理 LIVP...');execSync(`convert "${livpPath}[0]" -quality ${quality} "${outputPath}"`, {stdio: 'pipe',timeout: 30000});if (fs.existsSync(outputPath) && fs.statSync(outputPath).size > 0) {console.log('ImageMagick 直接转换成功');return outputPath;}} catch (error) {console.log('ImageMagick 直接处理失败:', error.message);// throw msgCode[39522]('上传');}// 方法2: 解压后转换console.log('解压 LIVP 文件...');execSync(`unzip -q -o "${livpPath}" -d "${tempDir}"`, {stdio: 'pipe'});// 查找 HEIC 文件const files = fs.readdirSync(tempDir);const heicFile = files.find(file => path.extname(file).toLowerCase() === '.heic');if (!heicFile) {logger.debug('在 LIVP 文件中找不到 HEIC 图片');throw msgCode[39522]('上传');}const heicPath = path.join(tempDir, heicFile);console.log('找到 HEIC 文件:', heicPath);// 方法3: 使用 ImageMagick 转换 HEICtry {console.log('使用 ImageMagick 转换 HEIC...');execSync(`convert "${heicPath}" -quality ${quality} "${outputPath}"`, {stdio: 'pipe',timeout: 30000});} catch (error) {console.log('ImageMagick 转换 HEIC 失败:', error.message);// 方法4: 使用 libheif 的 heif-converttry {console.log('尝试 heif-convert...');execSync(`heif-convert -q ${quality} "${heicPath}" "${outputPath}"`, {stdio: 'pipe',timeout: 30000});} catch (error2) {console.log('heif-convert 失败:', error2.message);logger.debug('所有转换方法都失败了');throw msgCode[39522]('上传');}}if (!fs.existsSync(outputPath) || fs.statSync(outputPath).size === 0) {logger.debug('转换失败,输出文件为空');throw msgCode[39522]('上传');}console.log('系统命令转换成功');return outputPath;} finally {// 清理临时文件if (tempDir && fs.existsSync(tempDir)) {try {fs.rmSync(tempDir, { recursive: true, force: true });} catch (cleanError) {console.warn('清理临时目录失败:', cleanError.message);}}}
};
// 创建带进度反馈的转换函数
ImageTool.convertHeicWithProgress = async function(heicBuffer, quality = 80) {console.log('开始 HEIC 转换,文件大小:', heicBuffer.length, 'bytes');// 添加进度反馈console.log('转换进行中...');try {const jpegBuffer = await convert({buffer: heicBuffer,format: 'JPEG',quality: quality / 100});console.log('HEIC 转换完成,JPEG 大小:', jpegBuffer.length, 'bytes');return jpegBuffer;} catch (error) {console.error('HEIC 转换错误:', error);// throw error;throw msgCode[39522]('上传');}
};
// 带超时和重试的转换函数
ImageTool.convertHeicWithRetry = async function(heicBuffer, quality = 80, maxRetries = 2) {for (let attempt = 1; attempt <= maxRetries; attempt++) {try {console.log(`转换尝试 ${attempt}/${maxRetries}`);return await ImageTool.convertHeicWithProgress(heicBuffer, quality);} catch (error) {console.log(`尝试 ${attempt} 失败:`, error.message);if (attempt === maxRetries) {// throw error;throw msgCode[39522]('上传');}// 等待一下再重试await new Promise(resolve => setTimeout(resolve, 1000));}}
};
ImageTool.convertLivpToJpeg = async function(livpPath, quality = 80) {let tempDir = null;try {console.log('1. 开始转换 LIVP 文件');const extractDir = path.dirname(livpPath);tempDir = path.join(extractDir, 'temp_livp_' + Date.now());// 创建临时目录fs.mkdirSync(tempDir, { recursive: true });console.log('2. 创建临时目录:', tempDir);// 解压 LIVP 文件console.log('3. 开始解压 ZIP 文件...');await new Promise((resolve, reject) => {fs.createReadStream(livpPath).pipe(unzipper.Extract({ path: tempDir })).on('close', () => {console.log('4. ZIP 解压完成');resolve();}).on('error', reject);});// 查找 HEIC 文件const files = fs.readdirSync(tempDir);console.log('5. 解压出的文件:', files);const heicFile = files.find(file => path.extname(file).toLowerCase() === '.heic');if (!heicFile) {logger.debug('在 LIVP 文件中找不到 HEIC 图片');throw msgCode[39522]('上传');}const heicPath = path.join(tempDir, heicFile);console.log('6. 找到 HEIC 文件:', heicPath);// 验证 HEIC 文件const stats = fs.statSync(heicPath);console.log('7. HEIC 文件大小:', stats.size, 'bytes');if (stats.size === 0) {throw new Error('HEIC 文件为空');}// 读取 HEIC 文件console.log('8. 读取 HEIC 文件...');const heicBuffer = fs.readFileSync(heicPath);console.log('9. HEIC 文件读取完成');// 使用带重试的转换console.log('10. 开始转换 HEIC 到 JPEG...');const jpegBuffer = await ImageTool.convertHeicWithRetry(heicBuffer, quality);const outputPath = livpPath.replace('.livp', '.jpg');console.log('11. 保存 JPEG 文件到:', outputPath);fs.writeFileSync(outputPath, jpegBuffer);// 验证输出文件const outputStats = fs.statSync(outputPath);console.log('12. 输出文件大小:', outputStats.size, 'bytes');if (outputStats.size === 0) {logger.debug('输出的 JPEG 文件为空');throw msgCode[39522]('上传');}console.log('13. LIVP 转换成功');return outputPath;} catch (error) {console.error('!!! LIVP 转换失败:', error);// throw error;throw msgCode[39522]('上传');} finally {// 清理临时文件if (tempDir && fs.existsSync(tempDir)) {try {console.log('清理临时目录:', tempDir);fs.rmSync(tempDir, { recursive: true, force: true });} catch (cleanError) {console.warn('清理临时目录失败:', cleanError.message);}}}
};
ImageTool.processLivpFile = async function(livpPath) {try {console.log('处理 LIVP 文件:', livpPath);// 首先尝试快速系统命令方法return await ImageTool.convertLivpToJpegDirect(livpPath, 80);} catch (error) {console.log('系统命令方法失败:', error.message);console.log('尝试 Node.js 方法...');// 备选:使用 Node.js 方法(带超时)return await ImageTool.convertLivpToJpeg(livpPath, 80);}
};
调用代码:
const supportedImageExt = ['.jpg', '.jpeg', '.png'];
// 容器格式
const containerFormats = ['.livp'];
const ImageTool = require('@app/tools/image_tool.js');
async function dealImg(ctx) {// 获取文件扩展名const fileExt = path.extname(ctx.req.file.originalname).toLowerCase();let convertedPath = null; // 检查是否需要转换// 1. 如果是 LIVP 容器格式,先解包if (containerFormats.includes(fileExt)) {console.log('=== 开始处理 LIVP 文件 ===');convertedPath = await ImageTool.processLivpFile(ctx.req.file.path);if (!convertedPath || !fs.existsSync(convertedPath)) {throw new Error('LIVP 文件处理失败');}console.log('转换完成,开始上传到 OSS...');const readStream = fs.createReadStream(convertedPath);const filename = '/img/' + path.basename(ctx.req.file.originalname, '.livp') + '.jpg';//上传图片到阿里云return ImageTool.uploadImage(filename, fileExt, readStream, convertedPath, ctx.req.file.path);}else if (fileExt === '.heic') { console.log('=== 开始处理 HEIC 文件 ===');convertedPath = await ImageTool.processHeicFile(ctx.req.file.path, 80);const readStream = fs.createReadStream(convertedPath);// 修复文件名处理 - 移除整个扩展名const originalNameWithoutExt = path.basename(ctx.req.file.originalname, path.extname(ctx.req.file.originalname));const filename = '/img/' + originalNameWithoutExt + '.jpg';// const urlPath = '/' + IMAGE_PATH + '/' + originalNameWithoutExt + '.jpg';//上传图片到阿里云return ImageTool.uploadImage(filename, fileExt, readStream, convertedPath, ctx.req.file.path);}
}