Зона кода

А давайте немного попрограммируем...

Примитивная графическая библиотека на C99. 1-я часть

C99Библиотека pgraph

Как известно, в языке C99 не предусмотрены стандартные библиотеки для работы с графикой. Впрочем, существуют такие библиотеки, созданные сторонними разработчиками. Самой известной из них, пожалуй, является GD Graphics Library. Возможности этой библиотеки огромны, кроме того, в неё включена поддержка большого числа языков программирования.

Но в данной статье мы рассмотрим построение собственной графической библиотеки имеющей, особенно по сравнению с GD, весьма скромные возможности. Для чего нужно создавать библиотеку, заведомо менее мощную, чем уже имеющаяся?

Во-первых, всегда приятно создавать что-то собственными руками, вернее, мозгами. Во-вторых, создание такой библиотеки — это отличный повод попрактиковаться в программировании и неплохая возможность улучшить свой интеллектуальный потенциал. В-третьих, это интересно! Ведь даже выполнение примитивных действий с изображениями требует использования определённых алгоритмов, которые могут оказаться весьма любопытными.

Разумеется, в предыдущем абзаце я высказал свою личную точку зрения. Но если вы, уважаемый читатель, думаете так же, как и я, то эта статья написана именно для вас! Итак, не будем терять время и приступим к работе!

Краткое описание библиотеки

Назовём нашу примитивную графическую библиотеку pgraph. Её предназначение — создание и обработка растровых изображений. Каждое такое изображение можно рассматривать как прямоугольную область, заполненную рядами пикселей — неделимых элементов изображений. Каждый пиксель характеризуется местом расположения внутри изображения и цветом.

Наша библиотека позволит выполнять следующие действия:

  • создавать пустые изображения произвольных размеров;
  • устанавливать цвет любого пикселя изображения;
  • получать цвет любого пикселя изображения;
  • закрашивать пиксели, расположенные на прямой линии, одним цветом;
  • заливать одноцветные области изображения новым цветом;
  • сохранять изображения в виде файлов формата BMP.

Вот и все действия. Как мы видим, возможности библиотеки оправдывают эпитет "примитивный". Тем не менее, этих возможностей не так уж и мало. Пользуясь ими, можно создавать достаточно сложные изображения. К тому же, чтобы реализовать даже эти возможности, придётся немало потрудиться.

Вспомогательные типы

Наша библиотека будет включать в себя заголовочный файл pgraph.h и файл с кодом функций pgraph.c. Чуть позже мы создадим ещё один файл и включим его в состав библиотеки, но об этом говорить ещё рано. А в этом и двух последующих разделах будет полностью сформирован файл pgraph.h.

Начинается файл pgraph.h с директив препроцессора #include, подключающих необходимые нам стандартные библиотеки:

#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <stdbool.h>

Обратите внимание на подключение заголовочного файла stdbool.h. Мы будем активно использовать булевый тип, и нам понадобятся определённые в этом файле макросы bool, true и false.

В основе библиотеки pgraph будет лежать структура image. Предназначение переменных типа image — хранение изображений. Для удобства переменные данного типа также будем называть изображениями.

Перед тем, как перейти к созданию image, объявим несколько вспомогательных типов.

В нашей библиотеке часто будут использоваться беззнаковые целые типы. Назначим, для удобства, этим типам короткие псевдонимы с помощью спецификатора typedef:

typedef unsigned int uint;
typedef unsigned char uchar;
typedef unsigned short ushort;

Как уже было сказано, каждому пикселю изображения соответствует свой цвет. Для хранения цвета будем использовать переменные типа color, описанного следующим образом:

typedef struct
{
    uchar red;
    uchar green;
    uchar blue;
} color;

Цвет будет храниться в RGB-формате. Поля структуры color red, green и blue предназначены для хранения интенсивности красного, зелёного и синего цветов соответственно. Результирующий цвет получается смешением перечисленных трёх цветов заданных интенсивностей. Каждое из трёх полей может принимать значения от 0 до 255, причём 0 соответствует максимальной интенсивности, а 255 — минимальной. Таким образом, общее количество доступных нам цветов составит 224, т. е. примерно 16,8 млн.

