Como comprimir uma imagem via Javascript no navegador?

94

TL; DR;

Existe uma maneira de compactar uma imagem (principalmente jpeg, png e gif) diretamente do lado do navegador, antes de enviá-la? Tenho certeza de que o JavaScript pode fazer isso, mas não consigo encontrar uma maneira de fazer isso.


Aqui está o cenário completo que eu gostaria de implementar:

  • o usuário vai ao meu site e escolhe uma imagem por meio de um input type="file"elemento,
  • esta imagem é recuperada via JavaScript, fazemos algumas verificações, como formato de arquivo correto, tamanho máximo do arquivo, etc.
  • se tudo estiver OK, uma prévia da imagem é exibida na página,
  • o usuário pode fazer algumas operações básicas, como girar a imagem em 90 ° / -90 °, recortá-la seguindo uma proporção pré-definida, etc, ou o usuário pode carregar outra imagem e retornar à etapa 1,
  • quando o usuário está satisfeito, a imagem editada é então compactada e "salva" localmente (não salva em um arquivo, mas na memória / página do navegador), -
  • o usuário preenche um formulário com dados como nome, idade etc,
  • o usuário clica no botão "Terminar", então o formulário contendo dados + imagem compactada é enviado para o servidor (sem AJAX),

O processo completo até a última etapa deve ser feito do lado do cliente e deve ser compatível com o Chrome e Firefox mais recentes, Safari 5+ e IE 8+ . Se possível, apenas JavaScript deve ser usado (mas tenho certeza de que isso não é possível).

Não codifiquei nada agora, mas já pensei sobre isso. A leitura de arquivos localmente é possível através da API File , a visualização e edição da imagem podem ser feitas usando o elemento Canvas , mas não consigo encontrar uma maneira de fazer a parte de compressão da imagem .

De acordo com html5please.com e caniuse.com , suportar esses navegadores é bastante difícil (graças ao IE), mas pode ser feito usando polyfill como FlashCanvas e FileReader .

Na verdade, o objetivo é reduzir o tamanho do arquivo, então vejo a compactação de imagens como uma solução. Mas, eu sei que as imagens carregadas serão exibidas no meu site, sempre no mesmo lugar, e eu sei a dimensão dessa área de exibição (por exemplo, 200x400). Então, eu poderia redimensionar a imagem para caber nessas dimensões, reduzindo assim o tamanho do arquivo. Não tenho ideia de qual seria a taxa de compressão para essa técnica.

O que você acha ? Você tem algum conselho para me dizer? Você conhece alguma maneira de compactar uma imagem do lado do navegador em JavaScript? Obrigado por suas respostas.

pomeh
fonte

Respostas:

170

Em resumo:

  • Leia os arquivos usando a API FileReader HTML5 com .readAsArrayBuffer
  • Crie um Blob com os dados do arquivo e obtenha seu url com window.URL.createObjectURL (blob)
  • Crie um novo elemento de imagem e defina seu src para o URL do blob do arquivo
  • Envie a imagem para a tela. O tamanho da tela está definido para o tamanho de saída desejado
  • Obtenha os dados reduzidos de volta do canvas via canvas.toDataURL ("image / jpeg", 0.7) (defina seu próprio formato de saída e qualidade)
  • Anexe novas entradas ocultas ao formulário original e transfira as imagens dataURI basicamente como texto normal
  • No back-end, leia o dataURI, decodifique de Base64 e salve-o

Fonte: código .

psicológico
fonte
1
Muito obrigado ! Isso é o que eu estava procurando. Você sabe o quão boa é a taxa de compressão com esta técnica?
pomeh
2
@NicholasKyriakides Posso confirmar que canvas.toDataURL("image/jpeg",0.7)efetivamente o comprime, ele salva JPEG com qualidade 70 (ao contrário do padrão, qualidade 100).
user1111929
4
@Nicholas Kyriakides, essa não é uma boa distinção a se fazer. A maioria dos codecs não é sem perdas, então eles se encaixariam em sua definição de "redução de escala" (ou seja, você não pode reverter para 100).
Billybobbonnet
5
Redução de escala refere-se a fazer imagens de um tamanho menor em termos de altura e largura. Isso realmente é compressão. É uma compressão com perdas, mas certamente uma compressão. Não está diminuindo a escala dos pixels, apenas altera alguns pixels para que tenham a mesma cor, de modo que a compressão pode atingir essas cores em menos bits. De qualquer maneira, o JPEG tem compactação embutida para os pixels, mas no modo com perdas diz que algumas cores fora podem ser chamadas da mesma cor. Isso ainda é compressão. Reduzir a escala em relação aos gráficos normalmente se refere a uma mudança no tamanho real.
Tatarize em
3
Só quero dizer o seguinte: o arquivo pode ir direto, URL.createObjectUrl()sem transformá-lo em um blob; o arquivo conta como um blob.
hellol11
20

Vejo duas coisas faltando nas outras respostas:

  • canvas.toBlob(quando disponível) tem mais desempenho do que canvas.toDataURL, e também assíncrono.
  • o arquivo -> imagem -> tela -> conversão de arquivo perde dados EXIF; em particular, dados sobre rotação de imagem comumente definidos por telefones / tablets modernos.

O script a seguir trata de ambos os pontos:

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

Exemplo de uso:

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(...);
    });
};
Simon Lindholm
fonte
ToBlob fez o truque para mim, criando um arquivo e recebendo no array $ _FILES no servidor. Obrigado!
Ricardo Ruiz Romero
Parece ótimo! Mas isso funcionará em todos os navegadores, web e celular? (Vamos ignorar o IE)
Garvit Jain
13

