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