Converter URI de dados em arquivo e depois anexar a FormData

282

Estou tentando reimplementar um carregador de imagens HTML5 como o do site Mozilla Hacks , mas que funciona com os navegadores WebKit. Parte da tarefa é extrair um arquivo de imagem do canvasobjeto e anexá-lo a um objeto FormData para upload.

O problema é que, embora canvastenha a toDataURLfunção de retornar uma representação do arquivo de imagem, o objeto FormData aceita apenas objetos File ou Blob da API de arquivos .

A solução Mozilla usou a seguinte função somente do Firefox em canvas:

var file = canvas.mozGetAsFile("foo.png");

... que não está disponível nos navegadores WebKit. A melhor solução que eu consegui pensar é encontrar uma maneira de converter um URI de dados em um objeto File, que eu pensei que poderia fazer parte da API de arquivos, mas não consigo encontrar uma coisa para isso.

É possível? Caso contrário, existem alternativas?

Obrigado.

Stoive
fonte
Se você deseja salvar o DataURI de uma imagem no servidor: stackoverflow.com/a/50131281/5466401
Sibin John Mattappallil

Respostas:

470

Depois de brincar com algumas coisas, eu mesmo consegui descobrir isso.

Primeiro de tudo, isso converterá um dataURI em um Blob:

function dataURItoBlob(dataURI) {
    // convert base64/URLEncoded data component to raw binary data held in a string
    var byteString;
    if (dataURI.split(',')[0].indexOf('base64') >= 0)
        byteString = atob(dataURI.split(',')[1]);
    else
        byteString = unescape(dataURI.split(',')[1]);

    // separate out the mime component
    var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];

    // write the bytes of the string to a typed array
    var ia = new Uint8Array(byteString.length);
    for (var i = 0; i < byteString.length; i++) {
        ia[i] = byteString.charCodeAt(i);
    }

    return new Blob([ia], {type:mimeString});
}

A partir daí, é fácil anexar os dados a um formulário para que sejam enviados como um arquivo:

var dataURL = canvas.toDataURL('image/jpeg', 0.5);
var blob = dataURItoBlob(dataURL);
var fd = new FormData(document.forms[0]);
fd.append("canvasImage", blob);
Stoive
fonte
28
Por que isso sempre acontece ... Você tenta resolver um problema por horas e horas com pesquisas de SO aqui e ali. Então você posta uma pergunta. Dentro de uma hora, você obtém a resposta de outra pergunta. Não que eu esteja reclamando ... stackoverflow.com/questions/9388412/…
syaz
@stoive eu sou capaz de contruct Blob mas você pode explicar como você construir o POSTou PUTa S3?
Gaurav Shah
1
@mimo - Aponta para o subjacente ArrayBuffer, que é gravado na BlobBuilderinstância.
Stoive
2
@stoive Nesse caso, por que não é bb.append (ia)?
Mimo
4
Obrigado! Isso resolveu o meu problema com uma pequena correçãovar file = new File( [blob], 'canvasImage.jpg', { type: 'image/jpeg' } ); fd.append("canvasImage", file);
Prasad19sara
141

BlobBuilder e ArrayBuffer agora estão obsoletos, eis o código do comentário principal atualizado com o construtor Blob:

function dataURItoBlob(dataURI) {
    var binary = atob(dataURI.split(',')[1]);
    var array = [];
    for(var i = 0; i < binary.length; i++) {
        array.push(binary.charCodeAt(i));
    }
    return new Blob([new Uint8Array(array)], {type: 'image/jpeg'});
}
vava720
fonte
2
Apenas uma ideia: array=[]; array.length=binary.length;... array[i]=bina... etc. Portanto, o array é pré-alocado. Isso evita que push () precise estender a matriz a cada iteração, e estamos processando possivelmente milhões de itens (= bytes) aqui, por isso importa.
DDS
2
Também falha para mim no Safari. @WilliamT. Porém, a resposta de funciona para Firefox / Safari / Chrome.
ObscureRobot
"binário" é um nome um pouco enganador, pois não é uma matriz de bits, mas uma matriz de bytes.
Niels Abildgaard
1
"type: 'image / jpeg'" - e se for uma imagem png OU se você não souber a extensão da imagem com antecedência?
Jasper
Criar Uint8Array primeiro é melhor do que criar uma matriz e depois converter para Uint8Array.
cuixiping 26/05
52

