/* Copyright (C) 2003 - 2018 by David White Part of the Battle for Wesnoth Project https://www.wesnoth.org/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. See the COPYING file for more details. */ /** * @file * Routines for images: load, scale, re-color, etc. */ #include "picture.hpp" #include "config.hpp" #include "display.hpp" #include "filesystem.hpp" #include "game_config.hpp" #include "image_modifications.hpp" #include "log.hpp" #include "preferences/general.hpp" #include "serialization/base64.hpp" #include "serialization/string_utils.hpp" #include "sdl/rect.hpp" #include "utils/general.hpp" #include #include #include #include #include static lg::log_domain log_display("display"); #define ERR_DP LOG_STREAM(err, log_display) #define LOG_DP LOG_STREAM(info, log_display) static lg::log_domain log_config("config"); #define ERR_CFG LOG_STREAM(err, log_config) using game_config::tile_size; template struct cache_item { cache_item() : item() , loaded(false) { } cache_item(const T& item) : item(item) , loaded(true) { } T item; bool loaded; }; namespace std { template<> struct hash { std::size_t operator()(const image::locator::value& val) const { std::size_t hash = std::hash{}(val.type_); if(val.type_ == image::locator::FILE || val.type_ == image::locator::SUB_FILE) { boost::hash_combine(hash, val.filename_); } if(val.type_ == image::locator::SUB_FILE) { boost::hash_combine(hash, val.loc_.x); boost::hash_combine(hash, val.loc_.y); boost::hash_combine(hash, val.center_x_); boost::hash_combine(hash, val.center_y_); boost::hash_combine(hash, val.modifications_); } return hash; } }; } namespace image { template class cache_type { public: cache_type() : content_() { } cache_item& get_element(int index) { if(static_cast(index) >= content_.size()) content_.resize(index + 1); return content_[index]; } void flush() { content_.clear(); } private: std::vector> content_; }; template bool locator::in_cache(cache_type& cache) const { return index_ < 0 ? false : cache.get_element(index_).loaded; } template const T& locator::locate_in_cache(cache_type& cache) const { static T dummy; return index_ < 0 ? dummy : cache.get_element(index_).item; } template T& locator::access_in_cache(cache_type& cache) const { static T dummy; return index_ < 0 ? dummy : cache.get_element(index_).item; } template void locator::add_to_cache(cache_type& cache, const T& data) const { if(index_ >= 0) { cache.get_element(index_) = cache_item(data); } } } namespace { image::locator::locator_finder_t locator_finder; /** Definition of all image maps */ image::image_cache images_, scaled_to_zoom_, hexed_images_, scaled_to_hex_images_, tod_colored_images_, brightened_images_; // cache storing if each image fit in a hex image::bool_cache in_hex_info_; // cache storing if this is an empty hex image::bool_cache is_empty_hex_; // caches storing the different lighted cases for each image image::lit_cache lit_images_, lit_scaled_images_; // caches storing each lightmap generated image::lit_variants lightmaps_; // const int cache_version_ = 0; std::map image_existence_map; // directories where we already cached file existence std::set precached_dirs; std::map reversed_images_; int red_adjust = 0, green_adjust = 0, blue_adjust = 0; unsigned int zoom = tile_size; unsigned int cached_zoom = 0; const std::string data_uri_prefix = "data:"; struct parsed_data_URI{ explicit parsed_data_URI(utils::string_view data_URI); utils::string_view scheme; utils::string_view mime; utils::string_view base64; utils::string_view data; bool good; }; parsed_data_URI::parsed_data_URI(utils::string_view data_URI) { const std::size_t colon = data_URI.find(':'); const utils::string_view after_scheme = data_URI.substr(colon + 1); const std::size_t comma = after_scheme.find(','); const utils::string_view type_info = after_scheme.substr(0, comma); const std::size_t semicolon = type_info.find(';'); scheme = data_URI.substr(0, colon); base64 = type_info.substr(semicolon + 1); mime = type_info.substr(0, semicolon); data = after_scheme.substr(comma + 1); good = (scheme == "data" && base64 == "base64" && mime.length() > 0 && data.length() > 0); } } // end anon namespace namespace image { mini_terrain_cache_map mini_terrain_cache; mini_terrain_cache_map mini_fogged_terrain_cache; mini_terrain_cache_map mini_highlighted_terrain_cache; static int last_index_ = 0; void flush_cache() { { images_.flush(); hexed_images_.flush(); tod_colored_images_.flush(); scaled_to_zoom_.flush(); scaled_to_hex_images_.flush(); brightened_images_.flush(); lit_images_.flush(); lit_scaled_images_.flush(); in_hex_info_.flush(); is_empty_hex_.flush(); mini_terrain_cache.clear(); mini_fogged_terrain_cache.clear(); mini_highlighted_terrain_cache.clear(); reversed_images_.clear(); image_existence_map.clear(); precached_dirs.clear(); } /* We can't reset last_index_, since some locators are still alive when using :refresh. That would cause them to point to the wrong images. Not resetting the variable causes a memory leak, though. */ // last_index_ = 0; } void locator::init_index() { auto i = locator_finder.find(val_); if(i == locator_finder.end()) { index_ = last_index_++; locator_finder.emplace(val_, index_); } else { index_ = i->second; } } void locator::parse_arguments() { std::string& fn = val_.filename_; if(fn.empty()) { return; } if(boost::algorithm::starts_with(fn, data_uri_prefix)) { parsed_data_URI parsed{fn}; if(!parsed.good) { utils::string_view view{ fn }; utils::string_view stripped = view.substr(0, view.find(",")); ERR_DP << "Invalid data URI: " << stripped << std::endl; } val_.is_data_uri_ = true; } std::size_t markup_field = fn.find('~'); if(markup_field != std::string::npos) { val_.type_ = SUB_FILE; val_.modifications_ = fn.substr(markup_field, fn.size() - markup_field); fn = fn.substr(0, markup_field); } } locator::locator() : index_(-1) , val_() { } locator::locator(const locator& a, const std::string& mods) : index_(-1) , val_(a.val_) { if(!mods.empty()) { val_.modifications_ += mods; val_.type_ = SUB_FILE; init_index(); } else { index_ = a.index_; } } locator::locator(const char* filename) : index_(-1) , val_(filename) { parse_arguments(); init_index(); } locator::locator(const std::string& filename) : index_(-1) , val_(filename) { parse_arguments(); init_index(); } locator::locator(const std::string& filename, const std::string& modifications) : index_(-1) , val_(filename, modifications) { init_index(); } locator::locator(const std::string& filename, const map_location& loc, int center_x, int center_y, const std::string& modifications) : index_(-1) , val_(filename, loc, center_x, center_y, modifications) { init_index(); } locator& locator::operator=(const locator& a) { index_ = a.index_; val_ = a.val_; return *this; } locator::value::value() : type_(NONE) , is_data_uri_(false) , filename_() , loc_() , modifications_() , center_x_(0) , center_y_(0) { } locator::value::value(const char* filename) : type_(FILE) , is_data_uri_(false) , filename_(filename) , loc_() , modifications_() , center_x_(0) , center_y_(0) { } locator::value::value(const std::string& filename) : type_(FILE) , is_data_uri_(false) , filename_(filename) , loc_() , modifications_() , center_x_(0) , center_y_(0) { } locator::value::value(const std::string& filename, const std::string& modifications) : type_(SUB_FILE) , is_data_uri_(false) , filename_(filename) , loc_() , modifications_(modifications) , center_x_(0) , center_y_(0) { } locator::value::value(const std::string& filename, const map_location& loc, int center_x, int center_y, const std::string& modifications) : type_(SUB_FILE) , is_data_uri_(false) , filename_(filename) , loc_(loc) , modifications_(modifications) , center_x_(center_x) , center_y_(center_y) { } bool locator::value::operator==(const value& a) const { if(a.type_ != type_) { return false; } else if(type_ == FILE) { return filename_ == a.filename_; } else if(type_ == SUB_FILE) { return filename_ == a.filename_ && loc_ == a.loc_ && modifications_ == a.modifications_ && center_x_ == a.center_x_ && center_y_ == a.center_y_; } return false; } bool locator::value::operator<(const value& a) const { if(type_ != a.type_) { return type_ < a.type_; } else if(type_ == FILE) { return filename_ < a.filename_; } else if(type_ == SUB_FILE) { if(filename_ != a.filename_) return filename_ < a.filename_; if(loc_ != a.loc_) return loc_ < a.loc_; if(center_x_ != a.center_x_) return center_x_ < a.center_x_; if(center_y_ != a.center_y_) return center_y_ < a.center_y_; return (modifications_ < a.modifications_); } return false; } // Load overlay image and compose it with the original surface. static void add_localized_overlay(const std::string& ovr_file, surface& orig_surf) { filesystem::rwops_ptr rwops = filesystem::make_read_RWops(ovr_file); surface ovr_surf = IMG_Load_RW(rwops.release(), true); // SDL takes ownership of rwops if(!ovr_surf) { return; } SDL_Rect area {0, 0, ovr_surf->w, ovr_surf->h}; sdl_blit(ovr_surf, 0, orig_surf, &area); } static surface load_image_file(const image::locator& loc) { surface res; std::string location = filesystem::get_binary_file_location("images", loc.get_filename()); { if(!location.empty()) { // Check if there is a localized image. const std::string loc_location = filesystem::get_localized_path(location); if(!loc_location.empty()) { location = loc_location; } filesystem::rwops_ptr rwops = filesystem::make_read_RWops(location); res = IMG_Load_RW(rwops.release(), true); // SDL takes ownership of rwops // If there was no standalone localized image, check if there is an overlay. if(res && loc_location.empty()) { const std::string ovr_location = filesystem::get_localized_path(location, "--overlay"); if(!ovr_location.empty()) { add_localized_overlay(ovr_location, res); } } } } if(!res && !loc.get_filename().empty()) { ERR_DP << "could not open image '" << loc.get_filename() << "'" << std::endl; if(game_config::debug && loc.get_filename() != game_config::images::missing) return get_image(game_config::images::missing, UNSCALED); } return res; } static surface load_image_sub_file(const image::locator& loc) { surface surf = get_image(loc.get_filename(), UNSCALED); if(surf == nullptr) { return nullptr; } modification_queue mods = modification::decode(loc.get_modifications()); while(!mods.empty()) { modification* mod = mods.top(); try { surf = (*mod)(surf); } catch(const image::modification::imod_exception& e) { std::ostringstream ss; ss << "\n"; for(const std::string& mod_name : utils::parenthetical_split(loc.get_modifications(), '~')) { ss << "\t" << mod_name << "\n"; } ERR_CFG << "Failed to apply a modification to an image:\n" << "Image: " << loc.get_filename() << "\n" << "Modifications: " << ss.str() << "\n" << "Error: " << e.message << "\n"; } // NOTE: do this *after* applying the mod or you'll get crashes! mods.pop(); } if(loc.get_loc().valid()) { SDL_Rect srcrect = sdl::create_rect( ((tile_size * 3) / 4) * loc.get_loc().x, tile_size * loc.get_loc().y + (tile_size / 2) * (loc.get_loc().x % 2), tile_size, tile_size ); if(loc.get_center_x() >= 0 && loc.get_center_y() >= 0) { srcrect.x += surf->w / 2 - loc.get_center_x(); srcrect.y += surf->h / 2 - loc.get_center_y(); } // cut and hex mask, but also check and cache if empty result surface cut(cut_surface(surf, srcrect)); bool is_empty = false; surf = mask_surface(cut, get_hexmask(), &is_empty); // discard empty images to free memory if(is_empty) { // Safe because those images are only used by terrain rendering // and it filters them out. // A safer and more general way would be to keep only one copy of it surf = nullptr; } loc.add_to_cache(is_empty_hex_, is_empty); } return surf; } static surface load_image_data_uri(const image::locator& loc) { surface surf; parsed_data_URI parsed{loc.get_filename()}; if(!parsed.good) { utils::string_view fn = loc.get_filename(); utils::string_view stripped = fn.substr(0, fn.find(",")); ERR_DP << "Invalid data URI: " << stripped << std::endl; } else if(parsed.mime.substr(0, 5) != "image") { ERR_DP << "Data URI not of image MIME type: " << parsed.mime << std::endl; } else { const std::vector image_data = base64::decode(parsed.data); filesystem::rwops_ptr rwops{SDL_RWFromConstMem(image_data.data(), image_data.size()), &SDL_FreeRW}; if(image_data.empty()) { ERR_DP << "Invalid encoding in data URI" << std::endl; } else if(parsed.mime == "image/png") { surf = IMG_LoadTyped_RW(rwops.release(), true, "PNG"); } else if(parsed.mime == "image/jpeg") { surf = IMG_LoadTyped_RW(rwops.release(), true, "JPG"); } else { ERR_DP << "Invalid image MIME type: " << parsed.mime << std::endl; } } return surf; } // small utility function to store an int from (-256,254) to an signed char static signed char col_to_uchar(int i) { return static_cast(std::min(127, std::max(-128, i / 2))); } light_string get_light_string(int op, int r, int g, int b) { light_string ls; ls.reserve(4); ls.push_back(op); ls.push_back(col_to_uchar(r)); ls.push_back(col_to_uchar(g)); ls.push_back(col_to_uchar(b)); return ls; } static surface apply_light(surface surf, const light_string& ls) { // atomic lightmap operation are handled directly (important to end recursion) if(ls.size() == 4) { // if no lightmap (first char = -1) then we need the initial value //(before the halving done for lightmap) int m = ls[0] == -1 ? 2 : 1; return adjust_surface_color(surf, ls[1] * m, ls[2] * m, ls[3] * m); } // check if the lightmap is already cached or need to be generated surface lightmap = nullptr; auto i = lightmaps_.find(ls); if(i != lightmaps_.end()) { lightmap = i->second; } else { // build all the paths for lightmap sources static const std::string p = "terrain/light/light"; static const std::string lm_img[19] { p + ".png", p + "-concave-2-tr.png", p + "-concave-2-r.png", p + "-concave-2-br.png", p + "-concave-2-bl.png", p + "-concave-2-l.png", p + "-concave-2-tl.png", p + "-convex-br-bl.png", p + "-convex-bl-l.png", p + "-convex-l-tl.png", p + "-convex-tl-tr.png", p + "-convex-tr-r.png", p + "-convex-r-br.png", p + "-convex-l-bl.png", p + "-convex-tl-l.png", p + "-convex-tr-tl.png", p + "-convex-r-tr.png", p + "-convex-br-r.png", p + "-convex-bl-br.png" }; // decompose into atomic lightmap operations (4 chars) for(std::size_t c = 0; c + 3 < ls.size(); c += 4) { light_string sls = ls.substr(c, 4); // get the corresponding image and apply the lightmap operation to it // This allows to also cache lightmap parts. // note that we avoid infinite recursion by using only atomic operation surface lts = image::get_lighted_image(lm_img[sls[0]], sls, HEXED); // first image will be the base where we blit the others if(lightmap == nullptr) { // copy the cached image to avoid modifying the cache lightmap = lts.clone(); } else { sdl_blit(lts, nullptr, lightmap, nullptr); } } // cache the result lightmaps_[ls] = lightmap; } // apply the final lightmap return light_surface(surf, lightmap); } bool locator::file_exists() const { return val_.is_data_uri_ ? parsed_data_URI{val_.filename_}.good : !filesystem::get_binary_file_location("images", val_.filename_).empty(); } surface load_from_disk(const locator& loc) { switch(loc.get_type()) { case locator::FILE: if(loc.is_data_uri()){ return load_image_data_uri(loc); } else { return load_image_file(loc); } case locator::SUB_FILE: return load_image_sub_file(loc); default: return surface(nullptr); } } manager::manager() { } manager::~manager() { flush_cache(); } void set_color_adjustment(int r, int g, int b) { if(r != red_adjust || g != green_adjust || b != blue_adjust) { red_adjust = r; green_adjust = g; blue_adjust = b; tod_colored_images_.flush(); brightened_images_.flush(); lit_images_.flush(); lit_scaled_images_.flush(); reversed_images_.clear(); } } void set_zoom(unsigned int amount) { if(amount != zoom) { zoom = amount; tod_colored_images_.flush(); brightened_images_.flush(); reversed_images_.clear(); // We keep these caches if: // we use default zoom (it doesn't need those) // or if they are already at the wanted zoom. if(zoom != tile_size && zoom != cached_zoom) { scaled_to_zoom_.flush(); scaled_to_hex_images_.flush(); lit_scaled_images_.flush(); cached_zoom = zoom; } } } static surface get_hexed(const locator& i_locator) { surface image(get_image(i_locator, UNSCALED)); // hex cut tiles, also check and cache if empty result bool is_empty = false; surface res = mask_surface(image, get_hexmask(), &is_empty, i_locator.get_filename()); i_locator.add_to_cache(is_empty_hex_, is_empty); return res; } static surface get_scaled_to_hex(const locator& i_locator) { surface img = get_image(i_locator, HEXED); // return scale_surface(img, zoom, zoom); if(img) { return scale_surface_nn(img, zoom, zoom); } return surface(nullptr); } static surface get_tod_colored(const locator& i_locator) { surface img = get_image(i_locator, SCALED_TO_HEX); return adjust_surface_color(img, red_adjust, green_adjust, blue_adjust); } static surface get_scaled_to_zoom(const locator& i_locator) { assert(zoom != tile_size); assert(tile_size != 0); surface res(get_image(i_locator, UNSCALED)); // For some reason haloes seems to have invalid images, protect against crashing if(res) { return scale_surface_nn(res, ((res->w * zoom) / tile_size), ((res->h * zoom) / tile_size)); } return surface(nullptr); } static surface get_brightened(const locator& i_locator) { surface image(get_image(i_locator, TOD_COLORED)); return brighten_image(image, ftofxp(game_config::hex_brightening)); } /// translate type to a simpler one when possible static TYPE simplify_type(const image::locator& i_locator, TYPE type) { switch(type) { case SCALED_TO_ZOOM: if(zoom == tile_size) { type = UNSCALED; } break; case BRIGHTENED: if(ftofxp(game_config::hex_brightening) == ftofxp(1.0)) { type = TOD_COLORED; } break; default: break; } if(type == TOD_COLORED) { if(red_adjust == 0 && green_adjust == 0 && blue_adjust == 0) { type = SCALED_TO_HEX; } } if(type == SCALED_TO_HEX) { if(zoom == tile_size) { type = HEXED; } } if(type == HEXED) { // check if the image is already hex-cut by the location system if(i_locator.get_loc().valid()) { type = UNSCALED; } } return type; } surface get_image(const image::locator& i_locator, TYPE type) { surface res; if(i_locator.is_void()) { return res; } type = simplify_type(i_locator, type); image_cache* imap; // select associated cache switch(type) { case UNSCALED: imap = &images_; break; case TOD_COLORED: imap = &tod_colored_images_; break; case SCALED_TO_ZOOM: imap = &scaled_to_zoom_; break; case HEXED: imap = &hexed_images_; break; case SCALED_TO_HEX: imap = &scaled_to_hex_images_; break; case BRIGHTENED: imap = &brightened_images_; break; default: return res; } // return the image if already cached bool tmp = i_locator.in_cache(*imap); if(tmp) { surface result = i_locator.locate_in_cache(*imap); return result; } // not cached, generate it switch(type) { case UNSCALED: // If type is unscaled, directly load the image from the disk. res = load_from_disk(i_locator); break; case TOD_COLORED: res = get_tod_colored(i_locator); break; case SCALED_TO_ZOOM: res = get_scaled_to_zoom(i_locator); break; case HEXED: res = get_hexed(i_locator); break; case SCALED_TO_HEX: res = get_scaled_to_hex(i_locator); break; case BRIGHTENED: res = get_brightened(i_locator); break; default: return res; } i_locator.add_to_cache(*imap, res); return res; } surface get_lighted_image(const image::locator& i_locator, const light_string& ls, TYPE type) { surface res; if(i_locator.is_void()) { return res; } if(type == SCALED_TO_HEX && zoom == tile_size) { type = HEXED; } // select associated cache lit_cache* imap = &lit_images_; if(type == SCALED_TO_HEX) { imap = &lit_scaled_images_; } // if no light variants yet, need to add an empty map if(!i_locator.in_cache(*imap)) { i_locator.add_to_cache(*imap, lit_variants()); } // need access to add it if not found { // enclose reference pointing to data stored in a changing vector const lit_variants& lvar = i_locator.locate_in_cache(*imap); auto lvi = lvar.find(ls); if(lvi != lvar.end()) { return lvi->second; } } // not cached yet, generate it switch(type) { case HEXED: res = get_image(i_locator, HEXED); res = apply_light(res, ls); break; case SCALED_TO_HEX: // we light before scaling to reuse the unscaled cache res = get_lighted_image(i_locator, ls, HEXED); res = scale_surface_nn(res, zoom, zoom); break; default: break; } // record the lighted surface in the corresponding variants cache i_locator.access_in_cache(*imap)[ls] = res; return res; } surface get_hexmask() { static const image::locator terrain_mask(game_config::images::terrain_mask); return get_image(terrain_mask, UNSCALED); } bool is_in_hex(const locator& i_locator) { bool result; { if(i_locator.in_cache(in_hex_info_)) { result = i_locator.locate_in_cache(in_hex_info_); } else { const surface image(get_image(i_locator, UNSCALED)); bool res = in_mask_surface(image, get_hexmask()); i_locator.add_to_cache(in_hex_info_, res); // std::cout << "in_hex : " << i_locator.get_filename() // << " " << (res ? "yes" : "no") << "\n"; result = res; } } return result; } bool is_empty_hex(const locator& i_locator) { if(!i_locator.in_cache(is_empty_hex_)) { const surface surf = get_image(i_locator, HEXED); // emptiness of terrain image is checked during hex cut // so, maybe in cache now, let's recheck if(!i_locator.in_cache(is_empty_hex_)) { // should never reach here // but do it manually if it happens // assert(false); bool is_empty = false; mask_surface(surf, get_hexmask(), &is_empty); i_locator.add_to_cache(is_empty_hex_, is_empty); } } return i_locator.locate_in_cache(is_empty_hex_); } surface reverse_image(const surface& surf) { if(surf == nullptr) { return surface(nullptr); } const auto itor = reversed_images_.find(surf); if(itor != reversed_images_.end()) { // sdl_add_ref(itor->second); return itor->second; } const surface rev(flip_surface(surf)); if(rev == nullptr) { return surface(nullptr); } reversed_images_.emplace(surf, rev); // sdl_add_ref(rev); return rev; } bool exists(const image::locator& i_locator) { typedef image::locator loc; loc::type type = i_locator.get_type(); if(type != loc::FILE && type != loc::SUB_FILE) { return false; } // The insertion will fail if there is already an element in the cache // and this will point to the existing element. auto iter = image_existence_map.begin(); bool success; std::tie(iter, success) = image_existence_map.emplace(i_locator.get_filename(), false); bool& cache = iter->second; if(success) { if(i_locator.is_data_uri()) { cache = parsed_data_URI{i_locator.get_filename()}.good; } else { cache = !filesystem::get_binary_file_location("images", i_locator.get_filename()).empty(); } } return cache; } static void precache_file_existence_internal(const std::string& dir, const std::string& subdir) { const std::string checked_dir = dir + "/" + subdir; if(precached_dirs.find(checked_dir) != precached_dirs.end()) { return; } precached_dirs.insert(checked_dir); if(!filesystem::is_directory(checked_dir)) { return; } std::vector files_found; std::vector dirs_found; filesystem::get_files_in_dir(checked_dir, &files_found, &dirs_found, filesystem::name_mode::FILE_NAME_ONLY, filesystem::filter_mode::NO_FILTER, filesystem::reorder_mode::DONT_REORDER); for(const auto& f : files_found) { image_existence_map[subdir + f] = true; } for(const auto& d : dirs_found) { precache_file_existence_internal(dir, subdir + d + "/"); } } void precache_file_existence(const std::string& subdir) { const std::vector& paths = filesystem::get_binary_paths("images"); for(const auto& p : paths) { precache_file_existence_internal(p, subdir); } } bool precached_file_exists(const std::string& file) { const auto b = image_existence_map.find(file); if(b != image_existence_map.end()) { return b->second; } return false; } save_result save_image(const locator& i_locator, const std::string& filename) { return save_image(get_image(i_locator), filename); } save_result save_image(const surface& surf, const std::string& filename) { if(!surf) { return save_result::no_image; } if(filesystem::ends_with(filename, ".jpeg") || filesystem::ends_with(filename, ".jpg") || filesystem::ends_with(filename, ".jpe")) { LOG_DP << "Writing a JPG image to " << filename << std::endl; const int err = IMG_SaveJPG_RW(surf, filesystem::make_write_RWops(filename).release(), true, 75); // SDL takes ownership of the RWops return err == 0 ? save_result::success : save_result::save_failed; } if(filesystem::ends_with(filename, ".png")) { LOG_DP << "Writing a PNG image to " << filename << std::endl; const int err = IMG_SavePNG_RW(surf, filesystem::make_write_RWops(filename).release(), true); // SDL takes ownership of the RWops return err == 0 ? save_result::success : save_result::save_failed; } if(filesystem::ends_with(filename, ".bmp")) { LOG_DP << "Writing a BMP image to " << filename << std::endl; const int err = SDL_SaveBMP(surf, filename.c_str()) == 0; return err == 0 ? save_result::success : save_result::save_failed; } return save_result::unsupported_format; } } // end namespace image