16 de junho de 2011

Representando dados binários como texto em Delphi

Estou desenvolvendo para a ABC71 um projeto para automatizar o processo de implantação do nosso ERP. Parte do projeto consiste em deixar nossos consultores criar passos num tipo de assistente, aproveitando o conhecimento de campo deles para montar cenários de implantação. Ao criar um passo, eles podem até mesmo carregar uma imagem para representá-lo. Tudo isso é salvo como um arquivo XML, o que traz um problema : imagens são arquivos binários e um XML contem apenas texto.

Mesmo que eu usasse uma seção CDATA, teria que aplicar alguma transformação no conteúdo binário para que ele pudesse ser lido corretamente - certos bytes representam caracteres especiais que não podem ser exibidos, tais como o 27 (escape) e o 0 (zero sinaliza fim de uma string em C/C++).

Uma forma de converter o conteúdo binário num texto legível seria tomar cada byte e usar a representação hexadecimal dele. Por exemplo, ao invés do byte com valor 27, eu teria um texto com valor 1B. Um inconveniente salta aos olhos : como cada byte é representado por 2 caracteres, o conteúdo binário dobra de tamanho no processo de conversão !

A melhor solução que encontrei para esse problema é usar a codificação chamada Base64. Nesse algoritmo, uma sequência de bytes é processada para gerar um texto contendo apenas caracteres que podem ser exibidos, selecionados em uma lista fixa com 64 caracteres possíveis - daí o nome de Base64. Não sei se a VCL inclui uma implementação desse algoritmo; a versão que usei está disponível para download no site Koders.com. O texto resultante dessa conversão ainda é maior do que o conteúdo original mas ao menos não dobra de tamanho.

Trabalhar com a Base64 é relativamente simples. Há somente duas funções no fonte indicado: uma para codificar a informação binária (B64Encode) e outra para obter de volta a informação originalmente codificada (B64Decode). Como exemplo, o trecho de código abaixo carrega uma imagem a partir de um arquivo, codifica o conteúdo encontrado e retorna o resultado do processamento como uma string. Por ser um texto, esse resultado pode ser facilmente utilizado em outras partes do programa. No meu caso, eu o incluí como valor de uma tag num XML:
var lConteudo : String;
lSsAux : TStringStream;
lStreamOrg : TFileStream;
begin
{ Stream auxiliar para recuperar o conteúdo binário do arquivo }
lSsAux := TStringStream.Create ('');
lStreamOrg := TFileStream.Create (_ImgFilePath, fmOpenRead);

{ Copia o conteúdo do arquivo para o stream em memória }
lSsAux.CopyFrom (lStreamOrg, 0);
lSsAux.Position = 0;

{ Prepara o conteúdo binário, codificando-o como um texto }
lConteudo := lSsAux.DataString;
lConteudo := B64Encode(lConteudo);

lStreamOrg.Free();
lSsAux.Free();

Result := lConteudo;
end;

Para obter o arquivo original de volta, basta decodificar o texto preparado pelo código anterior. O exemplo que segue traz um função que recebe esse texto, decodifica-o e grava o resultado num novo arquivo com o conteúdo original intocado:
procedure TWAssitente.DecodeContentToFile (AConteudo: String);
var lStreamDst : TFileStream;
Buffer : PChar;
begin
{ Cria o novo arquivo vazio }
lStreamDst := TFileStream.Create (_ImgFilePath, fmCreate);

{ Decodifica o conteúdo }
AConteudo := B64Decode (AConteudo);

{ Grava o conteúdo decodificado no arquivo }
Buffer := PChar (AConteudo);
lStreamDst.Write (Buffer^, Length(AConteudo));
lStreamDst.Free ();
end;

Como é possível deduzir da discussão até aqui, a Base64 pode ser usada para preparar qualquer conteúdo binário e não só imagens. Isto inclui tanto arquivos (documentos, planilhas, executáveis, músicas, etc.) quanto dados contidos em arrays e streams na memória de seu programa. Até mesmo o resultado de assinaturas digitais podem passar por esse processo, como mostra o post sobre a assinatura do XML de uma nota fiscal eletrônica.

