/* * DWN - Desktop Window Manager * retoor * Screenshot capture implementation using X11 and libpng */ #include "screenshot.h" #include "dwn.h" #include "client.h" #include "workspace.h" #include "util.h" #include #include #include #include static bool initialized = false; static pthread_mutex_t screenshot_mutex = PTHREAD_MUTEX_INITIALIZER; static ScreenshotRequest *screenshot_queue = NULL; static atomic_int screenshot_shutting_down = 0; static const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; bool screenshot_init(void) { atomic_store(&screenshot_shutting_down, 0); initialized = true; LOG_INFO("Screenshot module initialized"); return true; } void screenshot_cleanup(void) { atomic_store(&screenshot_shutting_down, 1); pthread_mutex_lock(&screenshot_mutex); while (screenshot_queue != NULL) { ScreenshotRequest *req = screenshot_queue; screenshot_queue = req->next; pthread_mutex_unlock(&screenshot_mutex); pthread_join(req->thread, NULL); screenshot_result_free(&req->result); dwn_free(req); pthread_mutex_lock(&screenshot_mutex); } pthread_mutex_unlock(&screenshot_mutex); initialized = false; } static size_t base64_encoded_size(size_t input_size) { return 4 * ((input_size + 2) / 3); } char *screenshot_to_base64(const ScreenshotResult *result, size_t *out_len) { if (result == NULL || result->data == NULL || result->size == 0) { return NULL; } size_t encoded_size = base64_encoded_size(result->size); char *encoded = dwn_malloc(encoded_size + 1); if (encoded == NULL) { return NULL; } const unsigned char *input = result->data; size_t input_len = result->size; char *output = encoded; for (size_t i = 0; i < input_len; i += 3) { unsigned int n = ((unsigned int)input[i]) << 16; if (i + 1 < input_len) { n |= ((unsigned int)input[i + 1]) << 8; } if (i + 2 < input_len) { n |= (unsigned int)input[i + 2]; } *output++ = base64_chars[(n >> 18) & 0x3F]; *output++ = base64_chars[(n >> 12) & 0x3F]; *output++ = (i + 1 < input_len) ? base64_chars[(n >> 6) & 0x3F] : '='; *output++ = (i + 2 < input_len) ? base64_chars[n & 0x3F] : '='; } *output = '\0'; if (out_len != NULL) { *out_len = output - encoded; } return encoded; } void screenshot_result_free(ScreenshotResult *result) { if (result == NULL) { return; } if (result->data != NULL) { dwn_free(result->data); result->data = NULL; } result->size = 0; result->width = 0; result->height = 0; } const char *screenshot_status_string(ScreenshotStatus status) { switch (status) { case SCREENSHOT_OK: return "OK"; case SCREENSHOT_ERROR_INVALID_ARG: return "Invalid argument"; case SCREENSHOT_ERROR_X11: return "X11 error"; case SCREENSHOT_ERROR_PNG: return "PNG encoding error"; case SCREENSHOT_ERROR_NO_MEMORY: return "Out of memory"; case SCREENSHOT_ERROR_WINDOW_NOT_FOUND: return "Window not found"; default: return "Unknown error"; } } typedef struct { unsigned char *data; size_t size; size_t capacity; } PngBuffer; static void png_write_callback(png_structp png, png_bytep data, png_size_t length) { PngBuffer *buf = (PngBuffer *)png_get_io_ptr(png); if (buf == NULL) { return; } size_t new_size = buf->size + length; if (new_size > buf->capacity) { size_t new_capacity = buf->capacity * 2; if (new_capacity < new_size) { new_capacity = new_size + 1024; } unsigned char *new_data = dwn_realloc(buf->data, new_capacity); if (new_data == NULL) { png_error(png, "Memory allocation failed"); return; } buf->data = new_data; buf->capacity = new_capacity; } memcpy(buf->data + buf->size, data, length); buf->size = new_size; } static void png_flush_callback(png_structp png) { (void)png; } static ScreenshotStatus ximage_to_png(XImage *img, ScreenshotResult *result) { if (img == NULL || result == NULL) { return SCREENSHOT_ERROR_INVALID_ARG; } png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (png == NULL) { return SCREENSHOT_ERROR_PNG; } png_infop info = png_create_info_struct(png); if (info == NULL) { png_destroy_write_struct(&png, NULL); return SCREENSHOT_ERROR_PNG; } if (setjmp(png_jmpbuf(png))) { png_destroy_write_struct(&png, &info); return SCREENSHOT_ERROR_PNG; } PngBuffer buf = {0}; buf.capacity = (size_t)(img->width * img->height * 3 / 2 + 1024); buf.data = dwn_malloc(buf.capacity); if (buf.data == NULL) { png_destroy_write_struct(&png, &info); return SCREENSHOT_ERROR_NO_MEMORY; } png_set_write_fn(png, &buf, png_write_callback, png_flush_callback); png_set_IHDR(png, info, img->width, img->height, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); png_write_info(png, info); unsigned char *row = dwn_malloc(img->width * 3); if (row == NULL) { dwn_free(buf.data); png_destroy_write_struct(&png, &info); return SCREENSHOT_ERROR_NO_MEMORY; } unsigned long red_mask = img->red_mask; unsigned long green_mask = img->green_mask; unsigned long blue_mask = img->blue_mask; int red_shift = 0; int green_shift = 0; int blue_shift = 0; while ((red_mask & 1) == 0 && red_shift < 32) { red_shift++; red_mask >>= 1; } while ((green_mask & 1) == 0 && green_shift < 32) { green_shift++; green_mask >>= 1; } while ((blue_mask & 1) == 0 && blue_shift < 32) { blue_shift++; blue_mask >>= 1; } for (int y = 0; y < img->height; y++) { for (int x = 0; x < img->width; x++) { unsigned long pixel = XGetPixel(img, x, y); row[x * 3 + 0] = (pixel >> red_shift) & 0xFF; row[x * 3 + 1] = (pixel >> green_shift) & 0xFF; row[x * 3 + 2] = (pixel >> blue_shift) & 0xFF; } png_write_row(png, row); } png_write_end(png, info); dwn_free(row); png_destroy_write_struct(&png, &info); result->data = buf.data; result->size = buf.size; result->width = img->width; result->height = img->height; return SCREENSHOT_OK; } ScreenshotStatus screenshot_capture_fullscreen(ScreenshotResult *result) { if (result == NULL) { return SCREENSHOT_ERROR_INVALID_ARG; } if (dwn == NULL || dwn->display == NULL) { return SCREENSHOT_ERROR_X11; } memset(result, 0, sizeof(ScreenshotResult)); XImage *img = XGetImage(dwn->display, dwn->root, 0, 0, dwn->screen_width, dwn->screen_height, AllPlanes, ZPixmap); if (img == NULL) { LOG_ERROR("XGetImage failed for fullscreen capture"); return SCREENSHOT_ERROR_X11; } ScreenshotStatus status = ximage_to_png(img, result); XDestroyImage(img); return status; } ScreenshotStatus screenshot_capture_window(Window window, ScreenshotResult *result) { if (result == NULL) { return SCREENSHOT_ERROR_INVALID_ARG; } if (dwn == NULL || dwn->display == NULL) { return SCREENSHOT_ERROR_X11; } if (window == None) { return SCREENSHOT_ERROR_WINDOW_NOT_FOUND; } memset(result, 0, sizeof(ScreenshotResult)); XWindowAttributes wa; if (!XGetWindowAttributes(dwn->display, window, &wa)) { return SCREENSHOT_ERROR_WINDOW_NOT_FOUND; } int x, y; Window child; XTranslateCoordinates(dwn->display, window, dwn->root, 0, 0, &x, &y, &child); int width = wa.width; int height = wa.height; if (x < 0) { width += x; x = 0; } if (y < 0) { height += y; y = 0; } if (x + width > dwn->screen_width) { width = dwn->screen_width - x; } if (y + height > dwn->screen_height) { height = dwn->screen_height - y; } if (width <= 0 || height <= 0) { return SCREENSHOT_ERROR_X11; } XImage *img = XGetImage(dwn->display, dwn->root, x, y, width, height, AllPlanes, ZPixmap); if (img == NULL) { LOG_ERROR("XGetImage failed for window capture"); return SCREENSHOT_ERROR_X11; } ScreenshotStatus status = ximage_to_png(img, result); XDestroyImage(img); return status; } ScreenshotStatus screenshot_capture_active(ScreenshotResult *result) { if (result == NULL) { return SCREENSHOT_ERROR_INVALID_ARG; } if (dwn == NULL) { return SCREENSHOT_ERROR_X11; } Workspace *ws = workspace_get_current(); if (ws == NULL || ws->focused == NULL) { return SCREENSHOT_ERROR_WINDOW_NOT_FOUND; } Client *c = ws->focused; Window target = (c->frame != None) ? c->frame : c->window; return screenshot_capture_window(target, result); } ScreenshotStatus screenshot_capture_area(int x, int y, int width, int height, ScreenshotResult *result) { if (result == NULL) { return SCREENSHOT_ERROR_INVALID_ARG; } if (width <= 0 || height <= 0) { return SCREENSHOT_ERROR_INVALID_ARG; } if (dwn == NULL || dwn->display == NULL) { return SCREENSHOT_ERROR_X11; } memset(result, 0, sizeof(ScreenshotResult)); if (x < 0) { width += x; x = 0; } if (y < 0) { height += y; y = 0; } if (x + width > dwn->screen_width) { width = dwn->screen_width - x; } if (y + height > dwn->screen_height) { height = dwn->screen_height - y; } if (width <= 0 || height <= 0) { return SCREENSHOT_ERROR_INVALID_ARG; } XImage *img = XGetImage(dwn->display, dwn->root, x, y, width, height, AllPlanes, ZPixmap); if (img == NULL) { LOG_ERROR("XGetImage failed for area capture"); return SCREENSHOT_ERROR_X11; } ScreenshotStatus status = ximage_to_png(img, result); XDestroyImage(img); return status; } static void *screenshot_worker(void *arg) { ScreenshotRequest *req = (ScreenshotRequest *)arg; if (atomic_load(&screenshot_shutting_down)) { req->state = SCREENSHOT_STATE_ERROR; req->status = SCREENSHOT_ERROR_X11; return NULL; } switch (req->mode) { case SCREENSHOT_MODE_FULLSCREEN: req->status = screenshot_capture_fullscreen(&req->result); break; case SCREENSHOT_MODE_WINDOW: req->status = screenshot_capture_window(req->window, &req->result); break; case SCREENSHOT_MODE_ACTIVE: req->status = screenshot_capture_active(&req->result); break; case SCREENSHOT_MODE_AREA: req->status = screenshot_capture_area(req->x, req->y, req->width, req->height, &req->result); break; } req->state = (req->status == SCREENSHOT_OK) ? SCREENSHOT_STATE_COMPLETED : SCREENSHOT_STATE_ERROR; return NULL; } ScreenshotRequest *screenshot_capture_async(ScreenshotMode mode, void (*callback)(ScreenshotRequest *)) { if (!initialized || atomic_load(&screenshot_shutting_down)) { return NULL; } ScreenshotRequest *req = dwn_calloc(1, sizeof(ScreenshotRequest)); if (req == NULL) { return NULL; } req->mode = mode; req->state = SCREENSHOT_STATE_PENDING; req->callback = callback; pthread_mutex_lock(&screenshot_mutex); req->next = screenshot_queue; screenshot_queue = req; pthread_mutex_unlock(&screenshot_mutex); if (pthread_create(&req->thread, NULL, screenshot_worker, req) != 0) { pthread_mutex_lock(&screenshot_mutex); ScreenshotRequest **pp = &screenshot_queue; while (*pp != NULL) { if (*pp == req) { *pp = req->next; break; } pp = &(*pp)->next; } pthread_mutex_unlock(&screenshot_mutex); dwn_free(req); return NULL; } LOG_DEBUG("Screenshot async request started (mode=%d)", mode); return req; } ScreenshotRequest *screenshot_capture_window_async(Window window, void (*callback)(ScreenshotRequest *)) { if (!initialized || atomic_load(&screenshot_shutting_down)) { return NULL; } ScreenshotRequest *req = dwn_calloc(1, sizeof(ScreenshotRequest)); if (req == NULL) { return NULL; } req->mode = SCREENSHOT_MODE_WINDOW; req->window = window; req->state = SCREENSHOT_STATE_PENDING; req->callback = callback; pthread_mutex_lock(&screenshot_mutex); req->next = screenshot_queue; screenshot_queue = req; pthread_mutex_unlock(&screenshot_mutex); if (pthread_create(&req->thread, NULL, screenshot_worker, req) != 0) { pthread_mutex_lock(&screenshot_mutex); ScreenshotRequest **pp = &screenshot_queue; while (*pp != NULL) { if (*pp == req) { *pp = req->next; break; } pp = &(*pp)->next; } pthread_mutex_unlock(&screenshot_mutex); dwn_free(req); return NULL; } LOG_DEBUG("Screenshot async window request started"); return req; } ScreenshotRequest *screenshot_capture_area_async(int x, int y, int w, int h, void (*callback)(ScreenshotRequest *)) { if (!initialized || atomic_load(&screenshot_shutting_down)) { return NULL; } ScreenshotRequest *req = dwn_calloc(1, sizeof(ScreenshotRequest)); if (req == NULL) { return NULL; } req->mode = SCREENSHOT_MODE_AREA; req->x = x; req->y = y; req->width = w; req->height = h; req->state = SCREENSHOT_STATE_PENDING; req->callback = callback; pthread_mutex_lock(&screenshot_mutex); req->next = screenshot_queue; screenshot_queue = req; pthread_mutex_unlock(&screenshot_mutex); if (pthread_create(&req->thread, NULL, screenshot_worker, req) != 0) { pthread_mutex_lock(&screenshot_mutex); ScreenshotRequest **pp = &screenshot_queue; while (*pp != NULL) { if (*pp == req) { *pp = req->next; break; } pp = &(*pp)->next; } pthread_mutex_unlock(&screenshot_mutex); dwn_free(req); return NULL; } LOG_DEBUG("Screenshot async area request started"); return req; } void screenshot_process_pending(void) { if (atomic_load(&screenshot_shutting_down)) { return; } pthread_mutex_lock(&screenshot_mutex); ScreenshotRequest **pp = &screenshot_queue; while (*pp != NULL) { ScreenshotRequest *req = *pp; if (req->state == SCREENSHOT_STATE_COMPLETED || req->state == SCREENSHOT_STATE_ERROR) { pthread_join(req->thread, NULL); *pp = req->next; pthread_mutex_unlock(&screenshot_mutex); if (req->callback != NULL) { req->callback(req); } pthread_mutex_lock(&screenshot_mutex); } else { pp = &(*pp)->next; } } pthread_mutex_unlock(&screenshot_mutex); } void screenshot_cancel(ScreenshotRequest *req) { if (req == NULL) { return; } pthread_mutex_lock(&screenshot_mutex); ScreenshotRequest **pp = &screenshot_queue; while (*pp != NULL) { if (*pp == req) { *pp = req->next; break; } pp = &(*pp)->next; } pthread_mutex_unlock(&screenshot_mutex); pthread_cancel(req->thread); pthread_join(req->thread, NULL); screenshot_result_free(&req->result); dwn_free(req); }