From ada13f9e9a297468cfdbda7d3ec721e598aa145c Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Sat, 6 Jul 2024 02:01:06 +0100 Subject: [PATCH] Take PNG screenshots by default in SDL2 builds PNG screenshots are also lossless and about half the size of the PCX screenshots. --- CMake/Definitions.cmake | 1 + CMakeLists.txt | 9 ++ Source/CMakeLists.txt | 11 ++ Source/capture.cpp | 173 ++++++++------------------------ Source/engine/palette.cpp | 3 + Source/utils/surface_to_pcx.cpp | 153 ++++++++++++++++++++++++++++ Source/utils/surface_to_pcx.hpp | 13 +++ Source/utils/surface_to_png.cpp | 27 +++++ Source/utils/surface_to_png.hpp | 13 +++ 9 files changed, 272 insertions(+), 131 deletions(-) create mode 100644 Source/utils/surface_to_pcx.cpp create mode 100644 Source/utils/surface_to_pcx.hpp create mode 100644 Source/utils/surface_to_png.cpp create mode 100644 Source/utils/surface_to_png.hpp diff --git a/CMake/Definitions.cmake b/CMake/Definitions.cmake index a444decdee3..a63be7ac822 100644 --- a/CMake/Definitions.cmake +++ b/CMake/Definitions.cmake @@ -93,6 +93,7 @@ foreach( DEVILUTIONX_DEFAULT_RESAMPLER STREAM_ALL_AUDIO_MIN_FILE_SIZE DEVILUTIONX_DISPLAY_TEXTURE_FORMAT + DEVILUTIONX_SCREENSHOT_FORMAT ) if(DEFINED ${def_name} AND NOT ${def_name} STREQUAL "") list(APPEND DEVILUTIONX_DEFINITIONS ${def_name}=${${def_name}}) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0518e8a054e..fef9d4bc15f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -113,6 +113,15 @@ if(NOT USE_SDL1) mark_as_advanced(DEVILUTIONX_DISPLAY_TEXTURE_FORMAT) endif() +if(USE_SDL1) + # SDL_image in SDL1 does not support PNG, making PCX the only option. + set(DEVILUTIONX_SCREENSHOT_FORMAT "DEVILUTIONX_SCREENSHOT_FORMAT_PCX") +else() + set(DEVILUTIONX_SCREENSHOT_FORMAT "DEVILUTIONX_SCREENSHOT_FORMAT_PNG" CACHE STRING "Screenshot format") + set_property(CACHE DEVILUTIONX_SCREENSHOT_FORMAT PROPERTY STRINGS "DEVILUTIONX_SCREENSHOT_FORMAT_PNG;DEVILUTIONX_SCREENSHOT_FORMAT_PCX") + mark_as_advanced(DEVILUTIONX_SCREENSHOT_FORMAT) +endif() + # Sound options option(NOSOUND "Disable sound support" OFF) option(DEVILUTIONX_RESAMPLER_SPEEX "Build with Speex resampler" ON) diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 4523712fdaa..058d30e9368 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -270,6 +270,17 @@ if(SCREEN_READER_INTEGRATION) ) endif() +if(DEVILUTIONX_SCREENSHOT_FORMAT STREQUAL DEVILUTIONX_SCREENSHOT_FORMAT_PCX) + list(APPEND libdevilutionx_SRCS + utils/surface_to_pcx.cpp + ) +endif() +if(DEVILUTIONX_SCREENSHOT_FORMAT STREQUAL DEVILUTIONX_SCREENSHOT_FORMAT_PNG) + list(APPEND libdevilutionx_SRCS + utils/surface_to_png.cpp + ) +endif() + add_devilutionx_library(libdevilutionx OBJECT ${libdevilutionx_SRCS}) target_include_directories(libdevilutionx PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/Source/capture.cpp b/Source/capture.cpp index a6d59805a73..5f8863f57d7 100644 --- a/Source/capture.cpp +++ b/Source/capture.cpp @@ -3,148 +3,56 @@ * * Implementation of the screenshot function. */ +#include #include #include +#include #include +#include +#include #include -#include "DiabloUI/diabloui.h" +#define DEVILUTIONX_SCREENSHOT_FORMAT_PCX 0 +#define DEVILUTIONX_SCREENSHOT_FORMAT_PNG 1 + +#if DEVILUTIONX_SCREENSHOT_FORMAT == DEVILUTIONX_SCREENSHOT_FORMAT_PCX +#include "utils/surface_to_pcx.hpp" +#endif +#if DEVILUTIONX_SCREENSHOT_FORMAT == DEVILUTIONX_SCREENSHOT_FORMAT_PNG +#include "utils/surface_to_png.hpp" +#endif + #include "engine/backbuffer_state.hpp" #include "engine/dx.h" #include "engine/palette.h" #include "utils/file_util.h" #include "utils/log.hpp" #include "utils/paths.h" -#include "utils/pcx.hpp" #include "utils/str_cat.hpp" -#include "utils/ui_fwd.h" namespace devilution { namespace { -/** - * @brief Write the PCX-file header - * @param width Image width - * @param height Image height - * @param out File stream to write to - * @return True on success - */ -bool CaptureHdr(int16_t width, int16_t height, FILE *out) -{ - PCXHeader buffer; - - memset(&buffer, 0, sizeof(buffer)); - buffer.Manufacturer = 10; - buffer.Version = 5; - buffer.Encoding = 1; - buffer.BitsPerPixel = 8; - buffer.Xmax = SDL_SwapLE16(width - 1); - buffer.Ymax = SDL_SwapLE16(height - 1); - buffer.HDpi = SDL_SwapLE16(width); - buffer.VDpi = SDL_SwapLE16(height); - buffer.NPlanes = 1; - buffer.BytesPerLine = SDL_SwapLE16(width); - - return std::fwrite(&buffer, sizeof(buffer), 1, out) == 1; -} - -/** - * @brief Write the current in-game palette to the PCX file - * @param palette Current palette - * @param out File stream for the PCX file. - * @return True if successful, else false - */ -bool CapturePal(SDL_Color *palette, FILE *out) -{ - uint8_t pcxPalette[1 + 256 * 3]; - - pcxPalette[0] = 12; - for (int i = 0; i < 256; i++) { - pcxPalette[1 + 3 * i + 0] = palette[i].r; - pcxPalette[1 + 3 * i + 1] = palette[i].g; - pcxPalette[1 + 3 * i + 2] = palette[i].b; - } - - return std::fwrite(pcxPalette, sizeof(pcxPalette), 1, out) == 1; -} - -/** - * @brief RLE compress the pixel data - * @param src Raw pixel buffer - * @param dst Output buffer - * @param width Width of pixel buffer - - * @return Output buffer - */ -uint8_t *CaptureEnc(uint8_t *src, uint8_t *dst, int width) -{ - int rleLength; - - do { - uint8_t rlePixel = *src; - src++; - rleLength = 1; - - width--; - - while (rlePixel == *src) { - if (rleLength >= 63) - break; - if (width == 0) - break; - rleLength++; - - width--; - src++; - } - - if (rleLength > 1 || rlePixel > 0xBF) { - *dst = rleLength | 0xC0; - dst++; - } - - *dst = rlePixel; - dst++; - } while (width > 0); - - return dst; -} - -/** - * @brief Write the pixel data to the PCX file - * - * @param buf Pixel data - * @param out File stream for the PCX file. - * @return True if successful, else false - */ -bool CapturePix(const Surface &buf, FILE *out) -{ - int width = buf.w(); - std::unique_ptr pBuffer { new uint8_t[2 * width] }; - uint8_t *pixels = buf.begin(); - for (int height = buf.h(); height > 0; height--) { - const uint8_t *pBufferEnd = CaptureEnc(pixels, pBuffer.get(), width); - pixels += buf.pitch(); - if (std::fwrite(pBuffer.get(), pBufferEnd - pBuffer.get(), 1, out) != 1) - return false; - } - return true; -} - FILE *CaptureFile(std::string *dstPath) { + const char *ext = +#if DEVILUTIONX_SCREENSHOT_FORMAT == DEVILUTIONX_SCREENSHOT_FORMAT_PCX + ".pcx"; +#elif DEVILUTIONX_SCREENSHOT_FORMAT == DEVILUTIONX_SCREENSHOT_FORMAT_PNG + ".png"; +#endif const std::time_t tt = std::time(nullptr); const std::tm *tm = std::localtime(&tt); const std::string filename = tm != nullptr ? fmt::format("Screenshot from {:04}-{:02}-{:02} {:02}-{:02}-{:02}", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec) : "Screenshot"; - *dstPath = StrCat(paths::PrefPath(), filename, ".pcx"); + *dstPath = StrCat(paths::PrefPath(), filename, ext); int i = 0; while (FileExists(dstPath->c_str())) { i++; - *dstPath = StrCat(paths::PrefPath(), filename, "-", i, ".pcx"); + *dstPath = StrCat(paths::PrefPath(), filename, "-", i, ext); } return OpenFile(dstPath->c_str(), "wb"); } @@ -162,42 +70,45 @@ void RedPalette() BltFast(nullptr, nullptr); RenderPresent(); } + } // namespace void CaptureScreen() { SDL_Color palette[256]; std::string fileName; - bool success; + const uint32_t startTime = SDL_GetTicks(); FILE *outStream = CaptureFile(&fileName); - if (outStream == nullptr) + if (outStream == nullptr) { + LogError("Failed to open {} for writing: {}", fileName, std::strerror(errno)); return; + } DrawAndBlit(); PaletteGetEntries(256, palette); RedPalette(); - - const Surface &buf = GlobalBackBuffer(); - success = CaptureHdr(buf.w(), buf.h(), outStream); - if (success) { - success = CapturePix(buf, outStream); - } - if (success) { - success = CapturePal(palette, outStream); + for (int i = 0; i < 256; i++) { + system_palette[i] = palette[i]; } - std::fclose(outStream); + palette_update(); + + const tl::expected result = +#if DEVILUTIONX_SCREENSHOT_FORMAT == DEVILUTIONX_SCREENSHOT_FORMAT_PCX + WriteSurfaceToFilePcx(GlobalBackBuffer(), outStream); +#elif DEVILUTIONX_SCREENSHOT_FORMAT == DEVILUTIONX_SCREENSHOT_FORMAT_PNG + WriteSurfaceToFilePng(GlobalBackBuffer(), outStream); +#endif - if (!success) { - Log("Failed to save screenshot at {}", fileName); + if (!result.has_value()) { + LogError("Failed to save screenshot at {}: ", fileName, result.error()); RemoveFile(fileName.c_str()); } else { Log("Screenshot saved at {}", fileName); } - SDL_Delay(300); - for (int i = 0; i < 256; i++) { - system_palette[i] = palette[i]; + const uint32_t timePassed = SDL_GetTicks() - startTime; + if (timePassed < 300) { + SDL_Delay(300 - timePassed); } - palette_update(); RedrawEverything(); } diff --git a/Source/engine/palette.cpp b/Source/engine/palette.cpp index 850ced74a37..bdcca1e04ef 100644 --- a/Source/engine/palette.cpp +++ b/Source/engine/palette.cpp @@ -283,6 +283,9 @@ void SetFadeLevel(int fadeval, bool updateHardwareCursor) system_palette[i].r = (fadeval * logical_palette[i].r) / 256; system_palette[i].g = (fadeval * logical_palette[i].g) / 256; system_palette[i].b = (fadeval * logical_palette[i].b) / 256; +#if SDL_VERSION_ATLEAST(2, 0, 0) + system_palette[i].a = SDL_ALPHA_OPAQUE; +#endif } palette_update(); if (updateHardwareCursor && IsHardwareCursor()) { diff --git a/Source/utils/surface_to_pcx.cpp b/Source/utils/surface_to_pcx.cpp new file mode 100644 index 00000000000..b1bdc696fa1 --- /dev/null +++ b/Source/utils/surface_to_pcx.cpp @@ -0,0 +1,153 @@ +#include "utils/surface_to_pcx.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include "engine/surface.hpp" +#include "utils/pcx.hpp" + +namespace devilution { +namespace { + +tl::expected CheckedFWrite(const void *ptr, size_t size, FILE *out) +{ + if (std::fwrite(ptr, size, 1, out) != 1) { + const char *errorMessage = std::strerror(errno); + if (errorMessage == nullptr) + errorMessage = ""; + return tl::make_unexpected(std::string("fwrite failed with: ").append(errorMessage)); + } + return {}; +} + +/** + * @brief Write the PCX-file header + * @param width Image width + * @param height Image height + * @param out File stream to write to + * @return True on success + */ +tl::expected WritePcxHeader(int16_t width, int16_t height, FILE *out) +{ + PCXHeader buffer; + + memset(&buffer, 0, sizeof(buffer)); + buffer.Manufacturer = 10; + buffer.Version = 5; + buffer.Encoding = 1; + buffer.BitsPerPixel = 8; + buffer.Xmax = SDL_SwapLE16(width - 1); + buffer.Ymax = SDL_SwapLE16(height - 1); + buffer.HDpi = SDL_SwapLE16(width); + buffer.VDpi = SDL_SwapLE16(height); + buffer.NPlanes = 1; + buffer.BytesPerLine = SDL_SwapLE16(width); + + return CheckedFWrite(&buffer, sizeof(buffer), out); +} + +/** + * @brief Write the current in-game palette to the PCX file + * @param palette Current palette + * @param out File stream for the PCX file. + * @return True if successful, else false + */ +tl::expected WritePcxPalette(SDL_Color *palette, FILE *out) +{ + uint8_t pcxPalette[1 + 256 * 3]; + + pcxPalette[0] = 12; + for (int i = 0; i < 256; i++) { + pcxPalette[1 + 3 * i + 0] = palette[i].r; + pcxPalette[1 + 3 * i + 1] = palette[i].g; + pcxPalette[1 + 3 * i + 2] = palette[i].b; + } + + return CheckedFWrite(pcxPalette, sizeof(pcxPalette), out); +} + +/** + * @brief RLE compress the pixel data + * @param src Raw pixel buffer + * @param dst Output buffer + * @param width Width of pixel buffer + + * @return Output buffer + */ +uint8_t *WritePcxLine(uint8_t *src, uint8_t *dst, int width) +{ + int rleLength; + + do { + const uint8_t rlePixel = *src; + src++; + rleLength = 1; + + width--; + + while (rlePixel == *src) { + if (rleLength >= 63) + break; + if (width == 0) + break; + rleLength++; + + width--; + src++; + } + + if (rleLength > 1 || rlePixel > 0xBF) { + *dst = rleLength | 0xC0; + dst++; + } + + *dst = rlePixel; + dst++; + } while (width > 0); + + return dst; +} + +/** + * @brief Write the pixel data to the PCX file + * + * @param buf Pixel data + * @param out File stream for the PCX file. + * @return True if successful, else false + */ +tl::expected WritePcxPixels(const Surface &buf, FILE *out) +{ + const int width = buf.w(); + const std::unique_ptr pBuffer { new uint8_t[static_cast(2 * width)] }; + uint8_t *pixels = buf.begin(); + for (int height = buf.h(); height > 0; height--) { + const uint8_t *pBufferEnd = WritePcxLine(pixels, pBuffer.get(), width); + pixels += buf.pitch(); + tl::expected result = CheckedFWrite(pBuffer.get(), pBufferEnd - pBuffer.get(), out); + if (!result.has_value()) return result; + } + return {}; +} + +} // namespace + +tl::expected +WriteSurfaceToFilePcx(const Surface &buf, FILE *outStream) +{ + tl::expected result = WritePcxHeader(buf.w(), buf.h(), outStream); + if (!result.has_value()) return result; + result = WritePcxPixels(buf, outStream); + if (!result.has_value()) return result; + result = WritePcxPalette(buf.surface->format->palette->colors, outStream); + if (!result.has_value()) return result; + std::fclose(outStream); + return {}; +} + +} // namespace devilution diff --git a/Source/utils/surface_to_pcx.hpp b/Source/utils/surface_to_pcx.hpp new file mode 100644 index 00000000000..92968a050b7 --- /dev/null +++ b/Source/utils/surface_to_pcx.hpp @@ -0,0 +1,13 @@ +#include +#include + +#include + +#include "engine/surface.hpp" + +namespace devilution { + +tl::expected +WriteSurfaceToFilePcx(const Surface &buf, FILE *outStream); + +} // namespace devilution diff --git a/Source/utils/surface_to_png.cpp b/Source/utils/surface_to_png.cpp new file mode 100644 index 00000000000..7979443ddf5 --- /dev/null +++ b/Source/utils/surface_to_png.cpp @@ -0,0 +1,27 @@ +#include "utils/surface_to_png.hpp" + +#include +#include + +#include +#include + +#include "engine/surface.hpp" + +namespace devilution { + +extern "C" int IMG_SavePNG_RW(SDL_Surface *surface, SDL_RWops *dst, int freedst); + +tl::expected +WriteSurfaceToFilePng(const Surface &buf, FILE *outStream) +{ + SDL_RWops *rwops = SDL_RWFromFP(outStream, /*autoclose=*/SDL_TRUE); + if (rwops == nullptr || IMG_SavePNG_RW(buf.surface, rwops, /*freedst=*/1) != 0) { + tl::expected result = tl::make_unexpected(std::string(SDL_GetError())); + SDL_ClearError(); + return result; + } + return {}; +} + +} // namespace devilution diff --git a/Source/utils/surface_to_png.hpp b/Source/utils/surface_to_png.hpp new file mode 100644 index 00000000000..bb442f5d546 --- /dev/null +++ b/Source/utils/surface_to_png.hpp @@ -0,0 +1,13 @@ +#include +#include + +#include + +#include "engine/surface.hpp" + +namespace devilution { + +tl::expected +WriteSurfaceToFilePng(const Surface &buf, FILE *outStream); + +} // namespace devilution