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

TL;DR;

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


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

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

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

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

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

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

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

15 ответов

Решение

Короче:

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

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

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

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

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

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

Попробуйте этот пример настраиваемого чистого JS - сжатие более 90%:

         <div id="root">
        <p>Upload an image and see the result</p>
        <input id="img-input" type="file" accept="image/*" style="display:block" />
    </div>

    <script>
        const MAX_WIDTH = 320;
        const MAX_HEIGHT = 180;
        const MIME_TYPE = "image/jpeg";
        const QUALITY = 0.7;

        const input = document.getElementById("img-input");
        input.onchange = function (ev) {
            const file = ev.target.files[0]; // get the file
            const blobURL = URL.createObjectURL(file);
            const img = new Image();
            img.src = blobURL;
            img.onerror = function () {
                URL.revokeObjectURL(this.src);
                // Handle the failure properly
                console.log("Cannot load image");
            };
            img.onload = function () {
                URL.revokeObjectURL(this.src);
                const [newWidth, newHeight] = calculateSize(img, MAX_WIDTH, MAX_HEIGHT);
                const canvas = document.createElement("canvas");
                canvas.width = newWidth;
                canvas.height = newHeight;
                const ctx = canvas.getContext("2d");
                ctx.drawImage(img, 0, 0, newWidth, newHeight);
                canvas.toBlob(
                    (blob) => {
                        // Handle the compressed image. es. upload or save in local state
                        displayInfo('Original file', file);
                        displayInfo('Compressed file', blob);
                    },
                    MIME_TYPE,
                    QUALITY
                );
                document.getElementById("root").append(canvas);
            };
        };

        function calculateSize(img, maxWidth, maxHeight) {
            let width = img.width;
            let height = img.height;

            // calculate the width and height, constraining the proportions
            if (width > height) {
                if (width > maxWidth) {
                    height = Math.round((height * maxWidth) / width);
                    width = maxWidth;
                }
            } else {
                if (height > maxHeight) {
                    width = Math.round((width * maxHeight) / height);
                    height = maxHeight;
                }
            }
            return [width, height];
        }

        // Utility functions for demo purpose

        function displayInfo(label, file) {
            const p = document.createElement('p');
            p.innerText = `${label} - ${readableBytes(file.size)}`;
            document.getElementById('root').append(p);
        }

        function readableBytes(bytes) {
            const i = Math.floor(Math.log(bytes) / Math.log(1024)),
                sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];

            return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
        }
    </script>

Я считаю, что есть более простое решение по сравнению с принятым ответом.

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

Для использования браузера ModerncreateImageBitmap()вместоimg.onload

У меня была проблема с 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;
    }

}

Изменить: Согласно комментарию Mr Me к этому ответу, похоже, что сжатие теперь доступно для форматов 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.

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

Я использовал следующий пакет: https://www.npmjs.com/package/browser-image-compression .

      npm install browser-image-compression
or
yarn add browser-image-compression

Затем просто следуйте документам:

      import imageCompression from 'browser-image-compression';
const options = {
 maxSizeMB: 0.5, // pretty much self-explanatory
 maxWidthOrHeight: 500, // apparently px
}

imageCompression(file, options)
            .then(function(compressedFile) {
                console.log(
                    "compressedFile instanceof Blob",
                    compressedFile instanceof Blob
                ); // true
                console.log(
                    `compressedFile size ${compressedFile.size /
                        1024 /
                        1024} MB`
                ); // smaller than maxSizeMB

                return uploader(compressedFile); // code to actual upload, in my case uploader() is a function to upload to Firebase storage.
            })

На всякий случай, если вам было любопытно узнать о uploader(), вот его код:

      import { initializeApp } from "firebase/app";

const firebaseConfig = {
    // your config
};

initializeApp(firebaseConfig);
import { getStorage, ref, uploadBytes, getDownloadURL } from "firebase/storage";
const storage = getStorage();
const sRef = ref(storage);

const uploader = async (file) => {
        /* uploads to root */

        // const imageRef = ref(sRef, file.name);
        // console.log(imageRef);
        // await uploadBytes(imageRef, file).then((snapshot) => {
        //  console.log("Uploaded a blob or file!", snapshot);
        // });

        /* upload to folder 'techs/' */
        const folderRef = ref(sRef, "techs/" + file.name);
        await uploadBytes(folderRef, file);

        // get URL
        const url = await getDownloadURL(ref(storage, folderRef));

        console.log("url: ", url);
        return url;
    };

Вы можете сжать изображение с помощью HTML <canvas>элемент:

Подробное объяснение см. в этом сообщении в блоге: https://img.ly/blog/how-to-compress-an-image-before-uploading-it-in-javascript/

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

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

наслаждаться;)

Когда изображение поступает с ввода и вам нужна строка base64, вы можете попробовать это:

      async function compressImage(input, maxWidth, maxHeight, quality) {
  const file = input.files[0];
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = event => {
      const img = new Image();
      img.src = event.target.result;
      img.onload = () => {
        let width = img.width;
        let height = img.height;

        if (width > height) {
          if (width > maxWidth) {
            height *= maxWidth / width;
            width = maxWidth;
          }
        } else {
          if (height > maxHeight) {
            width *= maxHeight / height;
            height = maxHeight;
          }
        }

        const canvas = document.createElement('canvas');
        canvas.width = width;
        canvas.height = height;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, width, height);
        const base64String = canvas.toDataURL('image/jpeg', quality);
        resolve(base64String);
      };
    };
    reader.onerror = error => reject(error);
  });
}

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

Другие вопросы по тегам