Pré-redimensionar imagens em HTML5 antes do upload

181

Aqui está um arranhão de macarrão.

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

É possível pré-dimensionar uma imagem usando o novo armazenamento local (ou o que for), para que um usuário que não tenha idéia de redimensionar uma imagem possa arrastar sua imagem de 10 MB para o meu site, redimensione-a usando o novo armazenamento local e ENTÃO carregue-o no tamanho menor.

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

Ansioso para a resposta sobre este.

Ta por enquanto.

Jimmyt1988
fonte

Respostas:

181

Sim, use a API do arquivo e processe as imagens com o elemento canvas .

Esta postagem do blog Mozilla Hacks orienta você durante a maior parte do processo. Para referência, aqui está o código-fonte montado 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
robertc
fonte
6
Por que obrigado bom senhor. Vou jogar hoje à noite ... Com o arquivo api que é. Fiz o upload arrastar e soltar para funcionar e percebi que esse também seria um recurso muito bom de incluir. Yippee.
precisa saber é o seguinte
3
Não precisamos usar esse mycanvas.toDataURL ("image / jpeg", 0.5) para garantir que a qualidade seja trazida para diminuir o tamanho do arquivo. Me deparei com esse comentário no post do mozilla e vi a resolução de erros aqui bugzilla.mozilla.org/show_bug.cgi?id=564388
sbose
33
Você deve aguardar onloada imagem (anexar manipulador antes de definir src), porque o navegador pode decodificar de forma assíncrona e os pixels podem não estar disponíveis antes drawImage.
precisa
6
Você pode colocar o código da tela na função onload do leitor de arquivos - há uma chance de que imagens grandes não sejam carregadas antes de você chegar ctx.drawImage()perto do final.
Greg
4
tenha cuidado, você precisa de magia adicional para fazê-lo funcionar no IOS: stackoverflow.com/questions/11929099/… #
flo850
107

Resolvi esse problema há alguns anos e carreguei minha solução no github como https://github.com/rossturner/HTML5-ImageUploader

A resposta de robertc usa a solução proposta na postagem do blog Mozilla Hacks , no entanto, eu achei que essa qualidade de imagem era realmente ruim ao redimensionar para uma escala que não era 2: 1 (ou um múltiplo dela). Comecei a experimentar diferentes algoritmos de redimensionamento de imagem, embora a maioria acabasse sendo muito lenta ou também não era boa em qualidade.

Finalmente, eu vim com uma solução que, acredito, é executada rapidamente e tem um desempenho muito bom também - já que a solução Mozilla de copiar de uma tela para outra funciona rapidamente e sem perda da qualidade da imagem na proporção 2: 1, dado o objetivo de x pixels de largura e y pixels de altura, que usam este método de redimensionamento de lona até que a imagem está entre x e 2 x , e Y e 2 y . Nesse ponto, volto para o redimensionamento algorítmico da imagem para a "etapa" final de redimensionar para o tamanho alvo. Depois de tentar vários algoritmos diferentes, decidi pela interpolação bilinear retirada de um blog que não está mais online, mas acessível através do Internet Archive, que fornece 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 reduz a imagem a uma largura de config.maxWidth, mantendo a proporção original. No momento do desenvolvimento, isso funcionou no iPad / iPhone Safari, além dos principais navegadores de desktop (IE9 +, Firefox, Chrome), então espero que ainda seja compatível, dada a adoção mais ampla do HTML5 atualmente. Observe que a chamada canvas.toDataURL () usa um tipo MIME e qualidade de imagem que permite controlar a qualidade e o formato do arquivo de saída (potencialmente diferente da entrada, se desejar).

O único ponto que isso não cobre é a manutenção das informações de orientação, sem o conhecimento desses metadados, a imagem é redimensionada e salva como está, perdendo quaisquer metadados na imagem para orientação, o que significa que as imagens tiradas em um tablet "de cabeça para baixo" foram renderizados como tal, apesar de terem sido invertidos no visor da câmera do dispositivo. Se isso for uma preocupação, esta postagem no blog tem um bom guia e exemplos de código sobre como fazer isso, que eu tenho certeza que poderia ser integrado ao código acima.