Este funciona no iOS e Safari.

Você precisa usar a solução ArrayBuffer da Stoive, mas não pode usar o BlobBuilder, como indica o vava720, então aqui está o mashup de ambos.

function dataURItoBlob(dataURI) {
    var byteString = atob(dataURI.split(',')[1]);
    var ab = new ArrayBuffer(byteString.length);
    var ia = new Uint8Array(ab);
    for (var i = 0; i < byteString.length; i++) {
        ia[i] = byteString.charCodeAt(i);
    }
    return new Blob([ab], { type: 'image/jpeg' });
}
William T.
fonte
11
Ótimo! Mas você ainda pode manter a seqüência de mímica dinâmica, como na solução de Stoive, suponho? // separa o componente mime var mimeString = dataURI.split (',') [0] .split (':') [1] .split (';') [0]
Por Quested Aronsson em
O que há com o fallback para iOS6 com prefixo de kit da web? Como você lida com isso?
Confile20 /
30

O Firefox possui os métodos canvas.toBlob () e canvas.mozGetAsFile () .

Mas outros navegadores não.

Podemos obter o dataurl da tela e, em seguida, converter o dataurl em objeto de blob.

Aqui está a minha dataURLtoBlob()função. É muito curto.

function dataURLtoBlob(dataurl) {
    var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
        bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
    while(n--){
        u8arr[n] = bstr.charCodeAt(n);
    }
    return new Blob([u8arr], {type:mime});
}

Use esta função com FormData para manipular sua tela ou dataurl.

Por exemplo:

var dataurl = canvas.toDataURL('image/jpeg',0.8);
var blob = dataURLtoBlob(dataurl);
var fd = new FormData();
fd.append("myFile", blob, "thumb.jpg");

Além disso, você pode criar um HTMLCanvasElement.prototype.toBlobmétodo para um navegador que não seja um mecanismo de lagartixa.

if(!HTMLCanvasElement.prototype.toBlob){
    HTMLCanvasElement.prototype.toBlob = function(callback, type, encoderOptions){
        var dataurl = this.toDataURL(type, encoderOptions);
        var bstr = atob(dataurl.split(',')[1]), n = bstr.length, u8arr = new Uint8Array(n);
        while(n--){
            u8arr[n] = bstr.charCodeAt(n);
        }
        var blob = new Blob([u8arr], {type: type});
        callback.call(this, blob);
    };
}

Agora canvas.toBlob()funciona para todos os navegadores modernos, não apenas para o Firefox. Por exemplo:

canvas.toBlob(
    function(blob){
        var fd = new FormData();
        fd.append("myFile", blob, "thumb.jpg");
        //continue do something...
    },
    'image/jpeg',
    0.8
);
cuixiping
fonte
1
O polyfill para canvas.toBlob mencionado aqui é a maneira correta de lidar com esse problema IMHO.
Jakob Kruse
2
Gostaria de enfatizar a última coisa neste post: "Agora o canvas.toBlob () funciona para todos os navegadores modernos".
Eric Simonton
25

Minha maneira preferida é canvas.toBlob ()

Mas, de qualquer forma, aqui está outra maneira de converter base64 em um blob usando fetch ^^,

var url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="

fetch(url)
.then(res => res.blob())
.then(blob => {
  var fd = new FormData()
  fd.append('image', blob, 'filename')
  
  console.log(blob)

  // Upload
  // fetch('upload', {method: 'POST', body: fd})
})

Sem fim
fonte
o que é buscar e como é relevante?
Ricardo Freitas
Fetch é um método ajax moderna que você pode usar em vez de XMLHttpRequestuma vez url de dados é apenas um URL, você pode usar ajax para buscar esse recurso e você obteve-se uma opção para decidir se você quer que ele como blob, ArrayBuffer ou texto
Sem Fim
1
@Endless 'fetch ()' uma string base64 local ... um truque muito inteligente!
Diego ZoracKy
1
Lembre-se disso blob:e data:não é universalmente suportado por todas as implementações de busca. Usamos essa abordagem, pois sabemos que lidaremos apenas com navegadores móveis (WebKit), mas o Edge, por exemplo, não oferece suporte a ela: developer.mozilla.org/en-US/docs/Web/API/…
oligofren
19

