Picture puzzle на C++. Часть 2

1 мар. 2025 г. 17:22:08 MSK

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

Ссылка на предыдущую часть

Конвертация изображения в массив данных

В прошлой части было пропущено описание метода LoadPictureParts из класса GameParams. Чтобы разобраться с ним, надо сначала понять, каким образом хранятся части стандартного изображения. Именно сейчас об этом и будет идти речь.

converter.c

Чтобы упростить распределение приложения в виде единственного исполняемого файла для 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. Здесь аналогично.

picture_parts

Целочисленный массив 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

Записанный файл resources.cpp с байтами данных изображения выглядит так:

bytes_of_image

Количество байт для каждой части картинки будет в самом низу файла:

sizes_of_image

resources.hpp

Предоставим также для других файлов проекта заголовочный файл, чтобы при компиляции не возникало ошибок. Здесь будет просто 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 (при создании виджета) из-за того, что не был раскрыт принцип хранения стандартного изображения. Настала пора вернуться к нему:

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 означающее порядковый номер части паззла. Для вычисления из него этих двух цифр используется такой принцип:

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. Для этого нужно проделать следующие действия:

Для изменения размера и обрезки на части было решено задействовать внешнюю библиотеку под названием 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. Обратите внимание, разделители также будут разными (обычный слэш и обратный слэш).

file_chooser

Затем проверяем, если диалоговое окно закрыто, то можно получить путь до выбранного файла:

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-мя важными моментами: просто подключить к проекту и нет никаких зависимостей. Последнее даёт возможность статически собрать исполняемый файл.

img_handler.hpp

Для удобства использования фукнциями обработки изображения был создан класс 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() для прохождения по массиву пикселей изменённого изображения и сохранения соответствующей части в нужных размерах.

img_handler.cpp

Файл реализации будет рассматриваться по частям. В нём в самом начале подключается библиотека 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* для решения этой головоломки, а также внедрение его в этот проект.