sábado, 25 de octubre de 2008

Sharp Develop


Sharp Develop


Buenas noches mis queridos cinco lectores. Como ya alguna vez he comentado antes, una parte de mi trabajo que me gusta es la investigación y desarrollo de soluciones nuevas. Soy programador, así que eso implica el desarrollo de sistemas o programas que hagan cosas que ninguno de los que ya tenemos desarrollados hacen.


Esto me gusta porque es muy interesante e incluso hasta si el objetivo no es alcanzado (cosa que no recuerdo que halla pasado, solo se tarda más) siempre aprendo algo, y por otro lado, al ser una cosa novedosa, no hay una medida fiable de los tiempos por lo que este tipo de desarrollos no me imponen un cronograma de trabajo y entregas que cumplir a rajatabla. Estará cuando este. También tiene la ventaja de que "estará cuando este" no le gusta a los jefes, así que me dan recursos y facilidades para acelerar el trabajo: internet libre y que yo no atienda llamadas si no es indispensable.


Bueno, basta de charla introductoria y vamos a lo que te truje... ¿Y de qué nos vas a hablar ahora Gnoblis? Pues de la investigación de turno que me ha absorbido los últimos días y que da titulo a esta entrada.

LA SITUACIÓN

La situación es esta; necesito hacer un repetitivo tramite en una página que implica el llenado de un formulario en una página web que no esta bajo mi control por pertenecer a un tercero, entonces le hago llegar la información a un web browser personalizado que hice basándome en el control ActiveX WebBrowser para controlar el motor de Internet Explorer (esto correrá sobre un Windows XP, es seguro que tenga IE), y así controlo automáticamente todo el proceso. La novedad es que introdujeron en la página una de esas imágenes de códigos generadas al azar con texto muy ofuscado para que no lo entienda ningún proceso automático de reconocimiento de caracteres.


El plan es poder obtener la imagen que el usuario debe reconocer, enviársela y obtener lo que el usuario tecleo. Para obtener la imagen probé varias cosas. La primera fue apoyarme con una aplicación que desarrolle para tomar una captura de pantalla automáticamente y con la capacidad de tomar todo el escritorio o recortar un recuadro indicado como parámetros. Lo primero fue solo incluir una llamada a esta aplicación a la que bautice ScreenShoter y de la que tal vez les cuente en otra ocasión. Pues bien, funciono pero solo es un uso temporal mientras se desarrolla la solución definitiva porque tiene dos graves inconvenientes.

- El explorador debe estar al frente al momento de la captura de la imagen.
- No funciona si se cierra la sesión, es decir, requiere un monitor al servidor siempre.

Debido a esto se requiere un diseño en la aplicación que permita la convivencia de múltiples instancias del programa de manera que tomen turnos para tomar el foco y ponerse al frente, y no se puede trabajar en esa PC porque si atraviesas algo frente a los exploradores la imagen que deseamos no saldrá. Por eso, esto fue un parche temporal para mantener la producción mientras al investigación real seguía.


Tenemos algunas limitantes, como el tiempo de desarrollo. Necesitamos una solución razonablemente rápida. También el uso de C# con el Framework 1.1 debido a que la plataforma de desarrollo de la empresa es Visual Studio 2003 y porque la aplicación base (el explorador personalizado) esta desarrollada con esto, y esta funcionalidad solo es un agregado al programa ya existente.


No se ha avanzado a un framework mayor porque las aplicaciones se distribuyen a muchísimas terminales y enviarlas requeriría la instalación de ese framework a todas las terminales, cosa que cansa solo de pensarlo. Se hará algún día pero hoy no.

INTENTOS QUE FRACASARON MISERABLEMENTE

Un compañero intento realizar una aplicación para captura de imagen de ventanas utilizando C++ y el control de PIXEL para tomar directamente la imagen desde la memoria de vídeo, a un nivel más bajo que el actual ScreenShoter. Esta aplicación recibiría por parámetro el Handler de la ventana a la que queríamos tomar la imagen y así se controlaba el uso de múltiples instancias o que la ventana escogida no tuviera el foco. Y funciono hasta que se cerraba la sesión, a partir de ahí solo obtenemos recuadros negros. Al parecer si no hay sesión abierta, esta memoria no se utiliza, así que este método seguía necesitando un monitor y una sesión abierta.


