15 de Septiembre de 2010

Todos sabemos que en el entorno gráfico de Linux se puede capturar la imagen de pantalla simplemente pulsando la tecla ImprPant. Pero, ¿cómo se puede capturar la imagen de un terminal virtual? Por si andas despistado, un terminal virtual (o VT de Virtual Terminal) es la pantalla de texto con la que arranca el sistema, y a la que puedes volver con Ctrl+Alt+F<n>. A continuación presento un programita que captura la imagen visible en el VT y la graba en un fichero PNG. Si no puedes esperar, descárgalo ya aquí: vcsa.py.zip.

Introducción

La idea principal de este programa es sencilla: leer el contenido del VT indicado usando de la interfaz que proporciona el Sistema Operativo y luego componer una imagen dibujando los caracteres correspondientes en las posiciones correctas. El programa está escrito en Python y utiliza PyCairo para componer la imagen y convertirla a PNG. Sí, sería más útil que fuera un programa C, compilado en estático, sin dependencias externas, para que se pudiera utilizar para capturar procesos de instalación y consolas de emergencia... pero eso lo dejo como "ejercicio para el lector". Además así aprovecho para mostrar un ejemplo de cómo crear imágenes y cómo acceder a ioctls desde Python, que siempre es interesante.

El contenido del VT

En linux los terminales virtuales o VT se llaman /dev/ttyN, siendo N un número mayor o igual a 0. En mi sistema Ubuntu tengo desde el 0 hasta el 63. El 0 representa la consola, y es un poco especial, pero no nos interesa ahora. El resto son los VT del número correspondiente. Puedes activar del 1 al 9 pulsando las teclas Ctrl+Alt+F1 a Ctrl+Alt+F12. El resto las puedes activar con el comando chvt, aunque la mayoría seguramente estén vacías. Se puede acceder al contenido de la pantalla de un VT leyendo o escribiendo en los dispositivos /dev/vcsN y /dev/vcsaN, siendo N el número del VT. La diferencia entre vcs y vcsa es que el último incluye los colores de cada carácter. Estos nombres significan Virtual Console Screen y Virtual Console Screen with Attributes, o algo así. El que nos interesa es el vcsaN, que con colores todo queda más bonito. El contenido de este dispositivo es muy sencillo:

  • El primer byte indica el número de filas de la pantalla.
  • El segundo byte indica el número de columnas de la pantalla.
  • Los bytes tercero y cuarto son las coordenadas del cursor de texto (caret). No me voy a molestar en pintar el cursor, así que voy a ignorarlos.
  • Por cada carácter de pantalla, de izquierda a derecha y de arriba a abajo, un entero de 16 bits. Los 8 bits menos significativos dicen qué caracter hay en esa posición, y los 8 más significativos son los atributos (color). Vale, esto no es exactamente así, pero eso lo resolveremos más adelante.

La imagen

Para crear la imagen utilizo PyCairo. El siguiente código crea la imagen e inicializa la fuente:

from cairo import *

surf = ImageSurface(FORMAT_RGB24, cols * 8, rows * 16)
cr = Context(surf)
cr.set_source_rgb(1,1,1)
cr.set_font_face(ToyFontFace("Mono", FONT_SLANT_NORMAL, FONT_WEIGHT_NORMAL));
m = Matrix()
m.scale(10.0, 12.0)
cr.set_font_matrix(m)

El programa asume que cada casilla de texto es de 8x16 píxeles. Esta información no la proporciona el sistema, pero este es el tamaño más habitual y queda bastante natural. La matriz m es la transformación de la fuente. Para ser fino tendría que ver cuál es el tamaño de la fuente seleccionada y escalarla en un factor apropiado para llenar las casillas de 8x16 píxeles. En la práctica, he ido tanteando varios valores hasta que quedó más o menos bien. Utilizo la ToyFontFace que viene de serie con Cairo, en lugar de algo más sofisticado, como Pango, porque no quiero formatear texto, sino dibujar caracteres individuales en puntos concretos de la imagen, y eso la Toy Font de Cario lo hace perfectamente bien.

Luego para dibujar un carácter:

cr.set_source_rgb(*backColor)
cr.rectangle(x*8, y*16, 8, 16)
cr.fill()

cr.set_source_rgb(*frontColor)
cr.move_to(x*8, 12 + y*16)
cr.show_text(char)
cr.stroke()

Primero se dibuja un rectángulo con el color de fondo, y luego el carácter en cuestión con el color frontal. Fíjate que hay que sumar 12 a la coordenada Y porque la función Cairo.show_text dibuja el carácter a partir de la baseline, es decir la parte baja del carácter.

Una vez finalizada la imagen se guarda a disco con la siguiente línea:

surf.write_to_png("file.png")

La codificación de caracteres

