Imagens no SGDB via DBAccess

Introdução

Recebi um e-mail, ou mensagem, ou post (não lembro agora) com uma sugestão interessante, para este tema (Imagens no SGDB) fosse abordado aqui no Blog. E, é totalmente possível de ser feito, de forma relativamente simples.

Imagens, por dentro

Um arquivo em disco que contém uma imagem pode ser salvo em diversos formatos: BMP (Bitmap Image File), JPEG (Joint Photographic Experts Group), PNG (Portable Network Graphics), entre outros. Cada formato consiste em uma especificação para a representação binária de uma imagem. Trocando em miúdos, um arquivo de imagem contém um determinado número de bytes, usando códigos ASCII de 0 a 255 (conteúdo binário), que são interpretados por uma aplicação capaz de mostrar seu conteúdo em uma interface. Cada tipo de imagem possui algumas características, como resolução, compressão, mas por dentro são apenas uma sequência de bytes.

Imagens no SGDB

Existem bancos de dados que possuem um tipo de campo próprio para imagens, como o Microsoft SQL Server (campo image), mas a grosso modo praticamente todos os bancos de dados comerciais possuem um tipo de campo conhecido por “BLOB” (Binary Large OBject), capaz de suportar conteúdo binário.

Acesso pelo DBAccess

Como é de conhecimento de todos que trabalham com o ERP Microsiga, todo o accesso a Banco de Dados relacional no Protheus é feito através do DBAccess, um gateway de acesso para bancos relacionais, que também é capaz de emular o acesso ISAM, ainda usado por boa parte do código legado do ERP.

O DBAccess não permite acesso direto a campos IMAGE, BLOB ou CLOB, mas internamente ele se utiliza destes campos para emular o campo do tipo “M” memo do AdvPL. Logo, para nos utilizarmos destes tipos de campo, devemos criar uma tabela no SGDB usando o tipo de campo “M” (Memo) do AdvPL.

Atenção, no ERP existe o conceito de campo Memo virtual, criado no dicionário de dados do ERP (SX3), que na prática utiliza um arquivo auxiliar (SYP) na Base de Dados principal, com acesso através de uma API Advpl, ao qual esse exemplo não se aplica. O campo Memo que será criado é um Memo “real” no SGDB.

Características e Limites

O AdvPL possui um limite de 1MB de tamanho máximo de String, logo ainda não é possível armazenar no SGDB uma imagem maior que isso. E, como o acesso ao conteúdo do campo é feito pelo DBAccess, não é possível fazer uma Query que recupere diretamente o conteúdo de um campo BLOB, CLOB ou IMAGE.

Para acessar o conteúdo de um campo “M” Memo criado em uma tabela, devemos abrir a tabela no AdvPL usando DbUseArea() — ou ChkFile(), para uma tabela de dados do ERP –, posicionar no registro desejado e ler o valor do campo do registro atual através da expressão cVARIAVEL := ALIAS->CAMPOMEMO, e o DBAccess irá fazer uma requisição exclusiva para trazer o conteúdo deste campo e colocá-lo na variável de memória.

Adicionalmente, o campo “M” Memo originalmente no AdvPL foi projetado para suportar apenas 64 KB de dados, e somente conseguimos aumentar esse limite para 1MB habilitando a configuração TOPMEMOMEGA=1 na configuração do environment desejado no arquivo de configuração do TOTVS Application Server (appserver.ini) — Vide TDN, no link http://tdn.totvs.com/pages/viewpage.action?pageId=6065746 )

Como as imagens gravadas costumam ser bem maiores que os registros gravados em tabelas de dados do ERP, deve-se tomar cuidado quando a leitura destes campos for realizada por muitos processos simultaneamente, isto pode gerar um gargalo na camada de rede entre as aplicações TOTVS Application Server, DBACcess e o SGDB.

E, existem alguns bancos de dados homologados que por default não se utilizam de campos BLOB ou similares para armazenar os dados de campo “M” Memo. Para ter certeza que a implementação vai funcionar em todos os bancos homologados, podemos limitar o tamanho da imagem em 745 KB, e converter o buffer binário da imágem para BASE64, onde são usadas strings de texto normal para a representação dos dados, e fazer as conversões em memória para Ler e Gravar o buffer binário.

Mãos à obra

