Como comprimir uma imagem via Javascript no navegador?

TL; DR;

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

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

  • o usuário acessa meu site e escolhe uma imagem por meio de um elemento input type="file" ,
  • essa imagem é recuperada via JavaScript, fazemos algumas verificações, como formato de arquivo correto, tamanho máximo de arquivo, etc.
  • se tudo estiver OK, uma pré-visualização da imagem é exibida na página,
  • o usuário pode fazer algumas operações básicas, como girar a imagem em 90 ° / -90 °, cortá-la seguindo uma taxa pré-definida, etc, ou o usuário pode fazer upload de outra imagem e voltar ao passo 1,
  • quando o usuário está satisfeito, a imagem editada é então compactada e “salva” localmente (não salva em um arquivo, mas na memory / página do navegador), –
  • o usuário preenche um formulário com dados como nome, idade etc,
  • o usuário clica no botão “Finish”, então o formulário contendo datas + imagem comprimida é enviado para o servidor (sem AJAX),

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

Eu não codifiquei nada agora, mas já pensei sobre isso. A leitura de arquivos localmente é possível por meio da API de arquivos , a visualização de imagens e a edição podem ser feitas usando o elemento Canvas , mas não consigo encontrar uma maneira de fazer a parte de compactação de imagens .

De acordo com o html5please.com e o caniuse.com , o suporte a esse navegador é bastante difícil (graças ao IE), mas pode ser feito usando o polyfill, como o FlashCanvas e o FileReader .

Na verdade, o objective é reduzir o tamanho do arquivo, portanto, vejo a compactação de imagens como uma solução. Mas, eu sei que as imagens enviadas serão exibidas no meu site, todas as vezes no mesmo local, e eu sei a dimensão dessa área de exibição (por exemplo, 200×400). Assim, eu poderia resize a imagem para se ajustar a essas dimensões, reduzindo assim o tamanho do arquivo. Eu não tenho idéia do que seria a taxa de compression para esta 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.

    Em resumo:

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

    Fonte: código

    A resposta da @PsychoWoods é boa. Eu gostaria de oferecer minha própria solução. Essa function JavaScript usa um URL de dados de imagem e uma largura, dimensiona 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 com redução de escala.

    Eu vejo duas coisas faltando nas outras respostas:

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

    O script a seguir lida com os dois 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(...); }); }; 

    Edit: De acordo com o Mr Me comentário sobre esta resposta, parece que a compression está agora disponível para os formatos JPG / WebP (ver https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL ) .

    Tanto quanto eu sei, você não pode comprimir imagens usando a canvas, em vez disso, você pode redimensioná-lo. O uso de canvas.toDataURL não permite escolher a taxa de compactação a ser usada. Você pode dar uma olhada no canimage que faz exatamente o que você quer: https://github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js

    Na verdade, muitas vezes basta resize a imagem para diminuir seu tamanho, mas se você quiser ir mais longe, precisará 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 da imagem ( http://en.wikipedia.org/wiki/JPEG ou http://en.wikipedia.org/wiki/Portable_Network_Graphics ).

    Vai ser difícil lidar com a compression de dados de imagem, mas é pior tentar. Por outro lado, você pode tentar excluir os headers PNG ou os dados exif JPEG para tornar sua imagem menor, deve ser mais fácil fazê-lo.

    Você terá que criar outro DataWiew em outro buffer e preenchê-lo com o conteúdo da imagem filtrada. Então, você só precisa codificar seu conteúdo de imagem para o DataURI usando window.btoa.

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

    Eu tive um problema com a function downscaleImage() postada acima por @ daniel-allen-langdon em que as propriedades image.height e image.height não estão disponíveis imediatamente porque o carregamento da imagem é asynchronous .

    Por favor, veja o exemplo de TypeScript atualizado abaixo que leva isso em conta, usa funções async e redimensiona a imagem com base na dimensão mais longa em vez de apenas na largura

     function getImage(dataUrl: string): Promise { 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, // eg 'image/jpeg' resolution: number, // max width/height in pixels quality: number // eg 0.9 = 90% quality ): Promise { // 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; } } 

    Para compression de imagens JPG, você pode usar a melhor técnica de compactação chamada JIC (JavaScript Image Compression). Isso definitivamente irá ajudá-lo -> https://github.com/brunobar79/JIC