En varios artículos previos contaba cómo hay que distinguir siempre cuándo tenemos caracteres y cuándo tenemos bytes. Del dispositivo vcsaN leemos bytes, y se puede comprobar fácilmente que los caracteres ASCII se codifican directamente, es decir los bytes del 32 al 127 representan los caracteres ASCII correspondientes. Pero (siempre que utilice un Linux mínimamente moderno) puedo escribir una "Ñ", por ejemplo, en un VT. La "Ñ" no es un carácter ASCII, así que ¿cómo se codifica en bytes? Eso es fácil de averiguar: ecribe una "Ñ" en el VT1 y mira a ver qué aparece en vcsa1. En mi ordenador aparece 0xEA, que no se corresponde al carácter "Ñ" en ninguna codificación que yo conozca... Investigando por ahí, resulta que la codificación utilizada por el VT está vinculada a la fuente que se ha definido para ese terminal. Esto se hace con el comando setfont, y la mayoría de las distribuciones lo utilizan en el proceso de arranque con unos valores generalmente razonables. La configuración de codificaciones de los VT en el arranque es un completo lío, y además ha cambiado varias veces a lo largo de la historia, por lo que no vale la pena adentrarse demasiado en él. Baste saber que existe un comando ioctl para obtener el mapa de caracteres en uso de un VT. Para quien no lo sepa, ioctl es una llamada al sistema que permite acceder a una cierta función de un driver directamente. Qué parámetros recibe y qué datos devuelve dependen totalmente de qué comando ioctl se esté utilizando. Nuestra dificultad aquí es que los comandos ioctl se definen (y a menudo se documentan) solamente en los ficheros *.h del Sistema Operativo. Utilizarlos en un programa en Python, por lo tanto, requiere cierta reflexión.

Llamando a ioctl

La función ioctl de Python reside en el módulo fcntl y recibe tres parámetros: el primero es el descriptor de fichero del dispositivo al que se le envía el comando; el segundo es el código del comando (un entero); el tercero es un array con los parámetros del comando. Al retorno de la función, el array puede haber variado su contenido, según el comando, y el valor devuelto es el que devuelva el sistema. Hay otras formas de llamar a esta función, pero esta es la más flexible y fiable.

El comando ioctl que nos interesa es el GIO_UNIMAP, pero hay que ejecutarlo sobre el /dev/ttyN correspondiente, no sobre /dev/vcsaN. Si abrimos el fichero /usr/include/linux/kd.h vemos que su valor numérico es 0x4B66. Justo a continuación podemos ver la definición de las estructuras asociadas:

#define GIO_UNIMAP  0x4B66  /* get unicode-to-font mapping from kernel */
struct unipair {
    unsigned short unicode;
    unsigned short fontpos;
};
struct unimapdesc {
    unsigned short entry_ct;
    struct unipair *entries;
};

Es fácil intuir cómo funciona esto: se pasa como argumentos una estructura unimapdesc, donde el campo entries apunta a un array de estructuras unipair, y el campo entry_ct dice cuál es el tamaño de ese array. Al retorno de ioctl, entry_ct se ha actualizado con el número de entradas del array rellenadas. Cada ítem de tipo unipair asocia un carácter Unicode con un valor binario. En el caso de que no quepan en el array devuelve el error ENOMEM, que Python convierte a una excepción IOError.

Para construir estructuras C en memoria desde Python contamos con dos módulos esenciales: struct y array El tipo array.array crea un array en memoria de tantos elementos de un tipo básico (byte, short, int, long, float, double, etc.) como se necesiten. El módulo struct contiene dos funciones, struct.pack y struct.unpack que convierten una secuencia de valores y tipos básicos a la secuencia de bytes correspondiente (un poco como printf pero para estructuras en lugar de texto). La secuencia de bytes se devuelve como un string, aunque existen las funciones struct.pack_into y struct.unpack_from que utilizan arrays. Este módulo tiene muchas opciones para utilizar formatos portables o nativos, alineación de datos, etc. No entraré aquí en esos detalles, pues ya está sobradamente documentado en otros sitios. Baste decir que, puesto que queremos construir una estructura C, debemos utilizar siempre formatos nativos (dependientes de la arquitectura en uso).

Como un trozo de código vale más que mil palabras, aquí está la llamada a este ioctl:

1
2
3
4
5
6
7
8
9
10
11
12
GIO_UNIMAP = 0x4B66

tty = open('/dev/tty%d' % ttyno, 'rb')

sz = 512
unipairs = array("H", [0]*(2*sz))
sdesc = struct.pack("@HP", sz, unipairs.buffer_info()[0])
unimapdesc = array("B", sdesc)
ioctl(tty, GIO_UNIMAP, unimapdesc)

ncodes, = struct.unpack_from("@H", unimapdesc)
utable = struct.unpack_from("@%dH" % (2*ncodes), unipairs)

