Описание распределения частей картинки на виджеты, а также преобразования стандартного изображения в массив данных, загрузка пользовательских изображений
В прошлой части было пропущено описание метода LoadPictureParts
из класса
GameParams
. Чтобы разобраться с ним, надо сначала понять, каким образом
хранятся части стандартного изображения. Именно сейчас об этом и будет идти
речь.
Чтобы упростить распределение приложения в виде единственного исполняемого файла для Windows платформы было решено хранить данные стандартного изображения в виде массива. Если пользователь захочет загрузить своё изображения, то для этого будет создан подкаталог, но об этом позже.
Для создания массива данных пришлось написать небольшую программу на Си. Её код:
9enum {
10 length = 9
11};
12
13static const char *parts[] = {
14 "00.png", "01.png", "02.png",
15 "10.png", "11.png", "12.png",
16 "20.png", "21.png", "22.png"
17};
18
19int main()
20{
21 int part_sizes[length];
22 int i, j, c;
23 char res_dir[] = "../resources/tucan/";
24 FILE * out = fopen("resources.cpp","wt");
25 if (out == NULL) {
26 printf("Can not open output\n");
27 return 1;
28 }
29 fprintf(out,"unsigned char image_data[] = {\n");
30 for(i = 0; i < length; ++i) {
31 char *str_copy = (char*)malloc(sizeof(res_dir) + strlen("00.png"));
32 strcpy(str_copy, res_dir);
33 FILE * in = fopen(strcat(str_copy, parts[i]), "rb");
34 if (in == NULL) {
35 printf("Can not open input file\n");
36 return 1;
37 }
38 for(j = 0, c = fgetc(in); c != EOF; c = fgetc(in), ++j) {
39 if (j%16 == 0) fprintf(out," ");
40 fprintf(out,"0x%02X, ", c);
41 if (j%16 == 15) fputs("\n", out);
42 }
43 part_sizes[i] = j;
44 fclose(in);
45 free(str_copy);
46 }
47 fprintf(out,"0xFF };\n\n");
48 fprintf(out, "int image_sizes[] = {\n");
49 for(int i = 0; i < length; ++i) {
50 if (i%16 == 0) fprintf(out," ");
51 fprintf(out,"%d, ", part_sizes[i]);
52 if (i%16 == 15) fputs("\n", out);
53 }
54 fprintf(out,"0xFF };\n\n");
55 fclose(out);
56 return 0;
57}
Сначала описываем массив строковых литералов parts
. Каждая часть изображения у нас
пронумерована двузначным числом: 1-ая цифра обозначает столбец, 2-ая строку. Так
сделано, потому что порядок виджетов идёт сверху вниз (подробнее об этом было в
прошлой статье).
В каталоге resources/tucan расположены все части изображения. Виджеты у нас создавались размером 100px на 100 px. Здесь аналогично.
Целочисленный массив part_sizes
хранит размер каждой части в байтах. res_dis
содержит путь к частям стандартного изображения.
Через fopen
открываем файл на запись. Сам файл будет называться resources.cpp
. И это неслучайно. Дело в том, что создаётся файл реализации C++ с суффиксом .cpp, в котором будет описан массив данных изображения. Мы заполняем его по всем правилам языка. После его комплиляции мы получем объектный файл, содержащий в секции .data
(инициализированные глобальные переменные) образ данных изображения. Другие файлы проекта, естественно, могу обратиться к ним.
После открытия файла на запись он связавается с Сишной структурой FILE. Мы
получаем только её адрес, сохраняем его в указатель out
. Проверяем out
на
NULL
, потому что файл может не создаться.
Если успешно создали файл, то сразу записываем в него тип и имя нашего массива данных:
29 fprintf(out,"unsigned char image_data[] = {\n");
То есть у нас получается массив беззнаковых байтов неопределённого размера. Пока что он описывается только с открывающей скобкой.
Самая главная часть происходит в цикле. Мы проходимся по каждой части
изображения. Порядок прохождения описан в массиве parts
, так что каждая
итерация будет соответствовать нужной части. Эту часть читаем, записываем в
массив image_data
, который будет содержать по порядку все байты всех частей
изображения.
Чтобы открыть на чтение нужную часть, создаём динамический массив символов.
Назвается он str_copy
. Средствами языка Си выделяем нужное количество байт в
куче:
31 char *str_copy = (char*)malloc(sizeof(res_dir) + strlen("00.png"));
Через операцию sizeof
определяем количество байт, которое занимает массив
res_dir
с учётом нулевого завершающего символа (обозначает конец строки). То
есть "../resources/tucan/"
будет определяться sizeof
20 байтами. Вместо
sizeof
также можно было бы использовать strlen
. Далее он как раз
используется. Нам нужно ещё значить количество байт для заполнения имени части
картинки. Так как они все одинаковы по длине, то можно просто использовать
00.png
. Кстати, здесь не будет учитываться нулевой символ, но это не нужно –
до этого он уже был учтён при выполнении sizeof
.
Копируем в только что выделенную память строку res_dir
через strcpy(str_copy, res_dir);
Теперь у нас в str_copy
находится "../resources/tucan/"
.
Теперь надо открыть на чтение нужную часть картинки. Для этого склеиваем строку
"../resources/tucan/
и название части из массива parts. Чтение выполняем с
флагом rb
, т.к. работаем с бинарным файлом:
33 FILE * in = fopen(strcat(str_copy, parts[i]), "rb");
Далее производим, собственно говоря, чтение файла до EOF
(конца файла). Через
цикл читаем побайтово с помощью функции библиотеки ввода-вывода fgetc
. Также у
нас есть счётчик j
. Он нужен для оформления массива в файле. Конкретно:
39 if (j%16 == 0) fprintf(out," ");
40 fprintf(out,"0x%02X, ", c);
41 if (j%16 == 15) fputs("\n", out);
Первое условие позволяем вставить отступ перед новой строкой. Количество байт
данных в строке – 16. То есть, если счётчик j
будет равен 0, 16, 32 и т.д. то
это означает, что байт данных будет находиться на новой строке.
Затем выводим байт данных. Запись 0x%02X
нужна, чтобы отобразить байт в виде
шестнадцатиричного числа, с двумя цифрам(если их меньше, то добавить 0 в
начале). 0x
для численного литерала, 02X
для форматной строки. X
здесь –
беззнаковое целое преобразовать в 16-ричное представление (при этом буквы будут
в виде ABCDEF
, тут особой разницы нет).
После окончания записи части картинки, записываем количество пройденных байт в
part_sizes[i] = j;
. Закрываем файл, освобождаем динамическую память для
формирования новой строки str_copy
.
Когда все части будут пройдены, нужно завершить описание массива в файле
resources.cpp
:
47 fprintf(out,"0xFF };\n\n");
Записываем в конец массива байт 0xFF
, который ничего не будет значить. Можно
его назвать фиктивным. После последней записанной запятой нельзя сразу поставить
закрывающую скобку, поэтому приходится его использовать.
Далее записываем полученные данных о количестве байт каждой части из массива
part_sizes
также в файл. Для этого создаём массив целых чисел под названием
image_size[]
. Проходимся по part_sizes
, записываем целочисленные значения с
переводом в десятичное представление. Оформление массива организовано также, как
и с байтами данных.
48 fprintf(out, "int image_sizes[] = {\n");
49 for(int i = 0; i < length; ++i) {
50 if (i%16 == 0) fprintf(out," ");
51 fprintf(out,"%d, ", part_sizes[i]);
52 if (i%16 == 15) fputs("\n", out);
53 }
54 fprintf(out,"0xFF };\n\n");
55 fclose(out);
Записанный файл resources.cpp
с байтами данных изображения выглядит так:
Количество байт для каждой части картинки будет в самом низу файла:
Предоставим также для других файлов проекта заголовочный файл, чтобы при компиляции не возникало ошибок. Здесь будет просто 2 объявления: массива байт, массива количества байт для каждой части изображения.
48
49#ifndef RESOURCES_HPP_SENTRY
50#define RESOURCES_HPP_SENTRY
51
52extern unsigned char image_data[];
53extern int image_sizes[];
54
55#endif
После компиляции получим файл resources.o, который будет содержать в секции
.data
записанные массивы.
LoadPictureParts
В прошлой части пришлось пропустить описание метода LoadPictureParts
из файла
puzzle.cpp
(при создании виджета) из-за того, что не был раскрыт принцип хранения стандартного
изображения. Настала пора вернуться к нему:
46Fl_PNG_Image *GameParams::LoadPictureParts(std::unique_ptr<Puzzle>& tmp_puzzle)
47{
48 Fl_PNG_Image *img = nullptr;
49 /*
50 * if loading from Resources dir
51 */
52 if(cur_directory.size()) {
53 find_path_to_picture(tmp_puzzle->path, cur_directory,
54 tmp_puzzle->sequence_number);
55 img = new Fl_PNG_Image(tmp_puzzle->path.c_str());
56 /*
57 * loading a standard image from RAM (image_converter/resources.cpp)
58 */
59 } else {
60 int offset = 0;
61 for(int i = 0; i < tmp_puzzle->sequence_number; ++i)
62 offset += image_sizes[i];
63 unsigned char *image_buffer = image_data + offset;
64 img = new Fl_PNG_Image(nullptr, image_buffer,
65 image_sizes[tmp_puzzle->sequence_number]);
66 }
67 return img;
68}
Через условие проверяем, выбрал ли рандом пользовательское или стандартное
изображение. Это можно узнать благодаря тому, что cur_directory
может хранить
относительный путь к каталогу с пользовательским изображением, иначе будет
определяться стандартное изображение.
Поиск нужной части паззла из пользовательского изображения осуществляется с
помощью функции find_path_to_picture
. Если быть точнее, то под ним
подразумевается формирование правильного относительного пути к конкретной части
паззла (части изображения):
38static void find_path_to_picture(std::string& path,
39 const std::string& cur_directory, int number)
40{
41 path =
42 cur_directory + std::to_string(number / puzzles_per_side) +
43 std::to_string(number % puzzles_per_side) + ".png";
44}
path
является полем объекта типа Puzzle
. Мы передаём по ссылке его, чтобы
найти и изменить значение.
Как помним, название частей состоят из 2-ух цифр: первая обозначает стобец,
вторая строку. В функцию передаётся number
означающее порядковый номер части
паззла. Для вычисления из него этих двух цифр используется такой принцип:
number
на
puzzles_per_side
(количество паззлов на сторону поля, т.е. 3). Полученное
число и будет столбцом. Например, порядкой номер 4 будет равен 1 столбцу.path
формируется из слияния строк: текущая директория, 2 цифры для определения
части, расширение файла (.png).
Затем создаётся объект Fl_PNG_Image
средствами библиотеки FLTK. Мы просто
через конструктор передаём указатель на строку (на первый элемент символьного
массива), который содержит путь к части паззла.
Это было описание формирования части пользовательского изображения. Для стандартного – принцип другой.
Напомню, в файле “resources.cpp” были описаны 2 массива: image_data
и
image_sizes
, которые дают всю информацию о стандартном изображении. Порядковый
номер нужной части изображения хранится в sequence_number
, поэтому нужно
только вычислить смещение относительно начала массива. Однако каждая часть имеет
своё количество байт данных. Здесь нам как раз пригодится массив image_sizes
.
С помощью него можно знать, на какое количество смещаться для пропуска каждой
части байт:
60 int offset = 0;
61 for(int i = 0; i < tmp_puzzle->sequence_number; ++i)
62 offset += image_sizes[i];
63 unsigned char *image_buffer = image_data + offset;
64 img = new Fl_PNG_Image(nullptr, image_buffer,
65 image_sizes[tmp_puzzle->sequence_number]);
Через цикл подсчитываем количество байт для пропуска. Сохраняем смещение в
переменную offset
. Указатель image_buffer
будет содержать адрес, откуда
начинать считывать байты.
Функция Fl_PNG_Image
перегружена. 2-ой её вариант имеет следующую сигнатуру:
Fl_PNG_Image (const char *name_png, const unsigned char *buffer, int datasize)
. Она позволяет создавать объект Fl_PNG_Image
через чтение
переданного массива (буфера) данных. Поэтому 2-ым аргументом передаём указатель
на первый элемент массива (со смещением), 3-им количество байт (через массив
image_sizes
).
После создания объекта Fl_PNG_Image
функция LoadPictureParts
возвращает его
адрес.
Таким образом, на данным момент были разобраны все ключевые моменты создания виджетов-паззлов.
Уже неоднократно говорилось, что программа перед началом игры выбирает
стандартное изображение или пользовательские. Про то, как хранится стандартное
– описано выше (в виде массива байт). Пользовательские же хранятся просто в
подкаталоге resources
. Для этого нужно проделать следующие действия:
resources
, а также подкаталог с таким же названием,
как и у загружаемого файла.Для изменения размера и обрезки на части было решено задействовать внешнюю библиотеку под названием stb (https://github.com/nothings/stb). Она примечательна тем, что распространяется в виде заголовочных файлов, т.е. вся её реализация заключается в них. Но об этом речь пойдет чуть позже.
В прошлой статье был разобран файл main.cpp
. В нём описывалось создание
подпунктов меню. Для каждого из них нужно было сопоставить callback-функцию. Его
часть была такая:
33 Fl_Sys_Menu_Bar *sys_bar = new Fl_Sys_Menu_Bar(0, 0, 320, 20, nullptr);
34 sys_bar->add("&File/&New game", nullptr, new_game_callback, params);
35 sys_bar->add("&File/&Load file", nullptr, load_file_callback);
36 sys_bar->add("&File/&Exit", nullptr, exit_callback);
37 sys_bar->add("&Options/&Show solution", nullptr, solve_problem_callback,
Нас конкретно интересует 35 строка в данном случае. При нажатии подпункта load
file будет вызвана функция load_file_callback
. Все эти функции описаны в файле
menu_callbacks
. Пока что рассмотрим в нём только эту функцию. Остальные – в
следующей статье.
19
20static bool check_correct_path_to_img(const char *path)
21{
22 if(path == nullptr)
23 return false;
24 std::string p(path);
25 std::string ext = p.substr(p.find_last_of('.'));
26 return ext == ".png" || ext == ".jpg";
27}
28
29void load_file_callback(Fl_Widget *sender, void*)
30{
31 const char *path = nullptr;
32 auto dialog = Fl_Native_File_Chooser{};
33 dialog.type(Fl_Native_File_Chooser::BROWSE_FILE);
34 dialog.filter("JPEG Files\t*.jpg\nPNG Files\t*.png");
35#if defined(_WIN32)
36 dialog.directory((std::string {getenv("HOMEPATH")} + "\\Desktop").c_str());
37#else
38 dialog.directory((std::string {getenv("HOME")} + "/Desktop").c_str());
39#endif
40 dialog.options(Fl_Native_File_Chooser::SAVEAS_CONFIRM |
41 Fl_Native_File_Chooser::NEW_FOLDER);
42 if (dialog.show() == 0)
43 path = dialog.filename();
44 if(check_correct_path_to_img(path)) {
45 ImageHandler ih(path);
46 ih.load_img();
47 ih.resize_img();
48 ih.save_img();
49 fl_message_title("Info");
50 fl_message("Image successfully added");
51 }
52}
Функция load_file_callback
создает виджет диалогового окна типа
Fl_Native_File_Chooser
. Этот класс позволяет приложению получить доступ к
нативному обзорщику файлов. В тех случаях, когда встроенного файлового браузера
нет, вместо него используется собственный файловый браузер FLTK.
Через метод type
передаём константу BROWSE_FILE
перечислимого типа,
описанную в библиотке FLTK. Конкретно эта константа обозначает обзор файлов,
позволяет выбрать только один файл.
Через filter
задаём фильтр типов файлов ,которые будут отображаться в
обзорщике. Нас интересуют только jpg, png.
Далее, в зависимости от того, для какой ОС мы производим компиляцию, будет
выбрана текущая директория. Через функцию из Си (которая доступна и в C++
разумеется) getenv
передаём переменную окружения. Для Windows это будет
HOMEPATH
, для Unix – HOME
. Функция вернёт значения переменной, т.е. путь до
домашней директории. Далее мы склеиваем эту строку и относительный путь к
каталогу Desktop
. Обратите внимание, разделители также будут разными (обычный
слэш и обратный слэш).
Затем проверяем, если диалоговое окно закрыто, то можно получить путь до выбранного файла:
42 if (dialog.show() == 0)
43
44 path = dialog.filename();
Окно могло быть закрыть и без выбора файла. Либо могло получиться так, что
пользователь в опциях обзорщика файлов выбрал просмотр файлов с любым
расширением. Поэтому надо обязательно проверить указатель на строку path
. Для
этого была написана функция check_correct_path_to_img()
:
20static bool check_correct_path_to_img(const char *path)
21{
22 if(path == nullptr)
23 return false;
24 std::string p(path);
25 std::string ext = p.substr(p.find_last_of('.'));
26 return ext == ".png" || ext == ".jpg";
27}
Здесь всё просто: если было закрыто окно без выбора файла, то path
будет иметь
nullptr
, следовательно никаких действия по обработке изображения не надо
предпринимать. Возвращаем логическое значение false
.
В ином случае создаём объект string
из path
для облегчённой работы со
строкой. Используем его метод find_last_of
для поиска последнего вхождения
символа “.”, который вернёт индекс. Этот индекс передаём уже методу substr
для
извлечения подстроки. Этими действиями мы хотим получить расширение файла.
Последней строкой как раз производится сравнения символьных литералов с
полученной строкой. В зависимости от этого функция вернёт логическое значение.
Возвращаемся к функции load_file_callback
. Если пользователь действительно
выбрал файл с нужным нам расширением, то нужно его обработать. Это происходит в
том числе с помощью внешней библиотеки. Подробнее об обработке речь пойдет ниже.
Пока что отмечу, что обработчик изображения ImageHandler
является классом,
описанным в файле img_handler.hpp
. Объекту этого класса передаётся путь до
выбранного файла (в это же время будет создаваться соответствующий подкаталог).
Поочерёдно вызываются методы для загрузки изображения в память, уменьшения его
размера, обрезки на части с последующим сохранением каждой. В конце показывается
диалоговое окно с информацией об успешной загрузки.
43 if(check_correct_path_to_img(path)) {
44 ImageHandler ih(path);
45 ih.load_img();
46 ih.resize_img();
47 ih.save_img();
48 fl_message_title("Info");
49 fl_message("Image successfully added");
50 }
Для обработки изображений задействована библиотека stb (https://github.com/nothings/stb), которая распространяется в виде заголовочных файлов. Каждый заголовочный файл объединяет в себе соответствующие функции обработки. В этом проекте использовались следующие файлы:
stb_image.h | stb_image_resize2.h | stb_image_write.h |
---|---|---|
image loading/decoding from file/memory: JPG, PNG, TGA, BMP, PSD, GIF, HDR, PIC | resize images larger/smaller with good quality | image writing to disk: PNG, TGA, BMP |
Как видим библиотека поддерживает многообразные форматы изображения.
Необычный способ реализации библиотеки оправдывается тем, что:
Что ж. Её действительно просто использовать, но время компиляции проекта заметно увеличилось. Так ещё при каждом исправлении файла img_handler.cpp препроцессору надо обработать эту библиотеку, чтоб вставить код в файл. Всё это занимает время прилично. Традиционный подход к библиотекам основан на отдельных единицах трансляции: скомпилированный код находится в объектном файле, и даже если он поменялся (изменилась внутренняя реализация), файлам проекта не надо заново перекомпилироваться, т.к. заголовочный файл остался тот же.
Но этот минус для меня перекрываются 2-мя важными моментами: просто подключить к проекту и нет никаких зависимостей. Последнее даёт возможность статически собрать исполняемый файл.
Для удобства использования фукнциями обработки изображения был создан класс
ImageHandler
. Обратимся к его описанию:
1#ifndef IMG_HANDLER_HPP_SENTRY
2#define IMG_HANDLER_HPP_SENTRY
3
4#include <string>
5
6enum {
7 width = 300,
8 height = 300
9};
10
11class ImageHandler {
12 int channel;
13 int in_width;
14 int in_height;
15 std::string source_path;
16 std::string destination_directory;
17 unsigned char *img_data;
18 unsigned char *resized_data;
19public:
20 ImageHandler(const char *p);
21 void load_img();
22 void resize_img();
23 void save_img();
24};
25
26#endif
Константы-идентификаторы width
и height
имеют такие значения ширины и
высоты, к которым исходное изображение должно быть преобразовано.
Переменные channel
, in_width
и in_height
будут заполнены на стадии
загрузки изображения через внешнюю библиотеку.
channel
обозначает количество 8-битных компонентов на пиксель (скорее всего
оно будет равно 3). Выходное изображение, содержащее N компонентов, содержит
следующие компоненты, чередующиеся в таком порядке в каждом пикселе:
N | components |
---|---|
1 | grey |
2 | grey, alpha |
3 | red, green, blue |
4 | red, green, blue, alpha |
in_width
– ширина входного изображения, in_height
– высота входного изображения.
source_path
– полный путь к изображению, полученный от файлового обзорщика.
destination_directory
– путь к созданной директории, где будут хранится части
изображения,
img_data
– возвращаемое значение из загрузчика изображений - это unsigned char*
, который указывает на пиксельные данные (или NULL в случае сбоя выделения
памяти, или если изображение повреждено или недействительно. Пиксельные данные
состоят из Y строк развертки, состоящих из X пикселей (где Y - высота в
пикселях, X - ширина в пикселях), причем каждый пиксель состоит из N (channel
)
чередующихся 8-битных компонентов; первый пиксель, на который указывает
указатель, находится в самом верхнем левом углу изображения. Между строками
развертки изображения или между пикселями нет отступов, независимо от формата.
resized_data
- указатель на первый элемент массива данных изображения с
изменённым размером.
Метод load_img()
для загрузки изображения и получения указателя img_data
.
resize_img()
для изменения размера. save_img()
для прохождения по массиву
пикселей изменённого изображения и сохранения соответствующей части в нужных
размерах.
Файл реализации будет рассматриваться по частям. В нём в самом начале подключается библиотека stb через обычные директивы #include. Дополнительно определяются макросимволы для правильной работы библиотеки:
9#define STB_IMAGE_IMPLEMENTATION
10#define STB_IMAGE_WRITE_IMPLEMENTATION
11#define STB_IMAGE_RESIZE_IMPLEMENTATION
12#include "lib/stb_image.h"
13#include "lib/stb_image_resize2.h"
14#include "lib/stb_image_write.h"
Далее начнём описание с единственного конструктора класса ImageHandler
:
22ImageHandler::ImageHandler(const char *p) : source_path(p) {
23 img_data = nullptr;
24 resized_data = nullptr;
25 /*
26 * creating resources directory (if it didn't exist)
27 */
28 std::filesystem::path res_path = std::string("resources");
29 if(!std::filesystem::exists(res_path))
30 std::filesystem::create_directory(res_path);
31
32#if defined(_WIN32)
33 auto idx = source_path.find_last_of('\\');
34#else
35 auto idx = source_path.find_last_of('/');
36#endif
37 std::string file_name = source_path.substr(idx + 1);
38 std::string fn_withot_ext =
39 file_name.substr(0, file_name.find_first_of('.'));
40#if defined(_WIN32)
41 destination_directory = "resources\\" + fn_withot_ext + "\\";
42#else
43 destination_directory = "resources/" + fn_withot_ext + '/';
44#endif
45 std::filesystem::create_directory(destination_directory);
46}
Конструктор получает в качестве аргумента указатель на строку,
которая хранит полный путь к изображению, который выбрал пользователь. Для
пользовательских изображений нужно организовать хранение его частей в каталоге.
Принцип такой: будет создана директория resources
, в ней будут создаваться
поддиректории с таким же именем, как и у файла.
Прежде чем создать директорию resources надо проверить, существует ли она уже.
Для этого воспользуемся средствами из C++ 17. Создаём объект типа path
, в
который записываем относительный путь к resources. Через метод exists
проверяем существование директории. Он должен вернуть логическое значение. Если
false
– создаём директорию:
29 if(!std::filesystem::exists(res_path))
30 std::filesystem::create_directory(res_path);
Создание этой директории было довольно простым шагом. Однако с созданием поддиректории придётся повозиться.
Как мы знаем, res_path
хранит полный путь к файлу. Из него надо вытащить имя
файла. Но всё портит тот факт, что разделители путей для UNIX и Windows
различаются. Поэтому приходится использовать предопределённый макросимвол
препроцессора _WIN32. Если он определён (а это произойдет только на платформе
Windows), то соответствующий отрывок кода будет участвовать в компиляции, иначе
компиляции происходит на UNIX, тогда будет другой участок (после макродирективы
#else
).
Перед именем файла должен находится символ разделителя в пути. Определим эту
позицию с помощью метода find_last_of()
(в программе он уже использовался и
описывался). Увеличим полученный индекс на 1, чтобы индексация была прямо на начало имени:
32#if defined(_WIN32)
33 auto idx = source_path.find_last_of('\\');
34#else
35 auto idx = source_path.find_last_of('/');
36#endif
37 std::string file_name = source_path.substr(idx + 1);
file_name
теперь содержит имя файла с расширением .png или .jpg. Расширение
нужно тоже убрать. Через substr()
можно получить подстроку, указав в качестве
начального индекса 0
, а в качестве конечного индекса (этот символ не будет
учитываться) значение от find_last_of()
(последнее вхождение точки).
fn_without_ext
будет как раз иметь имя подкатолога. Осталось только создать
относительный путь через слияние строкового литерала "resources/"
и
полученного имени подкаталога. Опять же, из-за разделителей в пути надо
использовать директивы условной компиляции. После этого уже создаём подкаталог
через create_directory
:
38 std::string fn_withot_ext =
39 file_name.substr(0, file_name.find_first_of('.'));
40#if defined(_WIN32)
41 destination_directory = "resources\\" + fn_withot_ext + "\\";
42#else
43 destination_directory = "resources/" + fn_withot_ext + '/';
44#endif
45 std::filesystem::create_directory(destination_directory);
Кстати говоря, destination_directory
ещё пригодится при сохранения вырезанных
частей изображения.
Далее рассмотрим 2 метода, которые уже задействуют функции внешней библиотеки stb:
48void ImageHandler::load_img()
49{
50 img_data = stbi_load(source_path.c_str(), &in_width, &in_height,
51 &channel, 0);
52}
53
54void ImageHandler::resize_img()
55{
56 resized_data =
57 reinterpret_cast<unsigned char*>(malloc(in_width * in_height *
58 channel));
59 stbir_resize_uint8_linear(img_data, in_width, in_height, 0, resized_data,
60 width, height, 0, (stbir_pixel_layout) channel);
61}
В методе load_img()
функция stbi_load
описана в заголовочном файле
stb_image.h. Нужно передать ей путь к файлу. Путь source_path
уже был заполнен
на этапе конструирования объекта ImageHandler
. Остальные 3 аргумента
передаются для заполнения данными функцией. Последний аргумент нужен для
изменения количества компонент на пиксель. По умолчанию оставляется 0
. В итоге
получаем указатель img_data
на первый пиксель изображения.
Метод resize_img
обрабатывает полученные данные. Для начала выделим
динамическую память под уменьшенное изображение через malloc
. Количество
нужных байт вычисляем как: in_width * in_height * channel
. То есть умножаем
количество байт в ширине на количество байт в длине на количество компонент в 1
пикселе.
Далее используется функция stbir_resize_uint8_linear
из заголовочника
stb_image_resize2.h для изменения размера изображения. Её сигнатура:
unsigned char* stbir_resize_uint8_linear(input_pixels, input_w, input_h, input_stride_in_bytes, output_pixels, output_w, output_h, output_stride_in_bytes, pixel_layout_enum)
.
Честно говоря, пример по её использованию был взят отсюда:
https://stackoverflow.com/questions/70683277/i-cant-resize-image-with-stb-image-resize-h
Можно сказать, что в input_pixels
, input_w
, input_h
должны передаваться
данные исходного изображения. Всё это у нас было получено в методе load_img
. В
output_w
и output_h
передаём целочисленные константы, которые обозначают
нужный нам размер изображения (т.е. 300 x 300). По итогу выделенный буфер
resized_data
будет заполнен данными изображения с изменённым размером.
Переходим к методу save_img()
:
63void ImageHandler::save_img()
64{
65 int x, y, i, j, k = 0;
66 for(i = 0; i < puzzles_per_side; ++i)
67 for(j = 0; j < puzzles_per_side; ++j, ++k) {
68 x = i * puzzle_size;
69 y = j * puzzle_size;
70 std::string tmp_path =
71 destination_directory +
72 std::to_string(k / puzzles_per_side) +
73 std::to_string(k % puzzles_per_side) + ".png";
74 stbi_write_png(tmp_path.c_str(), puzzle_size, puzzle_size, channel,
75 resized_data + channel * (x + y * width),
76 width * channel);
77 }
78 stbi_image_free(resized_data);
79 stbi_image_free(img_data);
80}
Данный метод реализует вырезку частей изображения с последующим сохранением в директории.
Итак, через циклы по порядку создаём координаты частей изображения.
puzzle_per_side
равен 3. То есть всего будет создано 9 частей. Внутренний цикл
для прохода по высоте, внешний – по ширине. Координата x (ширина) определяется через
количество пройденных частей: i * puzzle_size
(puzzle_size
равен 100). Тоже
самое проделывается и с координатой y (высота).
Далее надо сгенерировать путь вместе с именем, чтобы каждую часть сохранять по нему.
destination_directory
была заполнена на этапе конструирования объекта. То есть
сейчас он содержит относительный путь resources/%имя изображения%/. Осталось
только дополнить его именем части. Для этого был описан счётчик k
, который
увеличивается при каждой итерации внутреннего цикла. Таким образом получим
порядковый номер части. Номера частей идут сверху вниз, по столбцам. Об этом
говорилось и в прошлой статье, и в этой. Внутренний цикл идёт по высоте
(обновляя координаты y), что как раз и подходит нам.
Имея порядковый номер части, легко сформировать его имя. Принцип уже был описан
в функции find_path_to_picture
. Если кратко, то первая цифра должна обозначать
столбец, а вторая строку. К имени добавляется формат изображения .png.
Затем используется функция из библиотеки stb для сохранения соответствующей части:
74 stbi_write_png(tmp_path.c_str(), puzzle_size, puzzle_size, channel,
75 resized_data + channel * (x + y * width),
76 width * channel);
Первый аргументом как раз передаём созданный путь с именем части изображения.
puzzle_size
– размер по ширине и высоте. У нас это 100 px. channel
был
заполнен на этапе загрузки исходного изображения через функцию stbi_load
.
Оставляем его таким же. 5-ым аргументов передаём смещение относительно начала
массива пикселей. Как он организован было уже описано, но повторим ещё раз:
пиксельные данные состоят из Y строк развертки, состоящих из X пикселей (где Y -
высота в пикселях, X - ширина в пикселях), причем каждый пиксель состоит из N
(channel) чередующихся 8-битных компонентов.
То есть получается, надо сначала пропустить нужное количество строк. Y является
координатой, которая как раз обознает это количество, width
количество
пикселей в ширине (300 px), т.е. можно понимать под этим как саму строку. Но каждый
пиксель состоит из нескольких компонент (channel
), поэтому надо умножить y * width * channel
. Мы сместились по высоте, определили начало нужной строки.
Осталось сместиться по самой строке в ширину. Для этого прибавляем к полученному
значению x * channel
: координату по x с учётом количества компонент на
пиксель.
6-аргумент width * channel
представляет собой разницу между адресами строк.
Вообще, параметр называется как stride_in_bytes
(шаг в байтах). Этот шаг
определяется по количеству пикселей в ширину с учётом channel
.
В следуюущей (финальной) части будет рассмотрен алгоритм A* для решения этой головоломки, а также внедрение его в этот проект.