Flussi di I/O – operazioni elementari Il C++ dispone di una libreria fstream per l’I/O su file, derivata dalla libreria iostream (quella, già incontrata, che contiene le dichiarazioni per l’I/O su console). I flussi vengono descritti tramite classi. Semplificando, come “classe” possiamo intendere un elenco di dati e funzioni (rispettivamente chiamati proprietà e metodi), normalmente collegati tra loro. Variabili chiamate oggetti, dichiarate come appartenenti1 alla classe, dispongono di tutte le proprietà e possono essere elaborate con tutti i metodi associati. Ad esempio, nella libreria iostream sono dichiarate le classi istream e ostream: cin è un oggetto di classe istream; ad esso è associata l’operazione indicata con >> cout è un oggetto della classe ostream; ad esso è associata l’operazione indicata con << Questi oggetti non vanno nemmeno dichiarati, fanno parte delle variabili globali (note in ogni parte di un programma), per la precisione sono due flussi di dati globali (global streams). Nel caso dell’I/O su disco, i flussi vanno invece dichiarati; inoltre vanno associati ad un nome di file (filename) nell’operazione di apertura (altrimenti il file system non saprebbe in quale cartella registrare i dati). È anche buona norma chiudere il flusso alla fine delle operazioni, anche se la chiusura viene effettuata automaticamente alla chiusura del programma. Vediamo nell’esempio come registrare (output) informazioni in formato testo (txt): #include <fstream> using namespace std; int main() { ofstream flow; // stream di output flow.open( "testo.txt"); // apertura (con filename) flow << "Cochrane Foxx Bishop" << endl; flow << "Boley Dykes Simmons" << endl; // registrazione righe flow << "Miller Haas Grove" << endl; flow.close(); // chiusura getchar(); } Nell’esempio specifico, la dichiarazione ofstream implica l’apertura in output (il file viene “creato” se inesistente, sovrascritto se esistente). Come (quasi) sempre avviene con gli oggetti, i metodi vengono attivati con l’operatore punto (dot operator), che segnala l’appartenenza alla classe: secondo la terminologia OOP2, si può dire che “il metodo open appartiene all’oggetto flow“. Il programma appena visto fa in modo che nella cartella di esecuzione venga a trovarsi un nuovo file testo, di nome testo.txt, che può essere aperto con un apposito programma (blocco note, notepad++, …), oppure visualizzato nello shell (prompt dei comandi) con il comando type. Modifica con “blocco note” 1 Esame da shell con type Appartenere ad una classe è un’espressione tecnicamente scorretta, dal momento che una classe è una descrizione, non un contenitore: essendo però entrata nel gergo, il suo uso non provoca problemi. 2 Object Oriented Programming, o “programmazione orientata agli oggetti”. La pronuncia dell’acronimo è molto soggettiva: su www.sitepoint.com è stato anche proposto un sondaggio, secondo il quale la maggior parte scandisce le lettere “O O P”, un po’ meno preferisce la parola intera ”u:p”, poi seguono altre scelte come “double-O P”. Non manca chi disdegna la sigla e usa le parole intere. Questa visualizzazione dei dati è certamente meglio di quella nota, in tempi storici, come dump (discarica), che agli albori dell’informatica era l’unico modo di esaminare i dati. Se volessimo metterci nei panni delle ragazze dell’ENIAC3, il vero contenuto del file comprende dai seguenti 496 bit: 0100001101101111011000110110100001110010011000010110111001100101001000000100011001101111011110000111100000100 0000100001001101001011100110110100001101111011100000000110100001010010000100110111101101100011001010111100100 1000000100010001111001011010110110010101110011001000000101001101101001011011010110110101101111011011100111001 1000011010000101001001101011010010110110001101100011001010111001000100000010010000110000101100001011100110010 000001000111011100100110111101110110011001010000110100001010 (in rosso la "v" di "Grove", valore 118) Un tuffo nel passato: due “ragazze dell’ENIAC”, Gloria Ruth Gordon ed Ester Gerston, girano per il computer collegando un cavo per ogni bit. In realtà, per esaminare il contenuto del file direttamente, in alcuni text editor4 esiste un’opzione che permette di visualizzare l’esatto contenuto del file, byte per byte. Altri programmi, detti hex editor5, si aprono direttamente in questa modalità, mostrando da una parte il dump esatto di ciò che è registrato sul disco, dall’altra i corrispondenti caratteri visualizzabili. Nell’esempio è stato usato il “Free Hex Editor Neo”. Questo esame permette di risolvere un’apparente incongruenza: il file risulta lungo 62 byte, mentre la somma dei caratteri presenti nelle tre righe scritte è di 56. In realtà i byte mancanti sono i tre newline (a capo), ciascuno formato da due caratteri, che hanno i valori decimali 13 e 10. Il primo indica un “carriage return” (ritorno carrello), il secondo un “line feed” avanzamento riga), termini che ricordano le vecchie macchine da scrivere. Non essendoci caratteri visualizzabili corrispondenti, nella parte di testo sono segnalati con dei punti. Alcuni esempi di codifica: CARATTERE cr/lf (newline) blank 'a' (minuscola) 'S' (maiuscola) 3 ESADECIMALE 0d 0a 20 61 53 BINARIO 0000 1101 0000 1010 0010 0000 0110 0001 0101 0011 DECIMALE 13 10 32 97 83 Dette inizialmente “computer”, formavano un gruppo di circa 80 agguerrite scienziate, il cui contributo allo sviluppo dell’informatica fu fondamentale. 4 Sono applicazioni con limitate possibilità di effetti grafici, normalmente usati dai programmatori per scrivere il codice sorgente dei programmi. 5 Così chiamati perché ogni byte è visualizzato in base 16, con una cifra esadecimale per ogni nibble. Per leggere il file testo appena registrato si può usare un flusso ifstream, come nel prossimo esempio. Il nuovo programma richiede un ciclo, dal momento che il testo contiene varie parole. Prestiamo particolare attenzione a come viene scritto il ciclo, per costruire un programma correttamente strutturato. Il ciclo termina quando il metodo eof (end of file) restituisce true. ifstream flow; string z; È fondamentale, nella struttura del ciclo, leggere un dato (solitamente è il primo) prima di entrare. Questo flow.open( "testo.txt"); permette di gestire ogni dato nello stesso modo: altri flow >> z; // 1 costrutti rendono il ciclo più complesso, perché sorge la while( ! flow.eof()) // 2 necessità di verificare se il flusso è terminato o meno. { cout << z << " "; // 3 La tecnica è detta read ahead (leggere prima), ed è flow >> z; // 4 usata anche nella gestione del traffico dei dati tra la } RAM e le memorie periferiche. flow.close(); Si può notare che l’operatore >> considera terminato l’input non solo al “fine linea”, ma anche ad ogni spazio; il risultato della lettura è il seguente: Cochrane Foxx Bishop Boley Dykes Simmons Miller Haas Grove Per leggere le righe esattamente come sono state registrate si usa la funzione getline:, che permette di leggere una riga completa, compreso il doppio carattere di newline: ifstream flow; string z; Il risultato del nuovo programma corrisponde flow.open( "testo.txt"); al modo in cui sono stati registrati i dati: getline( flow, z); // 1 while( ! flow.eof()) // 2 Cochrane Foxx Bishop { Boley Dykes Simmons cout << z; // 3 Miller Haas Grove getline( flow, z); // 2 } flow.close(); Oltre alle due classi di flusso incontrati, ne esiste una più completa, detta fstream, che può essere usata in input o in output a seconda della modalità di apertura. Questa viene specificata come secondo parametro del metodo open, (detto open mode). Ci sono svariate possibilità, tra le quali quelle interessanti sono 3: input, output, append (aggiunta). La sintassi è un po’ criptica, ma si può intuire; un’altra stranezza è il prefisso che descrive l’open mode, che può essere ios (input/output stream) oppure fstream (file stream): Le modalità possono essere multiple, consentendo quindi più operazioni: vanno separate da un operatore |. fstream flow; flow.open("testo.txt", flow.open("testo.txt", flow.open("testo.txt", flow.open("testo.txt", flow.open("testo.txt", flow.open("testo.txt", ios::out); fstream::out); // ios, fstream: EQUIVALENTI ios::in); fstream::in); ios::in | ios::out | ios::app); fstream::in | fstream::out | fstream::app); Ad esempio, un programma simile a quello iniziale, con l’apertura del file testo in append e gli stessi output, aggiunge nuovi dati in coda a quelli già presenti, e il file può essere raddoppiato, come nell’esempio: Binary I/O Le operazioni di I/O su file di solo testo sono molto rare (tranne che, per ragioni probabilmente legate alla semplicità della correzione, nei corsi e negli esami scolastici): nella realtà i dati da scrivere, oltre ad essere più complessi, solitamente necessitano di essere registrati in formati numerici. In particolare, osserveremo come memorizzare i numeri interi. Nel C++ si definisce binary un flusso di dati in formato “non di testo”, e come tale va indicato nel metodo open. fstream flow; char w; int i; flow.open("bin.dat", ios::out | ios::binary); w = 66; flow.write( &w, 1); Per registrare i dati nel file serve un apposito w = 'A'; metodo del flusso, che sostituisce l’operatore flow.write( &w, 1); <<, riservato per le operazioni di testo: i = +1000000; flow.write((char*)&i, sizeof(int)); flow.write(<dato>,<lunghezza>) i = -1000000; flow.write((char*)&i, sizeof(int)); Le caratteristiche del metodo write sono 3: (char*) trasforma (temporaneamente) un dato in una sequenza di char. È necessaria perché anche un file binario è un flusso di byte. Questa operazione, detta typecasting (to cast =dare forma), induce il programma a trattare un dato in un modo prestabilito; & rende disponibile l’indirizzo del dato (una scelta un po’ curiosa); sizeof(...) serve perché il metodo richiede quanti sono i byte da registrare; con questa funzione lasciamo che sia il programma a fare i calcoli, qualunque sia il tipo di dato interessato (ad esempio, a seconda del compilatore C++, il tipo int potrebbe essere di 2 o di 4 byte). Osservando il dump del file, si può notare che, mentre i due char (che corrispondono alle lettere B e A) sono registrati esattamente come scritto nello snippet; i due numeri interi (in notazione binaria complementare a 32 bit), hanno i byte registrati in ordine inverso: ESADECIMALE 42 41 40 42 0f 00 c0 bd f0 ff BINARIO 01000010 01000001 01000000 01000010 00001111 00000000 11000000 10111101 11110000 11111111 ASCII/DECIMALE BA +1000000 -1000000 Rovesciando l’ordine dei byte troviamo l’effettiva codifica dei dati int (+1000000 e -1000000) : 0000 0000 0000 1111 0100 0010 0 0 0 f 4 2 1111 1111 1111 0000 1011 1101 f f f 0 b d 11111111111100001011110111000000 00000000000011110100001000111111 00000000000000000000000000000001 00000000000011110100001001000000 0100 0000 4 0 0*160+4*161+2*162+4*163+15*164 1100 0000 c 0 calcolo del valore di questo numero negativo rovesciamento dei bit +1 l’opposto del numero (corrisponde a +1000000) Gestione delle immagini A volte capita anche di dover raggruppare i dati per rispettare gli standard di registrazione, proprio come nel caso delle immagini. Il formato che verrà preso in esame è il più semplice: un’immagine “raster” in formato “bitmap” monocromatico (bianco e nero, o B/W), senza compressione. Un’immagine raster6 è una matrice di punti colorati (pixel7), che può essere memorizzata in vari formati8. L’aspetto e la qualità dell’immagine sono determinati dalla sua risoluzione e dal numero di colori usati: una bassa risoluzione (o uno fattore di zoom elevato) comporta un’immagine più sgranata. Il termine raster deriva dal latino rastrum (rastrello), ed è nato per descrivere la modalità di scansione dei vecchi monitor CRT, nei quali un fascio di elettroni disegna l’immagine passando lo schermo riga per riga. Nel formato bitmap, ogni pixel dell’immagine è memorizzato separatamente (proprio come in una “mappa”), con tutte le informazioni necessarie per la sua visualizzazione. Il numero di bit associati ad un pixel varia a seconda numero di colori: COLORI 2 16 256 65536 16M 4G BIT PER PIXEL N. EFFETTIVO 1 4 8 16 24 16.777.216 32 4.294.967.296 Nel formato monocromatico è sufficiente un bit per descrivere un pixel, quindi la mappa dell’immagine occuperà all’incirca 1/8 del numero totale di pixel (calcolato con il prodotto larghezza * altezza). In realtà i calcoli sono leggermente diversi, dato che i dati sono memorizzati in blocchi da 4 byte; la corrispondenza è tanto più precisa quanto la larghezza è vicina ad un multiplo di 32 pixel. In quel caso tutto il blocco di 4 byte è significativo, altrimenti ci sarà un certo numero di bit di “tara”, il cui numero può anche essere calcolato con precisione. Ad esempio, calcoliamo lo spazio necessario per una riga di 173 pixel. (173+7)/8=22 (il numero di byte necessario per i 173 bit). In 22 byte ci sono 176 bit, quindi i bit di tara per ogni blocco sono almeno 3 (ma non è finita) 22/4+1=6, 6*4=24 (la riga contiene un multiplo di 4 byte), quindi ci sono altri 16 bit di tara. In totale, per ogni riga ci sono 19 bit di tara, il cui contenuto non viene elaborato. Alla fine, il numero di byte necessario per ogni riga è 24. Moltiplicando per l’altezza dell’immagine, si ottiene lo spazio totale necessario per memorizzare i dati dell’immagine (in realtà, come vedremo nel prossimo paragrafo, il file dell’immagine contiene anche altre informazioni di servizio). Le formule generali sono le seguenti: CALCOLO SPAZIO OCCUPATO (w=width; h=height) 1) Byte per riga b = int((w+7)/8) 1 byte=8 pixel b = 4*((b-1)/4+1) blocchi di 4 byte 2) Byte totali b = b * h 6 In computer graphics le immagini raster sono generalmente usate come sfondo; le figure geometriche sono in un altro formato, detto vettoriale, che non è oggetto di questo capitolo. 7 Pixel=Pictorial element 8 I più comuni: BMP (bitmap), GIF (Graphic Interchange Format), JPG (actually JPEG, Joint Photographic Experts Group), TIFF (Tagged Image File Format), PNG (Portable Network Graphics), MrSID (Multiresolution Seamless Image Database). Formato immagine BMP Il formato più semplice è il bitmap monocromatico (B/W), che esamineremo in dettaglio. Un file BMP di questo tipo è composto dalle seguenti parti: File header (14 byte) Image header (40 byte) Palette (2*4 = 8 byte) Dati contiene “firma” e informazioni sulle dimensioni del file contiene informazioni sull’immagine 2 colori nel formato RGB (solo bianco e nero) ogni byte rappresenta 8 pixel; l’immagine è in blocchi di 4 byte Le informazioni sono codificate con numeri interi che occupano 1, 2 o 4 byte; storicamente questi tipi di dato sono noti come BYTE, WORD e DWORD (DOUBLE WORD), quindi sono stati usati questi nomi per semplificare il contenuto del sorgente. Tranne dove indicato, i dati sono DWORD (la maggior parte). I valori sono quasi tutti fissi, tranne quelli legati alle dimensioni: si possono calcolare una volta note larghezza e altezza dell’immagine, in pixel. La dimensione della palette, ovviamente, è diversa nei formati a colori. File header (W) Signature (firma, valore fisso = 19778, che corrisponde alle lettere BM); Dimensione totale (62 + dimensione immagine in byte); classificato come “unreliable”; (W) Dato Riservato (valore fisso = 0): in teoria dipende dal software grafico usato; (W) Dato Riservato (valore fisso = 0): in teoria come sopra; Offset dell’immagine (valore fisso = 62). Il numero 62 è dato dalla somma delle lunghezze delle informazioni di servizio (14+40+8 byte). La dimensione di 14 byte è data da 2+4+2+2+4. Image header Dimensione header (valore fisso = 40) Larghezza (in pixel) Altezza (in pixel) (W) Piani di colore (valore fisso = 1) (W) Profondità colori (1=B/W; 4=16 colori; 8=256 colori; 24=16M) Compressione (0=nessuna compressione) Dimensione immagine in byte; classificato come “unreliable”; Bit per metro orizzontali (valore fisso = 0); classificato come “unreliable” Bit per metro verticali (valore fisso = 0) ; classificato come “unreliable” Numero colori (valore fisso = 0) Numero colori “importanti” (valore fisso = 0) La dimensione di 40 byte è data da 4+4+4+2+2+4+4+4+4+4+4. Palette (monocromatica) (B) Nero: valore del blu (default: 0) (B) Nero: valore del verde (default: 0) (B) Nero: valore del rosso (default: 0) (B) Nero: valore riservato (default: 0) (B) Bianco: valore del blu (default: 255) (B) Bianco: valore del verde (default: 255) (B) Bianco: valore del rosso (default: 255) (B) Bianco: valore riservato (default: 0) Con i raggi luminosi, i colori primari sono rosso, verde, blu. Con la luce riflessa (come nei quadri), i colori primari sono rosso, giallo, blu. Il nero è dato dall’assenza dei colori primari (valore 0), il bianco dalla somma di tutti, nel loro valore massimo (255). I colori con valori uguali dei 3 componenti formano la scala dei grigi. Modificare i valori dei colori è sconsigliabile, non avendo un effetto uniforme: alcuni software riescono a rispettare la nuova palette, altri no. Tra i possibili effetti malefici, ogni colore diverso dal bianco potrebbe essere reso in nero; oppure, salvando l’immagine, la palette potrebbe tornare quella default. Memorizzazione informazioni di servizio È possibile registrare tutto il contenuto di una bitmap come char o string, ma non è molto consigliabile. Per raggruppare i dati in un formato più leggibile si possono dichiarare formati composti detti struct (strutture): una struttura dati non è normalmente formata da un dato singolo, ma da più dati associati ad essa. Ciascun componente di una variabile strutturata viene indicato con il formato degli oggetti: <struct>.<component> Anche con questi accorgimenti, tuttavia, sarà necessario rispettare il formato delle istruzioni, che spesso ricorda una gestione “old-style” dei flussi di dati come sequenze di caratteri. Per semplificare il sorgente vengono specificate alcune abbreviazioni dei tipi di dato presenti nelle strutture. typedef unsigned char BYTE; typedef unsigned short int WORD; typedef unsigned long int DWORD; struct fileHeader { WORD type; DWORD size; WORD reserved1; WORD reserved2; DWORD offset; }; struct imageHeader { DWORD size; DWORD width; DWORD height; WORD planes; WORD bitCount; DWORD compression; DWORD imageSize; DWORD xPixelsPerMeter; DWORD yPixelsPerMeter; DWORD clrUsed; DWORD clrImportant; }; struct color { BYTE blue; BYTE green; BYTE red; BYTE reserved; }; // // // BYTE: 1 byte unsigned WORD: 2 bytes unsigned DWORD: 4 bytes unsigned Strutture dati Dichiarazione di variabili composte: fileHeader h1; imageHeader h2; color black, white; In questo modo le variabili h1, h2, white, black risultano composte da più dati, corrispondenti alla definizione di struct Ad esempio, le componenti della variabile h1 saranno: h1.type h1.size h1.reserved1 h1.reserved2 h1.offset All’interno del programma è possibile associare alle variabili composte i valori distinti dei singoli dati atomici, evitando di costruire stringhe scarsamente significative e ancor meno gestibili: white.blue = 255; white.green = 255; white.red = 255; white.reserved = 0; Essendoci nel formato BMP un header di 14 byte, bisogna allineare la scrittura alla sua esatta dimensione (e non a 16); per farlo occorre aggiungere la seguente direttiva (che può essere associata alle include all’interno del programma). #pragma pack( push, 1) I colori sono descritti con le componenti fondamentali nel formato RGB (red, green, blue). I valori associati ai colori sono in ordine inverso perché è così che sono memorizzate le DWORD. Come si può immaginare, per un bitmap monocromatico basteranno le descrizioni del bianco (255,255,255) e del nero (0,0,0). Per un’immagine a colori servirebbe una palette con tutte le descrizioni dei colori, ma, per ovvi motivi di praticità, per più di 256 colori non viene memorizzata. Con 256 colori le informazioni di servizio occupano uno spazio ancora gestibile di 1078 byte (54+1024 per i colori), mentre per un’immagine a 16M il carico necessario diventerebbe improponibile. Una volta dichiarate le strutture, se ne definisce una per tipo (tranne per i colori, che possono essere due), oltre ad una variabile di flusso per direzionare il tutto nel file desiderato: fileHeader h1; imageHeader h2; color black, white; fstream flow; I dati dei due header sono per lo più fissi; quelli variabili, come già visto, sono i seguenti: h1.size UNRELIABLE (valore = 62+h2.imageSize) h2.width h2.height h2.imageSize SIGNIFICATIVO SIGNIFICATIVO UNRELIABLE Per definire le palette è opportuno seguire lo standard (nero=assenza di colori; bianco=tutti i colori): black.blue = 0; black.green = 0; black.red = 0; black.reserved = 0; white.blue = 255; white.green = 255; white.red = 255; white.reserved = 0; Per scrivere l’intera struttura è consigliabile usare il metodo write , che ha il seguente formato: flow.write( (char*)&<struct>, sizeof(<struct>)); Per registrare le informazioni di servizio si può procedere in questo modo: flow.open( z, ios::out | ios::binary); result = flow.good(); if ( result ) { flow.write ((char*)&h1, sizeof flow.write ((char*)&h2, sizeof flow.write ((char*)&black, sizeof flow.write ((char*)&white, sizeof flow.close(); } // z = nome del file (fileHeader)); (imageHeader)); (color)); (color)); sizeof( fileHeader) 14 sizeof( imageHeader) 40 sizeof( color) 4 La variabile z è la stringa contenente il nome del file. Il metodo .good() permette di verificare se un’operazione (in questo caso l’apertura) è andata a buon fine. Tra le cause di possibili errori, ci potrebbe essere il disco pieno (piuttosto improbabile), oppure l’accesso protetto alla cartella; se l’intero file system fosse danneggiato, probabilmente il programma non riuscirebbe nemmeno a funzionare. A questo punto qualche modulo si dovrà occupare della scrittura dei byte dati, aprendo nuovamente il file con una nuova clausola (ios::app), che consente di aggiungere in coda (operazione nota come append): flow.open( z, ios::out | ios::binary | ios::app); // append mode Nel prossimo paragrafo potremo vedere un esempio di come raggruppare le informazioni e i moduli in una libreria (un header file con estensione .h) Una libreria per la gestione delle immagini Le dichiarazioni e le funzioni necessarie alla registrazione possono essere inserite in una libreria, che potrebbe essere scritta come riportato nel seguito. #include <fstream> #pragma pack(push, 1) using namespace std; // byte-wise setting typedef unsigned char BYTE; typedef unsigned short int WORD; typedef unsigned long int DWORD; // // // 1 byte unsigned 2 bytes unsigned 4 bytes unsigned /* The two headers (file/image) and the palettes */ struct fileHeader { WORD type; DWORD size; Tutte le dichiarazioni dei dati e degli header WORD reserved1; WORD reserved2; sono stati esaminati in precedenza. DWORD offset; }; struct imageHeader { DWORD size; DWORD width; DWORD height; WORD planes; WORD bitCount; DWORD compression; DWORD imageSize; DWORD xPixelsPerMeter; DWORD yPixelsPerMeter; DWORD clrUsed; DWORD clrImportant; }; struct color { BYTE blue; // The colors are stored in reverse order BYTE green; BYTE red; BYTE reserved; }; /* This function computes the number of bytes required for each row of the bitmap */ int bytesPerRow( int width) { int b, row; b = ((width+7)/8); // bytes minumum row = 4 * ( (b-1)/4 + 1 ); // actual bytes per row return row; } Il modulo “bytesPerRow” calcola quanti byte sono occupati da una riga di pixel; non dipende dall’altezza dell’immagine. I calcoli sono già stati esaminati in /* Stores the bitmap header and the palette: Dimensions in bytes: file info 14 image info 40 palette 8 (4 black + 4 white) */ bool monoHeaders( const char* z, int width, int height) { fileHeader h1; Il modulo ha questo nome per i seguenti motivi: imageHeader h2; mono monocromatico color white, black; int b, i, j, row, size; Headers il plurale indica le 4 strutture di char bitmap, b1, b2; bool result; fstream x; servizio row = bytesPerRow( width); size = row * height; // // bytes per row total bytes h1.type = 19778; h1.size = 62 + size; h1.reserved1 = 0; h1.reserved2 = 0; h1.offset = 62; // // must be 19778 (signature: BM) ---SIZE (unreliable) // header size: 14 + 40 + (4+4) (black/white) h2.size = 40; h2.width = width; h2.height = height; h2.planes = 1; h2.bitCount = 1; h2.compression = 0; h2.imageSize = size; h2.xPixelsPerMeter = 0; h2.yPixelsPerMeter = 0; h2.clrUsed = 0; h2.clrImportant = 0; // // // // // header length (fixed) ---SIZE ---SIZE must be 1 (color planes) 1 = monochrome // // // ---SIZE unreliable unreliable black.blue = 0; black.green = 0; black.red = 0; black.reserved = 0; white.blue = 255; white.green = 255; white.red = 255; white.reserved = 0; Il modulo “monoHeaders” si occupa della registrazione del dati di servizio: l’unico dato veramente necessario è il nome del file, mentre le dimensioni sono inserite per completezza (l’immagine risulterebbe valida anche con questi dati posti a zero). x.open( z, ios::out | ios::binary); result = x.good(); if ( result ) { x.write ((char*)&h1, x.write ((char*)&h2, x.write ((char*)&black, x.write ((char*)&white, x.close(); } return result; } sizeof sizeof sizeof sizeof ( ( ( ( fileHeader)); imageHeader)); color)); color)); // // // // header 1 header 2 black = 0 white = 1 Un’immagine casuale Quello che segue è un esempio di creazione di un bitmap monocromatico affidato quasi completamente al caso (la prima e l’ultima riga sono state previste come “tutto bianco” e “tutto nero”). Va tenuto presente che le righe sono memorizzate dal basso all’alto. bool randomImage( const char* z, int width, int height) { int b, i, j, row, size; char b1, b2, bitmap; bool result; fstream x; result = monoHeaders( z, width, height); // headers/palette if ( result ) { row = bytesPerRow( width); x.open( z, ios::out | ios::binary | ios::app); // append mode for( i=1; i<=height; i++) { for( j=1; j<=row; j++) { if (i==1) bitmap = 255; // 1st row: white else if (i==height) bitmap = 0; // last row: black else bitmap = rand() % 256; // other: random x.write (&bitmap, 1); } } x.close(); } Il risultato del nostro modesto esempio è quasi completamente casuale; potrebbe essere come return result; nell’immagine seguente (23x7): } int main() { bool result; int width, height; srand(time(0)); width = rand() % 30 + 1; height = rand() % 10 + 1; result = randomImage( "test.bmp", width, height); cout << result; getchar(); } N.B.: Scrivere un programma C++ per la gestione di immagini, o anche per la sola lettura, è completamente fuori luogo, sia per le difficoltà pratiche, sia per la presenza di decine di programmi adeguati.