Gustavo's profileEspacio de Gustavo SuhitBlogListsGuestbookMore Tools Help

Espacio de Gustavo Suhit

No solo informático...

Gustavo Suhit

Occupation
Location
Interests
Hace varios años que trabajo relacionado con informática, ya sea por educación, desarrollos o roles técnicos de apoyo comercial.La mayoría de los años siempre relacionado con temas nuevos como PKI o Biometría.
10/18/2009

Download en Sharepoint

DownloadMOSS Hace un tiempo escribí un artículo introductorio acerca de Sharepoint. Mas específicamente sobre los web services de MOSS.

Para complementar este artículo, ahora entrego una rutina de descarga de documentos desde Sharepoint, sin usar sus web services, dado que en ciertas ocasiones se producen errores en las descargas, debido a fallas de configuración, diferencias configuración y de permisos (especialmente cuando se hacen entregas en desarrollo, QA y Producción), etc.

Los tiempos de descarga incluso son algo mejores, y la seguridad de acceso está garantizada debido a las credenciales presentadas.

Espero les sirva.

Download vía WS de MOSS

No desarrollaré este tema, dado que ya lo hice en el artículo anterior, y allí pueden encontrar el detalle.

Solo decir, que en mi caso, algunas veces tuve problemas, pero no con la versión actual del documento sino con la descarga de las versiones anteriores de los mismos. Por ese motivo, utilicé la segunda opción, que detallo ahora.

Nueva Forma de Download

Una forma fácil y rápida de download, pero no solo de download desde Sharepoint sino desde cualquier sitio web, es utilizando algo por estilo:

public int BajarDocumento(String url, out byte[] arreglo)
{

   try {
     WebClient request = new WebClient();
     request.Credentials = _Credential;
     byte[] newFileData = request.DownloadData(url);
     arreglo = newFileData;
     if (newFileData != null && newFileData.Length > 0)
     {
        return RET_SUCCEFUL;
     }
     else
     {
        return RET_ERR_UNKNOWN;
     }

    } catch (Exception ex) {

        log.error(ex); 

    }

}

En _Credential, se debe definir las credenciales de autenticación al sitio, de la forma:

_Credential = new NetworkCredential(_User, _Psw, _Domain);

De esta forma, se consigue descargar un documento, de cualquier tipo, y dar la posibilidad de descarga o visualización.

So far, So good!

Microsoft Reporting Services – Parte I

Comenzaré en este artículo, con una serie dedicada a ésta herramienta sumamente potente de Microsoft

MRS es un servidor de reportes, habilitados a través de una URL, y que pueden ser accedidos de diferentes formas.

El producto forma parte de las herramientas adicionales que vienen incluidas con la base de datos de Microsoft, SQL Server. En la versión 2005, se agregaron varias mejoras sustanciales, y en la 2008, varias mas. De todas formas, me centraré en la mayor cantidad de características incluidas en todas las versiones.

Algunas de las características mas importantes que puedo mencionar para comenzar son:

Fácil de instalar
Fácil de configurar
Potente en la generación de reportes de varios tipos
Seguro en la conexión a la base de datos
Fácil de integrar a las aplicaciones

A través de los diversos artículos cubriré diferentes aspectos del tema, algunos que parecen triviales, pero a la hora de hacerlos, es mejor contar con ayuda.

Go Ahead…

Requerimientos

Los requerimientos básicos son:

Hardware
  • Procesador Pentium II o compatible de 500 MHz o superior
  • Memoria 256 MB de RAM o superior
  • Disco duro 325 MB disponibles para la instalación completa
    • 50 MB disponible para el Report Server
    • 30 MB disponible para el Report Designer
    • 100 MB disponible para el Framework 1.1 o 2.0
    • 145 MB disponible para los ejemplos y Books Online.
Software
  • Sistema Operativo:
    • Windows Server 2003, Standard, Enterprise, Datacenter Editions.
    • Windows 2000 Server, Advanced o Datacenter con SP4 o superior.
    • Windows XP Professional con SP1 o superior
    • Windows 2000 Professional con SP4 o superior
  • Base de Datos:
    • SQL Server 2000 Standard, Enterprise o Developer Editions con SP3 o superior
  • Adicionales:
    • Se requiere una conexión local o remota a un servidor de base de datos Microsoft SQL Server 2000 con Service Pack 3, para almacenar los componentes del Report Server Manager y las definiciones de los reportes. Se recomienda usar la edición de Reporting Services que corresponda con la edición de SQL Server 2000 instalada.
    • La base de datos fuente puede ser MSDE, pero el Report Server Database no se puede instalar en esta edición.
    • La instalación de Microsoft .NET Framework es requerida.
    • Internet Explorer 6.0 o superior es requerido para el acceso al Report Manager desde los computadores cliente.
    • Internet Information Services 5.0 o superior.
    • Microsoft Visual Studio® .NET 2003/2005/2008 no es requerido, pero es la mejor opción para el diseño de reportes, utilizando el Report Designer. Se puede utilizar la Edición Standard de estas herramientas Microsoft .NET: VB, C#, C++ o J#. De todas formas, si se selecciona en el instalador de Reporting Services el Developer Studio, se instala el Visual Studio solo con los elementos de construcción de reportes. Si se tiene instalado el

Presentación de MRS y algo mas…

La instalación se realiza desde el mismo instalador que SQL Server, y se seleccionan ciertos parámetros necesarios.

No me extenderé en este (ni en otros temas) dado que los derivaré al blog de Fran Díaz, donde comienza su seguidilla de artículos acerca de Reporting Services con una presentación del producto. Desde allí, podrán encontrar varios puntos importantes relacionados con MRS que están muy claramente explicados.

Solo agregaré algunos trucos que me toco desarrollar en mis proyectos, y algunos detalles que no están relacionados directamente con el producto, pero que afectan al proyecto en última instancia.

Comenzamos

Para poder desarrollar un reporte en MRS, se debe instalar como dije antes, las herramientas necesarias. Cuando se instala MRS en nuestra máquina, aparece un ambiente similar al de VS2005, si es que no lo teníamos instalado desde antes. Si lo teníamos, lo que hace el instalador, es agregar los templates necesarios para crear Proyectos de Reportes.

