首页 文章

如何在浏览器中通过Javascript压缩图像?

提问于
浏览
56

TL;DR;

有没有办法在上传之前直接在浏览器端压缩图像(主要是jpeg,png和gif)?我很确定JavaScript可以做到这一点,但我找不到实现它的方法 .

这是我想要实现的完整场景:

  • 用户访问我的网站,并通过 input type="file" 元素选择图像,

  • 此图片是通过JavaScript检索的,我们会做一些验证,例如正确的文件格式,最大文件大小等,

  • 如果一切正常,页面上会显示图像的预览,

  • 用户可以进行一些基本操作,例如将图像旋转90°/ -90°,按照预定义的比例裁剪等,或者用户可以上传另一张图像并返回步骤1,

  • 当用户满意时,编辑后的图像会被压缩并在本地"saved"(不保存到文件中,但在浏览器内存/页面中), -

  • 用户填写表格,其中包含姓名,年龄等数据,

  • 用户点击"Finish"按钮,然后将包含数据压缩图像的表单发送到服务器(不带AJAX),

直到最后一步的完整过程应该在客户端完成,并且应该与最新的Chrome和Firefox,Safari 5和 IE 8+ 兼容 . 如果可能,只应使用JavaScript(但我很确定这是不可能的) .

我已经考虑过了 . 可以通过File API在本地读取文件,可以使用Canvas元素进行图像预览和编辑,但是 I can't find a way to do the image compression part .

根据html5please.comcaniuse.com,支持这些浏览器非常困难(感谢IE),但可以使用FlashCanvasFileReader之类的polyfill来完成 .

实际上,目标是减小文件大小,因此我将图像压缩视为一种解决方案 . 但是,我知道上传的图像将在我的网站上显示,每次都在同一个地方,我知道这个显示区域的尺寸(例如200x400) . 因此,我可以调整图像大小以适应这些尺寸,从而减小文件大小 . 我不知道这种技术的压缩比是多少 .

你怎么看 ?你有什么建议告诉我吗?你知道在JavaScript中压缩图像浏览器的方法吗?谢谢你的回复 .

