IE - carregue arquivos com a API de arquivo

Andrew Dodson

Desenvolvedor, tecnologia Web

Por que há restrições de tamanho de arquivo no carregamento de avatares? Você sabe, "Selecione uma imagem (máximo de 50 KB)”. E por que aplicativos Web de manipulação de fotos não se tornaram realidade, já que o Canvas existe há algum tempo? A resposta a essas duas perguntas se sresume a desempenho. Até agora precisamos de um plug-in lento ou de uma rota através de um servidor para criar e modificar arquivos no navegador. Mas, para usuários do Internet Explorer 10, Firefox e Chrome, os desenvolvedores têm a impressionante API de arquivo em seu arsenal para possibilitar essas operações nativamente no navegador.

A API de arquivo é uma nova API JavaScript que permite a leitura e a gravação de objetos de dados binários que representam arquivos em aplicativos Web. Resumindo, você pode ler arquivos selecionados pelo usuário em aplicativos Web e baixar objetos de dados novos como arquivos de aplicativos Web. Vejamos em mais detalhes.

Desmembramento da API de arquivo

A API de arquivo (como definida pelo W3C) não é um tipo, mas um conjunto de objetos, funções e métodos tipados.

FileList

FileList é um objeto tipado que existe em vários locais. Primeiro, como uma propriedade em um elemento de entrada de formulário cujo tipo é "arquivo". Segundo, como parte do evento despachado em um evento de remoção de arquivo ou no evento Área de transferência (como resultado de copia e colagem).

Por exemplo, digamos que você tenha um controle de entrada de formulário como abaixo.

<input type=file onchange="console.log(this.files.length)" multiple />

Sempre que o usuário clicar na entrada de formulário e selecionar um arquivo, essa ação despacha o manipulador de eventos onchange e o objeto FileList object (localizado em this.files relativo ao elemento de entrada) tem algumas novas entradas. Neste exemplo, o número de arquivos que o usuário selecionou é apresentado no console do navegador.

Embora o objeto FileList se comporte de forma semelhante a uma Matriz nativa—no sentido de que se pode iterar seu conteúdo como é possível com uma Matriz—um FileList contém apenas instâncias imutáveis de objetos Arquivo (descrito a seguir).

Arquivo

O objeto Arquivo representa um arquivo individual do FileList. O objeto Arquivo contém um hash de metadados somente leitura sobre o arquivo, inclusive nome, última data de modificação, tamanho e tipo. Ele também é usado como referência e pode ser passado para o FileReader para a leitura de seu conteúdo.

Blob (objeto binário grande)

A interface Blob expõe os dados binários brutos de um arquivo. Dados remotos podem ser servidos como um objeto blob via XHRRequest—se xhr.responseType estiver definido como "blob". O novo objeto blob é uma instância da API de blob e inclui métodos nativos como blob.slice(i,i+n), que podem ser usados para dividir um objeto blob grande em menores. (Uso a frase "interface de blob" quando falo sobre o tipo de objeto JavaScript e "objeto blob" para me referir a uma única instância da interface de blob.)

Além disso, com o construtor de interface de blob, objetos blob pequenos podem ser reunidos e mesclados novamente com essa interface.

new Blob([blob, blob,...])

Você encontrará um exemplo na demo do BlobBuilder, que carrega um único arquivo MP3 e depois o desmembra em diferentes arquivos de áudio (faixas) que são aplicadas a suas próprias marcas <áudio> para reprodução. Na demo, o usuário pode reconstruir o MP3 em outra ordem mesclando as faixas e até mesmo baixar o novo objeto blob como um MP3.

Observação: O W3C abandonou o BlobBuilder em favor do Blob. Ambos produzem o mesmo resultado, mas de maneiras diferentes. Além disso, o BlobBuilder da WebKit varia entre o Internet Explorer e o Firefox, então é melhor detectar o recurso Blob primeiro.

FileReader

