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