Помимо цвета, пиксель характеризуется также своим расположением. Для его описания нам потребуется ввести двухмерную прямоугольную систему координат, оси которой параллельны сторонам изображения. Тогда местоположение каждого пикселя будет описываться упорядоченной парой чисел, первое из которых — это координата по оси абсцисс, а вторая — по оси ординат.

В компьютерной графике обычно начало координат размещают в левом верхнем углу изображения, ось абсцисс направляют вправо, а ось ординат — вниз.

Но в математике принято направлять оси OX и OY таким образом, чтобы для наблюдателя, расположенного над плоскостью, в которой находится система координат, поворот оси OX на прямой угол до совмещения её с осью OY казался происходящим против часовой стрелки. Таким образом, если ось OX направлена вправо, то ось OY должна быть направлена вверх.

Именно "математический", а не традиционный "компьютерный" способ введения системы координат мы и реализуем. Такой подход немного упростит нам в дальнейшем преобразования координат в случае, если мы захотим с помощью нашей библиотеки строить графики функций.

Так что ось абсцисс мы направим вправо, а ось ординат — вверх. А для того, чтобы координаты пикселя являлись неотрицательными числами, разместим начало координат в левом нижнем углу изображения.

Пусть ширина изображения в пикселях равна w, а высота — h. Координаты пикселей по оси OX будут пробегать значения от 0 до w − 1 с шагом 1, а по оси OY — от 0 до h − 1 с тем же шагом. Таким образом, вершины изображения, начиная с левой нижней, в порядке обхода их по часовой стрелке будут следующими: (0, 0), (0, h − 1), (w − 1, h − 1), (w − 1, 0).

Совокупность двух координат пикселя задаёт точку, принадлежащую изображению. Таким образом, пиксель можно рассматривать как совокупность точки и цвета, а точку — как "бесцветный" пиксель. В программе, использующей библиотеку pgraph, координаты точек обычно будут задаваться двумя отдельными числами. Однако некоторым библиотечным функциям понадобится возможность работать с точкой как с единым объектом. Для её обеспечения мы создадим тип point:

typedef struct
{
    uint x;
    uint y;
} point;

Кроме того, тип point будет использован нами для создания типа image.

Тип image

Наконец, переходим к описанию типа image. Он представляет собой структуру, задаваемую следующим образом:

typedef struct
{
    uint width;
    uint height;
    color cur_col;
    point cur_pnt;
    color pixels[];
} image;

Рассмотрим назначение всех пяти полей структуры image.

Поля width и height предназначены для хранения соответственно ширины и высоты изображения в пикселях.

Теперь о полях cur_col и cur_pnt. В дальнейшем нами будут реализованы функции для построения отрезков. С помощью этих функций можно будет также рисовать и ломаные линии. Построение ломаной будет происходить позвенно. Каждое звено, начиная со второго, будет представлять собой отрезок. Оно будет "начинаться" в той точке, в которой предыдущее звено "закончилось". Точку начала звена будем называть его начальной вершиной, а точку конца — конечной.

Ясно, что для построения звена ломаной требуется знать его начальную и конечную вершины. Но если хотя бы одно звено уже построено, то для создания следующего достаточно знать только конечную вершину, поскольку координаты начальной известны: они совпадают с координатами конечной вершины предыдущего звена. Чтобы не требовалось для построения звеньев, начиная со второго, указывать их начальные вершины, мы будем их координаты хранить в переменной типа image.

Конечную вершину очередного звена ломанной, построенного последним, будем называть текущей точкой. Если ни одно звено пока не построено, то она будет совпадать с началом координат. Для хранения координат текущей точки и предназначено поле cur_pnt. Разумеется, значение этого поля будет изменяться после построения каждого звена ломаной.

