Làm cách nào để nén hình ảnh qua Javascript trong trình duyệt?


91

TL; DR;

Có cách nào để nén hình ảnh (chủ yếu là jpeg, png và gif) trực tiếp phía trình duyệt trước khi tải lên không? Tôi khá chắc rằng JavaScript có thể làm được điều này, nhưng tôi không thể tìm ra cách để đạt được nó.


Đây là toàn bộ kịch bản mà tôi muốn thực hiện:

  • người dùng truy cập trang web của tôi và chọn một hình ảnh thông qua một input type="file"phần tử,
  • hình ảnh này được truy xuất qua JavaScript, chúng tôi thực hiện một số xác minh chẳng hạn như định dạng tệp chính xác, kích thước tệp tối đa, v.v.,
  • nếu mọi thứ đều ổn, bản xem trước của hình ảnh sẽ được hiển thị trên trang,
  • người dùng có thể thực hiện một số thao tác cơ bản như xoay hình ảnh 90 ° / -90 °, cắt hình ảnh theo tỷ lệ được xác định trước, v.v. hoặc người dùng có thể tải lên một hình ảnh khác và quay lại bước 1,
  • Khi người dùng hài lòng, hình ảnh đã chỉnh sửa sau đó sẽ được nén và "lưu" cục bộ (không được lưu vào tệp, mà trong bộ nhớ / trang của trình duyệt), -
  • người dùng điền vào biểu mẫu với dữ liệu như tên, tuổi, v.v.,
  • người dùng nhấp vào nút "Hoàn tất", sau đó biểu mẫu chứa dữ liệu + hình ảnh nén được gửi đến máy chủ (không có AJAX),

Toàn bộ quy trình cho đến bước cuối cùng phải được thực hiện ở phía máy khách và phải tương thích trên Chrome và Firefox, Safari 5+ và IE 8+ mới nhất . Nếu có thể, chỉ nên sử dụng JavaScript (nhưng tôi khá chắc chắn rằng điều này là không thể).

Tôi chưa viết bất cứ thứ gì ngay bây giờ, nhưng tôi đã nghĩ về nó rồi. Có thể đọc tệp cục bộ thông qua API tệp , xem trước và chỉnh sửa hình ảnh có thể được thực hiện bằng cách sử dụng phần tử Canvas , nhưng tôi không thể tìm thấy cách thực hiện phần nén hình ảnh .

Theo html5please.comcaniuse.com , việc hỗ trợ những trình duyệt này khá khó khăn (nhờ IE), nhưng có thể được thực hiện bằng cách sử dụng polyfill như FlashCanvasFileReader .

Thực ra, mục đích là giảm dung lượng tệp, vì vậy tôi xem nén ảnh là một giải pháp. Nhưng, tôi biết rằng hình ảnh đã tải lên sẽ được hiển thị trên trang web của tôi, mọi lúc ở cùng một vị trí và tôi biết kích thước của vùng hiển thị này (ví dụ: 200x400). Vì vậy, tôi có thể thay đổi kích thước hình ảnh để phù hợp với các kích thước đó, do đó giảm kích thước tệp. Tôi không biết tỷ lệ nén cho kỹ thuật này sẽ như thế nào.

Bạn nghĩ sao ? Bạn có lời khuyên nào nói cho tôi biết không? Bạn có biết cách nào để nén hình ảnh phía trình duyệt trong JavaScript không? Cảm ơn cho câu trả lời của bạn.

Câu trả lời:


164

Nói ngắn gọn:

  • Đọc tệp bằng API HTML5 FileReader với .readAsArrayBuffer
  • Tạo một Blob với dữ liệu tệp và lấy url của nó bằng window.URL.createObjectURL (blob)
  • Tạo phần tử Hình ảnh mới và đặt nó là src thành url tệp blob
  • Gửi hình ảnh vào canvas. Kích thước canvas được đặt thành kích thước đầu ra mong muốn
  • Lấy lại dữ liệu thu nhỏ từ canvas thông qua canvas.toDataURL ("image / jpeg", 0,7) (đặt định dạng và chất lượng đầu ra của riêng bạn)
  • Đính kèm các đầu vào ẩn mới vào biểu mẫu ban đầu và chuyển các hình ảnh dataURI về cơ bản như văn bản bình thường
  • Trên phụ trợ, đọc dataURI, giải mã từ Base64 và lưu nó

