|
/*
|
|
* DWN - Desktop Window Manager
|
|
* retoor <retoor@molodetz.nl>
|
|
* Screenshot capture implementation using X11 and libpng
|
|
*/
|
|
|
|
#include "screenshot.h"
|
|
#include "dwn.h"
|
|
#include "client.h"
|
|
#include "workspace.h"
|
|
#include "util.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdatomic.h>
|
|
#include <png.h>
|
|
|
|
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);
|
|
}
|