HTML5 Pré-resize imagens antes de fazer o upload

Aqui está um scratcher de macarrão.

Tendo em mente que temos armazenamento local HTML5 e xhr v2 e quais não. Eu queria saber se alguém poderia encontrar um exemplo de trabalho ou apenas me dar um sim ou não para esta pergunta:

É possível pré-dimensionar uma imagem usando o novo armazenamento local (ou qualquer outro), para que um usuário que não tenha a menor idéia sobre o redimensionamento de uma imagem possa arrastar sua imagem de 10 MB para o meu site, redimensioná-la usando o novo localstorage e ENTÃO faça o upload no tamanho menor.

Eu sei muito bem que você pode fazer isso com Flash, Java applets, X ativo … A questão é se você pode fazer com Javascript + Html5.

Ansioso para a resposta sobre este.

Ta por enquanto.

Sim, use a API File e , em seguida, você pode processar as imagens com o elemento canvas .

Esta postagem no blog do Mozilla Hacks mostra a maior parte do processo. Para referência, aqui está o código-fonte montado a partir da postagem do blog:

// from an input element var filesToUpload = input.files; var file = filesToUpload[0]; var img = document.createElement("img"); var reader = new FileReader(); reader.onload = function(e) {img.src = e.target.result} reader.readAsDataURL(file); var ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); var MAX_WIDTH = 800; var MAX_HEIGHT = 600; var width = img.width; var height = img.height; if (width > height) { if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; width = MAX_WIDTH; } } else { if (height > MAX_HEIGHT) { width *= MAX_HEIGHT / height; height = MAX_HEIGHT; } } canvas.width = width; canvas.height = height; var ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, width, height); var dataurl = canvas.toDataURL("image/png"); //Post dataurl to the server with AJAX 

Eu lidei com esse problema há alguns anos e enviei minha solução para o github como https://github.com/rossturner/HTML5-ImageUploader

A resposta de robertc usa a solução proposta no post do blog do Mozilla Hacks , no entanto, descobri que isso dava uma qualidade de imagem muito ruim ao resize para uma escala que não era 2: 1 (ou um múltiplo disso). Comecei a experimentar com diferentes algoritmos de redimensionamento de imagens, embora a maioria acabasse sendo bastante lenta ou não fosse de grande qualidade.

