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