// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later #include "native.h" #include #include #include #include // Status codes: unavailable, source unavailable, source changed, limit. static Window active_window(Display *display, Window root) { Atom type; int format; unsigned long count, remaining; unsigned char *data = NULL; Window window = None; if (XGetWindowProperty(display, root, XInternAtom(display, "_NET_ACTIVE_WINDOW", False), 0, 1, False, XA_WINDOW, &type, &format, &count, &remaining, &data) == Success && data && type == XA_WINDOW && format == 32 && count == 1) window = *(Window *)data; if (data) XFree(data); return window; } static int is_descendant(Display *display, Window window, Window ancestor) { for (int depth = 0; window && depth < 64; depth++) { if (window == ancestor) return 1; Window root, parent, *children = NULL; unsigned int count; int ok = XQueryTree(display, window, &root, &parent, &children, &count); if (children) XFree(children); if (!ok || parent == window) break; window = parent; } return 0; } static int window_bounds(Display *display, Window root, Window window, int *x, int *y, int *width, int *height) { XWindowAttributes attributes; Window child; if (!XGetWindowAttributes(display, window, &attributes) || attributes.map_state != IsViewable || attributes.class != InputOutput || !XTranslateCoordinates(display, window, root, 0, 0, x, y, &child)) return 0; *width = attributes.width; *height = attributes.height; return *width > 0 && *height > 0; } static guchar component(unsigned long pixel, unsigned long mask) { if (!mask) return 0; unsigned long value = pixel & mask; while (!(mask & 1)) { mask >>= 1; value >>= 1; } return (guchar)(((uint64_t)value * 255 + mask / 2) / mask); } OCRCapture cthulhu_ocr_capture(int kind, unsigned long foreground, unsigned long window, int x, int y, int right, int bottom) { OCRCapture result = {.status = 1}; if (!gtk_init_check(NULL, NULL)) return result; GdkDisplay *gdkDisplay = gdk_display_get_default(); if (!GDK_IS_X11_DISPLAY(gdkDisplay)) return result; Display *display = gdk_x11_display_get_xdisplay(gdkDisplay); Window root = DefaultRootWindow(display); gdk_x11_display_error_trap_push(gdkDisplay); result.status = 2; if (active_window(display, root) != foreground) { result.status = 3; goto done; } Window source = window ? window : foreground; int sourceX, sourceY, sourceWidth, sourceHeight; if (!is_descendant(display, source, foreground) || !window_bounds(display, root, source, &sourceX, &sourceY, &sourceWidth, &sourceHeight)) goto done; if (kind == 4) { // Accept PNG image data only. Never fetch text, URIs or private formats. GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); GtkSelectionData *selection = gtk_clipboard_wait_for_contents(clipboard, gdk_atom_intern_static_string("image/png")); if (!selection) goto done; int length = gtk_selection_data_get_length(selection); if (length > 64 * 1024 * 1024) result.status = 4; else if (length > 0 && gtk_selection_data_get_format(selection) == 8) { result.clipboard = g_memdup2(gtk_selection_data_get_data(selection), length); result.clipboardLength = length; result.status = 0; } gtk_selection_data_free(selection); } else { XWindowAttributes rootAttributes; if (!XGetWindowAttributes(display, root, &rootAttributes)) goto done; // TrueColor pixels are physical root pixels even with GDK_SCALE=2. // Indexed/direct-color visuals would require a different color contract. if (rootAttributes.visual->class != TrueColor) { result.status = 1; goto done; } result.rootWidth = rootAttributes.width; result.rootHeight = rootAttributes.height; result.sourceWindow = kind == 3 ? 0 : source; result.sourceX = kind == 3 ? 0 : sourceX; result.sourceY = kind == 3 ? 0 : sourceY; result.sourceWidth = kind == 3 ? result.rootWidth : sourceWidth; result.sourceHeight = kind == 3 ? result.rootHeight : sourceHeight; int64_t left = result.sourceX, top = result.sourceY; int64_t endX = left + result.sourceWidth, endY = top + result.sourceHeight; if (kind == 1) { left = MAX(left, x); top = MAX(top, y); endX = MIN(endX, right); endY = MIN(endY, bottom); } left = MAX(left, 0); top = MAX(top, 0); endX = MIN(endX, result.rootWidth); endY = MIN(endY, result.rootHeight); if (endX <= left || endY <= top) goto done; int64_t width = endX - left, height = endY - top; if (height > 16000000 || width > 16000000 / height) { result.status = 4; goto done; } result.x = left; result.y = top; result.width = width; result.height = height; XImage *pixels = XGetImage(display, root, result.x, result.y, result.width, result.height, AllPlanes, ZPixmap); if (!pixels) { result.status = 1; goto done; } result.pixels = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, result.width, result.height); if (!result.pixels) { XDestroyImage(pixels); result.status = 1; goto done; } guchar *rgb = gdk_pixbuf_get_pixels(result.pixels); int stride = gdk_pixbuf_get_rowstride(result.pixels); for (int row = 0; row < result.height; row++) { for (int column = 0; column < result.width; column++) { unsigned long pixel = XGetPixel(pixels, column, row); guchar *target = rgb + row * stride + column * 3; target[0] = component(pixel, rootAttributes.visual->red_mask); target[1] = component(pixel, rootAttributes.visual->green_mask); target[2] = component(pixel, rootAttributes.visual->blue_mask); } } XDestroyImage(pixels); result.status = 0; } // Reject movement, destruction or a foreground switch during pixel/clipboard // transfer. Later activation must still recapture/revalidate independently. if (result.status == 0) { int nextX, nextY, nextWidth, nextHeight; if (active_window(display, root) != foreground || !window_bounds(display, root, source, &nextX, &nextY, &nextWidth, &nextHeight) || nextX != sourceX || nextY != sourceY || nextWidth != sourceWidth || nextHeight != sourceHeight) result.status = 3; } done: XSync(display, False); if (gdk_x11_display_error_trap_pop(gdkDisplay)) result.status = 2; if (result.status) cthulhu_ocr_capture_free(&result); return result; } void cthulhu_ocr_capture_free(OCRCapture *capture) { if (capture->pixels) g_object_unref(capture->pixels); g_free(capture->clipboard); capture->pixels = NULL; capture->clipboard = NULL; }