// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later // Package ocrcapture is used only by the short-lived, headless native helper. // The reader's worker uses ocr.Capturer and never calls GTK/X11 capture in-process. package ocrcapture /* #cgo pkg-config: gtk+-3.0 x11 #include "native.h" */ import "C" import ( "bytes" "context" "image" "image/png" "io" "os" "runtime" "unsafe" "git.stormux.org/storm/cthulhu/internal/ocr" ) func Run(ctx context.Context, input io.Reader, output io.Writer) error { request, err := ocr.ReadCaptureRequest(input) if err != nil { return err } if err := ctx.Err(); err != nil { return err } header := ocr.CaptureHeader{Request: request, Status: "unavailable"} runtime.LockOSThread() defer runtime.UnlockOSThread() // This backend captures physical X11 pixels, including Xwayland when it // provides them. Native display/source checks decide availability. _ = os.Setenv("GDK_BACKEND", "x11") _ = os.Setenv("NO_AT_BRIDGE", "1") kind := map[ocr.SourceKind]int{ocr.ObjectSource: 1, ocr.WindowSource: 2, ocr.DesktopSource: 3, ocr.ClipboardSource: 4}[request.Kind] native := C.cthulhu_ocr_capture(C.int(kind), C.ulong(request.Foreground), C.ulong(request.Window), C.int(request.Bounds.Min.X), C.int(request.Bounds.Min.Y), C.int(request.Bounds.Max.X), C.int(request.Bounds.Max.Y)) defer C.cthulhu_ocr_capture_free(&native) if err := ctx.Err(); err != nil { return err } if native.status != 0 { header.Status = map[int]string{1: "unavailable", 2: "source", 3: "changed", 4: "limit"}[int(native.status)] return ocr.WriteCaptureResponse(output, header, nil) } header.Status = "" var pixels image.Image if request.Kind == ocr.ClipboardSource { data := C.GoBytes(unsafe.Pointer(native.clipboard), native.clipboardLength) config, decodeErr := png.DecodeConfig(bytes.NewReader(data)) if decodeErr != nil { header.Status = "source" } else if config.Width <= 0 || config.Height <= 0 || config.Height > ocr.MaxPixels || config.Width > ocr.MaxPixels/config.Height { header.Status = "limit" } else { pixels, decodeErr = png.Decode(bytes.NewReader(data)) if decodeErr != nil { header.Status = "source" } } } else { header.SourceWindow = uint64(native.sourceWindow) header.RootBounds = image.Rect(0, 0, int(native.rootWidth), int(native.rootHeight)) header.SourceBounds = image.Rect(int(native.sourceX), int(native.sourceY), int(native.sourceX)+int(native.sourceWidth), int(native.sourceY)+int(native.sourceHeight)) header.ScreenBounds = image.Rect(int(native.x), int(native.y), int(native.x)+int(native.width), int(native.y)+int(native.height)) width, height := int(native.width), int(native.height) stride := int(C.gdk_pixbuf_get_rowstride(native.pixels)) data := C.GoBytes(unsafe.Pointer(C.gdk_pixbuf_get_pixels(native.pixels)), C.int((height-1)*stride+width*3)) owned := image.NewRGBA(image.Rect(0, 0, width, height)) for row := range height { for column := range width { source := row*stride + column*3 target := row*owned.Stride + column*4 copy(owned.Pix[target:target+3], data[source:source+3]) owned.Pix[target+3] = 255 } } pixels = owned } return ocr.WriteCaptureResponse(output, header, pixels) }