Лабораторне заняття 6. Робота з файлами
Мета роботи: освоїти роботу з файлами на мові С.
Теоретичні відомості
Для зручності використання інформація на запам’ятовуючих пристроях зберігається у вигляді файлів.
Файл – іменована область пам'яті, виділена для зберігання масиву даних. Дані, що містяться в файлах, мають найрізноманітніший характер: програми на алгоритмічній або машинному мовою; вихідні дані для роботи програм або результати виконання програм; довільні тексти; графічні зображення і т.п.
Для програміста відкритий файл представляється як послідовність даних, що зчитуються або записуються. При відкритті файлу з ним зв’язується потік вводу-виводу. Інформація, що виводиться – записується в потік, а інформація, що вводиться – зчитується з потоку.
Коли потік відкривається для введення-виведення, він зв’язується зі стандартною структурою типу FILE, яка визначена в stdio.h. Структура FILE містить необхідну інформацію про файл.
Відкриття файлу здійснюється за допомогою функції fopen(), яка повертає вказівник на структуру типу FILE, який можна використовувати для подальших операцій з файлом:
FILE *fopen(name, type);
де name – ім’я файлу (включаючи шлях),
type – вказівник на рядок символів, що визначають спосіб доступу до файлу:
- "r" – відкрити файл для читання (файл повинен існувати);
- "w" – відкрити порожній файл для запису; якщо файл існує, то його вміст перезаписується;
- "a" – відкрити файл для запису в кінець (для додавання); файл створюється, якщо він не існує.
Значення, що повертається – вказівник на відкритий потік. Якщо виявлена помилка, то повертається значення NULL.
Функція fclose() закриває потік або потоки, пов’язані з відкритими за допомогою функції fopen() файлами.
Розглянемо приклад програми, для відкриття файлу (лістинг 6.1):
Лістинг 6.1 – Відкриття файлу для читання
#include <stdio.h>
int main() {
FILE *fp; // вказівник на файл
char name[] = "my.txt"; // ім’я та шлях до файлу
if ((fp = fopen(name, "r")) == NULL) // відкриття потоку для роботи з файлом
{
printf("Не вдалося відкрити файл");
return 0;
}
... // дії над даними
fclose(fp); // закриття потоку
return 0;
}
кінець лістингу 6.1
Функції fscanf() і fprintf() аналогічні функціям scanf() і printf(), але працюють з файлами даних, і мають перший аргумент – вказівник на файл:
fscanf (потік, "ФорматВводу", аргументи);
fprintf (потік, "ФорматВиводу", аргументи);
Растрові зображення
Найпростіший спосіб представлення зображення – використати двомірну таблицю пікселів різних кольорів. Для чорно-білих зображень достатньо мати 1 біт на піксель, 0 може представляти чорний колір, а 1 – білий, як це показано на рис. 6.1.
Рисунок 6.1 – Бітова карта зображення «Смайлик»
У цьому сенсі таке зображення – лише «бітова карта». Для кольорових зображень просто знадобиться більше біт на піксель. Формати файлів (такі, як GIF), що підтримують 8-бітні кольори, використовують 8 біт на піксель. Інші формати файлів (такі, як BMP, JPEG або PNG) підтримують 24-бітні кольори та використовують 24 біт на піксель.
24-бітний BMP використовує 8 біт, щоб вказати кількість червоного у кольорі пікселя, 8 біт – щоб вказати кількість зеленого у кольорі пікселя і 8 біт – щоб вказати кількість синього у кольорі пікселя. Дана кольорова модель називається RGB: червоний, зелений та синій (red, green, blue).
Якщо значення R, G і B, у якомусь пікселі BMP дорівнюють 0xff, 0x00 та 0x00 у шістнадцятковій системі, цей піксель є виключно червоним, так як 0x00 і 0x00 означають відсутність зеленого і блакитного відповідно.
Структура растрових файлів
Файл – це лише послідовність бітів, що впорядковані певним чином. 24‑бітний BMP файл насправді є послідовністю бітів, в якій (майже) кожні 24 біти представляють колір певного пікселю. Але BMP файл також містить «метадані» – інформацію про висоту і ширину зображення тощо. Ці метадані зберігаються на початку файлу у формі двох структур даних, які зазвичай називають «заголовками».
Перший з цих заголовків, що називається BITMAPFILEHEADER, має довжину 14 байт. Другий із цих заголовків, що називається BITMAPINFOHEADER, має довжину 40 байт.
Одразу ж після цих заголовків йде сама «карта бітів»: масив байтів, кожна трійка з яких представляє колір пікселя. Однак, BMP зберігає ці трійки задом наперед (тобто у форматі BGR). Якщо конвертувати 1-бітний «Смайлик» (рис. 6.1) у 24-бітний «Смайлик», замінивши чорний колір на червоний, отримаємо наступний результат:
ffffff ffffff 0000ff 0000ff 0000ff 0000ff ffffff ffffff
ffffff 0000ff ffffff ffffff ffffff ffffff 0000ff ffffff
0000ff ffffff 0000ff ffffff ffffff 0000ff ffffff 0000ff
0000ff ffffff ffffff ffffff ffffff ffffff ffffff 0000ff
0000ff ffffff 0000ff ffffff ffffff 0000ff ffffff 0000ff
0000ff ffffff ffffff 0000ff 0000ff ffffff ffffff 0000ff
ffffff 0000ff ffffff ffffff ffffff ffffff 0000ff ffffff
ffffff ffffff 0000ff 0000ff 0000ff 0000ff ffffff ffffff
Розглянемо код програми, яка створює копію BMP-зображення на основі бітової карти (лістинг 6.2):
Лістинг 6.2 – Побітове копіювання BMP-зображень
#include <stdio.h>
#include <stdlib.h>
#include "bmp.h"
int main(int argc, char *argv[])
{
if (argc != 3) // перевірка наявності потрібної кількості аргументів
{
printf("Usage: copy infile outfile\n"); // підказка по використанню
return 1;
}
char *infile = argv[1]; // назва вхідного файлу
char *outfile = argv[2]; // назва вихідного файлу
FILE *inptr = fopen(infile, "r"); // відкриття вхідного файлу для читання
if (inptr == NULL) // перевірка на вдале відкриття
{
printf("Could not open %s.\n", infile);
return 2;
}
FILE *outptr = fopen(outfile, "w"); // створення вихідного файлу
if (outptr == NULL) // перевірка на вдале створення файлу
{
fclose(inptr);
printf("Could not create %s.\n", outfile);
return 3;
}
// зчитування заголовків файлу BMP
BITMAPFILEHEADER bf;
fread(&bf, sizeof(BITMAPFILEHEADER), 1, inptr);
BITMAPINFOHEADER bi;
fread(&bi, sizeof(BITMAPINFOHEADER), 1, inptr);
// перевірка заголовків на коректність
if (bf.bfType != 0x4d42 || bf.bfOffBits != 54 || bi.biSize != 40 ||
bi.biBitCount != 24 || bi.biCompression != 0)
{
fclose(outptr);
fclose(inptr);
printf("Unsupported file format.\n");
return 4;
}
// запис заголовків у вихідний файл
fwrite(&bf, sizeof(BITMAPFILEHEADER), 1, outptr);
fwrite(&bi, sizeof(BITMAPINFOHEADER), 1, outptr);
// визначення надлишкового заповнення
int padding = (4 - (bi.biWidth * sizeof(RGBTRIPLE)) % 4) % 4;
// перенесення бітової карти у вихідний файл
for (int i = 0, biHeight = abs(bi.biHeight); i < biHeight; i++)
{
for (int j = 0; j < bi.biWidth; j++)
{
RGBTRIPLE triple; // змінна для тимчасового збереження даних
fread(&triple, sizeof(RGBTRIPLE), 1, inptr); // читання з вхідного файлу
fwrite(&triple, sizeof(RGBTRIPLE), 1, outptr); // запис у вихідний файл
}
// запис надлишкового заповнення
fseek(inptr, padding, SEEK_CUR);
for (int k = 0; k < padding; k++)
{
fputc(0x00, outptr);
}
}
fclose(inptr); // закриття вхідного файлу
fclose(outptr); // закриття вихідного файлу
return 0;
}
кінець лістингу 6.2
В коді наведеному вище відбувається побітове копіювання зображення у форматі BMP, використовуючи особливості його структури (рис. 6.2). Для реалізації роботи з даною структурою, необхідно також підключити файл заголовків bmp.h, код якого наведено нижче (лістинг 6.3):
Лістинг 6.3 – Заголовковий файл bmp.h
#include <stdint.h>
typedef uint8_t BYTE;
typedef uint32_t DWORD;
typedef int32_t LONG;
typedef uint16_t WORD;
typedef struct
{
WORD bfType;
DWORD bfSize;
WORD bfReserved1;
WORD bfReserved2;
DWORD bfOffBits;
} __attribute__((__packed__))
BITMAPFILEHEADER;
typedef struct
{
DWORD biSize;
LONG biWidth;
LONG biHeight;
WORD biPlanes;
WORD biBitCount;
DWORD biCompression;
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} __attribute__((__packed__))
BITMAPINFOHEADER;
typedef struct
{
BYTE rgbtBlue;
BYTE rgbtGreen;
BYTE rgbtRed;
} __attribute__((__packed__))
RGBTRIPLE;
кінець лістингу 6.3
Рисунок 6.2 – Структура BMP-файлу
Завдяки bmp.h, RGBTRIPLE – це просто новий тип даних, з яким можна працювати, як з int або float, а змінна triple – це змінна типу RGBTRIPLE (див. лістинг 6.2).
Якщо є змінна типу RGBTRIPLE, можна отримати доступ до її властивостей, таких як rgbtRed, використавши запис з крапкою:
triple.rgbtRed = 0x80;
Запис префіксу 0x перед числом означає те, що числа записані у шістнадцятковому форматі в мові С. Наприклад, визначення певного кольору пікселя виглядатиме наступним чином:
triple.rgbtBlue = 0x80;
triple.rgbtGreen = 0x00;
triple.rgbtBlue = 0x80;