Nota Importante: Puede pasar que cuando tenemos instalado el VS2005, e instalamos VS2005, se eliminen desde la carpeta correspondiente, los templates de VS2005. Esto ocasiona que cuando abrimos el VS2005, y pedimos crear un nuevo proyecto, solo se muestre el template de reportes. Un forma fácil de corregir el problema, sin tener que reinstalar VS2005, es copiar los templates desde la carpeta global a los de nuestro perfil, de tal forma que vuelvan a aparecer. Eso se logra copiando desde la carpeta C:\Archivos de programa\Microsoft Visual Studio 8\Common7\IDE a la carpeta C:\Documents and Settings\<usuario>\Mis documentos\Visual Studio 2005\Templates, las carpetas ItemTemplates y ProjectTemplates.

Lo primero es crear un proyecto del tipo de reportes.

Templates Luego, se pueden definir DataSources o Reportes.

  • DataSources: son conexiones a bases de datos, con sus correspondientes credenciales. Estos DataSources son los utilizados por los reportes para conseguir los datos.
  • Reportes: Archivos .rdl, donde se dibuja el reporte y se definen comportamientos específicos a cada lógica de negocios.

La gran ventaja de los DataSources, es que se definen una única vez, y estos son mantenidos por un administrados de bases de datos, y los desarrolladores solo hacen referencia a estos DataSources desde sus reportes, olvidándose en tiempos de ejecución de enviar las credenciales de conexión a la base de datos.

Al seleccionar el Asistente, éste nos guía para rápidamente genera reportes con diferentes temas, subreportes, cortes de control, etc. Luego, una vez hecho este primer paso, podemos agregarle mejoras gráficas e incluso condiciones de visualización mas complejas dependiendo del tipo de informe.

Cuando visualizamos el reporte de la forma:

aparecen en la barra de herramientas, unos controles para utilizar de la forma:

Con estos controles es factible hacer casi cualquier tipo de reporte, haciéndolos no solo útiles sino vistosos.

Por último, es necesario realizar la consulta SQL para acceder a los datos. Para esto, se accede a la lengüeta Datos, en la misma ventana de reportes. Allí se selecciona uno de los DataSources generados, y desde allí se seleccionan las tablas necesarias para obtener los datos correctos. Luego se puede escribir la consulta SQL directamente o trabajar con un asistente. Se ve de la forma:

Consulta de informe en el diseñador de consultas genérico

Para comenzar con un reporte básico, paso a paso, les recomiendo seguir también la ayuda en línea de Microsoft.

En los próximos artículos d esta serie,expondré algunos trucos prácticos para mejorar la calidad de los reportes, así como temas determinantes en el éxito de los proyectos que utilizan MRS.

So far, So good!

Imágenes by Tinypic

5/14/2009

Office Communicator 2007 - Alertas

clip_image002Debido a un proyecto en el que estoy involucrado, tuve que investigar como integrar a nuestro sistema, el uso del Office Communicator 2007 (OC) para el envío de alertas a los usuarios.

Poco sabía del producto, y bastante me costó encontrar información de utilidad para poder hacerlo.

Aquí expongo algunos tips que creo de utilidad para poder realizar ésta tarea, con ejemplo concretos.

Espero les sirva, para no tener que navegar. tanto por internet :-).

Contexto

Office Communicator 2007 es un sistema de mensajería desarrollado por Microsoft, que permite establecer comunicaciones entre usuarios de un dominio, ya sea a través de mensajes instantáneos o voz, y compartir información.

Tiene varias características de ayuda al negocio, que pueden revisar en la descripción del producto en el sitio oficial.

El Desafío

Nuestro sistema es web. Es un manejador documental que permite un flujo manual de documentos, con bandejas de entradas y salidas de documentos.

El pedido concreto era que un usuario al que se le enviara un documento en esa bandeja de entrada,  recibiera un alerta en su Communicator corporativo, con un texto del tipo: “Ud. ha recibido un el documento DDD de parte del usuario UUU. Ingrese a URL …”, a través de un IM (Instant Message) 

(Esto es posible en esta compañía, porque el usuario en el momento en que se loguea en su máquina, automáticamente abre el OC).

Dado que el IM es una comunicación de máquina a máquina, y se hace a través del OC, que es una aplicación local, no es factible utilizar una especie de repositorio o cola en un servidor. 

La Solución

En primer lugar, para poder trabajar desde C#, lenguaje utilizado en nuestro caso, debimos descargar la API del producto para poder instanciarlo desde programa.

Luego, debimos comprender que se trata de una aplicación stand alone, por lo que no es un sistema de mensajería como el mail, donde el encargado de “rutear” el mensaje es el servidor.

Para poder enviar un mensaje se debe instanciar el OC, a través de la instrucción:

using CommunicatorAPI;

private Messenger _communicator;                    
private IMessengerAdvanced _communicatorAdvanced;   

if (_communicator == null)
{
    _communicator = new Messenger();
    _communicatorAdvanced = _communicator as IMessengerAdvanced;

    _communicator.OnSignin += new DMessengerEvents_OnSigninEventHandler(communicator_OnSignin);
    _communicator.OnSignout += new DMessengerEvents_OnSignoutEventHandler(communicator_OnSignout);
    _communicator.OnIMWindowCreated += new DMessengerEvents_OnIMWindowCreatedEventHandler(communicator_OnIMWindowCreated);
    _communicator.OnIMWindowDestroyed += new DMessengerEvents_OnIMWindowDestroyedEventHandler(communicator_OnIMWindowDestroyed);
    _communicator.OnAppShutdown += new DMessengerEvents_OnAppShutdownEventHandler(communicator_OnAppShutdown);
}

luego hacer el LogIn:

_communicator.Signin(0, _userIM, _pswIM);

donde el _userIM es un usaurio que se conectará al sistema, y que debe ser un usuario válido, sino fallará el proceso. Esto significa que se debe tener un usuario genérico de conexión por cada servidor redundante de nuestro sistema, desde donde se desea enviar alarmas.

Una vez logueado, para enviar un mensaje IM, nos debemos “colgar” del evento de apertura de una ventana (DMessengerEvents_OnIMWindowCreatedEventHandler) de la forma: 

