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