Os cálculos do algoritmo da Base64 não são complexos. Caso queira analisá-lo (ou simplesmente utilizá-lo), o fonte completo pode ser acessado através desse link.

29 comentários :

Anônimo disse...

Quero lhe dar os parabéns por compartilhar esse conhecimento. É um dos raros blogs que conheço de conteúdo sério. Já divulguei e coloquei nos meus favoritos.

Cordialmente,
Flavio Andrade.

Leonardo L Procópio disse...

Bom dia!
Gostei muito do seu artigo, como seria possivel converter imagens para base64?
Grande abraço, e parabéns!

Luís Gustavo Fabbro disse...

Leonardo

Se a sua imagem está num arquivo, é só seguir o próprio exemplo do post. Mas, se o que você tem é uma instância de TImage, você pode salvá-la num stream que substituirá o File Stream que há no exemplo do post :

/* Converte uma imagem no TImage para Base64 */
Image1.Picture.Graphic.SaveToStream(lSsAux);
lSsAux.Position = 0;
lConteudo := lSsAux.DataString;
lConteudo := B64Encode(lConteudo);

Leonardo L Procópio disse...

Salvando o arquivo externo eu consegui, mas pegando do TImage não funcionou, ele gera uma codificação com simbolos, sabe o que poderia ser?
Grande abraço!

Luís Gustavo Fabbro disse...

O que você quer dizer com "símbolos" ? O importante é que o resultado da codificação contenha apenas os caracteres alphanuméricos da Base64. Esse resultado, quando decodificado, deve restaurar o conteúdo original.

Essas características estão sendo respeitadas ? Senão, poste o seu código (ou envie para o email do balaio).

wilsongsjunior disse...

Olá Luís, a partir do seu texto resolvi buscar alternativas para conversão de imagem em base64 no ASP. Consegui um resultado satisfatório e compartilho-o abaixo. Contudo, voltando ao seu texto, você escreve: "eu o incluí como valor de uma tag num XML". Poderia me dizer como é montada esta tag? Existe alguma configuração diferente?
Segue abaixo a solução que encontrei para ASP. Obrigado. Wilson Junior, Rio de Janeiro.


Function base64_encode_fromfile( byval sFilename )
Dim objXMLDoc, objDocElem, objStream, sBase64String
Set objXMLDoc = Server.CreateObject("MSXML2.DOMDocument")
objXMLDoc.async = False
objXMLDoc.validateOnParse = False
Set objStream = Server.CreateObject("ADODB.Stream")
objStream.Type = 1
objStream.Open
'objStream.LoadFromFile Server.MapPath("/images/"&sFilename)
if err.Number <> 0 then exit function
Set objDocElem = objXMLDoc.createElement("A")
objDocElem.dataType = "bin.base64"
objDocElem.nodeTypedValue = objStream.Read
sBase64String = objDocElem.text
objStream.Close
Set objStream = Nothing
Set objDocElem = Nothing
Set objXMLDoc = Nothing
base64_encode_fromfile = sBase64String
End Function

Luís Gustavo Fabbro disse...

Wilson

Não há nenhuma configuração diferente para incluir num XML a codificação em Base64 de imagens (ou qquer outro conteúdo binário).

Como o resultado da codificação é um texto, basta atribuir esse texto como valor para a tag, que ficaria mais ou menos como segue :

<minhaImg>iVBORw0KGgoAAAANSUhEUgAAADIA...</minhaImg>

Daniel Azevedo disse...

Muito bom seu artigo. parabens
fiz alguns testes utilizando o delphi 7 e funcionou. Agora utilizando o delphi XE nao funciona. Sera q pode ser alguma coisa relacionada ao tipo String? Estou querendo usar para trafegar imagens no formato json.