Nguồn: .


1
Cảm ơn rất nhiều ! Đây là những gì tôi đang tìm kiếm. Bạn có biết tỷ lệ nén tốt như thế nào với kỹ thuật này không?
pomeh

2
@NicholasKyriakides Tôi có thể xác nhận rằng canvas.toDataURL("image/jpeg",0.7)nén hiệu quả, nó tiết kiệm JPEG với chất lượng 70 (trái ngược với mặc định, chất lượng 100).
user1111929

4
@Nicholas Kyriakides, đây không phải là sự phân biệt tốt. Hầu hết các codec không phải là không mất dữ liệu, vì vậy chúng sẽ phù hợp với định nghĩa "giảm tỷ lệ" của bạn (tức là bạn không thể hoàn nguyên về 100).
Billybobbonnet

5
Hạ cấp đề cập đến việc làm cho hình ảnh có kích thước nhỏ hơn về chiều cao và chiều rộng. Đây thực sự là nén. Đó là nén mất mát nhưng chắc chắn là nén. Nó không phải là giảm tỷ lệ pixel, nó chỉ di chuyển một số pixel thành cùng màu để nén có thể đánh những màu đó với ít bit hơn. JPEG dù sao cũng đã tích hợp tính năng nén cho các pixel, nhưng ở chế độ bị mất, nó nói rằng một vài màu bị tắt có thể được gọi là cùng một màu. Đó vẫn là nén. Hạ cấp liên quan đến đồ họa thường đề cập đến sự thay đổi về kích thước thực tế.
Tatarize

3
Tôi chỉ muốn nói điều này: tệp có thể đi thẳng đến URL.createObjectUrl()mà không cần biến tệp thành một đốm màu; tệp được tính là một đốm màu.
hellol

20

Tôi thấy hai điều còn thiếu trong các câu trả lời khác:

  • canvas.toBlob(khi có sẵn) hoạt động hiệu quả hơn canvas.toDataURLvà cũng không đồng bộ.
  • tệp -> hình ảnh -> canvas -> chuyển đổi tệp làm mất dữ liệu EXIF; đặc biệt là dữ liệu về xoay hình ảnh thường được thiết lập bởi điện thoại / máy tính bảng hiện đại.

Tập lệnh sau giải quyết cả hai điểm:

// 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);
}

Ví dụ sử dụng:

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(...);
    });
};

ToBlob đã thực hiện thủ thuật cho tôi, tạo một tệp và nhận trong mảng $ _FILES trên máy chủ. Cảm ơn bạn!
Ricardo Ruiz Romero

Trông rất tuyệt! Nhưng điều này sẽ hoạt động trên tất cả các trình duyệt, web và thiết bị di động? (Hãy bỏ qua IE)
Garvit Jain

14

Câu trả lời của @PsychoWoods là tốt. Tôi muốn đưa ra giải pháp của riêng tôi. Hàm Javascript này lấy một URL dữ liệu hình ảnh và chiều rộng, chia tỷ lệ nó thành chiều rộng mới và trả về một URL dữ liệu mới.

// 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;
}

Mã này có thể được sử dụng ở bất kỳ nơi nào bạn có URL dữ liệu và muốn có URL dữ liệu cho hình ảnh được giảm tỷ lệ.


Làm ơn, bạn có thể cho tôi biết chi tiết hơn về ví dụ này, cách gọi hàm và kết quả trả về như thế nào?
visulo

Đây là một ví dụ: danielsadventure.info/Html/scaleimage.html Hãy nhớ đọc nguồn của trang để xem nó hoạt động như thế nào.
Vivian River

1
web.archive.org/web/20171226190510/danielsadventure.info/Html/... Đối với những người khác, những người muốn đọc vào liên kết đề xuất bởi @DanielAllenLangdon
Cedric ARNOULD