А если все звенья ломаной имеют один и тот же цвет, то его можно будет задать заранее и сохранить в переменной типа image. Этот цвет будем называть текущим. Если ни одно звено пока не построено, то текущим цветом будет чёрный. Для хранения текущего цвета и предназначено поле cur_col. Использование текущего цвета избавит нас от необходимости каждый раз указывать цвет отрезка при вызове функций, выполняющих построение звеньев одноцветной ломаной.

Поле pixels, представляющее собой массив, предназначено для хранения непосредственно растра, т. е. цветов всех пикселей, входящих в изображение. Понятно, что число элементов массива равно произведению ширины и высоты, т. е. значений полей width и height.

Если мы будем заполнять массив поэлементно, в порядке увеличения индексов, то цвета пикселей будут записываться в массив горизонтальными рядами, начиная с нижнего ряда и заканчивая верхним, причём пиксели каждого ряда будут перебираться в направлении слева направо. Несложно заметить, что цвет пикселя с координатами x и y будет храниться в элементе массива pixels с индексом y * width + x.

Очевидно, что размер массива pixels, равный произведению ширины изображения на высоту, заранее неизвестен. Поэтому память для этого массива, а значит, и для всего объекта типа image, должна выделяться динамически. Это означает, что доступ к переменным данного типа будет осуществляться через указатели. Динамическое выделение памяти под переменные типа image будет рассмотрено позже.

Макросы, обозначающие цвета, и прототипы функций

Завершаем создание файла pgraph.h.

Для того, чтобы сформировать, а потом передать в в какую-либо функцию значение типа color, мы должны указать 3 числа — интенсивности красной, зелёной и синей составляющих цвета. Гораздо удобнее было бы задать цвет с помощью слова, данный цвет обозначающего. Чтобы предоставить пользователю такую возможность, определим 16 макросов, обозначающих 16 наиболее распространённых цветов, использующих в веб-графике. Каждый такой макрос можно использовать везде, где допустимо использование значений типа color. Ниже приведены 16 соответствующих макроопределений:

#define AQUA (color) {0, 255, 255}
#define BLACK (color) {0, 0, 0}
#define BLUE (color) {0, 0, 255}
#define FUCHSIA (color) {255, 0, 255}
#define GRAY (color) {128, 128, 128}
#define GREEN (color) {0, 128, 0}
#define LIME (color) {0, 255, 0}
#define MAROON (color) {128, 0, 0}
#define NAVY (color) {0, 0, 128}
#define OLIVE (color) {128, 128, 0}
#define PURPLE (color) {128, 0, 128}
#define RED (color) {255, 0, 0}
#define SILVER (color) {192, 192, 192}
#define TEAL (color) {0, 128, 128}
#define WHITE (color) {255, 255, 255}
#define YELLOW (color) {255, 255, 0}

А завершаться файл pgraph.h будет прототипами "интерфейсных" (пользовательских) функций:

image *create_image(uint width, uint height);
bool save_to_file(image *img, const char *filename);
image *set_color(image *img, uint x, uint y, color col);
color get_color(image *img, uint x, uint y);
image *set_cur_col(image *img, color col);
image *set_cur_pnt(image *img, uint x, uint y);
color to_color(uint colorref);
uint from_color(color col);
image *line(image *img, uint x1, uint y1, uint x2, uint y2);
image *line_to(image *img, uint x, uint y);
image *fill(image *img, uint x, uint y, color col);
image *fill_all(image *img, color col);

Назначение большинства из этих функций, надеюсь, ясно из их названия. Каждая из них будет подробно рассмотрена в дальнейшем.

Помимо данных функций в библиотеку будет включён ещё ряд "внутренних" функций, не предназначенных для вызова пользователями библиотеки. Они будут реализованы в файле pgraph.c, но их прототипы в заголовочном файле содержаться не будут.

Итак, файл pgraph.h полностью сформирован. Переходим к реализации функций нашей библиотеки.

Создание изображения — функция create_image()

Начинаем наполнять кодом файл pgraph.c. Начинаться он будет с подключения заголовочного файла pgraph.h:

