The Battle for Wesnoth  1.19.18+dev
picture.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2025
3  by David White <dave@whitevine.net>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 /**
17  * @file
18  * Routines for images: load, scale, re-color, etc.
19  */
20 
21 #include "picture.hpp"
22 
23 #include "filesystem.hpp"
24 #include "game_config.hpp"
25 #include "image_modifications.hpp"
26 #include "log.hpp"
27 #include "serialization/base64.hpp"
29 #include "sdl/rect.hpp"
30 #include "sdl/texture.hpp"
31 
32 #include <SDL2/SDL_image.h>
33 
34 #include <boost/algorithm/string.hpp>
35 
36 #if BOOST_VERSION >= 108100
37 
38 #include <boost/unordered/unordered_flat_map.hpp>
39 template<typename Key, typename Value>
40 using cache_map = boost::unordered_flat_map<Key, Value>;
41 
42 #else
43 
44 #include <boost/unordered/unordered_map.hpp>
45 template<typename Key, typename Value>
46 using cache_map = boost::unordered_map<Key, Value>;
47 
48 #endif
49 
50 #include <array>
51 #include <set>
52 
53 static lg::log_domain log_image("image");
54 #define ERR_IMG LOG_STREAM(err, log_image)
55 #define WRN_IMG LOG_STREAM(warn, log_image)
56 #define LOG_IMG LOG_STREAM(info, log_image)
57 #define DBG_IMG LOG_STREAM(debug, log_image)
58 
59 static lg::log_domain log_config("config");
60 #define ERR_CFG LOG_STREAM(err, log_config)
61 
63 
64 namespace image
65 {
66 /** Hash function overload for boost::hash. Must be in the image namespace to satisfy ADL. */
67 std::size_t hash_value(const image::locator& val)
68 {
69  std::size_t hash = std::hash<unsigned>{}(val.type_);
70 
72  boost::hash_combine(hash, val.filename_);
73  }
74 
75  if(val.type_ == image::locator::SUB_FILE) {
76  boost::hash_combine(hash, val.loc_.x);
77  boost::hash_combine(hash, val.loc_.y);
78  boost::hash_combine(hash, val.center_x_);
79  boost::hash_combine(hash, val.center_y_);
80  boost::hash_combine(hash, val.modifications_);
81  }
82 
83  return hash;
84 }
85 
86 template<typename T>
88 {
89 public:
90  bool in_cache(const locator& item) const
91  {
92  return content_.find(item) != content_.end(); // TODO C++20: use content_.contains()
93  }
94 
95  /** Returns a pointer to the cached value, or nullptr if not found. */
96  const T* locate_in_cache(const locator& item) const
97  {
98  if(auto iter = content_.find(item); iter != content_.end()) {
99  return &iter->second;
100  } else {
101  return nullptr;
102  }
103  }
104 
105  /**
106  * Returns a reference to the cache item associated with the given key.
107  * If no corresponding value is found, a default instance will be created.
108  */
109  T& access_in_cache(const locator& item)
110  {
111  return content_[item];
112  }
113 
114  void add_to_cache(const locator& item, T data)
115  {
116  content_.insert_or_assign(item, std::move(data));
117  }
118 
119  void flush()
120  {
121  content_.clear();
122  }
123 
124 private:
126 };
127 
128 namespace
129 {
130 using surface_cache = cache_type<surface>;
131 using texture_cache = cache_type<texture>;
132 using bool_cache = cache_type<bool>;
133 
134 /** Type used to pair light possibilities with the corresponding lit surface. */
135 using lit_surface_variants = cache_map<std::vector<light_adjust>, surface>;
136 using lit_texture_variants = cache_map<std::vector<light_adjust>, texture>;
137 
138 /** Lit variants for each locator. */
139 using lit_surface_cache = cache_type<lit_surface_variants>;
140 using lit_texture_cache = cache_type<lit_texture_variants>;
141 
142 /** Definition of all image maps */
143 std::array<surface_cache, NUM_TYPES> surfaces_;
144 
145 /**
146  * Texture caches.
147  * Note that the latter two are temporary and should be removed once we have OGL and shader support.
148  */
149 using texture_cache_map = std::map<image::scale_quality, image::texture_cache>;
150 
151 texture_cache_map textures_;
152 texture_cache_map textures_hexed_;
153 texture_cache_map texture_tod_colored_;
154 
155 // cache storing if each image fit in a hex
156 image::bool_cache in_hex_info_;
157 
158 // cache storing if this is an empty hex
159 image::bool_cache is_empty_hex_;
160 
161 // caches storing the different lighted cases for each image
162 image::lit_surface_cache lit_surfaces_;
163 image::lit_texture_cache lit_textures_;
164 // caches storing each lightmap generated
165 image::lit_surface_variants surface_lightmaps_;
166 image::lit_texture_variants texture_lightmaps_;
167 
168 // diagnostics for tracking skipped cache impact
169 std::array<bool_cache, NUM_TYPES> skipped_cache_;
170 int duplicate_loads_ = 0;
171 int total_loads_ = 0;
172 
173 // const int cache_version_ = 0;
174 
175 std::map<std::string, bool> image_existence_map;
176 
177 // directories where we already cached file existence
178 std::set<std::string> precached_dirs;
179 
180 int red_adjust = 0, green_adjust = 0, blue_adjust = 0;
181 
182 const std::string data_uri_prefix = "data:";
183 struct parsed_data_URI{
184  explicit parsed_data_URI(std::string_view data_URI);
185  std::string_view scheme;
186  std::string_view mime;
187  std::string_view base64;
188  std::string_view data;
189  bool good;
190 };
191 parsed_data_URI::parsed_data_URI(std::string_view data_URI)
192 {
193  const std::size_t colon = data_URI.find(':');
194  const std::string_view after_scheme = data_URI.substr(colon + 1);
195 
196  const std::size_t comma = after_scheme.find(',');
197  const std::string_view type_info = after_scheme.substr(0, comma);
198 
199  const std::size_t semicolon = type_info.find(';');
200 
201  scheme = data_URI.substr(0, colon);
202  base64 = type_info.substr(semicolon + 1);
203  mime = type_info.substr(0, semicolon);
204  data = after_scheme.substr(comma + 1);
205  good = (scheme == "data" && base64 == "base64" && mime.length() > 0 && data.length() > 0);
206 }
207 
208 } // end anon namespace
209 
211 {
212  for(surface_cache& cache : surfaces_) {
213  cache.flush();
214  }
215  lit_surfaces_.flush();
216  lit_textures_.flush();
217  surface_lightmaps_.clear();
218  texture_lightmaps_.clear();
219  in_hex_info_.flush();
220  is_empty_hex_.flush();
221  textures_.clear();
222  textures_hexed_.clear();
223  texture_tod_colored_.clear();
224  image_existence_map.clear();
225  precached_dirs.clear();
226 }
227 
228 locator locator::clone(const std::string& mods) const
229 {
230  locator res = *this;
231  if(!mods.empty()) {
232  res.modifications_ += mods;
233  res.type_ = SUB_FILE;
234  }
235 
236  return res;
237 }
238 
239 std::ostream& operator<<(std::ostream& s, const locator& l)
240 {
241  s << l.get_filename();
242  if(!l.get_modifications().empty()) {
243  if(l.get_modifications()[0] != '~') {
244  s << '~';
245  }
246  s << l.get_modifications();
247  }
248  return s;
249 }
250 
251 locator::locator(const std::string& fn)
252  : filename_(fn)
253 {
254  if(filename_.empty()) {
255  return;
256  }
257 
258  if(boost::algorithm::starts_with(filename_, data_uri_prefix)) {
259  if(parsed_data_URI parsed{ filename_ }; !parsed.good) {
260  std::string_view view{ filename_ };
261  std::string_view stripped = view.substr(0, view.find(","));
262  ERR_IMG << "Invalid data URI: " << stripped;
263  }
264 
265  is_data_uri_ = true;
266  }
267 
268  if(const std::size_t markup_field = filename_.find('~'); markup_field != std::string::npos) {
269  type_ = SUB_FILE;
270  modifications_ = filename_.substr(markup_field, filename_.size() - markup_field);
271  filename_ = filename_.substr(0, markup_field);
272  } else {
273  type_ = FILE;
274  }
275 }
276 
277 locator::locator(const std::string& filename, const std::string& modifications)
278  : type_(SUB_FILE)
280  , modifications_(modifications)
281 {
282 }
283 
285  const std::string& filename,
286  const map_location& loc,
287  int center_x,
288  int center_y,
289  const std::string& modifications)
290  : type_(SUB_FILE)
292  , modifications_(modifications)
293  , loc_(loc)
294  , center_x_(center_x)
295  , center_y_(center_y)
296 {
297 }
298 
299 bool locator::operator==(const locator& a) const
300 {
301  if(a.type_ != type_) {
302  return false;
303  } else if(type_ == FILE) {
304  return filename_ == a.filename_;
305  } else if(type_ == SUB_FILE) {
306  return std::tie(filename_, loc_, modifications_, center_x_, center_y_) ==
307  std::tie(a.filename_, a.loc_, a.modifications_, a.center_x_, a.center_y_);
308  }
309 
310  return false;
311 }
312 
313 bool locator::operator<(const locator& a) const
314 {
315  if(type_ != a.type_) {
316  return type_ < a.type_;
317  } else if(type_ == FILE) {
318  return filename_ < a.filename_;
319  } else if(type_ == SUB_FILE) {
320  return std::tie(filename_, loc_, modifications_, center_x_, center_y_) <
321  std::tie(a.filename_, a.loc_, a.modifications_, a.center_x_, a.center_y_);
322  }
323 
324  return false;
325 }
326 
327 // Load overlay image and compose it with the original surface.
328 static void add_localized_overlay(const std::string& ovr_file, surface& orig_surf)
329 {
331  surface ovr_surf = IMG_Load_RW(rwops.release(), true); // SDL takes ownership of rwops
332  if(!ovr_surf) {
333  return;
334  }
335 
336  rect area {0, 0, ovr_surf->w, ovr_surf->h};
337 
338  sdl_blit(ovr_surf, nullptr, orig_surf, &area);
339 }
340 
342 {
343  surface res;
344  const std::string& name = loc.get_filename();
345 
346  auto location = filesystem::get_binary_file_location("images", name);
347 
348  // Many images have been converted from PNG to WEBP format,
349  // but the old filename may still be saved in savegame files etc.
350  // If the file does not exist in ".png" format, also try ".webp".
351  // Similarly for ".jpg", which conveniently has the same number of letters as ".png".
352  if(!location && (boost::algorithm::ends_with(name, ".png") || boost::algorithm::ends_with(name, ".jpg"))) {
353  std::string webp_name = name.substr(0, name.size() - 4) + ".webp";
354  location = filesystem::get_binary_file_location("images", webp_name);
355  if(location) {
356  WRN_IMG << "Replaced missing '" << name << "' with found '"
357  << webp_name << "'.";
358  }
359  }
360 
361  {
362  if(location) {
363  // Check if there is a localized image.
364  const auto loc_location = filesystem::get_localized_path(location.value());
365  if(loc_location) {
366  location = loc_location.value();
367  }
368 
369  filesystem::rwops_ptr rwops = filesystem::make_read_RWops(location.value());
370  res = IMG_Load_RW(rwops.release(), true); // SDL takes ownership of rwops
371 
372  // If there was no standalone localized image, check if there is an overlay.
373  if(res && !loc_location) {
374  const auto ovr_location = filesystem::get_localized_path(location.value(), "--overlay");
375  if(ovr_location) {
376  add_localized_overlay(ovr_location.value(), res);
377  }
378  }
379  }
380  }
381 
382  if(!res && !name.empty()) {
383  ERR_IMG << "could not open image '" << name << "'";
386  if(name != game_config::images::blank)
388  }
389 
390  return res;
391 }
392 
394 {
395  // Create a new surface in-memory on which to apply the modifications
396  surface surf = get_surface(loc.get_filename(), UNSCALED).clone();
397  if(surf == nullptr) {
398  return nullptr;
399  }
400 
401  modification_queue mods = modification::decode(loc.get_modifications());
402 
403  while(!mods.empty()) {
404  try {
405  std::invoke(mods.top(), surf);
406  } catch(const image::modification::imod_exception& e) {
407  std::ostringstream ss;
408  ss << "\n";
409 
410  for(const std::string& mod_name : utils::parenthetical_split(loc.get_modifications(), '~')) {
411  ss << "\t" << mod_name << "\n";
412  }
413 
414  ERR_CFG << "Failed to apply a modification to an image:\n"
415  << "Image: " << loc.get_filename() << "\n"
416  << "Modifications: " << ss.str() << "\n"
417  << "Error: " << e.message;
418  }
419 
420  mods.pop();
421  }
422 
423  if(loc.get_loc().valid()) {
424  rect srcrect(
425  ((tile_size * 3) / 4) * loc.get_loc().x,
426  tile_size * loc.get_loc().y + (tile_size / 2) * (loc.get_loc().x % 2),
427  tile_size,
428  tile_size
429  );
430 
431  if(loc.get_center_x() >= 0 && loc.get_center_y() >= 0) {
432  srcrect.x += surf->w / 2 - loc.get_center_x();
433  srcrect.y += surf->h / 2 - loc.get_center_y();
434  }
435 
436  // cut and hex mask, but also check and cache if empty result
437  surface cut = cut_surface(surf, srcrect);
438  bool is_empty = mask_surface(cut, get_hexmask());
439 
440  // discard empty images to free memory
441  if(is_empty) {
442  // Safe because those images are only used by terrain rendering
443  // and it filters them out.
444  // A safer and more general way would be to keep only one copy of it
445  surf = nullptr;
446  } else {
447  surf = cut;
448  }
449 
450  is_empty_hex_.add_to_cache(loc, is_empty);
451  }
452 
453  return surf;
454 }
455 
457 {
458  surface surf;
459 
460  parsed_data_URI parsed{loc.get_filename()};
461 
462  if(!parsed.good) {
463  std::string_view fn = loc.get_filename();
464  std::string_view stripped = fn.substr(0, fn.find(","));
465  ERR_IMG << "Invalid data URI: " << stripped;
466  } else if(parsed.mime.substr(0, 5) != "image") {
467  ERR_IMG << "Data URI not of image MIME type: " << parsed.mime;
468  } else {
469  const std::vector<uint8_t> image_data = base64::decode(parsed.data);
470  filesystem::rwops_ptr rwops{SDL_RWFromConstMem(image_data.data(), image_data.size())};
471 
472  if(image_data.empty()) {
473  ERR_IMG << "Invalid encoding in data URI";
474  } else if(parsed.mime == "image/png") {
475  surf = IMG_LoadPNG_RW(rwops.release());
476  } else if(parsed.mime == "image/jpeg") {
477  surf = IMG_LoadJPG_RW(rwops.release());
478  } else if(parsed.mime == "image/webp") {
479  surf = IMG_LoadWEBP_RW(rwops.release());
480  } else {
481  ERR_IMG << "Invalid image MIME type: " << parsed.mime;
482  }
483  }
484 
485  return surf;
486 }
487 
488 light_adjust::light_adjust(int op, int rr, int gg, int bb)
489  : l(op), r(), g(), b()
490 {
491  constexpr int min = std::numeric_limits<int8_t>::min();
492  constexpr int max = std::numeric_limits<int8_t>::max();
493 
494  // Clamp in a wider type (int) to avoid truncating the input value prematurely
495  r = std::clamp(rr / 2, min, max);
496  g = std::clamp(gg / 2, min, max);
497  b = std::clamp(bb / 2, min, max);
498 }
499 
500 #ifndef __cpp_impl_three_way_comparison
502 {
503  return std::tie(l, r, g, b) == std::tie(o.l, o.r, o.g, o.b);
504 }
505 #endif
506 
507 /** Hash function overload for boost::hash. Must be in the image namespace to satisfy ADL. */
508 static std::size_t hash_value(const light_adjust& adj)
509 {
510  std::size_t hash{0};
511 
512  boost::hash_combine(hash, adj.l);
513  boost::hash_combine(hash, adj.r);
514  boost::hash_combine(hash, adj.g);
515  boost::hash_combine(hash, adj.b);
516 
517  return hash;
518 }
519 
520 static surface apply_light(surface surf, const std::vector<light_adjust>& ls)
521 {
522  // atomic lightmap operation are handled directly (important to end recursion)
523  if(ls.size() == 1) {
524  // if no lightmap (first char = -1) then we need the initial value
525  //(before the halving done for lightmap)
526  int m = ls[0].l == -1 ? 2 : 1;
527  adjust_surface_color(surf, ls[0].r * m, ls[0].g * m, ls[0].b * m);
528  return surf;
529  }
530 
531  const auto get_lightmap = [&ls]
532  {
533  const auto iter = surface_lightmaps_.find(ls);
534  if(iter != surface_lightmaps_.end()) {
535  return iter->second;
536  }
537 
538  // build all the paths for lightmap sources
539  static const std::string p = "terrain/light/light";
540  static const std::string lm_img[19] {
541  p + ".png",
542  p + "-concave-2-tr.png", p + "-concave-2-r.png", p + "-concave-2-br.png",
543  p + "-concave-2-bl.png", p + "-concave-2-l.png", p + "-concave-2-tl.png",
544  p + "-convex-br-bl.png", p + "-convex-bl-l.png", p + "-convex-l-tl.png",
545  p + "-convex-tl-tr.png", p + "-convex-tr-r.png", p + "-convex-r-br.png",
546  p + "-convex-l-bl.png", p + "-convex-tl-l.png", p + "-convex-tr-tl.png",
547  p + "-convex-r-tr.png", p + "-convex-br-r.png", p + "-convex-bl-br.png"
548  };
549 
550  // first image will be the base onto which we blit the others
551  surface base;
552 
553  for(const light_adjust& adj : ls) {
554  // get the corresponding image and apply the lightmap operation to it
555  // This allows to also cache lightmap parts.
556  // note that we avoid infinite recursion by using only atomic operation
557  surface lts = image::get_lighted_image(lm_img[adj.l], std::vector{adj});
558 
559  if(base == nullptr) {
560  // copy the cached image to avoid modifying the cache
561  base = lts.clone();
562  } else {
563  sdl_blit(lts, nullptr, base, nullptr);
564  }
565  }
566 
567  // cache the result
568  surface_lightmaps_[ls] = base;
569  return base;
570  };
571 
572  // apply the final lightmap
573  light_surface(surf, get_lightmap());
574  return surf;
575 }
576 
578 {
579  switch(loc.get_type()) {
580  case locator::FILE:
581  if(loc.is_data_uri()){
582  return load_image_data_uri(loc);
583  } else {
584  return load_image_file(loc);
585  }
586  case locator::SUB_FILE:
587  return load_image_sub_file(loc);
588  default:
589  return surface(nullptr);
590  }
591 }
592 
594 {
595 }
596 
598 {
599  flush_cache();
600 }
601 
602 void set_color_adjustment(int r, int g, int b)
603 {
604  if(r != red_adjust || g != green_adjust || b != blue_adjust) {
605  red_adjust = r;
606  green_adjust = g;
607  blue_adjust = b;
608  surfaces_[TOD_COLORED].flush();
609  lit_surfaces_.flush();
610  lit_textures_.flush();
611  texture_tod_colored_.clear();
612  }
613 }
614 
615 static surface get_hexed(const locator& i_locator, bool skip_cache = false)
616 {
617  surface image = get_surface(i_locator, UNSCALED, skip_cache).clone();
618  surface mask = get_hexmask();
619 
620  // Ensure the image is the correct size by cropping and/or centering.
621  // TODO: this should probably be a function of sdl/utils
622  if(image && (image->w != mask->w || image->h != mask->h)) {
623  DBG_IMG << "adjusting [" << image->w << ',' << image->h << ']'
624  << " image to hex mask: " << i_locator;
625 
626  auto fit = surface(mask->w, mask->h);
627 
628  // Fill the crop surface with transparency
629  SDL_FillRect(fit, nullptr, SDL_MapRGBA(fit->format, 0, 0, 0, 0));
630 
631  // Returns an area rectangle clamped at the max size of the base surface.
632  // If surf is smaller than base, the result is centered relative to base.
633  const auto centered_intersection = [](const surface& surf, const surface& base) -> rect {
634  return {
635  std::max(0, surf->w - base->w) / 2,
636  std::max(0, surf->h - base->h) / 2,
637  std::min(surf->w, base->w),
638  std::min(surf->h, base->h),
639  };
640  };
641 
642  rect src = centered_intersection(image, mask);
643  rect dst = centered_intersection(mask, image);
644 
645  SDL_BlendMode src_blend;
646  SDL_GetSurfaceBlendMode(image, &src_blend);
647  SDL_SetSurfaceBlendMode(image, SDL_BLENDMODE_NONE);
648  // Take the center area of the source image, up to the size of the hex mask,
649  // and copy it, likewise centered, to the temporary surface. If the image is
650  // larger than the hex mask, its center portion will be retained. If instead
651  // it's *smaller* than the hex mask, it will be copied wholesale to the temp
652  // surface and render centered in any hex to which it is drawn.
653  sdl_blit(image, &src, fit, &dst);
654  SDL_SetSurfaceBlendMode(image, src_blend);
655 
656  image = std::move(fit);
657  }
658 
659  // hex cut tiles, also check and cache if empty result
660  bool is_empty = mask_surface(image, mask, i_locator.get_filename());
661  is_empty_hex_.add_to_cache(i_locator, is_empty);
662  return image;
663 }
664 
665 static surface get_tod_colored(const locator& i_locator, bool skip_cache = false)
666 {
667  surface img = get_surface(i_locator, HEXED, skip_cache).clone();
668  adjust_surface_color(img, red_adjust, green_adjust, blue_adjust);
669  return img;
670 }
671 
672 /** translate type to a simpler one when possible */
673 static TYPE simplify_type(const image::locator& i_locator, TYPE type)
674 {
675  if(type == TOD_COLORED) {
676  if(red_adjust == 0 && green_adjust == 0 && blue_adjust == 0) {
677  type = HEXED;
678  }
679  }
680 
681  if(type == HEXED) {
682  // check if the image is already hex-cut by the location system
683  if(i_locator.get_loc().valid()) {
684  type = UNSCALED;
685  }
686  }
687 
688  return type;
689 }
690 
692  const image::locator& i_locator,
693  TYPE type,
694  bool skip_cache)
695 {
696  surface res;
697 
698  if(i_locator.is_void()) {
699  return res;
700  }
701 
702  type = simplify_type(i_locator, type);
703 
704  // select associated cache
705  if(type >= NUM_TYPES) {
706  WRN_IMG << "get_surface called with unknown image type";
707  return res;
708  }
709  surface_cache& imap = surfaces_[type];
710 
711  // return the image if already cached
712  if(const surface* cached_surf = imap.locate_in_cache(i_locator)) {
713  return *cached_surf;
714  } else {
715  DBG_IMG << "surface cache [" << type << "] miss: " << i_locator;
716  }
717 
718  // not cached, generate it
719  switch(type) {
720  case UNSCALED:
721  // If type is unscaled, directly load the image from the disk.
722  res = load_from_disk(i_locator);
723  break;
724  case TOD_COLORED:
725  res = get_tod_colored(i_locator, skip_cache);
726  break;
727  case HEXED:
728  res = get_hexed(i_locator, skip_cache);
729  break;
730  default:
731  throw game::error("get_surface somehow lost image type?");
732  }
733 
734  bool_cache& skip = skipped_cache_[type];
735 
736  // In cache...
737  if(const bool* cached_value = skip.locate_in_cache(i_locator)) {
738  // ... and cached as true
739  if(*cached_value) {
740  DBG_IMG << "duplicate load: " << i_locator
741  << " [" << type << "]"
742  << " (" << duplicate_loads_ << "/" << total_loads_ << " total)";
743  ++duplicate_loads_;
744  }
745  }
746 
747  ++total_loads_;
748 
749  if(skip_cache) {
750  DBG_IMG << "surface cache [" << type << "] skip: " << i_locator;
751  skip.add_to_cache(i_locator, true);
752  } else {
753  imap.add_to_cache(i_locator, res);
754  }
755 
756  return res;
757 }
758 
759 surface get_lighted_image(const image::locator& i_locator, const std::vector<light_adjust>& ls)
760 {
761  if(i_locator.is_void()) {
762  return {};
763  }
764 
765  lit_surface_variants& lvar = lit_surfaces_.access_in_cache(i_locator);
766 
767  // Check the matching list_string variants for this locator
768  if(const auto iter = lvar.find(ls); iter != lvar.end()) {
769  return iter->second;
770  } else {
771  DBG_IMG << "lit surface cache miss: " << i_locator;
772  }
773 
774  // not cached yet, generate it
775  surface res = apply_light(get_surface(i_locator, HEXED).clone(), ls);
776 
777  // record the lighted surface in the corresponding variants cache
778  lvar[ls] = res;
779  return res;
780 }
781 
782 texture get_lighted_texture(const image::locator& i_locator, const std::vector<light_adjust>& ls)
783 {
784  if(i_locator.is_void()) {
785  return texture();
786  }
787 
788  lit_texture_variants& lvar = lit_textures_.access_in_cache(i_locator);
789 
790  // Check the matching list_string variants for this locator
791  if(const auto iter = lvar.find(ls); iter != lvar.end()) {
792  return iter->second;
793  } else {
794  DBG_IMG << "lit texture cache miss: " << i_locator;
795  }
796 
797  // not cached yet, generate it
798  texture tex(get_lighted_image(i_locator, ls));
799 
800  // record the lighted texture in the corresponding variants cache
801  lvar[ls] = tex;
802  return tex;
803 }
804 
806 {
809 }
810 
811 point get_size(const locator& i_locator, bool skip_cache)
812 {
813  if(const surface s = get_surface(i_locator, UNSCALED, skip_cache)) {
814  return {s->w, s->h};
815  } else {
816  return {0, 0};
817  }
818 }
819 
820 bool is_in_hex(const locator& i_locator)
821 {
822  if(const bool* cached_value = in_hex_info_.locate_in_cache(i_locator)) {
823  return *cached_value;
824  } else {
825  bool res = in_mask_surface(get_surface(i_locator, UNSCALED), get_hexmask());
826  in_hex_info_.add_to_cache(i_locator, res);
827  return res;
828  }
829 }
830 
831 bool is_empty_hex(const locator& i_locator)
832 {
833  if(const bool* cached_value = is_empty_hex_.locate_in_cache(i_locator)) {
834  return *cached_value;
835  }
836 
837  surface surf = get_surface(i_locator, HEXED);
838 
839  // Empty state should be cached during surface fetch. Let's check again
840  if(const bool* cached_value = is_empty_hex_.locate_in_cache(i_locator)) {
841  return *cached_value;
842  }
843 
844  // Should never reach this point, but let's manually do it anyway.
845  surf = surf.clone();
846  bool is_empty = mask_surface(surf, get_hexmask());
847  is_empty_hex_.add_to_cache(i_locator, is_empty);
848  return is_empty;
849 }
850 
851 bool exists(const image::locator& i_locator)
852 {
853  if(i_locator.is_void()) {
854  return false;
855  }
856 
857  // The insertion will fail if there is already an element in the cache
858  // and this will point to the existing element.
859  auto [iter, success] = image_existence_map.emplace(i_locator.get_filename(), false);
860 
861  bool& cache = iter->second;
862  if(success) {
863  if(i_locator.is_data_uri()) {
864  cache = parsed_data_URI{i_locator.get_filename()}.good;
865  } else {
866  cache = filesystem::get_binary_file_location("images", i_locator.get_filename()).has_value();
867  }
868  }
869 
870  return cache;
871 }
872 
873 static void precache_file_existence_internal(const std::string& dir, const std::string& subdir)
874 {
875  const std::string checked_dir = dir + "/" + subdir;
876  if(precached_dirs.find(checked_dir) != precached_dirs.end()) {
877  return;
878  }
879 
880  precached_dirs.insert(checked_dir);
881 
882  if(!filesystem::is_directory(checked_dir)) {
883  return;
884  }
885 
886  std::vector<std::string> files_found;
887  std::vector<std::string> dirs_found;
888  filesystem::get_files_in_dir(checked_dir, &files_found, &dirs_found, filesystem::name_mode::FILE_NAME_ONLY,
890 
891  for(const auto& f : files_found) {
892  image_existence_map[subdir + f] = true;
893  }
894 
895  for(const auto& d : dirs_found) {
896  precache_file_existence_internal(dir, subdir + d + "/");
897  }
898 }
899 
900 void precache_file_existence(const std::string& subdir)
901 {
902  for(const auto& p : filesystem::get_binary_paths("images")) {
904  }
905 }
906 
907 bool precached_file_exists(const std::string& file)
908 {
909  const auto b = image_existence_map.find(file);
910  if(b != image_existence_map.end()) {
911  return b->second;
912  }
913 
914  return false;
915 }
916 
917 save_result save_image(const locator& i_locator, const std::string& filename)
918 {
919  return save_image(get_surface(i_locator), filename);
920 }
921 
922 save_result save_image(const surface& surf, const std::string& filename)
923 {
924  if(!surf) {
925  return save_result::no_image;
926  }
927 
928  if(boost::algorithm::ends_with(filename, ".jpeg") || boost::algorithm::ends_with(filename, ".jpg") || boost::algorithm::ends_with(filename, ".jpe")) {
929  LOG_IMG << "Writing a JPG image to " << filename;
930 
931  const int err = IMG_SaveJPG_RW(surf, filesystem::make_write_RWops(filename).release(), true, 75); // SDL takes ownership of the RWops
933  }
934 
935  if(boost::algorithm::ends_with(filename, ".png")) {
936  LOG_IMG << "Writing a PNG image to " << filename;
937 
938  const int err = IMG_SavePNG_RW(surf, filesystem::make_write_RWops(filename).release(), true); // SDL takes ownership of the RWops
940  }
941 
943 }
944 
945 /*
946  * TEXTURE INTERFACE ======================================================================
947  *
948  * The only important difference here is that textures must have their
949  * scale quality set before creation. All other handling is done by
950  * get_surface.
951  */
952 
953 texture get_texture(const image::locator& i_locator, TYPE type, bool skip_cache)
954 {
955  return get_texture(i_locator, scale_quality::nearest, type, skip_cache);
956 }
957 
958 /** Returns a texture for the corresponding image. */
959 texture get_texture(const image::locator& i_locator, scale_quality quality, TYPE type, bool skip_cache)
960 {
961  texture res;
962 
963  if(i_locator.is_void()) {
964  return res;
965  }
966 
967  type = simplify_type(i_locator, type);
968 
969  //
970  // Select the appropriate cache. We don't need caches for every single image types,
971  // since some types can be handled by render-time operations.
972  //
973  texture_cache* cache = nullptr;
974 
975  switch(type) {
976  case HEXED:
977  cache = &textures_hexed_[quality];
978  break;
979  case TOD_COLORED:
980  cache = &texture_tod_colored_[quality];
981  break;
982  default:
983  cache = &textures_[quality];
984  }
985 
986  //
987  // Now attempt to find a cached texture. If found, return it.
988  //
989  if(const texture* cached_texture = cache->locate_in_cache(i_locator)) {
990  return *cached_texture;
991  } else {
992  DBG_IMG << "texture cache [" << type << "] miss: " << i_locator;
993  }
994 
995  //
996  // No texture was cached. In that case, create a new one. The explicit cases require special
997  // handling with surfaces in order to generate the desired effect. This shouldn't be the case
998  // once we get OGL and shader support.
999  //
1000 
1001  // Get it from the surface cache, also setting the desired scale quality.
1002  const bool linear_scaling = quality == scale_quality::linear ? true : false;
1003  if(i_locator.get_modifications().empty()) {
1004  // skip cache if we're loading plain files with no modifications
1005  res = texture(get_surface(i_locator, type, true), linear_scaling);
1006  } else {
1007  res = texture(get_surface(i_locator, type, skip_cache), linear_scaling);
1008  }
1009 
1010  // Cache the texture.
1011  if(skip_cache) {
1012  DBG_IMG << "texture cache [" << type << "] skip: " << i_locator;
1013  } else {
1014  cache->add_to_cache(i_locator, res);
1015  }
1016 
1017  return res;
1018 }
1019 
1020 } // end namespace image
std::string filename_
Definition: action_wml.cpp:534
map_location loc
Definition: move.cpp:172
double g
Definition: astarsearch.cpp:63
bool in_cache(const locator &item) const
Definition: picture.cpp:90
T & access_in_cache(const locator &item)
Returns a reference to the cache item associated with the given key.
Definition: picture.cpp:109
cache_map< locator, T > content_
Definition: picture.cpp:125
const T * locate_in_cache(const locator &item) const
Returns a pointer to the cached value, or nullptr if not found.
Definition: picture.cpp:96
void add_to_cache(const locator &item, T data)
Definition: picture.cpp:114
Generic locator abstracting the location of an image.
Definition: picture.hpp:59
bool is_void() const
Returns true if the locator does not correspond to an actual image.
Definition: picture.hpp:93
map_location loc_
Definition: picture.hpp:100
std::string filename_
Definition: picture.hpp:98
const std::string & get_filename() const
Definition: picture.hpp:82
bool is_data_uri() const
Definition: picture.hpp:83
const std::string & get_modifications() const
Definition: picture.hpp:87
bool is_data_uri_
Definition: picture.hpp:97
const map_location & get_loc() const
Definition: picture.hpp:84
locator::type type_
Definition: picture.hpp:96
locator()=default
bool operator<(const locator &a) const
Definition: picture.cpp:313
bool operator==(const locator &a) const
Definition: picture.cpp:299
locator clone(const std::string &mods) const
Returns a copy of this locator with the given IPF.
Definition: picture.cpp:228
std::string modifications_
Definition: picture.hpp:99
A modified priority queue used to order image modifications.
const modification & top() const
Returns a const reference to the top element in the queue.
void pop()
Removes the top element from the queue.
static modification_queue decode(const std::string &)
Decodes modifications from a modification string.
surface clone() const
Creates a new, duplicate surface in memory using the 'neutral' pixel format.
Definition: surface.cpp:97
Wrapper class to encapsulate creation and management of an SDL_Texture.
Definition: texture.hpp:33
Declarations for File-IO.
Standard logging facilities (interface).
std::vector< uint8_t > decode(std::string_view in)
Definition: base64.cpp:219
void get_files_in_dir(const std::string &dir, std::vector< std::string > *files, std::vector< std::string > *dirs, name_mode mode, filter_mode filter, reorder_mode reorder, file_tree_checksum *checksum)
Get a list of all files and/or directories in a given directory.
Definition: filesystem.cpp:466
std::unique_ptr< SDL_RWops, sdl_rwops_deleter > rwops_ptr
Definition: filesystem.hpp:61
bool is_directory(const std::string &fname)
Returns true if the given file is a directory.
rwops_ptr make_read_RWops(const std::string &path)
utils::optional< std::string > get_binary_file_location(const std::string &type, const std::string &filename)
Returns a complete path to the actual file of a given type, if it exists.
utils::optional< std::string > get_localized_path(const std::string &file, const std::string &suff)
Returns the localized version of the given filename, if it exists.
rwops_ptr make_write_RWops(const std::string &path)
const std::vector< std::string > & get_binary_paths(const std::string &type)
Returns a vector with all possible paths to a given type of binary, e.g.
std::string terrain_mask
const bool & debug
Definition: game_config.cpp:95
unsigned int tile_size
Definition: game_config.cpp:55
Functions to load and save images from/to disk.
static surface load_image_sub_file(const image::locator &loc)
Definition: picture.cpp:393
bool is_empty_hex(const locator &i_locator)
Checks if an image is empty after hex masking.
Definition: picture.cpp:831
static void add_localized_overlay(const std::string &ovr_file, surface &orig_surf)
Definition: picture.cpp:328
bool precached_file_exists(const std::string &file)
Definition: picture.cpp:907
save_result
Definition: picture.hpp:271
static TYPE simplify_type(const image::locator &i_locator, TYPE type)
translate type to a simpler one when possible
Definition: picture.cpp:673
static surface load_image_data_uri(const image::locator &loc)
Definition: picture.cpp:456
static surface get_tod_colored(const locator &i_locator, bool skip_cache=false)
Definition: picture.cpp:665
bool exists(const image::locator &i_locator)
Returns true if the given image actually exists, without loading it.
Definition: picture.cpp:851
void flush_cache()
Purges all image caches.
Definition: picture.cpp:210
static void precache_file_existence_internal(const std::string &dir, const std::string &subdir)
Definition: picture.cpp:873
static surface load_from_disk(const locator &loc)
Definition: picture.cpp:577
surface get_surface(const image::locator &i_locator, TYPE type, bool skip_cache)
Returns an image surface suitable for software manipulation.
Definition: picture.cpp:691
static surface apply_light(surface surf, const std::vector< light_adjust > &ls)
Definition: picture.cpp:520
surface get_hexmask()
Retrieves the standard hexagonal tile mask.
Definition: picture.cpp:805
std::ostream & operator<<(std::ostream &s, const locator &l)
Definition: picture.cpp:239
save_result save_image(const locator &i_locator, const std::string &filename)
Definition: picture.cpp:917
void precache_file_existence(const std::string &subdir)
Precache the existence of files in a binary path subdirectory (e.g.
Definition: picture.cpp:900
TYPE
Used to specify the rendering format of images.
Definition: picture.hpp:171
@ HEXED
Standard hexagonal tile mask applied, removing portions that don't fit.
Definition: picture.hpp:175
@ NUM_TYPES
Definition: picture.hpp:178
@ TOD_COLORED
Same as HEXED, but with Time of Day color tint applied.
Definition: picture.hpp:177
@ UNSCALED
Unmodified original-size image.
Definition: picture.hpp:173
texture get_texture(const image::locator &i_locator, TYPE type, bool skip_cache)
Returns an image texture suitable for hardware-accelerated rendering.
Definition: picture.cpp:953
point get_size(const locator &i_locator, bool skip_cache)
Returns the width and height of an image.
Definition: picture.cpp:811
static surface get_hexed(const locator &i_locator, bool skip_cache=false)
Definition: picture.cpp:615
texture get_lighted_texture(const image::locator &i_locator, const std::vector< light_adjust > &ls)
Definition: picture.cpp:782
std::size_t hash_value(const image::locator &val)
Hash function overload for boost::hash.
Definition: picture.cpp:67
void set_color_adjustment(int r, int g, int b)
Changes Time of Day color tint for all applicable image types.
Definition: picture.cpp:602
surface get_lighted_image(const image::locator &i_locator, const std::vector< light_adjust > &ls)
Caches and returns an image with a lightmap applied to it.
Definition: picture.cpp:759
bool is_in_hex(const locator &i_locator)
Checks if an image fits into a single hex.
Definition: picture.cpp:820
scale_quality
Definition: picture.hpp:181
static surface load_image_file(const image::locator &loc)
Definition: picture.cpp:341
logger & err()
Definition: log.cpp:339
std::string img(const std::string &src, const std::string &align, bool floating)
Generates a Help markup tag corresponding to an image.
Definition: markup.cpp:36
std::vector< std::string > parenthetical_split(std::string_view val, const char separator, std::string_view left, std::string_view right, const int flags)
Splits a string based either on a separator, except then the text appears within specified parenthesi...
#define DBG_IMG
Definition: picture.cpp:57
static lg::log_domain log_image("image")
#define ERR_CFG
Definition: picture.cpp:60
std::string_view scheme
Definition: picture.cpp:185
boost::unordered_map< Key, Value > cache_map
Definition: picture.cpp:46
std::string_view mime
Definition: picture.cpp:186
bool good
Definition: picture.cpp:189
#define WRN_IMG
Definition: picture.cpp:55
std::string_view base64
Definition: picture.cpp:187
#define LOG_IMG
Definition: picture.cpp:56
#define ERR_IMG
Definition: picture.cpp:54
std::string_view data
Definition: picture.cpp:188
static lg::log_domain log_config("config")
Contains the SDL_Rect helper code.
rect dst
Location on the final composed sheet.
surface surf
Image.
rect src
Non-transparent portion of the surface to compose.
std::string filename
Filename.
Base class for all the errors encountered by the engine.
Definition: exceptions.hpp:29
Type used to store color information of central and adjacent hexes.
Definition: picture.hpp:124
bool operator==(const light_adjust &o) const
Definition: picture.cpp:501
light_adjust(int op, int r, int g, int b)
Definition: picture.cpp:488
Exception thrown by the operator() when an error occurs.
Encapsulates the map of the game.
Definition: location.hpp:46
bool valid() const
Definition: location.hpp:111
Holds a 2D point.
Definition: point.hpp:25
An abstract description of a rectangle with integer coordinates.
Definition: rect.hpp:49
mock_party p
static map_location::direction s
surface cut_surface(const surface &surf, const rect &r)
Cuts a rectangle from a surface.
Definition: utils.cpp:1098
void adjust_surface_color(surface &nsurf, int red, int green, int blue)
Definition: utils.cpp:405
void light_surface(surface &nsurf, const surface &lightmap)
Light surf using lightmap.
Definition: utils.cpp:796
bool in_mask_surface(const surface &nsurf, const surface &nmask)
Check if a surface fit into a mask.
Definition: utils.cpp:758
bool mask_surface(surface &nsurf, const surface &nmask, const std::string &filename)
Applies a mask on a surface.
Definition: utils.cpp:706
void sdl_blit(const surface &src, const SDL_Rect *src_rect, surface &dst, SDL_Rect *dst_rect)
Definition: utils.hpp:43
#define d
#define e
#define f
#define b