A interface FileReader toma um objeto Arquivo ou Blob e lê seu conteúdo—objetos Arquivo ou Blob são apenas referências a algo armazenado no computador local. O FileReader pode ser otimizado para ler arquivos de vários tipos—por exemplo, Text(UTF-8), ArrayBuffer(para binários) ou URI de dados base64. O exemplo a seguir mostra como se obter o texto de um arquivo de uma instância de objeto blob usando o FileReader.

  1. var reader = new FileReader();
  2. reader.onload = function(e){
  3. console.log(e.target.result);
  4. }
  5. reader.readAsText(blob);

De forma similar, você pode ler outro conteúdo usando o método de leitura que mais se adapte ao tipo de arquivo: readAsArrayBuffer (ótimo para se trabalhar com arquivos binários grandes) ou readAsDataURL (ótimo caso se queira embutir conteúdo rapidamente em um objeto DOM, como uma imagem ou arquivo de áudio).

O FileReader inclui os diversos ouvintes de evento: onerror, onloadstart, onabort e onprogress (que é útil para se criar uma barra de processo para arquivos grandes e para se identificarem problemas).

Esquemas de URI

Esquemas de URI são URIs representando objetos no documento. Um recurso pode ser um Arquivo ou um Blob e sua URL correspondente é conhecida como URL de objeto. Com uma referência de Blob ou de Arquivo, pode-se criar uma URL de objeto usando createObjectURL. Por exemplo.

  1. var objecturl = window.URL.createObjectURL(blob)

retorna uma URL que se refere ao objeto do recurso, algo como "blob:http%3A//test.com/666e6730-f45c-47c1-8012-ccc706f17191".

Essa cadeia de caracteres pode ser disposta em qualquer lugar em que uma URI típica possa ser colocada—por exemplo, no src de uma marca de imagem, isto é, se a URI do objeto for de uma imagem. O recurso do objeto dura tanto quanto o documento, portanto, ao se atualizar a página, ele se perde.

FileSaver

A interface FileSaver expõe métodos para se gravarem blobs no diretório Downloads do usuário. A implementação é bastante simples e direta:

window.saveAs(blob, "filename")

Contudo, nenhum dos navegadores possuem isso atualmente. Somente o Internet Explorer 10 suporta uma alternativa simples: navigator.msSaveOrOpenBlob assim como navigator.msSaveBlob. Mas, como se verá, existem shims que podem criar uma funcionalidade similar, embora sem uma experiência de usuário ágil.

Então, os detalhes da API de arquivo são todos básicos e é muita coisa se para digerir como um conceito abstrato. Mas não se preocupe. Em breve haverá uma aula prática sobre como utilizá-la.

Entre na festa e crie um exemplo

Pensei em me arrumar para a festa (Figura 1) e criar minha própria ferramenta simples de manipulação de fotos em http://adodson.com/graffiti. Dê uma olhada. Selecione uma imagem em seu sistema de arquivos, rabisque na tela e depois baixe sua obra-prima—tudo usando as APIs de arquivo (e alguns métodos de tela e eventos de ponteiro).

JJ916262.5AE790E027B22E889A3F2F75F24747CD(pt-br,MSDN.10).png

Figura 1. Aplicativo API de arquivo Graffiti

Implementação do aplicativo Graffiti

Mesmo com a imagem berrante, esse aplicativo tem uma premissa bastante simples.

  • Selecione uma imagem como pano de fundo usando Arquivo + FileList e FileReader.
  • Carregue a imagem na marca da tela e manipule-a com a API do aplicativo HTML5 Canvas
  • Baixe a nova imagem usando Blob (ou BlobBuilder), saveAs (ou saveBlob) ou um hack de marca de âncora com URLs de objeto.

Etapa 1: Selecione um arquivo

Existe mais de um modo para o usuário selecionar uma imagem para seu aplicativo.

  • Escolher um arquivo usando entrada de formulário
  • Arrastar e soltar um arquivo
  • Copiar e colar um arquivo da Área de transferência

