1 #include "picture_load.hpp"
2 #include <glibmm/miscutils.h>
3 #include <glibmm/fileutils.h>
4 #include <giomm/file.h>
5 #include <set>
6 #include "util/util.hpp"
7 #include <algorithm>
8 #include <png.h>
9 #include "logger/logger.hpp"
10 
11 namespace horizon {
12 
build_pic_filename(const std::string & dir,const UUID & uu,const std::string & suffix)13 static std::string build_pic_filename(const std::string &dir, const UUID &uu, const std::string &suffix)
14 {
15     return Glib::build_filename(dir, (std::string)uu + "_" + suffix + ".png");
16 }
17 
18 // adapted from https://gitlab.freedesktop.org/cairo/cairo/-/blob/master/src/cairo-png.c
19 
png_simple_error_callback(png_structp png,png_const_charp error_msg)20 static void png_simple_error_callback(png_structp png, png_const_charp error_msg)
21 {
22     throw std::runtime_error(std::string("png error: ") + error_msg);
23 }
24 
png_simple_warning_callback(png_structp png,png_const_charp error_msg)25 static void png_simple_warning_callback(png_structp png, png_const_charp error_msg)
26 {
27 }
28 
29 struct Png {
30     png_struct *png = nullptr;
31     png_info *info = nullptr;
32 };
33 
34 struct PngWrite : Png {
~PngWritehorizon::PngWrite35     ~PngWrite()
36     {
37         if (png)
38             png_destroy_write_struct(&png, &info);
39     }
40 };
41 
42 
43 struct PngRead : Png {
~PngReadhorizon::PngRead44     ~PngRead()
45     {
46         if (png)
47             png_destroy_read_struct(&png, &info, NULL);
48     }
49 };
50 
51 struct FileWrapper {
FileWrapperhorizon::FileWrapper52     FileWrapper(const char *filename, const char *modes)
53     {
54 #ifdef G_OS_WIN32
55         auto wfilename = reinterpret_cast<wchar_t *>(g_utf8_to_utf16(filename, -1, NULL, NULL, NULL));
56         auto wmodes = reinterpret_cast<wchar_t *>(g_utf8_to_utf16(modes, -1, NULL, NULL, NULL));
57         fp = _wfopen(wfilename, wmodes);
58         g_free(wfilename);
59         g_free(wmodes);
60 #else
61         fp = fopen(filename, modes);
62 #endif
63         if (!fp) {
64             throw std::runtime_error("fopen error");
65         }
66     }
67     FILE *fp = NULL;
~FileWrapperhorizon::FileWrapper68     ~FileWrapper()
69     {
70         if (fp)
71             fclose(fp);
72     }
73 };
74 
convert_data_to_bytes(png_structp png,png_row_infop row_info,png_bytep data)75 static void convert_data_to_bytes(png_structp png, png_row_infop row_info, png_bytep data)
76 {
77     unsigned int i;
78 
79     for (i = 0; i < row_info->rowbytes; i += 4) {
80         uint8_t *b = &data[i];
81         uint32_t pixel;
82 
83         memcpy(&pixel, b, sizeof(uint32_t));
84 
85         b[2] = (pixel & 0x00ff'0000) >> 16;
86         b[1] = (pixel & 0x0000'ff00) >> 8;
87         b[0] = (pixel & 0x0000'00ff) >> 0;
88         b[3] = (pixel & 0xff00'0000) >> 24;
89     }
90 }
91 
convert_bytes_to_data(png_structp png,png_row_infop row_info,png_bytep data)92 template <bool use_alpha> static void convert_bytes_to_data(png_structp png, png_row_infop row_info, png_bytep data)
93 {
94     unsigned int i;
95 
96     for (i = 0; i < row_info->rowbytes; i += 4) {
97         uint8_t *base = &data[i];
98         uint8_t red = base[2];
99         uint8_t green = base[1];
100         uint8_t blue = base[0];
101         uint8_t alpha = base[3];
102         uint32_t pixel;
103 
104         pixel = (red << 16) | (green << 8) | (blue << 0);
105         if constexpr (use_alpha) {
106             pixel |= alpha << 24;
107         }
108         else {
109             pixel |= (0xff << 24);
110         }
111         memcpy(base, &pixel, sizeof(uint32_t));
112     }
113 }
114 
read_png(const std::string & filename,const UUID & uu)115 static std::shared_ptr<PictureData> read_png(const std::string &filename, const UUID &uu)
116 {
117     FileWrapper fp{filename.c_str(), "rb"};
118 
119     int status = 0;
120     PngRead png;
121     png.png = png_create_read_struct(PNG_LIBPNG_VER_STRING, &status, png_simple_error_callback,
122                                      png_simple_warning_callback);
123     if (png.png == NULL) {
124         throw std::runtime_error("png error png");
125     }
126 
127     png.info = png_create_info_struct(png.png);
128     if (png.info == NULL) {
129         throw std::runtime_error("png error info");
130     }
131 
132     png_init_io(png.png, fp.fp);
133 
134     png_read_info(png.png, png.info);
135     int depth, color_type, interlace;
136     png_uint_32 png_width, png_height;
137 
138     png_get_IHDR(png.png, png.info, &png_width, &png_height, &depth, &color_type, &interlace, NULL, NULL);
139     if (status) { /* catch any early warnings */
140         throw std::runtime_error("png error header");
141     }
142 
143     switch (color_type) {
144     case PNG_COLOR_TYPE_RGB_ALPHA:
145         png_set_read_user_transform_fn(png.png, convert_bytes_to_data<true>);
146         break;
147     case PNG_COLOR_TYPE_RGB:
148         png_set_read_user_transform_fn(png.png, convert_bytes_to_data<false>);
149         break;
150     default:
151         throw std::runtime_error("unsupported color type " + std::to_string(color_type));
152     }
153 
154     if (depth != 8) {
155         throw std::runtime_error("unsupported color depth");
156     }
157 
158     if (interlace != PNG_INTERLACE_NONE)
159         throw std::runtime_error("unsupported interlacing");
160 
161     png_set_filler(png.png, 0xff, PNG_FILLER_AFTER);
162 
163     std::vector<uint32_t> data;
164     data.resize(png_width * png_height);
165 
166     std::vector<png_byte *> row_pointers;
167 
168     for (size_t i = 0; i < png_height; i++) {
169         row_pointers.push_back(reinterpret_cast<png_byte *>(data.data() + (i * png_width)));
170     }
171 
172     png_read_image(png.png, row_pointers.data());
173     png_read_end(png.png, png.info);
174 
175     return std::make_shared<PictureData>(uu, png_width, png_height, std::move(data));
176 }
177 
pictures_load(const std::list<std::map<UUID,Picture> * > & pictures,const std::string & dir,const std::string & suffix)178 void pictures_load(const std::list<std::map<UUID, Picture> *> &pictures, const std::string &dir,
179                    const std::string &suffix)
180 {
181     std::map<UUID, std::shared_ptr<const PictureData>> pictures_loaded;
182     for (const auto &it_map : pictures) {
183         for (auto &it : *it_map) {
184             if (pictures_loaded.count(it.second.data_uuid)) {
185                 it.second.data = pictures_loaded.at(it.second.data_uuid);
186             }
187             else {
188                 auto pic_filename = build_pic_filename(dir, it.second.data_uuid, suffix);
189                 try {
190                     if (Glib::file_test(pic_filename, Glib::FILE_TEST_IS_REGULAR)) {
191                         auto data = read_png(pic_filename, it.second.data_uuid);
192                         it.second.data = data;
193                         pictures_loaded.emplace(it.second.data_uuid, data);
194                     }
195                 }
196                 catch (const std::exception &e) {
197                     Logger::log_warning("error loading picture " + (std::string)it.second.data_uuid,
198                                         Logger::Domain::PICTURE, e.what());
199                 }
200                 catch (...) {
201                     Logger::log_warning("error loading picture " + (std::string)it.second.data_uuid,
202                                         Logger::Domain::PICTURE);
203                 }
204             }
205         }
206     }
207 }
208 
write_png_to_file(const std::string & filename,const PictureData & data)209 static void write_png_to_file(const std::string &filename, const PictureData &data)
210 {
211     /* PNG complains about "Image width or height is zero in IHDR" */
212     if (data.width == 0 || data.height == 0) {
213         throw std::runtime_error("image must not have zero width or height");
214     }
215 
216     FileWrapper fp{filename.c_str(), "wb"};
217     PngWrite png;
218 
219     int status = 0;
220     png.png = png_create_write_struct(PNG_LIBPNG_VER_STRING, &status, png_simple_error_callback,
221                                       png_simple_warning_callback);
222     if (png.png == NULL) {
223         throw std::runtime_error("png error");
224     }
225 
226     png.info = png_create_info_struct(png.png);
227     if (png.info == NULL) {
228         throw std::runtime_error("png error");
229     }
230 
231     png_init_io(png.png, fp.fp);
232 
233     const int bpc = 8;
234     const int png_color_type = PNG_COLOR_TYPE_RGB_ALPHA;
235 
236     png_set_IHDR(png.png, png.info, data.width, data.height, bpc, png_color_type, PNG_INTERLACE_NONE,
237                  PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
238 
239     png_color_16 white;
240     white.gray = (1 << bpc) - 1;
241     white.red = white.blue = white.green = white.gray;
242     png_set_bKGD(png.png, png.info, &white);
243 
244     std::vector<const png_byte *> row_pointers;
245 
246     for (size_t i = 0; i < data.height; i++) {
247         row_pointers.push_back(reinterpret_cast<const png_byte *>(data.data.data() + (i * data.width)));
248     }
249 
250     png_set_rows(png.png, png.info, const_cast<png_byte **>(row_pointers.data()));
251     png_set_write_user_transform_fn(png.png, convert_data_to_bytes);
252     png_write_png(png.png, png.info, PNG_TRANSFORM_IDENTITY, NULL);
253 }
254 
pictures_save(const std::list<const std::map<UUID,Picture> * > & pictures,const std::string & pic_dir,const std::string & suffix)255 void pictures_save(const std::list<const std::map<UUID, Picture> *> &pictures, const std::string &pic_dir,
256                    const std::string &suffix)
257 {
258     bool has_pics = std::any_of(pictures.begin(), pictures.end(), [](const auto &x) { return x->size(); });
259     if (has_pics && !Glib::file_test(pic_dir, Glib::FILE_TEST_IS_DIR)) {
260         Gio::File::create_for_path(pic_dir)->make_directory_with_parents();
261     }
262     std::set<UUID> pictures_to_delete;
263     if (Glib::file_test(pic_dir, Glib::FILE_TEST_IS_DIR)) {
264         Glib::Dir dir(pic_dir);
265         for (const auto &it : dir) {
266             if (endswith(it, "_" + suffix + ".png")) {
267                 pictures_to_delete.emplace(it.substr(0, 36));
268             }
269         }
270     }
271     for (const auto &it_map : pictures) {
272         for (const auto &it : *it_map) {
273             if (it.second.data) {
274                 try {
275                     auto data = it.second.data;
276                     pictures_to_delete.erase(it.second.data->uuid);
277                     auto pic_filename = build_pic_filename(pic_dir, it.second.data_uuid, suffix);
278                     if (!Glib::file_test(pic_filename, Glib::FILE_TEST_IS_REGULAR)) {
279                         write_png_to_file(pic_filename, *data);
280                     }
281                 }
282                 catch (const std::exception &e) {
283                     Logger::log_warning("error saving picture " + (std::string)it.second.data_uuid,
284                                         Logger::Domain::PICTURE, e.what());
285                 }
286                 catch (...) {
287                     Logger::log_warning("error saving picture " + (std::string)it.second.data_uuid,
288                                         Logger::Domain::PICTURE);
289                 }
290             }
291         }
292     }
293     for (const auto &it : pictures_to_delete) {
294         const auto filename = build_pic_filename(pic_dir, it, suffix);
295         try {
296             Gio::File::create_for_path(filename)->remove();
297         }
298         catch (const std::exception &e) {
299             Logger::log_warning("error deleting picture " + filename, Logger::Domain::PICTURE, e.what());
300         }
301         catch (const Gio::Error &e) {
302             Logger::log_warning("error deleting picture " + filename, Logger::Domain::PICTURE, e.what());
303         }
304         catch (...) {
305             Logger::log_warning("error deleting picture " + filename, Logger::Domain::PICTURE);
306         }
307     }
308 }
309 
save(const std::map<UUID,Picture> & pics)310 void PictureKeeper::save(const std::map<UUID, Picture> &pics)
311 {
312     for (const auto &[uu, it] : pics) {
313         if (it.data)
314             pictures.emplace(it.data->uuid, it.data);
315     }
316 }
317 
restore(std::map<UUID,Picture> & pics)318 void PictureKeeper::restore(std::map<UUID, Picture> &pics)
319 {
320     for (auto &[uu, it] : pics) {
321         if (pictures.count(it.data_uuid))
322             it.data = pictures.at(it.data_uuid);
323     }
324 }
325 
326 } // namespace horizon
327