#include "pgraph.h"

Эта директива препроцессора не будет являться единственной в файле pgraph.c, но остальные мы добавим позже.

А теперь переходим к реализации функции create_image(). Данная функция отвечает за динамическое создание изображений, т. е. переменных типа image. Она принимает в качестве параметров ширину и высоту создаваемого изображения, выделяет для изображения необходимый объём памяти и заполняет поля построенной структурной переменной значениями.

Полям width и height присваиваются ширина и высота изображения в пикселях соответственно; остальные поля получают значения по умолчанию. В качестве текущего цвета устанавливается чёрный, а начало координат выбирается в качестве текущей точки. Всем элементам массива pixels присваиваются значения, соответствующие белому цвету; таким образом, только что созданное изображение будет представлять собой полностью белый прямоугольник. Функция create_image() возвращает адрес созданного изображения. Ниже размещён код данной функции.

//Создание изображения
image *create_image(uint width, uint height)
{
    uint wh = width * height;
    image *img = malloc(sizeof(image) + wh * sizeof (color));
    if (!img)
    {
        puts("Недостаточно памяти. Работа программы прекращена");
        exit(1);
    }
    img->width = width;
    img->height = height;
    img->cur_col = (color) {0, 0, 0};
    img->cur_pnt = (point) {0, 0};
    for (uint i = 0; i < wh; i++)
        img->pixels[i] = (color) {255, 255, 255};
    return img;
}

Отдельно остановимся на динамическом выделении памяти для создаваемой переменной (см. строку 5). Как мы помним, в описании типа image массив pixels объявлен без указания его размера в квадратных скобках. В стандарте С99 объявлять массивы с пустыми квадратными скобками можно только при выполнении следующих требований:

  • массив должен быть одномерным;
  • массив должен являться полем структуры;
  • это поле должно быть объявлено в структуре последним;
  • это поле не должно являться единственным полем структуры.

Из третьего требования следует, что других таких "полей-массивов" в структуре быть не может.

Если эти требования выполнены, то переменные рассматриваемого структурного типа должны создаваться динамически, причём достаточно воспользоваться лишь одним вызовом функции динамического распределения памяти, что достаточно удобно.

Если мы хотим создать такую переменную, то должны сначала определиться с размером нашего массива. Обозначим его, для определённости, n. Размер памяти, необходимой для хранения переменной, рассчитывается как сумма двух слагаемых. Первое — это значение, возвращаемое оператором sizeof, применённого к рассматриваемой структуре. При вычислении этого значения массив, объявленный без указания размера, не учитывается. Второе — это, как раз, объём памяти, требуемой для хранения массива, равный размеру памяти, необходимой для хранения одного его элемента, умноженному на n.

Именно таким образом и рассчитывается память в строке 5. Размер массива pixels, содержащийся в переменной wh, вычисляется как произведение ширины изображения и его высоты (см. строку 4).

Такой подход к динамическому выделению памяти удобен ещё и тем, что для удаления переменной, созданной таким образом, достаточно один-единственный раз вызвать функцию free(). А если бы вместо массива в качестве поля структуры был бы объявлен указатель, которому требовалось бы присвоить адрес первого элемента массива, созданного динамически, то для создания переменной пришлось бы прибегнуть к двум вызовам функции динамического распределения памяти, а для её уничтожения — к двум вызовам функции free().

Остальной код функции create_image() прост, поэтому не вижу необходимости в его комментировании.

Запись изображения в файл формата BMP — функция save_to_file()

Переходим к функции, записывающей наше изображение в файл формата BMP. Для её реализации нам нужно знать, что представляет собой данный формат.

Мы будем использовать одну из самых простых разновидностей формата BMP — несжатый 24-разрядный рисунок, не содержащий палитры. Опишем структуру файла данного типа (именно такие файлы мы и будем в дальнейшем называть "файлами в формате BMP").

Файл формата BMP состоит из двух частей. Первая часть — это заголовок, содержащий информацию о изображении, необходимую программам, которые считывают файл, для корректного распознавания изображения. К ней относятся ширина и высота изображения, его разрядность (т. е. количество бит, требуемых для сохранения цвета одного пикселя; в нашем случае она равна 24) и некоторые другие параметры. Вторая часть — это, собственно, сам растр. Он представляет собой информацию о цветах всех пикселей изображения.

Заголовок занимает в файле 54 байта и состоит из двух частей. Размер первой из них — 14 байтов, а второй — 40. В первой части заголовка содержатся параметры, описывающие графический файл в целом, а во втором — относящиеся только к растру.

Перечислим параметры, содержащиеся в первой части заголовка, в том порядке, в котором они размещаются в файле, с указанием размера занимаемой ими памяти.

  1. Текстовая строка "BM" (без кавычек), или, что то же самое, число 19778 — 2 байта.
  2. Размер всего файла — 4 байта.
  3. Зарезервированная часть, заполненная нулями, — 4 байта.
  4. Размер всего заголовка, т. е. число 54 — 4 байта.

Теперь перечислим параметры второй части заголовка.

  1. Размер второй части заголовка, т. е. число 40 — 4 байта.
  2. Ширина изображения в пикселях — 4 байта.
  3. Высота изображения в пикселях — 4 байта.
  4. Количество цветовых плоскостей, т. е. число 1 — 2 байта.
  5. Разрядность изображения, т. е. число 24 — 2 байта.
  6. Несущественный для нас параметр, имеющий нулевое значение — 4 байта.
  7. Размер части файла, в которой хранится растр — 4 байта.
  8. Четыре несущественных для нас параметра, по 4 байта каждый, имеющих нулевые значения — суммарно 16 байтов.

Обсуждение данной статьи в комментариях привело к необходимости сделать пояснения насчёт 7-го пункта. Если изображение хранится в несжатом виде, как в нашем случае, то в упомянутые 4 байта можно записать нули. Однако некоторые приложения, в частности, Photoshop и Paint, при сохранении изображения в 24-разрядном несжатом BMP-формате, всё же, записывают в эти 4 байта размер растра. Точно так же поступим и мы.

Во вторую часть файла заносятся коды цветов пикселей изображения. При заполнении файла пиксели записываются в него по горизонтальным рядам, причём ряды перебираются в направлении снизу вверх. А внутри каждого ряда пиксели перебираются в направлении слева направо. В точно таком же порядке информация о цветах пикселей расположена и в массиве pixels. Совпадение порядков позволит нам,  записывая содержимое массива в файл, перебирать его элементы в "естественном" порядке, т. е. в порядке возрастания индексов. Это очень удобно.

Коды цветов представляют собой трёхбайтовые значения. Цвета кодируются в уже рассмотренном нами формате RGB, причём байты, соответствующие трём составляющим кодируемого цвета, записываются в файл в следующем порядке: сначала байт, отвечающий за синюю компоненту, затем — за зелёную, потом — за красную. Заметим, что в переменных типа color байты, содержащие интенсивности цветов, располагаются в противоположном порядке.

И ещё одна важная особенность формата BMP. Размер части файла в байтах, отводимой для хранения информации о цветах одного горизонтального ряда пикселей, должен быть кратен 4. Но если для фактического хранения данной информации требуются байты в количестве, не делящемся на 4, то после каждого такого набора байтов, "отвечающего" за ряд, должно быть добавлено минимально необходимое число "фиктивных" байтов, чтобы требование, в итоге, было выполнено.

Обозначим это число буквой d. Пусть k — это количество байтов, фактически используемых для хранения цветов ряда пикселей. Тогда сумма чисел k и d должна быть сравнима с 0 по модулю 4:

(k + d) ≡ 0 (mod 4).

Поскольку ряд состоит из w пикселей, а для записи цвета одного пикселя требуется 3 байта, то k = 3 w. Поэтому

(3 w + d) ≡ 0 (mod 4)

или, что то же самое,