Cada uma dessas abordagens ativa seus ouvintes para disparar uma função personalizada, chamada readFile(), que usa uma instância do FileReader para extrair os dados do arquivo e adicionar a imagem do usuário à tela. Eis o código da função.

  1. // readFile, loads File Objects (which are also images) into our Canvas
  2. // @param File Object
  3. function readFile(file){
  4. // Create a new FileReader Object
  5. var reader = new FileReader();
  6. // Set an onload handler because we load files into it asynchronously
  7. reader.onload = function(e){
  8. // The response contains the Data-Uri, which we can then load into the canvas
  9. applyDataUrlToCanvas( reader.result );
  10. };
  11. reader.reaAsDataURL(file);
  12. }

Aqui, readFile apanha a referência do Arquivo (mostrada depois) e cria uma nova instância do objeto FileReader. Essa função lê os dados como uma DataURL, mas também poderia lê-los como binários ou até mesmo como um ArrayBuffer.

Para ativar a função , você usa uma referência de Arquivo como parâmetro usando uma das abordagens mencionadas anteriormente.

Escolha um arquivo usando entrada de formulário

A API de arquivo atualmente não define um método nativo para disparar a seleção do arquivo. Mas a entrada de formulário antiga confiável com type=file faz o trabalho muito bem:

<input type="file" name="picture" accept="image/png, image/jpeg"/>

Por mais desajeitado que o elemento de entrada de formulário seja, ele possui alguns atributos adicionais novos que são perfeitos para esse aplicativo.

O atributo de aceitação indica que tipos de arquivos são aceitáveis. Neste exemplo, são PNGs e JPEGs. Cabe ao dispositivo lidar com isso de forma adequada (o Windows, por exemplo, abre a biblioteca Imagens do usuário por padrão e mostra apenas os arquivos desses tipos).

A marca múltipla permite que o usuário selecione um ou mais arquivos em uma única etapa.