Ross Taylor-Turner
fonte
4
Premiado-lhe a recompensa porque eu sinto que adiciona um monte para a resposta, conseguiu obter esse material de orientação integrado embora :)
Sam Saffron
3
Uma alma muito útil já se fundiram em algumas funcionalidades de metadados de orientação :)
Ross Taylor-Turner
26

Correção acima:

<img src="" id="image">
<input id="input" type="file" onchange="handleFiles()">
<script>

function handleFiles()
{
    var filesToUpload = document.getElementById('input').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;

        var canvas = document.createElement("canvas");
        //var canvas = $("<canvas>", {"id":"testing"})[0];
        var ctx = canvas.getContext("2d");
        ctx.drawImage(img, 0, 0);

        var MAX_WIDTH = 400;
        var MAX_HEIGHT = 300;
        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");
        document.getElementById('image').src = dataurl;     
    }
    // Load files into file reader
    reader.readAsDataURL(file);


    // Post the data
    /*
    var fd = new FormData();
    fd.append("name", "some_filename.jpg");
    fd.append("image", dataurl);
    fd.append("info", "lah_de_dah");
    */
}</script>
Justin Levene
fonte
2
Como você lida com a parte do PHP depois de adicioná-lo ao FormData ()? Você não procuraria $ _FILES ['name'] ['tmp_name'] [$ i], por exemplo? Estou tentando se (isset ($ _ POST ['image']))) ... mas dataurlnão existe.
Denikov
2
não funciona no firefox na primeira vez que você tenta fazer upload de uma imagem. A próxima vez que você tentar, funcionará. Como corrigi-lo?
Fred
retorne se o arquivo for uma matriz for nula ou vazia.
Sheelpriy
2
No Chrome ao tentar acessar img.widthe, img.heightinicialmente, eles são 0. Você deve colocar o resto da função dentroimg.addEventListener('load', function () { // now the image has loaded, continue... });
Inigo
2
@ Justin, você pode esclarecer "o que foi dito acima", como, em que postagens é uma correção? Cheers
Martin
16

Modificação na 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);
}
Vai
fonte
2
informações de orientação (EXIF) se perde
Konstantin Vahrushev
8

Se você não quiser reinventar a roda, tente plupload.com

nanospeck
fonte
3
Eu também uso o plupload para redimensionar do lado do cliente. A melhor coisa é que ele pode usar flash, html5, silverlight etc. O que quer que o usuário tenha disponível, porque cada usuário tem uma configuração diferente. Se você quiser apenas html5, acho que não funcionará em alguns navegadores antigos e no Opera.
JNL
6

Texto datilografado

async resizeImg(file: Blob): Promise<Blob> {
    let img = document.createElement("img");
    img.src = await new Promise<any>(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<Blob>(resolve => { canvas.toBlob(resolve, 'image/jpeg', 0.95); });
    return result;
}
jales cardoso
fonte
1
Qualquer um que possa se deparar com este post, essa resposta baseada em promessas é muito mais confiável na minha opinião. Eu converti para JS normal e funciona como um encanto.
gbland777
5
fd.append("image", dataurl);

Isso não vai funcionar. No lado do PHP, você não pode salvar arquivos com isso.

Use este código:

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
robô
fonte
5

O redimensionamento de imagens em um elemento de tela geralmente é uma má ideia, pois usa a interpolação de caixa mais barata. A imagem resultante visível diminui a qualidade. Eu recomendo usar http://nodeca.github.io/pica/demo/ que pode executar 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 trabalhadores da Web para redimensionar imagens em paralelo. Há também implementação WEBGL.

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

Ivan Nikitin
fonte
5

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, altura> maxHeight, mas largura <= maxWidth).

Acho que o código a seguir cuida de todos os casos de maneira mais direta e funcional (ignore as anotações do tipo datilografado 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 };
  }
Taro
fonte
0

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

Ele possui funções de redimensionamento integradas, mas você pode fornecer suas próprias, se desejar.

Michał Zalewski
fonte