Fișere

Toate fișierele sunt o înșiruire de bytes, indiferent de extensie. În realitate, extensia nu contează la nimic! Dacă numim un fișier text a.exe și îl deschidem cu un editor de text, va funcționa exact ca un a.txt. La fel, dacă numim executabilul unui joc video main.cpp și îl rulăm din consolă, va porni jocul ca și cum nu ar fi nimic greșit. Diferența între fișiere se face prin format. Fiecare tip de fișier (.pdf, .xlsx, .bmp, etc.) începe cu câțiva bytes (file header) care îl identifică în mod unic astfel încât un program să își poată da seama cum să îl gestioneze.

Fișiere text

Fișierele cu care suntem obișnuiți de la probleme de algoritmică sunt fișiere text. Ele nu au file header specific, și în realitate funcționează ca orice alt fișier, doar că cin interpretează fiecare byte ca un caracter ASCII și ne mai ajută sărind peste spații atunci când citim numere, etc.

Fișiere binare

C Style

O secvență completă de instrucțiuni pentru citirea unui fișier ca binar în C este următoarea:

FILE *fileIn = fopen(fileName, "rb");
fseek(fileIn, 0, SEEK_END);
uint32_t fileLen = ftell(fileIn);
uint8_t *buffer = (uint8_t *) malloc(fileLen);
fseek(fileIn, 0, SEEK_SET);
fread(buffer, sizeof(uint8_t), fileLen, fileIn);
fclose(fileIn);

Pentru scriere:

FILE *fileOut = fopen(fileName, "wb");
fwrite(buffer, sizeof(uint8_t), fileLen, fileOut);
fclose(fileOut);

FILE *fileIn = fopen(fileName, "rb") deschide fișierul fileName ca binar pentru citire. Toate modurile de deschidere a unui fișier în C se pot găsi în documentație;

fseek(fileIn, 0, SEEK_END) mută indicatorul de citire la finalul fișierului (la 0 bytes începând cu finalul fișierului).

ftell(fileIn) ne spune câți bytes au fost deja parcurși în fișier. Din moment ce indicatorul este la finalul fișierului deja, ne va da lungimea datelor din fișier. Creăm un buffer de asceastă dimensiune pentru a stoca conținutul.

fseek(fileIn, 0, SEEK_SET) mută indicatorul de citire la începutul fișierului (la 0 bytes de la începutul fișierului).

fread(buffer, sizeof(uint8_t), fileLen, fileIn) stochează la adresa buffer următorii sizeof(uint8_t) * fileLen bytes din fișier, începând cu poziția actuală a indicatorului de citire.

Exemplul de citire funcționează analog.

C++ Style

O secvență completă de instrucțiuni pentru citirea unui fișier ca binar este următoarea:

    std::ifstream fileIn(fileName, std::ios::ate | std::ios::binary);
    if (!file.is_open()) {
        throw std::runtime_error("Failed to open file");
    }

    size_t fileLen = (size_t) fileIn.tellg();
    std::vector<char> buffer(fileLen, 0);
    fileIn.seekg(0);
    fileIn.read(buffer.data(), fileLen);
    fileIn.close();

Pentru scriere:

std::ofstream fileOut(fileName, std::ios::trunc | std::ios::out);
fileOut.write(buffer.data(), fileLen * sizeof(uint8_t));
fileOut.close();

std::ifstream fileIn(fileName, std::ios::ate | std::ios::binary) deschide fișierul ca binar și mută indicatorul de citire la finalul fișierului. Al doilea parametru este o mască de biți care ne permite să specificăm cum vrem să fie deschis fișierul. Toate opțiunile pentru această mască se pot găsi în documentație

fileIn.tellg() ne spune câți bytes au fost deja parcurși în fișier. Din moment ce indicatorul este la finalul fișierului deja, ne va da lungimea datelor din fișier. Creăm un buffer de asceastă dimensiune pentru a stoca conținutul.

fileIn.seekg(0) mută indicatorul de citire la începutul fișierului (byte-ul de indice 0). De aici vom începe citirea efectivă.

fileIn.read(buffer.data(), fileLen) citește fileLen bytes și îi stochează la adresa de memorie care începe cu adresa dată ca primul parametru. buffer.data() întoarce pointerul către inceputul datelor efective din vectorul STL numit buffer.

fileOut.write(buffer.data(), fileLen) scrie fileLen bytes începând cu adresa datelor efective cu vectorul STL buffer.

Formatul de Fișier .bmp

bmp este un format de fișier simplu pentru imagini. Nu are compresie, ca png sau jpg, deci poate fi manipulat mult mai ușor. Întreaga specificație a formatului de fișier este disponibilă pe Wikipedia și nu va fi reexplicat aici.

Datele efective din imagine sunt ținute în format RGB, culorile fiind compuse din roșu, verde și albastru. Cele 3 canale de culori pot lua valori între 0 și 255. O observație importantă este că o culoare este o nuanță de gri dacă și numai dacă r = g = b.

Exerciții

  1. Creați o interfață numită ```FileHandler`` care va avea următoarele metode publice:
    • uint8_t *readBinaryFile(std::string fileName): întoarce adresa la care stochează datele citite binar din fișierul fileName
    • void writeBinaryFile(std::string fileName, uint8_t *buffer): scrie datele din buffer ca binar în fișierul fileName
    • Image *readImage(std::string fileName): creează un obiect de tip imagine și întoarce un pointer către acesta. Trebuie creată și clasa Image în care se află toate datele utile din fișierul fileName de tip bmp. O puteți lăsa neimplementată momentan.
    • void writeImage(std::string fileName, Image *image): scrie în fișierul fileName toate datele unei imagini de tip bmp, unde structura fișierului este corectă și dedusă din starea obiectului aflat la adresa image.
  2. Completați clasa Image, adăugând un buffer pentru pixelii din imagine și variabile membru pentru toate datele din specificația formatului de care am putea avea nevoie pentru afișare sau logică.
  3. Extindeți interfața FileHandler cu clasa CFileHandler și implementați, adăugând orice metode auxiliare aveți nevoie.
  4. Extindeți interfața FileHandler cu clasa CPPFileHandler și adăugați orice metode auxiliare aveți nevoie.
  5. În main, lucrați doar cu FileHandler. Niciodată CFileHandler sau CPPFileHandler, mai puțin la declarare. De exemplu, aveți voie FileHandler *fileHandler = new CFileHandler(). Schimbând acest unic rând, programul trebuie să funcționeze cu sintaxa de C sau sintaxa de C++.
  6. Completați clasa Image cu metoda Image *createGreyscaleImage() care să creeze o imagine nouă, doar că în alb-negru. Puteți folosi formula de transformare a unei culori în nuanță de gri: gray = 0.3 * r + 0.59 * g + 0.11 * b.