Al tiempo que pasaba eso yo empecé a desarrollar un método para obtener la imagen directamente del axWebBrowser de la misma aplicación que controla la imagen.


Usando el objeto IHTMLElementRender y su metodo DrawToDC se puede pasar a una imagen de mapa de bits la figura de un elemento de la página abierta en el ActiveX WebBrowser o en una instancia activa de Internet Explorer (mediante handlers). Aunque en realidad solo logre hacerlo exitosamente en C++, donde puedes montar el mismo control AxWebBrowser usando MFC o API para crear ventanas pero esa es otra historia y no era el caso ponerse a reescribir la aplicación entera a C++ solo porque yo no encontré el modo de hacerlo funcionar en C#, que se debe de poder si investigo más a fondo pero no hay tiempo.

// Obtener el contenido de la página cargada en el explorador
IHTMLDocument2 doc = (IHTMLDocument2)axWebBrowser.Document;
IHTMLElement body = (IHTMLElement)doc.body;
IHTMLElementRender render = (IHTMLElementRender)body;

El código anterior copia el cuerpo de la página a un objeto IHTMLElementRender, que usaremos para dibujar el objeto en una imagen de mapa de bits que será igual a lo que se ve en el IExplorer. No es necesario usar el render para toda la página, pueden manejarse objetos individuales como en mi caso que solo quiero una imagen en especial. Por ejemplo, puedes localizar un elemento por su posición en la página si conoces sus coordenadas invocando a IHTMLDocument2.elementFromPoint(int x, int y); donde X y Y son las coordenadas. También esta IHTMLDocument2.images para obtener la colección de imágenes en la página y ya de ahí tomar la necesaria. Yo use esto ultimo porque así no me afecta la resolución de la maquina donde ponga la aplicación. Hay otras formas de obtener elementos específicos y ya no profundizare en ese aspecto.


El metodo DrawToDC para dibujar el control necesita un apuntador a una imagen de mapa de bits donde guardar la imagen que dibujará, así que además del elemento cargado en el render, también necesitamos un apuntador, una imagen de mapa de bits y un objeto Graphics.

// Imagen de mapa de bits que contendra el elemento dibujado
Bitmap imagen = new Bitmap(ancho, alto);
Graphics grafico = Graphics.FromImage(imagen); // Ligar el Bitmap con el objeto Graphics
IntPtr hdc = grafico.GetHdc(); // Crear el apuntador al objeto grafico

Hasta aquí todo va muy bien, pero todos sabemos que cuando algo va tan bien es porque hay algo muy malo que no viste, y en este caso el punto esta en que el parámetro que recibe el metodo DrawToDC del render no es un puntero cualquiera, no, que va. Es un objeto del tipo mshtml._RemotableHandle que nunca supe bien como manejar. Si envías un puntero normal a DrawToDC, este simplemente marca error por parametro de tipo incorrecto. Intente crear una instancia de mshtml._RemotableHandle y pasar el valor del puntero común a este, pero a pesar de que ya no marcaba error, en realidad nunca logre resultados exitosos.

// Inicializar un objeto _RemotableHandle
mshtml._RemotableHandle manejador = new _RemotableHandle;
manejador.fContext = 0x48746457;
// Esto depende de tu sistema operativo y procesador si es de 32 o 64 bits
manejador.u.hInproc = grafico.GetHdc().ToInt32();
render.DrawToDC(ref manejador);

En C++ no tienes estos problemas porque puedes generar el puntero con la instrucción CreateCompatibleDC que funcionan para DrawToDC, así que me plantee agregar la DLL gdi32.dll como referencia a mi proyecto para utilizar el método como en C++ pero otra cosa que probé antes me detuvo. Procedí a sobrecargar el método DrawToDC para que aceptara punteros comunes. Agregue el siguiente código al principio de mi clase

[Guid("3050f669-98b5-11cf-bb82-00aa00bdce0b"), InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown),
ComVisible(true),ComImport]
interface IHTMLElementRender
{
void DrawToDC([In] IntPtr hDC);
void SetDocumentPrinter([In, MarshalAs(UnmanagedType.BStr)] string bstrPrinterName, [In] IntPtr hDC);
};

