1 /*
2 Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
3 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY.
11
12 See the COPYING file for more details.
13 */
14
15 /**
16 * @file
17 * Routines for images: load, scale, re-color, etc.
18 */
19
20 #define GETTEXT_DOMAIN "wesnoth-lib"
21
22 #include "picture.hpp"
23
24 #include "config.hpp"
25 #include "display.hpp"
26 #include "filesystem.hpp"
27 #include "game_config.hpp"
28 #include "gettext.hpp"
29 #include "image_modifications.hpp"
30 #include "log.hpp"
31 #include "preferences/general.hpp"
32 #include "serialization/base64.hpp"
33 #include "serialization/string_utils.hpp"
34 #include "sdl/rect.hpp"
35 #include "utils/general.hpp"
36
37 #include <SDL2/SDL_image.h>
38
39 #include "utils/functional.hpp"
40
41 #include <boost/algorithm/string.hpp>
42 #include <boost/functional/hash_fwd.hpp>
43
44 #include <set>
45
46 static lg::log_domain log_display("display");
47 #define ERR_DP LOG_STREAM(err, log_display)
48 #define LOG_DP LOG_STREAM(info, log_display)
49
50 static lg::log_domain log_config("config");
51 #define ERR_CFG LOG_STREAM(err, log_config)
52
53 using game_config::tile_size;
54
55 template<typename T>
56 struct cache_item
57 {
cache_itemcache_item58 cache_item()
59 : item()
60 , loaded(false)
61 {
62 }
63
cache_itemcache_item64 cache_item(const T& item)
65 : item(item)
66 , loaded(true)
67 {
68 }
69
70 T item;
71 bool loaded;
72 };
73
74 namespace std
75 {
76 template<>
77 struct hash<image::locator::value>
78 {
operator ()std::hash79 size_t operator()(const image::locator::value& val) const
80 {
81 size_t hash = std::hash<unsigned>{}(val.type_);
82
83 if(val.type_ == image::locator::FILE || val.type_ == image::locator::SUB_FILE) {
84 boost::hash_combine(hash, val.filename_);
85 }
86
87 if(val.type_ == image::locator::SUB_FILE) {
88 boost::hash_combine(hash, val.loc_.x);
89 boost::hash_combine(hash, val.loc_.y);
90 boost::hash_combine(hash, val.center_x_);
91 boost::hash_combine(hash, val.center_y_);
92 boost::hash_combine(hash, val.modifications_);
93 }
94
95 return hash;
96 }
97 };
98 }
99
100 namespace image
101 {
102 template<typename T>
103 class cache_type
104 {
105 public:
cache_type()106 cache_type()
107 : content_()
108 {
109 }
110
get_element(int index)111 cache_item<T>& get_element(int index)
112 {
113 if(static_cast<unsigned>(index) >= content_.size())
114 content_.resize(index + 1);
115 return content_[index];
116 }
117
flush()118 void flush()
119 {
120 content_.clear();
121 }
122
123 private:
124 std::vector<cache_item<T>> content_;
125 };
126
127 template<typename T>
in_cache(cache_type<T> & cache) const128 bool locator::in_cache(cache_type<T>& cache) const
129 {
130 return index_ < 0 ? false : cache.get_element(index_).loaded;
131 }
132
133 template<typename T>
locate_in_cache(cache_type<T> & cache) const134 const T& locator::locate_in_cache(cache_type<T>& cache) const
135 {
136 static T dummy;
137 return index_ < 0 ? dummy : cache.get_element(index_).item;
138 }
139
140 template<typename T>
access_in_cache(cache_type<T> & cache) const141 T& locator::access_in_cache(cache_type<T>& cache) const
142 {
143 static T dummy;
144 return index_ < 0 ? dummy : cache.get_element(index_).item;
145 }
146
147 template<typename T>
add_to_cache(cache_type<T> & cache,const T & data) const148 void locator::add_to_cache(cache_type<T>& cache, const T& data) const
149 {
150 if(index_ >= 0) {
151 cache.get_element(index_) = cache_item<T>(data);
152 }
153 }
154 }
155
156 namespace
157 {
158 image::locator::locator_finder_t locator_finder;
159
160 /** Definition of all image maps */
161 image::image_cache images_, scaled_to_zoom_, hexed_images_, scaled_to_hex_images_, tod_colored_images_,
162 brightened_images_;
163
164 // cache storing if each image fit in a hex
165 image::bool_cache in_hex_info_;
166
167 // cache storing if this is an empty hex
168 image::bool_cache is_empty_hex_;
169
170 // caches storing the different lighted cases for each image
171 image::lit_cache lit_images_, lit_scaled_images_;
172 // caches storing each lightmap generated
173 image::lit_variants lightmaps_;
174
175 // const int cache_version_ = 0;
176
177 std::map<std::string, bool> image_existence_map;
178
179 // directories where we already cached file existence
180 std::set<std::string> precached_dirs;
181
182 std::map<surface, surface> reversed_images_;
183
184 int red_adjust = 0, green_adjust = 0, blue_adjust = 0;
185
186 /** List of colors used by the TC image modification */
187 std::vector<std::string> team_colors;
188
189 unsigned int zoom = tile_size;
190 unsigned int cached_zoom = 0;
191
192 /** Algorithm choices */
193 // typedef std::function<surface(const surface &, int, int)> scaling_function;
194 typedef surface (*scaling_function)(const surface&, int, int);
195 scaling_function scale_to_zoom_func;
196 scaling_function scale_to_hex_func;
197
198 const std::string data_uri_prefix = "data:";
199 struct parsed_data_URI{
200 explicit parsed_data_URI(utils::string_view data_URI);
201 utils::string_view scheme;
202 utils::string_view mime;
203 utils::string_view base64;
204 utils::string_view data;
205 bool good;
206 };
parsed_data_URI(utils::string_view data_URI)207 parsed_data_URI::parsed_data_URI(utils::string_view data_URI)
208 {
209 const size_t colon = data_URI.find(':');
210 const utils::string_view after_scheme = data_URI.substr(colon + 1);
211
212 const size_t comma = after_scheme.find(',');
213 const utils::string_view type_info = after_scheme.substr(0, comma);
214
215 const size_t semicolon = type_info.find(';');
216
217 scheme = data_URI.substr(0, colon);
218 base64 = type_info.substr(semicolon + 1);
219 mime = type_info.substr(0, semicolon);
220 data = after_scheme.substr(comma + 1);
221 good = (scheme == "data" && base64 == "base64" && mime.length() > 0 && data.length() > 0);
222 }
223
224 } // end anon namespace
225
226 namespace image
227 {
228 mini_terrain_cache_map mini_terrain_cache;
229 mini_terrain_cache_map mini_fogged_terrain_cache;
230 mini_terrain_cache_map mini_highlighted_terrain_cache;
231
232 static int last_index_ = 0;
233
flush_cache()234 void flush_cache()
235 {
236 {
237 images_.flush();
238 hexed_images_.flush();
239 tod_colored_images_.flush();
240 scaled_to_zoom_.flush();
241 scaled_to_hex_images_.flush();
242 brightened_images_.flush();
243 lit_images_.flush();
244 lit_scaled_images_.flush();
245 in_hex_info_.flush();
246 is_empty_hex_.flush();
247 mini_terrain_cache.clear();
248 mini_fogged_terrain_cache.clear();
249 mini_highlighted_terrain_cache.clear();
250 reversed_images_.clear();
251 image_existence_map.clear();
252 precached_dirs.clear();
253 }
254 /* We can't reset last_index_, since some locators are still alive
255 when using :refresh. That would cause them to point to the wrong
256 images. Not resetting the variable causes a memory leak, though. */
257 // last_index_ = 0;
258 }
259
init_index()260 void locator::init_index()
261 {
262 auto i = locator_finder.find(val_);
263
264 if(i == locator_finder.end()) {
265 index_ = last_index_++;
266 locator_finder.emplace(val_, index_);
267 } else {
268 index_ = i->second;
269 }
270 }
271
parse_arguments()272 void locator::parse_arguments()
273 {
274 std::string& fn = val_.filename_;
275 if(fn.empty()) {
276 return;
277 }
278
279 if(boost::algorithm::starts_with(fn, data_uri_prefix)) {
280 parsed_data_URI parsed{fn};
281
282 if(!parsed.good) {
283 utils::string_view view{ fn };
284 utils::string_view stripped = view.substr(0, view.find(","));
285 ERR_DP << "Invalid data URI: " << stripped << std::endl;
286 }
287
288 val_.is_data_uri_ = true;
289 }
290
291 size_t markup_field = fn.find('~');
292
293 if(markup_field != std::string::npos) {
294 val_.type_ = SUB_FILE;
295 val_.modifications_ = fn.substr(markup_field, fn.size() - markup_field);
296 fn = fn.substr(0, markup_field);
297 }
298 }
299
locator()300 locator::locator()
301 : index_(-1)
302 , val_()
303 {
304 }
305
locator(const locator & a,const std::string & mods)306 locator::locator(const locator& a, const std::string& mods)
307 : index_(-1)
308 , val_(a.val_)
309 {
310 if(!mods.empty()) {
311 val_.modifications_ += mods;
312 val_.type_ = SUB_FILE;
313 init_index();
314 } else {
315 index_ = a.index_;
316 }
317 }
318
locator(const char * filename)319 locator::locator(const char* filename)
320 : index_(-1)
321 , val_(filename)
322 {
323 parse_arguments();
324 init_index();
325 }
326
locator(const std::string & filename)327 locator::locator(const std::string& filename)
328 : index_(-1)
329 , val_(filename)
330 {
331 parse_arguments();
332 init_index();
333 }
334
locator(const std::string & filename,const std::string & modifications)335 locator::locator(const std::string& filename, const std::string& modifications)
336 : index_(-1)
337 , val_(filename, modifications)
338 {
339 init_index();
340 }
341
locator(const std::string & filename,const map_location & loc,int center_x,int center_y,const std::string & modifications)342 locator::locator(const std::string& filename,
343 const map_location& loc,
344 int center_x,
345 int center_y,
346 const std::string& modifications)
347 : index_(-1)
348 , val_(filename, loc, center_x, center_y, modifications)
349 {
350 init_index();
351 }
352
operator =(const locator & a)353 locator& locator::operator=(const locator& a)
354 {
355 index_ = a.index_;
356 val_ = a.val_;
357
358 return *this;
359 }
360
value()361 locator::value::value()
362 : type_(NONE)
363 , is_data_uri_(false)
364 , filename_()
365 , loc_()
366 , modifications_()
367 , center_x_(0)
368 , center_y_(0)
369 {
370 }
371
value(const char * filename)372 locator::value::value(const char* filename)
373 : type_(FILE)
374 , is_data_uri_(false)
375 , filename_(filename)
376 , loc_()
377 , modifications_()
378 , center_x_(0)
379 , center_y_(0)
380 {
381 }
382
value(const std::string & filename)383 locator::value::value(const std::string& filename)
384 : type_(FILE)
385 , is_data_uri_(false)
386 , filename_(filename)
387 , loc_()
388 , modifications_()
389 , center_x_(0)
390 , center_y_(0)
391 {
392 }
393
value(const std::string & filename,const std::string & modifications)394 locator::value::value(const std::string& filename, const std::string& modifications)
395 : type_(SUB_FILE)
396 , is_data_uri_(false)
397 , filename_(filename)
398 , loc_()
399 , modifications_(modifications)
400 , center_x_(0)
401 , center_y_(0)
402 {
403 }
404
value(const std::string & filename,const map_location & loc,int center_x,int center_y,const std::string & modifications)405 locator::value::value(const std::string& filename,
406 const map_location& loc,
407 int center_x,
408 int center_y,
409 const std::string& modifications)
410 : type_(SUB_FILE)
411 , is_data_uri_(false)
412 , filename_(filename)
413 , loc_(loc)
414 , modifications_(modifications)
415 , center_x_(center_x)
416 , center_y_(center_y)
417 {
418 }
419
operator ==(const value & a) const420 bool locator::value::operator==(const value& a) const
421 {
422 if(a.type_ != type_) {
423 return false;
424 } else if(type_ == FILE) {
425 return filename_ == a.filename_;
426 } else if(type_ == SUB_FILE) {
427 return filename_ == a.filename_ && loc_ == a.loc_ && modifications_ == a.modifications_
428 && center_x_ == a.center_x_ && center_y_ == a.center_y_;
429 }
430
431 return false;
432 }
433
operator <(const value & a) const434 bool locator::value::operator<(const value& a) const
435 {
436 if(type_ != a.type_) {
437 return type_ < a.type_;
438 } else if(type_ == FILE) {
439 return filename_ < a.filename_;
440 } else if(type_ == SUB_FILE) {
441 if(filename_ != a.filename_)
442 return filename_ < a.filename_;
443 if(loc_ != a.loc_)
444 return loc_ < a.loc_;
445 if(center_x_ != a.center_x_)
446 return center_x_ < a.center_x_;
447 if(center_y_ != a.center_y_)
448 return center_y_ < a.center_y_;
449 return (modifications_ < a.modifications_);
450 }
451
452 return false;
453 }
454
455 // Check if localized file is up-to-date according to l10n track index.
456 // Make sure only that the image is not explicitly recorded as fuzzy,
457 // in order to be able to use non-tracked images (e.g. from UMC).
458 static std::set<std::string> fuzzy_localized_files;
localized_file_uptodate(const std::string & loc_file)459 static bool localized_file_uptodate(const std::string& loc_file)
460 {
461 if(fuzzy_localized_files.empty()) {
462 // First call, parse track index to collect fuzzy files by path.
463 std::string fsep = "\xC2\xA6"; // UTF-8 for "broken bar"
464 // Issue #4716 is that passing an empty string as the first argument of get_binary_file_location
465 // causes that function to find the file in both "wesnoth_dir//l10n-track" and "wesnoth_dir/l10n-track",
466 // triggering the warning about conflicting files with the same name.
467 std::string trackpath = filesystem::get_binary_file_location("workaround_for_issue_4716", "l10n-track");
468
469 // l10n-track file not present. Assume image is up-to-date.
470 if(trackpath.empty()) {
471 return true;
472 }
473
474 std::string contents = filesystem::read_file(trackpath);
475
476 for(const std::string& line : utils::split(contents, '\n')) {
477 size_t p1 = line.find(fsep);
478 if(p1 == std::string::npos) {
479 continue;
480 }
481
482 std::string state = line.substr(0, p1);
483 boost::trim(state);
484 if(state == "fuzzy") {
485 size_t p2 = line.find(fsep, p1 + fsep.length());
486 if(p2 == std::string::npos) {
487 continue;
488 }
489
490 std::string relpath = line.substr(p1 + fsep.length(), p2 - p1 - fsep.length());
491 fuzzy_localized_files.insert(game_config::path + '/' + relpath);
492 }
493 }
494
495 fuzzy_localized_files.insert(""); // make sure not empty any more
496 }
497
498 return fuzzy_localized_files.count(loc_file) == 0;
499 }
500
501 // Return path to localized counterpart of the given file, if any, or empty string.
502 // Localized counterpart may also be requested to have a suffix to base name.
get_localized_path(const std::string & file,const std::string & suff="")503 static std::string get_localized_path(const std::string& file, const std::string& suff = "")
504 {
505 std::string dir = filesystem::directory_name(file);
506 std::string base = filesystem::base_name(file);
507
508 const size_t pos_ext = base.rfind(".");
509
510 std::string loc_base;
511 if(pos_ext != std::string::npos) {
512 loc_base = base.substr(0, pos_ext) + suff + base.substr(pos_ext);
513 } else {
514 loc_base = base + suff;
515 }
516
517 // TRANSLATORS: This is the language code which will be used
518 // to store and fetch localized non-textual resources, such as images,
519 // when they exist. Normally it is just the code of the PO file itself,
520 // e.g. "de" of de.po for German. But it can also be a comma-separated
521 // list of language codes by priority, when the localized resource
522 // found for first of those languages will be used. This is useful when
523 // two languages share sufficient commonality, that they can use each
524 // other's resources rather than duplicating them. For example,
525 // Swedish (sv) and Danish (da) are such, so Swedish translator could
526 // translate this message as "sv,da", while Danish as "da,sv".
527 std::vector<std::string> langs = utils::split(_("language code for localized resources^en_US"));
528
529 // In case even the original image is split into base and overlay,
530 // add en_US with lowest priority, since the message above will
531 // not have it when translated.
532 langs.push_back("en_US");
533 for(const std::string& lang : langs) {
534 std::string loc_file = dir + "/" + "l10n" + "/" + lang + "/" + loc_base;
535 if(filesystem::file_exists(loc_file) && localized_file_uptodate(loc_file)) {
536 return loc_file;
537 }
538 }
539
540 return "";
541 }
542
543 // Load overlay image and compose it with the original surface.
add_localized_overlay(const std::string & ovr_file,surface & orig_surf)544 static void add_localized_overlay(const std::string& ovr_file, surface& orig_surf)
545 {
546 filesystem::rwops_ptr rwops = filesystem::make_read_RWops(ovr_file);
547 surface ovr_surf = IMG_Load_RW(rwops.release(), true); // SDL takes ownership of rwops
548 if(!ovr_surf) {
549 return;
550 }
551
552 SDL_Rect area {0, 0, ovr_surf->w, ovr_surf->h};
553
554 sdl_blit(ovr_surf, 0, orig_surf, &area);
555 }
556
load_image_file(const image::locator & loc)557 static surface load_image_file(const image::locator& loc)
558 {
559 surface res;
560
561 std::string location = filesystem::get_binary_file_location("images", loc.get_filename());
562
563 {
564 if(!location.empty()) {
565 // Check if there is a localized image.
566 const std::string loc_location = get_localized_path(location);
567 if(!loc_location.empty()) {
568 location = loc_location;
569 }
570
571 filesystem::rwops_ptr rwops = filesystem::make_read_RWops(location);
572 res = IMG_Load_RW(rwops.release(), true); // SDL takes ownership of rwops
573
574 // If there was no standalone localized image, check if there is an overlay.
575 if(res && loc_location.empty()) {
576 const std::string ovr_location = get_localized_path(location, "--overlay");
577 if(!ovr_location.empty()) {
578 add_localized_overlay(ovr_location, res);
579 }
580 }
581 }
582 }
583
584 if(!res && !loc.get_filename().empty()) {
585 ERR_DP << "could not open image '" << loc.get_filename() << "'" << std::endl;
586 if(game_config::debug && loc.get_filename() != game_config::images::missing)
587 return get_image(game_config::images::missing, UNSCALED);
588 }
589
590 return res;
591 }
592
load_image_sub_file(const image::locator & loc)593 static surface load_image_sub_file(const image::locator& loc)
594 {
595 surface surf = get_image(loc.get_filename(), UNSCALED);
596 if(surf == nullptr) {
597 return nullptr;
598 }
599
600 modification_queue mods = modification::decode(loc.get_modifications());
601
602 while(!mods.empty()) {
603 modification* mod = mods.top();
604
605 try {
606 surf = (*mod)(surf);
607 } catch(const image::modification::imod_exception& e) {
608 ERR_CFG << "Failed to apply a modification to an image:\n"
609 << "Image: " << loc.get_filename() << ".\n"
610 << "Modifications: " << loc.get_modifications() << ".\n"
611 << "Error: " << e.message;
612 }
613
614 // NOTE: do this *after* applying the mod or you'll get crashes!
615 mods.pop();
616 }
617
618 if(loc.get_loc().valid()) {
619 SDL_Rect srcrect = sdl::create_rect(
620 ((tile_size * 3) / 4) * loc.get_loc().x,
621 tile_size * loc.get_loc().y + (tile_size / 2) * (loc.get_loc().x % 2),
622 tile_size,
623 tile_size
624 );
625
626 if(loc.get_center_x() >= 0 && loc.get_center_y() >= 0) {
627 srcrect.x += surf->w / 2 - loc.get_center_x();
628 srcrect.y += surf->h / 2 - loc.get_center_y();
629 }
630
631 // cut and hex mask, but also check and cache if empty result
632 surface cut(cut_surface(surf, srcrect));
633 bool is_empty = false;
634 surf = mask_surface(cut, get_hexmask(), &is_empty);
635
636 // discard empty images to free memory
637 if(is_empty) {
638 // Safe because those images are only used by terrain rendering
639 // and it filters them out.
640 // A safer and more general way would be to keep only one copy of it
641 surf = nullptr;
642 }
643
644 loc.add_to_cache(is_empty_hex_, is_empty);
645 }
646
647 return surf;
648 }
649
load_image_data_uri(const image::locator & loc)650 static surface load_image_data_uri(const image::locator& loc)
651 {
652 surface surf;
653
654 parsed_data_URI parsed{loc.get_filename()};
655
656 if(!parsed.good) {
657 utils::string_view fn = loc.get_filename();
658 utils::string_view stripped = fn.substr(0, fn.find(","));
659 ERR_DP << "Invalid data URI: " << stripped << std::endl;
660 } else if(parsed.mime.substr(0, 5) != "image") {
661 ERR_DP << "Data URI not of image MIME type: " << parsed.mime << std::endl;
662 } else {
663 const std::vector<uint8_t> image_data = base64::decode(parsed.data);
664 filesystem::rwops_ptr rwops{SDL_RWFromConstMem(image_data.data(), image_data.size()), &SDL_FreeRW};
665
666 if(image_data.empty()) {
667 ERR_DP << "Invalid encoding in data URI" << std::endl;
668 } else if(parsed.mime == "image/png") {
669 surf = IMG_LoadTyped_RW(rwops.release(), true, "PNG");
670 } else if(parsed.mime == "image/jpeg") {
671 surf = IMG_LoadTyped_RW(rwops.release(), true, "JPG");
672 } else {
673 ERR_DP << "Invalid image MIME type: " << parsed.mime << std::endl;
674 }
675 }
676
677 return surf;
678 }
679
680 // small utility function to store an int from (-256,254) to an signed char
col_to_uchar(int i)681 static signed char col_to_uchar(int i)
682 {
683 return static_cast<signed char>(std::min<int>(127, std::max<int>(-128, i / 2)));
684 }
685
get_light_string(int op,int r,int g,int b)686 light_string get_light_string(int op, int r, int g, int b)
687 {
688 light_string ls;
689 ls.reserve(4);
690 ls.push_back(op);
691 ls.push_back(col_to_uchar(r));
692 ls.push_back(col_to_uchar(g));
693 ls.push_back(col_to_uchar(b));
694
695 return ls;
696 }
697
apply_light(surface surf,const light_string & ls)698 static surface apply_light(surface surf, const light_string& ls)
699 {
700 // atomic lightmap operation are handled directly (important to end recursion)
701 if(ls.size() == 4) {
702 // if no lightmap (first char = -1) then we need the initial value
703 //(before the halving done for lightmap)
704 int m = ls[0] == -1 ? 2 : 1;
705 return adjust_surface_color(surf, ls[1] * m, ls[2] * m, ls[3] * m);
706 }
707
708 // check if the lightmap is already cached or need to be generated
709 surface lightmap = nullptr;
710 auto i = lightmaps_.find(ls);
711 if(i != lightmaps_.end()) {
712 lightmap = i->second;
713 } else {
714 // build all the paths for lightmap sources
715 static const std::string p = "terrain/light/light";
716 static const std::string lm_img[19] {
717 p + ".png",
718 p + "-concave-2-tr.png", p + "-concave-2-r.png", p + "-concave-2-br.png",
719 p + "-concave-2-bl.png", p + "-concave-2-l.png", p + "-concave-2-tl.png",
720 p + "-convex-br-bl.png", p + "-convex-bl-l.png", p + "-convex-l-tl.png",
721 p + "-convex-tl-tr.png", p + "-convex-tr-r.png", p + "-convex-r-br.png",
722 p + "-convex-l-bl.png", p + "-convex-tl-l.png", p + "-convex-tr-tl.png",
723 p + "-convex-r-tr.png", p + "-convex-br-r.png", p + "-convex-bl-br.png"
724 };
725
726 // decompose into atomic lightmap operations (4 chars)
727 for(size_t c = 0; c + 3 < ls.size(); c += 4) {
728 light_string sls = ls.substr(c, 4);
729
730 // get the corresponding image and apply the lightmap operation to it
731 // This allows to also cache lightmap parts.
732 // note that we avoid infinite recursion by using only atomic operation
733 surface lts = image::get_lighted_image(lm_img[sls[0]], sls, HEXED);
734
735 // first image will be the base where we blit the others
736 if(lightmap == nullptr) {
737 // copy the cached image to avoid modifying the cache
738 lightmap = lts.clone();
739 } else {
740 sdl_blit(lts, nullptr, lightmap, nullptr);
741 }
742 }
743
744 // cache the result
745 lightmaps_[ls] = lightmap;
746 }
747
748 // apply the final lightmap
749 return light_surface(surf, lightmap);
750 }
751
file_exists() const752 bool locator::file_exists() const
753 {
754 return val_.is_data_uri_
755 ? parsed_data_URI{val_.filename_}.good
756 : !filesystem::get_binary_file_location("images", val_.filename_).empty();
757 }
758
load_from_disk(const locator & loc)759 surface load_from_disk(const locator& loc)
760 {
761 switch(loc.get_type()) {
762 case locator::FILE:
763 if(loc.is_data_uri()){
764 return load_image_data_uri(loc);
765 } else {
766 return load_image_file(loc);
767 }
768 case locator::SUB_FILE:
769 return load_image_sub_file(loc);
770 default:
771 return surface(nullptr);
772 }
773 }
774
manager()775 manager::manager()
776 {
777 }
778
~manager()779 manager::~manager()
780 {
781 flush_cache();
782 }
783
set_color_adjustment(int r,int g,int b)784 void set_color_adjustment(int r, int g, int b)
785 {
786 if(r != red_adjust || g != green_adjust || b != blue_adjust) {
787 red_adjust = r;
788 green_adjust = g;
789 blue_adjust = b;
790 tod_colored_images_.flush();
791 brightened_images_.flush();
792 lit_images_.flush();
793 lit_scaled_images_.flush();
794 reversed_images_.clear();
795 }
796 }
797
set_team_colors(const std::vector<std::string> * colors)798 void set_team_colors(const std::vector<std::string>* colors)
799 {
800 if(colors == nullptr) {
801 team_colors.clear();
802 } else {
803 team_colors = *colors;
804 }
805 }
806
get_team_colors()807 const std::vector<std::string>& get_team_colors()
808 {
809 return team_colors;
810 }
811
set_zoom(unsigned int amount)812 void set_zoom(unsigned int amount)
813 {
814 if(amount != zoom) {
815 zoom = amount;
816 tod_colored_images_.flush();
817 brightened_images_.flush();
818 reversed_images_.clear();
819
820 // We keep these caches if:
821 // we use default zoom (it doesn't need those)
822 // or if they are already at the wanted zoom.
823 if(zoom != tile_size && zoom != cached_zoom) {
824 scaled_to_zoom_.flush();
825 scaled_to_hex_images_.flush();
826 lit_scaled_images_.flush();
827 cached_zoom = zoom;
828 }
829 }
830 }
831
832 // F should be a scaling algorithm without "integral" zoom limitations
833 template<scaling_function F>
scale_xbrz_helper(const surface & res,int w,int h)834 static surface scale_xbrz_helper(const surface& res, int w, int h)
835 {
836 int best_integer_zoom = std::min(w / res->w, h / res->h);
837 int legal_zoom = utils::clamp(best_integer_zoom, 1, 5);
838 return F(scale_surface_xbrz(res, legal_zoom), w, h);
839 }
840
841 using SCALING_ALGORITHM = preferences::SCALING_ALGORITHM;
842
select_algorithm(SCALING_ALGORITHM algo)843 static scaling_function select_algorithm(SCALING_ALGORITHM algo)
844 {
845 switch(algo.v) {
846 case SCALING_ALGORITHM::LINEAR: {
847 scaling_function result = &scale_surface;
848 return result;
849 }
850 case SCALING_ALGORITHM::NEAREST_NEIGHBOR: {
851 scaling_function result = &scale_surface_nn;
852 return result;
853 }
854 case SCALING_ALGORITHM::XBRZ_LIN: {
855 scaling_function result = &scale_xbrz_helper<scale_surface>;
856 return result;
857 }
858 case SCALING_ALGORITHM::XBRZ_NN: {
859 scaling_function result = &scale_xbrz_helper<scale_surface_nn>;
860 return result;
861 }
862 default:
863 assert(false && "I don't know how to implement this scaling algorithm");
864 throw 42;
865 }
866 }
867
get_hexed(const locator & i_locator)868 static surface get_hexed(const locator& i_locator)
869 {
870 surface image(get_image(i_locator, UNSCALED));
871 // hex cut tiles, also check and cache if empty result
872 bool is_empty = false;
873 surface res = mask_surface(image, get_hexmask(), &is_empty, i_locator.get_filename());
874 i_locator.add_to_cache(is_empty_hex_, is_empty);
875 return res;
876 }
877
get_scaled_to_hex(const locator & i_locator)878 static surface get_scaled_to_hex(const locator& i_locator)
879 {
880 surface img = get_image(i_locator, HEXED);
881 // return scale_surface(img, zoom, zoom);
882
883 if(img) {
884 return scale_to_hex_func(img, zoom, zoom);
885 }
886
887 return surface(nullptr);
888
889 }
890
get_tod_colored(const locator & i_locator)891 static surface get_tod_colored(const locator& i_locator)
892 {
893 surface img = get_image(i_locator, SCALED_TO_HEX);
894 return adjust_surface_color(img, red_adjust, green_adjust, blue_adjust);
895 }
896
get_scaled_to_zoom(const locator & i_locator)897 static surface get_scaled_to_zoom(const locator& i_locator)
898 {
899 assert(zoom != tile_size);
900 assert(tile_size != 0);
901
902 surface res(get_image(i_locator, UNSCALED));
903 // For some reason haloes seems to have invalid images, protect against crashing
904 if(res) {
905 return scale_to_zoom_func(res, ((res->w * zoom) / tile_size), ((res->h * zoom) / tile_size));
906 }
907
908 return surface(nullptr);
909 }
910
get_brightened(const locator & i_locator)911 static surface get_brightened(const locator& i_locator)
912 {
913 surface image(get_image(i_locator, TOD_COLORED));
914 return brighten_image(image, ftofxp(game_config::hex_brightening));
915 }
916
917 /// translate type to a simpler one when possible
simplify_type(const image::locator & i_locator,TYPE type)918 static TYPE simplify_type(const image::locator& i_locator, TYPE type)
919 {
920 switch(type) {
921 case SCALED_TO_ZOOM:
922 if(zoom == tile_size) {
923 type = UNSCALED;
924 }
925
926 break;
927 case BRIGHTENED:
928 if(ftofxp(game_config::hex_brightening) == ftofxp(1.0)) {
929 type = TOD_COLORED;
930 }
931
932 break;
933 default:
934 break;
935 }
936
937 if(type == TOD_COLORED) {
938 if(red_adjust == 0 && green_adjust == 0 && blue_adjust == 0) {
939 type = SCALED_TO_HEX;
940 }
941 }
942
943 if(type == SCALED_TO_HEX) {
944 if(zoom == tile_size) {
945 type = HEXED;
946 }
947 }
948
949 if(type == HEXED) {
950 // check if the image is already hex-cut by the location system
951 if(i_locator.get_loc().valid()) {
952 type = UNSCALED;
953 }
954 }
955
956 return type;
957 }
958
get_image(const image::locator & i_locator,TYPE type)959 surface get_image(const image::locator& i_locator, TYPE type)
960 {
961 surface res;
962
963 if(i_locator.is_void()) {
964 return res;
965 }
966
967 type = simplify_type(i_locator, type);
968
969 image_cache* imap;
970 // select associated cache
971 switch(type) {
972 case UNSCALED:
973 imap = &images_;
974 break;
975 case TOD_COLORED:
976 imap = &tod_colored_images_;
977 break;
978 case SCALED_TO_ZOOM:
979 imap = &scaled_to_zoom_;
980 break;
981 case HEXED:
982 imap = &hexed_images_;
983 break;
984 case SCALED_TO_HEX:
985 imap = &scaled_to_hex_images_;
986 break;
987 case BRIGHTENED:
988 imap = &brightened_images_;
989 break;
990 default:
991 return res;
992 }
993
994 // return the image if already cached
995 bool tmp;
996 tmp = i_locator.in_cache(*imap);
997
998 if(tmp) {
999 surface result;
1000 result = i_locator.locate_in_cache(*imap);
1001 return result;
1002 }
1003
1004 // not cached, generate it
1005 switch(type) {
1006 case UNSCALED:
1007 // If type is unscaled, directly load the image from the disk.
1008 res = load_from_disk(i_locator);
1009 break;
1010 case TOD_COLORED:
1011 res = get_tod_colored(i_locator);
1012 break;
1013 case SCALED_TO_ZOOM:
1014 res = get_scaled_to_zoom(i_locator);
1015 break;
1016 case HEXED:
1017 res = get_hexed(i_locator);
1018 break;
1019 case SCALED_TO_HEX:
1020 res = get_scaled_to_hex(i_locator);
1021 break;
1022 case BRIGHTENED:
1023 res = get_brightened(i_locator);
1024 break;
1025 default:
1026 return res;
1027 }
1028
1029 i_locator.add_to_cache(*imap, res);
1030
1031 return res;
1032 }
1033
get_lighted_image(const image::locator & i_locator,const light_string & ls,TYPE type)1034 surface get_lighted_image(const image::locator& i_locator, const light_string& ls, TYPE type)
1035 {
1036 surface res;
1037 if(i_locator.is_void()) {
1038 return res;
1039 }
1040
1041 if(type == SCALED_TO_HEX && zoom == tile_size) {
1042 type = HEXED;
1043 }
1044
1045 // select associated cache
1046 lit_cache* imap = &lit_images_;
1047 if(type == SCALED_TO_HEX) {
1048 imap = &lit_scaled_images_;
1049 }
1050
1051 // if no light variants yet, need to add an empty map
1052 if(!i_locator.in_cache(*imap)) {
1053 i_locator.add_to_cache(*imap, lit_variants());
1054 }
1055
1056 // need access to add it if not found
1057 { // enclose reference pointing to data stored in a changing vector
1058 const lit_variants& lvar = i_locator.locate_in_cache(*imap);
1059 auto lvi = lvar.find(ls);
1060 if(lvi != lvar.end()) {
1061 return lvi->second;
1062 }
1063 }
1064
1065 // not cached yet, generate it
1066 switch(type) {
1067 case HEXED:
1068 res = get_image(i_locator, HEXED);
1069 res = apply_light(res, ls);
1070 break;
1071 case SCALED_TO_HEX:
1072 // we light before scaling to reuse the unscaled cache
1073 res = get_lighted_image(i_locator, ls, HEXED);
1074 res = scale_surface(res, zoom, zoom);
1075 break;
1076 default:
1077 break;
1078 }
1079
1080 // record the lighted surface in the corresponding variants cache
1081 i_locator.access_in_cache(*imap)[ls] = res;
1082
1083 return res;
1084 }
1085
get_hexmask()1086 surface get_hexmask()
1087 {
1088 static const image::locator terrain_mask(game_config::images::terrain_mask);
1089 return get_image(terrain_mask, UNSCALED);
1090 }
1091
is_in_hex(const locator & i_locator)1092 bool is_in_hex(const locator& i_locator)
1093 {
1094 bool result;
1095 {
1096 if(i_locator.in_cache(in_hex_info_)) {
1097 result = i_locator.locate_in_cache(in_hex_info_);
1098 } else {
1099 const surface image(get_image(i_locator, UNSCALED));
1100
1101 bool res = in_mask_surface(image, get_hexmask());
1102
1103 i_locator.add_to_cache(in_hex_info_, res);
1104
1105 // std::cout << "in_hex : " << i_locator.get_filename()
1106 // << " " << (res ? "yes" : "no") << "\n";
1107
1108 result = res;
1109 }
1110 }
1111
1112 return result;
1113 }
1114
is_empty_hex(const locator & i_locator)1115 bool is_empty_hex(const locator& i_locator)
1116 {
1117 if(!i_locator.in_cache(is_empty_hex_)) {
1118 const surface surf = get_image(i_locator, HEXED);
1119 // emptiness of terrain image is checked during hex cut
1120 // so, maybe in cache now, let's recheck
1121 if(!i_locator.in_cache(is_empty_hex_)) {
1122 // should never reach here
1123 // but do it manually if it happens
1124 // assert(false);
1125 bool is_empty = false;
1126 mask_surface(surf, get_hexmask(), &is_empty);
1127 i_locator.add_to_cache(is_empty_hex_, is_empty);
1128 }
1129 }
1130
1131 return i_locator.locate_in_cache(is_empty_hex_);
1132 }
1133
reverse_image(const surface & surf)1134 surface reverse_image(const surface& surf)
1135 {
1136 if(surf == nullptr) {
1137 return surface(nullptr);
1138 }
1139
1140 const auto itor = reversed_images_.find(surf);
1141 if(itor != reversed_images_.end()) {
1142 // sdl_add_ref(itor->second);
1143 return itor->second;
1144 }
1145
1146 const surface rev(flip_surface(surf));
1147 if(rev == nullptr) {
1148 return surface(nullptr);
1149 }
1150
1151 reversed_images_.emplace(surf, rev);
1152 // sdl_add_ref(rev);
1153 return rev;
1154 }
1155
exists(const image::locator & i_locator)1156 bool exists(const image::locator& i_locator)
1157 {
1158 typedef image::locator loc;
1159 loc::type type = i_locator.get_type();
1160 if(type != loc::FILE && type != loc::SUB_FILE) {
1161 return false;
1162 }
1163
1164 // The insertion will fail if there is already an element in the cache
1165 // and this will point to the existing element.
1166 auto iter = image_existence_map.begin();
1167 bool success;
1168
1169 std::tie(iter, success) = image_existence_map.emplace(i_locator.get_filename(), false);
1170
1171 bool& cache = iter->second;
1172 if(success) {
1173 if(i_locator.is_data_uri()) {
1174 cache = parsed_data_URI{i_locator.get_filename()}.good;
1175 } else {
1176 cache = !filesystem::get_binary_file_location("images", i_locator.get_filename()).empty();
1177 }
1178 }
1179
1180 return cache;
1181 }
1182
precache_file_existence_internal(const std::string & dir,const std::string & subdir)1183 static void precache_file_existence_internal(const std::string& dir, const std::string& subdir)
1184 {
1185 const std::string checked_dir = dir + "/" + subdir;
1186 if(precached_dirs.find(checked_dir) != precached_dirs.end()) {
1187 return;
1188 }
1189
1190 precached_dirs.insert(checked_dir);
1191
1192 if(!filesystem::is_directory(checked_dir)) {
1193 return;
1194 }
1195
1196 std::vector<std::string> files_found;
1197 std::vector<std::string> dirs_found;
1198 filesystem::get_files_in_dir(checked_dir, &files_found, &dirs_found, filesystem::FILE_NAME_ONLY,
1199 filesystem::NO_FILTER, filesystem::DONT_REORDER);
1200
1201 for(const auto& f : files_found) {
1202 image_existence_map[subdir + f] = true;
1203 }
1204
1205 for(const auto& d : dirs_found) {
1206 precache_file_existence_internal(dir, subdir + d + "/");
1207 }
1208 }
1209
precache_file_existence(const std::string & subdir)1210 void precache_file_existence(const std::string& subdir)
1211 {
1212 const std::vector<std::string>& paths = filesystem::get_binary_paths("images");
1213
1214 for(const auto& p : paths) {
1215 precache_file_existence_internal(p, subdir);
1216 }
1217 }
1218
precached_file_exists(const std::string & file)1219 bool precached_file_exists(const std::string& file)
1220 {
1221 const auto b = image_existence_map.find(file);
1222 if(b != image_existence_map.end()) {
1223 return b->second;
1224 }
1225
1226 return false;
1227 }
1228
save_image(const locator & i_locator,const std::string & filename)1229 save_result save_image(const locator& i_locator, const std::string& filename)
1230 {
1231 return save_image(get_image(i_locator), filename);
1232 }
1233
save_image(const surface & surf,const std::string & filename)1234 save_result save_image(const surface& surf, const std::string& filename)
1235 {
1236 if(!surf) {
1237 return save_result::no_image;
1238 }
1239
1240 #ifdef SDL_IMAGE_VERSION_ATLEAST
1241 #if SDL_IMAGE_VERSION_ATLEAST(2, 0, 2)
1242 if(filesystem::ends_with(filename, ".jpeg") || filesystem::ends_with(filename, ".jpg") || filesystem::ends_with(filename, ".jpe")) {
1243 LOG_DP << "Writing a JPG image to " << filename << std::endl;
1244
1245 const int err = IMG_SaveJPG_RW(surf, filesystem::make_write_RWops(filename).release(), true, 75); // SDL takes ownership of the RWops
1246 return err == 0 ? save_result::success : save_result::save_failed;
1247 }
1248 #endif
1249 #endif
1250
1251 if(filesystem::ends_with(filename, ".png")) {
1252 LOG_DP << "Writing a PNG image to " << filename << std::endl;
1253
1254 const int err = IMG_SavePNG_RW(surf, filesystem::make_write_RWops(filename).release(), true); // SDL takes ownership of the RWops
1255 return err == 0 ? save_result::success : save_result::save_failed;
1256 }
1257
1258 if(filesystem::ends_with(filename, ".bmp")) {
1259 LOG_DP << "Writing a BMP image to " << filename << std::endl;
1260 const int err = SDL_SaveBMP(surf, filename.c_str()) == 0;
1261 return err == 0 ? save_result::success : save_result::save_failed;
1262 }
1263
1264 return save_result::unsupported_format;
1265 }
1266
update_from_preferences()1267 bool update_from_preferences()
1268 {
1269 SCALING_ALGORITHM algo = preferences::default_scaling_algorithm;
1270 try {
1271 algo = SCALING_ALGORITHM::string_to_enum(preferences::get("scale_hex"));
1272 } catch(const bad_enum_cast&) {
1273 }
1274
1275 scale_to_hex_func = select_algorithm(algo);
1276
1277 algo = preferences::default_scaling_algorithm;
1278 try {
1279 algo = SCALING_ALGORITHM::string_to_enum(preferences::get("scale_zoom"));
1280 } catch(const bad_enum_cast&) {
1281 }
1282
1283 scale_to_zoom_func = select_algorithm(algo);
1284
1285 return true;
1286 }
1287
1288 } // end namespace image
1289