Подтвердить что ты не робот

Как сжать изображение через Javascript в браузере?

TL; DR;

Есть ли способ сжать изображение (в основном jpeg, png и gif) непосредственно на стороне браузера, прежде чем загружать его? Я уверен, что JavaScript может это сделать, но я не могу найти способ его достичь.


Здесь полный сценарий, который я хотел бы реализовать:

  • пользователь переходит на мой сайт и выбирает изображение с помощью элемента input type="file",
  • это изображение извлекается с помощью JavaScript, мы выполняем некоторую проверку, такую ​​как правильный формат файла, максимальный размер файла и т.д.,
  • Если все в порядке, на странице отображается предварительный просмотр изображения,
  • пользователь может выполнять некоторые основные операции, такие как поворот изображения на 90 °/-90 °, обрезать его по заранее определенному соотношению и т.д., или пользователь может загрузить другое изображение и вернуться к шагу 1,
  • когда пользователь удовлетворен, отредактированное изображение затем сжимается и "сохраняется" локально (не сохраняется в файле, а в памяти браузера/странице),
  • пользователь заполняет форму данными, такими как имя, возраст и т.д.
  • пользователь нажимает кнопку "Готово", затем форма, содержащая данные + сжатое изображение, отправляется на сервер (без AJAX),

Полный процесс до последнего шага должен выполняться на стороне клиента и должен быть совместим с последними браузерами Chrome и Firefox, Safari 5+ и IE 8 +. Если возможно, нужно использовать только JavaScript (но я уверен, что это невозможно).

Я ничего не кодировал прямо сейчас, но я уже думал об этом. Чтение файлов локально возможно через API файлов, предварительный просмотр и редактирование изображений можно выполнить с помощью Canvas элемента, но Я не могу найти способ сделать часть сжатия изображения.

В соответствии с html5please.com и caniuse.com поддержка этих браузеров довольно сложная (благодаря IE), но это можно сделать с помощью polyfill, например FlashCanvas и FileReader.

На самом деле, цель заключается в уменьшении размера файла, поэтому я вижу сжатие изображения как решение. Но я знаю, что загруженные изображения будут отображаться на моем сайте каждый раз в одном месте, и я знаю размер этой области отображения (например, 200x400). Таким образом, я мог бы изменить размер изображения в соответствии с этими размерами, тем самым уменьшив размер файла. Я понятия не имею, каков будет коэффициент сжатия для этой техники.

Как вы думаете? У вас есть какие-либо советы, чтобы рассказать мне? Вы знаете какой способ сжатия изображения на стороне браузера в JavaScript? Спасибо за ваши ответы.

4b9b3361

Ответ 1

Короче:

  • Прочитайте файлы с помощью HTML5 FileReader API с .readAsArrayBuffer
  • Создайте e Blob с данными файла и получите его url с window.URL.createObjectURL(blob)
  • Создайте новый элемент изображения и установите его src в файл blob url
  • Отправить изображение на холст. Размер холста установлен на желаемый выходной размер.
  • Получить уменьшенные данные с холста через canvas.toDataURL( "image/jpeg", 0.7) (установить собственный формат и качество вывода)
  • Приложить новые скрытые входы к исходной форме и перенести изображения dataURI в основном как обычный текст
  • На бэкэнд читайте dataURI, декодируйте его с Base64 и сохраните его

Источник: код.

Ответ 2

Ответ на

@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

Я вижу две вещи, отсутствующие в других ответах:

  • canvas.toBlob (если доступно) более эффективен, чем canvas.toDataURL, а также async.
  • файл → image → canvas → преобразование файлов теряет EXIF ​​данные; в частности, данные о вращении изображения, обычно устанавливаемые современными телефонами/планшетами.

Следующий script имеет дело с обеими точками:

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

Ответ 5

Изменить. Согласно комментарию г-на Ме к этому ответу, похоже, что сжатие теперь доступно для форматов JPG/WebP (см. https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL).

Насколько я знаю, вы не можете сжимать изображения с помощью canvas, вместо этого вы можете изменить его размер. Использование canvas.toDataURL не позволит вам выбрать степень сжатия для использования. Вы можете взглянуть на Canimage, который делает именно то, что вы хотите: https://github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js

На самом деле, часто достаточно просто изменить размер изображения, чтобы уменьшить его размер, но если вы хотите пойти дальше, вам придется использовать недавно представленный метод file.readAsArrayBuffer, чтобы получить буфер, содержащий данные изображения.

Затем просто используйте DataView для чтения содержимого в соответствии со спецификацией формата изображения (http://en.wikipedia.org/wiki/JPEG или http://en.wikipedia.org/wiki/Portable_Network_Graphics).

Будет сложно справиться со сжатием данных изображений, но попытка будет хуже. С другой стороны, вы можете попытаться удалить заголовки PNG или exif-данные JPEG, чтобы уменьшить размер изображения. Это должно быть проще.

Вам нужно будет создать еще один DataWiew в другом буфере и заполнить его содержимым отфильтрованного изображения. Затем вам просто нужно кодировать содержимое изображения в DataURI с помощью window.btoa.

Дайте мне знать, если вы реализуете что-то подобное, будет интересно пройтись по коду.

Ответ 6

У меня была проблема с функцией downscaleImage(), опубликованной выше @daniel-allen-langdon в связи с тем, что свойства image.width и image.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;
    }

}

Ответ 7

Для сжатия JPG изображения вы можете использовать наилучшую технологию сжатия JIC (Javascript Image Compression) Это определенно поможет вам → https://github.com/brunobar79/J-I-C

Ответ 8

Я улучшил функцию головы, чтобы быть таким:

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

его использование:

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

наслаждайся;)