Esto quizás requiere algún comentario. sz es el tamaño del array de unipairs. En la línea 6, 'H' indica enteros sin signo de dos bytes, o sea, unsigned short, y hacemos un array de 2*sz porque cada estructura unipair tiene dos unsigned short. En la línea 7 se construye una estructura unimapdesc. La '@' indica orden de bytes nativo; la 'H' es unsigned short para el campo entry_ct y 'P' es un puntero. Para obtener el puntero al array anterior usamos la función array.buffer_info, que devuelve en una tupla el puntero al contenido y el tamaño en bytes. En la línea 8 convertimos la estructura recién creada a un array de bytes. En la línea 9 llamamos a ioctl. En la línea 11 se lee el campo entry_ct devuelto por la llamada al sistema. Nótese que la función struct.unpack_from devuelve una tupla, en este caso de un único elemento, y por eso la coma solitaria. En la línea 12 se leen los valores del array y se guardan en una tupla. Son 2*ncodes valores de tipo unsigned short.

Este código se inserta en un bucle, de manera que si da error ENOMEM se duplica el valor de sz y se vuelve a intentar, pues eso es que el mapa de caracteres es más grande. En mi Ubuntu la tabla es de 694 elementos.

La codificación de caracteres, segunda parte

Pero... un momento. Una tabla de caracteres de 694 elementos, ¡eso no cabe en un byte! ¿Cómo es posible? Primero, observando la tabla se puede ver que algunos caracteres comparten código. Si lo piensas bien, esto es lógico, pues no se está codificando el carácter en sí, sino el glifo (en inglés, glyph), es decir, el gráfico correspondiente a ese carácter. Y muchos grupos de caracteres pueden compartir el mismo glifo, por ejemplo la A máyuscula latina (U+0041), la Alpha mayúscula griega (U+0391) y la A mayúscula cirílica (U+0410) tienen exactamente la misma apariencia, por lo que comparten código en pantalla: el 65. Claro, nosotros solo vemos el 65, por lo que no podemos saber si realmente es una letra latina, griega o cirílica. Tampoco es que importe demasiado, pues la vamos a volcar a una imagen, a no ser que en la fuente que utilicemos para crear la imagen haya alguna diferencia particular entre estos caracteres. En cualquier caso, para resolver la ambigüedad, este programa utilizará siempre el carácter Unicode más bajo, que se supone que será el más "normal". Así, el byte 65 se corresponderá al carácter Unicode U+0041: A mayúscula latina.

Reduciendo la tabla a códigos únicos, nos quedamos con 512 elementos en el array, ¡sigue sin caber en un byte! ¿Qué pasa ahora? Resulta que el código del caracter no es exactamente los 8 bits menos significativos de cada uno de los elementos leídos de vcsaN, sino los 8 bits menos significativos más un bit adicional que depende del hardware. Podemos averiguar dónde está el 9º bit usando otro comando ioctl: VT_GETHIFONTMASK. Este comando es muy sencillo así que no voy a comentarlo más.

Los atributos

Los atributos de un carácter se pueden leer en los 8 bits más significativos de cada entero de 16 bits leído de vcsaN. Tres bits son para el color de fondo, tres para el color del carácter, uno es el 9º bit del código del carácter y el otro es el bit de brillo o paleta alternativa. Qué bits forman cada uno de estos elementos depende del resultado del ioctl VT_GETHIFONTMASK, comentado antes. Y qué color corresponde a cada secuencia de bits varía según el hardware y la configuración particular. Incluso el programa que utiliza la consola puede variar esos colores. Por eso ni siquiera he intentado acercarme a los colores reales, simplemente he definido los colores primarios, más o menos.

Todo junto

Aquí puedes descargar el programa: vcsa.py.zip.

Después de esto, la evolución natural del programa es capturar vídeos... Pero resulta que no es necesario cambiar ni una línea de código. El siguiente script captura una serie de imágenes, a 10 FPS:

x = 0
while true
do
./vcsa.py 1 `printf "vt1-%05d.png" x`
x=$((x+1))
sleep 0.1
done

Recuerda que debes ejecutarlo como root.

Una vez capturada la serie de imágenes se puede convertir a vídeo con un comando similar a:

gst-launch-0.10 multifilesrc location=vt1-%05d.png ! image/png,framerate=10/1 ! \
pngdec ! ffmpegcolorspace ! xvidenc ! queue ! \
avimux ! filesink location=vt1.avi

Los detalles de este comando son un tema para otro día.

0 comments to Capturar imágenes de un terminal virtual

Help
:-(icon_sad :-)icon_smile :roll:icon_rolleyes
:-Dicon_biggrin :-Picon_razz :oops:icon_redface
:-xicon_mad :-|icon_neutral :arrow:icon_arrow
8-)icon_cool 8-Oicon_eek :mrgreen:icon_mrgreen
:!:icon_exclaim :?:icon_question :twisted:icon_twisted
;-)icon_wink :evil:icon_evil :idea:icon_idea
:-oicon_surprised :cry:icon_cry
:-?icon_confused :lol:icon_lol

Leave a comment