js 压缩base64图片 可设置缩放比例、质量、格式、大小、超时时间

/**
 * 压缩base64图片的函数
 *
 * @param {string} base64 - 图片的base64编码
 * @param {number} multiple - 缩放比例
 * @param {number} quality - 输出图片的质量(0.0到1.0之间)
 * @param {string} format - 输出图片的格式,默认为"image/webp" 可选参数 image/png、image/jpeg、image/webp 等
 * @param {number} min - 压缩后图片的最小KB
 * @param {number} max - 压缩后图片的最大KB
 * @timeout {number} timeout - 压缩图片最大等待时长,如果超时返回最后一次压缩数据
 * @returns {Promise<string>} - 返回压缩后的图片的base64编码
 */

export function compressImg(
    base64,
    multiple = 1,
    quality = 1,
    format = "image/webp",
    min,
    max,
    timeout = 1000
) {
    return new Promise((resolve, reject) => {
        if (!base64) return resolve(base64);
        const startTime = Date.now();
        const timeoutThreshold = startTime + timeout;
        let isCompleted = false;

        const finalize = (result, error = null) => {
            if (isCompleted) return;
            isCompleted = true;
            cleanup();
            error ? reject(error) : resolve(result);
        };

        const cleanup = () => {
            clearTimeout(loadTimer);
            img.onload = null;
            img.onerror = null;
            canvas = null;
        };

        const createStepGenerator = (target, currentSize, isMax) => {
            const sizeDiff = Math.abs(target - currentSize);
            const initialStep = sizeDiff > 100 ? 0.1 :
                sizeDiff > 50 ? 0.05 :
                    sizeDiff > 10 ? 0.02 : 0.01;

            return {
                current: initialStep,
                next() {
                    this.current = Math.max(
                        this.current * (isMax ? 0.8 : 1.2),
                        0.005
                    );
                    return this.current;
                }
            };
        };

        const optimizeQuality = (initialQ, targetKB, isMax) => {
            let q = Math.min(Math.max(initialQ, 0.1), 0.95);
            let bestQ = q;
            let bestSize = getSizeForQuality(q);
            const stepGen = createStepGenerator(targetKB, bestSize, isMax);
            let direction = isMax ? -1 : 1;

            while (true) {
                if (Date.now() > timeoutThreshold) return bestQ;
                const currentSize = getSizeForQuality(q);
                if (currentSize === undefined) return bestQ;

                if ((isMax && currentSize <= targetKB) ||
                    (!isMax && currentSize >= targetKB)) {
                    if ((isMax && q > bestQ) || (!isMax && q < bestQ)) {
                        bestQ = q;
                        bestSize = currentSize;
                    }
                }

                if ((isMax && currentSize > targetKB) ||
                    (!isMax && currentSize < targetKB)) {
                    q += direction * stepGen.next();
                } else {
                    q += direction * stepGen.current;
                }

                if (q > 0.95 || q < 0.1) {
                    direction *= -0.5;
                    q = Math.min(Math.max(q, 0.1), 0.95);
                }

                if (Math.abs(bestSize - targetKB) < 2 ||
                    stepGen.current < 0.006) {
                    return bestQ;
                }
            }
        };

        const getSizeForQuality = (q) => {
            if (Date.now() > timeoutThreshold) return;
            try {
                return canvas.toDataURL(validFormat, q).length / 1024;
            } catch {
                return Infinity;
            }
        };

        const img = new Image();
        let canvas;
        let validFormat = [...new Set([format, "image/jpeg", "image/png"])]
            .find(f => document.createElement("canvas").toDataURL(f) !== "data:,");

        const loadTimer = setTimeout(() => {
            finalize(null, new Error("Image load timeout"));
        }, timeout - 50);

        img.crossOrigin = "Anonymous";
        img.onerror = () => finalize(null, new Error("Image load failed"));
        img.src = base64;

        img.onload = function () {
            try {
                if (Date.now() > timeoutThreshold) {
                    return finalize(null, new Error("Operation timeout"));
                }
                const width = img.width * multiple;
                const height = img.height * multiple;
                canvas = document.createElement("canvas");
                canvas.width = width;
                canvas.height = height;
                canvas.getContext("2d").drawImage(img, 0, 0, width, height);

                let optimizedQuality = quality;
                let finalDataUrl;

                // 获取初始质量对应的文件大小
                const initialSize = getSizeForQuality(quality);


                // 优先处理max限制(基于调整后的质量)
                if (typeof max === "number" && initialSize > max) {
                    optimizedQuality = optimizeQuality(optimizedQuality, max, true);
                }

                // 处理min限制
                if (typeof min === "number" && initialSize < min) {
                    optimizedQuality = optimizeQuality(optimizedQuality, min, false);
                }
                finalDataUrl = canvas.toDataURL(
                    validFormat,
                    Math.min(Math.max(optimizedQuality, 0.1), 0.95)
                );
                finalize(finalDataUrl);
            } catch (error) {
                finalize(null, error);
            }
        };
    });
}

使用: const result = await compressImg(base64, 1, .8, "image/webp", 0, 1024,1000)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值