Basicamente, armazenar uma imagem no SGDB requer no mínimo 2 campos na tabela de imagens: Um campo caractere, identificador único da imagem, indexado, e um campo “M” Memo do AdvPL, para armazenar a imagem. Podemos encapsular isso em uma classe — vamos chamá-la ApDbImage() — e implementar os métodos de leitura e gravação, manutenção e status, além de dois métodos adicionais para ler imagens de um arquivo no disco para a memória, e gravar a imagem da memória para o disco.

A classe APDBIMAGE() foi implementada com este propósito, mas ela têm ainda alguns detalhes adicionais, ela guarda um HASH MD5 gerado a partir da imagem original, um campo separado para o tipo da imagem, e permite as operações básicas de inclusão, leitura, alteração e exclusão.

Exemplo de uso e fontes

O programa de exemplo funciona como um manager simples de imagens, permitindo abrir os formatos suportados de imagens do disco, para serem mostrados na tela, ou do próprio repositório, ou também da tabela de imagens do Banco de Dados. Uma vez visualizada uma imagem na interface, ela pode ser gravada em disco (ou exportada), no mesmo formato que foi aberto — o programa não realiza conversões — , e também pode ser inserida no DBimage com um nome identificador qualquer, ou usada para alterar uma imagem já existente na tabela de imagens.

O fonte da classe APDBImage() pode ser baixado no link https://github.com/siga0984/Blog/blob/master/ApDBImage.prw , e o fonte de exemplo que usa a classe está no link https://github.com/siga0984/Blog/blob/master/TSTDBIMG.prw , ambos no GitHub do Blog ( https://github.com/siga0984/blog ). Basta baixar os fontes, compilá-los com o IDE ou o TDS, e executar a função U_TSTDBIMG para acionar o programa de testes da classe ApDbImage().

E, para os curiosos e ávidos por código, segue o fonte da Classe APDBIMAGE logo abaixo:

#include "protheus.ch"
/* ---------------------------------------------------
Classe ApDBImage
Autor Júlio Wittwer
Data 27/02/2015 
Versão 1.150308
Descrição Classe para encapsular leitura e gravação de 
 imagens em tabela do SGDB através do DBACCESS
Observação
Como apenas o banco MSSQL aceita conteúdo binário ( ASCII 0 a 255 )
para campos MEMO, e os bancos ORACLE e DB2 ( quando usado BLOB ), 
para servir para todos os bancos, a imagem é gravada no banco 
usando Encode64 -- para converter conteúdo binário em Texto 
codificado em Base64, a maior imagem nao pode ter mais de 745000 bytes
Referências
http://tdn.totvs.com/display/tec/Acesso+ao+banco+de+dados+via+DBAccess
http://tdn.totvs.com/pages/viewpage.action?pageId=6063692
http://tdn.totvs.com/display/tec/Encode64
http://tdn.totvs.com/display/tec/Decode64
--------------------------------------------------- */
#define MAX_IMAGE_SIZE 745000
CLASS APDBIMAGE
// Propriedades
 DATA bOpened 
 DATA cError
// Métodos 
 METHOD New() 
 METHOD Open()
 METHOD Close() 
 METHOD ReadStr( cImgId , /* @ */ cImgType , /* @ */ cImgBuffer ) 
 METHOD Insert( cImgId , cImgType , /* @ */ cImgBuffer ) 
 METHOD Update( cImgId , cImgType , /* @ */ cImgBuffer ) 
 METHOD Delete( cImgId ) 
 METHOD Status()
// Metodos de acesso de imagens no disco
 METHOD LoadFrom( cFile, cImgBuffer )
 METHOD SaveTo( cFile, cImgBuffer )
 
ENDCLASS
/* ---------------------------------------------------------
Construtor da classe de Imagens no SGDB
Apenas inicializa propriedades
-------------------------------------------------------- */
METHOD New() CLASS APDBIMAGE
::bOpened := .F.
::cError := ''
Return self
/* ---------------------------------------------------------
Abre a tabela de imagens no SGDB
Conecta no DBAccess caso nao haja conexão
--------------------------------------------------------- */
METHOD Open( ) CLASS APDBIMAGE
Local nDBHnd := -1
Local aStru := {}
Local cOldAlias := Alias()
::cError := ''
IF ::bOpened 
 // Ja estava aberto, retorna direto
 Return .T.
Endif
If !TcIsConnected() 
 // Se não tem conexão com o DBAccess, cria uma agora
 // Utiliza as configurações default do appserver.ini
 nDBHnd := tcLink()
 If nDBHnd < 0
 ::cError := "TcLink() error "+cValToChar(nDbHnd)
 Return .F.
 Endif
Endif
If !TCCanOpen("ZDBIMAGE")
 
 // Cria array com a estrutura da tabela
 aAdd(aStru,{"ZDB_IMGID" ,"C",40,0})
 aAdd(aStru,{"ZDB_TYPE" ,"C",3,0}) // BMP JPG PNG 
 aAdd(aStru,{"ZDB_HASH" ,"C",32,0}) 
 aAdd(aStru,{"ZDB_SIZE" ,"N",8,0})
 aAdd(aStru,{"ZDB_MEMO" ,"M",10,0})
// Cria a tabela direto no SGDB
 DBCreate("ZDBIMAGE",aStru,"TOPCONN")
 
 // Abre em modo exclusivo para criar o índice de ID
 USE ("ZDBIMAGE") ALIAS ZDBIMAGE EXCLUSIVE NEW VIA "TOPCONN"
 
 If NetErr()
 ::cError := "Failed to open [ZDBIMAGE] on EXCLUSIVE Mode"
 Return
 Endif
 
 // Cria o índice por ID da imagem 
 INDEX ON ZDB_IMGID TO ("ZDBIMAGE1")
 
 // Fecha a tabela
 USE
 
Endif
 
// Abre em modo compartilhado
USE ("ZDBIMAGE") ALIAS ZDBIMAGE SHARED NEW VIA "TOPCONN"
If NetErr()
 ::cError := "Failed to open [ZDBIMAGE] on SHARED Mode"
 Return .F.
Endif
DbSetIndex("ZDBIMAGE1")
DbSetOrder(1)
::bOpened := .T.
If !Empty(cOldAlias) .and. Select(cOldAlias) > 0
 DbSelectArea(cOldAlias)
Endif
Return ::bOpened
/* ---------------------------------------------------------
Le uma imagem do banco para a memoria
recebe o nome da imgem, retorna por referencia o tipo
da imagem e seu conteudo 
-------------------------------------------------------- */
METHOD ReadStr( cImgId , /* @ */cImgType, /* @ */ cImgBuffer ) CLASS APDBIMAGE
::cError := ''
If !::bOpened
 ::cError := "APDBIMAGE:ReadStr() Error: Instance not opened."
 Return .F.
Endif
If empty(cImgId)
 ::cError := "APDBIMAGE:ReadStr() Error: ImageId not specified."
 Return .F. 
Endif
cImgId := Lower(cImgId)
If !ZDBIMAGE->(DbSeek(cImgId))
 ::cError := "APDBIMAGE:ReadStr() ImageId ["+cImgId+"] not found."
 Return .F.
Endif
// Caso a imagem com o ID informado seja encontrada
// Carrega o buffer da imagem para a variável de memória
cImgBuffer := Decode64(ZDBIMAGE->ZDB_MEMO)
cImgType := ZDBIMAGE->ZDB_TYPE
Return .T.
/* ---------------------------------------------------------
Insere uma imagem na tabela de imagens do SGDB
Recebe o ID da imagem, o tipo e o buffer 
-------------------------------------------------------- */
METHOD Insert( cImgId , cImgType, cImgBuffer ) CLASS APDBIMAGE
Local bOk := .F.
::cError := ''
If !::bOpened
 ::cError := "APDBIMAGE:Insert() Error: Instance not opened."
 Return .F. 
Endif
If empty(cImgId)
 ::cError := "APDBIMAGE:Insert() Error: ImageId not specified."
 Return .F. 
Endif
If empty(cImgType)
 ::cError := "APDBIMAGE:Insert() Error: ImageType not specified."
 Return .F. 
Endif
cImgId := Lower(cImgId)
cImgType := Lower(cImgType)
If !ZDBIMAGE->(DbSeek(cImgId))
 // Se a imagem não existe, insere
 ZDBIMAGE->(DBAppend(.T.))
 ZDBIMAGE->ZDB_IMGID := cImgId
 ZDBIMAGE->ZDB_TYPE := cImgType
 ZDBIMAGE->ZDB_SIZE := len(cImgBuffer)
 ZDBIMAGE->ZDB_HASH := Md5(cImgBuffer,2) // Hash String Hexadecimal
 ZDBIMAGE->ZDB_MEMO := Encode64(cImgBuffer)
 ZDBIMAGE->(DBRUnlock())
 bOk := .T.
else
 ::cError := 'Image Id ['+cImgId+'] already exists.'
Endif
Return bOk
/* ---------------------------------------------------------
Atualiza uma imagem ja existente no banco de imagens
Recebe ID, tipo e buffer
-------------------------------------------------------- */
METHOD Update( cImgId , cImgType, cImgBuffer ) CLASS APDBIMAGE
::cError := ''
If !::bOpened
 ::cError := "APDBIMAGE:Update() Error: Instance not opened."
 Return .F. 
Endif
If empty(cImgId)
 ::cError := "APDBIMAGE:Update() Error: ImageId not specified."
 Return .F. 
Endif
If empty(cImgType)
 ::cError := "APDBIMAGE:Update() Error: ImageType not specified."
 Return .F. 
Endif
cImgId := Lower(cImgId)
cImgType := Lower(cImgType)
 
If !ZDBIMAGE->(DbSeek(cImgId))
 ::cError := 'Image Id ['+cImgId+'] not found.'
 Return .F.
Endif
// Se a imagem existe, atualiza
IF !ZDBIMAGE->(DbrLock(recno()))
 ::cError := 'Image Id ['+cImgId+'] update lock failed.'
 Return .F.
Endif
ZDBIMAGE->ZDB_TYPE := cImgType
ZDBIMAGE->ZDB_SIZE := len(cImgBuffer)
ZDBIMAGE->ZDB_HASH := MD5(cImgBuffer,2) // Hash String Hexadecimal
ZDBIMAGE->ZDB_MEMO := Encode64(cImgBuffer)
ZDBIMAGE->(DBRUnlock())
Return .T.
/* ---------------------------------------------------------
Deleta fisicamente uma imagem da Tabela de Imagens
-------------------------------------------------------- */
METHOD Delete( cImgId , lHard ) CLASS APDBIMAGE
Local nRecNo
::cError := ''
If !::bOpened
 ::cError := "APDBIMAGE:Delete() Error: Instance not opened."
 Return .F. 
Endif
If empty(cImgId)
 ::cError := "APDBIMAGE:Delete() Error: ImageId not specified."
 Return .F. 
Endif
If !ZDBIMAGE->(DbSeek(cImgId))
 ::cError := 'Image Id ['+cImgId+'] not found.'
 Return .F.
Endif
// Se a imagem existe, marca o registro para deleção
nRecNo := ZDBIMAGE->(recno())
// Mesmo que a deleção seja fisica, eu garanto 
// o lock do registro na camada do dbaccess
If !ZDBIMAGE->(DbrLock(nRecNo))
 ::cError := 'Image Id ['+cImgId+'] delete lock failed.'
 Return .F.
Endif
// Deleta fisicamente no SGBD
nErr := TcSqlExec("DELETE FROM ZDBIMAGE WHERE R_E_C_N_O_ = " + cValToChar(nRecNo) )
If nErr < 0
 ::cError := 'Image Id ['+cImgId+'] delete error: '+TcSqlError()
Endif
// Solto o lock do registro no DBAccess
ZDBIMAGE->(DBRUnlock())
Return .T.
/* ---------------------------------------------------------
Fecha a tabela de imagens
-------------------------------------------------------- */
METHOD Close() CLASS APDBIMAGE
If Select('ZDBIMAGE') > 0
 ZDBIMAGE->(DbCloseArea())
Endif
::cError := '' 
::bOpened := .F.
Return .T.
/* ---------------------------------------------------------
Metodo Status()
Classe APDBIMAGE
Descrição Monta array por referencia contendo as informações da base 
 de imagens: Quantidade de registros total, tamanho estimado 
 total das imagens, quantidade de registros marcados para 
 deleção e tamanho estimado de imagens marcadas para deleçao 
-------------------------------------------------------- */
METHOD Status( /* @ */ aStat ) CLASS APDBIMAGE
Local cOldAlias := Alias()
Local cQuery 
Local nCountAll := 0
Local nSizeAll := 0
::cError := '' 
aStat := {}
If !::bOpened
 ::cError := "APDBIMAGE:Status() Error: Instance not opened."
 Return .F. 
