33#ifdef YAZE_SCREENSHOT_HAS_PNG
37#include "absl/status/status.h"
38#include "absl/strings/ascii.h"
39#include "absl/strings/str_format.h"
40#include "absl/time/clock.h"
42#include "imgui_internal.h"
56absl::StatusOr<SDL_Renderer*> GetScreenshotRenderer() {
57 if (!ImGui::GetCurrentContext()) {
58 return absl::FailedPreconditionError(
"No ImGui context");
60 const ImGuiIO& io = ImGui::GetIO();
62 constexpr const char* kBackendName =
"imgui_impl_sdlrenderer3";
64 constexpr const char* kBackendName =
"imgui_impl_sdlrenderer2";
66 if (!io.BackendRendererName ||
67 std::strcmp(io.BackendRendererName, kBackendName) != 0) {
68 return absl::UnimplementedError(
69 "Screenshot capture requires the SDL renderer backend");
71 if (!io.BackendRendererUserData) {
72 return absl::FailedPreconditionError(
"SDL renderer not available");
78 SDL_Renderer* renderer =
nullptr;
79 std::memcpy(&renderer, io.BackendRendererUserData,
sizeof(renderer));
81 return absl::FailedPreconditionError(
"SDL renderer not available");
83 if (SDL_GetRenderTarget(renderer) !=
nullptr) {
84 return absl::FailedPreconditionError(
85 "Screenshot capture requires the main framebuffer render target");
90absl::Status GetOutputSize(SDL_Renderer* renderer,
int* width,
int* height) {
92 const bool success = SDL_GetCurrentRenderOutputSize(renderer, width, height);
94 const bool success = SDL_GetRendererOutputSize(renderer, width, height) == 0;
97 return absl::InternalError(
98 absl::StrFormat(
"Failed to get renderer size: %s", SDL_GetError()));
100 if (*width <= 0 || *height <= 0) {
101 return absl::FailedPreconditionError(
"Renderer has no visible framebuffer");
103 return absl::OkStatus();
106absl::StatusOr<SDL_Rect> ClipRegion(
const CaptureRegion& region,
int width,
108 if (region.width <= 0 || region.height <= 0) {
109 return absl::InvalidArgumentError(
"Invalid capture region");
111 const int64_t left = std::max<int64_t>(0, region.x);
112 const int64_t top = std::max<int64_t>(0, region.y);
113 const int64_t right =
114 std::min<int64_t>(width,
static_cast<int64_t
>(region.x) + region.width);
115 const int64_t bottom =
116 std::min<int64_t>(height,
static_cast<int64_t
>(region.y) + region.height);
117 if (right <= left || bottom <= top) {
118 return absl::InvalidArgumentError(
119 "Capture region is outside the framebuffer");
121 return SDL_Rect{
static_cast<int>(left),
static_cast<int>(top),
122 static_cast<int>(right - left),
123 static_cast<int>(bottom - top)};
126absl::StatusOr<SurfacePtr> ReadFramebufferRegion(SDL_Renderer* renderer,
127 const SDL_Rect& region) {
131 SDL_Rect previous_viewport;
133 if (!SDL_GetRenderViewport(renderer, &previous_viewport)) {
134 return absl::InternalError(
"Failed to get renderer viewport");
136 const bool viewport_was_set = SDL_RenderViewportSet(renderer);
137 const bool reset = SDL_SetRenderViewport(renderer,
nullptr);
139 SDL_RenderGetViewport(renderer, &previous_viewport);
140 const bool reset = SDL_RenderSetViewport(renderer,
nullptr) == 0;
147 const bool restored = SDL_SetRenderViewport(
148 renderer, viewport_was_set ? &previous_viewport : nullptr);
150 const bool restored =
151 SDL_RenderSetViewport(renderer, &previous_viewport) == 0;
154 return absl::InternalError(absl::StrFormat(
155 "Failed to restore screenshot viewport: %s", SDL_GetError()));
158 return absl::InternalError(absl::StrFormat(
159 "Failed to reset screenshot viewport: %s", SDL_GetError()));
162 return absl::InternalError(absl::StrFormat(
163 "Failed to read pixels to surface: %s", SDL_GetError()));
168absl::StatusOr<CaptureRegion> WindowRegion(
const ImGuiWindow& window) {
169 if (!window.Active || window.Hidden || window.Collapsed) {
170 return absl::FailedPreconditionError(
"Requested window is not visible");
172 const ImGuiViewport* viewport = ImGui::GetMainViewport();
173 if (window.Viewport != viewport) {
174 return absl::UnimplementedError(
175 "Detached window screenshots are not supported by the main renderer");
177 const ImVec2 scale = ImGui::GetIO().DisplayFramebufferScale;
178 if (!std::isfinite(scale.x) || !std::isfinite(scale.y) || scale.x <= 0 ||
180 return absl::FailedPreconditionError(
"Invalid framebuffer scale");
182 const double left = std::floor(
183 (
static_cast<double>(window.Pos.x) - viewport->Pos.x) * scale.x);
184 const double top = std::floor(
185 (
static_cast<double>(window.Pos.y) - viewport->Pos.y) * scale.y);
186 const double right = std::ceil(
187 (
static_cast<double>(window.Pos.x) + window.Size.x - viewport->Pos.x) *
189 const double bottom = std::ceil(
190 (
static_cast<double>(window.Pos.y) + window.Size.y - viewport->Pos.y) *
192 constexpr double kMinInt = std::numeric_limits<int>::min();
193 constexpr double kMaxInt = std::numeric_limits<int>::max();
194 if (!std::isfinite(left) || !std::isfinite(top) || !std::isfinite(right) ||
195 !std::isfinite(bottom) || left < kMinInt || top < kMinInt ||
196 right > kMaxInt || bottom > kMaxInt || right <= left || bottom <= top ||
197 right - left > kMaxInt || bottom - top > kMaxInt) {
198 return absl::InvalidArgumentError(
"Invalid window framebuffer bounds");
200 return CaptureRegion{
static_cast<int>(left),
static_cast<int>(top),
201 static_cast<int>(right - left),
202 static_cast<int>(bottom - top)};
205absl::StatusOr<std::filesystem::path> ScreenshotPath(
207 std::filesystem::path path;
208 if (!preferred_path.empty()) {
209 path = preferred_path;
210 if (!path.has_extension()) {
211 path += ExtensionForFormat(format);
219 absl::StrFormat(
"yaze_%lld%s",
220 static_cast<long long>(absl::ToUnixMillis(absl::Now())),
221 ExtensionForFormat(format));
223 std::error_code error;
224 auto absolute = std::filesystem::absolute(path, error);
226 return absl::InternalError(absl::StrFormat(
227 "Failed to resolve screenshot path: %s", error.message()));
232absl::Status SaveSurface(SDL_Surface* surface,
233 const std::filesystem::path& path,
237 const bool success = SDL_SaveBMP(surface, path.string().c_str());
239 const bool success = SDL_SaveBMP(surface, path.string().c_str()) == 0;
241 return success ? absl::OkStatus()
242 : absl::InternalError(absl::StrFormat(
243 "Failed to save BMP: %s", SDL_GetError()));
245#ifdef YAZE_SCREENSHOT_HAS_PNG
250 return absl::InternalError(absl::StrFormat(
251 "Failed to convert screenshot pixels: %s", SDL_GetError()));
255 std::vector<png_bytep> rows(rgba->h);
256 for (
int y = 0; y < rgba->h; ++y) {
257 rows[y] =
static_cast<png_bytep
>(rgba->pixels) +
258 static_cast<size_t>(y) * rgba->pitch;
260 FILE* file = std::fopen(path.string().c_str(),
"wb");
262 return absl::InternalError(
"Failed to open PNG output file");
265 png_create_write_struct(PNG_LIBPNG_VER_STRING,
nullptr,
nullptr,
nullptr);
266 png_infop info = png ? png_create_info_struct(png) : nullptr;
269 png_destroy_write_struct(&png,
nullptr);
272 return absl::InternalError(
"Failed to initialize PNG encoder");
274 if (setjmp(png_jmpbuf(png))) {
275 png_destroy_write_struct(&png, &info);
277 return absl::InternalError(
"Failed to encode PNG screenshot");
279 png_init_io(png, file);
280 png_set_IHDR(png, info, rgba->w, rgba->h, 8, PNG_COLOR_TYPE_RGBA,
281 PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
282 PNG_FILTER_TYPE_DEFAULT);
283 png_write_info(png, info);
284 png_write_image(png, rows.data());
285 png_write_end(png,
nullptr);
286 png_destroy_write_struct(&png, &info);
287 if (std::fclose(file) != 0) {
288 return absl::InternalError(
"Failed to finish PNG screenshot file");
290 return absl::OkStatus();
292 return absl::UnimplementedError(
293 "PNG screenshot encoding unavailable (libpng missing)");
297void RevealScreenshot([[maybe_unused]]
const std::filesystem::path& path) {
300 std::error_code error;
301 const auto absolute = std::filesystem::absolute(path, error);
305 std::string url =
"file://";
306 for (
const unsigned char byte : absolute.generic_string()) {
307 if (absl::ascii_isalnum(
byte) ||
byte ==
'/' ||
byte ==
'-' ||
308 byte ==
'_' ||
byte ==
'.' ||
byte ==
'~') {
311 url += absl::StrFormat(
"%%%02X",
byte);
314 (void)SDL_OpenURL(url.c_str());
320absl::StatusOr<ScreenshotFormat> ResolveScreenshotFormat(
324 return absl::InvalidArgumentError(
"Unknown screenshot format");
326 const std::filesystem::path output(path);
328 (output.filename().empty() || output.filename() ==
"." ||
329 output.filename() ==
".." || path.find(
'\0') != std::string::npos)) {
330 return absl::InvalidArgumentError(
"Screenshot path must name a file");
332 const std::string extension =
333 absl::AsciiStrToLower(output.extension().string());
335 if (extension ==
".png") {
337 }
else if (extension ==
".bmp") {
339 }
else if (!extension.empty()) {
340 return absl::InvalidArgumentError(
341 "Screenshot extension must be .png or .bmp");
347 return absl::InvalidArgumentError(
348 "Screenshot extension does not match requested format");
350#ifndef YAZE_SCREENSHOT_HAS_PNG
352 return absl::UnimplementedError(
353 "PNG screenshot encoding unavailable (libpng missing)");
359absl::StatusOr<ScreenshotArtifact> CaptureHarnessScreenshot(
360 const std::string& preferred_path,
bool reveal_to_user,
362 return CaptureHarnessScreenshotRegion(std::nullopt, preferred_path,
363 reveal_to_user, format);
366absl::StatusOr<ScreenshotArtifact> CaptureHarnessScreenshotRegion(
367 const std::optional<CaptureRegion>& region,
368 const std::string& preferred_path,
bool reveal_to_user,
371 ResolveScreenshotFormat(preferred_path, format));
378 ClipRegion(region.value_or(CaptureRegion{0, 0, width, height}), width,
381 ASSIGN_OR_RETURN(
const auto output, ScreenshotPath(preferred_path, resolved));
382 if (output.has_parent_path()) {
383 std::error_code error;
384 std::filesystem::create_directories(output.parent_path(), error);
386 return absl::InternalError(absl::StrFormat(
387 "Failed to create screenshot directory: %s", error.message()));
391 std::error_code error;
392 const auto bytes = std::filesystem::file_size(output, error);
394 return absl::InternalError(absl::StrFormat(
395 "Failed to stat screenshot %s: %s", output.string(), error.message()));
397 ScreenshotArtifact artifact{output.string(), clipped.w, clipped.h,
398 static_cast<int64_t
>(bytes)};
399 if (reveal_to_user) {
400 RevealScreenshot(output);
405absl::StatusOr<ScreenshotArtifact> CaptureActiveWindow(
406 const std::string& preferred_path,
bool reveal_to_user,
408 const ImGuiContext* ctx = ImGui::GetCurrentContext();
409 if (!ctx || !ctx->NavWindow) {
410 return absl::FailedPreconditionError(
"No active ImGui window");
413 return CaptureHarnessScreenshotRegion(region, preferred_path, reveal_to_user,
417absl::StatusOr<ScreenshotArtifact> CaptureWindowByName(
418 const std::string& window_name,
const std::string& preferred_path,
420 if (!ImGui::GetCurrentContext()) {
421 return absl::FailedPreconditionError(
"No ImGui context");
423 const ImGuiWindow* window = ImGui::FindWindowByName(window_name.c_str());
425 return absl::NotFoundError(
426 absl::StrFormat(
"Window '%s' not found", window_name));
429 return CaptureHarnessScreenshotRegion(region, preferred_path, reveal_to_user,
#define ASSIGN_OR_RETURN(type_variable_name, expression)
SDL2/SDL3 compatibility layer.
#define RETURN_IF_ERROR(expr)