A resposta de @PsychoWoods é boa. Eu gostaria de oferecer minha própria solução. Esta função Javascript pega um URL de dados de imagem e uma largura, dimensiona para a nova largura e retorna um novo URL de dados.

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

Esse código pode ser usado em qualquer lugar em que você tenha um URL de dados e queira um URL de dados para uma imagem em escala reduzida.

Rio Vivian
fonte
por favor, você pode me dar mais detalhes sobre este exemplo, como chamar a função e como retornou o resultado?
visulo
Aqui está um exemplo: danielsadventure.info/Html/scaleimage.html Certifique-se de ler o código-fonte da página para ver como funciona.
Vivian River
1
web.archive.org/web/20171226190510/danielsadventure.info/Html/… Para outras pessoas que desejam ler o link proposto por @DanielAllenLangdon
Cedric Arnould
Como um alerta, às vezes image.width / height retornará 0, pois não foi carregado. Pode ser necessário converter isso em uma função assíncrona e ouvir image.onload para obter a imagem correta com e altura.
Matt Pope de
4

Tive um problema com a downscaleImage()função postada acima por @ daniel-allen-langdon em que as propriedades image.widthe image.heightnão estão disponíveis imediatamente porque o carregamento da imagem é assíncrono .

Consulte o exemplo TypeScript atualizado abaixo que leva isso em consideração, usa asyncfunções e redimensiona a imagem com base na dimensão mais longa, em vez de apenas na largura

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

}
Russell Briggs
fonte
Eu adicionaria a explicação de quality, resolutione imageType(formato disso)
Barabas
3

Editar: de acordo com o comentário do Sr. Me sobre esta resposta, parece que a compactação agora está disponível para formatos JPG / WebP (consulte https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL ) .

Até onde eu sei, você não pode compactar imagens usando canvas, em vez disso, você pode redimensioná-las. Usar canvas.toDataURL não permitirá que você escolha a taxa de compactação a ser usada. Você pode dar uma olhada em canimage que faz exatamente o que você deseja: https://github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js

Na verdade, geralmente é suficiente apenas redimensionar a imagem para diminuir seu tamanho, mas se você quiser ir além, terá que usar o método file.readAsArrayBuffer recém-introduzido para obter um buffer contendo os dados da imagem.

Em seguida, basta usar um DataView para ler seu conteúdo de acordo com a especificação do formato de imagem ( http://en.wikipedia.org/wiki/JPEG ou http://en.wikipedia.org/wiki/Portable_Network_Graphics ).

Será difícil lidar com a compactação de dados de imagem, mas é uma tentativa pior. Por outro lado, você pode tentar excluir os cabeçalhos PNG ou os dados JPEG exif para tornar a imagem menor; deve ser mais fácil fazer isso.

Você terá que criar outro DataWiew em outro buffer e preenchê-lo com o conteúdo da imagem filtrada. Em seguida, você apenas terá que codificar o conteúdo da imagem para DataURI usando window.btoa.

Deixe-me saber se você implementar algo semelhante, será interessante percorrer o código.

nfroidure
fonte
Talvez algo tenha mudado desde que você postou isso, mas o segundo argumento para a função canvas.toDataURL que você mencionou é a quantidade de compactação que você deseja aplicar.
wp-overwatch.com
2

Acho que há uma solução mais simples em comparação com a resposta aceita.

  • Leia os arquivos usando a API FileReader HTML5 com .readAsArrayBuffer
  • Crie um Blob com os dados do arquivo e obtenha seu url com window.URL.createObjectURL(blob)
  • Crie um novo elemento de imagem e defina seu src para o URL do blob do arquivo
  • Envie a imagem para a tela. O tamanho da tela está definido para o tamanho de saída desejado
  • Obtenha os dados reduzidos de volta do canvas via canvas.toDataURL("image/jpeg",0.7)(defina seu próprio formato de saída e qualidade)
  • Anexe novas entradas ocultas ao formulário original e transfira as imagens dataURI basicamente como texto normal
  • No back-end, leia o dataURI, decodifique de Base64 e salve-o

De acordo com sua pergunta:

Existe uma maneira de compactar uma imagem (principalmente jpeg, png e gif) diretamente do lado do navegador, antes de enviá-la

Minha solução:

  1. Crie um blob com o arquivo diretamente com URL.createObjectURL(inputFileElement.files[0]).

  2. Igual à resposta aceita.

  3. Igual à resposta aceita. Vale a pena mencionar que, o tamanho da tela é necessário e use img.widthe img.heightpara definir canvas.widthe canvas.height. Não img.clientWidth.

  4. Obtenha a imagem em escala reduzida por canvas.toBlob(callbackfunction(blob){}, 'image/jpeg', 0.5). A configuração 'image/jpg'não tem efeito. image/pngtambém é suportado. Faça um novo Fileobjeto dentro do callbackfunction corpo com let compressedImageBlob = new File([blob]).

  5. Adicione novas entradas ocultas ou envie via javascript . O servidor não precisa decodificar nada.

Verifique https://javascript.info/binary para todas as informações. Eu descobri a solução depois de ler este capítulo.


Código:

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

Este código parece muito menos assustador do que as outras respostas.

Atualizar:

É preciso colocar tudo dentro img.onload. Caso contrário canvas, não será possível obter a largura e a altura da imagem corretamente conforme o tempo canvasé atribuído.

    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 .pngteste de compressão de arquivo com image/jpegconjunto de argumentos.

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

Para compactação de imagens JPG, você pode usar a melhor técnica de compactação chamada JIC (Javascript Image Compression). Isso certamente ajudará você -> https://github.com/brunobar79/JIC

a vida é Bela
fonte
1
sem reduzir a qualidade
bonita
0

Eu melhorei a função de cabeça para ser esta:

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

o uso dele:

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

apreciar ;)

abdurhman Nughmshi
fonte