Finalmente, criei uma solução que, acredito, executa rapidamente e tem um desempenho muito bom também – como a solução Mozilla de copiar de 1 canvas para outra funciona rapidamente e sem perda de qualidade de imagem na proporção de 2: 1, dada a meta de x pixels de largura e y pixels de altura, eu uso este método de redimensionamento da canvas até que a imagem esteja entre xe 2x, ey e 2y . Neste ponto, passo a resize a imagem algorítmica para a “etapa” final do redimensionamento até o tamanho desejado. Depois de tentar vários algoritmos diferentes, decidi pela interpolação bilinear tirada de um blog que não está mais online, mas acessível através do Internet Archive , que dá bons resultados, eis o código aplicável:

 ImageUploader.prototype.scaleImage = function(img, completionCallback) { var canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height); while (canvas.width >= (2 * this.config.maxWidth)) { canvas = this.getHalfScaleCanvas(canvas); } if (canvas.width > this.config.maxWidth) { canvas = this.scaleCanvasWithAlgorithm(canvas); } var imageData = canvas.toDataURL('image/jpeg', this.config.quality); this.performUpload(imageData, completionCallback); }; ImageUploader.prototype.scaleCanvasWithAlgorithm = function(canvas) { var scaledCanvas = document.createElement('canvas'); var scale = this.config.maxWidth / canvas.width; scaledCanvas.width = canvas.width * scale; scaledCanvas.height = canvas.height * scale; var srcImgData = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height); var destImgData = scaledCanvas.getContext('2d').createImageData(scaledCanvas.width, scaledCanvas.height); this.applyBilinearInterpolation(srcImgData, destImgData, scale); scaledCanvas.getContext('2d').putImageData(destImgData, 0, 0); return scaledCanvas; }; ImageUploader.prototype.getHalfScaleCanvas = function(canvas) { var halfCanvas = document.createElement('canvas'); halfCanvas.width = canvas.width / 2; halfCanvas.height = canvas.height / 2; halfCanvas.getContext('2d').drawImage(canvas, 0, 0, halfCanvas.width, halfCanvas.height); return halfCanvas; }; ImageUploader.prototype.applyBilinearInterpolation = function(srcCanvasData, destCanvasData, scale) { function inner(f00, f10, f01, f11, x, y) { var un_x = 1.0 - x; var un_y = 1.0 - y; return (f00 * un_x * un_y + f10 * x * un_y + f01 * un_x * y + f11 * x * y); } var i, j; var iyv, iy0, iy1, ixv, ix0, ix1; var idxD, idxS00, idxS10, idxS01, idxS11; var dx, dy; var r, g, b, a; for (i = 0; i < destCanvasData.height; ++i) { iyv = i / scale; iy0 = Math.floor(iyv); // Math.ceil can go over bounds iy1 = (Math.ceil(iyv) > (srcCanvasData.height - 1) ? (srcCanvasData.height - 1) : Math.ceil(iyv)); for (j = 0; j < destCanvasData.width; ++j) { ixv = j / scale; ix0 = Math.floor(ixv); // Math.ceil can go over bounds ix1 = (Math.ceil(ixv) > (srcCanvasData.width - 1) ? (srcCanvasData.width - 1) : Math.ceil(ixv)); idxD = (j + destCanvasData.width * i) * 4; // matrix to vector indices idxS00 = (ix0 + srcCanvasData.width * iy0) * 4; idxS10 = (ix1 + srcCanvasData.width * iy0) * 4; idxS01 = (ix0 + srcCanvasData.width * iy1) * 4; idxS11 = (ix1 + srcCanvasData.width * iy1) * 4; // overall coordinates to unit square dx = ixv - ix0; dy = iyv - iy0; // I let the r, g, b, a on purpose for debugging r = inner(srcCanvasData.data[idxS00], srcCanvasData.data[idxS10], srcCanvasData.data[idxS01], srcCanvasData.data[idxS11], dx, dy); destCanvasData.data[idxD] = r; g = inner(srcCanvasData.data[idxS00 + 1], srcCanvasData.data[idxS10 + 1], srcCanvasData.data[idxS01 + 1], srcCanvasData.data[idxS11 + 1], dx, dy); destCanvasData.data[idxD + 1] = g; b = inner(srcCanvasData.data[idxS00 + 2], srcCanvasData.data[idxS10 + 2], srcCanvasData.data[idxS01 + 2], srcCanvasData.data[idxS11 + 2], dx, dy); destCanvasData.data[idxD + 2] = b; a = inner(srcCanvasData.data[idxS00 + 3], srcCanvasData.data[idxS10 + 3], srcCanvasData.data[idxS01 + 3], srcCanvasData.data[idxS11 + 3], dx, dy); destCanvasData.data[idxD + 3] = a; } } }; 

Isso redimensiona uma imagem para uma largura de config.maxWidth , mantendo a proporção original. Na época do desenvolvimento, isso funcionou no iPad / iPhone Safari, além dos principais navegadores de desktop (IE9 +, Firefox, Chrome), então espero que ele seja compatível, considerando a maior aceitação do HTML5 hoje. Observe que a chamada canvas.toDataURL () aceita um tipo mime e uma qualidade de imagem que permitem controlar a qualidade e o formato do arquivo de saída (potencialmente diferente da input, se desejar).

O único ponto que isso não cobre é manter a informação de orientação, sem o conhecimento desses metadados a imagem é redimensionada e salva como está, perdendo qualquer metadado dentro da imagem para orientação, o que significa que as imagens tiradas em um tablet “de cabeça para baixo” renderizado como tal, embora eles tenham sido colocados no visor da câmera do dispositivo. Se isso for uma preocupação, este post do blog tem um bom guia e exemplos de código sobre como fazer isso, o que eu tenho certeza que poderia ser integrado ao código acima.

Correção para acima:

    

Modificação para a resposta de Justin que funciona para mim:

  1. Adicionado img.onload
  2. Expanda a solicitação POST com um exemplo real
 function handleFiles() { var dataurl = null; var filesToUpload = document.getElementById('photo').files; var file = filesToUpload[0]; // Create an image var img = document.createElement("img"); // Create a file reader var reader = new FileReader(); // Set the image once loaded into file reader reader.onload = function(e) { img.src = e.target.result; img.onload = function () { var canvas = document.createElement("canvas"); var ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); var MAX_WIDTH = 800; var MAX_HEIGHT = 600; var width = img.width; var height = img.height; if (width > height) { if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; width = MAX_WIDTH; } } else { if (height > MAX_HEIGHT) { width *= MAX_HEIGHT / height; height = MAX_HEIGHT; } } canvas.width = width; canvas.height = height; var ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, width, height); dataurl = canvas.toDataURL("image/jpeg"); // Post the data var fd = new FormData(); fd.append("name", "some_filename.jpg"); fd.append("image", dataurl); fd.append("info", "lah_de_dah"); $.ajax({ url: '/ajax_photo', data: fd, cache: false, contentType: false, processData: false, type: 'POST', success: function(data){ $('#form_photo')[0].reset(); location.reload(); } }); } // img.onload } // Load files into file reader reader.readAsDataURL(file); } 

Se você não quer reinventar a roda, você pode tentar plupload.com

 fd.append("image", dataurl); 

Isso não funcionará. No lado do PHP você não pode salvar o arquivo com isso.

Use este código em vez disso:

 var blobBin = atob(dataurl.split(',')[1]); var array = []; for(var i = 0; i < blobBin.length; i++) { array.push(blobBin.charCodeAt(i)); } var file = new Blob([new Uint8Array(array)], {type: 'image/png', name: "avatar.png"}); fd.append("image", file); // blob file 

A resposta aceita funciona muito bem, mas a lógica de redimensionamento ignora o caso em que a imagem é maior que o máximo em apenas um dos eixos (por exemplo, height> maxHeight mas width <= maxWidth).

Eu acho que o código a seguir cuida de todos os casos de uma maneira mais direta e funcional (ignore as annotations do tipo typescript se estiver usando javascript simples):

 private scaleDownSize(width: number, height: number, maxWidth: number, maxHeight: number): {width: number, height: number} { if (width <= maxWidth && height <= maxHeight) return { width, height }; else if (width / maxWidth > height / maxHeight) return { width: maxWidth, height: height * maxWidth / width}; else return { width: width * maxHeight / height, height: maxHeight }; } 

O redimensionamento de imagens em um elemento de canvas geralmente é uma má idéia, já que ele usa a interpolação de checkbox mais barata. A imagem resultante perceptível degrada em qualidade. Eu recomendaria o uso de http://nodeca.github.io/pica/demo/, que pode realizar a transformação de Lanczos. A página de demonstração acima mostra a diferença entre as abordagens canvas e Lanczos.

Ele também usa web workers para resize imagens em paralelo. Há também a implementação WEBGL.

Existem alguns redimensionadores de imagens online que usam pica para fazer o trabalho, como https://myimageresizer.com

Você pode usar o dropzone.js se quiser usar um gerenciador de upload simples e fácil com redimensionamento antes das funções de upload.

Ele tem funções de redimensionamento embutidas, mas você pode fornecer o seu próprio, se quiser.

Datilografado

 async resizeImg(file: Blob): Promise { let img = document.createElement("img"); img.src = await new Promise(resolve => { let reader = new FileReader(); reader.onload = (e: any) => resolve(e.target.result); reader.readAsDataURL(file); }); await new Promise(resolve => img.onload = resolve) let canvas = document.createElement("canvas"); let ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); let MAX_WIDTH = 1000; let MAX_HEIGHT = 1000; let width = img.naturalWidth; let height = img.naturalHeight; if (width > height) { if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; width = MAX_WIDTH; } } else { if (height > MAX_HEIGHT) { width *= MAX_HEIGHT / height; height = MAX_HEIGHT; } } canvas.width = width; canvas.height = height; ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0, width, height); let result = await new Promise(resolve => { canvas.toBlob(resolve, 'image/jpeg', 0.95); }); return result; }