video-flow-toon/src/utils/imageTools.ts
2026-02-04 14:39:05 +08:00

122 lines
3.8 KiB
TypeScript

import sharp from "sharp";
/**
* 解析大小字符串为字节数
*/
function parseSize(size: string): number {
const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(kb|mb|gb|b)?$/);
if (!match) {
throw new Error(`无效的大小格式: ${size}`);
}
const value = parseFloat(match[1]);
const unit = match[2] || "b";
const multipliers: Record<string, number> = {
b: 1,
kb: 1024,
mb: 1024 * 1024,
gb: 1024 * 1024 * 1024,
};
return Math.floor(value * multipliers[unit]);
}
/**
* 将base64字符串转换为Buffer
*/
function base64ToBuffer(base64: string): Buffer {
const base64Data = base64.replace(/^data:image\/\w+;base64,/, "");
return Buffer.from(base64Data, "base64");
}
/**
* 压缩Buffer到指定大小以内
*/
async function compressToSize(imageBuffer: Buffer, maxBytes: number, originalWidth: number, originalHeight: number): Promise<Buffer> {
let quality = 90;
let scale = 1;
while (true) {
const targetWidth = Math.round(originalWidth * scale);
const targetHeight = Math.round(originalHeight * scale);
const resultBuffer = await sharp(imageBuffer).resize(targetWidth, targetHeight, { fit: "fill" }).jpeg({ quality }).toBuffer();
if (resultBuffer.length <= maxBytes) {
return resultBuffer;
}
if (quality > 10) {
quality -= 10;
} else {
quality = 90;
scale *= 0.8;
}
}
}
/**
* 压缩单张图片到指定大小以内
* @param imageBase64 - base64编码的图片
* @param maxSize - 最大输出大小,支持格式如 "10mb", "5MB", "1024kb" 等
* @returns 压缩后的图片base64字符串
*/
export async function compressImage(imageBase64: string, maxSize = "10mb"): Promise<string> {
const maxBytes = parseSize(maxSize);
const imageBuffer = base64ToBuffer(imageBase64);
const metadata = await sharp(imageBuffer).metadata();
const resultBuffer = await compressToSize(imageBuffer, maxBytes, metadata.width || 1, metadata.height || 1);
return resultBuffer.toString("base64");
}
/**
* 将多张图片横向拼接为一张,并确保输出大小不超过指定限制
* @param imageBase64List - base64编码的图片数组
* @param maxSize - 最大输出大小,支持格式如 "10mb", "5MB", "1024kb" 等
* @returns 拼接后的图片base64字符串
*/
export async function mergeImages(imageBase64List: string[], maxSize = "10mb"): Promise<string> {
if (imageBase64List.length === 0) {
throw new Error("图片列表不能为空");
}
const maxBytes = parseSize(maxSize);
const imageBuffers = imageBase64List.map(base64ToBuffer);
const imageMetadatas = await Promise.all(imageBuffers.map((buffer) => sharp(buffer).metadata()));
const maxHeight = Math.max(...imageMetadatas.map((m) => m.height || 0));
// 计算各图片调整后的宽度
const imageWidths = imageMetadatas.map((metadata) => {
const aspectRatio = (metadata.width || 1) / (metadata.height || 1);
return Math.round(maxHeight * aspectRatio);
});
const totalWidth = imageWidths.reduce((sum, w) => sum + w, 0);
// 拼接图片
const resizedImages = await Promise.all(
imageBuffers.map(async (buffer, index) => {
return sharp(buffer).resize(imageWidths[index], maxHeight, { fit: "cover" }).toBuffer();
}),
);
let currentX = 0;
const compositeInputs = resizedImages.map((buffer, index) => {
const input = { input: buffer, left: currentX, top: 0 };
currentX += imageWidths[index];
return input;
});
const mergedBuffer = await sharp({
create: {
width: totalWidth,
height: maxHeight,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 1 },
},
})
.composite(compositeInputs)
.jpeg({ quality: 90 })
.toBuffer();
// 复用压缩逻辑
const resultBuffer = await compressToSize(mergedBuffer, maxBytes, totalWidth, maxHeight);
return resultBuffer.toString("base64");
}