void communicator_OnIMWindowCreated(object pIMWindow)
{
    try
    {
        IMessengerConversationWndAdvanced imWindow = (IMessengerConversationWndAdvanced)pIMWindow;

        if (((IMessengerConversationWndAdvanced)pIMWindow).HWND == windowHandle)
        {
            // outgoing
            if (!String.IsNullOrEmpty(_messageIM))
                imWindow.SendText(_messageIM);

            imWindow.Close();
            windowHandle = 0;
            _messageIM = null;
        }
        else
        {
            // incoming
            windowHandle = imWindow.HWND;
        }
    }
    catch (COMException comException)
    {
        log.Error(communicator_OnIMWindowCreated [comException].", comException);
    }
    catch (Exception exception)
    {
        log.Error("communicator_OnIMWindowCreated [exception].", exception);
    }
}

Donde el envío se hace exactamente en la línea:

            if (!String.IsNullOrEmpty(_messageIM))
                imWindow.SendText(_messageIM);

Siendo _messageIM una variable global antes completada.

Pero para enviar el mensaje se debe abrir la ventana. Para esto podemos usar:

public void SendIM(string userIM, string msgIM)
{
    try
    {
        if (msgIM != null) _messageIM = msgIM;
        // Find the Contact using the SipUri
        IMessengerContact contact = null;
        if (userIM != null)
            contact = (IMessengerContact)_communicator.GetContact(userIM, _communicator.MyServiceId);

        // Use IMessenger.InstantMessage to send an IM
        if (contact != null)
        {
            IMessengerWindow window = (IMessengerWindow)_communicator.InstantMessage(contact);
            windowHandle = window.HWND; // Capture window handle
            System.Threading.Thread.Sleep(_timeToSleepBetweebnMsgIM);
            log.Debug("OCSender.SendAllIM - Contacto encontrado " + userIM +
                        ", para enviar mensaje: " + msgIM);
        }
        else
        {
            log.Debug("OCSender.SendAllIM - No encontro el contacto " + userIM +
                        ", para enviar mensaje: " + msgIM);
        }
    }
    catch (Exception e)
    {
        log.Error("SendIM [exception].", e);
    }
}

Donde userIM es un usuario corporativo, definido por su mail o userid, y msgIM el mensaje propiamente dicho.

Notarán que existe un Sleep, y esto se debe a que se debe dar el tiempo suficiente a la apertura de la ventana y la ejecución del correspondiente código en el evento de apertura, para evitar saltear mensajes.

Ahora bien, si existe la posibilidad de concurrencia en la emisión de alertas, deberíamos manejar una cola de mensajes, y tener una rutina que agregue mensajes a la cola y otra rutina que envíe en algún momento todos los mensajes encolados. Para acceder a dicha cola se podría acceder bloqueándola, para evitar problemas, de la forma:

lock (_colaMsg)
{
       _colaMsg.Add(new MensajeIM(toUserIM, messageIM));
}

Donde MensajeIM es un clase que tiene dos propiedades para efectuar luego los envíos.

Espero les sirva, y de necesitar algún soporte, no duden en contactarme. Les puedo enviar una clase que hace todo esto.

So far, So good!

4/21/2009

Sharepoint – Usando sus Web Services

LogoMOSS_50Dado un proyecto en el que estoy involucrado directamente, he tenido que meterme fuerte en el uso de Sharepoint 2007 (MOSS) para utilizarlo como repositorio de documentos desde un sistema externo.

Aquí comparto con Uds. algunos detalles importantes que me toco investigar, con los típicos “detalles” que terminan haciéndote pasar momentos difíciles.

Si bien aquí desarrollo algo muy básico, les aseguro que si necesitan desarrollar algo del estilo, este artículo les servirá, porque en definitiva, recopila información recogida a lo largo de varias horas de navegación en la web, y luego de probarlo en un ambiente real.

Es factible probar todo esto contra una máquina virtual con Windows 2003 Server Trial y Sharepoint 2007 Trial, que tiene una duración de 60 días, y se pueden descargar desde Microsoft Download Center..

El Contexto

Lo primero que hay que aclarar es que Sharepoint es una herramienta mucho mas amplia que un simple repositorio de documentos. Como explica el link al site de MOSS, este producto permite la colaboración entre equipos, compartiendo información fácil y rápidamente, incluso definiendo workflow a los documentos, lo que lo hace muy potente.

Es factible definir colecciones de sitios web, desde plantillas predefinidas,  permitiendo luego que cada usuario personalice su propio sitio, e incluso configure los permisos de acceso a su espacio de trabajo. Todos los permisos están relacionados directamente al AD de Windows.

Como todo repositorio, es factible alojar documentos de diversos tipos,DiagramaMOSS como office, pdf, musicales, videos, imágenes, etc.

Se pueden definir cotas de espacio por sites, por tipos de documentos, e incluso por base de datos, utilizadas para el almacenamiento. Esto permite, desde el punto de vista administrativo, tener un control en el uso de espacio, dado que los usuarios solemos desestimar ese tema, hasta que nos quedamos sin espacio para trabajar. Cada site puede tener asociada una base de datos, o compartirla con otras. Esto queda a criterio del administrador del sistema, pero normalmente se opta por separar las bases de datos, para acelerar los procesos de recuperación en caso de ser necesario, y para tener mas control por aplicación de negocios o espacios específicos de colaboración. Lo importante, es que TODO se almacena en base de datos SQL Server, y no se usa filesystem para nada. Podría se factible desarrollar un provider para almacenamiento en otro repositorio, pero habría que analizar bien la necesidad para justificar en ese caso, la pérdida de seguridad y eventualmente falta de integridad, características que son nativas de MOSS cuando se utiliza SQL Server.

Para trabajar con MOSS, es factible hacerlo de 3 formas:

  1. A través de su interface (Web), accediendo al sitio en que tenemos permisos, y utilizando las opciones habilitadas según nuestro perfil.
  2. A través de la API MOSS, pero SOLO si estamos en la misma máquina donde está instalado el producto.
  3. A través de Web Services estándares de MOSS, que se publican en cada uno de los sites que se crean, sea principal o secundario, y que permiten hacer casi la totalidad de las operaciones que se pueden hacer desde la interface web standard. En caso de existir alguna operación que no se pueda hacer con estos web services, se desarrolla uno nuevo con la API MOSS, se la publica en el sitio que se necesita, y se lo consume tal cual los web services estándares.

Nuestro Ejemplo

Mas allá de las pocas características antes enumeradas, lo que utilizamos en nuestro proyecto fue solo la capacidad de almacenamiento y versionamiento de documentos, en lo que MOSS denomina Document Libraries (Biblioteca de Documentos). Básicamente una DL es un espacio físico que almacena documentos de cualquier tipo, dentro de la estructura de directorios que definamos.

Aquí comienzan a aparecer los problemas típicos de tamaños, espacios, etc. Una limitante que tiene MOSS 2007, cuando utilizamos SQL Server 2005, es que podemos almacenar documentos menores a 2Gb. Esto es porque la limitante se hereda del SQL Server 2005, donde es imposible almacenar una fila mas grande que ese tamaño. No es el caso del SQL Server 2008, que no tiene cota máxima.

De todas formas, el tamaño de los documentos, no es solo por las cotas, sino por la congestión de la red, en caso que sean demasiado grandes y/o que las conexiones sean algo lentas.

Ok Según recomendaciones de Microsoft, es mejor no sobrepasar ciertos límites de cantidades de documentos por carpetas, para evitar problemas en el uso de la interface web. Cuando trabajamos como en nuestro caso programáticamente, pero sin usar la interface nativa, ese límite es mas flexible.

Un tema importante que mencione al pasar antes, es el versionamiento automático que se puede definir en las DL, para que cada vez que subamos un documento, MOSS se encargue de éste tema. Luego es factible recuperar la lista de versiones así como los documentos correspondientes a cada una.

Acerca de la lista de web services estándares, no haré demasiados comentarios, porque aquí pueden encontrar mucha información al respecto, así como en web. Luego, es mucho de prueba y error, para ver como se comportan cada uno de ellos. Es importante aclarar que no todos tienen el mismo tipo de salida, es decir, el resultado retornado no es siempre del mismo tipo, lo que hace mas dificultoso la programación, pero luego de un poco de pruebas, se generalizan las operaciones mas importantes.

Algunos Tips importantes para la programación

Si deseamos hacer un upload de un documento programáticamente a través de los web services estándares de MOSS, podemos hacer algo del estilo:

using (Copy ws = new Copy())
{
    try
    {
        ws.Timeout = _Timeout;
        ws.Credentials = _Credential;

        string[] copyDest = { <URL de MOSS> };

        FieldInformation myFieldInfo1 = new FieldInformation();
        myFieldInfo1.InternalName = "Title"; //Titulo
        myFieldInfo1.DisplayName = "Título"; //Titulo
        myFieldInfo1.Type = FieldType.Text;
        myFieldInfo1.Value = filename;

        FieldInformation[] myFieldInfoArray = { myFieldInfo1 };

        CopyResult myCopyResult1 = new CopyResult();
        CopyResult[] myCopyResultArray = { myCopyResult1 };

        //Hago el upload propiamente dicho
        uint myCopyUint = ws.CopyIntoItems("
http://null", copyDest,myFieldInfoArray, file, out myCopyResultArray);
        if (myCopyUint == 0)
        {
            if (myCopyResultArray[0].ErrorMessage != null)
            {
                log.Error("MOSSHelper.UploadDocument [ErrorCode=" + myCopyResultArray[0].ErrorCode + " -  ErrorMessage=" + myCopyResultArray[0].ErrorMessage + "]");
             return ParseErrorCopy(myCopyResultArray[0].ErrorCode);
            }

}

Copy hace referencia al WS Copy.asmx.

Nota Importnate: Tengan especial cuidado cuando utilicen FieldInformation, para agregar un metadato asociado al documento, dado  que si no se completan tanto el Inernal como el DisplayName, ademas del Type y el Value, se completa el upload pero no se llena ese metadata. Y si despues se necesita ese metadata para efecutar búsquedas, estarás en problemas. Además, dependiendo de la versión (inglés, español, etc) el InternalName cambia, y ocurre lo mismo que si no se llenara.

Un Download del mismo archivo podría ser:

using (Copy ws = new Copy())
{
    try
    {
        ws.Timeout = _Timeout;
        ws.Credentials = _Credential;

        string urlSource = <URL total del docuemnto en MOSS>;

        FieldInformation[] myFieldInfoArray;
        CopyResult myCopyResult1 = new CopyResult();
        //Hago el download propiamente dicho
        uint myCopyUint = ws.GetItem(urlSource, out myFieldInfoArray, out file);
        if (myCopyUint != 0)
        {
            ret = (int)myCopyUint;
        }
        else if (file == null)
        {
            ret = RET_ERR_DOCUMENT_NOT_FOUNT_IN_MOSS;
        }
    }   

}

Crear un DL:

using (Lists ws = new Lists())
{
    try
    {
        ws.Timeout = _Timeout;
        ws.Credentials = _Credential;
        string strResult = "";
        System.Xml.XmlNode nodeListItems = ws.AddList(nameDL, descripcionDL, 101);
        ret = ParseErrorCreateDL(nodeListItems);

    }    }

Aquí usamos el WS Lists.asmx.

Utilizando DWS.asmx, es factible crear carpetas, con CreateFolder.

Por último, con Versions.asmx, se pueden recuperar las versiones de un documento, y luego, a través de su URL completa, y con ayuda nuevamente de Copy, se pueden recuperar.

Espero les sirvan estos comentarios, y cualquier duda me avisan, para ver si puedo ayudarlos, y de paso, sigo aprendiendo.

So far, So good!

2/18/2009

NHibernate – Tips Útiles para el Desarrollo Efectivo

BDDesde hace ya tiempo utilizo como framework de base de datos Hibernate o NHibernate. Si bien Hibernate ha tenido un desarrollo constante, NHibernate, además de aparecer posteriormente, no ha tenido el empuje de su “hermano mayor”.

De todas formas, la experiencia que he tenido con NHibernate es mas que buena hasta el momento, y seguiré usándolo, debido a que permite a los desarrolladores de productos como yo, una gran flexibilidad a la hora de vender sus productos.

Un tema que en Hibernate ya estaba solucionado desde hace buen tiempo, que era la generación automática de las clases y los archivos de mapeo tomando como base las tablas desde la base de datos en cuestión, en NHibernate no estaba tan desarrollado. Hoy, buceando en  internet encontré una herramienta mas que interesante, que no solo genera los mapeos y class para NHibernate, sino para varios templates mas. 

En este artículo haré algunas referencias rápidas para temas importantes en el uso de NHibernate, y posteriormente desarrollaré una introducción a la herramienta mencionada, debido a que permite una forma rápida y consistente de adelantar trabajo, por demás tedioso si se lo intenta manual.

NHibernate – Que és y qué ventaja tiene?

NHibernate, es un framework de O/RM (Object/Relational Mapping), un port de Hibernate de Java, que tiene como función principal mapear los objetos desde una aplicación .Net a una base de datos Relacional.

La ventaja fundamental, es que el programador se abstrae de la base de datos con la que está trabajando, y solo trabaja a nivel de clases C#.NET o VB.NET, y NHibernate se preocupa de hacer los mapeos entre las propiedades y las columnas de la base de datos, generando dinámicamente las sentencias SQL para interactuar con la base de datos, según sea la base definida en el archivo de configuración.

NHibernate – Algunas Características

BD_Bullet Bases de Datos Soportadas
Están soportadas una lista bastante amplia entre las que se encuentran las bases de datos mas utilizadas a nivel de mercado, tales como Oracle, SQL Server, MySQL, etc.
BD_Bullet

Relaciones
Es factible modelar relaciones de 1:1 o 1:N, asociando en las clases propiedades que contienen una lista de objetos (filas) de otro tipo de entidad (tabla) definida. Es importante definir correctamente, como NHibernate debe cargar la entidad cuando tiene relaciones de 1:N, dado que esto puede significar problemas graves de performance. Lo ideal es configurarlo para que solo se carguen las listas cuando realmente se necesitan. Verificar el atributo Lazy.

BD_Bullet

Performance
Dependiendo del modelo claro está, pero en general he conseguido muy buena performance en los accesos a base de datos, probando especialmente con Oracle, SQL Server 2000/2005, MySQL, Sybase y Postgress. En caso de necesitar hacer alguna consulta muy complicada a la base de datos, NHibernate permite generarla a través de código SQL directo, evitando tiempos adicionales innecesarios.

NHibernate – Herramientas de Apoyo

Una de las tareas mas engorrosas es la generación de los archivos de mapeo, que relacionan los nombres de las propiedades en una clase, y las columnas de la base de datos. Esto se genera en XML que debe ser compilado como Embebed Resource en un proyecto .NET.

Una opción es hacerlo manualmente, generando el XML necesario y detallando cada propiedad. 

Otra opción, por demás mas productiva, es utilizar herramientas como MyGeneration, con la cual configurando la conexión a la base de datos que con la que deseamos trabajar, y descargando una plantilla de NHibernate de las varias que existen, es factible generar, al mismo tiempo, una clase por cada tabla de la base de datos seleccionada, junto con el archivo de mapeo XML asociado, incluyendo relaciones, claves y hasta vistas.

MyGeneration

Desde la ventana principal de la aplicación, si seleccionamos Template Browser, haciendo click en el icono del mundo, podemos descargar mas plantillas, no solo de NHibernate.

Luego, seleccionamos el template deseado, presionamos Execute, y la aplicación generará el código definido (C#, VB, o lo que sea según el template seleccionado) y en el caso específico de estar generando para NHibernate, se generará el archivo de mapeo XML de la forma:

<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
  <class name="MyAsembly.BtrIdentity,MyAsembly" table="btr_identity" lazy="true">
    <id name="IdentId" column="ident_id" type="int">
      <generator class="native" />
    </id>
    <property type="string" length="80" name="IdentNick" column="[ident_nick]" />
    <property type="string" not-null="true" length="10" name="IdentTipodocumento" column="[ident_tipodocumento]" />
    <property type="string" not-null="true" length="20" name="IdentNrodocumento" column="[ident_nrodocumento]" />
    <property type="string" length="50" name="IdentNombre" column="[ident_nombre]" />
    <property type="string" length="50" name="IdentApellido" column="[ident_apellido]" />
    <property type="string" length="1" name="IdentSexo" column="[ident_sexo]" />
    <bag name="FkBtrBirBtrIdentity" inverse="true" lazy="true" cascade="all">
      <key column="ident_id" />
      <one-to-many class="MyAsembly.BtrBir,MyAsembly" />
    </bag>
  </class>
</hibernate-mapping>

Incluso, hay algunos templates de NHibernate, que generan hasta el proyecto VS2005 o VS2003, con el correspondiente archivo de configuración para establecer la conexión a la base de datos en cuestión.

So far, So good!

2/17/2009

Nuevos Horizontes…

Desde el 26 de Diciembre del 2008, ya no pertenezco a la empresa Biokey Identity Technologies Ltda., y de hecho desde el 27 de Enero del 2009, tampoco existe mas la empresa.

A partir de allí, comencé a diagramar mi futuro, y comencé a analizar nuevos horizontes.

Luego de ésta experiencia, con muchos aprendizajes, algunos lindos, otros no tanto, y comprendiendo finalmente de la peor forma la inflexibilidad y la necedad de algunas personas, dejando escapar enormes oportunidades por ataques de ego, llegue a algunas conclusiones que estoy seguro me ayudarán en el futuro. Aquí las comparto, y siéntanse libres  de opinar si lo desean.

Por lo pronto, estoy trabajando bien, de forma independiente, y armando proyectos mas que prometedores, en el mismo rubro y en otros. Además, cualquier oportunidad será bien recibida.

Conclusiones

No poner todos los huevos en la misma canasta

Me costó mucho tiempo aceptar que puede hacerse, siempre y cuando consigas “aprender” a delegar, y luego seas claro en la comunicación con quien delegas. Quiero decir, no hace falta estar en todo, en cada detalle, solo debes saber rodearte de las personas indicadas y luego ser claro, coherente y “consecuente” en el tiempo. Poner todos los huevos en la misma canasta no hace mas que asegurarnos que si esa canasta se cae, es factible que se rompan todos los huevos. Así pues, entonces, diversificar para crecer ha sido mi primera conclusión.

Animarse a descubrir caminos nuevos derribando prejuicios

Desde que tengo memoria, siempre me llamó la atención la mente y sus poderes. Siempre estuve de acuerdo con lo que decía Einstein: “la mente te enferma y la mente te cura”.  Por supuesto encontré por consecuencia apasionante todas las disciplinas que trabajan con la mente, como 3044919392_4f0a0d5612_mcontrol mental, hipnosis, etc. Es por ese motivo que derribando  fantasmas de prejuicios sociales, muy míos, “aprendidos”, me forme como Hipnoterapeuta, y desde ya hace un buen tiempo comencé con un camino paralelo, bastante alejado (o no, quién sabe) de la lógica informática, el que me produce un desafío constante, me apasiona, y me llena de satisfacciones cuando siento que ayudo a alguno de mis pacientes. 

La conclusión, en todo caso, para mi con este “animarme”, fue que cuanto mas te alejas de las cosas que te movilizan, mas te apagas en el resto de tu vida. Si bien la informática me gusta, no me termina de llenar, por lo que la conjunción de estos (y otros) temas, hacen de mi una mejor persona. O al menos me hacen sentir mejor, que aunque suene egoísta, es el comienzo básico para cualquier otro tema.

La invitación es entonces, a que visiten mi otro blog, Hipnomateando…, y aporten lo que quieran, que será siempre bienvenido, estén de acuerdo o no.

No hay pero ciego...que el que no puede ver?

Solo haré referencia a un artículo que lo explica mejor. A mi me paso en este caso, y me parece bueno compartirlo.

Claridad de rumbo 

Muchas veces uno tiene vagamente en su cabeza que es lo que quiere, y difícilmente lo instancia en algo concreto. Muchas veces decimos “quiero ser rico” por ejemplo, pero que es eso en realidad?  Lo que para mi es “rico” para otra persona puede ser tener un buen fin de mes, y  viceversa. Lo que si hacemos, es que nos ponemos firmes en el “como hacernos ricos”, convirtiéndonos a veces, en “adoquines inflexibles” tirando para adelante.

Relacionado con la “apertura de posibilidades” que hablaba al comienzo, aprendí que debo fijar mis objetivos, lo mas claros y palpables (medibles) posible, y luego confiar que eso llegará, sin ser tan “cabezón”  y pretender que llegará de la forma que yo creo sin dejar la puerta abierta para que llegue por otro lado. Claro, tampoco la pavada, al “confiar” hay que ayudarlo trabajando, pero siempre dentro de los parámetros que nos hagan sentir bien, en la mayor parte del camino (ojala en todo el camino), de otra forma, es una cuestión de tiempo, para que la bomba explote en nuestra cara. Quiero decir, trabajar seriamente no significa ni trabajar excesivo, ni trabajar con cara de c… Se puede ser feliz trabajando y eso se refleja invariablemente en los resultados.

So far, so good

Imágenes by Flickr

12/15/2008

Firma Digital en PDF

Firma_PDF Días atrás estuve probando las firmas digitales en PDF. Hacía buen tiempo que no trabajaba en temas relacionados con PKI, y me interesé en el tema debido a un artículo de un diario español, donde decían que un sistema de notificaciones a infractores de tránsito sería implementado con PDF firmado digitalmente.

Lo primero fue descargar las herramientas necesarias y luego, hacer un ejemplo básico con un certificado autogenerado. Aquí detallo la problemática, las herramientas utilizadas y los ejemplos desarrollados, para que sirva de punto de partida, si alguien necesita implementar algo del estilo.

Herramientas Utilizadas

Las herramientas que utilicé son:

Ejemplo

El proceso es el siguiente:

  1. Se genera con o lee un archivo PDF.
  2. Se carga un certificado digital en un formato PKCS12.
  3. Se genera un objeto de firma.
  4. Se lo inserta dentro del documento PDF original.

El código sería algo como:

import java.io.*;
import java.security.*;
import java.security.cert.Certificate;
import com.lowagie.text.*;
import com.lowagie.text.pdf.*;

public boolean sign(String pathpdf, String pathPKCS12,  String passwordPKCS12) {
    String fileKey = pathPKCS12 ;
    String fileKeyPassword  = passwordPKCS12 ;
    try {
        //Crea un KeyStore
        KeyStore ks = KeyStore.getInstance("pkcs12");
       
//Carga Certificado
        ks.load(new FileInputStream(fileKey), fileKeyPassword.toCharArray());
        String alias = (String)ks.aliases().nextElement();
       
//Recupera Clave Privada
        PrivateKey key = (PrivateKey)ks.getKey(alias, fileKeyPassword.toCharArray());
        //Recupera Cadena de Certificacion si existe
        Certificate[] chain = ks.getCertificateChain(alias);
       
//Lee Documento PDF y crea archivo de salida con otro nombre para no pisar el original
        PdfReader pdfReader = new PdfReader((new File(pathpdf)).getAbsolutePath());

        File outputFile = new File(pathpdf + ".signed.pdf");
       
// Crea la firma en el objeto PdfStamper de la librería iText
        PdfStamper pdfStamper;
        pdfStamper = PdfStamper.createSignature(pdfReader, null, '\0', outputFile);
        PdfSignatureAppearance sap = pdfStamper.getSignatureAppearance();
        sap.setCrypto(key, chain, null, PdfSignatureAppearance.SELF_SIGNED);
        sap.setReason("Test GGS");
        sap.setLocation("");
        // Posiciona la Firma
        sap.setVisibleSignature(new Rectangle(10, 10, 50, 30), 1, "sign_ggs");
        pdfStamper.setFormFlattening(true);
        pdfStamper.close();
    }
    catch (Exception key) {
        key.printStackTrace();
        return false;
    }
    return true;
}

Pendientes

Lo que quedaría pendiente, y si alguien lo completa les agradecería incluya un comentario es:

  1. Probar con versiones diferentes de JRE.
  2. Investigar en versiones anteriores de iText si ya estaba esta característica incluida.

So far, so good!

Imágenes by Tinypic

11/28/2008

Applets - Ayudando el Deployment

Para complementar el artículo anterior acerca de JTwain, es que escribo este nuevo, incorporando varios tips importantes en el momento de poner en producción un applet, especialmente si éste debe tener acceso a ciertos recursos locales de la máquina cliente, o ejecutar acciones que requieran de permisos especiales.  

También abordo temas como el empaquetado (en archivos JAR) con librerías externas, y un método fácil y rápido de testing y debug, utilizando la consola de Java standard.

Primero - Packaging (Empaquetado)

JAR Cuando terminamos de desarrollar un applet, y debemos empezar a probarlo dentro del contexto de la aplicación de negocios, se hace necesario empaquetarlo en un archivo JAR (no entraré en detalles de porqué, dado que hay mucha información al respecto). Lo que ocurre normalmente, es que en nuestro ambiente de desarrollo, el applet funciona perfectamente, pero cuando lo empaquetamos y lo colocamos en un sitio, comienzan los problemas.  Uno de los errores mas  comunes es que en el empaquetado nos olvidemos de hacer referencia a las librerías externas utilizadas en el desarrollo y que se necesitan en tiempo de runtime.

Para solucionar esto, y evitar que el JAR generado sea muy grande, se pueden colocar las librerías mencionadas (JAR de éstas) en una ubicación específica en el mismo sitio, y hacer referencia a ellas en nuestro applet a través del archivo MANIFEST.MF, incluyendo la ruta en la propiedad Class-Path. Por ejemplo el archivo quedaría:

Manifest-Version: 1.0
Main-Class: MiEspacioDeNombres.MiClase.class
Class-Path: lib_externa1.jar lib_externa2.jar  lib_externa3.jar

Algunos detalles importantes son que la separación de los JAR debe ser con espacios, la lista NO debe ser en una sola línea si hay varios JAR (el largo de cada fila no debería superar los 80 caracteres, y cuando pasamos a la fila de abajo, debemos dejar un espacio inicial para que sea reconocida. Un ejemplo completo se puede descargar de aquí.  En este ejemplo las librerías se deberían ubicar en el mismo directorio del JAR de nuestro applet.  

 
Segundo - Permisos de Ejecución

Si el applet no accede a ningún recurso local de la máquina cliente donde ejecuta, no habrá problemas con los permisos de ejecución, de otra forma, es necesario agregar esos permisos en el archivo java.policy, ubicado en el directorio C:\Archivos de programa\Java\jre<version>\lib\security  (obviamente si la distribución del sistema operativo es en inglés será C:\Programs Files). Allí se deben agregar los permisos necesarios de acuerdo a nuestras necesidades. En ésta página encontrarán detalles de suma utilidad para este tema. Por supuesto que una salida rápida es agregar permisos para todo de la forma:

permission java.security.AllPermission;

Esto soluciona rápidamente el problema de funcionamiento, pero abre la puerta a potenciales ataques malintencionados. Mi recomendación es que se tomen el trabajo de ir probando (si al menos no tienen claro que permisos necesitan) y habilitando solo lo necesario. Para este trabajo es útil el siguiente punto de testing y debug.

 
Tercero - Testing y Debug

Cuando desarrollamos un applet utilizamos una versión específica de Java. Otro problema común es que no sabemos cual es la versión instalada en la máquina cliente que lo ejecutará. Para evitar malos resultados, es necesario entonces probar el applet con diferentes versiones JVM.

Para esto es muy útil:

  1. Habilitar la consola de Java en tiempos de ejecución
  2. Ir cambiando la versión de java, con la que el navegador utilizado en las pruebas, ejecutará el applet.

Para lo primero se debe ingresar a Panel de Control > Java > Avanzado y habilitar la consola de la forma:

          Consola

Para lo segundo, en Panel de Control > Java > Java, se habilita o deshabilita la versión que deseamos utilizar con el navegador, de la forma:
            Consola_Ver

Nota: Es necesario cerrar el navegador cada prueba que hagamos para que no use caché.

 

Luego, cuando ejecutamos el applet desde el navegador, llamando a la página donde se encuentra, Java abre automáticamente la Consola y muestra allí cualquier excepción que se produzca, tanto por falta de permisos como por fallas del funcionamiento propio del applet (faltas de librerías externas, etc.).
                            Consola_Debug

So far, so good!

 Images by Tinypic

11/7/2008

Comparación de Navegadores

Logos Desde hace buen tiempo que vengo probando los 3 navegadores mas usados últimamente en Windows, o al menos los 3 mas nombrados: Internet Explorer - IE (Microsoft), Firefox (Mozilla) y Chrome (Google).

Como siempre ocurre, existen pro y contras en cada uno, y aquí enumeraré algunas de esas características, como ayuda para seleccionar uno de los 3, de acuerdo a cada necesidad.

Tabla comparativa

Esta tabla comparativa permite tener una idea rápida de los pro y contras de cada navegador:

  Característica Chrome Firefox IE
ok Gráfica Diseño minimalista. Cambio algunos conceptos arraigados de otros navegadores. Permite personalizar el diseño gráfico a través de plantillas que se descargan del site. No permite cambios además de los colores básicos desde Windows.
ok Soporte Applet Si - Pero solo versión de java versión  1.6.0_10-rc-b28 (mas adelante explico por que). Si - Desde versión 1.3 probé que funciona bien. Si - Desde versión 1.3 probé que funciona bien.
ok Soporte ActiveX No No Si
ok Soporte PDF Si Si Si
ok Soporte Flash Si Si Si
ok Rapidez (de 1 a 10) 9 7 8
ok Usabilidad (de 1 a 10) 7 (Cambió algunas convenciones ya establecidas por los demás navegadores) 9 9
ok Lengüetas Si Si Si (pero desde versión 7 en adelante)
ok Robustez (de 1 a 10) 8 9 9
ok Estabilidad (de 1 a 10) 9 7 Versión 6 - 9
Versión 7 - 7
Versión 8 - 6

El análisis podría seguir, con varios puntos mas, pero con estos desarrollados creo que se pueden sacar algunas conclusiones.

Temas Importantes

Un tema importante que agregó Chrome, es que cada lengüeta se ejecuta como un thread separado, lo que permite que si alguna de las lengüetas falla, no es necesario cerrar las demás, solo se cierra esa. Ese es el motivo por el cual fue desarrollado y permite ejecución de applets con la última versión de JRE (versión  1.6.0_10-rc-b28). Esta versión hace factible esta característica. De todas formas, "no todo lo que brilla es oro". Me pasó algunas veces que al fallar una lengüeta, igualmente tuve que cerrar todas las demás. Otro tema destacable es que es Open Source, por lo que puede ser descargado el código fuente para generar una versión de navegador personalizada.

Otro tema relevante de mencionar es el soporte de ActiveX. No se la causa, aunque se me ocurre que es netamente comercial (licenciamiento), por el que Firefox (Mozilla) o Chrome (Google) NO soportan ActiveX. La realidad indica que existen varias soluciones web que se basan en ActiveX, lo que hace que estos navegadores pierdan mercado con respecto al IE (Microsoft).  Quizá alguien pueda comentarme cual es el motivo real de este tema.

Conclusiones

  Si el objetivo del navegador es para acceso a webmail,  lectura de diarios on line, búsqueda de información en general, Chrome es una buena opción, dado que es un navegador liviano, rápido, y con su diseño minimalista (y opciones reducidas también), hace la vida fácil a los usuario inexpertos. Una vez que se acostumbran a la nueva propuesta de diseño, es fácil de manejar y configurar.

  Si usa Windows como  sistema operativo, IE (Microsoft) viene incluido en él. Por lo que al instalar el sistema operativo, también se configura el navegador. Esto hace que no se necesite descargar ni instalar nada externo. Facilita la instalación y soporta tanto Applet como ActiveX.

Si es un fanático de la imagen, Firefox (Mozilla) le dará a través de los temas y complementos, gran variedad de opciones para jugar.

Ahora, decida y disfrute.

So far, So good!

Imágenes by Tinypic

11/5/2008

JTwain - Utilizando el scanner desde Java

Con motivo de una consulta, estuve probando y analizando librerías, algunas gratuitas y otras licenciadas, para el manejo de scanners en Java. Todas ofrecen características similares y permiten manejar un scanner bajo el standard TWAIN, claro que con algunas diferencias sustanciales, las cuales son relevantes en momento de seleccionar la correcta para nuestro proyecto.

Aquí expongo algunas experiencias de la librería seleccionada en mi caso y comparto información útil y ejemplos, para aquellos que necesiten desarrollar alguna aplicación basada en este tema. además dejo referencias de otras librerías, y temas relacionados, como el reconocimiento de códigos de barras de las imágenes capturadas y OCR.

Librerías

Algunas librerías que puedo mencionar, son:

  Librería Descripción
image mmscomputing Es una librería Open Source, que además del manejo de scanner para Windows, implementa Sane (manejo de scanner para Linux), fax, etc. Está disponible para Java y funciona perfectamente con JRE 1.4.2 o superior. Tiene un foro bastante completo y el coordinador (Michael Meiwald) responde muy rápidamente ante consultas. Sus respuestas son de gran ayuda. Además, existen ejemplos de uso de la librería muy completos y operativos, que ayudan mucho en el momento de armar nuestra solución.
image JTwain Es licenciada, y permite el manejo del scanner, además de tratamiento de imágenes TIFF multipágina. El valor depende del modo de licenciamiento, dentro del rango de USD 198 a Euros.
image Morena Es el sucesor de JavaTwain. También cubre tanto Twain como Sane. Es una librería licenciada

Existen algunas mas, que no las probé, pero que cubren aproximadamente los mismos temas.

Trabajando con el standard

Todas las librerías permiten seleccionar el scanner con el que deseamos trabajar, así como el formato de imagen a capturar (JPG, TIFF, etc), calidad  y tamaño de la misma.

En el caso de la librería que finalmente recomendé, mmscomputing, se debe implementar una interface en donde existe un método llamado negotiate, donde se realizan todos los ajustes de calidad de imagen, tamaño, y algunos otros, de acuerdo al standard TWAIN y las características soportadas de éste en el scanner seleccionado. Por ejemplo:

private void negotiate(ScannerIOMetadata metadata) {
    ScannerDevice sd = metadata.getDevice();
    try {
       //Anulo pantalla de configuracion del scanner
        sd.setShowUserInterface(false);
        //Seteo que muestre barra progreso del scanner
        sd.setShowProgressBar(true); 
       //Seteo area de escaneo 
        sd.setRegionOfInterest(TopLeft,TopRigth,Width,Heigth);
        sd.setResolution(dDPI);  
    } catch (Exception e) {
        addToLog("Error configurando scanner [" + e.getMessage() + "]",true);
        metadata.setCancel(true); 
    }

    if (metadata instanceof TwainIOMetadata) { 
        TwainSource source = null;
        try {
            source = ((TwainIOMetadata) metadata).getSource();
        } catch (Exception ex) {
            if (source!=null) source.setCancel(true);
        }

        try {
            /* Habilito esto si deseo saber que está implementado del standard para este scanner 
            TwainCapability[] cap = source.getCapabilities();
            for (int h = 0; h < cap.length; h++) {
               System.out.println(cap[h].getName());
            } 
*/
            
            source.setShowProgressBar(true);
            source.setCapability(TwainConstants.ICAP_UNITS,  TwainConstants.TWUN_INCHES);

         //Seteo colo de la imágen a capturar                                                
if (isColor) {
                source.setCapability(TwainConstants.ICAP_PIXELTYPE,                                      TwainConstants.TWPT_RGB);
            } else if (isGrayScale) {
                source.setCapability(TwainConstants.ICAP_PIXELTYPE,                                      TwainConstants.TWPT_GRAY);
            } else if (isBW) {
                source.setCapability(TwainConstants.ICAP_PIXELTYPE,                                      TwainConstants.TWPT_BW);
            }
        } catch (Exception e) {
            if (source!=null) source.setCancel(true);
            addToLog("Error configurando scanner [" + e.getMessage() + "]",true);
            e.printStackTrace();
        }
    }

Formatos de Salida

Si la necesidad es escanear imágenes de una página, lo mejor es utilizar JPG, que guarda una beuna relación entre tamaño y calidad (es una imagen con compresión con pérdida).

Si la necesidad es escanear imágenes multipágina, es necesario entonces usar TIFF.

Un problema que encontré, es que varias de ellas están generando archivos archivos .TIFF pero de la versión mas nueva (6.0) pero el visualizador de Windows, NO lo reconoce, porque por alguna razón nunca se actualizó a ese formato. Conclusión, si se desea visualizar la imagen generada en TIFF, ya sea multipágina o no, se debe usar otro software. Por ejemplo yo uso IrfanView, que es gratuito.

Por último, es bueno mencionar, que existen otras opciones de generación que no sean imágenes. Por ejemplo, una opción es PDF. Si usamos por ejemplo, una librería gratuita llamada iText, podemos generar documentos PDF multipáginas, con el agregado que este caso podemos incluir imágenes de distintos tamaños, colores y calidades, siendo que en el caso de TIFF no es factible. 

OCR y Códigos de Barras

Cuando lo que escaneamos es un formulario por ejemplo, que contiene uno o más códigos de barra, a veces es útil reconocerlos, y con el valor obtenido, hacer un control para nuestra aplicación. Para ello es necesario otras librerías como por ejemplo Aspose (es licenciada con un valor aproximado entre 390 y 1200 Euros, dependiendo del tipo de licenciamiento). Esta librería permite reconocer en una imagen JPG o TIFF un código de barras de diferentes tipos (CODE128, PDF417, etc).

El funcionamiento es muy sencillo, dado que existen primitivas para generar y reconocer códigos, así como también letras (OCR) con una buena calidad final.

Comentario Final

Son varias las opciones existentes, y se pueden combinar de muchas maneras, de tal forma que el proyecto en curso tenga final feliz.

So far, So good!

Imágenes by TinyPic

 
¡Gracias por tu visita!
Please wait...
Sorry, the comment you entered is too long. Please shorten it.
You didn't enter anything. Please try again.
Sorry, we can't add your comment right now. Please try again later.
To add a comment, you need permission from your parent. Ask for permission
Your parent has turned off comments.
Sorry, we can't delete your comment right now. Please try again later.
You've exceeded the maximum number of comments that can be left in one day. Please try again in 24 hours.
Your account has had the ability to leave comments disabled because our systems indicate that you may be spamming other users. If you believe that your account has been disabled in error please contact Windows Live support.
Complete the security check below to finish leaving your comment.
The characters you type in the security check must match the characters in the picture or audio.
Espero tu feedback!
July 28