Luís Gustavo Fabbro disse...

Não me lembro a partir de qual versão o Delphi assumiu como padrão para string o tipo UnicodeString e como padrão para char o tipo UnicodeChar. Isso significa que cada caractere nesse ambiente é representado por 2 bytes.

O código para Base64 usado no post não está preparado para lidar com isso : ele espera que um caractere caiba num byte.

A melhor solução é preparar o Base64.pas para o novo ambiente, trocando os tipos de dados usados por ele pelos tipos que ele efetivamente espera. Ou seja, troque nesse fonte o que for string para AnsiString e o que for Char para AnsiChar.

Outra solução seria forçar seu projeto a trabalhar com AnsiString e AnsiChar, tipos onde cada caractere cabe num byte. Mas essa solução pode ter outras implicações para seu projeto, principalmente se ele também for dirigido a usuários chineses, japoneses ou outros cujas línguas exijam o uso de caracteres Unicode.

[]s

Daniel Azevedo disse...

Ola luiz.. infelizmente nao deu certo. qudo troco o tipo char por AnsiChar da erro na linha
OutBuf[0]:= B64Table[((InBuf[0] and $FC) shr 2) + 1];

o que eu precisava mesmo era trafegar uma imagem usando json... mas ta dificil

Luís Gustavo Fabbro disse...

Daniel

A constante B64Table também é um texto. É provável que você não tenha alterado a declaração dela para forçá-la a ser AnsiString. Ela deve estar declarada como segue:

const
  B64Table: AnsiString = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

Se não fizer isso, ela será por padrão do tipo UnicodeString, com cada caractere ocupando 2 bytes, tornando o cálculo binário inválido.

Daniel Azevedo disse...

Luiz agradeço pela ajuda... mas nao deu certo. Parou de dar erro mas a imagem decodificada nao carrega... fica uma imagem invalida.. de qualquer forma vou aki vai o codigo que estou usando e funcionando para o Delphi XE, mas so serve para imagem JPG... Obrigado e espero contribuir de alguma forma..

//======= Transforma Jpg em String
function Jpeg_em_texto(Local_imagem : string) : string;
var Foto_Memoria : TStringStream;
begin
Result := '';

if FileExists(Local_imagem) = False then
Exit;

//========= salva a foto na memoria
Foto_Memoria := TStringStream.Create;
Foto_Memoria.LoadFromFile(Local_imagem);

//========= Transforma a foto em string
Result := Foto_Memoria.DataString;

Foto_Memoria.Destroy;
end;


//========== transforma a string em Jpeg procedure TfrmPrincipal.CarregaFoto(Foto_String: string);
var NovaImagem : TJPEGImage;
Imagem_na_memoria : TMemoryStream;
Foto : TStringStream;
begin

//============ Decodifica uma string para o formato de imagem
Foto := TStringStream.Create;
Imagem_na_memoria := TMemoryStream.Create;
NovaImagem := TJPEGImage.Create;

//Carrega a foto de uma string
Foto.WriteString(Foto_String);
foto.SaveToStream(Imagem_na_memoria);
Imagem_na_memoria.Position :=0;
NovaImagem.LoadFromStream(Imagem_na_memoria);

//======= salva a imagem na area de transferencia para salvar no Cliente DataSet
clipboard.Assign(NovaImagem);

cdsInner.Edit;
DBImage1.PasteFromClipboard;
cdsInner.Post;

Foto.Destroy;
Imagem_na_memoria.Destroy;
NovaImagem.Destroy;
end;

Unknown disse...

Pessoal, para quem não está conseguindo, fiz como no post e também não deu certo, mas dando uma olhada no código e com a ajuda de nosso amigo google, consegui da seguinte maneira utilizando o delphi 2010:

procedure Decodificar(AConteudo : AnsiString; SaveAs: String);
var
lStreamDst: TFileStream;
Buffer: PChar;
begin
{ Cria o novo arquivo vazio }
lStreamDst := TFileStream.Create(SaveAs, fmCreate);

