Uso de la memoria como un dispositivo de E/S con QBuffer

Uso de la memoria como un dispositivo de E/S con QBuffer

Algunas libreras se diseñan específicamente para leer y/o escribir datos en dispositivos de entrada/salida. Por ejemplo, una librería desarrollada en C++ para codificar y decodificar archivos MP3 recibiría objetos std::istream o std::ostream para indicar el flujo del que leer o en el que escribir los datos respectivamente:

  • std::istream Clase de flujo de entrada. Los objetos de esta clase pueden leer e interpretar la entrada de datos a partir de secuencias de caracteres.

  • std::ostream Clase de flujo de salida. Los objetos de esta clase pueden escribir secuencias de caracteres y representar como cadenas otras clases de datos.

Por lo tanto, si deseáramos guardar o recuperar audio codificado en MP3 en un medio de almacenamiento concreto, necesitaríamos disponer de una clase que nos ofrezca el acceso al mismo a través de la interfaz de flujos de C++ de entrada/salida descrita por las clases anteriores.

Objetos como std::cin y std::cout son instancias de estas clases, lo que permitiría que nuestra aplicación codificara y decodificara audio hacia y desde la entrada/salida estándar del proceso. Lo mismo ocurre con std::ifstream, std::ofstream y std::fstream; que heredan de las clases anteriores e implementan la interfaz de flujos de C++ para los archivos. De esta manera, nuestra hipotética librería de MP3 podría codificar y decodificar datos en este formato hacia y desde dichos archivos.

En este punto la cuestión que se nos plantea es ¿qué podríamos hacer si, por ejemplo, quisiéramos codificar el audio para posteriormente transmitirlo a otro ordenador a través de una red? Teniendo en cuenta lo comentado hasta ahora, una opción sería codificarlo, almacenándolo en un archivo y posteriormente leer del mismo para transmitir los datos por la red. Obviamente, esto dista de ser ideal porque sería preferible disponer de una clase que implementara la interfaz de std::ostream para almacenar los datos codificados directamente en la memoria, evitando tener que dar pasos intermedios, como por ejemplo guardarlos y leerlos de un archivo temporal.

Por fortuna, la librería estándar de C++ nos provee de las clases:

  • std::istringstream Flujo de entrada para operar sobre cadenas. Los objetos de esta clase permiten leer los caracteres del flujo de entrada a partir del contenido de un objeto std::string.

  • std::ostringstream Flujo de salida para operar sobre cadenas. Los objetos de esta clase permiten escribir los caracteres del flujo de salida en un objeto std::string.

  • std::stringstream Flujo para operar sobre cadenas. Los objetos de esta clase permiten leer y escribir en un objeto std::string.

Estas clases nos ofrecen una forma sencilla de almacenar o leer de la memoria del proceso los datos codificados en MP3, lo que facilitaría cualquier tarea que quisiéramos realizar con ellos posteriormente.

QIODevice

En el framework Qt ocurre algo muy parecido. Todas las clases diseñadas para acceder a dispositivos de entrada/salida reciben un objeto de la clase QIODevice. Por ejemplo:

  • QTextStream Proporciona una interfaz adecuada para leer y escribir datos en formato texto desde un QIODevice.

  • QDataStream Proporciona una interfaz adecuada para leer y escribir datos binarios desde un QIODevice.

  • QImageReader Proporciona una interfaz independiente del formato para leer archivos de imágenes desde un dispositivo QIODevice.

  • QImageWriter Proporciona una interfaz independiente del formato para escribir archivos de imágenes desde un dispositivo QIODevice.

  • QMovie Es una clase diseñada para reproducir películas leídas con QImageReader desde un QIODevice.

La clase QIODevice no sé instancia directamente para crear objetos, sino que su función es definir una interfaz genérica válida para todo tipo de dispositivos de entrada/salida. En el framework Qt diversas clases heredan de esta, implementando la interfaz QIODevice para un dispositivo concreto:

Ejemplos con imágenes

Para ilustrar lo comentado vamos a codificar y decodificar una imagen QImage directamente desde la memoria.

Codificando una imagen en la memoria

Supongamos que image es un objeto QImage que queremos codificar en formato PNG, guardando el resultado en un array en la memoria para su procesamiento posterior —por ejemplo para transmitirlo a través de una red de comunicaciones —.

Hacerlo es tan sencillo como incorporar las siguientes líneas al programa:

QBuffer buffer;
image.save(&buffer, “png”);

Como hemos comentado, QBuffer es una clase heredada de QIODevice, por lo que podemos usarla allí donde se requiera un dispositivo de entrada/salida. Por defecto, los objetos de QBuffer se crean con un buffer interno de tipo QByteArray, al que podemos acceder directamente invocando el método QBuffer::buffer()`. Por ejemplo:

QByteArray bytes = buffer.buffer();

// Guardamos en un string los primeros 6 bytes de la imagen en PNG
std::string pngHeader(bytes.constData(), 6);
std::cout << pngHeader << std::endl;

lo que mostraría por la salida estándar algo como lo siguiente:

�PNG

Esta forma de guardar los datos es adecuada cuando no necesitamos más control sobre las opciones del formato en cuestión. Si, por el contrario, queremos controlar el nivel de compresión, el de gamma o algunos otros parámetros específicos del formato, tendremos que emplear un objeto QImageWriter:

QBuffer buffer;
QImageWriter writer(&buffer, “jpeg”);
writer.setCompression(70);
writer.write(image):

Decodificando una imagen almacenada en la memoria

Ahora vamos a hacerlo en sentido inverso. Si tenemos un puntero const char* bytes a una zona de memoria con size bytes donde se almacena una imagen en formato PNG y queremos cargarla en un objeto QImage, solo tenemos que asignar los datos a un objeto QBuffer y leer desde él:

QBuffer buffer;
buffer.setData(bytes, size);
QImage image();
image.setDevice(&buffer);
image.setFormat(“png”);
image.read();

o lo que es equivalente y mucho más simple:

QByteArray buffer(bytes, size);
QImage image();
image.loadFromData(buffer, “png”);