diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e3956b588d..865326cb40c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,6 +85,7 @@ option(ENABLE_LOW_MEM "Reduce memory usage by removing extra functionality" OFF) option(ENABLE_OMP "Enables OpenMP, and has additional dependencies" OFF) option(ENABLE_PANDORA "Add support for the OpenPandora by deactivating libvorbis support" OFF) option(ENABLE_SDL_GPU "Enable building with SDL_gpu (experimental" OFF) +option(ENABLE_LIBPNG "Enable support for writing png files (screenshots, images)" ON) if(NOT DEFINED ENABLE_DISPLAY_REVISION) # Test whether the code is used in a repository if not autorevision will @@ -586,6 +587,12 @@ if(ENABLE_GAME) endif(LIBDBUS_FOUND) endif(ENABLE_NOTIFICATIONS) + find_package( PNG ) + if(ENABLE_LIBPNG AND PNG_FOUND) + add_definitions(-DHAVE_LIBPNG) + else + message("Could not find lib PNG. Disabling support for writing PNG images.") + endif() endif(ENABLE_GAME) if(ENABLE_POT_UPDATE_TARGET) diff --git a/SConstruct b/SConstruct index e8420044b61..4744f60e4df 100755 --- a/SConstruct +++ b/SConstruct @@ -74,6 +74,7 @@ opts.AddVariables( BoolVariable('notifications', 'Enable support for desktop notifications', True), BoolVariable('nls','enable compile/install of gettext message catalogs',True), BoolVariable('boostfilesystem', 'Use boost filesystem', True), + BoolVariable('png', 'Clear to disable writing png files for screenshots, images', True), PathVariable('prefix', 'autotools-style installation prefix', "/usr/local", PathVariable.PathAccept), PathVariable('prefsdir', 'user preferences directory', "", PathVariable.PathAccept), PathVariable('default_prefs_file', 'default preferences file name', "", PathVariable.PathAccept), @@ -398,6 +399,10 @@ if env["prereqs"]: if client_env['fribidi']: client_env['fribidi'] = conf.CheckPKG('fribidi >= 0.10.9') or Warning("Can't find libfribidi, disabling freebidi support.") + env["png"] = env["png"] and conf.CheckLib("png") + if env["png"]: + client_env.Append(CPPDEFINES = ["HAVE_LIBPNG"]) + if env["forum_user_handler"]: flags = env.ParseFlags("!mysql_config --libs --cflags") try: # Some versions of mysql_config add -DNDEBUG but we don't want it diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8babbd2bcfc..da618e131c3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -365,6 +365,14 @@ if(LIBDBUS_FOUND) ) endif() +if(PNG_FOUND) + set_sources_files_properties( + SDL_SavePNG/savepng.cpp + PROPERTIES + COMPILE_FLAGS "${CXX_FLAG_NO_OLD_STYLE_CAST}" + ) +endif() + ########### Helper libraries ############### set(wesnoth-sdl_SRC @@ -1026,6 +1034,14 @@ if(LIBDBUS_FOUND) ) endif(LIBDBUS_FOUND) +# If requested, compile and link this file also to be able to save png files +if(ENABLE_LIBPNG AND PNG_FOUND) + set(libwesnoth-game_STAT_SRC + ${libwesnoth-game_STAT_SRC} + SDL_SavePNG/savepng.cpp + ) +endif(ENABLE_LIBPNG AND PNG_FOUND) + if(ENABLE_GAME AND ENABLE_TESTS) set(libwesnoth-game_STAT_SRC ${libwesnoth-game_STAT_SRC} diff --git a/src/SConscript b/src/SConscript index d3d2d671aac..68b45c4b7e8 100644 --- a/src/SConscript +++ b/src/SConscript @@ -571,6 +571,9 @@ if env["PLATFORM"] == "win32": if env["notifications"]: wesnoth_sources.append("desktop/dbus_notification.cpp") +if env["png"]: + wesnoth_sources.append("SDL_SavePNG/savepng.cpp") + wesnoth_sources.extend(client_env.Object("game_preferences_display.cpp", EXTRA_DEFINE = env["PLATFORM"] != "win32" and "WESNOTH_PREFIX='\"$prefix\"'" or None)) libwesnoth_extras = client_env.Library("wesnoth_extras", wesnoth_sources) diff --git a/src/SDL_SavePNG/README.md b/src/SDL_SavePNG/README.md new file mode 100644 index 00000000000..cf5b0f2ef3f --- /dev/null +++ b/src/SDL_SavePNG/README.md @@ -0,0 +1,101 @@ +# SDL_SavePNG + +Minimal libpng interface to save SDL_Surfaces as PNG files. + +You might want to take a look in "savepng.h" - it is much shorter and simpler +than this README. + +## Install + +Add "savepng.c" and "savepng.h" to your project. + +Link the libpng library, i.e. add the `-lpng` LDFLAG (even if you already have +`-lSDL_image`). + +## Use + +``` +#include "savepng.h" + +SDL_Surface *bmp = ... //your surface +if (SDL_SavePNG(bmp, "image.png")) { //boring way with error checking + printf("Unable to save png -- %s\n", SDL_GetError()); +} +``` + +As you can see, `SDL_SavePNG` accepts an SDL_Surface and a filename for it's +input. Similar to SDL_SaveBMP, it is a wrapper around the actual RWops-based +`SDL_SavePNG_RW` function, so you could use that, if needed. + +Lastly, there is `SDL_PNGFormatAlpha`, modeled after SDL_DisplayFormatAlpha, +that would convert *any SDL_Surface* to an *SDL_Surface suitable for PNG +output*. Each call to `SDL_PNGFormatAlpha` produces a **new** SDL_Surface that +**must** be freed using `SDL_FreeSurface`. + +``` +//safest way, usefull for 'screen' surface +SDL_Surface *tmp = SDL_PNGFormatAlpha(screen); +SDL_SavePNG(tmp, "screenshot.png"); +SDL_FreeSurface(tmp) +``` + +Such conversion is actually only required for *one* surface format (see below), +and would do **nothing** for all other formats, making it **very fast**. The +format in question is: + +### 32-bpp surfaces without alpha + +There is a interesting caveat of combining naive libpng and cunning SDL in a +32-bpp video mode. + +The *screen* surface (obtained by `SDL_SetVideoMode` or similarly) might (and +will!) ignore it's alpha-component even in the 32bpp mode. Meaning that an +0xAARRGGBB color would be blitted as 0xFFrrggbb irregardless, as if it was a +24bpp color. + +Since screen itself is never blitted onto anything else, ignoring the alpha +makes perfect sense. However, unlike 24bpp images, the alpha component *does* +exist. Thus, when such surface is saved, it appears to be completely +transparent, as the alpha values for each pixel are set to 0. + +Depending on your video mode, you might or might not need to first convert your +surface using `SDL_PNGFormatAlpha`. If you have absolute control over the video +surface, you can force it to 24bpp (or less) mode, which would avoid the +problem. + +If the surface passed to `SDL_PNGFormatAlpha` is already suitable, a no-op is +performed. It is very fast, so you should probably always convert your surfaces +before saving. + +### No text chunks + +Unfortunately, a simplistic interface such as SDL_SavePNG provides no means to +write PNG meta-data. If you need to add iTXT chunks to your PNGs, you would +have to modify this code or write your own version. + +If you have some kind of simple API, that would be thematically consistent with +SDL, in mind -- please share. + +## Demo + +See `main.c` and `Makefile` for an example program. It too is shorter than this +README. + +# About + +The problem in question is very simple, and this little piece of functionality +was implemented and re-implemented multiple times by multiple authors (notably, +Angelo "Encelo" Theodorou and Darren Grant, among others). I decided to write +my own version to ensure it's correctness, learn more about libpng, and to +provide a copy-pastable, maintained, libpng15-aware, palette-supporting +variation that I could link to. You can view it as a continuation of their +efforts. + +SDL_Image would've been perfect place for this, but that library has different +purposes. + +*Next up: code to load SDL_Surfaces as OpenGL 1.1 textures. J/K ;)* + +# Copying + +SDL_SavePNG is available under the zlib/libpng license. \ No newline at end of file diff --git a/src/SDL_SavePNG/savepng.cpp b/src/SDL_SavePNG/savepng.cpp new file mode 100644 index 00000000000..8bd33a0eea1 --- /dev/null +++ b/src/SDL_SavePNG/savepng.cpp @@ -0,0 +1,157 @@ +/* + * SDL_SavePNG -- libpng-based SDL_Surface writer. + * + * This code is free software, available under zlib/libpng license. + * http://www.libpng.org/pub/png/src/libpng-LICENSE.txt + */ +#include +#include + +#include "savepng.h" + +#define SUCCESS 0 +#define ERROR -1 + +#define USE_ROW_POINTERS + +#if SDL_BYTEORDER == SDL_BIG_ENDIAN +#define rmask 0xFF000000 +#define gmask 0x00FF0000 +#define bmask 0x0000FF00 +#define amask 0x000000FF +#else +#define rmask 0x000000FF +#define gmask 0x0000FF00 +#define bmask 0x00FF0000 +#define amask 0xFF000000 +#endif + +/* libpng callbacks */ +static void png_error_SDL(png_structp /*ctx*/, png_const_charp str) +{ + SDL_SetError("libpng: %s\n", str); +} +static void png_write_SDL(png_structp png_ptr, png_bytep data, png_size_t length) +{ + SDL_RWops *rw = (SDL_RWops*)png_get_io_ptr(png_ptr); + SDL_RWwrite(rw, data, sizeof(png_byte), length); +} + +SDL_Surface *SDL_PNGFormatAlpha(SDL_Surface *src) +{ + SDL_Surface *surf; + SDL_Rect rect = { 0 , 0 , 0 , 0 }; + + /* NO-OP for images < 32bpp and 32bpp images that already have Alpha channel */ + if (src->format->BitsPerPixel <= 24 || src->format->Amask) { + src->refcount++; + return src; + } + + /* Convert 32bpp alpha-less image to 24bpp alpha-less image */ + rect.w = src->w; + rect.h = src->h; + surf = SDL_CreateRGBSurface(src->flags, src->w, src->h, 24, + src->format->Rmask, src->format->Gmask, src->format->Bmask, 0); + SDL_LowerBlit(src, &rect, surf, &rect); + + return surf; +} + +int SDL_SavePNG_RW(SDL_Surface *surface, SDL_RWops *dst, int freedst) +{ + png_structp png_ptr; + png_infop info_ptr; + png_colorp pal_ptr; + SDL_Palette *pal; + int i, colortype; +#ifdef USE_ROW_POINTERS + png_bytep *row_pointers; +#endif + /* Initialize and do basic error checking */ + if (!dst) + { + SDL_SetError("Argument 2 to SDL_SavePNG_RW can't be NULL, expecting SDL_RWops*\n"); + if (freedst) SDL_RWclose(dst); + return (ERROR); + } + if (!surface) + { + SDL_SetError("Argument 1 to SDL_SavePNG_RW can't be NULL, expecting SDL_Surface*\n"); + if (freedst) SDL_RWclose(dst); + return (ERROR); + } + png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, png_error_SDL, NULL); /* err_ptr, err_fn, warn_fn */ + if (!png_ptr) + { + SDL_SetError("Unable to png_create_write_struct on %s\n", PNG_LIBPNG_VER_STRING); + if (freedst) SDL_RWclose(dst); + return (ERROR); + } + info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) + { + SDL_SetError("Unable to png_create_info_struct\n"); + png_destroy_write_struct(&png_ptr, NULL); + if (freedst) SDL_RWclose(dst); + return (ERROR); + } + if (setjmp(png_jmpbuf(png_ptr))) /* All other errors, see also "png_error_SDL" */ + { + png_destroy_write_struct(&png_ptr, &info_ptr); + if (freedst) SDL_RWclose(dst); + return (ERROR); + } + + /* Setup our RWops writer */ + png_set_write_fn(png_ptr, dst, png_write_SDL, NULL); /* w_ptr, write_fn, flush_fn */ + + /* Prepare chunks */ + colortype = PNG_COLOR_MASK_COLOR; + if (surface->format->BytesPerPixel > 0 + && surface->format->BytesPerPixel <= 8 + && (pal = surface->format->palette)) + { + colortype |= PNG_COLOR_MASK_PALETTE; + pal_ptr = (png_colorp)malloc(pal->ncolors * sizeof(png_color)); + for (i = 0; i < pal->ncolors; i++) { + pal_ptr[i].red = pal->colors[i].r; + pal_ptr[i].green = pal->colors[i].g; + pal_ptr[i].blue = pal->colors[i].b; + } + png_set_PLTE(png_ptr, info_ptr, pal_ptr, pal->ncolors); + free(pal_ptr); + } + else if (surface->format->BytesPerPixel > 3 || surface->format->Amask) + colortype |= PNG_COLOR_MASK_ALPHA; + + png_set_IHDR(png_ptr, info_ptr, surface->w, surface->h, 8, colortype, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + +// png_set_packing(png_ptr); + + /* Allow BGR surfaces */ + if (surface->format->Rmask == bmask + && surface->format->Gmask == gmask + && surface->format->Bmask == rmask) + png_set_bgr(png_ptr); + + /* Write everything */ + png_write_info(png_ptr, info_ptr); +#ifdef USE_ROW_POINTERS + row_pointers = (png_bytep*) malloc(sizeof(png_bytep)*surface->h); + for (i = 0; i < surface->h; i++) + row_pointers[i] = (png_bytep)(Uint8*)surface->pixels + i * surface->pitch; + png_write_image(png_ptr, row_pointers); + free(row_pointers); +#else + for (i = 0; i < surface->h; i++) + png_write_row(png_ptr, (png_bytep)(Uint8*)surface->pixels + i * surface->pitch); +#endif + png_write_end(png_ptr, info_ptr); + + /* Done */ + png_destroy_write_struct(&png_ptr, &info_ptr); + if (freedst) SDL_RWclose(dst); + return (SUCCESS); +} diff --git a/src/SDL_SavePNG/savepng.h b/src/SDL_SavePNG/savepng.h new file mode 100644 index 00000000000..1f97e32c3ec --- /dev/null +++ b/src/SDL_SavePNG/savepng.h @@ -0,0 +1,36 @@ +#ifndef _SDL_SAVEPNG +#define _SDL_SAVEPNG +/* + * SDL_SavePNG -- libpng-based SDL_Surface writer. + * + * This code is free software, available under zlib/libpng license. + * http://www.libpng.org/pub/png/src/libpng-LICENSE.txt + */ +#include +/* + * Save an SDL_Surface as a PNG file. + * + * Returns 0 success or -1 on failure, the error message is then retrievable + * via SDL_GetError(). + */ +#define SDL_SavePNG(surface, file) \ + SDL_SavePNG_RW(surface, SDL_RWFromFile(file, "wb"), 1) + +/* + * Save an SDL_Surface as a PNG file, using writable RWops. + * + * surface - the SDL_Surface structure containing the image to be saved + * dst - a data stream to save to + * freedst - non-zero to close the stream after being written + * + * Returns 0 success or -1 on failure, the error message is then retrievable + * via SDL_GetError(). + */ +extern int SDL_SavePNG_RW(SDL_Surface *surface, SDL_RWops *rw, int freedst); + +/* + * Return new SDL_Surface with a format suitable for PNG output. + */ +extern SDL_Surface *SDL_PNGFormatAlpha(SDL_Surface *src); + +#endif diff --git a/src/display.cpp b/src/display.cpp index 791dc5b4d3e..99f6882a55b 100644 --- a/src/display.cpp +++ b/src/display.cpp @@ -34,6 +34,7 @@ #include "play_controller.hpp" //note: this can probably be refactored out #include "reports.hpp" #include "resources.hpp" +#include "SDL_SavePNG/savepng.h" #include "synced_context.hpp" #include "team.hpp" #include "terrain_builder.hpp" @@ -770,7 +771,9 @@ int display::screenshot(std::string filename, bool map_screenshot) int size = 0; if (!map_screenshot) { surface screenshot_surf = screen_.getSurface(); - SDL_SaveBMP(screenshot_surf, filename.c_str()); + + image::save_image(screenshot_surf, filename); + size = screenshot_surf->w * screenshot_surf->h; } else { if (get_map().empty()) { @@ -802,7 +805,7 @@ int display::screenshot(std::string filename, bool map_screenshot) draw(true,true); // finally save the image on disk - SDL_SaveBMP(map_screenshot_surf_, filename.c_str()); + image::save_image(map_screenshot_surf_,filename); //NOTE: need to be sure that we free this huge surface (is it enough?) map_screenshot_surf_ = NULL; diff --git a/src/image.cpp b/src/image.cpp index e1695e5add3..a285ea0d67e 100644 --- a/src/image.cpp +++ b/src/image.cpp @@ -30,6 +30,11 @@ #include "log.hpp" #include "gettext.hpp" #include "sdl/rect.hpp" + +#ifdef HAVE_LIBPNG +#include "SDL_SavePNG/savepng.h" +#endif + #include "serialization/string_utils.hpp" #include "video.hpp" @@ -1192,7 +1197,23 @@ bool precached_file_exists(const std::string& file) void save_image(const locator & i_locator, const std::string & filename) { - surface surf = get_image(i_locator); + save_image(get_image(i_locator), filename); +} + +void save_image(const surface & surf, const std::string & filename) +{ + #ifdef HAVE_LIBPNG + if (! ((filename.length() > 3) && (filename.substr(filename.length()-3, 3) == "bmp"))) { + LOG_DP << "Writing a png image to " << filename << std::endl; + //safest way, + SDL_Surface *tmp = SDL_PNGFormatAlpha(surf.get()); + SDL_SavePNG(tmp, filename.c_str()); + SDL_FreeSurface(tmp); + return ; + } + #endif + + LOG_DP << "Writing a bmp image to " << filename << std::endl; SDL_SaveBMP(surf, filename.c_str()); } diff --git a/src/image.hpp b/src/image.hpp index 8c43918d86c..d6a4f7d4bca 100644 --- a/src/image.hpp +++ b/src/image.hpp @@ -251,6 +251,7 @@ namespace image { std::string describe_versions(); void save_image(const locator& i_locator, const std::string& outfile); + void save_image(const surface& surf, const std::string& outfile); } #endif