Lưu ý, đôi khi image.width / height sẽ trả về 0 vì nó chưa được tải. Bạn có thể cần chuyển đổi nó thành một hàm không đồng bộ và nghe image.onload để có được hình ảnh chính xác với chiều cao và hình ảnh.
Matt Pope


4

Tôi đã gặp sự cố với downscaleImage()hàm do @ daniel-allen-langdon đăng ở trên, trong đó các thuộc tính image.widthimage.heightkhông khả dụng ngay lập tức vì tải hình ảnh không đồng bộ .

Vui lòng xem ví dụ về TypeScript được cập nhật bên dưới có tính đến điều này, sử dụng các asyncchức năng và thay đổi kích thước hình ảnh dựa trên kích thước dài nhất thay vì chỉ chiều rộng

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;
    }

}

Tôi sẽ thêm lời giải thích về quality, resolutionimageType(định dạng của điều này)
Barabas

3

Chỉnh sửa: Theo nhận xét của Mr Me về câu trả lời này, có vẻ như tính năng nén hiện có sẵn cho các định dạng JPG / WebP (xem https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL ) .

Theo như tôi biết, bạn không thể nén hình ảnh bằng canvas, thay vào đó, bạn có thể thay đổi kích thước của nó. Sử dụng canvas.toDataURL sẽ không cho phép bạn chọn tỷ lệ nén để sử dụng. Bạn có thể xem canimage thực hiện chính xác những gì bạn muốn: https://github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js

Trên thực tế, chỉ cần thay đổi kích thước hình ảnh để giảm kích thước là đủ nhưng nếu bạn muốn đi xa hơn, bạn sẽ phải sử dụng phương thức mới được giới thiệu file.readAsArrayBuffer để lấy bộ đệm chứa dữ liệu hình ảnh.