Em seguida, você precisa vincular ouvintes de evento ao evento de mudança da entrada do formulário para que a seleção do usuário dispare readFile automaticamente.

  1. document.querySelector('input[name=picture]').onchange = function(e){
  2. readFile(e.target.files[0]);

Esse código simplesmente apanha o primeiro arquivo selecionado pelo usuário (independentemente de o atributo múltiplo ter sido usado) e invoca readFile, passando o arquivo como o primeiro parâmetro.

JJ916262.note(pt-br,MSDN.10).gifNote:
Entre com um pouco de estilo
A caixa de entrada de formulário mostrada aqui não reflete minha estética, ou provavelmente a sua. Assim, posicione-a absolutamente sobre outro elemento de sua escolha com uma opacidade zero (mas esteja preparado para ela ter uma largura fixa, o que pode interferir com outros elementos). Ou, um pouco mais complicado: despache um evento de clique personalizado em uma caixa de entrada posicionada fora da tela. Leia mais sobre essa discussão aqui.

Arraste e solte arquivos

Arquivos podem ser arrastados do Explorador de arquivos e usar um modelo de evento similar ao hack de entrada de formulário. O evento de "soltar" ocorre quando o usuário coloca a imagem na tela. O evento contém uma propriedade chamada dataTransfer, que é um arquivo nomeado de propriedade-filha. No código a seguir, e.dataTransfer.files é uma instância de uma FileList (como mencionado antes, a FileList contém uma lista de referências de Arquivos), e o primeiro item Arquivo é o parâmetro para readFile. Isso é suportado no Webkit, Firefox e Internet Explorer 10. Veja um exemplo.

  1. // stop FireFox from replacing the whole page with the file.
  2. canvas.ondragover = function () { return false; };
  3. // Add drop handler
  4. canvas.ondrop = function (e) {
  5. e.preventDefault(); e = e || window.event;
  6. var files = e.dataTransfer.files;
  7. if(files){
  8. readFile(files[0]);
  9. }
  10. };

Copie e cole dados de arquivo

Dados da área de transferência podem ser acessados quando o usuário cola conteúdo no documento. Esses dados podem ser um misto de texto e imagens e não uma FileList com apenas referências de arquivo, como no controle de entrada de formulário ou no arrastamento de arquivos.

No código abaixo, os Dados da área de transferência são partilhados e entradas correspondentes às propriedades tipo e espécie que são "*/image" e arquivo "file" respectivamente, são filtrados. A instância de Arquivo do item é obtida usando getAsFile(), que é passado para readFile.

  1. // paste Clipboard data
  2. // Well not all of it just the images.
  3. document.onpaste = function(e){
  4. e.preventDefault();
  5. if(e.clipboardData&&e.clipboardData.items){
  6. // pasted image
  7. for(var i=0, items = e.clipboardData.items;i<items.length;i++){
  8. if( items[i].kind==='file' && items[i].type.match(/^image/) ){
  9. readFile(items[i].getAsFile());
  10. break;
  11. }
  12. }
  13. }
  14. return false;
  15. };

Isso conclui a primeira etapa. Mostrei três abordagens para se obter uma referência de Arquivo e carregar dados de arquivo no documento. Gostaria de saber se há outras maneiras, então, por favor, envie comentários.

Etapa 2: Carregue a imagem na tela

A função readFile na Etapa 1 transmite uma URL de dados gerados a outra função personalizada. applyDataUrlToCanvas. Essa função desenha uma imagem selecionada na tela. Eis como funciona.

  • Encontre a largura e a altura da imagem.
  • Encontre a orientação da imagem.
  • Desenhe o melhor ajuste da imagem na tela.

Encontre a largura e a altura

Usando a função Imagem DOM, você pode determinar facilmente as dimensões de qualquer imagem. É uma técnica prática e mais ou menos assim.

  1. var img = new Image();
  2. img.onload = function(){
  3. // img.width
  4. // img.height
  5. }
  6. img.src = dataURL;

Encontre a orientação

Infelizmente, há uma "pegadinha" com a largura e altura, e é bem complicada: a foto pode ter sido tirada no modo retrato e salva em paisagem, ou tirada de cabeça para baixo.

Algumas câmeras, em vez de salvarem na orientação correta, fornecem uma propriedade Orientação dentro dos dados EXIF da imagem. Ela pode ser lida a partir dos dados binários da imagem.

Converter a URL de dados em uma cadeia de caracteres binários é fácil:

  1. var base64 = dataUrl.replace(/^.*?,/,'');
  2. var binary = atob(base64);

E, felizmente, existe uma biblioteca EXIF no lado do cliente, de código aberto, escrita por Jacob Seidelin, que retorna os dados EXIF como um Objeto. Sim, fantástico!

  1. <script src="http://www.nihilogic.dk/labs/exif/exif.js"></script>
  2. <script src="http://www.nihilogic.dk/labs/binaryajax/binaryajax.js"></script>
  3. <script>
  4. var exif = EXIF.readFromBinaryFile(new BinaryFile(binary));
  5. //exif.Orientation
  6. </script>

A propriedade Orientação é um número inteiro na faixa 1–8 correspondendo a quatro rotações e quatro inversões (bastante redundante).

Desenhe a imagem na tela

Tendo descoberto o valor Orientação, você pode girar e desenhar na tela. Se quiser ver o meu algoritmo, examine o código-fonte, que pode ser encontrado em http://adodson.com/graffiti/ ehttps://github.com/MrSwitch/graffiti.

Etapa 3: Baixe a imagem

A última etapa é baixar a imagem modificada. Dentro do repertório da API do arquivo, você pode usar a interface de Blob para criar arquivos no cliente. Termine com o novo msSaveBlob no Internet Explorer 10, ou com o hack baixar atributo (em breve) em outros navegadores modernos e, juntos, eles permitem que você baixe arquivos no cliente.

Tente você mesmo na demo. Clique no botão Baixar.

A demo usa o método canvas.toBlob no Internet Explorer 10 para obter a referência de arquivo da imagem desenhada dentro da marca da tela. Para Chrome e FireFox, o shim toBlob funciona de maneira ótima.

  1. canvas.toBlob(function( file ){
  2. // Create a blob file,
  3. // then download with the FileSaver
  4. }

Faça uma cópia de uma instância de Objeto Blob

Devemos poder pular esta etapa, mas, em virtude de uma peculiaridade em todos os navegadores, não se pode usar a API FileSave diretamente da instância de Blob retornada por canvas.toBlob. Você deve copiá-la.

A interface do BlobBuilder usada para criar novas referências de Blob é suportada no Internet Explorer 10, Chrome e FireFox. Mas essa interface já foi superada pelo construtor de Blob, que tem suporte limitado no momento.

Primeiro, calce prefixos de fornecedores do BlobBuilder:

  1. // Shim the BlobBuilder with the vendor prefixes
  2. window.BlobBuilder || (window.BlobBuilder = window.MSBlobBuilder||window.MozBlobBuilder||window.WebKitBlobBuilder);

Em seguida, garanta seu código para o futuro e teste o construtor de Blob. Caso contrário, construa um BlobBuilder para montar o blob. (É melhor reunir os métodos em um try-catch.) O Blob tem bugs no navegador Chrome for Android atual. Eis o código.

  1. var blob;
  2. if('Blob' in window){
  3. try{
  4. // The new Blob interface
  5.     blob = new Blob([file],{ "type" : "image\/png"});
  6.   catch(e){}
  7. }
  8. if(!blob){
  9.   try{
  10.     // The deprecated BlobBuilder interface
  11.     var bb = new BlobBuilder();
  12. bb.append( file );
  13. blob = bb.getBlob("image\/png");
  14.   }
  15.   catch(e){}
  16. }

Download do Blob com FileSaver

A API FileSaver API também é um padrão ainda a ser adotado por qualquer dos navegadores da safra atual. Entretanto, no caso do Internet Explorer 10, você pode usar a função msSaveBlob (que é fantástica) e, para o Chrome e o FireFox, pode pelo menos garanti-los para o futuro com prefixos de fornecedores. Assim, a função saveAs precisa de calços fortes:

  1. window.saveAs || (window.saveAs == window.navigator.msSaveBlob || window.webkitSaveAs || window.mozSaveAs || window.msSaveAs /** || URL Download Hack **/ );

Esse fallback (descrito em detalhes em https://gist.github.com/3552985) claça a interface FileSaver usando a URL de objeto para nossa imagem nova. Para navegadores que suportam o atributo download na marca de âncora, o shim define a href como a URL do objeto e depois despacha o evento de clique para forçá-lo a baixar ou abrir em uma guia nova. Ah, que emaranhados criamos.

Finalmente, com o blob e um nome de arquivo, o método saveAs inicia o download:

  1. var name = 'Graffiti.png';
  2. if(window.saveAs){
  3.   // Move the builder object content to a blob and 
  4. window.saveAs(blob, name);
  5. }
  6. else{
  7.   // Fallover, open as DataURL
  8. window.open(canvas.toDataURL());
  9. }

Aqui, se o shim saveAs for incompatível, o fallover abrirá uma guia nova com uma URL de dados base64. Isso funciona no Internet Explorer 9, mas o Internet Explorer 8 é limitado a tamanho de URI de dados de 32 KB.

Término

Se você ainda não brincou com a API de arquivo, recomendo que o faça. A API de arquivo abre muito potencial para a criação de aplicativos tipo área de trabalho para o navegador. Você pode querer um pouco mais de controle sobre onde salva arquivos e até mesmo sobrescrever o arquivo existente. Mas, por enquanto, há muita preocupação com segurança, então é improvável que você veja recursos como esses em breve. As especificações ainda estão em curso, mas o que destaquei neste artigo recebeu um grande investimento de fornecedores de navegadores e é improvável que mude muito, se mudar. Contudo, não me cite, por favor.

Caso precise suportar navegadores mais antigos, dê uma olhada em meu shim dropfile.js para o Internet Explorer, que calça o FileReader e cria uma URI de dados base64 e Downloadify para substituição de shim baseado em Flash para FileSaver.

Recursos

Mostrar: