jueves, marzo 07, 2019

Imagenes y campos memo, guia de supervivencia con ADS

Mis chicos favoritos, como casi todo el mundo sabe, es la gente de Sanrom's Software de México, con quien me une algo mas que una amistad de muchos años.

Hace un par de años, por estas fechas, concluíamos la migración a 32 bits de sus productos de control escolar, con la promesa de que la nueva versión estaría hecha en Xailer, desafortunadamente nos ha ganado el tiempo, ha llegado el momento de lanzar una nueva versión, que aunque no será en Xailer, si tendrá mejoras significativas a la versión de 32 bits y aunque ya casi está terminado el primer producto Sanroms hecho con Xailer, aún no ha sido probado a conciencia, con esto en mente, la prudencia aconseja seguir por el mismo camino que a la fecha nos ha dado resultado.

En el proceso de migración cambiamos de tabla de datos, de DBF/CDX, se decidió cambiar al formato nativo de Advantage Database Server: ADT/ADI por varias razones, primero por la tecnología cliente/servidor, segundo por la seguridad de la información y tercero por desempeño de la aplicación, hoy, a 2 años de haber lanzado la primera versión con ADS, yo creo que la mas estable hasta el momento en cuanto a manejo de tablas, no hemos tenido ningún "susto" ya se que los usuarios trabajen con el servidor local o bien tengan un servidor remoto instalado.

Uno de los grandes atractivos de esta nueva revisión, es la capacidad de tener acceso a los datos vía Internet, esta mejora se hizo por la necesidad de algunos colegios con varios planteles remotos de acceder a los datos centralizados, en esta nueva revisión se hizo la adaptación al software para que hiciera uso del Advantage Internet Server.

La implementación del acceso remoto no fue complicada, en realidad a parte del diccionario de datos, poco mas se tuvo que mover del programa original, el verdadero problema se presentó cuando hubo que manipular imágenes.

Los sistemas Sanrom's tienen ya muchos años en el mercado, programados originalmente en FiveWin 2.0 a 16 bits y con Clipper 5.3 e índices CDX, la manipulación de las imágenes, en este caso las fotografías de alumnos y maestros, se hacia mediante un campo cadena de caracteres que hacía referencia a un archivo de imagen (.bmp, jpg, gif, etc) dentro del disco duro del servidor, esto no planteaba problema alguno en la red de área local, porque la imagen se lee por referencia al archivo, pero para Internet si que presenta un problema, ya que usando el AIS, no es posible acceder al disco duro remoto, únicamente a los archivos de datos ADS.

Para que el sistema funcionara eficientemente sobre Internet, había que añadir la imagen un registro de alguna tabla, ya no por referencia, es decir, ya no como una ruta a un archivo, sino que había que poner el archivo de imagen completo dentro de un campo de la base de datos, a fin de que la imagen estuviera disponible en el equipo cliente tanto si estuviera conectado en la red de área local, como si estuviera conectado por Internet, el mecanismo de despliegue en pantalla se explicará mas adelante.

Bien, la primera aproximación fue trabajar en un campo memo, en ADS, al igual que con DBF, el campo memo se almacena en un archivo independiente a la tabla de datos, con la extensión ADM, en nuestro primer intento tratamos de hacer algo como esto

field->campomemo := MemoRead("\\servidor\recurso\directorio\imagen.jpg")

Es decir, directamente, leer el archivo como si se tratara de un texto y guardarlo en el campo memo correspondiente, mala idea, mas adelante veremos porqué.

Para el despliegue de la imagen en pantalla, había que generar un archivo de imagen a partir del dato almacenado, ya que la clase TIMAGE de FiveWin, solo puede hacer visualización de una imagen que esté almacenada en un recurso, o bien de un archivo de imagen que exista en disco (nota: Xailer puede visualizar las imágenes leídas directamente del campo, sin necesidad de crear un archivo a disco.), para lo cual, la ruta mas práctica aparentemente era:

MemoWrit("imagentemp.jpg",field->campomemo)

Sin embargo, al hacer el despliegue en pantalla del archivo "imagentemp.jpg", esta aparecía cortada, solo aparecía aproximadamente una tercera parte de la imagen, ya que el proceso de MemoRead() no marcaba ningún error, y aparecía un valor en el campo, no pensamos que la lectura y guardado de la imagen, como un texto, en un campo memo, pudiera estar causando problemas, antes bien, pensábamos equivocadamente que el proceso de escritura era el que estaba dañando la imagen.

Después de darle mucho la vuelta (toda una tarde) y buscar por el lado equivocado, el de la escritura, decidimos hacer un "back to the basics" y analizar el proceso de lectura, ¡ oh sorpresa !, nos pusimos a comparar los tamaños en bytes del archivo original, contra el LEN(MemoRead(...)) y ambos valores eran el mismo, por decir algo, 90 kbytes, el problema venía en la asignación al campo memo, cosa que descubrimos cuando al hacer un LEN(field->campomemo), nos devolvió 30 kbytes, cuando, si el proceso de guardado hubiese hecho bien todo, este valor tendría que ser 90 kbytes, por esta razón al hacer el MemoWrit(), solo aparecía una tercera parte de la imagen.

Dependiendo de la imagen, algunas veces lo hacía bien, y otras lo hacia mal, cuando la imagen media menos de 30 kbytes, hacía el guardado y el despliegue en pantalla perfectamente, pero cuando era mayor de este tamaño, la imagen quedaba truncada a 30 kbytes.

Evidentemente el formato de almacenamiento estaba fallando, así que Israel y yo nos pusimos a estudiar un poco el tema y encontramos que dentro de ADS, para el tipo de tabla ADT, los campos memo puede ser de 3 tipos:

Memo - para almacenar solo texto, igual que en un DBF
Binary - para almacenar cualquier cosa binaria, por ejemplo un documento de word
Image - para almacenar, como su nombre lo indica, una imagen, el formato es lo de menos.

Con esto en mente, procedimos a hacer el cambio de tipo de campo, modificando la estructura de la tabla, y cambiando el campo tipo MEMO por IMAGE, una cosa que descubrimos fue que cuando se define un campo del tipo Binary ó Image dentro de la estructra del archivo ADT, al momento de crear la estructura nueva, los campos se "renombran" como de tipo "BLOB" (Binary Large OBject), así que para efectos de definición se pueden declarar de 3 formas, como IMAGE, como BINARY o bien directamente como BLOB, que es otro tipo válido de campo en una tabla ADT de Advantage.

Una vez redefinido el tipo, procedimos a hacer lo que estábamos haciendo anteriormente, es decir, field->campoblob := Memoread().... otro error, cuando intentábamos hacer esta asignación el sistema marcaba un error de Field type mismatch, es decir, el tipo de campo no coincide con el tipo de variable que se le está asignando, lo cual es entendible, el campo es tipo BLOB, y le estabamos tratando de meter una cadena de caracteres, es como si trataras de meter una cadena en un campo numerico o fecha o lógico, sin embargo, el tipo BLOB no existe en (x)Harbour, ¿ que podíamos hacer entonces ? ¿ como transformar una cadena de caracteres para que sea reconocida como BLOB ?.

De vuelta al manual de ADS, y encontramos que en ADS existen un par de funciones que quizá nos podía ayudar: AdsBinaryToFile() y AdsFileToBinary(), así que vimos sus parámetros, las metimos en el código fuente, y nos llevamos una desliusión cuando vimos aparecer el temido "Unresolved external HB_FUNC_ADSFILETOBINARY()", que indica que dichas funciones no estaban definidas dentro del RDDADS.LIB, ¿ que acaso nadie había trabajado con blobs antes con ADS ?

Antes de ponernos a pegarle la "C" para hacer wrappers para llamar a las funciones que nos faltaban, se nos ocurrió revisar el código fuente del RDDADS, mismo que se puede encontrar dentro de las contribuciones de xHarbour, y en el archivo ADSFUNC.C encontramos 2 funciones que nos salvaron de estar perdiendo el tiempo con código en "C": AdsBlob2File() y AdsFile2Blob(), estas funciones son wrappers muy elaborados para las funciones AdsFileToBinary() y AdsBinaryToFile(), solo que sus parámetros son mas simples de manejar.

La sintaxis es:

AdsFile2Blob(,,)

Donde:

es el nombre del archivo que deseamos leer
es el nombre del campo blob donde se guardará el archivo leido
es el tipo de campo (imagen o binario) que esta definido en una constante predefinida dentro del archivo ACE.H y que es:

#define ADS_BINARY 6 /* BLOB - any data */
#define ADS_IMAGE 7 /* BLOB - bitmap */


Con lo cual, en vez de estar haciendo experimentos con el MemoRead() un simple:

AdsFile2Blob("nombredelaimagen.jpg",campoblob,ADS_IMAGE)

Se encargaron de todo, la imagen se lee perfectamente y se guarda en el campo sin perder un solo byte, lo anterior es válido no importa que tipo de campo blob tengas, solo tienes que cambiar el tercer parámetro, lo mismo puede ser una imagen, en cuyo caso usaremos ADS_IMAGE, que un documento de word o una hoja de excel, para los cuales usaremos ADS_BINARY.

Para la operación contraria, es decir, crear un archivo de imagen a partir del campo blob, utilizamos la función contraria cuya sintaxis es:

AdsBlob2File(, )

De tal forma que un simple

AdsBlob2File("imagen.jpg",campoblob)

Crea el archivo que necesitaremos para visualizar con FiveWin, sin necesidad de estar enrredando con MemoWrit().

Una vez descubiertas estas 2 funciones, el resto fue fácil, para cargar todas las imagenes dentro del campo de la base de datos hicimos:

USE alumnos
GO TOP
DO WHILE ! EOF()
AdsFile2Blob(alumnos->foto, alumnos->imagen, ADS_IMAGE)
SKIP
ENDDO

Donde el campo alumnos->foto era el que contenía una cadena de caracteres con la dirección del archivo de foto, y alumnos->imagen es el campo blob que contendrá la imagen dentro de la tabla.

Eso si, después de hacer la importación de foto de un colegio de 2,500 alumnos, el archivo memo ADM quedó de 170 megas de tamaño, mas grande que el archivo ADT, que contiene los datos, tal ves pudieras pensar que con ese tamaño la tabla sería lentísima de acceder a los datos, pues va a ser que no, la tabla tiene el mismo rendimiento que si no tuviera las imágenes pegadas, los indices se generan a la velocidad de ADS, vamos que no perdemos nada de performance por tener las imágenes como parte de la tabla de datos.

Para el acceso via internet tampoco tenemos problema, porque la imagen se genera dentro del disco duro de los clientes y se borra cuando se ha dejado de utilizar, con lo cual podemos enviar esos archivos de imagenes a los equipos clientes sin necesidad de que tengan acceso al disco del servidor, cosa que por internet puede ser lentisima.

Eso nos abre nuevas posiblidades de trabajo, ahora podemos añadir practicamente cualquier tipo de archivo como un campo de la base de datos, eso me esta dando nuevas ideas.....