// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package atspi /* #include "adapter_native.h" */ import "C" import ( "context" "time" ) // CaptureOCRGeometry runs only a narrow target/ancestor query on the GLib // owner. Call it from a worker, not the event loop. A zone remains provider // coordinates and is validated/clipped to the fresh object's bounds. func (adapter *Adapter) CaptureOCRGeometry(ctx context.Context, object ObjectID, zone *Rectangle) (OCRGeometry, error) { if !object.Valid() { return OCRGeometry{}, ErrObjectNotFound } ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond) defer cancel() var ownedZone *Rectangle if zone != nil { copy := *zone ownedZone = © } result, err := adapter.documentOperation(ctx, treeCaptureRequest{ocrGeometry: true, reviewFocus: object, ocrZone: ownedZone}) return result.ocr, err } func (adapter *Adapter) nativeOCRGeometry(ctx context.Context, object ObjectID, zone *Rectangle) (OCRGeometry, error) { var source *C.AtspiAccessible for _, item := range adapter.nativeFocus { if item.id == object { source = item.source break } } if source == nil { source = adapter.nativeReview[object] } if source == nil { source = adapter.nativeDocument[object] } if source == nil { return OCRGeometry{}, ErrObjectNotFound } adapter.refreshForeground() before := adapter.Foreground() if !before.Client.Known || before.Client.Window == 0 || before.Client.PID <= 0 { return OCRGeometry{}, ErrOCRGeometry } // Reject dead source/ancestor content without querying text. OCR reads only // visible pixels, so a password role does not prohibit capture geometry. current := (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(source))) valid := false seen := make(map[ObjectID]bool) for depth := 0; current != nil && depth < 128 && ctx.Err() == nil; depth++ { id := nativeObjectID(current) C.atspi_accessible_clear_cache_single(current) states := C.atspi_accessible_get_state_set(current) safe := states != nil && C.atspi_state_set_contains(states, C.ATSPI_STATE_DEFUNCT) == 0 if states != nil { C.g_object_unref(C.gpointer(states)) } role := C.atspi_accessible_get_role(current, nil) if !safe || !id.Valid() || seen[id] || role == C.ATSPI_ROLE_INVALID { C.g_object_unref(C.gpointer(current)) return OCRGeometry{}, ErrOCRGeometry } seen[id] = true if isWindowRole(role) { valid = true C.g_object_unref(C.gpointer(current)) current = nil break } parent := C.atspi_accessible_get_parent(current, nil) C.g_object_unref(C.gpointer(current)) current = parent } if current != nil { C.g_object_unref(C.gpointer(current)) } if err := ctx.Err(); err != nil { return OCRGeometry{}, err } if !valid { return OCRGeometry{}, ErrOCRGeometry } window := C.cthulhu_window_ancestor(source) if window == nil { return OCRGeometry{}, ErrOCRGeometry } defer C.g_object_unref(C.gpointer(window)) if int(C.atspi_accessible_get_process_id(window, nil)) != before.Client.PID || (adapter.foregroundWindow.Valid() && adapter.foregroundWindow != nativeObjectID(window)) { return OCRGeometry{}, ErrOCRGeometry } states := C.atspi_accessible_get_state_set(window) active := states != nil && C.atspi_state_set_contains(states, C.ATSPI_STATE_ACTIVE) != 0 && C.atspi_state_set_contains(states, C.ATSPI_STATE_ICONIFIED) == 0 if states != nil { C.g_object_unref(C.gpointer(states)) } if !active { return OCRGeometry{}, ErrOCRGeometry } geometry, err := geometryFromNative(ctx, source) if err != nil { return OCRGeometry{}, err } windowGeometry, err := geometryFromNative(ctx, window) if err != nil { return OCRGeometry{}, err } if !geometry.BoundsKnown || !windowGeometry.BoundsKnown { return OCRGeometry{}, ErrOCRGeometry } bounds := geometry.Bounds if zone != nil { clipped, ok := zone.Intersection(bounds) // A stale zone must not move or expand its captured area implicitly. if !ok || clipped != *zone { return OCRGeometry{}, ErrStaleGeometry } bounds = clipped } screen, err := ocr_screen_bounds(bounds, windowGeometry.Bounds, before.Client) adapter.refreshForeground() if err := ctx.Err(); err != nil { return OCRGeometry{}, err } if adapter.Foreground() != before { return OCRGeometry{}, ErrStaleGeometry } if err != nil { return OCRGeometry{}, err } return OCRGeometry{Object: object, ProviderBounds: bounds, WindowBounds: windowGeometry.Bounds, ScreenBounds: screen, Foreground: before}, nil } func isWindowRole(role C.AtspiRole) bool { return role == C.ATSPI_ROLE_FRAME || role == C.ATSPI_ROLE_DIALOG || role == C.ATSPI_ROLE_WINDOW }