Graças a @Stoive e @ vava720, combinei os dois dessa maneira, evitando usar o BlobBuilder e o ArrayBuffer obsoletos

function dataURItoBlob(dataURI) {
    'use strict'
    var byteString, 
        mimestring 

    if(dataURI.split(',')[0].indexOf('base64') !== -1 ) {
        byteString = atob(dataURI.split(',')[1])
    } else {
        byteString = decodeURI(dataURI.split(',')[1])
    }

    mimestring = dataURI.split(',')[0].split(':')[1].split(';')[0]

    var content = new Array();
    for (var i = 0; i < byteString.length; i++) {
        content[i] = byteString.charCodeAt(i)
    }

    return new Blob([new Uint8Array(content)], {type: mimestring});
}
Mimo
fonte
12

O padrão em evolução parece ser canvas.toBlob () e não canvas.getAsFile (), pois a Mozilla corria o risco de adivinhar.

Ainda não vejo nenhum navegador que suporte :(

Obrigado por este ótimo tópico!

Além disso, qualquer pessoa que tente a resposta aceita deve ter cuidado com o BlobBuilder, pois estou achando o suporte limitado (e com espaço para nome):

    var bb;
    try {
        bb = new BlobBuilder();
    } catch(e) {
        try {
            bb = new WebKitBlobBuilder();
        } catch(e) {
            bb = new MozBlobBuilder();
        }
    }

Você estava usando o polyfill de outra biblioteca para o BlobBuilder?

Chris Bosco
fonte
Eu estava usando o Chrome sem polyfills e não me lembro de encontrar espaço para nome. Eu espero ansiosamente canvas.toBlob()- parece muito mais apropriado do que getAsFile.
Stoive
1
BlobBuilderparece estar obsoleto em favor deBlob
sandstrom 10/10/12
O BlobBuilder está obsoleto e esse padrão é horrível. Melhor seria: window.BlobBuilder = (window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder); as tentativas aninhadas são realmente feias e o que acontece se nenhum dos construtores estiver disponível?
Chris Hanson
Como isso é horrível? Se uma exceção for lançada e 1) o BlobBuilder não existir, nada acontecerá e o próximo bloco será executado. 2) Se existir, mas uma exceção for lançada, ela será descontinuada e não deverá ser usada de qualquer maneira, continuando no próximo bloco de tentativa. Idealmente, você iria verificar se Blob é suportado em primeiro lugar, e antes mesmo de esta verificação para suporte toBlob
TaylorMac
5
var BlobBuilder = (window.MozBlobBuilder || window.WebKitBlobBuilder || window.BlobBuilder);

pode ser usado sem a tentativa de captura.

Obrigado a check_ca. Ótimo trabalho.

Nafis Ahmad
fonte
1
Isso ainda gerará um erro se o navegador suportar o BlobBuilder descontinuado. O navegador usará o método antigo se for compatível, mesmo se for compatível com o novo método. Esta não é desejada, consulte a abordagem de Chris Bosco abaixo
TaylorMac
4

A resposta original de Stoive é facilmente corrigível alterando a última linha para acomodar o Blob:

function dataURItoBlob (dataURI) {
    // convert base64 to raw binary data held in a string
    // doesn't handle URLEncoded DataURIs
    var byteString;
    if (dataURI.split(',')[0].indexOf('base64') >= 0)
        byteString = atob(dataURI.split(',')[1]);
    else
        byteString = unescape(dataURI.split(',')[1]);
    // separate out the mime component
    var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];

    // write the bytes of the string to an ArrayBuffer
    var ab = new ArrayBuffer(byteString.length);
    var ia = new Uint8Array(ab);
    for (var i = 0; i < byteString.length; i++) {
        ia[i] = byteString.charCodeAt(i);
    }

    // write the ArrayBuffer to a blob, and you're done
    return new Blob([ab],{type: mimeString});
}
topkara
fonte
3

