295 lines
10 KiB
JavaScript
295 lines
10 KiB
JavaScript
// /**
|
||
// * 获取文件大小(Promise封装)
|
||
// * @param {string} filePath
|
||
// * @returns {Promise<number>} 文件字节数
|
||
// */
|
||
// const getFileSize = (filePath) => {
|
||
// return new Promise(resolve => {
|
||
// uni.getFileInfo({
|
||
// filePath,
|
||
// success: (res) => resolve(res.size),
|
||
// fail: () => resolve(Infinity)
|
||
// });
|
||
// });
|
||
// };
|
||
|
||
|
||
/**
|
||
* 获取文件大小(兼容新版API)
|
||
* @param {string} filePath
|
||
* @returns {Promise<number>} 文件字节数
|
||
*/
|
||
const getFileSize = async (filePath) => {
|
||
try {
|
||
const res = await new Promise((resolve, reject) => {
|
||
|
||
wx.getFileSystemManager().getFileInfo({
|
||
filePath,
|
||
success: resolve,
|
||
fail: reject
|
||
});
|
||
});
|
||
return res.size || Infinity;
|
||
} catch (err) {
|
||
console.error('获取文件大小失败:', err);
|
||
return Infinity;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 执行图片压缩(Promise封装)
|
||
* @param {string} src
|
||
* @param {number} quality
|
||
* @returns {Promise<string>} 压缩后的临时路径
|
||
*/
|
||
const doCompressImage = (src, quality) => {
|
||
return new Promise(resolve => {
|
||
uni.compressImage({
|
||
src,
|
||
quality,
|
||
success: (res) => resolve(res.tempFilePath),
|
||
fail: () => resolve(src) // 失败返回原路径
|
||
});
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 智能图片压缩(支持多平台)
|
||
* @param {string} tempFilePath - 图片临时路径
|
||
* @param {number} maxSize - 目标最大字节数(默认500KB)
|
||
* @param {number} [initialQuality=80] - 初始压缩质量(1-100)
|
||
* @returns {Promise<string>} 压缩后的临时路径
|
||
*/
|
||
export const compressImage = async (tempFilePath, maxSize = 500 * 1024, initialQuality = 80) => {
|
||
let quality = initialQuality;
|
||
let compressedPath = tempFilePath;
|
||
let currentSize = Infinity;
|
||
|
||
// 二分法压缩参数
|
||
let low = 10;
|
||
let high = initialQuality;
|
||
|
||
while (low <= high) {
|
||
quality = Math.floor((low + high) / 2);
|
||
|
||
// 使用提取的Promise方法
|
||
compressedPath = await doCompressImage(tempFilePath, quality);
|
||
currentSize = await getFileSize(compressedPath);
|
||
|
||
if (currentSize <= maxSize) {
|
||
low = quality + 1; // 尝试更高质量
|
||
} else {
|
||
high = quality - 1; // 降低质量
|
||
}
|
||
|
||
if (high - low < 5) break; // 安全阀
|
||
}
|
||
|
||
// 最终强制压缩(如果仍未达标)
|
||
if (currentSize > maxSize) {
|
||
compressedPath = await doCompressImage(tempFilePath, 10);
|
||
}
|
||
|
||
return compressedPath;
|
||
};
|
||
|
||
|
||
/**
|
||
* 精准图片压缩(优化版)
|
||
* @param {string} tempFilePath - 图片临时路径
|
||
* @param {number} targetSize - 目标字节数(默认500KB)
|
||
* @param {number} [initialQuality=85] - 初始压缩质量(1-100)
|
||
* @returns {Promise<{path: string, quality: number, iterations: number, finalSize: number}>}
|
||
*/
|
||
export const preciseCompress = async (tempFilePath, targetSize = 500 * 1024, initialQuality = 85) => {
|
||
let quality = initialQuality;
|
||
let compressedPath = tempFilePath;
|
||
let currentSize = Infinity;
|
||
let iterations = 0;
|
||
const maxIterations = 10; // 增加最大迭代次数
|
||
const history = []; // 记录每次压缩结果用于分析
|
||
|
||
// 首先检查原图大小,如果已经满足直接返回
|
||
const originalSize = await getFileSize(tempFilePath);
|
||
if (originalSize <= targetSize) {
|
||
return { path: tempFilePath, quality: 100, iterations: 0, finalSize: originalSize };
|
||
}
|
||
|
||
// 智能预测初始质量范围(新增)
|
||
const predictQualityRange = (originalSize, targetSize) => {
|
||
const ratio = targetSize / originalSize;
|
||
if (ratio > 0.8) return { low: 70, high: 95 };
|
||
if (ratio > 0.5) return { low: 45, high: 75 };
|
||
if (ratio > 0.3) return { low: 25, high: 50 };
|
||
if (ratio > 0.1) return { low: 10, high: 30 };
|
||
return { low: 5, high: 15 }; // 极限压缩
|
||
};
|
||
|
||
const { low: predictedLow, high: predictedHigh } = predictQualityRange(originalSize, targetSize);
|
||
let low = predictedLow;
|
||
let high = Math.min(predictedHigh, initialQuality);
|
||
|
||
console.log(`预测质量范围: ${low}-${high} (原图${(originalSize/1024).toFixed(2)}KB → 目标${(targetSize/1024).toFixed(2)}KB)`);
|
||
|
||
// 动态调整二分法边界(优化版)
|
||
const adjustBounds = (currentQuality, currentSize, targetSize) => {
|
||
const sizeRatio = currentSize / targetSize;
|
||
const qualityDelta = Math.max(2, Math.floor(currentQuality * 0.1)); // 动态调整幅度
|
||
|
||
if (sizeRatio > 2.5) {
|
||
return { lowDelta: -qualityDelta * 2, highDelta: -qualityDelta };
|
||
} else if (sizeRatio > 1.5) {
|
||
return { lowDelta: -qualityDelta, highDelta: -Math.floor(qualityDelta / 2) };
|
||
} else if (sizeRatio > 1.1) {
|
||
return { lowDelta: -3, highDelta: -1 };
|
||
} else {
|
||
return { lowDelta: -1, highDelta: 0 };
|
||
}
|
||
};
|
||
|
||
let bestResult = null; // 记录最佳结果
|
||
|
||
while (iterations < maxIterations && low <= high) {
|
||
iterations++;
|
||
quality = Math.floor((low + high) / 2);
|
||
|
||
// 跳过已经测试过的quality值
|
||
if (history.some(item => item.quality === quality)) {
|
||
break;
|
||
}
|
||
|
||
compressedPath = await doCompressImage(tempFilePath, quality);
|
||
currentSize = await getFileSize(compressedPath);
|
||
history.push({ quality, size: currentSize });
|
||
|
||
console.log(`第${iterations}次尝试: quality=${quality} → ${(currentSize / 1024).toFixed(2)}KB (目标${(targetSize/1024).toFixed(2)}KB)`);
|
||
|
||
// 更新最佳结果
|
||
if (!bestResult || Math.abs(currentSize - targetSize) < Math.abs(bestResult.size - targetSize)) {
|
||
bestResult = { quality, size: currentSize, path: compressedPath };
|
||
}
|
||
|
||
// 精准匹配检查(容差范围调整为3%)
|
||
if (Math.abs(currentSize - targetSize) < targetSize * 0.03) {
|
||
console.log('✅ 达到精准匹配,提前结束');
|
||
break;
|
||
}
|
||
|
||
if (currentSize > targetSize) {
|
||
const { lowDelta, highDelta } = adjustBounds(quality, currentSize, targetSize);
|
||
high = Math.max(5, quality + highDelta); // 确保不会低于最小值
|
||
} else {
|
||
low = quality + 1; // 当小于目标时,尝试更高质量
|
||
}
|
||
|
||
// 收敛检查
|
||
if (high - low <= 1) break;
|
||
}
|
||
|
||
// 如果最佳结果仍然超标,尝试极限压缩
|
||
if (bestResult && bestResult.size > targetSize * 1.1) {
|
||
console.log('🔥 尝试极限压缩...');
|
||
const extremePath = await doCompressImage(tempFilePath, 5);
|
||
const extremeSize = await getFileSize(extremePath);
|
||
console.log(`极限压缩结果: quality=5 → ${(extremeSize / 1024).toFixed(2)}KB`);
|
||
|
||
if (extremeSize < bestResult.size) {
|
||
bestResult = { quality: 5, size: extremeSize, path: extremePath };
|
||
}
|
||
}
|
||
|
||
const finalResult = bestResult || { quality: 10, size: currentSize, path: compressedPath };
|
||
|
||
console.log(`🎯 最终结果:quality=${finalResult.quality} → ${(finalResult.size / 1024).toFixed(2)}KB` +
|
||
`(迭代${iterations}次,压缩率${((1 - finalResult.size / originalSize) * 100).toFixed(1)}%)`);
|
||
|
||
return {
|
||
path: finalResult.path,
|
||
quality: finalResult.quality,
|
||
iterations,
|
||
finalSize: finalResult.size
|
||
};
|
||
};
|
||
|
||
|
||
|
||
/**
|
||
* 图片转Base64(兼容多平台)
|
||
* @param {string} filePath - 图片临时路径
|
||
* @returns {Promise<string>} Base64字符串(带data:image/前缀)
|
||
*/
|
||
export const imageToBase64 = (filePath) => {
|
||
return new Promise((resolve, reject) => {
|
||
// 先获取图片类型
|
||
uni.getImageInfo({
|
||
src: filePath,
|
||
success: (info) => {
|
||
const type = info.type || 'jpeg';
|
||
// 读取文件为Base64
|
||
uni.getFileSystemManager().readFile({
|
||
filePath,
|
||
encoding: 'base64',
|
||
success: (res) => resolve(`data:image/${type};base64,${res.data}`),
|
||
fail: reject
|
||
});
|
||
},
|
||
fail: () => {
|
||
// 类型获取失败时默认jpeg
|
||
uni.getFileSystemManager().readFile({
|
||
filePath,
|
||
encoding: 'base64',
|
||
success: (res) => resolve(`data:image/jpeg;base64,${res.data}`),
|
||
fail: reject
|
||
});
|
||
}
|
||
});
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 完整处理流程(智能判断是否压缩):
|
||
* 选择图片 → 检查大小 → 按需压缩 → 转Base64
|
||
* @returns {Promise<{ base64: string, path: string, compressed: boolean }>}
|
||
*/
|
||
export const processImage = async (maxSize = 500 * 1024) => {
|
||
// 1. 选择图片
|
||
const chooseRes = await new Promise(resolve => {
|
||
uni.chooseImage({
|
||
count: 1,
|
||
sizeType: ['original', 'compressed'],
|
||
success: resolve,
|
||
fail: () => resolve(null)
|
||
});
|
||
});
|
||
if (!chooseRes) throw new Error('选择图片失败');
|
||
|
||
const tempFilePath = chooseRes.tempFilePaths[0];
|
||
let finalPath = tempFilePath;
|
||
let isCompressed = false;
|
||
|
||
// 2. 检查文件大小
|
||
const fileSize = await getFileSize(tempFilePath);
|
||
console.log(`原图大小: ${(fileSize / 1024).toFixed(2)}KB`);
|
||
|
||
// 3. 按需压缩
|
||
if (fileSize > maxSize) {
|
||
console.log('🔄 开始智能压缩...');
|
||
const compressResult = await preciseCompress(tempFilePath, maxSize);
|
||
finalPath = compressResult.path;
|
||
isCompressed = true;
|
||
|
||
// 验证压缩结果
|
||
console.log(`✅ 压缩完成: ${(fileSize / 1024).toFixed(2)}KB → ${(compressResult.finalSize / 1024).toFixed(2)}KB` +
|
||
` (节省${((1 - compressResult.finalSize / fileSize) * 100).toFixed(1)}%)`);
|
||
}
|
||
|
||
// 4. 转换为Base64
|
||
const base64 = await imageToBase64(finalPath);
|
||
console.log(`📄 Base64长度: ${base64.length} 字符`);
|
||
|
||
return {
|
||
base64, // Base64字符串
|
||
path: finalPath, // 最终文件路径(可能是原图或压缩图)
|
||
compressed: isCompressed // 是否经过压缩
|
||
};
|
||
}; |