{ Decodifica o conteúdo }
AConteudo := DecodeBase64(AConteudo);

{ Grava o conteúdo decodificado no arquivo }
Buffer := PChar(AConteudo);
lStreamDst.Write(Buffer^, Length(AConteudo));
lStreamDst.Free();
end;

function Codificar(Arquivo: String): AnsiString;
var
lConteudo: String;
lSsAux: TStringStream;
lStreamOrg: TFileStream;
begin
{ Stream auxiliar para recuperar o conteúdo binário do arquivo }
lSsAux := TStringStream.Create('');
lStreamOrg := TFileStream.Create(Arquivo, fmOpenRead);

{ Copia o conteúdo do arquivo para o stream em memória }
lSsAux.CopyFrom(lStreamOrg, 0);
lSsAux.Position := 0;

{ Prepara o conteúdo binário, codificando-o como um texto }
lConteudo := lSsAux.DataString;
lConteudo := EncodeBase64(lConteudo);

lStreamOrg.Free();
lSsAux.Free();

Result := lConteudo;
end;

Unknown disse...

Pessoal, para quem não está conseguindo, fiz como no post e também não deu certo, mas dando uma olhada no código e com a ajuda de nosso amigo google, consegui da seguinte maneira utilizando o delphi 2010:

procedure Decodificar(AConteudo : AnsiString; SaveAs: String);
var
lStreamDst: TFileStream;
Buffer: PChar;
begin
{ Cria o novo arquivo vazio }
lStreamDst := TFileStream.Create(SaveAs, fmCreate);

{ Decodifica o conteúdo }
AConteudo := DecodeBase64(AConteudo);

{ Grava o conteúdo decodificado no arquivo }
Buffer := PChar(AConteudo);
lStreamDst.Write(Buffer^, Length(AConteudo));
lStreamDst.Free();
end;

function Codificar(Arquivo: String): AnsiString;
var
lConteudo: String;
lSsAux: TStringStream;
lStreamOrg: TFileStream;
begin
{ Stream auxiliar para recuperar o conteúdo binário do arquivo }
lSsAux := TStringStream.Create('');
lStreamOrg := TFileStream.Create(Arquivo, fmOpenRead);

{ Copia o conteúdo do arquivo para o stream em memória }
lSsAux.CopyFrom(lStreamOrg, 0);
lSsAux.Position := 0;

{ Prepara o conteúdo binário, codificando-o como um texto }
lConteudo := lSsAux.DataString;
lConteudo := EncodeBase64(lConteudo);

lStreamOrg.Free();
lSsAux.Free();

Result := lConteudo;
end;

Valeu pelo excelento blog. Espero ter ajudado.

Unknown disse...

Faltou que deve ser declarada a uses synacode.

Pixel Perfect Beauties disse...

Muito obrigado, resolvi meu problema com este post ;)

Anônimo disse...

Pessoal, como eu recupero stream do Excel?

Luís Gustavo Fabbro disse...

O que você chama de "stream" do excel? Se for o conteúdo completo de uma planilha, carregue com um TFileStream o próprio arquivo com a planilha salva.

[]s

Anônimo disse...

bem simples mas porem me ajudou na mosca! obrigado

Unknown disse...

Olá Luís,

Gostei muito do post, sempre vejo os assuntos do Balaio Tecnológico e são sempre muito bons.
Teria como disponibilizar novamente o aquivo Base64.pas pois o link para download está com erro.

Obrigado

Luís Gustavo Fabbro disse...

Yrakitan

O site Koders se fundiu ao Ohloh e deixou de publicar projetos. Há uma versão do fonte base64.pas neste endereço mas vc pode pesquisar o site Ohloh.net pra encontrar outras.

[]s

Unknown disse...

Olá Luís,

Obrigado por compartilhar seu conhecimento, me salvou :)
O próprio Delphi possui uma unit para Encode e Decode, acrescentei no uses EncdDecd e utilizei as funções EncodeString e DecodeString