Ya con esto podemos enviar variables IntPtr al metodo DrawToDC del objeto render como algo normal.

// Guardar la página en la imagen bitmap
IHTMLDocument2 doc = (IHTMLDocument2)axWebBrowser.Document;
IHTMLElement body = (IHTMLElement)doc.body;
IHTMLElementRender render = (IHTMLElementRender)body;
Bitmap imagen = new Bitmap(ancho, alto);
Graphics grafico = Graphics.FromImage(imagen);
IntPtr hdc = grafico.GetHdc();
// Liberar memoria que ya no necesitamos
grafico.ReleaseHdc(hdc);
grafico.Dispose();
// Guardar la imagen en el disco duro como .jpg
imagen.Save("c:\\captura_de_pantalla.jpg", ImageFormat.Jpeg);

En la bella teoría funciona, pero ya en la practica, al igual que la primera aplicación, al cerrar la sesión solo salva recuadros negros. Hora de aplicar el plan C... solo que en ese momento me habría gustado tener más claro cual era ese plan.

EXITO

Después de investigar un rato dí con el método DrawToBitmap. Esta instrucción es capaz de dibujar un control de una ventana como una imagen de mapa de bits, solo que no existe en el NetFramework 1.1, pues apareció con la versión 2.0


En fin, me la jugue. Para poder hacer el desarrollo instale SHARP DEVELOP 2.2, un IDE de programación .NET opensource gratuito. Hice una copia del proyecto, la cargue con Sharp Develop y lo actualice al nuevo framework y me puse a implementar el cambio.

// Declarar una imagen del tamaño del explorador
Rectangle area = new Rectangle(0, 0, axWebBrowser.Width, axWebBrowser.Height);
Bitmap imagen = new Bitmap(axWebBrowser.Width, axWebBrowser.Height);
// Obtener la imagen de la página cargada en el explorador
axWebBrowser.DrawToBitmap(imagen, area);

¡Y voila!, con eso tenemos la imagen de la parte visible de la página. Según investigue, para obtener toda la página y no solo la parte visible esta la posibilidad de usar un método sobrecargado para inicializar el objeto Rectangle para tomar toda la página.

Rectangle area = new Rectangle(0, 0, axWebBrowser.Document.ActiveElement.ScrollRectangle);

Lo intente pero siempre me dio una excepción "'object' does not contain a definition for 'ActiveElement' (CS0117)" y como yo no necesito toda la página pues no me puse a buscar a fondo porque ni como usarlo, pero ahí queda para que quien necesite una imagen de toda la página completa pueda ver si le sirve como punto de partida.


Bueno, como ya había mencionado, yo no quería toda la página, solo una parte. Para obtener la parte que quiero hice lo siguiente.

// Imagen que guardara el recuadro que me interesa
Bitmap recorte = new Bitmap(ancho, alto, PixelFormat.Format24bppRgb);
// Área de la imagen que quiero obtener
area = new Rectangle(x, y, ancho, alto);
// Tomar este recuadro de la imagen original completa
recorte = imagen.Clone(area, PixelFormat.Format24bppRgb);
// Guardar la imagen en el disco duro como .jpg
recorte.Save("c:\\imagen.jpg", ImageFormat.Jpeg);

La función Bitmap.Clone() me permite obtener una copia de una imagen, ya sea completa o solo de las coordenadas y tamaño de un rectángulo indicado. También use en esta aplicación el parámetro PixelFormat.Format24bppRgb porque la aplicación que hará uso de estas imagenes solo soporta esa compresión como máxima calidad. El poder dar formato desde aquí nos ahorro un paso porque el ScreenShoter toma las imágenes a 32 bpp y usábamos una instrucción de ImageMagick para convertirlas antes de que la aplicación final empezará a trabajar con las imágenes.


Y bueno, mis cinco lectores. Perdón por aburrirlos pero esta semana eso hice en el trabajo y queria publicar lo que encontré por si a alguien más le sirve, pues yo también busque en páginas durante la semana, todo la investigación y desarrollo termina expresado como unas lineas de código.

0 comentarios:

Publicar un comentario

Por favor trata de escribir bien, no te pido que no te falte ni un acento pero por favor evita escribir como metroflogger o facebookero. Este blog es un sitio decente. Gracias.

Subscribe to RSS Feed Follow me on Twitter!