(4 ww + d) ≡ 0 (mod 4).

В силу 4 w ≡ 0 (mod 4), получаем:

(w + d) ≡ 0 (mod 4),

откуда следует, что

dw (mod 4).

Но, поскольку d < 4, это означает равенство d остатку от деления числа w на 4.

Зная d, можно найти размер в байтах той части файла, в которой хранится растр. Он равен h (3 w + d). А размер всего файла равен h (3 w + d) + 54.

Заметим, что "фиктивные" байты могут иметь произвольные значения. Мы, для определённости, в качестве "фиктивных" байтов будем использовать нулевые.

Переходим к функции save_to_file(), создающей файл формата BMP, содержащий заданное изображение. Непосредственная запись данных в файл будет осуществляться стандартной библиотечной функцией fwrite(), объявленной в заголовочном файле <stdio.h>, имеющей следующий прототип:

size_t fwrite(const void * restrict buf, size_t size, size_t count, FILE * restrict stream);

Функция fwrite() осуществляет запись в поток, адресуемый указателем stream, count элементов массива, адресуемого указателем buf, каждый элемент которого имеет размер size. Функция возвращает число успешно записанных элементов массива. Таким образом, возвращённое значение, отличное от переданного функции числа элементов массива, свидетельствует об ошибке.

Ниже приведён код самой функции save_to_file().

//Запись изображения в файл
bool save_to_file(image *img, const char *filename)
{
    FILE *fp;
    if (!(fp = fopen(filename, "wb")))
        return false;
    uint w = img->width, h = img->height;
    color *pixels = img->pixels;
    char d = w % 4;
    uint z = 0;
    ushort header[27];
    for (int i = 0; i < 27; i++)
        header[i] = 0;
    header[0] = 19778;
    *(uint *) (header + 1) = (*(uint *) (header + 17) = h * (w * 3 + d)) + 54;
    header[5] = 54;
    header[7] = 40;
    *(uint *) (header + 9) = w;
    *(uint *) (header + 11) = h;
    header[13] = 1;
    header[14] = 24;
    if (fwrite(header, 2, 27, fp) != 27)
        return false;
    for (uint i = 0; i < h; i++)
    {
        for (uint j = 0; j < w; j++)
        {
            color col = pixels[i * w + j];
            if (fwrite(&(color) {col.blue, col.green, col.red}, 1, 3, fp) != 3)
                return false;
        }
        if (d && fwrite(&z, 1, d, fp) != d)
            return false;
    }
    fclose(fp);
    return true;
}

Функция save_to_file() принимает в качестве параметров адрес изображения и адрес текстовой строки, содержащей имя создаваемого файла. Она пытается записать данное изображение в файл формата BMP, имеющий заданное имя, и возвращает значение true в случае, если создание файла прошло успешно. Если попытка записи оказалась неудачной, то возвращается значение false.

В строках 5, 6 осуществляется попытка создать двоичный файл для записи и связанный с ним поток. В случае удачи адрес потока присваивается переменной fp, объявленной в строке 4. В противном случае переменная fp получает нулевое значение, после чего в вызывающую функцию возвращается false. На этом выполнение функции save_to_file() завершается.

В строках 7,8 вводятся переменные, посредством которых можно получать значения полей переменной, адресуемой указателем img, без использования имени этого указателя.

Затем создаются вспомогательные переменные. Одна из них — переменная z, содержащая 0 (см. строку 10). Из неё при записи растра в файл мы будем брать нулевые байты, в случае, если понадобится дополнять ими наборы байтов, фактически используемых для хранения информации о цветах пикселей в горизонтальных рядах. А количество нулевых байтов, требуемых для дополнения ряда, будет находиться в переменной d, значение которой вычисляется как остаток от деления на 4 ширины изображения в пикселях (строка 9). Эта переменная будет использоваться и при вычислении размера части файла, содержащей растр.