Unknown disse...

Olá Luís, obrigado por compartilhar seu conhecimento :)
Como o arquivo base64 não estava disponível, fucei o google para procurar algo nativo para encode e decode, encontrei no stackoverflow uma dica.
Basta acrescentar no uses EncdDecd e utilizar os métodos EncodeString e DecodeString.

Obrigado!

Luís Gustavo Fabbro disse...

Dyego

Obrigado pela dica. A Embarcadero vem fazendo um bom trabalho com o Delphi/C++ Builder, acrescentando recursos e funcionalidades à ferramenta; como efeito colateral, muitos bons recursos acabam ficando escondidos e só mesmo "fuçando" é que acabamos descobrindo-os.

[]s

Unknown disse...

olá bom dia, muito bom o post, mais eu gostaria de saber como fazer pra decodificar um captcha ? por exemplo: eu tenho uma captcha num componente timage com as letras: asgo, eu quero uma função que retorne digitado num edit as mesmas letras do captcha, eu quero resolver o captcha automaticamente sabe me dizer como ?

Luís Gustavo Fabbro disse...

Diron

O objetivo do CAPTCHA é justamente tornar impossível automatizar o acesso a um recurso (página, arquivo, operação, etc.). Isto é, somente um humano deveria ser capaz de olhar pra imagem e extrair o texto contido nela; mas nunca um programa ou script.

Se é seu próprio programa que está gerando a imagem, a abordagem correta é armazenar sob seu controle tanto o texto em si quanto a imagem gerada a partir dele.

[]s

Anônimo disse...

Opa Luis Gustavo,

você possui o arquivo Base64.pas funcional, porque os que encontrei na internet não funcionam.

Se você tiver, fico grato.

Grande abraço.

Luís Gustavo Fabbro disse...

Fernando

Veja o comentário do Dyego Brito; ele encontrou as funções necessárias no próprio Delphi. Basta incluir na cláusula uses a unit EncdDecd e utilizar os métodos EncodeString e DecodeString.

Caso esteja usando uma versão muito antiga do Delphi, o fonte Base64.pas pode ser baixado no site Ohloh - veja aqui.

[]s

Anônimo disse...

No delphi Xe3 consegui dessa forma:
procedure Decode64(AConteudo : AnsiString; SaveAs: String);
var
lStreamDst: TFileStream;
Buffer: PChar;
begin
{ Cria o novo arquivo vazio }
lStreamDst := TFileStream.Create(SaveAs, fmCreate);
{ Decodifica o conteúdo }

AConteudo:=DecodeString(AConteudo);

//Grava o conteúdo decodificado no arquivo }
Buffer := PChar(AConteudo);
lStreamDst.Write(Buffer^, Length(AConteudo));
lStreamDst.Free();

end;

function EncodeB64(_ImgFilePath:string): AnsiString;
var
lConteudo: String;
lSsAux: TStringStream;
lStreamOrg: TFileStream;
begin
{ Stream auxiliar para recuperar o conteúdo binário do arquivo }
lSsAux := TStringStream.Create('');
lStreamOrg := TFileStream.Create(_ImgFilePath, fmOpenRead or fmShareDenyWrite);

{ Copia o conteúdo do arquivo para o stream em memória }
lSsAux.CopyFrom(lStreamOrg, 0);
lSsAux.Position := 0;

{ Prepara o conteúdo binário, codificando-o como um texto }
lConteudo := lSsAux.DataString;

lConteudo:=EncodeString(lConteudo);

lStreamOrg.Free();
lSsAux.Free();

Result := lConteudo;
end;

Luis muito obrigado por compartilhar o seu conhecimento

Postar um comentário

OBS: Os comentários enviados a este Blog são submetidos a moderação. Por isso, eles serão publicados somente após aprovação.

Observação: somente um membro deste blog pode postar um comentário.