diff --git a/Makefile b/Makefile index b14ee92025..1510406c89 100644 --- a/Makefile +++ b/Makefile @@ -157,6 +157,11 @@ ifeq ($(HAVE_SDL_IMAGE), 1) DEFINES += $(SDL_IMAGE_CFLAGS) endif +ifeq ($(HAVE_LIBPNG), 1) + LIBS += $(LIBPNG_LIBS) + DEFINES += $(LIBPNG_CFLAGS) +endif + ifeq ($(HAVE_FFMPEG), 1) OBJ += record/ffemu.o LIBS += $(AVCODEC_LIBS) $(AVFORMAT_LIBS) $(AVUTIL_LIBS) $(SWSCALE_LIBS) diff --git a/config.features.h b/config.features.h index 6060a4ab73..d05067885d 100644 --- a/config.features.h +++ b/config.features.h @@ -110,6 +110,12 @@ static const bool _sdl_image_supp = true; static const bool _sdl_image_supp = false; #endif +#ifdef HAVE_LIBPNG +static const bool _libpng_supp = true; +#else +static const bool _libpng_supp = false; +#endif + #ifdef HAVE_FBO static const bool _fbo_supp = true; #else diff --git a/qb/config.libs.sh b/qb/config.libs.sh index b5cefda93b..8c352df57e 100644 --- a/qb/config.libs.sh +++ b/qb/config.libs.sh @@ -81,6 +81,7 @@ fi check_pkgconf XML libxml-2.0 check_pkgconf SDL_IMAGE SDL_image +check_pkgconf LIBPNG libpng if [ "$HAVE_THREADS" != 'no' ]; then if [ "$HAVE_FFMPEG" != 'no' ]; then @@ -128,6 +129,6 @@ check_pkgconf PYTHON python3 add_define_make OS "$OS" # Creates config.mk and config.h. -VARS="ALSA OSS OSS_BSD OSS_LIB AL RSOUND ROAR JACK COREAUDIO PULSE SDL OPENGL DYLIB GETOPT_LONG THREADS CG XML SDL_IMAGE DYNAMIC FFMPEG AVCODEC AVFORMAT AVUTIL SWSCALE CONFIGFILE FREETYPE XVIDEO X11 XEXT NETPLAY NETWORK_CMD SOCKET_LEGACY FBO STRL PYTHON FFMPEG_ALLOC_CONTEXT3 FFMPEG_AVCODEC_OPEN2 FFMPEG_AVIO_OPEN FFMPEG_AVFORMAT_WRITE_HEADER FFMPEG_AVFORMAT_NEW_STREAM FFMPEG_AVCODEC_ENCODE_AUDIO2 FFMPEG_AVCODEC_ENCODE_VIDEO2 X264RGB SINC BSV_MOVIE" +VARS="ALSA OSS OSS_BSD OSS_LIB AL RSOUND ROAR JACK COREAUDIO PULSE SDL OPENGL DYLIB GETOPT_LONG THREADS CG XML SDL_IMAGE LIBPNG DYNAMIC FFMPEG AVCODEC AVFORMAT AVUTIL SWSCALE CONFIGFILE FREETYPE XVIDEO X11 XEXT NETPLAY NETWORK_CMD SOCKET_LEGACY FBO STRL PYTHON FFMPEG_ALLOC_CONTEXT3 FFMPEG_AVCODEC_OPEN2 FFMPEG_AVIO_OPEN FFMPEG_AVFORMAT_WRITE_HEADER FFMPEG_AVFORMAT_NEW_STREAM FFMPEG_AVCODEC_ENCODE_AUDIO2 FFMPEG_AVCODEC_ENCODE_VIDEO2 X264RGB SINC BSV_MOVIE" create_config_make config.mk $VARS -create_config_header config.h $VARS \ No newline at end of file +create_config_header config.h $VARS diff --git a/qb/config.params.sh b/qb/config.params.sh index 3a2f311363..676e53b7e8 100644 --- a/qb/config.params.sh +++ b/qb/config.params.sh @@ -21,6 +21,7 @@ HAVE_PULSE=auto # Enable PulseAudio support HAVE_FREETYPE=auto # Enable FreeType support HAVE_XVIDEO=auto # Enable XVideo support HAVE_SDL_IMAGE=auto # Enable SDL_image support +HAVE_LIBPNG=auto # Enable libpng support HAVE_PYTHON=auto # Enable Python 3 support for shaders HAVE_SINC=yes # Disable SINC resampler HAVE_BSV_MOVIE=yes # Disable BSV movie support diff --git a/retroarch.c b/retroarch.c index 80346d3dcd..2062d2803c 100644 --- a/retroarch.c +++ b/retroarch.c @@ -495,6 +495,7 @@ static void print_features(void) _PSUPP(cg, "Cg", "Cg pixel shaders"); _PSUPP(xml, "XML", "bSNES XML pixel shaders"); _PSUPP(sdl_image, "SDL_image", "SDL_image image loading"); + _PSUPP(libpng, "libpng", "libpng screenshot support"); _PSUPP(fbo, "FBO", "OpenGL render-to-texture (multi-pass shaders)"); _PSUPP(dynamic, "Dynamic", "Dynamic run-time loading of libretro library"); _PSUPP(ffmpeg, "FFmpeg", "On-the-fly recording of gameplay with libavcodec"); diff --git a/screenshot.c b/screenshot.c index 5be6b7d8df..b111453838 100644 --- a/screenshot.c +++ b/screenshot.c @@ -22,9 +22,80 @@ #include #include "general.h" -// Simple 24bpp .BMP writer. +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif -static void write_header(FILE *file, unsigned width, unsigned height) +#ifdef HAVE_LIBPNG +#include +#endif + +#ifdef HAVE_LIBPNG +static png_structp png_ptr; +static png_infop png_info_ptr; + +static void destroy_png(void) +{ + if (png_ptr) + png_destroy_write_struct(&png_ptr, &png_info_ptr); +} + +static bool write_header_png(FILE *file, unsigned width, unsigned height) +{ + png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png_ptr) + return false; + + if (setjmp(png_jmpbuf(png_ptr))) + goto error; + + png_info_ptr = png_create_info_struct(png_ptr); + if (!png_info_ptr) + goto error; + + png_init_io(png_ptr, file); + + png_set_IHDR(png_ptr, png_info_ptr, width, height, 8, + PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + png_write_info(png_ptr, png_info_ptr); + png_set_compression_level(png_ptr, 9); + + return true; + +error: + destroy_png(); + return false; +} + +static void dump_lines_png(uint8_t **lines, int height) +{ + if (setjmp(png_jmpbuf(png_ptr))) + { + RARCH_ERR("PNG: dump_lines_png() failed!\n"); + goto end; + } + + // PNG is top-down, BMP is bottom-up. + for (int i = 0, j = height - 1; i < j; i++, j--) + { + uint8_t *tmp = lines[i]; + lines[i] = lines[j]; + lines[j] = tmp; + } + + png_set_rows(png_ptr, png_info_ptr, lines); + png_write_png(png_ptr, png_info_ptr, PNG_TRANSFORM_BGR, NULL); + png_write_end(png_ptr, NULL); + +end: + destroy_png(); +} + +#else + +static bool write_header_bmp(FILE *file, unsigned width, unsigned height) { unsigned line_size = (width * 3 + 3) & ~3; unsigned size = line_size * height + 54; @@ -49,9 +120,17 @@ static void write_header(FILE *file, unsigned width, unsigned height) 0, 0, 0, 0 }; - fwrite(header, 1, sizeof(header), file); + return fwrite(header, 1, sizeof(header), file) == sizeof(header); } +static void dump_lines_file(FILE *file, uint8_t **lines, size_t line_size, unsigned height) +{ + for (unsigned i = 0; i < height; i++) + fwrite(lines[i], 1, line_size, file); +} + +#endif + static void dump_line_bgr(uint8_t *line, const uint8_t *src, unsigned width) { memcpy(line, src, width * 3); @@ -78,31 +157,42 @@ static void dump_content(FILE *file, const void *frame, const uint16_t *frame16 = (const uint16_t*)frame; if (!bgr24) - pitch /= 2; + pitch /= sizeof(uint16_t); - unsigned line_size = (width * 3 + 3) & ~3; - uint8_t *line = (uint8_t*)calloc(1, line_size); - if (!line) + uint8_t **lines = (uint8_t**)calloc(height, sizeof(uint8_t*)); + if (!lines) return; + size_t line_size = (width * 3 + 3) & ~3; + + for (int i = 0; i < height; i++) + { + lines[i] = (uint8_t*)calloc(1, line_size); + if (!lines[i]) + goto end; + } + if (bgr24) // BGR24 byte order. Can directly copy. { for (int j = 0; j < height; j++, frame_bgr += pitch) - { - dump_line_bgr(line, frame_bgr, width); - fwrite(line, 1, line_size, file); - } + dump_line_bgr(lines[j], frame_bgr, width); } else // ARGB1555 { for (int j = 0; j < height; j++, frame16 += pitch) - { - dump_line_16(line, frame16, width); - fwrite(line, 1, line_size, file); - } + dump_line_16(lines[j], frame16, width); } - free(line); +#ifdef HAVE_LIBPNG + dump_lines_png(lines, height); +#else + dump_lines_file(file, lines, line_size, height); +#endif + +end: + for (int i = 0; i < height; i++) + free(lines[i]); + free(lines); } bool screenshot_dump(const char *folder, const void *frame, @@ -111,8 +201,14 @@ bool screenshot_dump(const char *folder, const void *frame, time_t cur_time; time(&cur_time); +#ifdef HAVE_LIBPNG +#define IMG_EXT "png" +#else +#define IMG_EXT "bmp" +#endif + char timefmt[128]; - strftime(timefmt, sizeof(timefmt), "RetroArch-%m%d-%H%M%S.bmp", localtime(&cur_time)); + strftime(timefmt, sizeof(timefmt), "RetroArch-%m%d-%H%M%S." IMG_EXT, localtime(&cur_time)); char filename[PATH_MAX]; snprintf(filename, sizeof(filename), "%s/%s", folder, timefmt); @@ -124,11 +220,18 @@ bool screenshot_dump(const char *folder, const void *frame, return false; } - write_header(file, width, height); - dump_content(file, frame, width, height, pitch, bgr24); +#ifdef HAVE_LIBPNG + bool ret = write_header_png(file, width, height); +#else + bool ret = write_header_bmp(file, width, height); +#endif + + if (ret) + dump_content(file, frame, width, height, pitch, bgr24); + else + RARCH_ERR("Failed to write image header.\n"); fclose(file); - - return true; + return ret; }