Sau đó, chỉ cần sử dụng DataView để đọc nội dung của nó theo đặc điểm định dạng hình ảnh ( http://en.wikipedia.org/wiki/JPEG hoặc http://en.wikipedia.org/wiki/Portable_Network_Graphics ).

Sẽ khó giải quyết vấn đề nén dữ liệu hình ảnh, nhưng còn tệ hơn nếu bạn thử. Mặt khác, bạn có thể cố gắng xóa tiêu đề PNG hoặc dữ liệu JPEG exif để làm cho hình ảnh của bạn nhỏ hơn, việc này sẽ dễ dàng hơn.

Bạn sẽ phải tạo một DataWiew khác trên bộ đệm khác và điền vào nó với nội dung hình ảnh đã lọc. Sau đó, bạn sẽ chỉ phải mã hóa nội dung hình ảnh của mình thành DataURI bằng window.btoa.

Hãy cho tôi biết nếu bạn triển khai điều gì đó tương tự, sẽ rất thú vị khi xem qua mã.


Có lẽ điều gì đó đã thay đổi kể từ khi bạn đăng bài này, nhưng đối số thứ hai cho hàm canvas.toDataURL mà bạn đề cập là số lượng nén bạn muốn áp dụng.
wp-overwatch.com

2

Tôi thấy rằng có giải pháp đơn giản hơn so với câu trả lời được chấp nhận.

  • Đọc tệp bằng API HTML5 FileReader với .readAsArrayBuffer
  • Tạo Blob với dữ liệu tệp và lấy url của nó bằng window.URL.createObjectURL(blob)
  • Tạo phần tử Hình ảnh mới và đặt nó là src thành url tệp blob
  • Gửi hình ảnh vào canvas. Kích thước canvas được đặt thành kích thước đầu ra mong muốn
  • Lấy lại dữ liệu thu nhỏ từ canvas thông qua canvas.toDataURL("image/jpeg",0.7)(đặt chất lượng và định dạng đầu ra của riêng bạn)
  • Đính kèm các đầu vào ẩn mới vào biểu mẫu ban đầu và chuyển các hình ảnh dataURI về cơ bản như văn bản bình thường
  • Trên phụ trợ, đọc dataURI, giải mã từ Base64 và lưu nó

Theo câu hỏi của bạn:

Có cách nào để nén hình ảnh (chủ yếu là jpeg, png và gif) trực tiếp phía trình duyệt trước khi tải lên không

Giải pháp của tôi:

  1. Tạo một đốm màu với tệp trực tiếp với URL.createObjectURL(inputFileElement.files[0]).

  2. Giống như câu trả lời được chấp nhận.

  3. Giống như câu trả lời được chấp nhận. Đáng nói rằng, kích thước canvas là cần thiết và sử dụng img.widthimg.heightđặt canvas.widthcanvas.height. Không img.clientWidth.

  4. Nhận hình ảnh thu nhỏ theo canvas.toBlob(callbackfunction(blob){}, 'image/jpeg', 0.5). Cài đặt 'image/jpg'không có hiệu lực. image/pngcũng được hỗ trợ. Tạo một Fileđối tượng mới bên trong callbackfunction cơ thể với let compressedImageBlob = new File([blob]).

  5. Thêm đầu vào ẩn mới hoặc gửi qua javascript . Máy chủ không phải giải mã bất cứ thứ gì.

Kiểm tra https://javascript.info/binary để biết tất cả thông tin. Tôi đã đưa ra giải pháp sau khi đọc chương này.


Mã:

    <!DOCTYPE html>
    <html>
    <body>
    <form action="upload.php" method="post" enctype="multipart/form-data">
      Select image to upload:
      <input type="file" name="fileToUpload" id="fileToUpload" multiple>
      <input type="submit" value="Upload Image" name="submit">
    </form>
    </body>
    </html>

Mã này trông ít đáng sợ hơn nhiều so với các câu trả lời khác ..

Cập nhật:

Người ta phải đặt mọi thứ vào bên trong img.onload. Nếu không canvassẽ không thể lấy chiều rộng và chiều cao của hình ảnh một cách chính xác như thời gian canvasđược ấn định.

    function upload(){
        var f = fileToUpload.files[0];
        var fileName = f.name.split('.')[0];
        var img = new Image();
        img.src = URL.createObjectURL(f);
        img.onload = function(){
            var canvas = document.createElement('canvas');
            canvas.width = img.width;
            canvas.height = img.height;
            var ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0);
            canvas.toBlob(function(blob){
                    console.info(blob.size);
                    var f2 = new File([blob], fileName + ".jpeg");
                    var xhr = new XMLHttpRequest();
                    var form = new FormData();
                    form.append("fileToUpload", f2);
                    xhr.open("POST", "upload.php");
                    xhr.send(form);
            }, 'image/jpeg', 0.5);
        }
    }

3.4MB .pngkiểm tra nén tệp với image/jpegtập đối số.

    |0.9| 777KB |
    |0.8| 383KB |
    |0.7| 301KB |
    |0.6| 251KB |
    |0.5| 219kB |


0

tôi đã cải thiện chức năng một đầu để trở thành:

var minifyImg = function(dataUrl,newWidth,imageType="image/jpeg",resolve,imageArguments=0.7){
    var image, oldWidth, oldHeight, newHeight, canvas, ctx, newDataUrl;
    (new Promise(function(resolve){
      image = new Image(); image.src = dataUrl;
      log(image);
      resolve('Done : ');
    })).then((d)=>{
      oldWidth = image.width; oldHeight = image.height;
      log([oldWidth,oldHeight]);
      newHeight = Math.floor(oldHeight / oldWidth * newWidth);
      log(d+' '+newHeight);

      canvas = document.createElement("canvas");
      canvas.width = newWidth; canvas.height = newHeight;
      log(canvas);
      ctx = canvas.getContext("2d");
      ctx.drawImage(image, 0, 0, newWidth, newHeight);
      //log(ctx);
      newDataUrl = canvas.toDataURL(imageType, imageArguments);
      resolve(newDataUrl);
    });
  };

việc sử dụng nó:

minifyImg(<--DATAURL_HERE-->,<--new width-->,<--type like image/jpeg-->,(data)=>{
   console.log(data); // the new DATAURL
});

thưởng thức ;)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.