Aqui está uma versão ES6 da resposta do Stoive :

export class ImageDataConverter {
  constructor(dataURI) {
    this.dataURI = dataURI;
  }

  getByteString() {
    let byteString;
    if (this.dataURI.split(',')[0].indexOf('base64') >= 0) {
      byteString = atob(this.dataURI.split(',')[1]);
    } else {
      byteString = decodeURI(this.dataURI.split(',')[1]);
    }
    return byteString;
  }

  getMimeString() {
    return this.dataURI.split(',')[0].split(':')[1].split(';')[0];
  }

  convertToTypedArray() {
    let byteString = this.getByteString();
    let ia = new Uint8Array(byteString.length);
    for (let i = 0; i < byteString.length; i++) {
      ia[i] = byteString.charCodeAt(i);
    }
    return ia;
  }

  dataURItoBlob() {
    let mimeString = this.getMimeString();
    let intArray = this.convertToTypedArray();
    return new Blob([intArray], {type: mimeString});
  }
}

Uso:

const dataURL = canvas.toDataURL('image/jpeg', 0.5);
const blob = new ImageDataConverter(dataURL).dataURItoBlob();
let fd = new FormData(document.forms[0]);
fd.append("canvasImage", blob);
vekerdyb
fonte
3

Obrigado! @steovi para esta solução.

Eu adicionei suporte à versão ES6 e mudei do unescape para dataURI (o unescape está obsoleto).

converterDataURItoBlob(dataURI) {
    let byteString;
    let mimeString;
    let ia;

    if (dataURI.split(',')[0].indexOf('base64') >= 0) {
      byteString = atob(dataURI.split(',')[1]);
    } else {
      byteString = encodeURI(dataURI.split(',')[1]);
    }
    // separate out the mime component
    mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];

    // write the bytes of the string to a typed array
    ia = new Uint8Array(byteString.length);
    for (var i = 0; i < byteString.length; i++) {
      ia[i] = byteString.charCodeAt(i);
    }
    return new Blob([ia], {type:mimeString});
}
Wilfredonoyola
fonte
1

simplifique: D

function dataURItoBlob(dataURI,mime) {
    // convert base64 to raw binary data held in a string
    // doesn't handle URLEncoded DataURIs

    var byteString = window.atob(dataURI);

    // separate out the mime component


    // write the bytes of the string to an ArrayBuffer
    //var ab = new ArrayBuffer(byteString.length);
    var ia = new Uint8Array(byteString.length);
    for (var i = 0; i < byteString.length; i++) {
        ia[i] = byteString.charCodeAt(i);
    }

    // write the ArrayBuffer to a blob, and you're done
    var blob = new Blob([ia], { type: mime });

    return blob;
}
Sendy
fonte
-2

toDataURL fornece uma string e você pode colocá-la em uma entrada oculta.

Cat Chen
fonte
Você poderia dar um exemplo? Eu não quero fazer upload de uma string base64 (que é o que fazer <input type=hidden value="data:..." />faria), quero fazer upload dos dados do arquivo (como o que <input type="file" />faz, exceto que você não tem permissão para definir a valuepropriedade).
Stoive
Este deve ser um comentário e não uma resposta. Por favor, elabore a resposta com uma explicação adequada. @Cat Chen
Lucky
-5

Eu tive exatamente o mesmo problema que Ravinder Payal e encontrei a resposta. Tente o seguinte:

var dataURL = canvas.toDataURL("image/jpeg");

var name = "image.jpg";
var parseFile = new Parse.File(name, {base64: dataURL.substring(23)});
feng
fonte
2
Você está realmente sugerindo usar o Parse.com? Você deve mencionar que sua resposta requer dependências!
Pierre Maoui 07/07
2
WTF? Por que alguém fará o upload do código de imagem base64 no servidor PARSE e depois fará o download? Quando podemos fazer upload diretamente do base64 em nossos servidores, o principal é que são necessários os mesmos dados para fazer o upload do arquivo de imagem ou da string base64. E se você quiser permitir que o usuário baixe a imagem, você pode usá-lowindow.open(canvas.toDataURL("image/jpeg"))
Ravinder Payal