В строке 11 создаётся 54-байтовый массив header. В этот массиве будет сформирован заголовок создаваемого графического файла. Сначала мы заполняем массив нулями (строки 12, 13), после чего сохраняем в массиве отличные от нуля значения параметров заголовка (см. строки 14-21). Полагаю, читатель сам сможет разобраться в том, значения каких именно параметров помещаются в массив в тех или иных строках кода.

Далее в строках 22, 23 содержимое массива сохраняется в файле.

Запись растра в файл осуществляется в строках 24-34. Во внешнем цикле for перебираются горизонтальные ряды пикселей в направлении снизу вверх, а во внутреннем цикле for в направлении слева направо перебираются пиксели, образующие конкретный ряд, и их цвета записываются в файл. По окончании внутреннего цикла, при необходимости, в файл записывается требуемое число дополнительных нулевых байтов (см. строки 32-33).

По окончании внешнего цикла закрывается поток, связанный с создаваемым файлом (строка 35) и функция save_to_file() возвращает значение true, свидетельствующее об успешном завершении её работы (строка 36).

Обратите внимание на то, что при каждом вызове функции fwrite() возвращаемое ею количество записанных в файл элементов массива сравнивается с тем, которое должно быть записано. В случае их несовпадения функция save_to_file() сразу же возвращает значение false, тем самым прерывая свою работу.

Получение и установка цветов пикселей изображения: функции get_color() и set_color()

Функция get_color() предназначена для получения цветов произвольных пикселей изображения. Вот её код:

//Получение цвета пикселя
color get_color(image *img, uint x, uint y)
{
    if (x < img->width && y < img->height)
        return img->pixels[y * img->width + x];
    return WHITE;
}

Функция get_color() принимает в качестве параметров адрес изображения, а также координаты пикселя по осям OX и OY. Если координаты заданы корректно, т. е. пиксель с заданными координатами принадлежит изображению, то функция возвращает его цвет в виде значения переменной типа color. В противном случае возвращается белый цвет.

Проверка координат на корректность, как видно из кода, заключается в сравнении абсциссы и ординаты пикселя с шириной и высотой изображения соответственно. Абсцисса должна быть меньше ширины, а ордината — высоты. Помимо этого, координаты должны быть неотрицательными. Но их неотрицательность уже гарантируется типом uint, к которому они принадлежат.

Следующая функция — set_color(), устанавливающая цвет произвольного пикселя изображения. Она имеет следующий вид:

//Установка цвета пикселя
image *set_color(image *img, uint x, uint y, color col)
{
    if (x < img->width && y < img->height)
        img->pixels[y * img->width + x] = col;
    return img;
}

Функция set_color() принимает в качестве параметров адрес изображения, координаты пикселя по осям OX и OY и желаемый цвет этого пикселя в виде значения типа color. Если координаты заданы корректно, то соответствующему им пикселю "присваивается" заданный цвет. В противном случае никаких действий не производится.

Функция set_color() всегда возвращает адрес изображения, который она сама приняла в качестве первого параметра. Благодаря этому, вызов данной функции можно использовать везде, где ожидается адрес изображения. Например, на месте первого аргумента в списке фактических аргументов большинства функций, прототипы которых были приведены выше.

Установка текущих цвета и точки: функции set_cur_col() и set_cur_pnt()

Для установки текущего цвета в переменной типа image предназначена функция set_cur_col():

//Установление текущего цвета
image *set_cur_col(image *img, color col)
{
    img->cur_col = col;
    return img;
}

Функция принимает в качестве параметров адрес изображения и цвет в виде значения типа color и присваивает это значение полю cur_col данного изображения. Возвращает адрес изображения, принятый в качестве первого параметра.

Функция set_cur_pnt() устанавливает текущую точку изображения:

//Установление текущей точки
image *set_cur_pnt(image *img, uint x, uint y)
{
    if (x < img->width && y < img->height)
        img->cur_pnt = (point) {x, y};
    return img;
}

Функция принимает в качестве параметров адрес изображения и координаты устанавливаемой точки. Если координаты корректны, то из них формируется значение типа point, которое присваивается полю cur_pnt изображения. Функция возвращает адрес изображения, принятый в качестве первого параметра.

