Як стиснути зображення через Javascript у браузері?


91

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? Дякую за відповіді.

Відповіді:


164

Коротко:

  • Читайте файли за допомогою API HTML5 FileReader за допомогою .readAsArrayBuffer
  • Створіть BLOB із даними файлу та отримайте його URL за допомогою window.URL.createObjectURL (BLOB)
  • Створіть новий елемент Image і встановіть для нього src значення файлу BLOB-адреси
  • Надішліть зображення на полотно. Розмір полотна встановлений на бажаний вихідний розмір
  • Отримайте зменшені дані назад із полотна через canvas.toDataURL ("image / jpeg", 0,7) (встановіть власний формат виводу та якість)
  • Приєднайте нові приховані входи до оригінальної форми та передайте зображенняURI даних в основному як звичайний текст
  • На бекенді прочитайте dataURI, декодуйте з Base64 та збережіть його

Джерело: код .


1
Дуже дякую ! Це те, що я шукав. Чи знаєте ви, наскільки хороший коефіцієнт стиснення в цій техніці?
помех

2
@NicholasKyriakides Я можу підтвердити, що canvas.toDataURL("image/jpeg",0.7)ефективно стискає його, він зберігає JPEG із якістю 70 (на відміну від стандартної, якість 100).
user1111929

4
@Nicholas Kyriakides, це не є добрим розрізненням. Більшість кодеків не без втрат, тому вони вписуються у ваше визначення "зменшення масштабу" (тобто ви не можете повернутися до 100).
Billybobbonnet

5
Зменшення масштабу стосується створення зображень меншого розміру з точки зору висоти та ширини. Це справді стиснення. Це стиснення з втратами, але, безумовно, стиснення. Це не зменшує масштаб пікселів, він просто підштовхує деякі пікселі до однакових кольорів, так що стиснення може вразити ці кольори меншою кількістю бітів. JPEG в будь-якому випадку вбудував стиснення для пікселів, але в режимі з втратами він говорить, що кілька кольорів можна назвати однаковим кольором. Це все ще стиснення. Зменшення масштабу щодо графіки зазвичай стосується зміни фактичного розміру.
Татарізувати

3
Я просто хочу сказати наступне: файл може перейти прямо, URL.createObjectUrl()не перетворюючи файл у крапку; файл вважається крапкою.
hellol11

20

В інших відповідях я бачу дві речі:

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

ToBlob зробив для мене фокус, створивши файл і отримавши масив $ _FILES на сервері. Дякую!
Рікардо Руїс Ромеро,

Виглядає здорово! Але чи спрацює це у всіх браузерах, Інтернеті та мобільних? (Давайте проігноруємо IE)
Гарвіт Джейн

14

Відповідь @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-адреса даних для зменшеного зображення.


будь ласка, чи можете ви дати мені докладніше про цей приклад, як викликати функцію і як повернуто результат?
visulo

Ось приклад: danielsadventure.info/Html/scaleimage.html Обов’язково прочитайте джерело сторінки, щоб побачити, як це працює.
Річка Вівіан

1
web.archive.org/web/20171226190510/danielsadventure.info/Html/... Для інших людей, які хочуть прочитати посилання, запропоноване @DanielAllenLangdon
Cedric Arnould

У міру збільшення, іноді image.width / height поверне 0, оскільки не завантажився. Можливо, вам доведеться перетворити це на функцію асинхронізації та прослухати image.onload, щоб отримати правильне зображення з і висотою.
Метт Папа

9

Ви можете поглянути на перетворення зображень , спробуйте тут -> демонстраційна сторінка

введіть тут опис зображення


1
Будь ласка, додайте трохи інформації про пов’язані ресурси
SanSolo,

Ця відповідь ідеальна!
djcaesar9114

Посилання на демонстраційну сторінку порушено. Ви можете протестувати тут: demo.wangyulue.com/image-conversion
gabrielstuff

4

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

}

Я хотів би додати пояснення quality, resolutionі imageType(формат цього)
Барабас

3

Редагувати: Відповідно до коментаря 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/JPEG або http://en.wikipedia.org/wiki/Portable_Network_Graphics ).

Важко впоратися зі стисненням даних зображень, але гірше спробувати. З іншого боку, ви можете спробувати видалити заголовки PNG або exif-дані JPEG, щоб зменшити ваше зображення, це має бути простіше зробити.

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

Повідомте мене, якщо ви реалізуєте щось подібне, буде цікаво переглянути код.


Можливо, щось змінилося після того, як ви це опублікували, але другим аргументом цієї згаданої вами функції canvas.toDataURL є величина стиснення, яку ви хочете застосувати.
wp-overwatch.com

2

Я вважаю, що існує простіше рішення порівняно з прийнятою відповіддю.

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

Відповідно до вашого запитання:

Чи є спосіб стиснути зображення (здебільшого jpeg, png та gif) безпосередньо на стороні браузера, перш ніж завантажувати його

Моє рішення:

  1. Створіть крапку з файлом безпосередньо за допомогою URL.createObjectURL(inputFileElement.files[0]).

  2. Те саме, що прийнята відповідь.

  3. Те саме, що прийнята відповідь. Варто згадати, що розмір полотна необхідний і для використання, img.widthі img.heightдля встановлення canvas.widthта canvas.height. Ні img.clientWidth.

  4. Отримайте зменшене зображення за canvas.toBlob(callbackfunction(blob){}, 'image/jpeg', 0.5). Налаштування 'image/jpg'не впливає. image/pngтакож підтримується. Зробіть новий Fileпредмет всередині callbackfunction тіла за допомогою let compressedImageBlob = new File([blob]).

  5. Додайте нові приховані вхідні дані або надішліть через javascript . Сервер не повинен нічого декодувати.

Перевірте https://javascript.info/binary, щоб отримати всю інформацію. Я придумав рішення, прочитавши цю главу.


Код:

    <!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>

Цей код виглядає набагато менш страшним, ніж інші відповіді ..

Оновлення:

Потрібно все вкласти всередину img.onload. Інакше canvasне вдасться правильно визначити ширину та висоту зображення, як призначено час canvas.

    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 .pngтест на стиснення файлу з image/jpegнабором аргументів.

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


0

я вдосконалив функцію head, щоб вона була такою:

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

насолоджуватися;)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.