Endif
// Conta quantas imagens tem na tabela, por tipo 
cQuery := "SELECT ZDB_TYPE, count(*) AS TOTAL"+;
 " FROM ZDBIMAGE GROUP BY ZDB_TYPE ORDER BY ZDB_TYPE"
 
USE (TcGenQry(,,cQuery)) ALIAS QRY EXCLUSIVE NEW VIA "TOPCONN"
While !eof()
 aadd(aStat , {"TOTAL_COUNT_"+QRY->ZDB_TYPE,QRY->TOTAL})
 nCountAll += QRY->TOTAL
 DbSkip()
Enddo
USE
// Acrescenta total de imagens
aadd(aStat , {"TOTAL_COUNT_ALL",nCountAll})
 
// Levanta o total de bytes usados por tipo de imagem
cQuery := "SELECT ZDB_TYPE, SUM(ZDB_SIZE) AS TOTAL"+;
 " FROM ZDBIMAGE GROUP BY ZDB_TYPE ORDER BY ZDB_TYPE"
 
USE (TcGenQry(,,cQuery)) ALIAS QRY EXCLUSIVE NEW VIA "TOPCONN"
While !eof()
 aadd(aStat , {"TOTAL_SIZE_"+QRY->ZDB_TYPE,QRY->TOTAL})
 nSizeAll += QRY->TOTAL
 DbSkip()
Enddo
USE
// Acrescenta total de bytes usados 
aadd(aStat , {"TOTAL_SIZE_ALL",nSizeAll})
If !Empty(cOldAlias)
 DbSelectArea(cOldAlias)
Endif
Return .T.
/* ---------------------------------------------------------
Ler um arquivo de imagem do disco para a memoria
Nao requer que a instancia esteja inicializada / Aberta
--------------------------------------------------------- */
METHOD LoadFrom( cFile, /* @ */ cImgBuffer ) CLASS APDBIMAGE
Local nH, nSize, nRead
::cError := ''
If !file(cFile)
 ::cError := "APDBIMAGE:LoadFrom() Error: File ["+cFile+"]not found."
 Return .F. 
Endif
nH := Fopen(cFile,0)
If nH == -1 
 ::cError := "APDBIMAGE:LoadFrom() File Open Error ( FERROR "+cValToChar( Ferror() )+")" 
 Return .F. 
Endif
nSize := fSeek(nH,0,2)
fSeek(nH,0)
If nSize <= 0 
 ::cError := "APDBIMAGE:LoadFrom() File Size Error : Empty File" 
 fClose(nH)
 Return .F. 
Endif
If nSize > MAX_IMAGE_SIZE
 ::cError := "APDBIMAGE:LoadFrom() File TOO BIG ("+ cValToChar(nSize) +" bytes)" 
 fClose(nH)
 Return .F. 
Endif
// Aloca buffer para ler o arquivo do disco 
// e le o arquivo para a memoria
cImgBuffer := space(nSize)
nRead := fRead(nH,@cImgBuffer,nSize)
// e fecha o arquivo no disco 
fClose(nH)
If nRead < nSize
 cImgBuffer := ''
 ::cError := "APDBIMAGE:LoadFrom() Read Error ( FERROR "+cValToChar( Ferror() )+")" 
 Return .F. 
Endif
Return .T.
/* ---------------------------------------------------------
Gravar um arquivo de imagem no disco a partir de uma imagem na memoria
Nao requer que a instancia esteja inicializada / Aberta
--------------------------------------------------------- */
METHOD SaveTo( cFile, cImgBuffer ) CLASS APDBIMAGE
Local nH, nSize , nSaved 
::cError := ''
If file(cFile)
 ::cError := "APDBIMAGE:SaveTo() Error: File ["+cFile+"] alreay exists."
 Return .F. 
Endif
// Cria o arquivo no disco 
nH := fCreate(cFile)
If nH == -1 
 ::cError := "APDBIMAGE:SaveTo() File Create Error ( FERROR "+cValToChar( Ferror() )+")" 
 Return .F. 