Мы не будем создавать функции для чтения значений полей cur_col и cur_pnt переменной типа image. Пользователь библиотеки, при необходимости, сможет прочитать их, обратившись непосредственно к самим полям через указатель на данную переменную.

Преобразования форматов хранения цветов: функции to_color() и from_color()

В структурных переменных типа color, предназначенных для хранения цветов в формате RGB, каждая цветовая компонента содержится в отдельном поле. Однако имеется ещё один вариант хранения цветов в данном формате, называющийся COLORREF, предполагающий записи всех трёх компонент цвета в четырёхбайтовую переменную, в качестве которой может выступать, например, переменная типа uint.

Цвет в формате COLORREF представляет собой значение вида 0xrrggbb. Здесь rr, gg и bb — это записанные в шестнадцатеричном формате значения интенсивностей красной, зелёной и синей составляющих цвета соответственно. Важно отметить, что в обоих рассмотренных нами способах хранения цветов они представляются одним и тем же форматом — RGB. Различаются лишь способы размещения в памяти байтов, отвечающих за цветовые компоненты.

Пусть, например, мы хотим сохранить светло-голубой цвет, красная, зелёная и синяя компоненты которого равны 153, 217 и 234 соответственно, в переменной col типа color. Это можно сделать следующим образом:

color col = (color) {153, 217, 234};

Теперь запишем тот же самый цвет в переменную colorref типа uint, на этот раз используя способ хранения COLORREF:

uint colorref = 0x0099d9ea;

Как видим, второй вариант более компактен, к тому же, цвет задаётся единственным числом, тогда как в предыдущем случае были использованы три числа. Но зато во втором случае для хранения цвета требуется 4 байта памяти, один из которых всегда имеет нулевое значение, тогда как переменная типа color занимает лишь 3 байта. Как раз по этой причине в качестве формата хранения цветов в переменных типа image был выбран color, а не COLORREF. И именно color является основным форматом хранения цветов в библиотеке pgraph.

Однако, чтобы предоставить пользователю библиотеки возможность оперировать цветами в формате COLORREF, мы создадим две функции, выполняющие преобразования цветов из одного формата хранения в другой. Ниже приведён код этих функций.

//Из COLORREF в color
color to_color(uint colorref)
{
    uchar *p = (uchar *) &colorref;
    return (color) {*(p + 2), *(p + 1), *p};
}

//Из color в COLORREF
uint from_color(color col)
{
    uint i = 0;
    uchar *p = (uchar *) &i;
    *(p + 2) = col.red;
    *(p + 1) = col.green;
    *p = col.blue;
    return i;
}

Функция to_color() принимает в качестве параметра значение цвета в формате COLORREF и преобразует его в формат color. Функция from_color(), наоборот, принимает значение цвета в формате color и преобразует его в формат COLORREF. Каждая из функции возвращает значение цвета в новом формате.

По сути, каждая из функций копирует 3 байта из одного участка памяти в другой. Однако в новом участке памяти байты размещаются в порядке, обратном тому, в котором они располагаются в старом. Причина этого заключается в особенности хранения в памяти целочисленных значений, байты которых размещаются в памяти в порядке возрастания их старшинства. Например, число 0x0099d9ea в памяти будет размещено так: |ea|d9|99|00|.

Таким образом, компоненты цвета в формате color хранятся в памяти в порядке "от красного — к синему", а в формате COLORREF — в противоположном.

Заключение

Функции, реализованные нами в этой статье, уже позволяют нам создавать изображения "по точкам" и записывать их в графические файлы. Но нам предстоит ещё создать функции, строящие прямые линии и осуществляющие заливку одноцветных областей. Это уже будет сделано во второй статье нашего цикла публикаций, посвящённых библиотеке pgraph. Там же будет приведена программа для тестирования библиотеки и будет дана ссылка для скачивания исходного кода как самой библиотеки, так и тестирующей программы.