6 回答

  • 8

    编辑:根据Mr Me对此答案的评论,看起来压缩现在可用于JPG / WebP格式(请参阅https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL) .

    据我所知,你无法使用画布压缩图像,相反,你可以调整它的大小 . 使用canvas.toDataURL不会让您选择要使用的压缩率 . 您可以查看完全符合您要求的canimage:https://github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js

    实际上,通常只需调整图像大小以减小其大小即可,但如果您想要更进一步,则必须使用新引入的方法file.readAsArrayBuffer来获取包含图像数据的缓冲区 .

    然后,只需使用DataView根据图像格式规范(http://en.wikipedia.org/wiki/JPEGhttp://en.wikipedia.org/wiki/Portable_Network_Graphics)读取其内容 .

    处理图像数据压缩会很困难,但尝试更糟糕 . 另一方面,您可以尝试删除PNG标头或JPEG exif数据以使图像更小,这样做应该更容易 .

    您必须在另一个缓冲区上创建另一个DataWiew,并用过滤后的图像内容填充它 . 然后,您只需使用window.btoa将图像内容编码为DataURI .

    让我知道如果你实现类似的东西,将通过代码将是有趣的 .

  • 104

    @PsychoWoods的回答很好 . 我想提供自己的解决方案 . 此Javascript函数获取图像数据URL和宽度,将其缩放到新宽度,并返回新的数据URL .

    // Take an image URL, downscale it to the given width, and return a new image URL.
    function downscaleImage(dataUrl, newWidth, imageType, imageArguments) {
        "use strict";
        var image, oldWidth, oldHeight, newHeight, canvas, ctx, newDataUrl;
    
        // Provide default values
        imageType = imageType || "image/jpeg";
        imageArguments = imageArguments || 0.7;
    
        // Create a temporary image so that we can compute the height of the downscaled image.
        image = new Image();
        image.src = dataUrl;
        oldWidth = image.width;
        oldHeight = image.height;
        newHeight = Math.floor(oldHeight / oldWidth * newWidth)
    
        // Create a temporary canvas to draw the downscaled image on.
        canvas = document.createElement("canvas");
        canvas.width = newWidth;
        canvas.height = newHeight;
    
        // Draw the downscaled image on the canvas and return the new data URL.
        ctx = canvas.getContext("2d");
        ctx.drawImage(image, 0, 0, newWidth, newHeight);
        newDataUrl = canvas.toDataURL(imageType, imageArguments);
        return newDataUrl;
    }
    

    此代码可以在您拥有数据URL的任何地方使用,并且需要缩小图像的数据URL .

  • 3

    简而言之:

    • 使用带有.readAsArrayBuffer的HTML5 FileReader API读取文件

    • 使用文件数据创建e Blob并使用window.URL.createObjectURL(blob)获取其URL

    • 创建新的Image元素并将其src设置为文件blob url

    • 将图像发送到画布 . 画布大小设置为所需的输出大小

    • 通过canvas.toDataURL("image/jpeg",0.7)从画布返回缩小的数据(设置您自己的输出格式和质量)

    • 将新的隐藏输入附加到原始表单,并将dataURI图像基本上作为普通文本传输

    • 在后端,读取dataURI,从Base64解码,然后保存

    资料来源:code .

  • 10

    上面由@ daniel-allen-langdon发布的 downscaleImage() 函数存在一个问题,因为图像加载是 asynchronousimage.widthimage.height 属性不能立即使用 .

    请参阅下面更新的TypeScript示例,该示例将此考虑在内,使用 async 函数,并根据最长维度调整图像大小,而不仅仅是宽度

    function getImage(dataUrl: string): Promise<HTMLImageElement> 
    {
        return new Promise((resolve, reject) => {
            const image = new Image();
            image.src = dataUrl;
            image.onload = () => {
                resolve(image);
            };
            image.onerror = (el: any, err: ErrorEvent) => {
                reject(err.error);
            };
        });
    }
    
    export async function downscaleImage(
            dataUrl: string,  
            imageType: string,  // e.g. 'image/jpeg'
            resolution: number,  // max width/height in pixels
            quality: number   // e.g. 0.9 = 90% quality
        ): Promise<string> {
    
        // Create a temporary image so that we can compute the height of the image.
        const image = await getImage(dataUrl);
        const oldWidth = image.naturalWidth;
        const oldHeight = image.naturalHeight;
        console.log('dims', oldWidth, oldHeight);
    
        const longestDimension = oldWidth > oldHeight ? 'width' : 'height';
        const currentRes = longestDimension == 'width' ? oldWidth : oldHeight;
        console.log('longest dim', longestDimension, currentRes);
    
        if (currentRes > resolution) {
            console.log('need to resize...');
    
            // Calculate new dimensions
            const newSize = longestDimension == 'width'
                ? Math.floor(oldHeight / oldWidth * resolution)
                : Math.floor(oldWidth / oldHeight * resolution);
            const newWidth = longestDimension == 'width' ? resolution : newSize;
            const newHeight = longestDimension == 'height' ? resolution : newSize;
            console.log('new width / height', newWidth, newHeight);
    
            // Create a temporary canvas to draw the downscaled image on.
            const canvas = document.createElement('canvas');
            canvas.width = newWidth;
            canvas.height = newHeight;
    
            // Draw the downscaled image on the canvas and return the new data URL.
            const ctx = canvas.getContext('2d')!;
            ctx.drawImage(image, 0, 0, newWidth, newHeight);
            const newDataUrl = canvas.toDataURL(imageType, quality);
            return newDataUrl;
        }
        else {
            return dataUrl;
        }
    
    }
    
  • 1

    我看到其他答案中缺少两件事:

    • canvas.toBlob (如果可用)比 canvas.toDataURL 更高效,也是异步 .

    • 该文件 - >图像 - >画布 - >文件转换丢失EXIF数据;特别是,关于图像旋转的数据通常由现代手机/平板电脑设定 .

    以下脚本处理这两点:

    // From https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob, needed for Safari:
    if (!HTMLCanvasElement.prototype.toBlob) {
        Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
            value: function(callback, type, quality) {
    
                var binStr = atob(this.toDataURL(type, quality).split(',')[1]),
                    len = binStr.length,
                    arr = new Uint8Array(len);
    
                for (var i = 0; i < len; i++) {
                    arr[i] = binStr.charCodeAt(i);
                }
    
                callback(new Blob([arr], {type: type || 'image/png'}));
            }
        });
    }
    
    window.URL = window.URL || window.webkitURL;
    
    // Modified from https://stackoverflow.com/a/32490603, cc by-sa 3.0
    // -2 = not jpeg, -1 = no data, 1..8 = orientations
    function getExifOrientation(file, callback) {
        // Suggestion from http://code.flickr.net/2012/06/01/parsing-exif-client-side-using-javascript-2/:
        if (file.slice) {
            file = file.slice(0, 131072);
        } else if (file.webkitSlice) {
            file = file.webkitSlice(0, 131072);
        }
    
        var reader = new FileReader();
        reader.onload = function(e) {
            var view = new DataView(e.target.result);
            if (view.getUint16(0, false) != 0xFFD8) {
                callback(-2);
                return;
            }
            var length = view.byteLength, offset = 2;
            while (offset < length) {
                var marker = view.getUint16(offset, false);
                offset += 2;
                if (marker == 0xFFE1) {
                    if (view.getUint32(offset += 2, false) != 0x45786966) {
                        callback(-1);
                        return;
                    }
                    var little = view.getUint16(offset += 6, false) == 0x4949;
                    offset += view.getUint32(offset + 4, little);
                    var tags = view.getUint16(offset, little);
                    offset += 2;
                    for (var i = 0; i < tags; i++)
                        if (view.getUint16(offset + (i * 12), little) == 0x0112) {
                            callback(view.getUint16(offset + (i * 12) + 8, little));
                            return;
                        }
                }
                else if ((marker & 0xFF00) != 0xFF00) break;
                else offset += view.getUint16(offset, false);
            }
            callback(-1);
        };
        reader.readAsArrayBuffer(file);
    }
    
    // Derived from https://stackoverflow.com/a/40867559, cc by-sa
    function imgToCanvasWithOrientation(img, rawWidth, rawHeight, orientation) {
        var canvas = document.createElement('canvas');
        if (orientation > 4) {
            canvas.width = rawHeight;
            canvas.height = rawWidth;
        } else {
            canvas.width = rawWidth;
            canvas.height = rawHeight;
        }
    
        if (orientation > 1) {
            console.log("EXIF orientation = " + orientation + ", rotating picture");
        }
    
        var ctx = canvas.getContext('2d');
        switch (orientation) {
            case 2: ctx.transform(-1, 0, 0, 1, rawWidth, 0); break;
            case 3: ctx.transform(-1, 0, 0, -1, rawWidth, rawHeight); break;
            case 4: ctx.transform(1, 0, 0, -1, 0, rawHeight); break;
            case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
            case 6: ctx.transform(0, 1, -1, 0, rawHeight, 0); break;
            case 7: ctx.transform(0, -1, -1, 0, rawHeight, rawWidth); break;
            case 8: ctx.transform(0, -1, 1, 0, 0, rawWidth); break;
        }
        ctx.drawImage(img, 0, 0, rawWidth, rawHeight);
        return canvas;
    }
    
    function reduceFileSize(file, acceptFileSize, maxWidth, maxHeight, quality, callback) {
        if (file.size <= acceptFileSize) {
            callback(file);
            return;
        }
        var img = new Image();
        img.onerror = function() {
            URL.revokeObjectURL(this.src);
            callback(file);
        };
        img.onload = function() {
            URL.revokeObjectURL(this.src);
            getExifOrientation(file, function(orientation) {
                var w = img.width, h = img.height;
                var scale = (orientation > 4 ?
                    Math.min(maxHeight / w, maxWidth / h, 1) :
                    Math.min(maxWidth / w, maxHeight / h, 1));
                h = Math.round(h * scale);
                w = Math.round(w * scale);
    
                var canvas = imgToCanvasWithOrientation(img, w, h, orientation);
                canvas.toBlob(function(blob) {
                    console.log("Resized image to " + w + "x" + h + ", " + (blob.size >> 10) + "kB");
                    callback(blob);
                }, 'image/jpeg', quality);
            });
        };
        img.src = URL.createObjectURL(file);
    }
    

    用法示例:

    inputfile.onchange = function() {
        // If file size > 500kB, resize such that width <= 1000, quality = 0.9
        reduceFileSize(this.files[0], 500*1024, 1000, Infinity, 0.9, blob => {
            let body = new FormData();
            body.set('file', blob, blob.name || "file.jpg");
            fetch('/upload-image', {method: 'POST', body}).then(...);
        });
    };
    
  • 0

    对于JPG图像压缩,您可以使用称为JIC(Javascript图像压缩)的最佳压缩技术 . 这肯定会帮助您 - > https://github.com/brunobar79/J-I-C

相关问题