Endif
 
// Calcula tamanho do buffer de memoria
// e grava ele no arquivo 
nSize := len(cImgBuffer)
nSaved := fWrite(nH,cImgBuffer)
// Fecha o arquivo 
fClose(nH)
If nSaved < nSize
 ::cError := "APDBIMAGE:SaveTo() Write Error ( FERROR "+cValToChar( Ferror() )+")" 
 Return .F. 
Endif
Return .T.
Conclusão

Esta classe é só um esboço, com alguns parafusos a mais ela pode ser usada para construir um assistente para, por exemplo, importar uma pasta cheia de imagens para o banco de dados, dando o nome das imagens automaticamente baseado no nome do arquivo original, e o fato dela gerar o MD5 Hash a partir do buffer binário original pode permitir uma busca mais rápida por imagens idênticas repetidas dentro do banco, fazendo apenas uma Query para mostrar quais os ImageID´s que possuem o mesmo HASH !!!

Pessoal, novamente agradeço a audiência, espero que gostem do Post. Já tenho alguma coisa no forno para os próximos posts, mas continuo aceitando sugestões !! Até o próximo post, pessoal 😉

15 respostas em “Imagens no SGDB via DBAccess

    • Olá Allan, boa noite,

      Entrei no site e dei uma passada geral nos tópicos, o site está muito bem diagramado, e com um conteúdo votado ao Produto ERP. #recomendo #parabéns

      Parabéns cara 😉

      Abraços

      Curtir

  1. Um colega perguntou como enviar imagens via Web Services. Logo pensei que seria difícil, mas depois de pesquisar um pouco e ler seu artigo, percebi o quão simples e descomplicado é.

    Tire uma dúvida: O “encapsulamento” de imagens no RPO (exemplo ícones) segue o mesmo conceito?

    Obrigado Júlio!

    Curtido por 1 pessoa

    • Olá Caio, boa noite. Vejamos … as imagens no Repositório do ERP são armazenadas como “recursos” no RPO. Isto é, elas são acrescentadas em um projeto AdvPL através do IDE ou TDS, e no momento de compilação, elas são gravadas dentro do Repositório de Objetos. Você somente consegue inserir as imagens desta forma, e consegue recuperar as imagens gravadas no repositório em tempo de execução de código, através do nome delas, usando por exemplo a função GetApoRes() — documentada no TDN no link http://tdn.totvs.com/display/tec/GetApoRes 😀

      Espero ter ajudado ! Grande abraço e obrigado pela audiência !!

      Curtir

  2. Isso se aplica somente para imagem, ou pode ser para qualquer tipo de arquivo? Pergunto isso pois estou com necessidade de criar um WebService que disponibilize o que estiver no Banco de Conhecimento relacionado a tabela de produto por exemplo. Ou seja, o arquivo pode ser um PDF, DOC, XLS, etc…

    Curtido por 1 pessoa

    • Olá Evaldo, bom dia ! Isto se aplica a qualquer arquivo com conteúdo binário, de qualquer extensão. As questões são apenas a limitação de tamanho do campo “M” (memo) do DBAccess, e a limitação do tamanho de String do AdvPl, para recepção e retornos de Web Services.

      Abraços 😉

      Curtir

  3. Ola,
    Muito bom o conteúdo desse post, me ajudou muito.

    Apenas gostaria de tirar uma duvida.

    Estou gravando varias imagens
    no BD e enviando para um ftp, e percebi que a funcao Encode64 eh lenta,
    demora quase 1 segundo para executar, sao imagens com tamanho em torno de 300Kb.
    Existe alguma outra funcao que poderia ser utilizada para codificar a imagem para binario
    no qual a execucao fosse mais rapida ?

    Att

    Curtido por 1 pessoa

    • Rapaz, eu fiquei sabendo que tinha um problema de desempenho na função Encode64(). Experimenta baixar a última versão do Binário do TOTVS Application Server no Portal da TOTVS. Parece que esta ocorrência já foi corrigida. — E desculpe a demora na resposta, a notificação de POST foi apagada acidentalmente … Abraços

      Curtir

  4. Pingback: Criptografia em AdvPL – Parte 02 | Tudo em AdvPL

Deixar mensagem para Siga0984 Cancelar resposta